#include "miniaudio.h" #include "sound_manager.h" #include "voice_audio_io.h" #include "../transport/audio_diagnostics.h" #include #include #include #include #include #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(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& 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(out), frames * 2, 0.0f); return; // GUI объединит совпавшие маршруты. } renderOutput(nullptr, route.voices, static_cast(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(); 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(); 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> 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(); 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(&id), sizeof(id)); return QString::fromLatin1(ma_get_backend_name(context->backend)) + '/' + QString::fromLatin1(bytes.toBase64()); } QVector SoundManager::enumeratePlaybackDevices() { QVector 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::enumerateCaptureDevices() { QVector 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; }