radio: add activation controls, live VAD meters and signal diagnostics

This commit is contained in:
evgeny committed 2026-10-09 16:11:11 +02:00
1 parent fddbba217d
commit 7f3247bfcc
23 files changed
+818 -182

No files matched your search

+70 -16
View File
@@ -205,10 +205,15 @@ void VoiceAudioIo::captureLoop() {
int16_t raw[1920]{}, cleaned[1920]{}, referencePcm[1920]{};
uint8_t muteMask[960]{};
unsigned filled = 0;
int64_t frameTime = 0, lastReport = voice_time_us();
int64_t frameTime = 0, lastReport = voice_time_us(), lastCapture = lastReport, maxQueueAge = 0;
uint64_t expectedSequence = 0, missingReference = 0, reportedReferenceDrops = referenceDrops, captureRoute = 0;
int64_t expectedCaptureTime = 0;
bool referenceAvailable = false;
const char* referenceReason = "empty_history";
double referenceGapUs = 0;
double rawEnergy = 0, cleanEnergy = 0, referenceEnergy = 0;
float rawPeak = 0, cleanPeak = 0, referencePeak = 0;
unsigned signalFrames = 0, aecFrames = 0;
bool muted = false;
auto applyMarker = [&](EventKind kind, bool value) {
if (kind == EventKind::Mute) muted = value;
@@ -238,6 +243,7 @@ void VoiceAudioIo::captureLoop() {
}
};
auto alignReference = [&]() {
referenceReason = "empty_history"; referenceGapUs = 0;
if (!historyCount) return false;
size_t index = 0;
for (unsigned i = 0; i < 960; ++i) {
@@ -251,7 +257,11 @@ void VoiceAudioIo::captureLoop() {
if (measured > interval * 0.95 && measured < interval * 1.05) interval = measured;
}
double position = (time - block.time) / interval;
if (position < 0 || position >= block.frames) return false;
if (position < 0 || position >= block.frames) {
referenceReason = position < 0 ? "history_too_new" : index + 1 == historyCount ? "render_not_ready" : "render_gap";
referenceGapUs = position < 0 ? -position * interval : (position - block.frames) * interval;
return false;
}
unsigned sample = (unsigned)position;
double fraction = position - sample;
for (unsigned channel = 0; channel < 2; ++channel) {
@@ -260,6 +270,7 @@ void VoiceAudioIo::captureLoop() {
referencePcm[2*i+channel] = (int16_t)(first + (second - first) * fraction);
}
}
referenceReason = "aligned";
return true;
};
auto deliver = [&](unsigned frames) {
@@ -274,8 +285,42 @@ void VoiceAudioIo::captureLoop() {
if (m_kind == Kind::Call && frames == 960) call_audio_feed_prepared_pcm(m_session, cleaned, muteMask);
filled = 0; markerCount = 0;
};
// Отчёт работает и при отсутствии PCM, иначе длительный провал захвата невидим.
auto report = [&]() {
int64_t now = voice_time_us();
if (now - lastReport >= 1000000) {
DEBUG_DEBUG(DEBUG_CATEGORY_AEC, "voice-io: capture health session=%llu span=%.1fms pcm_age=%.1fms "
"queue_age_max=%.1fms queue=%zu pending=%u sequence=%llu",
(unsigned long long)m_session, (now - lastReport) / 1000.0, (now - lastCapture) / 1000.0,
maxQueueAge / 1000.0, m_captureEvents.size(), filled, (unsigned long long)expectedSequence);
if (now - lastCapture >= 1000000)
DEBUG_WARN(DEBUG_CATEGORY_CALL, "voice-io: capture stalled session=%llu no_pcm=%.1fms queue=%zu",
(unsigned long long)m_session, (now - lastCapture) / 1000.0, m_captureEvents.size());
maxQueueAge = 0;
if (signalFrames) {
auto db = [](double power) { return 10 * std::log10(std::max(power, 1e-12)); };
DEBUG_DEBUG(DEBUG_CATEGORY_AEC, "voice-io: signal session=%llu frames=%u aec_frames=%u raw=%.1fdBFS clean=%.1fdBFS "
"reference=%.1fdBFS peaks=%.4f/%.4f/%.4f reference_state=%s gap=%.1fus",
(unsigned long long)m_session, signalFrames, aecFrames, db(rawEnergy / (signalFrames * 960 * m_channels)),
db(cleanEnergy / (signalFrames * 960 * m_channels)), db(aecFrames ? referenceEnergy / (aecFrames * 1920) : 0),
rawPeak, cleanPeak, referencePeak, referenceReason, referenceGapUs);
signalFrames = aecFrames = 0; rawEnergy = cleanEnergy = referenceEnergy = 0;
rawPeak = cleanPeak = referencePeak = 0;
}
uint64_t captureDrops = m_captureDropped.exchange(0), refDrops = m_referenceDropped.load();
uint64_t underruns = m_playbackUnderruns.exchange(0);
if (captureDrops || refDrops != reportedReferenceDrops || missingReference || underruns)
DEBUG_WARN(DEBUG_CATEGORY_AEC, "voice-io: session=%llu capture_drop=%llu reference_drop=%llu missing_ref=%llu output_missing=%llu",
(unsigned long long)m_session, (unsigned long long)captureDrops, (unsigned long long)refDrops,
(unsigned long long)missingReference, (unsigned long long)underruns);
else DEBUG_DEBUG(DEBUG_CATEGORY_AEC, "voice-io: session=%llu reference history=%zu capture_queue=%zu",
(unsigned long long)m_session, historyCount, m_captureEvents.size());
missingReference = 0; reportedReferenceDrops = refDrops; lastReport = now;
}
};
bool stopping = false;
while (m_capturing && !stopping) {
report();
consumeReference();
Event event{};
if (!m_captureEvents.pop(event)) {
@@ -289,9 +334,12 @@ void VoiceAudioIo::captureLoop() {
else { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "voice-io: too many controls within one capture frame"); stopping = true; }
continue;
}
if (voice_time_us() - event.enqueued > 100000) {
int64_t received = voice_time_us();
maxQueueAge = std::max(maxQueueAge, received - event.enqueued);
if (received - event.enqueued > 100000) {
m_captureDropped.fetch_add(event.frames); continue;
}
lastCapture = received;
bool clockJump = expectedCaptureTime && std::abs(event.time - expectedCaptureTime) > 20000;
if (event.sequence != expectedSequence || (captureRoute && event.route != captureRoute) || clockJump) {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "voice-io: capture discontinuity expected=%llu actual=%llu pending=%u route=%016llx->%016llx",
@@ -315,28 +363,34 @@ void VoiceAudioIo::captureLoop() {
memcpy(cleaned, raw, 960 * m_channels * sizeof(int16_t));
consumeReference();
bool available = aec && alignReference();
if (!aec) referenceReason = "disabled";
if (available) {
speex_aec_feed_playback(aec, referencePcm, 1920);
speex_aec_process_capture(aec, raw, 960 * m_channels, cleaned);
} else if (aec) {
++missingReference;
if (referenceAvailable) speex_aec_reset(aec);
if (referenceAvailable) {
DEBUG_DEBUG(DEBUG_CATEGORY_AEC, "voice-io: AEC bypass/reset session=%llu reason=%s gap=%.1fus history=%zu capture_time=%lld",
(unsigned long long)m_session, referenceReason, referenceGapUs, historyCount, (long long)frameTime);
speex_aec_reset(aec);
}
}
if (debug_should_output(DEBUG_LEVEL_DEBUG, DEBUG_CATEGORY_AEC)) {
++signalFrames; if (available) ++aecFrames;
for (int i = 0; i < 960 * m_channels; ++i) {
float in = raw[i] / 32768.0f, out = cleaned[i] / 32768.0f;
rawEnergy += in * in; cleanEnergy += out * out;
rawPeak = std::max(rawPeak, std::abs(in)); cleanPeak = std::max(cleanPeak, std::abs(out));
}
if (available) for (int i = 0; i < 1920; ++i) {
float ref = referencePcm[i] / 32768.0f;
referenceEnergy += ref * ref; referencePeak = std::max(referencePeak, std::abs(ref));
}
}
referenceAvailable = available;
deliver(960);
}
int64_t now = voice_time_us();
if (now - lastReport >= 1000000) {
uint64_t captureDrops = m_captureDropped.exchange(0), refDrops = m_referenceDropped.load();
uint64_t underruns = m_playbackUnderruns.exchange(0);
if (captureDrops || refDrops != reportedReferenceDrops || missingReference || underruns)
DEBUG_WARN(DEBUG_CATEGORY_AEC, "voice-io: session=%llu capture_drop=%llu reference_drop=%llu missing_ref=%llu output_missing=%llu",
(unsigned long long)m_session, (unsigned long long)captureDrops, (unsigned long long)refDrops,
(unsigned long long)missingReference, (unsigned long long)underruns);
else DEBUG_DEBUG(DEBUG_CATEGORY_AEC, "voice-io: session=%llu aligned history=%zu capture_queue=%zu",
(unsigned long long)m_session, historyCount, m_captureEvents.size());
missingReference = 0; reportedReferenceDrops = refDrops; lastReport = now;
}
}
if (filled) {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "voice-io: capture tail=%u frames bypasses AEC; call tail discarded, radio tail preserved", filled);