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