Раздели аудиопотоки и привяжи AEC к фактическому общему выходу

This commit is contained in:
evgeny committed 2026-10-02 10:22:34 +03:00
1 parent d7fb0c83f9
commit bd082a26bf
30 files changed
+1839 -412

No files matched your search

+35
View File
@@ -3,6 +3,11 @@
#include "debug_config.h"
#include <algorithm>
#include <utility>
#include <chrono>
extern "C" int64_t audio_stream_frame_time(ma_device* device, unsigned int frames, int* valid);
extern "C" uint64_t audio_stream_route(ma_device* device, int capture);
extern "C" int audio_stream_capture_missing(ma_device* device);
extern "C" void audio_stream_log_route(ma_device* device, int capture);
AudioDevice::AudioDevice() { m_clock.start(); }
AudioDevice::~AudioDevice() { close(); }
@@ -14,6 +19,7 @@ bool AudioDevice::init(ma_context* context, ma_device_config config, const char*
if (!context) { retryLater(); return false; }
auto* device = new ma_device{};
config.dataCallback = data;
config.noFixedSizedCallback = MA_TRUE; // Аппаратные чанки не требуют скрытого накопителя miniaudio.
config.notificationCallback = notification;
config.pUserData = this;
ma_result result = ma_device_init(context, &config, device);
@@ -25,6 +31,8 @@ bool AudioDevice::init(ma_context* context, ma_device_config config, const char*
}
m_device = device;
m_frames = 0;
m_route = 0; m_reportedRoute = 0;
m_timingMeasured = false; m_reportedTiming = -1;
m_seenFrames = 0;
m_suspended = false;
audio_diag_label(device, session, role);
@@ -56,10 +64,30 @@ void AudioDevice::close() {
DEBUG_INFO(DEBUG_CATEGORY_CALL, "audio: %s closed", m_role);
}
m_callback = {};
m_route = 0;
}
void AudioDevice::pause() {
if (!m_device) return;
ma_result result = ma_device_stop(m_device);
if (result != MA_SUCCESS) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: %s pause failed result=%d", m_role, result);
close();
}
}
void AudioDevice::data(ma_device* device, void* out, const void* in, ma_uint32 frames) {
auto* self = static_cast<AudioDevice*>(device->pUserData);
int valid = 0;
bool capture = device->type == ma_device_type_capture;
int64_t time = audio_stream_frame_time(device, frames, &valid);
int64_t now = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
if (!time) time = capture ? now - (int64_t)frames * 1000000 / device->sampleRate : now;
self->m_callbackTime.store(time, std::memory_order_relaxed);
self->m_timingMeasured.store(valid, std::memory_order_relaxed);
uint64_t route = audio_stream_route(device, capture);
if (route != self->m_route.exchange(route, std::memory_order_relaxed)) audio_stream_log_route(device, capture);
self->m_captureMissing.store(audio_stream_capture_missing(device), std::memory_order_relaxed);
if (self->m_callback) self->m_callback(in, out, frames);
self->m_frames.fetch_add(frames, std::memory_order_relaxed);
}
@@ -73,6 +101,13 @@ void AudioDevice::notification(const ma_device_notification* event) {
bool AudioDevice::needsRecovery() {
if (!m_device) return false;
uint64_t frames = m_frames.load(std::memory_order_relaxed);
uint64_t route = m_route.load();
int measured = m_timingMeasured.load();
if (frames && (route != m_reportedRoute || measured != m_reportedTiming)) {
DEBUG_INFO(DEBUG_CATEGORY_AEC, "audio: %s timing=%s physical_route=%016llx", m_role,
measured ? "backend-snapshot" : "callback-estimate", (unsigned long long)route);
m_reportedRoute = route; m_reportedTiming = measured;
}
qint64 now = m_clock.elapsed();
if (frames != m_seenFrames || m_suspended.load()) {
m_seenFrames = frames;
+11
View File
@@ -16,6 +16,7 @@ public:
AudioDevice& operator=(const AudioDevice&) = delete;
bool init(ma_context* context, ma_device_config config, const char* role, uint64_t session, Callback callback);
bool start();
void pause(); // GUI: завершает текущий callback, сохраняя backend stream и его физический route.
void close();
bool needsRecovery();
bool retryReady() const;
@@ -23,6 +24,10 @@ public:
void retryNow();
bool initialized() const { return m_device != nullptr; }
ma_device* device() const { return m_device; }
int64_t callbackTimeUs() const { return m_callbackTime.load(std::memory_order_relaxed); }
bool timingMeasured() const { return m_timingMeasured.load(std::memory_order_relaxed); }
uint64_t route() const { return m_route.load(std::memory_order_relaxed); }
bool captureMissing() const { return m_captureMissing.load(std::memory_order_relaxed); }
private:
static void data(ma_device* device, void* out, const void* in, ma_uint32 frames);
static void notification(const ma_device_notification* event);
@@ -31,6 +36,12 @@ private:
const char* m_role = "audio";
QElapsedTimer m_clock;
std::atomic<uint64_t> m_frames{0};
std::atomic<int64_t> m_callbackTime{0};
std::atomic<bool> m_timingMeasured{false};
std::atomic<uint64_t> m_route{0};
std::atomic<bool> m_captureMissing{false};
uint64_t m_reportedRoute = 0;
int m_reportedTiming = -1;
std::atomic<bool> m_suspended{false};
uint64_t m_seenFrames = 0;
qint64 m_progressAt = 0, m_retryAt = 0;
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include <array>
#include <atomic>
#include <cstddef>
#include <cstdint>
/* Ограниченная очередь с несколькими producers. Producer не меняет позицию consumer.
* Резервирование и публикация разделены sequence каждого слота; callback не ждёт занятый слот.
* reset допустим только до запуска всех участников. Последние 8 слотов зарезервированы для команд. */
template<class T, size_t Capacity> class AudioEventQueue {
static_assert(Capacity > 8);
struct Slot { std::atomic<uint64_t> sequence{0}; T value{}; };
std::array<Slot, Capacity> m_slots;
alignas(64) std::atomic<uint64_t> m_write{0};
alignas(64) std::atomic<uint64_t> m_read{0};
public:
AudioEventQueue() { reset(); }
void reset() {
m_write = 0; m_read = 0;
for (size_t i = 0; i < Capacity; ++i) m_slots[i].sequence = i;
}
bool push(const T& value, bool control = false) {
uint64_t position = m_write.load(std::memory_order_relaxed);
for (int attempt = 0; attempt < 8; ++attempt) {
uint64_t read = m_read.load(std::memory_order_acquire);
if (read > position) { position = m_write.load(std::memory_order_relaxed); continue; }
if (position - read >= Capacity - (control ? 0 : 8)) return false;
auto& slot = m_slots[position % Capacity];
int64_t difference = (int64_t)(slot.sequence.load(std::memory_order_acquire) - position);
if (difference < 0) return false;
if (difference == 0 && m_write.compare_exchange_weak(position, position + 1, std::memory_order_relaxed)) {
slot.value = value;
slot.sequence.store(position + 1, std::memory_order_release);
return true;
}
position = m_write.load(std::memory_order_relaxed);
}
return false;
}
bool pop(T& value) {
uint64_t position = m_read.load(std::memory_order_relaxed);
auto& slot = m_slots[position % Capacity];
if (slot.sequence.load(std::memory_order_acquire) != position + 1) return false;
value = slot.value;
slot.sequence.store(position + Capacity, std::memory_order_release);
m_read.store(position + 1, std::memory_order_release);
return true;
}
size_t size() const {
uint64_t read = m_read.load(std::memory_order_acquire);
uint64_t count = m_write.load(std::memory_order_acquire) - read;
return (size_t)(count > Capacity ? Capacity : count); // Приблизительный снимок, включая ещё не опубликованные слоты.
}
};
@@ -363,7 +363,7 @@ void AudioDeviceSettingsPage::onTestMicToggled() {
m_testRecorder->init();
QString id = m_inputCombo->currentData().toString();
m_testRecorder->setCaptureDevice(id);
m_testRecorder->startRecording();
m_testRecorder->startRecording(true);
m_micTestActive = true;
m_levelTimer->start();
m_testMicBtn->setText("Stop Test");
+60 -24
View File
@@ -12,29 +12,50 @@ extern "C" {
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <algorithm>
#include <chrono>
#include <exception>
#include <system_error>
void AudioRecorder::captureCallback(const void* pInput, unsigned int frameCount) {
if (!pInput || !frameCount || !m_recording.load()) return;
const int16_t* src = (const int16_t*)pInput;
size_t add = (size_t)frameCount * (size_t)m_channels;
if (m_compressor && m_captureCompressorEnabled) {
audio_compressor_push(m_compressor, src, add);
} else {
size_t cur = m_pcmBuffer.size();
m_pcmBuffer.resize(cur + add);
std::memcpy(m_pcmBuffer.data() + cur, src, add * sizeof(int16_t));
/* Аппаратный callback не выделяет память и не обрабатывает компрессор. */
void AudioRecorder::captureCallback(const void* input, unsigned frames) {
if (!input || !m_recording.load() || m_captureFailed.load()) return;
auto* pcm = static_cast<const int16_t*>(input);
for (unsigned offset = 0; offset < frames;) {
CaptureBlock block{}; block.frames = std::min(frames - offset, 960u);
memcpy(block.pcm, pcm + offset * m_channels, block.frames * m_channels * sizeof(int16_t));
if (!m_captureQueue.push(block)) { m_captureFailed = true; break; }
offset += block.frames;
}
m_wake.notify_one();
}
float sumSq = 0.0f;
for (unsigned int i = 0; i < frameCount * (unsigned int)m_channels; i++) {
float v = (float)src[i] / 32768.0f;
sumSq += v * v;
/* Только worker изменяет PCM и compressor; GUI получает атомарные метрики. */
void AudioRecorder::recordLoop() {
try {
while (m_workerRunning || m_captureQueue.size()) {
CaptureBlock block{};
if (!m_captureQueue.pop(block)) {
std::unique_lock lock(m_waitMutex);
m_wake.wait_for(lock, std::chrono::milliseconds(5));
continue;
}
if (m_captureFailed) continue;
size_t count = (size_t)block.frames * m_channels;
if (!m_meterOnly) {
if (m_compressor && m_captureCompressorEnabled) {
if (audio_compressor_push(m_compressor, block.pcm, count) != 0) { m_captureFailed = true; continue; }
} else m_pcmBuffer.insert(m_pcmBuffer.end(), block.pcm, block.pcm + count);
}
double sum = 0;
for (size_t i = 0; i < count; ++i) { double value = block.pcm[i] / 32768.0; sum += value * value; }
m_peakLevel = (float)std::sqrt(sum / count);
m_recordedFrames.fetch_add(block.frames, std::memory_order_relaxed);
}
} catch (const std::exception& error) {
m_captureFailed = true;
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "recorder: worker failed reason=%s", error.what());
}
float rms = sqrtf(sumSq / (float)(frameCount * m_channels));
m_peakLevel = rms;
m_recordedFrames.fetch_add(frameCount, std::memory_order_relaxed);
}
AudioRecorder::AudioRecorder(QObject* parent)
@@ -90,17 +111,26 @@ void AudioRecorder::shutdown() {
m_initialized = false;
}
void AudioRecorder::startRecording() {
void AudioRecorder::startRecording(bool meterOnly) {
if (m_recording) return;
if (!m_initialized) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "recorder: not initialized"); return; }
m_pcmBuffer.clear();
m_pcmBuffer.clear(); m_meterOnly = meterOnly;
m_captureQueue.reset(); m_captureFailed = false;
m_recordedFrames = 0; m_elapsedMs = 0; m_peakLevel = 0;
m_captureCompressorEnabled = m_compressorEnabled;
m_captureCompressorEnabled = !meterOnly && m_compressorEnabled;
if (m_compressor) {
audio_compressor_configure(m_compressor, &m_compressorConfig);
if (audio_compressor_configure(m_compressor, &m_compressorConfig) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "recorder: cannot configure compressor"); return;
}
audio_compressor_set_enabled(m_compressor, m_captureCompressorEnabled);
audio_compressor_reset(m_compressor);
}
m_workerRunning = true;
try { m_recordWorker = std::thread(&AudioRecorder::recordLoop, this); }
catch (const std::system_error& error) {
m_workerRunning = false;
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "recorder: worker start failed reason=%s", error.what()); return;
}
m_recording = true;
m_capture.retryNow();
recoverCapture();
@@ -111,6 +141,10 @@ void AudioRecorder::startRecording() {
void AudioRecorder::recoverCapture() {
if (!m_recording) return;
if (m_captureFailed) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "recorder: capture loss or processing failure; recording rejected");
stopRecording(); return;
}
if (m_capture.needsRecovery()) { m_capture.close(); m_durationTimer.stop(); m_capture.retryLater(); }
if (!m_capture.retryReady()) return;
ma_context* context = SoundManager::instance()->context();
@@ -137,6 +171,8 @@ void AudioRecorder::stopRecording() {
if (!m_recording.exchange(false)) return;
m_recoveryTimer.stop(); m_durationTimer.stop();
m_capture.close();
m_workerRunning = false; m_wake.notify_one();
if (m_recordWorker.joinable()) m_recordWorker.join();
m_elapsedMs = (int)(m_recordedFrames.load() * 1000 / m_sampleRate);
int duration = m_elapsedMs;
size_t samples = m_compressor && m_captureCompressorEnabled ? audio_compressor_output_size(m_compressor) : m_pcmBuffer.size();
@@ -151,7 +187,7 @@ int AudioRecorder::durationMs() const {
/* Worker получает владельца буфера; новая запись имеет отдельное состояние. */
bool AudioRecorder::takeRecording(struct attachment_send_req* request) {
if (!request || m_recording) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "recorder: cannot detach active recording"); return false; }
if (!request || m_recording || m_meterOnly || m_captureFailed) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "recorder: cannot detach active recording"); return false; }
if (m_compressor && m_captureCompressorEnabled) {
auto* replacement = audio_compressor_clone_config(m_compressor);
if (!replacement) return false;
+13 -1
View File
@@ -6,6 +6,10 @@
#include <vector>
#include <cstdint>
#include "audio_device.h"
#include "audio_event_queue.h"
#include <thread>
#include <mutex>
#include <condition_variable>
extern "C" {
#include "audio_compressor.h"
}
@@ -26,7 +30,7 @@ public:
void setCaptureDevice(const QString& id) { m_captureId = id; m_customDevice = true; }
void startRecording();
void startRecording(bool meterOnly = false);
void stopRecording();
bool isRecording() const { return m_recording; }
@@ -49,6 +53,14 @@ private:
QTimer m_recoveryTimer;
void recoverCapture();
void captureCallback(const void* input, unsigned int frames);
void recordLoop();
struct CaptureBlock { unsigned frames = 0; int16_t pcm[1920]{}; };
AudioEventQueue<CaptureBlock, 32> m_captureQueue;
std::thread m_recordWorker;
std::mutex m_waitMutex;
std::condition_variable m_wake;
std::atomic<bool> m_workerRunning{false}, m_captureFailed{false};
bool m_meterOnly = false;
bool m_initialized = false;
std::atomic<bool> m_recording{false};
int m_sampleRate = 48000;
+39 -64
View File
@@ -4,6 +4,7 @@
#include "../transport/audio_diagnostics.h"
extern "C" {
#include "call/call_audio.h"
#include "chat/chat_setting.h"
#include "debug_config.h"
}
#include <cstring>
@@ -24,7 +25,7 @@ CallAudioEngine::CallAudioEngine() {
connect(&m_recoveryTimer, &QTimer::timeout, this, &CallAudioEngine::recoverDevices);
connect(SoundManager::instance(), &SoundManager::contextAboutToReset, this, &CallAudioEngine::pauseDevices);
connect(SoundManager::instance(), &SoundManager::contextRestored, this, [this]() {
m_capture.retryNow(); m_playback.retryNow();
m_capture.retryNow();
if (m_active) recoverDevices();
});
}
@@ -32,100 +33,90 @@ CallAudioEngine::CallAudioEngine() {
int CallAudioEngine::start(quint64 callId) {
if (m_active && m_callId == callId && !m_ending) return 0;
stop();
if (!gui_bridge_get_inst() || call_audio_start(callId) != 0) {
if (!gui_bridge_get_inst() || call_audio_start_prepared(callId) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "CallAudio: cannot start call=%016llx", (unsigned long long)callId);
return -1;
}
++m_generation;
m_callId = callId;
m_captureBuf.assign(CALL_AUDIO_FRAME_SAMPLES, 0);
m_capturePos = 0;
if (!m_voice.start(VoiceAudioIo::Kind::Call, callId)) { call_audio_release(callId); m_callId = 0; return -1; }
m_muted = false;
m_ending = false;
m_active = true;
m_capture.retryNow(); m_playback.retryNow();
m_capture.retryNow();
recoverDevices();
m_recoveryTimer.start();
DEBUG_INFO(DEBUG_CATEGORY_CALL, "CallAudio: session started call=%016llx", (unsigned long long)callId);
return 0;
}
bool CallAudioEngine::openDevice(bool playback) {
AudioDevice& device = playback ? m_playback : m_capture;
ma_context* context = SoundManager::instance()->context();
bool CallAudioEngine::openCapture() {
auto* context = SoundManager::instance()->context();
if (!context) return false;
ma_device_config config = ma_device_config_init(playback ? ma_device_type_playback : ma_device_type_capture);
config.capture.format = config.playback.format = ma_format_s16;
config.capture.channels = config.playback.channels = 1;
config.sampleRate = CALL_AUDIO_SAMPLE_RATE;
ma_device_config config = ma_device_config_init(ma_device_type_capture);
config.capture.format = ma_format_s16; config.capture.channels = 1; config.sampleRate = CALL_AUDIO_SAMPLE_RATE;
ma_device_id id;
bool selected = SoundManager::deviceId(playback ? m_playbackId : m_captureId, &id);
if (selected) {
if (playback) config.playback.pDeviceID = &id;
else config.capture.pDeviceID = &id;
}
auto callback = [this](const void* in, void* out, unsigned int frames) { dataCallback(in, out, frames); };
const char* role = playback ? "call-playback" : "call-capture";
bool selected = SoundManager::deviceId(m_captureId, &id);
if (selected) config.capture.pDeviceID = &id;
auto callback = [this](const void* in, void*, unsigned frames) { dataCallback(in, nullptr, frames); };
auto attempt = [&]() {
if (!playback) m_capturePos = 0;
bool ready = device.init(context, config, role, m_callId.load(), callback) && device.start();
if (!ready) call_audio_reset_io(m_callId.load(), playback);
return ready;
if (!m_capture.init(context, config, "call-capture", m_callId.load(), callback)) return false;
bool aec = chat_setting_get_int(gui_bridge_get_inst(), "aec_enabled", 1) != 0;
if (!m_voice.startCapture(1, aec)) { m_capture.close(); return false; }
m_voice.setMuted(m_muted.load());
if (m_capture.start()) return true;
m_voice.stopCapture(); call_audio_reset_io(m_callId.load(), 0);
return false;
};
if (attempt()) return true;
if (!selected) return false;
DEBUG_WARN(DEBUG_CATEGORY_CALL, "CallAudio: %s selected device unavailable, trying default", role);
config.capture.pDeviceID = config.playback.pDeviceID = nullptr;
DEBUG_WARN(DEBUG_CATEGORY_CALL, "CallAudio: selected input unavailable, trying default");
config.capture.pDeviceID = nullptr;
return attempt();
}
void CallAudioEngine::recoverDevices() {
if (!m_active) return;
if (m_playback.needsRecovery()) {
m_playback.close();
call_audio_reset_io(m_callId.load(), 1);
m_playback.retryLater();
auto* manager = SoundManager::instance();
if (!m_outputAttached && manager->context()) m_outputAttached = manager->attachVoice(&m_voice, m_playbackId);
if (!m_ending && ((m_capture.initialized() && !m_voice.captureRunning()) || m_capture.needsRecovery())) {
m_capture.close(); m_voice.stopCapture();
call_audio_reset_io(m_callId.load(), 0); m_capture.retryLater();
}
if (!m_ending && m_capture.needsRecovery()) {
m_capture.close();
m_capturePos = 0;
call_audio_reset_io(m_callId.load(), 0);
m_capture.retryLater();
}
if (m_playback.retryReady()) openDevice(true);
if (!m_ending && m_capture.retryReady()) openDevice(false);
if (!m_ending && m_capture.retryReady()) openCapture();
if (!m_ending) m_voice.setAecEnabled(chat_setting_get_int(gui_bridge_get_inst(), "aec_enabled", 1) != 0);
}
void CallAudioEngine::pauseDevices() {
m_capture.close(); m_playback.close();
m_capturePos = 0;
if (m_active) call_audio_reset_io(m_callId.load(), 1);
m_capture.close(); m_voice.stopCapture();
SoundManager::instance()->detachVoice(&m_voice); m_outputAttached = false;
m_voice.resetPlayback();
}
void CallAudioEngine::setMuted(bool muted) {
if (m_muted.exchange(muted) == muted) return;
m_voice.setMuted(muted);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "CallAudio: mute=%d", muted);
}
void CallAudioEngine::setPlaybackDevice(const QString& id) {
if (id == m_playbackId) return;
m_playbackId = id;
m_playback.close(); m_playback.retryNow();
if (m_active) { call_audio_reset_io(m_callId.load(), 1); recoverDevices(); }
SoundManager::instance()->detachVoice(&m_voice); m_outputAttached = false;
if (m_active) recoverDevices();
}
void CallAudioEngine::setCaptureDevice(const QString& id) {
if (id == m_captureId) return;
m_captureId = id;
m_capture.close(); m_capture.retryNow();
m_capturePos = 0;
m_capture.close(); m_voice.stopCapture(); m_capture.retryNow();
if (m_active) { call_audio_reset_io(m_callId.load(), 0); recoverDevices(); }
}
void CallAudioEngine::beginEnd(quint64 callId) {
if (!m_active || m_callId != callId || m_ending) return;
m_ending = true;
m_capture.close();
m_capture.close(); m_voice.stopCapture();
call_audio_begin_end(callId);
uint64_t generation = m_generation;
QTimer::singleShot(500, this, [this, generation]() { if (m_generation == generation) stop(); });
@@ -135,7 +126,7 @@ void CallAudioEngine::stop() {
++m_generation;
m_recoveryTimer.stop();
m_active = false;
pauseDevices();
pauseDevices(); m_voice.stop();
uint64_t id = m_callId.exchange(0);
if (id) call_audio_release(id);
m_muted = false;
@@ -143,22 +134,6 @@ void CallAudioEngine::stop() {
if (id) DEBUG_INFO(DEBUG_CATEGORY_CALL, "CallAudio: session stopped call=%016llx", (unsigned long long)id);
}
void CallAudioEngine::dataCallback(const void* in, void* out, unsigned int frames) {
if (in) {
const auto* pcm = static_cast<const int16_t*>(in);
bool muted = m_muted.load();
for (unsigned int i = 0; i < frames; ++i) {
m_captureBuf[m_capturePos++] = pcm[i];
if (m_capturePos == CALL_AUDIO_FRAME_SAMPLES) {
int result = call_audio_feed_pcm_muted(m_callId.load(), m_captureBuf.data(), CALL_AUDIO_FRAME_SAMPLES, muted);
if (result != 0) audio_diag_event(m_capture.device(), AUDIO_DIAG_ERROR, AUDIO_DIAG_FEED, 1, result);
m_capturePos = 0;
}
}
}
if (out) {
auto* pcm = static_cast<int16_t*>(out);
int n = call_audio_pull_pcm(m_callId.load(), pcm, (int)frames);
if (n < (int)frames) std::memset(pcm + n, 0, (frames - n) * sizeof(*pcm));
}
void CallAudioEngine::dataCallback(const void* in, void*, unsigned int frames) {
if (in) m_voice.capture(static_cast<const int16_t*>(in), frames, m_capture.callbackTimeUs(), m_capture.route(), m_capture.captureMissing());
}
+12 -12
View File
@@ -6,16 +6,16 @@
#include <vector>
#include <QTimer>
#include "audio_device.h"
#include "voice_audio_io.h"
struct ma_device;
/* Аудио-контур звонка (desktop): только miniaudio I/O + захват/воспроизведение.
* Весь аудиопроцессинг (Opus, джиттер-буфер, time-stretch, тоны) — в сервисном
* слое src/call/call_audio.c; сюда приходит/отсюда уходит финальный PCM.
*
* Потоки:
* - независимые callbacks capture → call_audio_feed_pcm и call_audio_pull_pcm → playback;
* - GUI-поток: start()/beginEnd()/stop() по ACCEPTED/ENDED. */
/* Desktop-контур звонка: GUI владеет capture device и жизненным циклом.
* Hardware callback копирует PCM в ограниченную очередь VoiceAudioIo.
* Capture worker выполняет AEC и упорядочивает PCM/PTT/mute; независимый RX worker
* получает готовый голос из сервисного слоя. SoundManager смешивает его со всеми
* звуками приложения на том же выходе и возвращает итоговый stereo reference.
* Stop: close capture → join capture worker → detach output → join RX → release core. */
class CallAudioEngine : public QObject {
Q_OBJECT
public:
@@ -42,12 +42,12 @@ public:
QString playbackDevice() const { return m_playbackId; }
void setCaptureDevice(const QString& id);
/* аудио-поток: miniaudio data callback (захват + воспроизведение) */
/* Hardware capture callback: без DSP и растущих буферов. */
void dataCallback(const void* in, void* out, unsigned int frames);
private:
CallAudioEngine();
bool openDevice(bool playback);
bool openCapture();
void recoverDevices();
void pauseDevices();
@@ -58,11 +58,11 @@ private:
QString m_playbackId; /* только GUI-поток */
QString m_captureId;
AudioDevice m_capture, m_playback;
AudioDevice m_capture;
VoiceAudioIo m_voice;
bool m_outputAttached = false;
QTimer m_recoveryTimer;
uint64_t m_generation = 0;
bool m_ending = false;
std::vector<int16_t> m_captureBuf; /* 960 samples, аудио-поток */
int m_capturePos = 0;
};
+23 -72
View File
@@ -26,11 +26,11 @@ RadioAudioEngine::RadioAudioEngine() {
auto* manager = SoundManager::instance();
if (m_captureId != manager->captureDevice()) {
m_captureId = manager->captureDevice();
m_capture.close(); if (m_active) radio_audio_capture_stop(); m_capture.retryNow();
m_capture.close(); m_voice.stopCapture(); if (m_active) radio_audio_capture_stop(); m_capture.retryNow();
}
if (m_playbackId != manager->playbackDevice()) {
m_playbackId = manager->playbackDevice();
m_playback.close(); if (m_active) radio_audio_reset_playback(m_groupId.load()); m_playback.retryNow();
manager->detachVoice(&m_voice); m_outputAttached = false;
}
recoverDevices();
});
@@ -38,7 +38,7 @@ RadioAudioEngine::RadioAudioEngine() {
connect(&m_recoveryTimer, &QTimer::timeout, this, &RadioAudioEngine::recoverDevices);
connect(SoundManager::instance(), &SoundManager::contextAboutToReset, this, &RadioAudioEngine::pauseDevices);
connect(SoundManager::instance(), &SoundManager::contextRestored, this, [this]() {
m_capture.retryNow(); m_playback.retryNow();
m_capture.retryNow();
recoverDevices();
});
}
@@ -52,9 +52,10 @@ int RadioAudioEngine::start(quint64 groupId) {
return -1;
}
m_groupId = groupId;
if (!m_voice.start(VoiceAudioIo::Kind::Radio, groupId)) { radio_audio_stop(); m_groupId = 0; return -1; }
m_wantStereo = chat_setting_get_int(inst, "radio_capture_stereo", 0) != 0;
m_active = true;
m_capture.retryNow(); m_playback.retryNow();
m_capture.retryNow();
recoverDevices();
m_recoveryTimer.start();
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "RadioAudio: listening group=%016llx", (unsigned long long)groupId);
@@ -77,65 +78,44 @@ void RadioAudioEngine::openCapture() {
if (attempt) DEBUG_WARN(DEBUG_CATEGORY_RADIO, "RadioAudio: selected input unavailable, trying default ch=%d", channels);
if (!m_capture.init(context, config, "radio-capture", m_groupId.load(), callback)) continue;
m_captureChannels = channels;
m_captureBuf.assign(RADIO_AUDIO_FRAME_SAMPLES * channels, 0);
m_capturePos = 0;
auto* inst = gui_bridge_get_inst();
int vad = chat_setting_get_int(inst, "radio_vad_enabled", 0);
if (radio_audio_capture_start(m_groupId.load(), channels, vad) != 0) {
m_capture.close(); m_capture.retryLater(); return;
}
m_vadMode = radio_audio_vad_mode() != 0;
if (!m_capture.start()) { radio_audio_capture_stop(); continue; }
if (m_talking) radio_audio_talk_begin(m_groupId.load());
if (!m_voice.startCapture(channels, chat_setting_get_int(inst, "aec_enabled", 1) != 0)) {
m_capture.close(); radio_audio_capture_stop(); return;
}
if (m_talking) m_voice.setPtt(true);
if (!m_capture.start()) { m_voice.stopCapture(); radio_audio_capture_stop(); continue; }
if (channels == 1 && m_wantStereo) DEBUG_WARN(DEBUG_CATEGORY_RADIO, "RadioAudio: stereo input unavailable, using mono");
return;
}
}
}
void RadioAudioEngine::openPlayback() {
auto* context = SoundManager::instance()->context();
if (!context) return;
ma_device_config config = ma_device_config_init(ma_device_type_playback);
config.playback.format = ma_format_s16;
config.playback.channels = 2;
config.sampleRate = RADIO_AUDIO_SAMPLE_RATE;
auto callback = [this](const void*, void* out, unsigned int frames) { playbackCallback(out, frames); };
ma_device_id id;
bool selected = SoundManager::deviceId(m_playbackId, &id);
if (selected) config.playback.pDeviceID = &id;
if (m_playback.init(context, config, "radio-playback", m_groupId.load(), callback) && m_playback.start()) return;
if (!selected) return;
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "RadioAudio: selected output unavailable, trying default");
config.playback.pDeviceID = nullptr;
if (m_playback.init(context, config, "radio-playback", m_groupId.load(), callback)) m_playback.start();
}
void RadioAudioEngine::recoverDevices() {
if (!m_active) return;
if (m_capture.needsRecovery()) {
m_capture.close(); radio_audio_capture_stop();
m_capturePos = 0; m_capture.retryLater();
auto* manager = SoundManager::instance();
if (!m_outputAttached && manager->context()) m_outputAttached = manager->attachVoice(&m_voice, m_playbackId);
if ((m_capture.initialized() && !m_voice.captureRunning()) || m_capture.needsRecovery()) {
m_capture.close(); m_voice.stopCapture(); radio_audio_capture_stop(); m_capture.retryLater();
}
if (m_playback.needsRecovery()) {
m_playback.close(); radio_audio_reset_playback(m_groupId.load()); m_playback.retryLater();
}
if (m_playback.retryReady()) openPlayback();
if (m_capture.retryReady()) openCapture();
m_voice.setAecEnabled(chat_setting_get_int(gui_bridge_get_inst(), "aec_enabled", 1) != 0);
}
void RadioAudioEngine::pauseDevices() {
m_capture.close(); m_playback.close();
m_capturePos = 0;
if (m_active) {
radio_audio_capture_stop();
radio_audio_reset_playback(m_groupId.load());
}
m_capture.close(); m_voice.stopCapture();
SoundManager::instance()->detachVoice(&m_voice); m_outputAttached = false;
if (m_active) radio_audio_capture_stop();
m_voice.resetPlayback();
}
void RadioAudioEngine::stop() {
m_recoveryTimer.stop();
m_capture.close(); m_playback.close();
pauseDevices(); m_voice.stop();
uint64_t group = m_groupId.exchange(0);
bool talking = m_talking.exchange(false);
m_buttonTalking = m_hotkeyTalking = false;
@@ -164,41 +144,12 @@ void RadioAudioEngine::setHotkeyTalking(bool pressed) {
void RadioAudioEngine::updateTalking() {
bool talking = m_active && (m_buttonTalking || m_hotkeyTalking);
if (m_talking.exchange(talking) == talking || !m_active) return;
if (talking) radio_audio_talk_begin(m_groupId.load());
else radio_audio_talk_end(m_groupId.load());
m_voice.setPtt(talking);
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "RadioAudio: PTT %s group=%016llx button=%d hotkey=%d",
talking ? "started" : "stopped", (unsigned long long)m_groupId.load(), m_buttonTalking, m_hotkeyTalking);
emit talkingChanged(talking);
}
void RadioAudioEngine::captureCallback(const void* in, unsigned int frames) {
const int16_t* in16 = (const int16_t*)in;
if (!in16) return;
/* VAD-режим: захват фидится постоянно (решение «говорить» — в radio_audio).
* Обычный PTT: только когда зажата кнопка. */
if (!m_vadMode.load() && !m_talking.load()) return;
/* захват: копим FRAME_SAMPLES * captureChannels → encode+send. */
int totalIn = (int)frames * m_captureChannels;
for (int i = 0; i < totalIn; i++) {
m_captureBuf[m_capturePos++] = in16[i];
if (m_capturePos >= (int)m_captureBuf.size()) {
int result = radio_audio_feed_pcm(m_groupId.load(), m_captureBuf.data(), (int)m_captureBuf.size());
if (result != 0) audio_diag_event(m_capture.device(), AUDIO_DIAG_ERROR, AUDIO_DIAG_FEED, 1, result);
m_capturePos = 0;
}
}
}
void RadioAudioEngine::playbackCallback(void* out, unsigned int frames) {
int16_t* out16 = (int16_t*)out;
/* воспроизведение: микшированный PCM из сервисного слоя (всегда стерео) */
unsigned int offset = 0;
while (offset < frames) {
unsigned int count = std::min(frames - offset, (unsigned int)RADIO_AUDIO_FRAME_SAMPLES);
int n = radio_audio_pull_pcm(m_groupId.load(), out16 + offset * 2, count * 2);
if (n < (int)count * 2) std::memset(out16 + offset * 2 + n, 0, (count * 2 - n) * sizeof(*out16));
offset += count;
}
if (in) m_voice.capture(static_cast<const int16_t*>(in), frames, m_capture.callbackTimeUs(), m_capture.route(), m_capture.captureMissing());
}
+13 -16
View File
@@ -5,18 +5,17 @@
#include <cstdint>
#include <vector>
#include "audio_device.h"
#include "voice_audio_io.h"
#include <QTimer>
struct ma_device;
/* Аудио-контур рации (desktop): только miniaudio I/O + захват/воспроизведение.
* Весь аудиопроцессинг (Opus, per-source декодеры, микшер) — в сервисном слое
* src/radio/radio_audio.c; сюда приходит/отсюда уходит финальный PCM.
*
* Потоки:
* - аудио-поток (miniaudio callback): capture → radio_audio_feed_pcm,
* radio_audio_pull_pcm → playback;
* - GUI-поток: start()/stop() по включению/выключению рации канала. */
/* Desktop-контур рации: GUI владеет capture device и жизненным циклом.
* Hardware callback копирует PCM в ограниченную очередь VoiceAudioIo.
* Capture worker выполняет AEC и упорядочивает PCM/PTT/mute; независимый RX worker
* получает готовый голос из сервисного слоя. SoundManager смешивает его со всеми
* звуками приложения на том же выходе и возвращает итоговый stereo reference.
* Stop: close capture → join capture worker → detach output → join RX → release core. */
class RadioAudioEngine : public QObject {
Q_OBJECT
public:
@@ -37,9 +36,8 @@ public:
bool isActive() const { return m_active.load(); }
quint64 groupId() const { return m_groupId.load(); }
/* аудио-поток: miniaudio callbacks (захват и воспроизведение раздельно) */
/* Hardware capture callback: без DSP и растущих буферов. */
void captureCallback(const void* in, unsigned int frames);
void playbackCallback(void* out, unsigned int frames);
signals:
void talkingChanged(bool talking); // GUI-поток, суммарное состояние кнопки и хоткея
@@ -51,19 +49,18 @@ private:
void pauseDevices();
void recoverDevices();
void openCapture();
void openPlayback();
std::atomic<bool> m_active{false};
std::atomic<bool> m_talking{false};
std::atomic<bool> m_vadMode{false}; /* VAD авто-PTT: фидим PCM постоянно */
std::atomic<uint64_t> m_groupId{0};
AudioDevice m_capture, m_playback;
AudioDevice m_capture;
VoiceAudioIo m_voice;
bool m_outputAttached = false;
QTimer m_recoveryTimer;
bool m_wantStereo = false;
QString m_captureId, m_playbackId;
std::vector<int16_t> m_captureBuf; /* FRAME_SAMPLES * m_captureChannels, аудио-поток */
int m_capturePos = 0;
int m_captureChannels = 1; /* 1=моно, 2=стерео — по настройке/возможности */
int m_captureChannels = 1;
};
+259 -45
View File
@@ -1,11 +1,13 @@
#include "miniaudio.h"
#include "sound_manager.h"
#include "voice_audio_io.h"
#include "../transport/audio_diagnostics.h"
#include <QFile>
#include <QDebug>
#include <algorithm>
#include <cstring>
#include <cmath>
#include "../../lib/debug_config.h"
SoundManager* SoundManager::instance() {
@@ -20,6 +22,14 @@ SoundManager::~SoundManager() {
extern "C" int audio_context_failed(ma_context* context);
static uint64_t pulse_output_key(const QString& key) {
ma_device_id id{};
if (!SoundManager::deviceId(key, &id)) return 0;
uint64_t hash = UINT64_C(14695981039346656037);
for (const char* name = id.pulse; *name; ++name) { hash ^= (unsigned char)*name; hash *= UINT64_C(1099511628211); }
return hash;
}
SoundManager::SoundManager() {
m_clock.start();
m_recoveryTimer.setInterval(200);
@@ -65,11 +75,15 @@ bool SoundManager::openContext() {
m_backendSelected = true;
m_contextRetryDelay = 250;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "SoundManager: context ready backend=%s", ma_get_backend_name(m_backend));
m_resetting = false;
emit contextRestored();
return true;
}
void SoundManager::openOutput() {
for (auto* voice : m_voices) if (voice) voice->resetPlayback();
if (m_output.initialized() && m_output.start()) return;
m_restoreSelections = true;
ma_device_config config = ma_device_config_init(ma_device_type_playback);
config.playback.format = ma_format_f32;
config.playback.channels = 2;
@@ -77,33 +91,213 @@ void SoundManager::openOutput() {
ma_device_id id;
bool selected = deviceId(m_playbackId, &id);
if (selected) config.playback.pDeviceID = &id;
auto callback = [this](const void*, void* out, unsigned int frames) {
ma_engine_read_pcm_frames(m_engine, out, frames, nullptr);
auto callback = [this, previousRoute = uint64_t(0)](const void*, void* out, unsigned int frames) mutable {
uint64_t route = m_output.route();
if (previousRoute && route != previousRoute) for (auto* voice : m_voices) if (voice) voice->resetPlayback();
previousRoute = route;
m_renderErrors.fetch_add(renderOutput(m_engine, m_voices, static_cast<float*>(out), frames, m_output.callbackTimeUs()));
};
if (m_output.init(m_context, config, "sound-engine", 0, callback) && m_output.start()) return;
if (m_output.init(m_context, config, "sound-engine", 0, callback) && m_output.start()) {
m_actualOutput = resolveOutput(m_playbackId); return;
}
if (!selected) return;
DEBUG_WARN(DEBUG_CATEGORY_CALL, "SoundManager: selected output unavailable, trying default");
config.playback.pDeviceID = nullptr;
if (m_output.init(m_context, config, "sound-engine", 0, callback)) m_output.start();
if (m_output.init(m_context, config, "sound-engine", 0, callback) && m_output.start()) m_actualOutput = resolveOutput({});
}
void SoundManager::recoverAudio() {
if (!m_initialized) return;
unsigned errors = m_renderErrors.exchange(0);
if (errors) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "audio: mixer read failures=%u; output replaced with silence", errors);
for (auto it = m_pcmSessions.begin(); it != m_pcmSessions.end();) {
if (it->second->transient && !isPcmPlaying(it->first)) it = m_pcmSessions.erase(it);
else ++it;
}
if (m_context && audio_context_failed(m_context)) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "SoundManager: context failed, closing dependent devices");
m_resetting = true;
emit contextAboutToReset();
m_output.close();
for (auto& [key, route] : m_voiceRoutes) route->device.close();
ma_context_uninit(m_context); delete m_context; m_context = nullptr;
m_contextRetryAt = m_clock.elapsed() + 250;
}
if (!m_context && m_clock.elapsed() >= m_contextRetryAt) openContext();
if (!m_context) return;
if (m_output.needsRecovery()) { m_output.close(); m_output.retryLater(); }
if (m_output.retryReady()) openOutput();
if (m_output.needsRecovery()) {
m_output.close();
for (auto* voice : m_voices) if (voice) voice->resetPlayback();
m_output.retryLater();
}
bool reopened = m_output.retryReady();
if (reopened) openOutput();
for (auto& [key, route] : m_voiceRoutes) {
if (route->device.needsRecovery()) {
route->device.close();
for (auto* voice : route->voices) if (voice) voice->resetPlayback();
route->device.retryLater();
}
if (route->device.retryReady()) openVoiceRoute(*route);
}
mergeVoiceRoutes();
if (m_restoreSelections && m_output.initialized() && (m_backend != ma_backend_pulseaudio || m_output.route())) {
m_restoreSelections = false;
auto voices = m_voices;
for (auto* voice : voices) if (voice) {
QString wanted = m_voiceSelections.at(voice);
bool same = m_backend == ma_backend_pulseaudio ? pulse_output_key(wanted) == m_output.route() : wanted == m_actualOutput;
if (!wanted.isEmpty() && wanted == resolveOutput(wanted) && !same) attachVoice(voice, wanted);
}
}
}
/* Последнее смешивание и ограничение: тот же PCM передаётся устройству и всем AEC этого выхода. */
unsigned SoundManager::renderOutput(ma_engine* engine, const std::array<VoiceAudioIo*, 2>& voices,
float* out, unsigned frames, int64_t firstTimeUs) {
unsigned errors = 0;
for (unsigned offset = 0; offset < frames;) {
unsigned count = std::min(frames - offset, 960u);
float* destination = out + offset * 2;
std::fill_n(destination, count * 2, 0.0f);
if (engine && ma_engine_read_pcm_frames(engine, destination, count, nullptr) != MA_SUCCESS) {
++errors; std::fill_n(destination, count * 2, 0.0f);
}
for (auto* voice : voices) if (voice) voice->mix(destination, count);
for (unsigned i = 0; i < count * 2; ++i) {
float value = destination[i];
destination[i] = std::isfinite(value) ? std::clamp(value, -1.0f, 1.0f) : 0.0f;
}
for (auto* voice : voices) if (voice)
voice->reference(destination, count, firstTimeUs + (int64_t)offset * 1000000 / 48000);
offset += count;
}
return errors;
}
/* Default и явный ID одного устройства получают один микшер. */
QString SoundManager::resolveOutput(const QString& requested) {
auto devices = enumeratePlaybackDevices();
for (const auto& device : devices) if (device.id == requested) return requested;
for (const auto& device : devices) if (device.isDefault) return device.id;
return {};
}
bool SoundManager::openVoiceRoute(VoiceRoute& route) {
if (!m_context || m_resetting) return false;
for (auto* voice : route.voices) if (voice) voice->resetPlayback();
if (route.device.initialized() && route.device.start()) return true;
ma_device_config config = ma_device_config_init(ma_device_type_playback);
config.playback.format = ma_format_f32; config.playback.channels = 2; config.sampleRate = 48000;
ma_device_id id;
bool selected = deviceId(route.wanted, &id);
if (selected) config.playback.pDeviceID = &id;
auto callback = [this, &route, previousRoute = uint64_t(0)](const void*, void* out, unsigned frames) mutable {
uint64_t physical = route.device.route();
if (previousRoute && physical != previousRoute) for (auto* voice : route.voices) if (voice) voice->resetPlayback();
previousRoute = physical;
if (physical && physical == m_output.route()) {
std::fill_n(static_cast<float*>(out), frames * 2, 0.0f); return; // GUI объединит совпавшие маршруты.
}
renderOutput(nullptr, route.voices, static_cast<float*>(out), frames, route.device.callbackTimeUs());
};
if (route.device.init(m_context, config, "voice-output", 0, callback) && route.device.start()) {
route.actual = resolveOutput(route.wanted); return true;
}
if (!selected) return false;
DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio: voice output unavailable, trying default");
config.playback.pDeviceID = nullptr;
if (route.device.init(m_context, config, "voice-output", 0, callback) && route.device.start()) {
route.actual = resolveOutput({}); return true;
}
return false;
}
bool SoundManager::attachVoice(VoiceAudioIo* voice, const QString& output) {
detachVoice(voice);
if (!voice || !m_initialized || !m_context || m_resetting) return false;
size_t attached = std::count_if(m_voices.begin(), m_voices.end(), [](auto* v) { return v != nullptr; });
for (const auto& [key, route] : m_voiceRoutes)
attached += std::count_if(route->voices.begin(), route->voices.end(), [](auto* v) { return v != nullptr; });
if (attached >= 2) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: only one call and one radio source are supported"); return false; }
QString key = resolveOutput(output);
QString root = m_actualOutput.isEmpty() ? resolveOutput(m_playbackId) : m_actualOutput;
bool same = key == root;
if (m_backend == ma_backend_pulseaudio && m_output.route()) same = pulse_output_key(key) == m_output.route();
if (same) {
m_output.pause();
auto slot = std::find(m_voices.begin(), m_voices.end(), nullptr);
if (slot == m_voices.end()) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: too many voice sources on output"); openOutput(); return false; }
*slot = voice; voice->resetPlayback();
openOutput();
} else {
auto& route = m_voiceRoutes[key];
if (!route) { route = std::make_unique<VoiceRoute>(); route->wanted = key; }
route->device.pause();
auto slot = std::find(route->voices.begin(), route->voices.end(), nullptr);
if (slot == route->voices.end()) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: too many voice sources on separate output"); openVoiceRoute(*route); return false; }
*slot = voice; voice->resetPlayback();
openVoiceRoute(*route);
mergeVoiceRoutes();
}
DEBUG_INFO(DEBUG_CATEGORY_CALL, "audio: voice attached to shared output default=%d", output.isEmpty());
m_voiceSelections[voice] = output;
return true;
}
void SoundManager::detachVoice(VoiceAudioIo* voice) {
if (!voice) return;
m_voiceSelections.erase(voice);
auto slot = std::find(m_voices.begin(), m_voices.end(), voice);
if (slot != m_voices.end()) {
m_output.pause(); *slot = nullptr;
if (m_initialized && m_context && !m_resetting) openOutput();
}
for (auto it = m_voiceRoutes.begin(); it != m_voiceRoutes.end();) {
auto& route = *it->second;
auto entry = std::find(route.voices.begin(), route.voices.end(), voice);
if (entry == route.voices.end()) { ++it; continue; }
route.device.pause(); *entry = nullptr;
if (std::all_of(route.voices.begin(), route.voices.end(), [](auto* v) { return !v; })) it = m_voiceRoutes.erase(it);
else { if (!m_resetting) openVoiceRoute(route); ++it; }
}
}
/* Fallback на уже используемый выход не оставляет два независимых референса одного динамика. */
void SoundManager::mergeVoiceRoutes() {
if (m_resetting || !m_output.initialized()) return;
for (auto first = m_voiceRoutes.begin(); first != m_voiceRoutes.end(); ++first) {
auto& target = *first->second;
for (auto next = std::next(first); next != m_voiceRoutes.end();) {
auto& source = *next->second;
uint64_t physical = target.device.route();
if (!physical || physical != source.device.route()) { ++next; continue; }
target.device.pause(); source.device.close();
for (auto* voice : source.voices) if (voice) {
auto slot = std::find(target.voices.begin(), target.voices.end(), nullptr);
if (slot == target.voices.end()) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: voice route merge capacity exceeded"); return; }
*slot = voice;
}
next = m_voiceRoutes.erase(next);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "audio: merged voice streams on physical route=%016llx", (unsigned long long)physical);
openVoiceRoute(target);
}
}
for (auto it = m_voiceRoutes.begin(); it != m_voiceRoutes.end();) {
auto& route = *it->second;
bool same = m_backend == ma_backend_pulseaudio ? m_output.route() && m_output.route() == route.device.route()
: route.actual == m_actualOutput;
if (!route.device.initialized() || !same) { ++it; continue; }
m_output.pause(); route.device.close();
for (auto* voice : route.voices) if (voice) {
auto slot = std::find(m_voices.begin(), m_voices.end(), nullptr);
if (slot == m_voices.end()) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: output merge capacity exceeded"); break; }
*slot = voice; voice->resetPlayback();
}
it = m_voiceRoutes.erase(it);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "audio: merged voice fallback with shared output");
openOutput();
}
}
void SoundManager::setDevices(const QString& output, const QString& input) {
@@ -111,7 +305,7 @@ void SoundManager::setDevices(const QString& output, const QString& input) {
bool outputChanged = output != m_playbackId;
m_playbackId = output; m_captureId = input;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "audio: device selection changed output_default=%d input_default=%d", output.isEmpty(), input.isEmpty());
if (outputChanged) { m_output.close(); m_output.retryNow(); }
if (outputChanged) { m_output.close(); m_actualOutput.clear(); m_output.retryNow(); }
emit devicesChanged();
recoverAudio();
}
@@ -137,6 +331,7 @@ bool SoundManager::createPcmNode(PcmSession& session, bool playing) {
return false;
}
session.sound = sound;
session.detached = false;
ma_sound_set_volume(sound, m_volume);
if (playing && ma_sound_start(sound) != MA_SUCCESS) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "playback: node start failed");
@@ -150,14 +345,11 @@ void SoundManager::shutdown() {
if (!m_initialized) return;
m_recoveryTimer.stop();
m_initialized = false;
m_resetting = true;
emit contextAboutToReset();
m_output.close();
m_output.close(); m_voiceRoutes.clear(); m_voices = {}; m_voiceSelections.clear();
m_pcmSessions.clear();
for (auto it = m_sounds.begin(); it != m_sounds.end(); ++it) {
if (it->sound) { ma_sound_uninit(it->sound); delete it->sound; }
if (it->decoder) { ma_decoder_uninit(it->decoder); delete it->decoder; }
}
m_sounds.clear();
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "SoundManager: before ma_engine_uninit engine=%p", (void*)m_engine);
@@ -197,47 +389,49 @@ SoundManager::EventSettings SoundManager::eventSettings(const QString& name) con
bool SoundManager::loadSound(const QString& name, const QString& filePath) {
if (!m_initialized) {
qWarning("SoundManager: not initialized, cannot load %s", qPrintable(name));
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "SoundManager: not initialized, cannot load '%s'", qPrintable(name));
return false;
}
if (m_sounds.contains(name)) {
qWarning("SoundManager: sound '%s' already loaded", qPrintable(name));
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "SoundManager: sound '%s' already loaded", qPrintable(name));
return false;
}
QFile f(filePath);
if (!f.open(QIODevice::ReadOnly)) {
qWarning("SoundManager: cannot open %s", qPrintable(filePath));
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "SoundManager: cannot open '%s'", qPrintable(filePath));
return false;
}
LoadedSound ls;
ls.mp3Data = f.readAll();
QByteArray encoded = f.readAll();
f.close();
ls.decoder = new ma_decoder;
ma_decoder_config decConfig = ma_decoder_config_init_default();
ma_result result = ma_decoder_init_memory(ls.mp3Data.constData(), ls.mp3Data.size(),
&decConfig, ls.decoder);
if (result != MA_SUCCESS) {
qWarning("SoundManager: ma_decoder_init_memory failed for %s (%d)", qPrintable(name), result);
delete ls.decoder;
/* Декодируем короткие сигналы при загрузке, до playback: hardware callback читает только PCM. */
ma_decoder_config decConfig = ma_decoder_config_init(ma_format_s16, 2, 48000);
ma_uint64 frames = 0; void* decoded = nullptr;
ma_result result = ma_decode_memory(encoded.constData(), encoded.size(), &decConfig, &frames, &decoded);
if (result != MA_SUCCESS || !frames) {
ma_free(decoded, &decConfig.allocationCallbacks);
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "SoundManager: decode '%s' failed result=%d frames=%llu",
qPrintable(name), result, (unsigned long long)frames);
return false;
}
ls.sound = new ma_sound;
result = ma_sound_init_from_data_source(m_engine, (ma_data_source*)&ls.decoder->ds,
0, NULL, ls.sound);
LoadedSound ls;
ls.pcm = std::make_shared<PcmSession>();
ls.pcm->frames = frames; ls.pcm->rate = 48000;
ma_audio_buffer_config bufferConfig = ma_audio_buffer_config_init(ma_format_s16, 2, frames, decoded, nullptr);
bufferConfig.sampleRate = 48000;
ls.pcm->buffer = new ma_audio_buffer{};
result = ma_audio_buffer_init_copy(&bufferConfig, ls.pcm->buffer);
ma_free(decoded, &decConfig.allocationCallbacks);
if (result != MA_SUCCESS) {
qWarning("SoundManager: sound init failed for %s (%d)", qPrintable(name), result);
ma_decoder_uninit(ls.decoder); delete ls.decoder;
delete ls.sound;
delete ls.pcm->buffer; ls.pcm->buffer = nullptr;
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "SoundManager: signal PCM init failed result=%d", result);
return false;
}
if (!createPcmNode(*ls.pcm, false)) return false;
m_sounds.insert(name, ls);
qDebug("SoundManager: loaded '%s' from %s (%lld bytes)",
qPrintable(name), qPrintable(filePath), (long long)ls.mp3Data.size());
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "SoundManager: predecoded '%s' frames=%llu rate=48000 ch=2",
qPrintable(name), (unsigned long long)frames);
return true;
}
@@ -254,12 +448,15 @@ void SoundManager::play(const QString& name) {
return;
}
ma_sound* s = it->sound;
auto& pcm = *it->pcm;
if (pcm.sound) { ma_sound_uninit(pcm.sound); delete pcm.sound; pcm.sound = nullptr; }
ma_audio_buffer_seek_to_pcm_frame(pcm.buffer, 0);
if (!createPcmNode(pcm, false)) return;
ma_sound* s = pcm.sound;
float effectiveVolume = m_volume * es.volume;
ma_sound_set_volume(s, effectiveVolume);
ma_sound_stop(s);
ma_sound_seek_to_pcm_frame(s, 0);
ma_sound_start(s);
if (ma_sound_start(s) != MA_SUCCESS) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "SoundManager: signal start failed '%s'", qPrintable(name));
}
void SoundManager::playLoop(const QString& name) {
@@ -278,12 +475,15 @@ void SoundManager::playLoop(const QString& name) {
return;
}
ma_sound* s = it->sound;
auto& pcm = *it->pcm;
if (pcm.sound) { ma_sound_uninit(pcm.sound); delete pcm.sound; pcm.sound = nullptr; }
ma_audio_buffer_seek_to_pcm_frame(pcm.buffer, 0);
if (!createPcmNode(pcm, false)) return;
ma_sound* s = pcm.sound;
ma_sound_stop(s);
ma_sound_seek_to_pcm_frame(s, 0);
ma_sound_set_volume(s, m_volume * es.volume);
ma_sound_set_looping(s, MA_TRUE);
ma_sound_start(s);
if (ma_sound_start(s) != MA_SUCCESS) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "SoundManager: loop start failed '%s'", qPrintable(name)); return; }
m_loopName = name;
}
@@ -292,7 +492,8 @@ void SoundManager::stopLoop() {
auto it = m_sounds.find(m_loopName);
m_loopName.clear();
if (it == m_sounds.end()) return;
ma_sound* s = it->sound;
ma_sound* s = it->pcm->sound;
if (!s) return;
ma_sound_stop(s);
ma_sound_set_looping(s, MA_FALSE);
ma_sound_seek_to_pcm_frame(s, 0);
@@ -336,13 +537,26 @@ SoundManager::PlaybackId SoundManager::playRawPcm(const int16_t* pcm, int frameC
void SoundManager::pausePcm(PlaybackId id) {
auto* session = pcmSession(id);
if (session && session->sound) ma_sound_stop(session->sound);
if (!session || !session->sound) return;
ma_result result = ma_sound_stop(session->sound);
/* Stop меняет флаг; detach также ждёт уже начатое чтение и фиксирует cursor. */
if (result == MA_SUCCESS && !session->detached) {
result = ma_node_detach_output_bus(session->sound, 0);
if (result == MA_SUCCESS) session->detached = true;
}
if (result != MA_SUCCESS) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "playback: pause failed id=%llu result=%d", (unsigned long long)id, result);
}
void SoundManager::resumePcm(PlaybackId id) {
auto* session = pcmSession(id);
if (session && session->sound && ma_sound_start(session->sound) != MA_SUCCESS)
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "playback: resume failed id=%llu", (unsigned long long)id);
if (!session || !session->sound) return;
ma_result result = MA_SUCCESS;
if (session->detached) {
result = ma_node_attach_output_bus(session->sound, 0, ma_engine_get_endpoint(m_engine), 0);
if (result == MA_SUCCESS) session->detached = false;
}
if (result == MA_SUCCESS) result = ma_sound_start(session->sound);
if (result != MA_SUCCESS) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "playback: resume failed id=%llu result=%d", (unsigned long long)id, result);
}
void SoundManager::stopPcm(PlaybackId id) {
+26 -3
View File
@@ -7,6 +7,7 @@
#include <vector>
#include <memory>
#include <map>
#include <array>
#include <cstdint>
#include <QTimer>
#include "audio_device.h"
@@ -15,6 +16,7 @@ struct ma_engine;
struct ma_context;
struct ma_decoder;
struct ma_sound;
class VoiceAudioIo;
class SoundManager : public QObject {
Q_OBJECT
@@ -48,6 +50,10 @@ public:
QString playbackDevice() const { return m_playbackId; }
QString captureDevice() const { return m_captureId; }
/* GUI: voice живёт до detachVoice; изменение списка выполняется при остановленном output. */
bool attachVoice(VoiceAudioIo* voice, const QString& output);
void detachVoice(VoiceAudioIo* voice);
using PlaybackId = uint64_t;
/* GUI: независимая сессия. owner удерживает PCM до detach; без owner данные копируются. */
PlaybackId playRawPcm(const int16_t* pcm, int frameCount, int sampleRate, int channels,
@@ -84,6 +90,7 @@ private:
uint64_t frames = 0, origin = 0;
int rate = 0;
bool transient = false;
bool detached = false;
~PcmSession();
};
PcmSession* pcmSession(PlaybackId id) const;
@@ -92,10 +99,26 @@ private:
bool openContext();
void openOutput();
struct VoiceRoute {
AudioDevice device;
QString wanted, actual;
std::array<VoiceAudioIo*, 2> voices{};
};
QString resolveOutput(const QString& requested);
bool openVoiceRoute(VoiceRoute& route);
void mergeVoiceRoutes();
static unsigned renderOutput(ma_engine* engine, const std::array<VoiceAudioIo*, 2>& voices,
float* out, unsigned frames, int64_t firstTimeUs);
std::atomic<unsigned> m_renderErrors{0};
std::array<VoiceAudioIo*, 2> m_voices{};
std::map<QString, std::unique_ptr<VoiceRoute>> m_voiceRoutes;
std::map<VoiceAudioIo*, QString> m_voiceSelections; // Desired выбор сохраняется при объединении physical outputs.
QString m_actualOutput;
bool m_resetting = false;
bool m_restoreSelections = false;
struct LoadedSound {
QByteArray mp3Data;
ma_decoder* decoder = nullptr;
ma_sound* sound = nullptr;
std::shared_ptr<PcmSession> pcm;
};
ma_engine* m_engine = nullptr;
+331
View File
@@ -0,0 +1,331 @@
#include "voice_audio_io.h"
extern "C" {
#include "call/call_audio.h"
#include "radio/radio_audio.h"
#include "speex_aec.h"
#include "debug_config.h"
}
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstring>
#include <system_error>
static int64_t voice_time_us() {
return std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
}
VoiceAudioIo::~VoiceAudioIo() { stop(); }
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_playPosition = 960; m_playEpoch = 1;
m_captureDropped = m_referenceDropped = m_playbackUnderruns = 0;
m_running = true;
try { m_playbackThread = std::thread(&VoiceAudioIo::playbackLoop, this); }
catch (const std::system_error& error) {
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",
(unsigned long long)session, kind == Kind::Call ? "call" : "radio");
return true;
}
void VoiceAudioIo::stop() {
stopCapture();
m_running = false; m_wake.notify_all();
if (m_playbackThread.joinable()) m_playbackThread.join();
}
bool VoiceAudioIo::startCapture(int channels, bool aecEnabled) {
stopCapture();
if (!m_running || channels < 1 || channels > 2 || (m_kind == Kind::Call && channels != 1)) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "voice-io: invalid capture channels=%d", channels); return false;
}
m_captureEvents.reset(); m_channels = channels; m_captureSequence = 0;
m_aecRequested = aecEnabled; m_capturing = true;
try { m_captureThread = std::thread(&VoiceAudioIo::captureLoop, this); }
catch (const std::system_error& error) {
m_capturing = false;
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "voice-io: capture worker start failed reason=%s", error.what()); return false;
}
return true;
}
void VoiceAudioIo::stopCapture() {
if (m_captureThread.joinable()) {
if (m_capturing) command(EventKind::Stop);
m_captureThread.join();
}
m_capturing = false;
}
/* GUI-команда не теряется при заполнении PCM: зарезервированы слоты; отказ завершает capture. */
void VoiceAudioIo::command(EventKind kind, bool value) {
if (!m_capturing) return;
Event event{}; event.kind = kind; event.value = value;
for (int attempt = 0; attempt < 100; ++attempt) {
if (m_captureEvents.push(event, true)) { m_wake.notify_all(); return; }
m_wake.notify_all(); std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "voice-io: control queue full session=%llu command=%d; stopping capture",
(unsigned long long)m_session, (int)kind);
m_capturing = false; m_wake.notify_all();
}
void VoiceAudioIo::setPtt(bool pressed) { command(EventKind::Ptt, pressed); }
void VoiceAudioIo::setMuted(bool muted) { command(EventKind::Mute, muted); }
void VoiceAudioIo::setAecEnabled(bool enabled) {
if (enabled == m_aecRequested) return;
m_aecRequested = enabled; command(EventKind::Aec, enabled);
}
void VoiceAudioIo::resetPlayback() { ++m_playEpoch; m_wake.notify_all(); }
void VoiceAudioIo::capture(const int16_t* pcm, unsigned frames, int64_t firstTimeUs, uint64_t route, bool missing) {
if (!pcm || !m_capturing) return;
if (missing) { m_captureSequence += frames; m_captureDropped.fetch_add(frames); return; }
int64_t enqueued = voice_time_us();
if (!firstTimeUs) firstTimeUs = enqueued - (int64_t)frames * 1000000 / 48000;
unsigned offset = 0;
while (offset < frames) {
Event event{}; event.frames = std::min(frames - offset, 960u);
event.time = firstTimeUs + (int64_t)offset * 1000000 / 48000;
event.sequence = m_captureSequence; m_captureSequence += event.frames;
event.route = route;
event.enqueued = enqueued;
memcpy(event.pcm, pcm + offset * m_channels, event.frames * m_channels * sizeof(int16_t));
if (!m_captureEvents.push(event)) m_captureDropped.fetch_add(event.frames, std::memory_order_relaxed);
offset += event.frames;
}
m_wake.notify_all();
}
void VoiceAudioIo::reference(const float* pcm, unsigned frames, int64_t firstTimeUs) {
if (!m_capturing) return;
if (!firstTimeUs) firstTimeUs = voice_time_us();
for (unsigned offset = 0; offset < frames;) {
Event event{}; event.frames = std::min(frames - offset, 960u);
event.time = firstTimeUs + (int64_t)offset * 1000000 / 48000;
event.sequence = m_playEpoch.load();
for (unsigned i = 0; i < event.frames * 2; ++i) {
float sample = pcm[offset * 2 + i];
event.pcm[i] = (int16_t)std::clamp(sample * 32768.0f, -32768.0f, 32767.0f);
}
if (!m_references.push(event)) m_referenceDropped.fetch_add(event.frames, std::memory_order_relaxed);
offset += event.frames;
}
m_wake.notify_all();
}
/* Выход всегда обслуживается независимо от capture/VAD. Уже потреблённый PCM становится референсом у владельца микшера. */
void VoiceAudioIo::mix(float* stereo, unsigned frames) {
uint64_t epoch = m_playEpoch.load();
for (unsigned i = 0; i < frames; ++i) {
if (m_playCurrent.epoch != epoch) m_playPosition = 960;
if (m_playPosition == 960) {
bool available = false;
for (int attempt = 0; attempt < 12 && m_playback.pop(m_playCurrent); ++attempt) {
if (m_playCurrent.epoch == epoch) { available = true; m_playPosition = 0; break; }
}
if (!available) { m_playbackUnderruns.fetch_add(frames - i, std::memory_order_relaxed); break; }
}
stereo[2*i] += m_playCurrent.pcm[2*m_playPosition] / 32768.0f;
stereo[2*i+1] += m_playCurrent.pcm[2*m_playPosition+1] / 32768.0f;
++m_playPosition;
}
m_wake.notify_all();
}
void VoiceAudioIo::playbackLoop() {
uint64_t epoch = m_playEpoch.load();
while (m_running) {
uint64_t requested = m_playEpoch.load();
if (requested != epoch) {
if (m_kind == Kind::Call) call_audio_reset_io(m_session, 1);
else radio_audio_reset_playback(m_session);
epoch = requested;
}
if (m_playback.size() < 2) {
Playback block{}; block.epoch = epoch;
if (m_kind == Kind::Call) {
int16_t mono[960]{};
int count = std::clamp(call_audio_pull_pcm(m_session, mono, 960), 0, 960);
for (int i = 0; i < count; ++i) block.pcm[2*i] = block.pcm[2*i+1] = mono[i];
} else radio_audio_pull_pcm(m_session, block.pcm, 1920);
if (!m_playback.push(block)) DEBUG_WARN(DEBUG_CATEGORY_CALL, "voice-io: playback queue contention");
} else {
std::unique_lock lock(m_waitMutex);
m_wake.wait_for(lock, std::chrono::milliseconds(5));
}
}
}
/* Референс хранится как PCM + время, а не как фиксированный двухкадровый FIFO независимых часов. */
void VoiceAudioIo::captureLoop() {
std::array<Event, 96> history{};
size_t historyHead = 0, historyCount = 0;
uint64_t referenceEpoch = m_playEpoch.load(), referenceDrops = m_referenceDropped.load();
speex_aec_t* aec = nullptr;
auto configureAec = [&](bool enabled) {
speex_aec_destroy(aec); aec = nullptr;
if (enabled) aec = speex_aec_create_mc(48000, 960, 14400, 1, m_channels, 2);
if (enabled && !aec) DEBUG_ERROR(DEBUG_CATEGORY_AEC, "voice-io: AEC unavailable session=%llu", (unsigned long long)m_session);
DEBUG_INFO(DEBUG_CATEGORY_AEC, "voice-io: capture AEC=%d mic=%d render=2 session=%llu",
aec != nullptr, m_channels, (unsigned long long)m_session);
};
configureAec(m_aecRequested);
struct Marker { unsigned position; EventKind kind; bool value; };
std::array<Marker, 64> markers{};
size_t markerCount = 0;
int16_t raw[1920]{}, cleaned[1920]{}, referencePcm[1920]{};
uint8_t muteMask[960]{};
unsigned filled = 0;
int64_t frameTime = 0, lastReport = voice_time_us();
uint64_t expectedSequence = 0, missingReference = 0, reportedReferenceDrops = referenceDrops, captureRoute = 0;
int64_t expectedCaptureTime = 0;
bool referenceAvailable = false;
bool muted = false;
auto applyMarker = [&](EventKind kind, bool value) {
if (kind == EventKind::Mute) muted = value;
else if (kind == EventKind::Ptt && m_kind == Kind::Radio) {
if (value) radio_audio_talk_begin(m_session); else radio_audio_talk_end(m_session);
}
};
auto consumeReference = [&]() {
uint64_t epoch = m_playEpoch.load(), drops = m_referenceDropped.load();
if (epoch != referenceEpoch || drops != referenceDrops) {
DEBUG_DEBUG(DEBUG_CATEGORY_AEC, "voice-io: reference reset epoch=%llu->%llu dropped=%llu",
(unsigned long long)referenceEpoch, (unsigned long long)epoch, (unsigned long long)(drops - referenceDrops));
historyHead = historyCount = 0;
if (aec) speex_aec_reset(aec);
referenceEpoch = epoch; referenceDrops = drops;
}
Event event{};
for (unsigned drained = 0; drained < 32 && m_references.pop(event); ++drained) {
if (event.sequence != referenceEpoch) continue;
if (historyCount && event.time <= history[(historyHead + historyCount - 1) % history.size()].time) {
DEBUG_WARN(DEBUG_CATEGORY_AEC, "voice-io: non-monotonic render clock, resetting AEC");
historyHead = historyCount = 0;
if (aec) speex_aec_reset(aec);
}
if (historyCount == history.size()) { historyHead = (historyHead + 1) % history.size(); --historyCount; }
history[(historyHead + historyCount++) % history.size()] = event;
}
};
auto alignReference = [&]() {
if (!historyCount) return false;
size_t index = 0;
for (unsigned i = 0; i < 960; ++i) {
double time = frameTime + (double)i * 1000000.0 / 48000.0;
while (index + 1 < historyCount && history[(historyHead + index + 1) % history.size()].time <= time) ++index;
auto& block = history[(historyHead + index) % history.size()];
double interval = 1000000.0 / 48000.0;
if (index + 1 < historyCount) {
auto& next = history[(historyHead + index + 1) % history.size()];
double measured = (double)(next.time - block.time) / block.frames;
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;
unsigned sample = (unsigned)position;
double fraction = position - sample;
for (unsigned channel = 0; channel < 2; ++channel) {
int first = block.pcm[2*sample+channel];
int second = sample + 1 < block.frames ? block.pcm[2*(sample+1)+channel] : first;
referencePcm[2*i+channel] = (int16_t)(first + (second - first) * fraction);
}
}
return true;
};
auto deliver = [&](unsigned frames) {
unsigned begin = 0;
for (size_t mark = 0; mark <= markerCount; ++mark) {
unsigned end = mark < markerCount ? markers[mark].position : frames;
if (m_kind == Kind::Call) memset(muteMask + begin, muted, end - begin);
else if (end > begin) radio_audio_feed_pcm(m_session, cleaned + begin * m_channels, (end - begin) * m_channels);
if (mark < markerCount) applyMarker(markers[mark].kind, markers[mark].value);
begin = end;
}
if (m_kind == Kind::Call && frames == 960) call_audio_feed_prepared_pcm(m_session, cleaned, muteMask);
filled = 0; markerCount = 0;
};
bool stopping = false;
while (m_capturing && !stopping) {
consumeReference();
Event event{};
if (!m_captureEvents.pop(event)) {
std::unique_lock lock(m_waitMutex); m_wake.wait_for(lock, std::chrono::milliseconds(5)); continue;
}
if (event.kind == EventKind::Stop) { stopping = true; break; }
if (event.kind == EventKind::Aec) { configureAec(event.value); continue; }
if (event.kind != EventKind::Pcm) {
if (!filled) applyMarker(event.kind, event.value);
else if (markerCount < markers.size()) markers[markerCount++] = {filled, event.kind, event.value};
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) {
m_captureDropped.fetch_add(event.frames); continue;
}
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",
(unsigned long long)expectedSequence, (unsigned long long)event.sequence, filled,
(unsigned long long)captureRoute, (unsigned long long)event.route);
if (clockJump) DEBUG_WARN(DEBUG_CATEGORY_AEC, "voice-io: capture clock jump=%lldus", (long long)(event.time - expectedCaptureTime));
for (size_t i = 0; i < markerCount; ++i) applyMarker(markers[i].kind, markers[i].value);
filled = 0; markerCount = 0;
if (aec) speex_aec_reset(aec);
if (m_kind == Kind::Radio) radio_audio_capture_discontinuity(m_session);
}
expectedSequence = event.sequence + event.frames;
captureRoute = event.route;
expectedCaptureTime = event.time + (int64_t)event.frames * 1000000 / 48000;
for (unsigned offset = 0; offset < event.frames;) {
if (!filled) frameTime = event.time + (int64_t)offset * 1000000 / 48000;
unsigned take = std::min(960 - filled, event.frames - offset);
memcpy(raw + filled * m_channels, event.pcm + offset * m_channels, take * m_channels * sizeof(int16_t));
filled += take; offset += take;
if (filled != 960) continue;
memcpy(cleaned, raw, 960 * m_channels * sizeof(int16_t));
consumeReference();
bool available = aec && alignReference();
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);
}
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);
memcpy(cleaned, raw, filled * m_channels * sizeof(int16_t));
deliver(filled);
}
uint64_t lostCapture = m_captureDropped.exchange(0), lostReference = m_referenceDropped.load() - reportedReferenceDrops;
if (lostCapture || lostReference || missingReference)
DEBUG_WARN(DEBUG_CATEGORY_AEC, "voice-io: capture stopped lost_capture=%llu lost_reference=%llu missing_reference=%llu",
(unsigned long long)lostCapture, (unsigned long long)lostReference, (unsigned long long)missingReference);
speex_aec_destroy(aec);
m_capturing = false;
}
+61
View File
@@ -0,0 +1,61 @@
#pragma once
#include "audio_event_queue.h"
#include <atomic>
#include <condition_variable>
#include <mutex>
#include <thread>
#include <cstdint>
/* Desktop: аппаратные callbacks только копируют PCM, render также выполняет готовое смешивание.
* Capture worker — единственный владелец AEC и порядка PCM/PTT/mute. RX worker независим от VAD.
* Управляющий поток закрывает capture и отсоединяет output до stop/destruction.
* Финальный stereo render приходит с того же выходного устройства после всего микширования. */
class VoiceAudioIo {
public:
enum class Kind { Call, Radio };
VoiceAudioIo() = default;
~VoiceAudioIo();
bool start(Kind kind, uint64_t session);
void stop();
bool startCapture(int channels, bool aecEnabled);
void stopCapture();
bool captureRunning() const { return m_capturing.load(); }
void setPtt(bool pressed);
void setMuted(bool muted);
void setAecEnabled(bool enabled);
void resetPlayback();
/* 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);
private:
enum class EventKind { Pcm, Ptt, Mute, Aec, Stop };
struct Event {
EventKind kind = EventKind::Pcm;
unsigned frames = 0;
int64_t time = 0;
int64_t enqueued = 0;
uint64_t sequence = 0;
uint64_t route = 0;
bool value = false;
int16_t pcm[1920]{};
};
struct Playback { uint64_t epoch = 0; int16_t pcm[1920]{}; };
AudioEventQueue<Event, 32> m_captureEvents, m_references;
AudioEventQueue<Playback, 12> m_playback;
std::atomic<bool> m_running{false}, m_capturing{false};
std::atomic<uint64_t> m_playEpoch{0}, m_captureDropped{0}, m_referenceDropped{0}, m_playbackUnderruns{0};
uint64_t m_captureSequence = 0; // Только capture callback; reset после его остановки.
Kind m_kind = Kind::Call;
uint64_t m_session = 0;
int m_channels = 1;
std::atomic<bool> m_aecRequested{false};
std::thread m_captureThread, m_playbackThread;
std::mutex m_waitMutex;
std::condition_variable m_wake;
Playback m_playCurrent{};
unsigned m_playPosition = 960;
void command(EventKind kind, bool value = false);
void captureLoop();
void playbackLoop();
};