From 0c20766cfe2ecc2da34488633f4b44cf5e7bf59f Mon Sep 17 00:00:00 2001 From: evgeny Date: Fri, 30 Oct 2026 19:57:25 +0100 Subject: [PATCH] Gate startup on published PCM and test recovery beyond the initial reserve --- tools/chatgui/src/audio_event_queue.h | 9 ++++++ tools/chatgui/src/voice_audio_io.cpp | 2 +- tools/chatgui/tests/test_call_jitter.cpp | 35 ++++++++++++++++++++++-- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/tools/chatgui/src/audio_event_queue.h b/tools/chatgui/src/audio_event_queue.h index 94811639..8ab56fe7 100644 --- a/tools/chatgui/src/audio_event_queue.h +++ b/tools/chatgui/src/audio_event_queue.h @@ -58,4 +58,13 @@ public: uint64_t count = m_write.load(std::memory_order_acquire) - read; return (size_t)(count > Capacity ? Capacity : count); // Приблизительный снимок, включая ещё не опубликованные слоты. } + /* Только consumer: число подряд опубликованных элементов. Для порога старта PCM + * size() недостаточно: зарезервированный producer-ом блок ещё нельзя воспроизводить. */ + size_t readable() const { + uint64_t position = m_read.load(std::memory_order_relaxed); + size_t count = 0; + while (count < Capacity && m_slots[(position + count) % Capacity].sequence.load(std::memory_order_acquire) == position + count + 1) + ++count; + return count; + } }; diff --git a/tools/chatgui/src/voice_audio_io.cpp b/tools/chatgui/src/voice_audio_io.cpp index ef6e2a86..69822735 100644 --- a/tools/chatgui/src/voice_audio_io.cpp +++ b/tools/chatgui/src/voice_audio_io.cpp @@ -157,7 +157,7 @@ void VoiceAudioIo::mix(float* stereo, unsigned frames, int64_t firstTimeUs, bool for (int attempt = 0; m_playPosition == 960 && attempt < 12 && m_playback.pop(m_playCurrent); ++attempt) if (m_playCurrent.epoch == epoch) m_playPosition = 0; unsigned required = std::max(1920u, callbackFrames ? callbackFrames : frames); - unsigned ready = unsigned(m_playback.size() * 960) + (m_playCurrent.epoch == epoch ? 960 - m_playPosition : 0); + unsigned ready = unsigned(m_playback.readable() * 960) + (m_playCurrent.epoch == epoch ? 960 - m_playPosition : 0); if (ready < required) { // PREBUFFER: устройство работает, но речь ещё не началась. Это не starvation. PlaybackProgress waiting{}; waiting.epoch = epoch; waiting.callbackFrames = callbackFrames ? callbackFrames : frames; diff --git a/tools/chatgui/tests/test_call_jitter.cpp b/tools/chatgui/tests/test_call_jitter.cpp index 02625f38..4df9962a 100644 --- a/tools/chatgui/tests/test_call_jitter.cpp +++ b/tools/chatgui/tests/test_call_jitter.cpp @@ -146,32 +146,61 @@ static void test_startup_policy() { p.observe(15101, 350, 1000, true); CHECK(p.reserve_ms == 350); p.observe(15102, 20, 1000, true); CHECK(p.reserve_ms == 350); + for (bool abrupt : {false, true}) { decoder d; auto* j = call_jitter_create(decode, &d, 1000); int next = 0, packets = 0, batch = 4, nominal = 80, due = nominal; - int16_t pcm[480]; bool played = false; + int16_t pcm[480]; bool played = false; uint32_t previous_underruns = 0; for (int now = 0; now < 20000; now += 10) { if (now >= due) { for (int i = 0; i < batch; i++, next++) call_jitter_push(j, (uint8_t*)&next, sizeof(next), nominal, now, nullptr, i == 0); - if (++packets == 60) batch = 10; + ++packets; + if (abrupt && packets == 60) batch = 10; + if (!abrupt && packets % 20 == 0 && batch < 10) ++batch; nominal += batch * 20; due = nominal + (packets % 4 == 0 ? 120 : 0); } int n = call_jitter_pull(j, pcm, 480); played |= n > 0; if (n < 480) call_jitter_silence(j, 480 - n); call_jitter_diagnostics state{}; call_jitter_get_diagnostics(j, &state, 0); - CHECK(state.underruns == 0); + if (state.underruns != previous_underruns) { + printf("startup shortage: now=%d next=%d batch=%d due=%d depth=%d encoded=%d reserve=%d tempo=%d\n", + now, next, batch, due, state.depth_ms, state.encoded_ms, state.reserve_ms, state.tempo_x100); + // Резкий рост 4->10 добавляет 120мс к пачке плюс 120мс джиттера: больше исходных 200мс. + CHECK(abrupt && now < 6000 && state.underruns == 1); previous_underruns = state.underruns; + } if (now < 5000) CHECK(state.reserve_ms >= 200); } CHECK(played); + CHECK(previous_underruns == (abrupt ? 1u : 0u)); call_jitter_destroy(j); + } puts("startup policy: cold start, held reserve, packet size change and jitter PASS"); } +static void test_render_readiness() { + const uint64_t id = 990; + CHECK(call_audio_start(id) == 0); + int16_t pcm[960] = {}; call_audio_render state{}; call_clock_snapshot clock{}; + for (int i = 0; i < 10; i++) { + CHECK(call_audio_render_pcm(id, pcm, 960, &state) == 0); + CHECK(state.phase == CALL_AUDIO_WAITING && state.speech_samples == 0); + } + CHECK(call_audio_get_clock(id, 0, &clock) == 0 && clock.written_samples == 0); + auto* enc = opus_codec_encoder_create(48000, 1); CHECK(enc); + uint8_t wire[512]; int len = opus_codec_encode(enc, pcm, 960, wire, sizeof(wire)); CHECK(len > 0); + for (int i = 0; i < 12; i++) call_audio_on_media(&audio_owner, id, i, 0, wire, len, nullptr, 1); + CHECK(call_audio_render_pcm(id, pcm, 960, &state) == 960); + CHECK(state.phase == CALL_AUDIO_SPEECH && state.speech_samples == 960); // Закодированная тишина тоже настоящая речь. + CHECK(call_audio_get_clock(id, 0, &clock) == 0 && clock.written_samples == 960); + call_audio_release(id); opus_codec_encoder_destroy(enc); +} + int main() { test_startup_policy(); test_packet_arrival_sampling(); CHECK(call_audio_init(&audio_owner) == 0); + test_render_readiness(); { call_jitter_window w; w.observe(0, 100); w.observe(500, 400);