Раздели аудиопотоки и привяжи AEC к фактическому общему выходу
This commit is contained in:
1 parent
d7fb0c83f9
commit
bd082a26bf
30 files changed
+1839
-412
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
Reference in new issue
Block a user