Сохрани накопленные исправления аудио, async и диагностики

This commit is contained in:
evgeny committed 2026-10-02 01:49:32 +03:00
1 parent b1e3193899
commit bdcf8b5634
59 files changed
+4611 -1441

No files matched your search

+96
View File
@@ -0,0 +1,96 @@
#include "audio_device.h"
#include "../transport/audio_diagnostics.h"
#include "debug_config.h"
#include <algorithm>
#include <utility>
AudioDevice::AudioDevice() { m_clock.start(); }
AudioDevice::~AudioDevice() { close(); }
bool AudioDevice::init(ma_context* context, ma_device_config config, const char* role, uint64_t session, Callback callback) {
close();
m_role = role;
m_callback = std::move(callback);
if (!context) { retryLater(); return false; }
auto* device = new ma_device{};
config.dataCallback = data;
config.notificationCallback = notification;
config.pUserData = this;
ma_result result = ma_device_init(context, &config, device);
if (result != MA_SUCCESS) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: %s init failed result=%d", role, result);
delete device;
retryLater();
return false;
}
m_device = device;
m_frames = 0;
m_seenFrames = 0;
m_suspended = false;
audio_diag_label(device, session, role);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "audio: %s initialized backend=%s rate=%u capture='%s' playback='%s' period=%u/%u",
role, ma_get_backend_name(context->backend), device->sampleRate, device->capture.name, device->playback.name,
device->capture.internalPeriodSizeInFrames, device->playback.internalPeriodSizeInFrames);
return true;
}
bool AudioDevice::start() {
if (!m_device) return false;
m_progressAt = m_clock.elapsed();
ma_result result = ma_device_start(m_device);
if (result != MA_SUCCESS) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: %s start failed result=%d", m_role, result);
close();
retryLater();
return false;
}
DEBUG_INFO(DEBUG_CATEGORY_CALL, "audio: %s started", m_role);
return true;
}
void AudioDevice::close() {
if (m_device) {
ma_device_uninit(m_device); /* uninit включает stop/join, в том числе после неудачного start. */
delete m_device;
m_device = nullptr;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "audio: %s closed", m_role);
}
m_callback = {};
}
void AudioDevice::data(ma_device* device, void* out, const void* in, ma_uint32 frames) {
auto* self = static_cast<AudioDevice*>(device->pUserData);
if (self->m_callback) self->m_callback(in, out, frames);
self->m_frames.fetch_add(frames, std::memory_order_relaxed);
}
void AudioDevice::notification(const ma_device_notification* event) {
auto* self = static_cast<AudioDevice*>(event->pDevice->pUserData);
if (event->type == ma_device_notification_type_interruption_began) self->m_suspended = true;
if (event->type == ma_device_notification_type_interruption_ended) self->m_suspended = false;
}
bool AudioDevice::needsRecovery() {
if (!m_device) return false;
uint64_t frames = m_frames.load(std::memory_order_relaxed);
qint64 now = m_clock.elapsed();
if (frames != m_seenFrames || m_suspended.load()) {
m_seenFrames = frames;
m_progressAt = now;
if (frames) m_retryDelay = 250;
}
bool stopped = ma_device_get_state(m_device) == ma_device_state_stopped;
if (!stopped && now - m_progressAt < 1000) return false;
DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio: %s recovery reason=%s idle=%lldms frames=%llu suspended=%d",
m_role, stopped ? "stopped" : "no-progress", (long long)(now - m_progressAt),
(unsigned long long)frames, m_suspended.load());
return true;
}
bool AudioDevice::retryReady() const { return !m_device && m_clock.elapsed() >= m_retryAt; }
void AudioDevice::retryLater() {
m_retryAt = m_clock.elapsed() + m_retryDelay;
DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio: %s retry in %dms", m_role, m_retryDelay);
m_retryDelay = std::min(m_retryDelay * 2, 2000);
}
void AudioDevice::retryNow() { m_retryAt = 0; m_retryDelay = 250; }
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include "miniaudio.h"
#include <QElapsedTimer>
#include <atomic>
#include <functional>
/* Один владелец в GUI-потоке. Callback публикует только прогресс и уведомления;
* close() дожидается worker перед освобождением callback и его состояния. */
class AudioDevice {
public:
using Callback = std::function<void(const void*, void*, unsigned int)>;
AudioDevice();
~AudioDevice();
AudioDevice(const AudioDevice&) = delete;
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 close();
bool needsRecovery();
bool retryReady() const;
void retryLater();
void retryNow();
bool initialized() const { return m_device != nullptr; }
ma_device* device() const { return m_device; }
private:
static void data(ma_device* device, void* out, const void* in, ma_uint32 frames);
static void notification(const ma_device_notification* event);
ma_device* m_device = nullptr;
Callback m_callback;
const char* m_role = "audio";
QElapsedTimer m_clock;
std::atomic<uint64_t> m_frames{0};
std::atomic<bool> m_suspended{false};
uint64_t m_seenFrames = 0;
qint64 m_progressAt = 0, m_retryAt = 0;
int m_retryDelay = 250;
};
+35 -20
View File
@@ -183,25 +183,25 @@ void AudioDeviceSettingsPage::loadFromDb() {
/* Output devices */
m_outputCombo->clear();
m_outputCombo->addItem("Default (auto)", -1);
m_outputCombo->addItem("Default (auto)", QString());
for (int i = 0; i < playbackDevs.size(); i++) {
QString label = playbackDevs[i].name;
if (playbackDevs[i].isDefault) label += " (default)";
m_outputCombo->addItem(label, i);
m_outputCombo->addItem(label, playbackDevs[i].id);
}
/* Input devices */
m_inputCombo->clear();
m_inputCombo->addItem("Default (auto)", -1);
m_inputCombo->addItem("Default (auto)", QString());
for (int i = 0; i < captureDevs.size(); i++) {
QString label = captureDevs[i].name;
if (captureDevs[i].isDefault) label += " (default)";
m_inputCombo->addItem(label, i);
m_inputCombo->addItem(label, captureDevs[i].id);
}
if (m_db) {
m_savedOutputIdx = m_db->getUiStateInt("audio_output_device", -1);
m_savedInputIdx = m_db->getUiStateInt("audio_input_device", -1);
m_savedOutputId = m_db->getUiState("audio_output_id");
m_savedInputId = m_db->getUiState("audio_input_id");
m_savedCodecPreset = m_db->getUiStateInt("opus_codec_preset", 1);
m_savedRadioOpusMode = m_db->getUiStateInt("radio_opus_mode", 0);
m_savedRadioOpusBitrate = m_db->getUiStateInt("radio_opus_bitrate", 32);
@@ -215,11 +215,11 @@ void AudioDeviceSettingsPage::loadFromDb() {
/* Select saved indices */
for (int i = 0; i < m_outputCombo->count(); i++) {
if (m_outputCombo->itemData(i).toInt() == m_savedOutputIdx)
if (m_outputCombo->itemData(i).toString() == m_savedOutputId)
{ m_outputCombo->setCurrentIndex(i); break; }
}
for (int i = 0; i < m_inputCombo->count(); i++) {
if (m_inputCombo->itemData(i).toInt() == m_savedInputIdx)
if (m_inputCombo->itemData(i).toString() == m_savedInputId)
{ m_inputCombo->setCurrentIndex(i); break; }
}
for (int i = 0; i < m_codecPresetCombo->count(); i++) {
@@ -244,8 +244,8 @@ void AudioDeviceSettingsPage::loadFromDb() {
}
void AudioDeviceSettingsPage::applyAndSave() {
int outIdx = m_outputCombo->currentData().toInt();
int inIdx = m_inputCombo->currentData().toInt();
QString outputId = m_outputCombo->currentData().toString();
QString inputId = m_inputCombo->currentData().toString();
int codecPreset = m_codecPresetCombo->currentData().toInt();
auto saveKey = [](const char* key, int val) {
@@ -260,8 +260,23 @@ void AudioDeviceSettingsPage::applyAndSave() {
memcpy(arg->data + klen + 1, v.constData(), vlen + 1);
gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg);
};
saveKey("audio_output_device", outIdx);
saveKey("audio_input_device", inIdx);
auto saveDevice = [](const char* key, const QString& id) {
QByteArray value = id.toUtf8();
size_t keyLen = strlen(key), valueLen = value.size();
auto* arg = static_cast<save_ui_state_arg*>(u_calloc(1, sizeof(save_ui_state_arg)));
if (!arg) return;
if (keyLen + valueLen + 2 > sizeof(arg->data)) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: device ID too long bytes=%zu", valueLen);
u_free(arg); return;
}
arg->inst = gui_bridge_get_inst();
memcpy(arg->data, key, keyLen + 1);
memcpy(arg->data + keyLen + 1, value.constData(), valueLen + 1);
gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg);
};
saveDevice("audio_output_id", outputId);
saveDevice("audio_input_id", inputId);
SoundManager::instance()->setDevices(outputId, inputId);
saveKey("opus_codec_preset", codecPreset);
VoiceEncoder::setPreset(codecPreset);
@@ -312,8 +327,8 @@ void AudioDeviceSettingsPage::applyAndSave() {
postChat(m_configPath, "call_jitter_max_reserve_ms", jitterReserveMs);
}
m_savedOutputIdx = outIdx;
m_savedInputIdx = inIdx;
m_savedOutputId = outputId;
m_savedInputId = inputId;
m_savedCodecPreset = codecPreset;
m_savedRadioOpusMode = radioOpusMode;
m_savedRadioOpusBitrate = radioOpusBitrate;
@@ -322,13 +337,13 @@ void AudioDeviceSettingsPage::applyAndSave() {
m_savedCompressorMaxGainDb = compMaxGain;
m_savedCompressorRiseRate = compRiseRate;
m_savedAecEnabled = aecEnabled;
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "saved output=%d input=%d codec=%d radioOpus=%d/%dkbps stereo=%d compressor=%d gain=%ddB rise=%.1fx aec=%d",
outIdx, inIdx, codecPreset, radioOpusMode, radioOpusBitrate, radioCaptureStereo,
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "saved output_default=%d input_default=%d codec=%d radioOpus=%d/%dkbps stereo=%d compressor=%d gain=%ddB rise=%.1fx aec=%d",
outputId.isEmpty(), inputId.isEmpty(), codecPreset, radioOpusMode, radioOpusBitrate, radioCaptureStereo,
compEnabled, compMaxGain, (float)compRiseRate, aecEnabled);
if (m_recorder) {
m_recorder->setCompressorEnabled(compEnabled != 0);
if (m_recorder->compressor() && compEnabled) {
{
audio_compressor_config_t compCfg = {0};
compCfg.sample_rate = 48000;
compCfg.channels = 1;
@@ -337,7 +352,7 @@ void AudioDeviceSettingsPage::applyAndSave() {
compCfg.lookahead_ms = 100;
compCfg.rise_rate_per_sec = (float)compRiseRate;
compCfg.target_level = 1.0f;
audio_compressor_configure(m_recorder->compressor(), &compCfg);
m_recorder->configureCompressor(compCfg);
}
}
}
@@ -346,8 +361,8 @@ void AudioDeviceSettingsPage::onTestMicToggled() {
if (m_testMicBtn->isChecked()) {
m_testRecorder = new AudioRecorder(this);
m_testRecorder->init();
int inIdx = m_inputCombo->currentData().toInt();
m_testRecorder->setCaptureDeviceIndex(inIdx);
QString id = m_inputCombo->currentData().toString();
m_testRecorder->setCaptureDevice(id);
m_testRecorder->startRecording();
m_micTestActive = true;
m_levelTimer->start();
+2 -2
View File
@@ -41,8 +41,8 @@ private:
QPushButton* m_testSpeakerBtn;
QTimer* m_levelTimer;
bool m_micTestActive = false;
int m_savedOutputIdx = -1;
int m_savedInputIdx = -1;
QString m_savedOutputId;
QString m_savedInputId;
int m_savedCodecPreset = 1;
QCheckBox* m_compressorEnabledCb;
+78 -116
View File
@@ -2,6 +2,7 @@
#include "media_async/attachment_send.h"
#include "miniaudio.h"
#include "sound_manager.h"
#include "../transport/audio_diagnostics.h"
extern "C" {
#include "../../lib/audio_compressor.h"
@@ -11,57 +12,60 @@ extern "C" {
#include <cstring>
#include <cstdlib>
#include <cmath>
static AudioRecorder* g_recorder = nullptr;
void audioCaptureCallback(ma_device* pDevice, void* pOutput, const void* pInput, unsigned int frameCount) {
(void)pOutput;
AudioRecorder* self = g_recorder;
if (!self || !self->m_recording) return;
if (!pInput) return;
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)self->m_channels;
size_t add = (size_t)frameCount * (size_t)m_channels;
if (self->m_compressor && self->m_compressorEnabled) {
audio_compressor_push(self->m_compressor, src, add);
if (m_compressor && m_captureCompressorEnabled) {
audio_compressor_push(m_compressor, src, add);
} else {
size_t cur = self->m_pcmBuffer.size();
self->m_pcmBuffer.resize(cur + add);
std::memcpy(self->m_pcmBuffer.data() + cur, src, add * sizeof(int16_t));
size_t cur = m_pcmBuffer.size();
m_pcmBuffer.resize(cur + add);
std::memcpy(m_pcmBuffer.data() + cur, src, add * sizeof(int16_t));
}
float sumSq = 0.0f;
for (unsigned int i = 0; i < frameCount * (unsigned int)self->m_channels; i++) {
for (unsigned int i = 0; i < frameCount * (unsigned int)m_channels; i++) {
float v = (float)src[i] / 32768.0f;
sumSq += v * v;
}
float rms = sqrtf(sumSq / (float)(frameCount * self->m_channels));
self->m_peakLevel = rms;
float rms = sqrtf(sumSq / (float)(frameCount * m_channels));
m_peakLevel = rms;
self->m_recentRms[self->m_rmsIdx % 5] = rms;
self->m_rmsIdx++;
if (self->m_rmsIdx >= 5) {
float avg = 0.0f;
for (int i = 0; i < 5; i++) avg += self->m_recentRms[i];
avg /= 5.0f;
self->m_waveformLevels.push_back(avg);
}
m_recordedFrames.fetch_add(frameCount, std::memory_order_relaxed);
}
AudioRecorder::AudioRecorder(QObject* parent)
: QObject(parent) {
g_recorder = this;
m_compressor = audio_compressor_create();
if (m_compressor) {
audio_compressor_config_t cfg = {0};
cfg.sample_rate = m_sampleRate;
cfg.channels = m_channels;
m_compressorConfig = cfg;
audio_compressor_configure(m_compressor, &cfg);
}
m_captureId = SoundManager::instance()->captureDevice();
connect(SoundManager::instance(), &SoundManager::devicesChanged, this, [this]() {
QString id = SoundManager::instance()->captureDevice();
if (!m_customDevice && id != m_captureId) {
m_captureId = id; m_capture.close(); m_capture.retryNow(); recoverCapture();
}
});
m_recoveryTimer.setInterval(200);
connect(&m_recoveryTimer, &QTimer::timeout, this, &AudioRecorder::recoverCapture);
connect(SoundManager::instance(), &SoundManager::contextAboutToReset, this, [this]() {
m_capture.close(); m_durationTimer.stop();
});
connect(SoundManager::instance(), &SoundManager::contextRestored, this, [this]() {
m_capture.retryNow(); recoverCapture();
});
m_durationTimer.setInterval(100);
connect(&m_durationTimer, &QTimer::timeout, this, [this]() {
if (m_recording) {
m_elapsedMs += 100;
m_elapsedMs = (int)(m_recordedFrames.load(std::memory_order_relaxed) * 1000 / m_sampleRate);
emit durationChanged(m_elapsedMs);
}
});
@@ -71,103 +75,71 @@ AudioRecorder::~AudioRecorder() {
shutdown();
audio_compressor_destroy(m_compressor);
m_compressor = nullptr;
if (g_recorder == this) g_recorder = nullptr;
}
bool AudioRecorder::init() {
if (m_initialized) return true;
m_device = new ma_device;
std::memset(m_device, 0, sizeof(*m_device));
m_initialized = true;
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "initialized (sampleRate=%d, channels=%d)", m_sampleRate, m_channels);
return true;
}
void AudioRecorder::shutdown() {
if (!m_initialized) return;
if (m_recording) stopRecording();
if (m_deviceStarted) {
m_deviceStarted = false;
if (m_device) { ma_device_stop(m_device); ma_device_uninit(m_device); }
}
if (m_device) { delete m_device; m_device = nullptr; }
stopRecording();
m_recoveryTimer.stop();
m_capture.close();
m_initialized = false;
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "shutdown");
}
void AudioRecorder::startRecording() {
if (m_recording) return;
if (!m_initialized) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "not initialized"); return; }
if (!m_initialized) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "recorder: not initialized"); return; }
m_pcmBuffer.clear();
m_waveformLevels.clear();
m_rmsIdx = 0;
m_elapsedMs = 0;
if (m_compressor) audio_compressor_reset(m_compressor);
ma_context* ctx = SoundManager::instance()->context();
if (!ctx) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "no audio context"); return; }
ma_device_config config = ma_device_config_init(ma_device_type_capture);
config.capture.format = ma_format_s16;
config.capture.channels = (ma_uint32)m_channels;
config.sampleRate = (ma_uint32)m_sampleRate;
config.periodSizeInFrames = 480;
config.dataCallback = audioCaptureCallback;
config.pUserData = this;
if (m_captureDeviceIndex >= 0) {
ma_device_info* pCaptureDevices = nullptr;
ma_uint32 captureCount = 0;
ma_result r = ma_context_get_devices(ctx, nullptr, nullptr, &pCaptureDevices, &captureCount);
if (r == MA_SUCCESS && (ma_uint32)m_captureDeviceIndex < captureCount) {
config.capture.pDeviceID = &pCaptureDevices[m_captureDeviceIndex].id;
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "using capture device[%d]: %s", m_captureDeviceIndex,
pCaptureDevices[m_captureDeviceIndex].name);
}
m_recordedFrames = 0; m_elapsedMs = 0; m_peakLevel = 0;
m_captureCompressorEnabled = m_compressorEnabled;
if (m_compressor) {
audio_compressor_configure(m_compressor, &m_compressorConfig);
audio_compressor_set_enabled(m_compressor, m_captureCompressorEnabled);
audio_compressor_reset(m_compressor);
}
ma_result r = ma_device_init(ctx, &config, m_device);
if (r != MA_SUCCESS) {
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "device init failed (%d), trying default", r);
config.capture.pDeviceID = nullptr;
r = ma_device_init(ctx, &config, m_device);
if (r != MA_SUCCESS) {
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "default device init also failed (%d)", r);
return;
}
}
r = ma_device_start(m_device);
if (r != MA_SUCCESS) {
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "device start failed (%d)", r);
ma_device_uninit(m_device);
return;
}
m_recording = true;
m_deviceStarted = true;
m_durationTimer.start();
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "recording started");
m_capture.retryNow();
recoverCapture();
m_recoveryTimer.start();
DEBUG_INFO(DEBUG_CATEGORY_CALL, "recorder: recording requested compressor=%d", m_captureCompressorEnabled);
emit recordingStarted();
}
void AudioRecorder::stopRecording() {
void AudioRecorder::recoverCapture() {
if (!m_recording) return;
m_durationTimer.stop();
if (m_deviceStarted) {
ma_device_stop(m_device);
ma_device_uninit(m_device);
m_deviceStarted = false;
if (m_capture.needsRecovery()) { m_capture.close(); m_durationTimer.stop(); m_capture.retryLater(); }
if (!m_capture.retryReady()) return;
ma_context* context = SoundManager::instance()->context();
if (!context) return;
ma_device_config config = ma_device_config_init(ma_device_type_capture);
config.capture.format = ma_format_s16;
config.capture.channels = m_channels;
config.sampleRate = m_sampleRate;
ma_device_id id;
bool selected = SoundManager::deviceId(m_captureId, &id);
if (selected) config.capture.pDeviceID = &id;
auto callback = [this](const void* input, void*, unsigned int frames) { captureCallback(input, frames); };
bool ready = m_capture.init(context, config, "voice-recorder", 0, callback) && m_capture.start();
if (!ready) {
if (!selected) return;
DEBUG_WARN(DEBUG_CATEGORY_CALL, "recorder: selected input unavailable, trying default");
config.capture.pDeviceID = nullptr;
ready = m_capture.init(context, config, "voice-recorder", 0, callback) && m_capture.start();
}
m_recording = false;
if (ready) m_durationTimer.start();
}
void AudioRecorder::stopRecording() {
if (!m_recording.exchange(false)) return;
m_recoveryTimer.stop(); m_durationTimer.stop();
m_capture.close();
m_elapsedMs = (int)(m_recordedFrames.load() * 1000 / m_sampleRate);
int duration = m_elapsedMs;
size_t bufSize = (m_compressor && m_compressorEnabled) ? audio_compressor_output_size(m_compressor) : m_pcmBuffer.size();
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "recording stopped, duration=%dms, pcmSamples=%zu, compressor=%d",
duration, bufSize, m_compressorEnabled ? 1 : 0);
size_t samples = m_compressor && m_captureCompressorEnabled ? audio_compressor_output_size(m_compressor) : m_pcmBuffer.size();
DEBUG_INFO(DEBUG_CATEGORY_CALL, "recorder: stopped duration=%dms samples=%zu", duration, samples);
emit recordingStopped(duration);
emit durationChanged(0);
}
@@ -179,7 +151,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 (m_compressor && m_compressorEnabled) {
if (m_compressor && m_captureCompressorEnabled) {
auto* replacement = audio_compressor_clone_config(m_compressor);
if (!replacement) return false;
request->compressor = m_compressor;
@@ -192,24 +164,14 @@ bool AudioRecorder::takeRecording(struct attachment_send_req* request) {
return true;
}
float AudioRecorder::peakLevel() const {
float v = m_peakLevel;
m_peakLevel = 0.0f;
return v;
}
float AudioRecorder::peakLevel() const { return m_peakLevel.exchange(0.0f); }
const int16_t* AudioRecorder::bufferData() const {
if (m_compressor && m_compressorEnabled) return audio_compressor_output(m_compressor);
return m_pcmBuffer.data();
}
size_t AudioRecorder::bufferSize() const {
if (m_compressor && m_compressorEnabled) return audio_compressor_output_size(m_compressor);
return m_pcmBuffer.size();
void AudioRecorder::configureCompressor(const audio_compressor_config_t& config) {
m_compressorConfig = config; /* Применяется перед следующей записью, когда callback ещё не запущен. */
DEBUG_INFO(DEBUG_CATEGORY_CALL, "recorder: compressor configuration saved for next recording");
}
void AudioRecorder::setCompressorEnabled(bool enabled) {
m_compressorEnabled = enabled;
if (m_compressor) audio_compressor_set_enabled(m_compressor, enabled);
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "compressor %s", enabled ? "enabled" : "disabled");
DEBUG_INFO(DEBUG_CATEGORY_CALL, "recorder: compressor=%d for next recording", enabled);
}
+17 -15
View File
@@ -5,6 +5,10 @@
#include <QTimer>
#include <vector>
#include <cstdint>
#include "audio_device.h"
extern "C" {
#include "audio_compressor.h"
}
struct ma_device;
struct audio_compressor;
@@ -20,8 +24,7 @@ public:
void shutdown();
bool isInitialized() const { return m_initialized; }
void setCaptureDeviceIndex(int index) { m_captureDeviceIndex = index; }
int captureDeviceIndex() const { return m_captureDeviceIndex; }
void setCaptureDevice(const QString& id) { m_captureId = id; m_customDevice = true; }
void startRecording();
void stopRecording();
@@ -29,13 +32,10 @@ public:
float peakLevel() const;
const int16_t* bufferData() const;
size_t bufferSize() const;
const std::vector<float>& waveformLevels() const { return m_waveformLevels; }
int durationMs() const;
bool takeRecording(struct attachment_send_req* request); /* Передать остановленную запись без копии PCM. */
struct audio_compressor* compressor() const { return m_compressor; }
void configureCompressor(const audio_compressor_config_t& config);
void setCompressorEnabled(bool enabled);
bool isCompressorEnabled() const { return m_compressorEnabled; }
@@ -45,24 +45,26 @@ signals:
void durationChanged(int ms);
private:
ma_device* m_device = nullptr;
AudioDevice m_capture;
QTimer m_recoveryTimer;
void recoverCapture();
void captureCallback(const void* input, unsigned int frames);
bool m_initialized = false;
bool m_recording = false;
bool m_deviceStarted = false;
std::atomic<bool> m_recording{false};
int m_sampleRate = 48000;
int m_channels = 1;
int m_captureDeviceIndex = -1;
QString m_captureId;
bool m_customDevice = false;
std::vector<int16_t> m_pcmBuffer;
mutable float m_peakLevel = 0.0f;
std::vector<float> m_recentRms = std::vector<float>(5, 0.0f);
int m_rmsIdx = 0;
std::vector<float> m_waveformLevels;
mutable std::atomic<float> m_peakLevel{0.0f};
std::atomic<uint64_t> m_recordedFrames{0};
QTimer m_durationTimer;
int m_elapsedMs = 0;
struct audio_compressor* m_compressor = nullptr;
bool m_compressorEnabled = false;
bool m_captureCompressorEnabled = false;
audio_compressor_config_t m_compressorConfig{};
friend void audioCaptureCallback(ma_device* pDevice, void* pOutput, const void* pInput, unsigned int frameCount);
};
+136 -204
View File
@@ -1,232 +1,164 @@
#include "call_audio_engine.h"
#include "sound_manager.h"
#include "../transport/gui_bridge.h"
#include "miniaudio.h"
#include "../transport/audio_diagnostics.h"
extern "C" {
#include "call/call_audio.h"
#include "../../lib/debug_config.h"
#include "../../lib/u_async.h"
#include "debug_config.h"
}
#include <QTimer>
#include <algorithm>
#include <cstring>
static CallAudioEngine* g_callAudioEngine = nullptr;
static void callAudioDataCallback(ma_device* dev, void* out, const void* in, ma_uint32 frames) {
CallAudioEngine* e = (CallAudioEngine*)dev->pUserData;
if (e) e->dataCallback(in, out, frames);
}
CallAudioEngine* CallAudioEngine::instance() {
if (!g_callAudioEngine) g_callAudioEngine = new CallAudioEngine();
return g_callAudioEngine;
static auto* engine = new CallAudioEngine();
return engine;
}
int CallAudioEngine::start(quint64 callId) {
if (m_active) {
if (m_callId == callId) return 0;
stop();
}
m_callId = callId;
struct UTUN_INSTANCE* inst = gui_bridge_get_inst();
if (!inst) {
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "CallAudio: no instance, cannot start");
return -1;
}
if (call_audio_init(inst) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "CallAudio: call_audio_init failed");
return -1;
}
if (call_audio_start(callId) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "CallAudio: call_audio_start failed call=%016llx", (unsigned long long)callId);
return -1;
}
m_captureBuf.assign(CALL_AUDIO_FRAME_SAMPLES, 0);
m_capturePos = 0;
m_muted = false;
if (initDevice() != 0) {
call_audio_release(callId);
return -1;
}
m_active = true;
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "CallAudio: started call=%016llx", (unsigned long long)callId);
return 0;
}
int CallAudioEngine::initDevice() {
ma_context* ctx = SoundManager::instance()->context();
if (!ctx) {
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "CallAudio: no audio context");
return -1;
}
m_device = new ma_device;
std::memset(m_device, 0, sizeof(*m_device));
ma_device_config config = ma_device_config_init(ma_device_type_duplex);
config.capture.format = ma_format_s16;
config.capture.channels = 1;
config.playback.format = ma_format_s16;
config.playback.channels = 1;
config.sampleRate = CALL_AUDIO_SAMPLE_RATE;
config.periodSizeInFrames = 480;
config.dataCallback = callAudioDataCallback;
config.pUserData = this;
if (m_playbackDeviceIndex >= 0) {
ma_device_info* pPlaybackDevices = nullptr;
ma_uint32 playbackCount = 0;
if (ma_context_get_devices(ctx, &pPlaybackDevices, &playbackCount, nullptr, nullptr) == MA_SUCCESS &&
(ma_uint32)m_playbackDeviceIndex < playbackCount) {
config.playback.pDeviceID = &pPlaybackDevices[m_playbackDeviceIndex].id;
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "CallAudio: using playback device[%d]: %s",
m_playbackDeviceIndex, pPlaybackDevices[m_playbackDeviceIndex].name);
} else {
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "CallAudio: playback device[%d] not found, using default",
m_playbackDeviceIndex);
}
}
if (ma_device_init(ctx, &config, m_device) != MA_SUCCESS) {
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "CallAudio: device init failed, trying default");
config.playback.pDeviceID = nullptr;
if (ma_device_init(ctx, &config, m_device) != MA_SUCCESS) {
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "CallAudio: default device init also failed");
delete m_device; m_device = nullptr;
return -1;
}
}
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "CallAudio: device backend=%s rate=%u capture='%s' rate=%u period=%u playback='%s' rate=%u period=%u",
ma_get_backend_name(ctx->backend), m_device->sampleRate,
m_device->capture.name, m_device->capture.internalSampleRate, m_device->capture.internalPeriodSizeInFrames,
m_device->playback.name, m_device->playback.internalSampleRate, m_device->playback.internalPeriodSizeInFrames);
m_capturePos = 0;
m_dbg = {};
if (ma_device_start(m_device) != MA_SUCCESS) {
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "CallAudio: device start failed");
ma_device_uninit(m_device);
delete m_device; m_device = nullptr;
return -1;
}
m_deviceStarted = true;
return 0;
}
void CallAudioEngine::setMuted(bool muted) {
if (m_muted.load() == muted) return;
m_muted = muted;
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "CallAudio: mute %s", muted ? "on" : "off");
}
void CallAudioEngine::setPlaybackDeviceIndex(int index) {
if (index == m_playbackDeviceIndex) return;
int prev = m_playbackDeviceIndex;
m_playbackDeviceIndex = index;
if (m_active) {
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "CallAudio: switch playback device %d -> %d", prev, index);
teardownDevice();
if (initDevice() != 0)
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "CallAudio: switch playback device failed (kept index=%d)", index);
}
}
void CallAudioEngine::beginEnd(quint64 callId) {
if (!m_active || m_callId != callId) return;
call_audio_begin_end(callId); /* старт тона завершения */
QTimer::singleShot(500, this, [this, callId]() {
if (m_callId == callId) stop();
CallAudioEngine::CallAudioEngine() {
m_playbackId = SoundManager::instance()->playbackDevice();
m_captureId = SoundManager::instance()->captureDevice();
connect(SoundManager::instance(), &SoundManager::devicesChanged, this, [this]() {
setPlaybackDevice(SoundManager::instance()->playbackDevice());
setCaptureDevice(SoundManager::instance()->captureDevice());
});
m_recoveryTimer.setInterval(200);
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();
if (m_active) recoverDevices();
});
}
void CallAudioEngine::stop() {
if (!m_active && !m_device && !m_deviceStarted) return;
m_active = false;
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) {
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;
m_muted = false;
quint64 cid = m_callId.load();
teardownDevice();
call_audio_release(cid);
m_callId = 0;
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "CallAudio: stopped");
m_ending = false;
m_active = true;
m_capture.retryNow(); m_playback.retryNow();
recoverDevices();
m_recoveryTimer.start();
DEBUG_INFO(DEBUG_CATEGORY_CALL, "CallAudio: session started call=%016llx", (unsigned long long)callId);
return 0;
}
void CallAudioEngine::teardownDevice() {
if (m_deviceStarted) {
ma_device_stop(m_device);
ma_device_uninit(m_device);
m_deviceStarted = false;
bool CallAudioEngine::openDevice(bool playback) {
AudioDevice& device = playback ? m_playback : m_capture;
ma_context* 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_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;
}
if (m_device) { delete m_device; m_device = nullptr; }
auto callback = [this](const void* in, void* out, unsigned int frames) { dataCallback(in, out, frames); };
const char* role = playback ? "call-playback" : "call-capture";
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 (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;
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();
}
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);
}
void CallAudioEngine::pauseDevices() {
m_capture.close(); m_playback.close();
m_capturePos = 0;
if (m_active) call_audio_reset_io(m_callId.load(), 1);
}
void CallAudioEngine::setMuted(bool muted) {
if (m_muted.exchange(muted) == muted) return;
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(); }
}
void CallAudioEngine::setCaptureDevice(const QString& id) {
if (id == m_captureId) return;
m_captureId = id;
m_capture.close(); m_capture.retryNow();
m_capturePos = 0;
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();
call_audio_begin_end(callId);
uint64_t generation = m_generation;
QTimer::singleShot(500, this, [this, generation]() { if (m_generation == generation) stop(); });
}
void CallAudioEngine::stop() {
++m_generation;
m_recoveryTimer.stop();
m_active = false;
pauseDevices();
uint64_t id = m_callId.exchange(0);
if (id) call_audio_release(id);
m_muted = false;
m_ending = false;
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) {
const int16_t* in16 = (const int16_t*)in;
int16_t* out16 = (int16_t*)out;
uint64_t startTb = get_time_tb();
uint64_t idleTb = m_dbg.lastEndTb ? startTb - m_dbg.lastEndTb : 0;
if (!m_dbg.lastReportTb) m_dbg.lastReportTb = startTb;
/* диагностика (DEBUG_CATEGORY_DEBUG): фактические параметры miniaudio-коллбэка */
if (m_dbg.frames != frames) {
m_dbg.frames = frames;
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "CallAudio: callback frames=%u in=%s out=%s",
frames, in16 ? "yes" : "no", out16 ? "yes" : "no");
}
/* захват: копим 960 samples → encode+send (сервисный слой). mute → тишина. */
uint64_t captureStartTb = get_time_tb();
if (in16) {
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++] = muted ? 0 : in16[i];
if (m_capturePos >= (int)m_captureBuf.size()) {
call_audio_feed_pcm(m_callId.load(), m_captureBuf.data(), (int)m_captureBuf.size());
for (unsigned int i = 0; i < frames; ++i) {
m_captureBuf[m_capturePos++] = muted ? 0 : pcm[i];
if (m_capturePos == CALL_AUDIO_FRAME_SAMPLES) {
int result = call_audio_feed_pcm(m_callId.load(), m_captureBuf.data(), CALL_AUDIO_FRAME_SAMPLES);
if (result != 0) audio_diag_event(m_capture.device(), AUDIO_DIAG_ERROR, AUDIO_DIAG_FEED, 1, result);
m_capturePos = 0;
}
}
}
uint64_t captureEndTb = get_time_tb();
/* воспроизведение: финальный PCM из сервисного слоя (jitter+stretch+тоны) */
int n = call_audio_pull_pcm(m_callId.load(), out16, (int)frames);
if (n < (int)frames)
std::memset(out16 + n, 0, (size_t)(frames - n) * sizeof(int16_t));
uint64_t endTb = get_time_tb();
uint64_t captureTb = captureEndTb - captureStartTb;
uint64_t playbackTb = endTb - captureEndTb;
m_dbg.callbacks++;
m_dbg.captureFrames += in16 ? frames : 0;
m_dbg.playbackFrames += out16 ? frames : 0;
m_dbg.maxIdleTb = std::max(m_dbg.maxIdleTb, idleTb);
m_dbg.maxCaptureTb = std::max(m_dbg.maxCaptureTb, captureTb);
m_dbg.maxPlaybackTb = std::max(m_dbg.maxPlaybackTb, playbackTb);
if (idleTb > 500 || endTb - startTb > 200) {
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "CallAudio: stall call=%016llx idle=%.1fms work=%.1fms capture=%.1fms playback=%.1fms frames=%u",
(unsigned long long)m_callId.load(), idleTb / 10.0, (endTb - startTb) / 10.0,
captureTb / 10.0, playbackTb / 10.0, frames);
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));
}
uint64_t elapsedTb = endTb - m_dbg.lastReportTb;
if (elapsedTb >= 10000) {
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG,
"CallAudio: interval=%.1fms cb=%.1f/s capture=%.0f/s playback=%.0f/s max_idle=%.1fms max_capture=%.1fms max_playback=%.1fms",
elapsedTb / 10.0, m_dbg.callbacks * 10000.0 / elapsedTb,
m_dbg.captureFrames * 10000.0 / elapsedTb, m_dbg.playbackFrames * 10000.0 / elapsedTb,
m_dbg.maxIdleTb / 10.0, m_dbg.maxCaptureTb / 10.0, m_dbg.maxPlaybackTb / 10.0);
m_dbg.callbacks = 0;
m_dbg.captureFrames = 0;
m_dbg.playbackFrames = 0;
m_dbg.maxIdleTb = 0;
m_dbg.maxCaptureTb = 0;
m_dbg.maxPlaybackTb = 0;
m_dbg.lastReportTb = endTb;
}
m_dbg.lastEndTb = get_time_tb();
}
+17 -24
View File
@@ -4,6 +4,8 @@
#include <atomic>
#include <cstdint>
#include <vector>
#include <QTimer>
#include "audio_device.h"
struct ma_device;
@@ -12,8 +14,7 @@ struct ma_device;
* слое src/call/call_audio.c; сюда приходит/отсюда уходит финальный PCM.
*
* Потоки:
* - аудио-поток (miniaudio callback): capture → call_audio_feed_pcm,
* call_audio_pull_pcm → playback;
* - независимые callbacks capture → call_audio_feed_pcm и call_audio_pull_pcm → playback;
* - GUI-поток: start()/beginEnd()/stop() по ACCEPTED/ENDED. */
class CallAudioEngine : public QObject {
Q_OBJECT
@@ -36,40 +37,32 @@ public:
void setMuted(bool muted);
bool isMuted() const { return m_muted.load(); }
/* Устройство вывода звонка (-1 = default). Смена на лету пересоздаёт ma_device. */
void setPlaybackDeviceIndex(int index);
int playbackDeviceIndex() const { return m_playbackDeviceIndex; }
/* Backend ID (пустой = default). Смена пересоздаёт только соответствующее устройство. */
void setPlaybackDevice(const QString& id);
QString playbackDevice() const { return m_playbackId; }
void setCaptureDevice(const QString& id);
/* аудио-поток: miniaudio data callback (захват + воспроизведение) */
void dataCallback(const void* in, void* out, unsigned int frames);
private:
CallAudioEngine() = default;
void teardownDevice();
int initDevice();
CallAudioEngine();
bool openDevice(bool playback);
void recoverDevices();
void pauseDevices();
std::atomic<bool> m_active{false};
std::atomic<uint64_t> m_callId{0};
std::atomic<bool> m_muted{false};
int m_playbackDeviceIndex = -1; /* только GUI-поток */
QString m_playbackId; /* только GUI-поток */
ma_device* m_device = nullptr;
bool m_deviceStarted = false;
QString m_captureId;
AudioDevice m_capture, m_playback;
QTimer m_recoveryTimer;
uint64_t m_generation = 0;
bool m_ending = false;
std::vector<int16_t> m_captureBuf; /* 960 samples, аудио-поток */
int m_capturePos = 0;
/* Только аудио-поток; сброс до ma_device_start. Время в 0.1мс. */
struct {
unsigned int frames = 0;
uint32_t callbacks = 0;
uint64_t captureFrames = 0;
uint64_t playbackFrames = 0;
uint64_t lastReportTb = 0;
uint64_t lastEndTb = 0;
uint64_t maxIdleTb = 0;
uint64_t maxCaptureTb = 0;
uint64_t maxPlaybackTb = 0;
} m_dbg;
};
+6 -6
View File
@@ -279,16 +279,16 @@ void CallWindow::setState(State s) {
void CallWindow::populateDeviceCombo() {
m_deviceCombo->blockSignals(true);
m_deviceCombo->clear();
m_deviceCombo->addItem(QStringLiteral("Default (auto)"), -1);
m_deviceCombo->addItem(QStringLiteral("Default (auto)"), QString());
const auto devs = SoundManager::enumeratePlaybackDevices();
for (int i = 0; i < devs.size(); i++) {
QString label = devs[i].name;
if (devs[i].isDefault) label += QStringLiteral(" (default)");
m_deviceCombo->addItem(label, i);
m_deviceCombo->addItem(label, devs[i].id);
}
int cur = CallAudioEngine::instance()->playbackDeviceIndex();
QString cur = CallAudioEngine::instance()->playbackDevice();
for (int i = 0; i < m_deviceCombo->count(); i++) {
if (m_deviceCombo->itemData(i).toInt() == cur) { m_deviceCombo->setCurrentIndex(i); break; }
if (m_deviceCombo->itemData(i).toString() == cur) { m_deviceCombo->setCurrentIndex(i); break; }
}
m_deviceCombo->blockSignals(false);
}
@@ -332,8 +332,8 @@ void CallWindow::onMuteToggled(bool checked) {
}
void CallWindow::onDeviceChanged(int index) {
int devIdx = m_deviceCombo->itemData(index).toInt();
CallAudioEngine::instance()->setPlaybackDeviceIndex(devIdx);
QString id = m_deviceCombo->itemData(index).toString();
CallAudioEngine::instance()->setPlaybackDevice(id);
}
void CallWindow::closeEvent(QCloseEvent* event) {
+9 -8
View File
@@ -19,6 +19,7 @@
#include "voicemessageencoder.h"
#include "callwindow.h"
#include "call_audio_engine.h"
#include "radio_audio_engine.h"
#include "ptt_hotkey_manager.h"
extern "C" {
@@ -177,7 +178,8 @@ MainWindow::~MainWindow() {
gui_bridge_set_channel_peers_online_cb(nullptr);
gui_bridge_set_db_ready_cb(nullptr);
gui_bridge_set_attachment_downloaded_cb(nullptr);
QCoreApplication::processEvents(QEventLoop::AllEvents);
CallAudioEngine::instance()->stop();
RadioAudioEngine::instance()->stop();
if (m_recorder) { m_recorder->shutdown(); m_recorder = nullptr; }
SoundManager::instance()->shutdown();
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "MainWindow: ~MainWindow — saving channel state");
@@ -335,6 +337,7 @@ void MainWindow::setupSoundFromConfig() {
if (!connectFile.isEmpty()) sm->loadSound("connect", resolvePath(connectFile));
if (!disconnectFile.isEmpty()) sm->loadSound("disconnect", resolvePath(disconnectFile));
if (m_db && m_db->isOpen()) {
sm->setDevices(m_db->getUiState("audio_output_id"), m_db->getUiState("audio_input_id"));
sm->setEnabled(m_db->getUiStateInt("sound_enabled", 1));
sm->setVolume(m_db->getUiStateInt("sound_volume", 80) / 100.0f);
QString se[] = {"msg_incoming", "msg_outgoing", "connect", "disconnect",
@@ -349,12 +352,11 @@ void MainWindow::setupSoundFromConfig() {
if (m_recorder) {
m_recorder->init();
if (m_db && m_db->isOpen()) {
m_recorder->setCaptureDeviceIndex(m_db->getUiStateInt("audio_input_device", -1));
VoiceEncoder::setPreset(m_db->getUiStateInt("opus_codec_preset", 1));
int compEnabled = m_db->getUiStateInt("compressor_enabled", 0);
m_recorder->setCompressorEnabled(compEnabled != 0);
if (m_recorder->compressor()) {
{
audio_compressor_config_t compCfg = {0};
compCfg.sample_rate = 48000;
compCfg.channels = 1;
@@ -363,11 +365,11 @@ void MainWindow::setupSoundFromConfig() {
compCfg.lookahead_ms = 100;
compCfg.rise_rate_per_sec = (float)m_db->getUiStateInt("compressor_rise_rate", 10);
compCfg.target_level = 1.0f;
audio_compressor_configure(m_recorder->compressor(), &compCfg);
m_recorder->configureCompressor(compCfg);
}
}
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "AudioRecorder initialized (captureDevice=%d, opusPreset=%d, compressor=%d)",
m_recorder->captureDeviceIndex(), VoiceEncoder::preset(),
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "AudioRecorder initialized (opusPreset=%d, compressor=%d)",
VoiceEncoder::preset(),
m_recorder->isCompressorEnabled() ? 1 : 0);
}
}
@@ -598,8 +600,7 @@ void MainWindow::onCallAccepted(quint64 callId) {
SoundManager::instance()->stopLoop();
CallWindow* w = m_activeCalls.value(callId, nullptr);
if (w) w->setState(CallWindow::Active);
if (m_db)
CallAudioEngine::instance()->setPlaybackDeviceIndex(m_db->getUiStateInt("audio_output_device", -1));
if (!s_mainWindow) return;
CallAudioEngine::instance()->start(callId);
}
+119 -178
View File
@@ -1,5 +1,6 @@
#include "radio_audio_engine.h"
#include "sound_manager.h"
#include "../transport/audio_diagnostics.h"
#include "../transport/gui_bridge.h"
#include "miniaudio.h"
@@ -8,162 +9,140 @@ extern "C" {
#include "radio/radio_audio.h"
#include "chat/chat_setting.h"
#include "../../lib/debug_config.h"
#include "../../lib/u_async.h"
}
#include <cstring>
static RadioAudioEngine* g_radioAudioEngine = nullptr;
static void radioAudioCaptureCallback(ma_device* dev, void* out, const void* in, ma_uint32 frames) {
(void)out;
RadioAudioEngine* e = (RadioAudioEngine*)dev->pUserData;
if (e) e->captureCallback(in, frames);
}
static void radioAudioPlaybackCallback(ma_device* dev, void* out, const void* in, ma_uint32 frames) {
(void)in;
RadioAudioEngine* e = (RadioAudioEngine*)dev->pUserData;
if (e) e->playbackCallback(out, frames);
}
#include <algorithm>
RadioAudioEngine* RadioAudioEngine::instance() {
if (!g_radioAudioEngine) g_radioAudioEngine = new RadioAudioEngine();
return g_radioAudioEngine;
static auto* engine = new RadioAudioEngine();
return engine;
}
RadioAudioEngine::RadioAudioEngine() {
m_captureId = SoundManager::instance()->captureDevice();
m_playbackId = SoundManager::instance()->playbackDevice();
connect(SoundManager::instance(), &SoundManager::devicesChanged, this, [this]() {
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();
}
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();
}
recoverDevices();
});
m_recoveryTimer.setInterval(200);
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();
recoverDevices();
});
}
int RadioAudioEngine::start(quint64 groupId) {
if (m_active) {
if (m_groupId == groupId) return 0;
stop();
if (m_active && m_groupId == groupId) return 0;
stop();
auto* inst = gui_bridge_get_inst();
if (!inst || radio_audio_start(groupId, 0) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "RadioAudio: cannot start group=%016llx", (unsigned long long)groupId);
return -1;
}
m_groupId = groupId;
struct UTUN_INSTANCE* inst = gui_bridge_get_inst();
if (!inst) {
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "RadioAudio: no instance, cannot start");
return -1;
}
if (radio_audio_init(inst) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "RadioAudio: radio_audio_init failed");
return -1;
}
/* желаемые каналы захвата из настройки; initDevice() при недоступности откатывает на моно */
m_captureChannels = chat_setting_get_int(inst, "radio_capture_stereo", 0) ? 2 : 1;
if (initDevice() != 0) {
radio_audio_stop();
return -1;
}
if (radio_audio_start(groupId, m_captureChannels) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "RadioAudio: radio_audio_start failed grp=%016llx", (unsigned long long)groupId);
teardownDevice();
radio_audio_stop();
return -1;
}
/* VAD авто-PTT: в этом режиме захват фидится постоянно, решение «говорить» — в сервисном слое. */
m_vadMode = radio_audio_vad_mode() ? true : false;
if (m_vadMode)
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "RadioAudio: VAD auto-PTT enabled grp=%016llx", (unsigned long long)groupId);
m_captureBuf.assign((size_t)RADIO_AUDIO_FRAME_SAMPLES * m_captureChannels, 0);
m_capturePos = 0;
m_wantStereo = chat_setting_get_int(inst, "radio_capture_stereo", 0) != 0;
m_active = true;
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "RadioAudio: started grp=%016llx captureCh=%d", (unsigned long long)groupId, m_captureChannels);
m_capture.retryNow(); m_playback.retryNow();
recoverDevices();
m_recoveryTimer.start();
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "RadioAudio: listening group=%016llx", (unsigned long long)groupId);
return 0;
}
int RadioAudioEngine::initDevice() {
ma_context* ctx = SoundManager::instance()->context();
if (!ctx) {
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "RadioAudio: no audio context");
return -1;
}
/* ── capture-устройство (стерео → откат на моно → дефолтное устройство) ── */
const int wantCh = m_captureChannels;
int capCh = 0;
for (int useDefault = 0; useDefault < 2 && capCh == 0; useDefault++) {
for (int ch = wantCh; ch >= 1; ch--) {
ma_device* dev = new ma_device;
std::memset(dev, 0, sizeof(*dev));
ma_device_config config = ma_device_config_init(ma_device_type_capture);
config.capture.format = ma_format_s16;
config.capture.channels = ch;
config.sampleRate = RADIO_AUDIO_SAMPLE_RATE;
config.periodSizeInFrames = 480;
config.dataCallback = radioAudioCaptureCallback;
config.pUserData = this;
if (useDefault) config.capture.pDeviceID = nullptr;
if (ma_device_init(ctx, &config, dev) != MA_SUCCESS) { delete dev; continue; }
if (ma_device_start(dev) != MA_SUCCESS) { ma_device_uninit(dev); delete dev; continue; }
m_captureDevice = dev;
m_captureStarted = true;
capCh = ch;
if (ch != wantCh)
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "RadioAudio: stereo capture unavailable — fallback to mono");
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "RadioAudio: capture started ch=%d cap='%s'",
ch, dev->capture.name ? dev->capture.name : "?");
break;
void RadioAudioEngine::openCapture() {
auto* context = SoundManager::instance()->context();
if (!context) return;
for (int channels = m_wantStereo ? 2 : 1; channels >= 1; --channels) {
ma_device_config config = ma_device_config_init(ma_device_type_capture);
config.capture.format = ma_format_s16;
config.capture.channels = channels;
config.sampleRate = RADIO_AUDIO_SAMPLE_RATE;
auto callback = [this](const void* in, void*, unsigned int frames) { captureCallback(in, frames); };
ma_device_id id;
bool selected = SoundManager::deviceId(m_captureId, &id);
for (int attempt = 0; attempt < (selected ? 2 : 1); ++attempt) {
config.capture.pDeviceID = selected && attempt == 0 ? &id : nullptr;
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 (channels == 1 && m_wantStereo) DEBUG_WARN(DEBUG_CATEGORY_RADIO, "RadioAudio: stereo input unavailable, using mono");
return;
}
}
if (capCh == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "RadioAudio: capture device init failed");
teardownDevice();
return -1;
}
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();
}
m_captureChannels = capCh;
/* ── playback-устройство (всегда стерео) ── */
m_playbackDevice = new ma_device;
std::memset(m_playbackDevice, 0, sizeof(*m_playbackDevice));
{
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;
config.periodSizeInFrames = 480;
config.dataCallback = radioAudioPlaybackCallback;
config.pUserData = this;
if (ma_device_init(ctx, &config, m_playbackDevice) != MA_SUCCESS) {
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "RadioAudio: playback device init failed");
teardownDevice();
return -1;
}
if (ma_device_start(m_playbackDevice) != MA_SUCCESS) {
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "RadioAudio: playback device start failed");
teardownDevice();
return -1;
}
m_playbackStarted = true;
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();
}
void RadioAudioEngine::pauseDevices() {
m_capture.close(); m_playback.close();
m_capturePos = 0;
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "RadioAudio: playback started ch=2 pb='%s'",
m_playbackDevice->playback.name ? m_playbackDevice->playback.name : "?");
return 0;
if (m_active) {
radio_audio_capture_stop();
radio_audio_reset_playback(m_groupId.load());
}
}
void RadioAudioEngine::stop() {
if (!m_active && !m_captureDevice && !m_playbackDevice && !m_captureStarted && !m_playbackStarted) return;
quint64 gid = m_groupId.load();
m_recoveryTimer.stop();
m_capture.close(); m_playback.close();
uint64_t group = m_groupId.exchange(0);
bool talking = m_talking.exchange(false);
m_buttonTalking = m_hotkeyTalking = false;
bool wasTalking = m_talking.exchange(false);
if (wasTalking) radio_audio_talk_end(gid); /* честный FIN: пиры дренируют звук, а не ждут таймаут */
m_active = false;
teardownDevice();
radio_audio_stop();
m_groupId = 0;
if (wasTalking) emit talkingChanged(false);
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "RadioAudio: stopped");
if (group) radio_audio_stop();
if (talking) emit talkingChanged(false);
if (group) DEBUG_INFO(DEBUG_CATEGORY_RADIO, "RadioAudio: stopped group=%016llx", (unsigned long long)group);
}
void RadioAudioEngine::talkBegin() {
@@ -192,21 +171,6 @@ void RadioAudioEngine::updateTalking() {
emit talkingChanged(talking);
}
void RadioAudioEngine::teardownDevice() {
if (m_captureStarted && m_captureDevice) {
ma_device_stop(m_captureDevice);
ma_device_uninit(m_captureDevice);
m_captureStarted = false;
}
if (m_captureDevice) { delete m_captureDevice; m_captureDevice = nullptr; }
if (m_playbackStarted && m_playbackDevice) {
ma_device_stop(m_playbackDevice);
ma_device_uninit(m_playbackDevice);
m_playbackStarted = false;
}
if (m_playbackDevice) { delete m_playbackDevice; m_playbackDevice = nullptr; }
}
void RadioAudioEngine::captureCallback(const void* in, unsigned int frames) {
const int16_t* in16 = (const int16_t*)in;
if (!in16) return;
@@ -219,7 +183,8 @@ void RadioAudioEngine::captureCallback(const void* in, unsigned int frames) {
for (int i = 0; i < totalIn; i++) {
m_captureBuf[m_capturePos++] = in16[i];
if (m_capturePos >= (int)m_captureBuf.size()) {
radio_audio_feed_pcm(m_groupId.load(), m_captureBuf.data(), (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;
}
}
@@ -228,36 +193,12 @@ void RadioAudioEngine::captureCallback(const void* in, unsigned int frames) {
void RadioAudioEngine::playbackCallback(void* out, unsigned int frames) {
int16_t* out16 = (int16_t*)out;
/* диагностика (DEBUG_CATEGORY_DEBUG): cadence playback-коллбэка */
uint64_t now = get_time_tb();
if (m_dbgFrames == 0) {
m_dbgFrames = frames;
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "RadioAudio: playback frames=%u", frames);
}
m_dbgCbCount++;
if (m_dbgPrevCbTb != 0) {
uint64_t gap = now - m_dbgPrevCbTb;
if (gap > m_dbgGapMax) m_dbgGapMax = gap;
}
m_dbgPrevCbTb = now;
if (m_dbgLastTb == 0) {
m_dbgLastTb = now;
} else if (now - m_dbgLastTb >= 10000) {
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "RadioAudio: pb cb=%u/s frames=%u gapMax=%.1fms bodyMax=%.1fms",
m_dbgCbCount, frames, (double)m_dbgGapMax / 10.0, (double)m_dbgBodyMax / 10.0);
m_dbgCbCount = 0;
m_dbgLastTb = now;
m_dbgGapMax = 0;
m_dbgBodyMax = 0;
}
/* воспроизведение: микшированный PCM из сервисного слоя (всегда стерео) */
int total = (int)frames * 2;
int n = radio_audio_pull_pcm(m_groupId.load(), out16, total);
if (n < total)
std::memset(out16 + n, 0, (size_t)(total - n) * sizeof(int16_t));
uint64_t end = get_time_tb();
uint64_t body = end - now;
if (body > m_dbgBodyMax) m_dbgBodyMax = body;
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;
}
}
+11 -15
View File
@@ -4,6 +4,8 @@
#include <atomic>
#include <cstdint>
#include <vector>
#include "audio_device.h"
#include <QTimer>
struct ma_device;
@@ -43,31 +45,25 @@ signals:
void talkingChanged(bool talking); // GUI-поток, суммарное состояние кнопки и хоткея
private:
RadioAudioEngine() = default;
RadioAudioEngine();
void updateTalking();
bool m_buttonTalking = false, m_hotkeyTalking = false; // GUI-поток
void teardownDevice();
int initDevice();
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};
ma_device* m_captureDevice = nullptr;
ma_device* m_playbackDevice = nullptr;
bool m_captureStarted = false;
bool m_playbackStarted = false;
AudioDevice m_capture, m_playback;
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=стерео — по настройке/возможности */
/* диагностика (DEBUG_CATEGORY_DEBUG): фактический cadence playback-коллбэка */
unsigned int m_dbgFrames = 0;
uint32_t m_dbgCbCount = 0;
uint64_t m_dbgLastTb = 0; /* момент предыдущего сброса статистики (для cb/s) */
uint64_t m_dbgPrevCbTb = 0; /* момент входа предыдущего коллбэка */
uint64_t m_dbgGapMax = 0; /* макс. gap между коллбэками за окно (tb 0.1мс) */
uint64_t m_dbgBodyMax = 0; /* макс. длительность тела коллбэка за окно (tb 0.1мс) */
};
+130 -18
View File
@@ -1,8 +1,11 @@
#include "miniaudio.h"
#include "sound_manager.h"
#include "../transport/audio_diagnostics.h"
#include <QFile>
#include <QDebug>
#include <algorithm>
#include <cstring>
#include "../../lib/debug_config.h"
SoundManager* SoundManager::instance() {
@@ -15,30 +18,100 @@ SoundManager::~SoundManager() {
shutdown();
}
extern "C" int audio_context_failed(ma_context* context);
SoundManager::SoundManager() {
m_clock.start();
m_recoveryTimer.setInterval(200);
connect(&m_recoveryTimer, &QTimer::timeout, this, &SoundManager::recoverAudio);
}
bool SoundManager::init() {
if (m_initialized) return true;
ma_engine* engine = new ma_engine;
ma_result result = ma_engine_init(NULL, engine);
audio_diag_start();
ma_engine_config config = ma_engine_config_init();
config.noDevice = MA_TRUE; /* Микшер не владеет контекстом/устройством; его PCM и звуки переживают recovery. */
config.channels = 2;
config.sampleRate = 48000;
auto* engine = new ma_engine{};
ma_result result = ma_engine_init(&config, engine);
if (result != MA_SUCCESS) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "SoundManager: ma_engine_init failed (%d)", result);
delete engine;
audio_diag_stop();
return false;
}
m_engine = engine;
m_context = ma_device_get_context(ma_engine_get_device(engine));
m_initialized = true;
ma_device* dev = ma_engine_get_device(engine);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL,
"SoundManager: initialized engine=%p ctx=%p dev='%s' fmt=%d ch=%u sr=%u internalSr=%u",
(void*)engine, (void*)m_context,
dev ? dev->playback.name : "?", dev ? (int)dev->playback.format : -1,
dev ? dev->playback.channels : 0, dev ? dev->sampleRate : 0,
dev ? dev->playback.internalSampleRate : 0);
m_contextRetryAt = 0;
recoverAudio();
m_recoveryTimer.start();
DEBUG_INFO(DEBUG_CATEGORY_CALL, "SoundManager: mixer initialized independently of output");
return true;
}
bool SoundManager::openContext() {
auto* context = new ma_context{};
ma_result result = ma_context_init(m_backendSelected ? &m_backend : nullptr, m_backendSelected ? 1 : 0, nullptr, context);
if (result != MA_SUCCESS) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "SoundManager: context init failed result=%d retry=%dms", result, m_contextRetryDelay);
delete context;
m_contextRetryAt = m_clock.elapsed() + m_contextRetryDelay;
m_contextRetryDelay = std::min(m_contextRetryDelay * 2, 2000);
return false;
}
m_context = context;
m_backend = context->backend;
m_backendSelected = true;
m_contextRetryDelay = 250;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "SoundManager: context ready backend=%s", ma_get_backend_name(m_backend));
emit contextRestored();
return true;
}
void SoundManager::openOutput() {
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(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);
};
if (m_output.init(m_context, config, "sound-engine", 0, callback) && m_output.start()) 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();
}
void SoundManager::recoverAudio() {
if (!m_initialized) return;
if (m_context && audio_context_failed(m_context)) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "SoundManager: context failed, closing dependent devices");
emit contextAboutToReset();
m_output.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();
}
void SoundManager::setDevices(const QString& output, const QString& input) {
if (output == m_playbackId && input == m_captureId) return;
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(); }
emit devicesChanged();
recoverAudio();
}
void SoundManager::clearCurrentPcm() {
if (m_currentPcmSound) { ma_sound_uninit(m_currentPcmSound); delete m_currentPcmSound; m_currentPcmSound = nullptr; }
if (m_currentPcmBuffer) {
@@ -51,7 +124,10 @@ void SoundManager::clearCurrentPcm() {
void SoundManager::shutdown() {
if (!m_initialized) return;
m_recoveryTimer.stop();
m_initialized = false;
emit contextAboutToReset();
m_output.close();
clearCurrentPcm();
for (auto it = m_sounds.begin(); it != m_sounds.end(); ++it) {
@@ -63,7 +139,8 @@ void SoundManager::shutdown() {
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "SoundManager: before ma_engine_uninit engine=%p", (void*)m_engine);
if (m_engine) { ma_engine_uninit(m_engine); delete m_engine; m_engine = nullptr; }
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "SoundManager: after ma_engine_uninit");
m_initialized = false;
if (m_context) { ma_context_uninit(m_context); delete m_context; m_context = nullptr; }
audio_diag_stop();
m_loopName.clear();
}
@@ -279,6 +356,11 @@ static QString decodeDeviceName(const char* name) {
return s;
}
static QString deviceKey(ma_context* context, const ma_device_id& id) {
QByteArray bytes(reinterpret_cast<const char*>(&id), sizeof(id));
return QString::fromLatin1(ma_get_backend_name(context->backend)) + '/' + QString::fromLatin1(bytes.toBase64());
}
QVector<SoundManager::DeviceInfo> SoundManager::enumeratePlaybackDevices() {
QVector<DeviceInfo> result;
ma_context* ctx = instance()->m_context;
@@ -286,9 +368,10 @@ QVector<SoundManager::DeviceInfo> SoundManager::enumeratePlaybackDevices() {
ma_device_info* pDevices = nullptr;
ma_uint32 count = 0;
if (ma_context_get_devices(ctx, &pDevices, &count, nullptr, nullptr) != MA_SUCCESS) return result;
ma_result status = ma_context_get_devices(ctx, &pDevices, &count, nullptr, nullptr);
if (status != MA_SUCCESS) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: output enumeration failed result=%d", status); return result; }
for (ma_uint32 i = 0; i < count; i++)
result.append({decodeDeviceName(pDevices[i].name), pDevices[i].isDefault != MA_FALSE});
result.append({decodeDeviceName(pDevices[i].name), pDevices[i].isDefault != MA_FALSE, deviceKey(ctx, pDevices[i].id)});
return result;
}
@@ -299,8 +382,37 @@ QVector<SoundManager::DeviceInfo> SoundManager::enumerateCaptureDevices() {
ma_device_info* pDevices = nullptr;
ma_uint32 count = 0;
if (ma_context_get_devices(ctx, nullptr, nullptr, &pDevices, &count) != MA_SUCCESS) return result;
ma_result status = ma_context_get_devices(ctx, nullptr, nullptr, &pDevices, &count);
if (status != MA_SUCCESS) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: input enumeration failed result=%d", status); return result; }
for (ma_uint32 i = 0; i < count; i++)
result.append({decodeDeviceName(pDevices[i].name), pDevices[i].isDefault != MA_FALSE});
result.append({decodeDeviceName(pDevices[i].name), pDevices[i].isDefault != MA_FALSE, deviceKey(ctx, pDevices[i].id)});
return result;
}
bool SoundManager::deviceId(const QString& key, ma_device_id* id) {
if (key.isEmpty()) return false;
ma_context* context = instance()->context();
if (!context) return false;
QString prefix = QString::fromLatin1(ma_get_backend_name(context->backend)) + '/';
QByteArray bytes = QByteArray::fromBase64(key.mid(prefix.size()).toLatin1());
if (!key.startsWith(prefix) || bytes.size() != sizeof(*id) || QString::fromLatin1(bytes.toBase64()) != key.mid(prefix.size())) {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio: invalid or different-backend device ID, using default backend=%s",
ma_get_backend_name(context->backend));
return false;
}
std::memcpy(id, bytes.constData(), sizeof(*id));
/* Сохранённая строка должна иметь terminator внутри соответствующего поля backend. */
bool valid = true;
switch (context->backend) {
case ma_backend_pulseaudio: valid = std::memchr(id->pulse, 0, sizeof(id->pulse)); break;
case ma_backend_alsa: valid = std::memchr(id->alsa, 0, sizeof(id->alsa)); break;
case ma_backend_coreaudio: valid = std::memchr(id->coreaudio, 0, sizeof(id->coreaudio)); break;
case ma_backend_sndio: valid = std::memchr(id->sndio, 0, sizeof(id->sndio)); break;
case ma_backend_audio4: valid = std::memchr(id->audio4, 0, sizeof(id->audio4)); break;
case ma_backend_oss: valid = std::memchr(id->oss, 0, sizeof(id->oss)); break;
case ma_backend_wasapi: valid = std::find(std::begin(id->wasapi), std::end(id->wasapi), 0) != std::end(id->wasapi); break;
default: break;
}
if (!valid) DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio: unterminated device ID, using default");
return valid;
}
+24 -1
View File
@@ -7,6 +7,8 @@
#include <vector>
#include <memory>
#include <cstdint>
#include <QTimer>
#include "audio_device.h"
struct ma_engine;
struct ma_context;
@@ -40,6 +42,10 @@ public:
void stopLoop();
ma_context* context();
static bool deviceId(const QString& key, ma_device_id* id);
void setDevices(const QString& output, const QString& input);
QString playbackDevice() const { return m_playbackId; }
QString captureDevice() const { return m_captureId; }
/* owner удерживает готовый PCM без копии в GUI; без owner короткие буферы копируются. */
void playRawPcm(const int16_t* pcm, int sampleCount, int sampleRate, int channels,
@@ -55,15 +61,24 @@ public:
struct DeviceInfo {
QString name;
bool isDefault = false;
QString id;
};
static QVector<DeviceInfo> enumeratePlaybackDevices();
static QVector<DeviceInfo> enumerateCaptureDevices();
signals:
void contextAboutToReset(); // Все зависимые устройства закрываются синхронно перед uninit контекста.
void contextRestored();
void devicesChanged();
private:
SoundManager() = default;
SoundManager();
~SoundManager();
void clearCurrentPcm();
void recoverAudio();
bool openContext();
void openOutput();
struct LoadedSound {
QByteArray mp3Data;
@@ -73,6 +88,14 @@ private:
ma_engine* m_engine = nullptr;
ma_context* m_context = nullptr;
AudioDevice m_output;
QTimer m_recoveryTimer;
QElapsedTimer m_clock;
qint64 m_contextRetryAt = 0;
int m_contextRetryDelay = 250;
QString m_playbackId, m_captureId;
ma_backend m_backend = ma_backend_null;
bool m_backendSelected = false;
ma_sound* m_currentPcmSound = nullptr;
void* m_currentPcmBuffer = nullptr;
int m_currentPcmSampleRate = 0;