From 05ed516cf56eb13402237f235674c8c455ed888d Mon Sep 17 00:00:00 2001 From: evgeny Date: Fri, 16 Oct 2026 01:05:14 +0200 Subject: [PATCH] Use shared capture subscription for desktop radio --- tools/chatgui/src/audio_capture_lease.h | 10 ++-- tools/chatgui/src/radio_audio_engine.cpp | 67 ++++++++---------------- tools/chatgui/src/radio_audio_engine.h | 10 ++-- 3 files changed, 32 insertions(+), 55 deletions(-) diff --git a/tools/chatgui/src/audio_capture_lease.h b/tools/chatgui/src/audio_capture_lease.h index b17dfde8..047fb21e 100644 --- a/tools/chatgui/src/audio_capture_lease.h +++ b/tools/chatgui/src/audio_capture_lease.h @@ -1,18 +1,19 @@ -/* Desktop-микрофон: publisher получает Exclusive, существующие voice-контуры — Shared. - * Сохраняется совместная работа звонка/рации/записи; publisher исключает остальные captures в обе стороны. +/* Publisher исключает звонок и запись. Рация согласует устройство через AudioCapture, + * поэтому может работать с publisher и не участвует в этом запрете. * Lease берётся до открытия backend, сохраняется при recovery и освобождается после close/join. * Каждый lease использует управляющий поток, общее atomic состояние доступно из разных потоков. */ #pragma once #include class AudioCaptureLease { public: - enum class Mode { Shared, Exclusive }; + enum class Mode { Shared, Exclusive, Radio }; explicit AudioCaptureLease(Mode mode = Mode::Shared) : m_mode(mode) {} ~AudioCaptureLease() { release(); } AudioCaptureLease(const AudioCaptureLease&) = delete; AudioCaptureLease& operator=(const AudioCaptureLease&) = delete; bool acquire() { if (m_held) return true; + if (m_mode == Mode::Radio) return m_held = true; int expected = 0; if (m_mode == Mode::Exclusive) m_held = owners.compare_exchange_strong(expected, -1); else { @@ -23,7 +24,8 @@ public: } void release() { if (!m_held) return; - if (m_mode == Mode::Exclusive) owners.store(0); else owners.fetch_sub(1); + if (m_mode == Mode::Exclusive) owners.store(0); + else if (m_mode == Mode::Shared) owners.fetch_sub(1); m_held = false; } bool held() const { return m_held; } diff --git a/tools/chatgui/src/radio_audio_engine.cpp b/tools/chatgui/src/radio_audio_engine.cpp index c2196d59..dbc4bcf8 100644 --- a/tools/chatgui/src/radio_audio_engine.cpp +++ b/tools/chatgui/src/radio_audio_engine.cpp @@ -48,7 +48,7 @@ RadioAudioEngine::RadioAudioEngine() { 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(); + m_capture.close(); } if (m_playbackId != manager->playbackDevice()) { m_playbackId = manager->playbackDevice(); @@ -60,7 +60,6 @@ RadioAudioEngine::RadioAudioEngine() { 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(); }); } @@ -192,11 +191,8 @@ void RadioAudioEngine::reconfigureCapture() { DEBUG_INFO(DEBUG_CATEGORY_RADIO, "RadioAudio: reconfigure capture group=%016llx manual=%d auto=%d", (unsigned long long)m_groupId.load(), m_talking.load(), autoEnabled()); m_capture.close(); - m_voice.stopCapture(); - radio_audio_capture_stop(); m_wantStereo = chat_setting_get_int(gui_bridge_get_inst(), "radio_capture_stereo", 0) != 0; updateState(); - m_capture.retryNow(); recoverDevices(); } @@ -216,7 +212,6 @@ int RadioAudioEngine::start(quint64 groupId) { if (!m_voice.start(VoiceAudioIo::Kind::Radio, groupId)) { radio_audio_stop(); m_groupId = 0; m_microphone.release(); return -1; } m_wantStereo = chat_setting_get_int(inst, "radio_capture_stereo", 0) != 0; m_active = true; - m_capture.retryNow(); recoverDevices(); m_recoveryTimer.start(); emit stateChanged(); @@ -225,55 +220,36 @@ int RadioAudioEngine::start(quint64 groupId) { } 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; - AudioDevice::requestLowLatency(context, config); - 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(); - if (radio_audio_capture_start(m_groupId.load(), channels, autoEnabled() ? autoMode() : RADIO_AUTO_OFF) != 0) { - DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "RadioAudio: core capture start failed ch=%d mode=%d", channels, autoEnabled() ? autoMode() : RADIO_AUTO_OFF); - m_capture.close(); m_capture.retryLater(); return; - } - if (!m_voice.startCapture(channels, chat_setting_get_int(inst, "aec_enabled", 1) != 0)) { - DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "RadioAudio: capture worker start failed ch=%d", channels); - 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; + if (!SoundManager::instance()->context()) return; + m_capture.open(m_captureId, AudioCapture::Role::Radio, m_wantStereo ? 2 : 1, m_groupId.load(), [this](int channels, int) { + m_captureChannels = channels; + if (radio_audio_capture_start(m_groupId.load(), channels, autoEnabled() ? autoMode() : RADIO_AUTO_OFF) != 0) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "RadioAudio: core capture start failed ch=%d mode=%d", channels, autoMode()); + return false; } - } + if (!m_voice.startCapture(channels, chat_setting_get_int(gui_bridge_get_inst(), "aec_enabled", 1) != 0)) { + DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "RadioAudio: capture worker start failed ch=%d", channels); + radio_audio_capture_stop(); return false; + } + if (m_talking) m_voice.setPtt(true); + return true; + }, [this] { m_voice.stopCapture(); radio_audio_capture_stop(); }, + [this](const AudioCapture::Frame& frame) { captureCallback(frame); }); } 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(); + if (m_capture.running() && !m_voice.captureRunning()) m_capture.close(); + if (!m_capture.subscribed()) openCapture(); else m_capture.recover(); m_voice.setAecEnabled(chat_setting_get_int(gui_bridge_get_inst(), "aec_enabled", 1) != 0); updateState(); } /* Снимок фактического TX включает ручной PTT и VAD; уведомляем GUI только при изменении. */ void RadioAudioEngine::updateState() { - bool capture = m_active && m_capture.initialized() && m_voice.captureRunning() && ma_device_is_started(m_capture.device()); + bool capture = m_active && m_capture.running() && m_voice.captureRunning(); auto input = radio_audio_get_input_status(); bool valid = capture && input.valid; bool vadValid = capture && input.vad_valid; @@ -295,9 +271,8 @@ void RadioAudioEngine::updateState() { } void RadioAudioEngine::pauseDevices() { - m_capture.close(); m_voice.stopCapture(); + m_capture.close(); SoundManager::instance()->detachVoice(&m_voice); m_outputAttached = false; - if (m_active) radio_audio_capture_stop(); m_voice.resetPlayback(); updateState(); } @@ -342,6 +317,6 @@ void RadioAudioEngine::updateTalking() { 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()); +void RadioAudioEngine::captureCallback(const AudioCapture::Frame& frame) { + m_voice.capture(frame.pcm, frame.frames, frame.timeUs, frame.route, frame.missing); } diff --git a/tools/chatgui/src/radio_audio_engine.h b/tools/chatgui/src/radio_audio_engine.h index 71506ed2..539d18c0 100644 --- a/tools/chatgui/src/radio_audio_engine.h +++ b/tools/chatgui/src/radio_audio_engine.h @@ -4,7 +4,7 @@ #include #include #include -#include "audio_device.h" +#include "audio_capture.h" #include "audio_capture_lease.h" #include "voice_audio_io.h" #include @@ -13,7 +13,7 @@ struct ma_device; -/* Desktop-контур рации: GUI владеет capture device и жизненным циклом. +/* Desktop-контур рации: GUI владеет подпиской AudioCapture и жизненным циклом. * Hardware callback копирует PCM в ограниченную очередь VoiceAudioIo. * Capture worker выполняет AEC и упорядочивает PCM/PTT/mute; независимый RX worker * получает готовый голос из сервисного слоя. SoundManager смешивает его со всеми @@ -60,7 +60,7 @@ public: quint64 groupId() const { return m_groupId.load(); } /* Hardware capture callback: без DSP и растущих буферов. */ - void captureCallback(const void* in, unsigned int frames); + void captureCallback(const AudioCapture::Frame& frame); signals: void talkingChanged(bool talking); // GUI-поток, суммарное состояние кнопки и хоткея @@ -92,8 +92,8 @@ private: std::atomic m_talking{false}; std::atomic m_groupId{0}; - AudioDevice m_capture; - AudioCaptureLease m_microphone; + AudioCapture m_capture; + AudioCaptureLease m_microphone{AudioCaptureLease::Mode::Radio}; VoiceAudioIo m_voice; bool m_outputAttached = false; QTimer m_recoveryTimer;