You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

665 lines
30 KiB

#include "miniaudio.h"
#include "sound_manager.h"
#include "voice_audio_io.h"
#include "../transport/audio_diagnostics.h"
#include <QFile>
#include <QDebug>
#include <algorithm>
#include <cstring>
#include <cmath>
#include "../../lib/debug_config.h"
SoundManager* SoundManager::instance() {
static SoundManager s;
return &s;
}
SoundManager::~SoundManager() {
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "SoundManager: static destructor entered");
shutdown();
}
extern "C" int audio_context_failed(ma_context* context);
static uint64_t pulse_output_key(const QString& key) {
ma_device_id id{};
if (!SoundManager::deviceId(key, &id)) return 0;
uint64_t hash = UINT64_C(14695981039346656037);
for (const char* name = id.pulse; *name; ++name) { hash ^= (unsigned char)*name; hash *= UINT64_C(1099511628211); }
return hash;
}
SoundManager::SoundManager() {
m_clock.start();
m_recoveryTimer.setInterval(200);
connect(&m_recoveryTimer, &QTimer::timeout, this, &SoundManager::recoverAudio);
}
bool SoundManager::init() {
if (m_initialized) return true;
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_initialized = true;
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));
m_resetting = false;
emit contextRestored();
return true;
}
void SoundManager::openOutput() {
for (auto* voice : m_voices) if (voice) voice->resetPlayback();
if (m_output.initialized() && m_output.start()) return;
m_restoreSelections = true;
ma_device_config config = ma_device_config_init(ma_device_type_playback);
config.playback.format = ma_format_f32;
config.playback.channels = 2;
config.sampleRate = 48000;
ma_device_id id;
bool selected = deviceId(m_playbackId, &id);
if (selected) config.playback.pDeviceID = &id;
auto callback = [this, previousRoute = uint64_t(0)](const void*, void* out, unsigned int frames) mutable {
uint64_t route = m_output.route();
if (previousRoute && route != previousRoute) for (auto* voice : m_voices) if (voice) voice->resetPlayback();
previousRoute = route;
m_renderErrors.fetch_add(renderOutput(m_engine, m_voices, static_cast<float*>(out), frames, m_output.callbackTimeUs()));
};
if (m_output.init(m_context, config, "sound-engine", 0, callback) && m_output.start()) {
m_actualOutput = resolveOutput(m_playbackId); return;
}
if (!selected) return;
DEBUG_WARN(DEBUG_CATEGORY_CALL, "SoundManager: selected output unavailable, trying default");
config.playback.pDeviceID = nullptr;
if (m_output.init(m_context, config, "sound-engine", 0, callback) && m_output.start()) m_actualOutput = resolveOutput({});
}
void SoundManager::recoverAudio() {
if (!m_initialized) return;
unsigned errors = m_renderErrors.exchange(0);
if (errors) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "audio: mixer read failures=%u; output replaced with silence", errors);
for (auto it = m_pcmSessions.begin(); it != m_pcmSessions.end();) {
if (it->second->transient && !isPcmPlaying(it->first)) it = m_pcmSessions.erase(it);
else ++it;
}
if (m_context && audio_context_failed(m_context)) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "SoundManager: context failed, closing dependent devices");
m_resetting = true;
emit contextAboutToReset();
m_output.close();
for (auto& [key, route] : m_voiceRoutes) route->device.close();
ma_context_uninit(m_context); delete m_context; m_context = nullptr;
m_contextRetryAt = m_clock.elapsed() + 250;
}
if (!m_context && m_clock.elapsed() >= m_contextRetryAt) openContext();
if (!m_context) return;
if (m_output.needsRecovery()) {
m_output.close();
for (auto* voice : m_voices) if (voice) voice->resetPlayback();
m_output.retryLater();
}
bool reopened = m_output.retryReady();
if (reopened) openOutput();
for (auto& [key, route] : m_voiceRoutes) {
if (route->device.needsRecovery()) {
route->device.close();
for (auto* voice : route->voices) if (voice) voice->resetPlayback();
route->device.retryLater();
}
if (route->device.retryReady()) openVoiceRoute(*route);
}
mergeVoiceRoutes();
if (m_restoreSelections && m_output.initialized() && (m_backend != ma_backend_pulseaudio || m_output.route())) {
m_restoreSelections = false;
auto voices = m_voices;
for (auto* voice : voices) if (voice) {
QString wanted = m_voiceSelections.at(voice);
bool same = m_backend == ma_backend_pulseaudio ? pulse_output_key(wanted) == m_output.route() : wanted == m_actualOutput;
if (!wanted.isEmpty() && wanted == resolveOutput(wanted) && !same) attachVoice(voice, wanted);
}
}
}
/* Последнее смешивание и ограничение: тот же PCM передаётся устройству и всем AEC этого выхода. */
unsigned SoundManager::renderOutput(ma_engine* engine, const std::array<VoiceAudioIo*, 2>& voices,
float* out, unsigned frames, int64_t firstTimeUs) {
unsigned errors = 0;
for (unsigned offset = 0; offset < frames;) {
unsigned count = std::min(frames - offset, 960u);
float* destination = out + offset * 2;
std::fill_n(destination, count * 2, 0.0f);
if (engine && ma_engine_read_pcm_frames(engine, destination, count, nullptr) != MA_SUCCESS) {
++errors; std::fill_n(destination, count * 2, 0.0f);
}
for (auto* voice : voices) if (voice) voice->mix(destination, count, firstTimeUs + (int64_t)offset * 1000000 / 48000);
for (unsigned i = 0; i < count * 2; ++i) {
float value = destination[i];
destination[i] = std::isfinite(value) ? std::clamp(value, -1.0f, 1.0f) : 0.0f;
}
for (auto* voice : voices) if (voice)
voice->reference(destination, count, firstTimeUs + (int64_t)offset * 1000000 / 48000);
offset += count;
}
return errors;
}
/* Default и явный ID одного устройства получают один микшер. */
QString SoundManager::resolveOutput(const QString& requested) {
auto devices = enumeratePlaybackDevices();
for (const auto& device : devices) if (device.id == requested) return requested;
for (const auto& device : devices) if (device.isDefault) return device.id;
return {};
}
bool SoundManager::openVoiceRoute(VoiceRoute& route) {
if (!m_context || m_resetting) return false;
for (auto* voice : route.voices) if (voice) voice->resetPlayback();
if (route.device.initialized() && route.device.start()) return true;
ma_device_config config = ma_device_config_init(ma_device_type_playback);
config.playback.format = ma_format_f32; config.playback.channels = 2; config.sampleRate = 48000;
ma_device_id id;
bool selected = deviceId(route.wanted, &id);
if (selected) config.playback.pDeviceID = &id;
auto callback = [this, &route, previousRoute = uint64_t(0)](const void*, void* out, unsigned frames) mutable {
uint64_t physical = route.device.route();
if (previousRoute && physical != previousRoute) for (auto* voice : route.voices) if (voice) voice->resetPlayback();
previousRoute = physical;
if (physical && physical == m_output.route()) {
std::fill_n(static_cast<float*>(out), frames * 2, 0.0f); return; // GUI объединит совпавшие маршруты.
}
renderOutput(nullptr, route.voices, static_cast<float*>(out), frames, route.device.callbackTimeUs());
};
if (route.device.init(m_context, config, "voice-output", 0, callback) && route.device.start()) {
route.actual = resolveOutput(route.wanted); return true;
}
if (!selected) return false;
DEBUG_WARN(DEBUG_CATEGORY_CALL, "audio: voice output unavailable, trying default");
config.playback.pDeviceID = nullptr;
if (route.device.init(m_context, config, "voice-output", 0, callback) && route.device.start()) {
route.actual = resolveOutput({}); return true;
}
return false;
}
bool SoundManager::attachVoice(VoiceAudioIo* voice, const QString& output) {
detachVoice(voice);
if (!voice || !m_initialized || !m_context || m_resetting) return false;
size_t attached = std::count_if(m_voices.begin(), m_voices.end(), [](auto* v) { return v != nullptr; });
for (const auto& [key, route] : m_voiceRoutes)
attached += std::count_if(route->voices.begin(), route->voices.end(), [](auto* v) { return v != nullptr; });
if (attached >= 2) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: only one call and one radio source are supported"); return false; }
QString key = resolveOutput(output);
QString root = m_actualOutput.isEmpty() ? resolveOutput(m_playbackId) : m_actualOutput;
bool same = key == root;
if (m_backend == ma_backend_pulseaudio && m_output.route()) same = pulse_output_key(key) == m_output.route();
if (same) {
m_output.pause();
auto slot = std::find(m_voices.begin(), m_voices.end(), nullptr);
if (slot == m_voices.end()) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: too many voice sources on output"); openOutput(); return false; }
*slot = voice; voice->resetPlayback();
openOutput();
} else {
auto& route = m_voiceRoutes[key];
if (!route) { route = std::make_unique<VoiceRoute>(); route->wanted = key; }
route->device.pause();
auto slot = std::find(route->voices.begin(), route->voices.end(), nullptr);
if (slot == route->voices.end()) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: too many voice sources on separate output"); openVoiceRoute(*route); return false; }
*slot = voice; voice->resetPlayback();
openVoiceRoute(*route);
mergeVoiceRoutes();
}
DEBUG_INFO(DEBUG_CATEGORY_CALL, "audio: voice attached to shared output default=%d", output.isEmpty());
m_voiceSelections[voice] = output;
return true;
}
void SoundManager::detachVoice(VoiceAudioIo* voice) {
if (!voice) return;
m_voiceSelections.erase(voice);
auto slot = std::find(m_voices.begin(), m_voices.end(), voice);
if (slot != m_voices.end()) {
m_output.pause(); *slot = nullptr;
if (m_initialized && m_context && !m_resetting) openOutput();
}
for (auto it = m_voiceRoutes.begin(); it != m_voiceRoutes.end();) {
auto& route = *it->second;
auto entry = std::find(route.voices.begin(), route.voices.end(), voice);
if (entry == route.voices.end()) { ++it; continue; }
route.device.pause(); *entry = nullptr;
if (std::all_of(route.voices.begin(), route.voices.end(), [](auto* v) { return !v; })) it = m_voiceRoutes.erase(it);
else { if (!m_resetting) openVoiceRoute(route); ++it; }
}
}
/* Fallback на уже используемый выход не оставляет два независимых референса одного динамика. */
void SoundManager::mergeVoiceRoutes() {
if (m_resetting || !m_output.initialized()) return;
for (auto first = m_voiceRoutes.begin(); first != m_voiceRoutes.end(); ++first) {
auto& target = *first->second;
for (auto next = std::next(first); next != m_voiceRoutes.end();) {
auto& source = *next->second;
uint64_t physical = target.device.route();
if (!physical || physical != source.device.route()) { ++next; continue; }
target.device.pause(); source.device.close();
for (auto* voice : source.voices) if (voice) {
auto slot = std::find(target.voices.begin(), target.voices.end(), nullptr);
if (slot == target.voices.end()) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: voice route merge capacity exceeded"); return; }
*slot = voice;
}
next = m_voiceRoutes.erase(next);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "audio: merged voice streams on physical route=%016llx", (unsigned long long)physical);
openVoiceRoute(target);
}
}
for (auto it = m_voiceRoutes.begin(); it != m_voiceRoutes.end();) {
auto& route = *it->second;
bool same = m_backend == ma_backend_pulseaudio ? m_output.route() && m_output.route() == route.device.route()
: route.actual == m_actualOutput;
if (!route.device.initialized() || !same) { ++it; continue; }
m_output.pause(); route.device.close();
for (auto* voice : route.voices) if (voice) {
auto slot = std::find(m_voices.begin(), m_voices.end(), nullptr);
if (slot == m_voices.end()) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "audio: output merge capacity exceeded"); break; }
*slot = voice; voice->resetPlayback();
}
it = m_voiceRoutes.erase(it);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "audio: merged voice fallback with shared output");
openOutput();
}
}
void SoundManager::setDevices(const QString& output, const QString& input) {
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_actualOutput.clear(); m_output.retryNow(); }
emit devicesChanged();
recoverAudio();
}
/* Detach ждёт читателя графа; владелец PCM освобождается последним. */
SoundManager::PcmSession::~PcmSession() {
if (sound) { ma_sound_uninit(sound); delete sound; }
if (buffer) { ma_audio_buffer_uninit(buffer); delete buffer; }
}
SoundManager::PcmSession* SoundManager::pcmSession(PlaybackId id) const {
auto it = m_pcmSessions.find(id);
return it == m_pcmSessions.end() ? nullptr : it->second.get();
}
/* Новый узел имеет пустой read/resampler cache; общий PCM и handle сохраняются. */
bool SoundManager::createPcmNode(PcmSession& session, bool playing) {
auto* sound = new ma_sound{};
ma_result result = ma_sound_init_from_data_source(m_engine, (ma_data_source*)session.buffer, 0, nullptr, sound);
if (result != MA_SUCCESS) {
delete sound;
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "playback: node init failed result=%d", result);
return false;
}
session.sound = sound;
session.detached = false;
ma_sound_set_volume(sound, m_volume);
if (playing && ma_sound_start(sound) != MA_SUCCESS) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "playback: node start failed");
ma_sound_uninit(sound); delete sound; session.sound = nullptr;
return false;
}
return true;
}
void SoundManager::shutdown() {
if (!m_initialized) return;
m_recoveryTimer.stop();
m_initialized = false;
m_resetting = true;
emit contextAboutToReset();
m_output.close(); m_voiceRoutes.clear(); m_voices = {}; m_voiceSelections.clear();
m_pcmSessions.clear();
m_sounds.clear();
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");
if (m_context) { ma_context_uninit(m_context); delete m_context; m_context = nullptr; }
audio_diag_stop();
m_loopName.clear();
}
void SoundManager::setEnabled(bool enabled) { m_enabled = enabled; }
bool SoundManager::isEnabled() const { return m_enabled; }
void SoundManager::setVolume(float volume) {
if (volume < 0.0f) volume = 0.0f;
if (volume > 1.0f) volume = 1.0f;
m_volume = volume;
}
float SoundManager::volume() const { return m_volume; }
void SoundManager::setEvent(const QString& name, bool enabled, float volume) {
if (volume < 0.0f) volume = 0.0f;
if (volume > 1.0f) volume = 1.0f;
EventSettings es;
es.enabled = enabled;
es.volume = volume;
m_eventSettings[name] = es;
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "SoundManager: event '%s' enabled=%d vol=%.2f",
qPrintable(name), enabled, volume);
}
SoundManager::EventSettings SoundManager::eventSettings(const QString& name) const {
return m_eventSettings.value(name);
}
bool SoundManager::loadSound(const QString& name, const QString& filePath) {
if (!m_initialized) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "SoundManager: not initialized, cannot load '%s'", qPrintable(name));
return false;
}
if (m_sounds.contains(name)) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "SoundManager: sound '%s' already loaded", qPrintable(name));
return false;
}
QFile f(filePath);
if (!f.open(QIODevice::ReadOnly)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "SoundManager: cannot open '%s'", qPrintable(filePath));
return false;
}
QByteArray encoded = f.readAll();
f.close();
/* Декодируем короткие сигналы при загрузке, до playback: hardware callback читает только PCM. */
ma_decoder_config decConfig = ma_decoder_config_init(ma_format_s16, 2, 48000);
ma_uint64 frames = 0; void* decoded = nullptr;
ma_result result = ma_decode_memory(encoded.constData(), encoded.size(), &decConfig, &frames, &decoded);
if (result != MA_SUCCESS || !frames) {
ma_free(decoded, &decConfig.allocationCallbacks);
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "SoundManager: decode '%s' failed result=%d frames=%llu",
qPrintable(name), result, (unsigned long long)frames);
return false;
}
LoadedSound ls;
ls.pcm = std::make_shared<PcmSession>();
ls.pcm->frames = frames; ls.pcm->rate = 48000;
ma_audio_buffer_config bufferConfig = ma_audio_buffer_config_init(ma_format_s16, 2, frames, decoded, nullptr);
bufferConfig.sampleRate = 48000;
ls.pcm->buffer = new ma_audio_buffer{};
result = ma_audio_buffer_init_copy(&bufferConfig, ls.pcm->buffer);
ma_free(decoded, &decConfig.allocationCallbacks);
if (result != MA_SUCCESS) {
delete ls.pcm->buffer; ls.pcm->buffer = nullptr;
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "SoundManager: signal PCM init failed result=%d", result);
return false;
}
if (!createPcmNode(*ls.pcm, false)) return false;
m_sounds.insert(name, ls);
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "SoundManager: predecoded '%s' frames=%llu rate=48000 ch=2",
qPrintable(name), (unsigned long long)frames);
return true;
}
void SoundManager::play(const QString& name) {
if (!m_enabled) return;
if (!m_initialized) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "SoundManager::play: not initialized"); return; }
EventSettings es = m_eventSettings.value(name);
if (!es.enabled) return;
auto it = m_sounds.find(name);
if (it == m_sounds.end()) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "SoundManager::play: sound '%s' not loaded", qPrintable(name));
return;
}
auto& pcm = *it->pcm;
if (pcm.sound) { ma_sound_uninit(pcm.sound); delete pcm.sound; pcm.sound = nullptr; }
ma_audio_buffer_seek_to_pcm_frame(pcm.buffer, 0);
if (!createPcmNode(pcm, false)) return;
ma_sound* s = pcm.sound;
float effectiveVolume = m_volume * es.volume;
ma_sound_set_volume(s, effectiveVolume);
ma_sound_stop(s);
if (ma_sound_start(s) != MA_SUCCESS) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "SoundManager: signal start failed '%s'", qPrintable(name));
}
void SoundManager::playLoop(const QString& name) {
if (!m_enabled) return;
if (!m_initialized) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "SoundManager::playLoop: not initialized"); return; }
/* остановить предыдущий цикл (иначе call_connecting + call_ringing звучат вместе) */
stopLoop();
EventSettings es = m_eventSettings.value(name);
if (!es.enabled) return;
auto it = m_sounds.find(name);
if (it == m_sounds.end()) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "SoundManager::playLoop: sound '%s' not loaded", qPrintable(name));
return;
}
auto& pcm = *it->pcm;
if (pcm.sound) { ma_sound_uninit(pcm.sound); delete pcm.sound; pcm.sound = nullptr; }
ma_audio_buffer_seek_to_pcm_frame(pcm.buffer, 0);
if (!createPcmNode(pcm, false)) return;
ma_sound* s = pcm.sound;
ma_sound_stop(s);
ma_sound_set_volume(s, m_volume * es.volume);
ma_sound_set_looping(s, MA_TRUE);
if (ma_sound_start(s) != MA_SUCCESS) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "SoundManager: loop start failed '%s'", qPrintable(name)); return; }
m_loopName = name;
}
void SoundManager::stopLoop() {
if (m_loopName.isEmpty()) return;
auto it = m_sounds.find(m_loopName);
m_loopName.clear();
if (it == m_sounds.end()) return;
ma_sound* s = it->pcm->sound;
if (!s) return;
ma_sound_stop(s);
ma_sound_set_looping(s, MA_FALSE);
ma_sound_seek_to_pcm_frame(s, 0);
}
ma_context* SoundManager::context() {
return m_context;
}
SoundManager::PlaybackId SoundManager::playRawPcm(const int16_t* pcm, int frameCount, int sampleRate, int channels,
std::shared_ptr<std::vector<int16_t>> owner, bool transient) {
if (!m_engine || !m_initialized || !pcm || frameCount <= 0 || sampleRate <= 0 || channels < 1 || channels > 2) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "playback: invalid PCM frames=%d rate=%d ch=%d ready=%d",
frameCount, sampleRate, channels, m_initialized);
return 0;
}
if (owner && (pcm != owner->data() || (size_t)frameCount > owner->size() / channels)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "playback: PCM owner does not contain requested frames");
return 0;
}
auto session = std::make_unique<PcmSession>();
session->owner = std::move(owner);
session->frames = frameCount; session->rate = sampleRate; session->transient = transient;
ma_audio_buffer_config config = ma_audio_buffer_config_init(ma_format_s16, channels, frameCount, pcm, nullptr);
config.sampleRate = sampleRate;
auto* buffer = new ma_audio_buffer{};
ma_result result = session->owner ? ma_audio_buffer_init(&config, buffer) : ma_audio_buffer_init_copy(&config, buffer);
if (result != MA_SUCCESS) {
delete buffer;
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "playback: PCM buffer init failed result=%d", result);
return 0;
}
session->buffer = buffer;
if (!createPcmNode(*session, true)) return 0;
PlaybackId id = ++m_nextPlaybackId;
m_pcmSessions.emplace(id, std::move(session));
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "playback: started id=%llu frames=%d rate=%d ch=%d transient=%d",
(unsigned long long)id, frameCount, sampleRate, channels, transient);
return id;
}
void SoundManager::pausePcm(PlaybackId id) {
auto* session = pcmSession(id);
if (!session || !session->sound) return;
ma_result result = ma_sound_stop(session->sound);
/* Stop меняет флаг; detach также ждёт уже начатое чтение и фиксирует cursor. */
if (result == MA_SUCCESS && !session->detached) {
result = ma_node_detach_output_bus(session->sound, 0);
if (result == MA_SUCCESS) session->detached = true;
}
if (result != MA_SUCCESS) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "playback: pause failed id=%llu result=%d", (unsigned long long)id, result);
}
void SoundManager::resumePcm(PlaybackId id) {
auto* session = pcmSession(id);
if (!session || !session->sound) return;
ma_result result = MA_SUCCESS;
if (session->detached) {
result = ma_node_attach_output_bus(session->sound, 0, ma_engine_get_endpoint(m_engine), 0);
if (result == MA_SUCCESS) session->detached = false;
}
if (result == MA_SUCCESS) result = ma_sound_start(session->sound);
if (result != MA_SUCCESS) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "playback: resume failed id=%llu result=%d", (unsigned long long)id, result);
}
void SoundManager::stopPcm(PlaybackId id) {
if (m_pcmSessions.erase(id)) DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "playback: stopped id=%llu", (unsigned long long)id);
}
uint64_t SoundManager::pcmCursor(PlaybackId id) const {
auto* session = pcmSession(id);
if (!session || !session->sound) return 0;
uint64_t rendered = ma_sound_get_time_in_pcm_frames(session->sound);
return std::min(session->frames, session->origin + rendered * (uint64_t)session->rate / 48000);
}
bool SoundManager::isPcmPlaying(PlaybackId id) const {
auto* session = pcmSession(id);
return session && session->sound && ma_sound_is_playing(session->sound);
}
int SoundManager::pcmSampleRate(PlaybackId id) const {
auto* session = pcmSession(id);
return session ? session->rate : 0;
}
bool SoundManager::seekPcm(PlaybackId id, uint64_t frame) {
auto* session = pcmSession(id);
if (!session || !session->sound) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "playback: seek missing id=%llu", (unsigned long long)id); return false; }
bool playing = ma_sound_is_playing(session->sound);
ma_sound_uninit(session->sound); delete session->sound; session->sound = nullptr;
session->origin = std::min(frame, session->frames);
ma_result result = ma_audio_buffer_seek_to_pcm_frame(session->buffer, session->origin);
if (result != MA_SUCCESS || !createPcmNode(*session, playing)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "playback: seek failed id=%llu frame=%llu result=%d",
(unsigned long long)id, (unsigned long long)frame, result);
return false;
}
DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "playback: seek id=%llu frame=%llu", (unsigned long long)id, (unsigned long long)session->origin);
return true;
}
static QString decodeDeviceName(const char* name) {
QString s = QString::fromUtf8(name);
if (s.contains(QChar(0xFFFD)))
s = QString::fromLocal8Bit(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;
if (!ctx) return result;
ma_device_info* pDevices = nullptr;
ma_uint32 count = 0;
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, deviceKey(ctx, pDevices[i].id)});
return result;
}
QVector<SoundManager::DeviceInfo> SoundManager::enumerateCaptureDevices() {
QVector<DeviceInfo> result;
ma_context* ctx = instance()->m_context;
if (!ctx) return result;
ma_device_info* pDevices = nullptr;
ma_uint32 count = 0;
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, 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;
}