From 50005b108f0329a7b121ff96d94a9e36e721733a Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 13 Oct 2026 14:39:08 +0200 Subject: [PATCH] Generate continuous stereo Opus in live playback regression --- .../com/utun/chat/ui/screens/StreamScreen.kt | 2 - tools/chatgui/tests/test_stream_view.cpp | 44 ++++++++++++------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/StreamScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/StreamScreen.kt index 6cf93921..32c701d2 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/StreamScreen.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/StreamScreen.kt @@ -156,7 +156,6 @@ fun StreamScreen(channelId: String, channelName: String, sources: List& audio = {}, unsigned videoFrames = 1) { stream_media_block media{}; media.format = {160, 90, rotation, STREAM_MEDIA_H264, STREAM_MEDIA_OPUS_STEREO, revision}; mediaPts += 100000; media.first_frame_offset = 0; media.video_count = videoFrames; @@ -95,9 +95,9 @@ class StreamViewTest : public QObject { static_cast(video.size()), 0, static_cast(video.size()), STREAM_MEDIA_KEY, reinterpret_cast(video.constData())}; if (!audio.isEmpty()) { - media.audio_count = videoFrames > 1 ? 5 : 1; + media.audio_count = audio.size(); for (unsigned i = 0; i < media.audio_count; i++) media.audio[i] = {mediaPts + i * 20000, - static_cast(audio.size()), 960, reinterpret_cast(audio.constData())}; + static_cast(audio[i].size()), 960, reinterpret_cast(audio[i].constData())}; } uint8_t wire[7080]; size_t length = 0; QCOMPARE(stream_media_encode(&media, wire, sizeof(wire), &length), STREAM_OK); @@ -146,7 +146,7 @@ private slots: void mediaAndPressure() { QVERIFY(gui_stream_open(102, group, inst->node_id, epoch)); StreamRecord event; QVERIFY(receive(102, event)); QCOMPARE(event.kind, StreamRecordKind::Opened); - const QByteArray frame(1500, 'k'); const QByteArray audio("opus"); publish(frame, audio); + const QByteArray frame(1500, 'k'); const QByteArray audio("opus"); publish(frame, {audio}); QVERIFY(receive(102, event)); QCOMPARE(event.kind, StreamRecordKind::Video); QCOMPARE(event.data, frame); QCOMPARE(event.format.rotation, uint16_t(90)); QVERIFY(receive(102, event)); QCOMPARE(event.kind, StreamRecordKind::Audio); QCOMPARE(event.data, audio); @@ -176,24 +176,33 @@ private slots: AVPacket* packet = av_packet_alloc(); QVERIFY(packet); QCOMPARE(avcodec_receive_packet(context, packet), 0); QByteArray video(reinterpret_cast(packet->data), packet->size); av_packet_free(&packet); av_frame_free(&frame); avcodec_free_context(&context); - QByteArray audio; + std::unique_ptr opus(nullptr, opus_codec_encoder_destroy); + uint64_t sampleFrame = 0; #ifdef TEST_NULL_AUDIO QVERIFY(SoundManager::instance()->init()); - auto* opus = opus_codec_encoder_create(48000, 2); QVERIFY(opus); - QCOMPARE(opus_codec_encoder_application_set(opus, OPUS_CODEC_APP_AUDIO), 0); - QCOMPARE(opus_codec_encoder_bitrate_set(opus, 128000), 0); - int16_t pcm[1920]; - for (unsigned i = 0; i < 960; i++) { - pcm[2 * i] = static_cast(6000 * std::sin(i * 0.05)); - pcm[2 * i + 1] = static_cast(1500 * std::sin(i * 0.13)); - } - uint8_t encoded[1275]; int length = opus_codec_encode(opus, pcm, 960, encoded, sizeof(encoded)); QVERIFY(length > 0); - audio = QByteArray(reinterpret_cast(encoded), length); opus_codec_encoder_destroy(opus); + opus.reset(opus_codec_encoder_create(48000, 2)); QVERIFY(opus); + QCOMPARE(opus_codec_encoder_application_set(opus.get(), OPUS_CODEC_APP_AUDIO), 0); + QCOMPARE(opus_codec_encoder_bitrate_set(opus.get(), 128000), 0); nonzeroSamples = 0; leftEnergy = rightEnergy = 0; playbackDelayUs = 200000; #endif StreamPlayerWindow window(group, inst->node_id, epoch, "Source"); QSignalSpy shown(&window, &StreamPlayerWindow::framePresented); window.show(); - QTimer sender; connect(&sender, &QTimer::timeout, this, [this, video, audio] { publish(video, audio, 3); }); sender.start(100); + QTimer sender; + connect(&sender, &QTimer::timeout, this, [this, video, &opus, &sampleFrame] { + QList packets; + if (opus) for (unsigned frame = 0; frame < 5; frame++) { + int16_t pcm[1920]; uint8_t encoded[1275]; + for (unsigned i = 0; i < 960; i++) { + pcm[2 * i] = static_cast(6000 * std::sin((sampleFrame + i) * 0.05)); + pcm[2 * i + 1] = static_cast(1500 * std::sin((sampleFrame + i) * 0.13)); + } + sampleFrame += 960; + int length = opus_codec_encode(opus.get(), pcm, 960, encoded, sizeof(encoded)); QVERIFY(length > 0); + packets.append(QByteArray(reinterpret_cast(encoded), length)); + } + publish(video, packets, 3); + }); + sender.start(100); QTRY_VERIFY_WITH_TIMEOUT(shown.count() > 0, 5000); int beforeSteady = shown.count(); QTest::qWait(1500); QVERIFY2(shown.count() >= beforeSteady + 30, "30fps video must keep advancing with 200ms physical audio latency"); @@ -204,6 +213,9 @@ private slots: #ifdef TEST_NULL_AUDIO QTRY_VERIFY_WITH_TIMEOUT(nonzeroSamples.load() > 0, 5000); QVERIFY(rightEnergy.load() > 0); + qInfo("Stereo playback energies: left=%llu right=%llu ratio=%.3f", + (unsigned long long)leftEnergy.load(), (unsigned long long)rightEnergy.load(), + static_cast(leftEnergy.load()) / rightEnergy.load()); QVERIFY2(leftEnergy.load() > rightEnergy.load() * 8 && leftEnergy.load() < rightEnergy.load() * 25, "Independent stereo channels must reach the playback device without downmix"); auto* volume = window.findChild(); QVERIFY(volume); volume->setValue(0);