From cfa3ed1f6152e9f2a3520933b7af92b53d5a27c1 Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 15 Oct 2026 13:55:49 +0200 Subject: [PATCH] Adapt desktop call PCM reserve to output period and underruns --- tools/chatgui/src/voice_audio_io.cpp | 39 ++++++++++++++++++++--- tools/chatgui/src/voice_audio_io.h | 8 +++-- tools/chatgui/src/voice_playback_buffer.h | 29 +++++++++++++++++ 3 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 tools/chatgui/src/voice_playback_buffer.h diff --git a/tools/chatgui/src/voice_audio_io.cpp b/tools/chatgui/src/voice_audio_io.cpp index 1bb89af7..e15ea6bb 100644 --- a/tools/chatgui/src/voice_audio_io.cpp +++ b/tools/chatgui/src/voice_audio_io.cpp @@ -1,5 +1,6 @@ #include "voice_audio_io.h" #include "voice_capture_clock.h" +#include "voice_playback_buffer.h" extern "C" { #include "call/call_audio.h" #include "radio/radio_audio.h" @@ -30,11 +31,17 @@ struct VoiceLatencyRange { VoiceAudioIo::~VoiceAudioIo() { stop(); } +VoiceAudioIo::OutputLatency VoiceAudioIo::outputLatency() const { + std::lock_guard lock(m_latencyMutex); + return m_running && voice_time_us() - m_outputLatency.sampledUs <= 500000 ? m_outputLatency : OutputLatency{}; +} + bool VoiceAudioIo::start(Kind kind, uint64_t session) { stop(); m_kind = kind; m_session = session; m_captureEvents.reset(); m_references.reset(); m_playback.reset(); m_playProgress.reset(); m_progressDropped = 0; + { std::lock_guard lock(m_latencyMutex); m_outputLatency = {}; } m_playPosition = 960; m_playEpoch = 1; m_captureDropped = m_referenceDropped = m_playbackUnderruns = 0; m_running = true; @@ -43,7 +50,7 @@ bool VoiceAudioIo::start(Kind kind, uint64_t session) { m_running = false; DEBUG_ERROR(DEBUG_CATEGORY_CALL, "voice-io: RX worker start failed reason=%s", error.what()); return false; } - DEBUG_INFO(DEBUG_CATEGORY_CALL, "voice-io: started session=%llu kind=%s PCM reserve<=40ms", + DEBUG_INFO(DEBUG_CATEGORY_CALL, "voice-io: started session=%llu kind=%s PCM ready=40ms plus partial block; calls adaptive=20..80ms", (unsigned long long)session, kind == Kind::Call ? "call" : "radio"); return true; } @@ -134,7 +141,7 @@ void VoiceAudioIo::reference(const float* pcm, unsigned frames, int64_t firstTim } /* Выход всегда обслуживается независимо от capture/VAD. Уже потреблённый PCM становится референсом у владельца микшера. */ -void VoiceAudioIo::mix(float* stereo, unsigned frames, int64_t firstTimeUs, bool measured) { +void VoiceAudioIo::mix(float* stereo, unsigned frames, int64_t firstTimeUs, bool measured, unsigned callbackFrames) { uint64_t epoch = m_playEpoch.load(); if (!firstTimeUs) firstTimeUs = voice_time_us(); unsigned consumed = 0; @@ -156,7 +163,8 @@ void VoiceAudioIo::mix(float* stereo, unsigned frames, int64_t firstTimeUs, bool unsigned remaining = m_playCurrent.epoch == epoch ? 960 - m_playPosition : 0; PlaybackProgress progress{epoch, m_playCurrent.position + m_playPosition, firstTimeUs + (int64_t)consumed * 1000000 / 48000, measured, - consumed, frames - consumed, unsigned(m_playback.size() * 960) + remaining}; + consumed, frames - consumed, unsigned(m_playback.size() * 960) + remaining, + callbackFrames ? callbackFrames : frames}; if (!m_playProgress.push(progress)) m_progressDropped.fetch_add(1, std::memory_order_relaxed); m_wake.notify_all(); } @@ -167,6 +175,8 @@ void VoiceAudioIo::playbackLoop() { int64_t lastClockReport = 0; VoiceLatencyRange queueUs, pullUs; unsigned missing = 0; + VoicePlaybackBuffer buffering; + PlaybackProgress lastProgress{}; call_clock_snapshot clock{}; if (m_kind == Kind::Call && call_audio_get_clock(m_session, voice_time_us(), &clock) != 0) DEBUG_WARN(DEBUG_CATEGORY_CALL, "voice-io: call clock unavailable session=%llu", (unsigned long long)m_session); @@ -182,12 +192,24 @@ void VoiceAudioIo::playbackLoop() { else radio_audio_reset_playback(m_session); position = 0; epoch = requested; + buffering.reset(); lastProgress = {}; + { std::lock_guard lock(m_latencyMutex); m_outputLatency = {}; } } PlaybackProgress progress{}; for (unsigned count = 0; count < 32 && m_playProgress.pop(progress); ++count) { if (progress.epoch != epoch) continue; + if (m_kind == Kind::Call) { + unsigned previous = buffering.blocks; + if (progress.callbackFrames > 3840 && progress.callbackFrames != buffering.callbackFrames) + DEBUG_ERROR(DEBUG_CATEGORY_CALL, "voice-io: output callback exceeds PCM capacity frames=%u", progress.callbackFrames); + buffering.sample(voice_time_us(), progress.callbackFrames, progress.missing != 0); + if (buffering.blocks != previous) + DEBUG_INFO(DEBUG_CATEGORY_CALL, "voice-io: PCM reserve session=%llu blocks=%u->%u callback=%u missing=%u", + (unsigned long long)m_session, previous, buffering.blocks, progress.callbackFrames, progress.missing); + } queueUs.add(int64_t(progress.queuedFrames) * 1000000 / 48000); missing += progress.missing; + lastProgress = progress; if (!progress.consumed) continue; int64_t now = voice_time_us(), remaining = progress.endTimeUs - now; if (m_kind == Kind::Call) @@ -200,6 +222,8 @@ void VoiceAudioIo::playbackLoop() { int64_t reportTime = voice_time_us(); if (m_kind == Kind::Call && reportTime - lastClockReport >= 1000000) { lastClockReport = reportTime; + if (missing) DEBUG_WARN(DEBUG_CATEGORY_CALL, "voice-io: output starved session=%llu missing_samples=%u ready_blocks=%u", + (unsigned long long)m_session, missing, buffering.blocks); if (queueUs.count || pullUs.count) DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "voice-io: playback latency session=%llu samples=%u pulls=%u " "pcm_after_mix_us=%lld/%lld/%lld pull_us=%lld/%lld/%lld missing_samples=%u queue_estimate=1", @@ -215,7 +239,7 @@ void VoiceAudioIo::playbackLoop() { (unsigned long long)snapshot.played_samples, (unsigned long long)snapshot.written_samples, (long long)snapshot.pts_us, snapshot.valid, snapshot.measured, snapshot.held_back_us); } - if (m_playback.size() < 2) { + if (m_playback.size() < (m_kind == Kind::Call ? buffering.blocks : 2)) { Playback block{}; block.epoch = epoch; block.position = position; int64_t pullAt = voice_time_us(); if (m_kind == Kind::Call) { @@ -230,6 +254,13 @@ void VoiceAudioIo::playbackLoop() { std::unique_lock lock(m_waitMutex); m_wake.wait_for(lock, std::chrono::milliseconds(5)); } + if (lastProgress.epoch == epoch && lastProgress.consumed && position >= lastProgress.position) { + int64_t now = voice_time_us(); + int64_t queuedUs = (position - lastProgress.position) * 1000000 / 48000; + std::lock_guard lock(m_latencyMutex); + m_outputLatency = {int(queuedUs / 1000), int((queuedUs + std::max(lastProgress.endTimeUs - now, int64_t(0))) / 1000), + lastProgress.measured, now}; + } } } diff --git a/tools/chatgui/src/voice_audio_io.h b/tools/chatgui/src/voice_audio_io.h index 736bcf91..6d56a66f 100644 --- a/tools/chatgui/src/voice_audio_io.h +++ b/tools/chatgui/src/voice_audio_io.h @@ -13,6 +13,8 @@ class VoiceAudioIo { public: enum class Kind { Call, Radio }; + struct OutputLatency { int queueMs = -1, outputMs = -1; bool measured = false; int64_t sampledUs = 0; }; + OutputLatency outputLatency() const; VoiceAudioIo() = default; ~VoiceAudioIo(); bool start(Kind kind, uint64_t session); @@ -27,7 +29,7 @@ public: /* firstTimeUs — монотонное время первого физического PCM-фрейма (backend либо оценка). */ void capture(const int16_t* pcm, unsigned frames, int64_t firstTimeUs, uint64_t route = 0, bool missing = false); void reference(const float* pcm, unsigned frames, int64_t firstTimeUs); - void mix(float* stereo, unsigned frames, int64_t firstTimeUs = 0, bool measured = false); + void mix(float* stereo, unsigned frames, int64_t firstTimeUs = 0, bool measured = false, unsigned callbackFrames = 0); private: enum class EventKind { Pcm, Ptt, Mute, Aec, Stop }; struct Event { @@ -45,7 +47,7 @@ private: uint64_t epoch = 0, position = 0; int64_t endTimeUs = 0; bool measured = false; - unsigned consumed = 0, missing = 0, queuedFrames = 0; + unsigned consumed = 0, missing = 0, queuedFrames = 0, callbackFrames = 0; }; AudioEventQueue m_captureEvents, m_references; AudioEventQueue m_playback; @@ -60,6 +62,8 @@ private: std::atomic m_aecRequested{false}; std::thread m_captureThread, m_playbackThread; std::mutex m_waitMutex; + mutable std::mutex m_latencyMutex; // Только worker и GUI; callback не берёт lock. + OutputLatency m_outputLatency; std::condition_variable m_wake; Playback m_playCurrent{}; unsigned m_playPosition = 960; diff --git a/tools/chatgui/src/voice_playback_buffer.h b/tools/chatgui/src/voice_playback_buffer.h new file mode 100644 index 00000000..216be161 --- /dev/null +++ b/tools/chatgui/src/voice_playback_buffer.h @@ -0,0 +1,29 @@ +#pragma once +#include +#include + +/* Резерв готового PCM desktop. Размер callback задаёт нижнюю границу; недостача добавляет + * один блок, 10 секунд стабильного вывода убирают один. Частично сыгранный блок — сверх резерва. + * Только RX worker; время монотонное. Четыре PCM-слота доступны в AudioEventQueue. */ +class VoicePlaybackBuffer { +public: + unsigned blocks = 2; + unsigned callbackFrames = 960; + void reset() { blocks = 2; callbackFrames = 960; stableSince = 0; lastGrowth = 0; } + void sample(int64_t nowUs, unsigned frames, bool missing) { + callbackFrames = frames; + unsigned minimum = std::clamp((frames + 959) / 960, 1u, 4u); + if (!stableSince) stableSince = nowUs; + if (minimum > blocks) { blocks = minimum; stableSince = nowUs; } + if (missing) { + stableSince = nowUs; + if (!lastGrowth || nowUs - lastGrowth >= 500000) { + blocks = std::min(blocks + 1, 4u); lastGrowth = nowUs; + } + } else if (blocks > minimum && nowUs - stableSince >= 10000000) { + --blocks; stableSince = nowUs; + } + } +private: + int64_t stableSince = 0, lastGrowth = 0; +};