Adapt desktop call PCM reserve to output period and underruns

This commit is contained in:
evgeny committed 2026-10-15 13:55:49 +02:00
1 parent cb9852e8d5
commit cfa3ed1f61
3 files changed
+70 -6

No files matched your search

+35 -4
View File
@@ -1,5 +1,6 @@
#include "voice_audio_io.h" #include "voice_audio_io.h"
#include "voice_capture_clock.h" #include "voice_capture_clock.h"
#include "voice_playback_buffer.h"
extern "C" { extern "C" {
#include "call/call_audio.h" #include "call/call_audio.h"
#include "radio/radio_audio.h" #include "radio/radio_audio.h"
@@ -30,11 +31,17 @@ struct VoiceLatencyRange {
VoiceAudioIo::~VoiceAudioIo() { stop(); } 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) { bool VoiceAudioIo::start(Kind kind, uint64_t session) {
stop(); stop();
m_kind = kind; m_session = session; m_kind = kind; m_session = session;
m_captureEvents.reset(); m_references.reset(); m_playback.reset(); m_playProgress.reset(); m_captureEvents.reset(); m_references.reset(); m_playback.reset(); m_playProgress.reset();
m_progressDropped = 0; m_progressDropped = 0;
{ std::lock_guard lock(m_latencyMutex); m_outputLatency = {}; }
m_playPosition = 960; m_playEpoch = 1; m_playPosition = 960; m_playEpoch = 1;
m_captureDropped = m_referenceDropped = m_playbackUnderruns = 0; m_captureDropped = m_referenceDropped = m_playbackUnderruns = 0;
m_running = true; m_running = true;
@@ -43,7 +50,7 @@ bool VoiceAudioIo::start(Kind kind, uint64_t session) {
m_running = false; m_running = false;
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "voice-io: RX worker start failed reason=%s", error.what()); return 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"); (unsigned long long)session, kind == Kind::Call ? "call" : "radio");
return true; return true;
} }
@@ -134,7 +141,7 @@ void VoiceAudioIo::reference(const float* pcm, unsigned frames, int64_t firstTim
} }
/* Выход всегда обслуживается независимо от capture/VAD. Уже потреблённый PCM становится референсом у владельца микшера. */ /* Выход всегда обслуживается независимо от 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(); uint64_t epoch = m_playEpoch.load();
if (!firstTimeUs) firstTimeUs = voice_time_us(); if (!firstTimeUs) firstTimeUs = voice_time_us();
unsigned consumed = 0; 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; unsigned remaining = m_playCurrent.epoch == epoch ? 960 - m_playPosition : 0;
PlaybackProgress progress{epoch, m_playCurrent.position + m_playPosition, PlaybackProgress progress{epoch, m_playCurrent.position + m_playPosition,
firstTimeUs + (int64_t)consumed * 1000000 / 48000, measured, 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); if (!m_playProgress.push(progress)) m_progressDropped.fetch_add(1, std::memory_order_relaxed);
m_wake.notify_all(); m_wake.notify_all();
} }
@@ -167,6 +175,8 @@ void VoiceAudioIo::playbackLoop() {
int64_t lastClockReport = 0; int64_t lastClockReport = 0;
VoiceLatencyRange queueUs, pullUs; VoiceLatencyRange queueUs, pullUs;
unsigned missing = 0; unsigned missing = 0;
VoicePlaybackBuffer buffering;
PlaybackProgress lastProgress{};
call_clock_snapshot clock{}; call_clock_snapshot clock{};
if (m_kind == Kind::Call && call_audio_get_clock(m_session, voice_time_us(), &clock) != 0) 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); 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); else radio_audio_reset_playback(m_session);
position = 0; position = 0;
epoch = requested; epoch = requested;
buffering.reset(); lastProgress = {};
{ std::lock_guard lock(m_latencyMutex); m_outputLatency = {}; }
} }
PlaybackProgress progress{}; PlaybackProgress progress{};
for (unsigned count = 0; count < 32 && m_playProgress.pop(progress); ++count) { for (unsigned count = 0; count < 32 && m_playProgress.pop(progress); ++count) {
if (progress.epoch != epoch) continue; 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); queueUs.add(int64_t(progress.queuedFrames) * 1000000 / 48000);
missing += progress.missing; missing += progress.missing;
lastProgress = progress;
if (!progress.consumed) continue; if (!progress.consumed) continue;
int64_t now = voice_time_us(), remaining = progress.endTimeUs - now; int64_t now = voice_time_us(), remaining = progress.endTimeUs - now;
if (m_kind == Kind::Call) if (m_kind == Kind::Call)
@@ -200,6 +222,8 @@ void VoiceAudioIo::playbackLoop() {
int64_t reportTime = voice_time_us(); int64_t reportTime = voice_time_us();
if (m_kind == Kind::Call && reportTime - lastClockReport >= 1000000) { if (m_kind == Kind::Call && reportTime - lastClockReport >= 1000000) {
lastClockReport = reportTime; 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) if (queueUs.count || pullUs.count)
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "voice-io: playback latency session=%llu samples=%u pulls=%u " 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", "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, (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); (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; Playback block{}; block.epoch = epoch; block.position = position;
int64_t pullAt = voice_time_us(); int64_t pullAt = voice_time_us();
if (m_kind == Kind::Call) { if (m_kind == Kind::Call) {
@@ -230,6 +254,13 @@ void VoiceAudioIo::playbackLoop() {
std::unique_lock lock(m_waitMutex); std::unique_lock lock(m_waitMutex);
m_wake.wait_for(lock, std::chrono::milliseconds(5)); 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};
}
} }
} }
+6 -2
View File
@@ -13,6 +13,8 @@
class VoiceAudioIo { class VoiceAudioIo {
public: public:
enum class Kind { Call, Radio }; 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() = default;
~VoiceAudioIo(); ~VoiceAudioIo();
bool start(Kind kind, uint64_t session); bool start(Kind kind, uint64_t session);
@@ -27,7 +29,7 @@ public:
/* firstTimeUs — монотонное время первого физического PCM-фрейма (backend либо оценка). */ /* firstTimeUs — монотонное время первого физического PCM-фрейма (backend либо оценка). */
void capture(const int16_t* pcm, unsigned frames, int64_t firstTimeUs, uint64_t route = 0, bool missing = false); 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 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: private:
enum class EventKind { Pcm, Ptt, Mute, Aec, Stop }; enum class EventKind { Pcm, Ptt, Mute, Aec, Stop };
struct Event { struct Event {
@@ -45,7 +47,7 @@ private:
uint64_t epoch = 0, position = 0; uint64_t epoch = 0, position = 0;
int64_t endTimeUs = 0; int64_t endTimeUs = 0;
bool measured = false; bool measured = false;
unsigned consumed = 0, missing = 0, queuedFrames = 0; unsigned consumed = 0, missing = 0, queuedFrames = 0, callbackFrames = 0;
}; };
AudioEventQueue<Event, 32> m_captureEvents, m_references; AudioEventQueue<Event, 32> m_captureEvents, m_references;
AudioEventQueue<Playback, 12> m_playback; AudioEventQueue<Playback, 12> m_playback;
@@ -60,6 +62,8 @@ private:
std::atomic<bool> m_aecRequested{false}; std::atomic<bool> m_aecRequested{false};
std::thread m_captureThread, m_playbackThread; std::thread m_captureThread, m_playbackThread;
std::mutex m_waitMutex; std::mutex m_waitMutex;
mutable std::mutex m_latencyMutex; // Только worker и GUI; callback не берёт lock.
OutputLatency m_outputLatency;
std::condition_variable m_wake; std::condition_variable m_wake;
Playback m_playCurrent{}; Playback m_playCurrent{};
unsigned m_playPosition = 960; unsigned m_playPosition = 960;
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include <algorithm>
#include <cstdint>
/* Резерв готового PCM desktop. Размер callback задаёт нижнюю границу; недостача добавляет
* один блок, 10 секунд стабильного вывода убирают один. Частично сыгранный блок — сверх резерва.
* Только RX worker; время монотонное. Четыре PCM-слота доступны в AudioEventQueue<Playback, 12>. */
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;
};