diff --git a/tests/test_android_stream.c b/tests/test_android_stream.c index 47dcb8c4..0f97853b 100644 --- a/tests/test_android_stream.c +++ b/tests/test_android_stream.c @@ -18,7 +18,7 @@ #define CHECK(x) do { if (!(x)) { DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "Android stream test line=%d: %s", __LINE__, #x); abort(); } } while (0) static uint8_t video[9000]; -static struct stream_media_format current_format = {1280, 720, 90, STREAM_MEDIA_H264, STREAM_MEDIA_OPUS, 1}; +static struct stream_media_format current_format = {1280, 720, 90, STREAM_MEDIA_H264, STREAM_MEDIA_OPUS_STEREO, 1}; static int feed(uint64_t id, int is_video, uint64_t pts, int key, const uint8_t *data, size_t length) { return android_stream_feed(id, is_video, pts, key, data, length, is_video ? ¤t_format : NULL); } @@ -101,7 +101,7 @@ int main(void) { /* Commit новой revision удаляет старое видео, сохраняет аудио/epoch и не требует нового owner. */ CHECK(feed(1, 1, 200000, 0, video, sizeof(video)) == 1); CHECK(feed(1, 0, 120000, 0, (const uint8_t *)"ops", 3) == 1); - struct stream_media_format next_format = {1920, 1080, 270, STREAM_MEDIA_H264, STREAM_MEDIA_OPUS, 2}; + struct stream_media_format next_format = {1920, 1080, 270, STREAM_MEDIA_H264, STREAM_MEDIA_OPUS_STEREO, 2}; CHECK(android_stream_feed(1, 1, 300000, 0, video, sizeof(video), &next_format) == 0); CHECK(android_stream_feed(1, 1, 300000, 1, video, sizeof(video), &next_format) == 1); CHECK(feed(1, 1, 350000, 1, video, sizeof(video)) == 0); diff --git a/tools/chatgui/src/streamplayer_window.cpp b/tools/chatgui/src/streamplayer_window.cpp index 649e36fc..ad1fad36 100644 --- a/tools/chatgui/src/streamplayer_window.cpp +++ b/tools/chatgui/src/streamplayer_window.cpp @@ -39,7 +39,7 @@ QString text(const char* value) { return QCoreApplication::translate("ChatGui", struct StreamPlayerWindow::Playback { struct Video { QImage image; int64_t pts; }; - struct Audio { std::array pcm; int64_t pts; unsigned offset = 0; }; + struct Audio { std::array pcm; int64_t pts; unsigned offset = 0; }; uint64_t id = ++nextId, group = 0, source = 0, epoch = 0; std::atomic running{true}, audioAvailable{false}; std::atomic volume{1.0f}; @@ -96,11 +96,11 @@ void StreamPlayerWindow::openAudio() { ma_context* context = SoundManager::instance()->context(); if (!context) { DEBUG_WARN(DEBUG_CATEGORY_STREAM, "desktop stream audio unavailable: no context"); return; } auto config = ma_device_config_init(ma_device_type_playback); - config.sampleRate = 48000; config.playback.channels = 1; config.playback.format = ma_format_s16; + config.sampleRate = 48000; config.playback.channels = 2; config.playback.format = ma_format_s16; ma_device_id device; if (SoundManager::deviceId(SoundManager::instance()->playbackDevice(), &device)) config.playback.pDeviceID = &device; bool initialized = m_audio.init(context, config, "stream", m_playback->id, [this](const void*, void* output, unsigned frames) { - auto* pcm = static_cast(output); std::fill(pcm, pcm + frames, 0); + auto* pcm = static_cast(output); std::fill(pcm, pcm + frames * 2, 0); auto& p = *m_playback; std::lock_guard guard(p.lock); int64_t now = nowUs(); if (!p.prefillUntil || now < p.prefillUntil) return; @@ -109,7 +109,7 @@ void StreamPlayerWindow::openAudio() { auto& packet = p.audios.front(); if (firstPts < 0) firstPts = packet.pts + packet.offset * 1000000LL / 48000 - written * 1000000LL / 48000; unsigned count = std::min(frames - written, 960 - packet.offset); - for (unsigned i = 0; i < count; i++) pcm[written + i] = static_cast(packet.pcm[packet.offset + i] * volume); + for (unsigned i = 0; i < count * 2; i++) pcm[written * 2 + i] = static_cast(packet.pcm[packet.offset * 2 + i] * volume); written += count; packet.offset += count; if (packet.offset == 960) p.audios.pop_front(); } @@ -124,7 +124,7 @@ void StreamPlayerWindow::openAudio() { m_audio.close(); m_playback->audioAvailable = false; return; } m_playback->audioAvailable = true; - DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop stream audio output ready id=%llu", (unsigned long long)m_playback->id); + DEBUG_INFO(DEBUG_CATEGORY_STREAM, "desktop stream audio output ready id=%llu rate=48000 channels=2", (unsigned long long)m_playback->id); } /* GUI забирает последний готовый кадр по аппаратным часам, не накапливая Qt-сигналы с QImage. */ @@ -216,7 +216,7 @@ void StreamPlayerWindow::decode() { if (record.pts > INT64_MAX) { fail(text("Invalid stream timestamp")); break; } if (record.kind == StreamRecordKind::Audio) { if (!p.audioAvailable) continue; - if (!opus) opus = opus_codec_decoder_create(48000, 1); + if (!opus) opus = opus_codec_decoder_create(48000, 2); if (!opus) { fail(text("Stream audio decoder failed")); break; } if (expectedAudio >= 0 && (static_cast(record.pts) < expectedAudio - 10000 || static_cast(record.pts) > expectedAudio + 100000)) { diff --git a/tools/chatgui/tests/test_stream_view.cpp b/tools/chatgui/tests/test_stream_view.cpp index cfa81011..1ca59b0e 100644 --- a/tools/chatgui/tests/test_stream_view.cpp +++ b/tools/chatgui/tests/test_stream_view.cpp @@ -30,6 +30,7 @@ static QList snapshots; #ifdef TEST_NULL_AUDIO static std::atomic originalAudioCallback{nullptr}; static std::atomic nonzeroSamples{0}; +static std::atomic leftEnergy{0}, rightEnergy{0}; static std::atomic playbackDelayUs{0}; extern "C" int64_t __real_audio_stream_frame_time(ma_device*, unsigned int, int*); extern "C" int64_t __wrap_audio_stream_frame_time(ma_device* device, unsigned int frames, int* valid) { @@ -46,11 +47,17 @@ extern "C" ma_result __wrap_ma_context_init(const ma_backend*, ma_uint32, const static void observeAudio(ma_device* device, void* out, const void* in, ma_uint32 frames) { originalAudioCallback.load()(device, out, in, frames); const auto* pcm = static_cast(out); - for (unsigned i = 0; i < frames; i++) if (pcm[i]) nonzeroSamples.fetch_add(1); + uint64_t left = 0, right = 0, nonzero = 0; + for (unsigned i = 0; i < frames; i++) { + left += static_cast(pcm[2 * i]) * pcm[2 * i]; + right += static_cast(pcm[2 * i + 1]) * pcm[2 * i + 1]; + nonzero += pcm[2 * i] != 0; nonzero += pcm[2 * i + 1] != 0; + } + leftEnergy.fetch_add(left); rightEnergy.fetch_add(right); nonzeroSamples.fetch_add(nonzero); } extern "C" ma_result __wrap_ma_device_init(ma_context* context, const ma_device_config* config, ma_device* device) { ma_device_config copy = *config; - if (copy.deviceType == ma_device_type_playback && copy.playback.format == ma_format_s16 && copy.playback.channels == 1) { + if (copy.deviceType == ma_device_type_playback && copy.playback.format == ma_format_s16 && copy.playback.channels == 2) { originalAudioCallback = copy.dataCallback; copy.dataCallback = observeAudio; } return __real_ma_device_init(context, ©, device); @@ -81,7 +88,7 @@ class StreamViewTest : public QObject { } void publish(const QByteArray& video, const QByteArray& audio = {}, unsigned videoFrames = 1) { - stream_media_block media{}; media.format = {160, 90, rotation, STREAM_MEDIA_H264, STREAM_MEDIA_OPUS, revision}; + 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; for (unsigned i = 0; i < videoFrames; i++) media.video[i] = {++frameId, mediaPts + i * 100000 / videoFrames, @@ -172,11 +179,17 @@ private slots: QByteArray audio; #ifdef TEST_NULL_AUDIO QVERIFY(SoundManager::instance()->init()); - auto* opus = opus_codec_encoder_create(48000, 1); QVERIFY(opus); - int16_t pcm[960]; for (unsigned i = 0; i < 960; i++) pcm[i] = static_cast(6000 * std::sin(i * 0.05)); + 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); - nonzeroSamples = 0; playbackDelayUs = 200000; + nonzeroSamples = 0; leftEnergy = rightEnergy = 0; playbackDelayUs = 200000; #endif StreamPlayerWindow window(group, inst->node_id, epoch, "Source"); QSignalSpy shown(&window, &StreamPlayerWindow::framePresented); window.show(); @@ -190,6 +203,9 @@ private slots: QCOMPARE(switched.descriptor.source_epoch, epoch); QCOMPARE(switched.owners, 2u); #ifdef TEST_NULL_AUDIO QTRY_VERIFY_WITH_TIMEOUT(nonzeroSamples.load() > 0, 5000); + QVERIFY(rightEnergy.load() > 0); + 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); QTest::qWait(50); uint64_t played = nonzeroSamples.load(); QTest::qWait(100); QCOMPARE(nonzeroSamples.load(), played); #endif