From e1bb12d16a5597f61b975f852ea4204d1ba6f7ed Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 15 Oct 2026 13:57:03 +0200 Subject: [PATCH] Expose desktop output latency and verify hardware clock transitions --- tools/chatgui/src/call_audio_engine.h | 1 + tools/chatgui/src/callwindow.cpp | 14 ++++++++++-- tools/chatgui/tests/test_call_timeline.cpp | 26 +++++++++++++++++++++- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/tools/chatgui/src/call_audio_engine.h b/tools/chatgui/src/call_audio_engine.h index bc2aa714..07030fea 100644 --- a/tools/chatgui/src/call_audio_engine.h +++ b/tools/chatgui/src/call_audio_engine.h @@ -33,6 +33,7 @@ public: bool isActive() const { return m_active.load(); } quint64 callId() const { return m_callId.load(); } + VoiceAudioIo::OutputLatency outputLatency() const { return m_voice.outputLatency(); } /* Mute: пир слышит тишину (отправляем нулевые кадры вместо захваченного PCM). */ void setMuted(bool muted); diff --git a/tools/chatgui/src/callwindow.cpp b/tools/chatgui/src/callwindow.cpp index 416540f4..9d02288e 100644 --- a/tools/chatgui/src/callwindow.cpp +++ b/tools/chatgui/src/callwindow.cpp @@ -506,10 +506,20 @@ void CallWindow::updateDuration() { void CallWindow::setAudioStats(int rtt, int buffer, int tempo, int dropped, int underruns, int minimum, int maximum, int reserve) { - m_statsLabel->setText(QCoreApplication::translate("ChatGui", + QString text = QCoreApplication::translate("ChatGui", "Buffer: %1 ms · Reserve: %2 ms\nOver 10 s: minimum %3 · maximum %4 ms\nSpeed: %5× · RTT: %6 " "ms\nBuffer drops: %7 · Underruns: %8") .arg(buffer).arg(reserve).arg(minimum).arg(maximum) - .arg(tempo / 100.0, 0, 'f', 2).arg(rtt).arg(dropped).arg(underruns)); + .arg(tempo / 100.0, 0, 'f', 2).arg(rtt).arg(dropped).arg(underruns); + auto* audio = CallAudioEngine::instance(); + if (audio->callId() == m_callId) { + auto output = audio->outputLatency(); + auto value = [](int ms) { return ms >= 0 ? QString::number(ms) : QStringLiteral("—"); }; + QString total = value(output.outputMs); + if (output.outputMs >= 0 && !output.measured) total.prepend(QStringLiteral("~")); + text += QLatin1Char('\n') + QCoreApplication::translate("ChatGui", "Audio output: %1 ms (includes app queue: %2 ms)") + .arg(total).arg(value(output.queueMs)); + } + m_statsLabel->setText(text); m_statsLabel->setVisible(m_state == Active); } diff --git a/tools/chatgui/tests/test_call_timeline.cpp b/tools/chatgui/tests/test_call_timeline.cpp index ad960e9d..b15a490d 100644 --- a/tools/chatgui/tests/test_call_timeline.cpp +++ b/tools/chatgui/tests/test_call_timeline.cpp @@ -53,6 +53,30 @@ static void test_mix() { DEBUG_INFO(DEBUG_CATEGORY_CALL, "PASS timeline: weighted overlap, input boundary, source discontinuity"); } +static void test_hardware_anchor_transition() { + CallTimeline map; + map.reset(1); + for (unsigned i = 0; i < 20; ++i) { + map.feed(1000000 + i * 20000, 960, 1); + map.source({SourceKind::Copy, SourceError::None, i * 960, i * 960, 0, 960}); + map.read(960); + } + map.progress(1, 9600, 1020000, 1000000, false); + auto before = map.clock(1000000); + CHECK(before.valid && before.played_samples == 8640 && !before.measured); + // Новая аппаратная метка обнаружила ещё 100мс HAL: не регрессируем позицию, ждём реальный звук. + map.progress(1, 10560, 1140000, 1020000, true); + auto after = map.clock(1020000); + CHECK(after.valid && after.measured && after.pts_us == before.pts_us); + CHECK(map.clock(1100000).pts_us == before.pts_us); + CHECK(map.clock(1120000).pts_us > before.pts_us); + CHECK(!map.clock(1300000).valid); + // После смены выхода позиция написанного PCM продолжается, меняется лишь физический срок. + map.progress(1, 19200, 1320000, 1300000, true); + CHECK(map.clock(1300000).valid && map.clock(1300000).pts_us >= after.pts_us); + DEBUG_INFO(DEBUG_CATEGORY_CALL, "PASS timeline: hardware timestamp acquisition, route change, no clock regression"); +} + static void test_long_run() { CallTimeline map; map.reset(1); @@ -108,6 +132,6 @@ static void test_failure_and_recovery() { int main() { debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); - test_boundaries(); test_mix(); test_long_run(); test_failure_and_recovery(); + test_boundaries(); test_mix(); test_hardware_anchor_transition(); test_long_run(); test_failure_and_recovery(); return 0; }