#include "radio_audio_engine.h" #include "sound_manager.h" #include "../transport/audio_diagnostics.h" #include "../transport/gui_bridge.h" #include "miniaudio.h" extern "C" { #include "radio/radio_audio.h" #include "chat/chat_setting.h" #include "../../lib/debug_config.h" } #include #include RadioAudioEngine* RadioAudioEngine::instance() { 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(); m_voice.stopCapture(); if (m_active) radio_audio_capture_stop(); m_capture.retryNow(); } if (m_playbackId != manager->playbackDevice()) { m_playbackId = manager->playbackDevice(); manager->detachVoice(&m_voice); m_outputAttached = false; } 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(); recoverDevices(); }); } int RadioAudioEngine::start(quint64 groupId) { 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; if (!m_voice.start(VoiceAudioIo::Kind::Radio, groupId)) { radio_audio_stop(); m_groupId = 0; return -1; } m_wantStereo = chat_setting_get_int(inst, "radio_capture_stereo", 0) != 0; m_active = true; m_capture.retryNow(); recoverDevices(); m_recoveryTimer.start(); DEBUG_INFO(DEBUG_CATEGORY_RADIO, "RadioAudio: listening group=%016llx", (unsigned long long)groupId); return 0; } 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; 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; } if (!m_voice.startCapture(channels, chat_setting_get_int(inst, "aec_enabled", 1) != 0)) { m_capture.close(); radio_audio_capture_stop(); return; } if (m_talking) m_voice.setPtt(true); if (!m_capture.start()) { m_voice.stopCapture(); radio_audio_capture_stop(); continue; } if (channels == 1 && m_wantStereo) DEBUG_WARN(DEBUG_CATEGORY_RADIO, "RadioAudio: stereo input unavailable, using mono"); return; } } } void RadioAudioEngine::recoverDevices() { if (!m_active) return; auto* manager = SoundManager::instance(); if (!m_outputAttached && manager->context()) m_outputAttached = manager->attachVoice(&m_voice, m_playbackId); if ((m_capture.initialized() && !m_voice.captureRunning()) || m_capture.needsRecovery()) { m_capture.close(); m_voice.stopCapture(); radio_audio_capture_stop(); m_capture.retryLater(); } if (m_capture.retryReady()) openCapture(); m_voice.setAecEnabled(chat_setting_get_int(gui_bridge_get_inst(), "aec_enabled", 1) != 0); } void RadioAudioEngine::pauseDevices() { m_capture.close(); m_voice.stopCapture(); SoundManager::instance()->detachVoice(&m_voice); m_outputAttached = false; if (m_active) radio_audio_capture_stop(); m_voice.resetPlayback(); } void RadioAudioEngine::stop() { m_recoveryTimer.stop(); pauseDevices(); m_voice.stop(); uint64_t group = m_groupId.exchange(0); bool talking = m_talking.exchange(false); m_buttonTalking = m_hotkeyTalking = false; m_active = false; 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() { if (!m_active) return; m_buttonTalking = true; updateTalking(); } void RadioAudioEngine::talkEnd() { m_buttonTalking = false; updateTalking(); } void RadioAudioEngine::setHotkeyTalking(bool pressed) { m_hotkeyTalking = pressed && m_active; updateTalking(); } void RadioAudioEngine::updateTalking() { bool talking = m_active && (m_buttonTalking || m_hotkeyTalking); if (m_talking.exchange(talking) == talking || !m_active) return; m_voice.setPtt(talking); DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "RadioAudio: PTT %s group=%016llx button=%d hotkey=%d", talking ? "started" : "stopped", (unsigned long long)m_groupId.load(), m_buttonTalking, m_hotkeyTalking); emit talkingChanged(talking); } void RadioAudioEngine::captureCallback(const void* in, unsigned int frames) { if (in) m_voice.capture(static_cast(in), frames, m_capture.callbackTimeUs(), m_capture.route(), m_capture.captureMissing()); }