Browse Source

Adapt desktop call PCM reserve to output period and underruns

master
evgeny 4 days ago
parent
commit
cfa3ed1f61
  1. 39
      tools/chatgui/src/voice_audio_io.cpp
  2. 8
      tools/chatgui/src/voice_audio_io.h
  3. 29
      tools/chatgui/src/voice_playback_buffer.h

39
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};
}
}
}

8
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<Event, 32> m_captureEvents, m_references;
AudioEventQueue<Playback, 12> m_playback;
@ -60,6 +62,8 @@ private:
std::atomic<bool> 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;

29
tools/chatgui/src/voice_playback_buffer.h

@ -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;
};
Loading…
Cancel
Save