Сохрани накопленные исправления аудио, async и диагностики
This commit is contained in:
1 parent
b1e3193899
commit
bdcf8b5634
59 files changed
+4611
-1441
No files matched your search
@@ -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; }
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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мс) */
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user