Сохрани накопленные исправления аудио, 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

+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;
}