Use shared capture subscription for desktop radio
This commit is contained in:
1 parent
6d7b3e8e02
commit
05ed516cf5
3 files changed
+32
-55
No files matched your search
@@ -1,18 +1,19 @@
|
|||||||
/* Desktop-микрофон: publisher получает Exclusive, существующие voice-контуры — Shared.
|
/* Publisher исключает звонок и запись. Рация согласует устройство через AudioCapture,
|
||||||
* Сохраняется совместная работа звонка/рации/записи; publisher исключает остальные captures в обе стороны.
|
* поэтому может работать с publisher и не участвует в этом запрете.
|
||||||
* Lease берётся до открытия backend, сохраняется при recovery и освобождается после close/join.
|
* Lease берётся до открытия backend, сохраняется при recovery и освобождается после close/join.
|
||||||
* Каждый lease использует управляющий поток, общее atomic состояние доступно из разных потоков. */
|
* Каждый lease использует управляющий поток, общее atomic состояние доступно из разных потоков. */
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
class AudioCaptureLease {
|
class AudioCaptureLease {
|
||||||
public:
|
public:
|
||||||
enum class Mode { Shared, Exclusive };
|
enum class Mode { Shared, Exclusive, Radio };
|
||||||
explicit AudioCaptureLease(Mode mode = Mode::Shared) : m_mode(mode) {}
|
explicit AudioCaptureLease(Mode mode = Mode::Shared) : m_mode(mode) {}
|
||||||
~AudioCaptureLease() { release(); }
|
~AudioCaptureLease() { release(); }
|
||||||
AudioCaptureLease(const AudioCaptureLease&) = delete;
|
AudioCaptureLease(const AudioCaptureLease&) = delete;
|
||||||
AudioCaptureLease& operator=(const AudioCaptureLease&) = delete;
|
AudioCaptureLease& operator=(const AudioCaptureLease&) = delete;
|
||||||
bool acquire() {
|
bool acquire() {
|
||||||
if (m_held) return true;
|
if (m_held) return true;
|
||||||
|
if (m_mode == Mode::Radio) return m_held = true;
|
||||||
int expected = 0;
|
int expected = 0;
|
||||||
if (m_mode == Mode::Exclusive) m_held = owners.compare_exchange_strong(expected, -1);
|
if (m_mode == Mode::Exclusive) m_held = owners.compare_exchange_strong(expected, -1);
|
||||||
else {
|
else {
|
||||||
@@ -23,7 +24,8 @@ public:
|
|||||||
}
|
}
|
||||||
void release() {
|
void release() {
|
||||||
if (!m_held) return;
|
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;
|
m_held = false;
|
||||||
}
|
}
|
||||||
bool held() const { return m_held; }
|
bool held() const { return m_held; }
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ RadioAudioEngine::RadioAudioEngine() {
|
|||||||
auto* manager = SoundManager::instance();
|
auto* manager = SoundManager::instance();
|
||||||
if (m_captureId != manager->captureDevice()) {
|
if (m_captureId != manager->captureDevice()) {
|
||||||
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()) {
|
if (m_playbackId != manager->playbackDevice()) {
|
||||||
m_playbackId = manager->playbackDevice();
|
m_playbackId = manager->playbackDevice();
|
||||||
@@ -60,7 +60,6 @@ RadioAudioEngine::RadioAudioEngine() {
|
|||||||
connect(&m_recoveryTimer, &QTimer::timeout, this, &RadioAudioEngine::recoverDevices);
|
connect(&m_recoveryTimer, &QTimer::timeout, this, &RadioAudioEngine::recoverDevices);
|
||||||
connect(SoundManager::instance(), &SoundManager::contextAboutToReset, this, &RadioAudioEngine::pauseDevices);
|
connect(SoundManager::instance(), &SoundManager::contextAboutToReset, this, &RadioAudioEngine::pauseDevices);
|
||||||
connect(SoundManager::instance(), &SoundManager::contextRestored, this, [this]() {
|
connect(SoundManager::instance(), &SoundManager::contextRestored, this, [this]() {
|
||||||
m_capture.retryNow();
|
|
||||||
recoverDevices();
|
recoverDevices();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -192,11 +191,8 @@ void RadioAudioEngine::reconfigureCapture() {
|
|||||||
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "RadioAudio: reconfigure capture group=%016llx manual=%d auto=%d",
|
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "RadioAudio: reconfigure capture group=%016llx manual=%d auto=%d",
|
||||||
(unsigned long long)m_groupId.load(), m_talking.load(), autoEnabled());
|
(unsigned long long)m_groupId.load(), m_talking.load(), autoEnabled());
|
||||||
m_capture.close();
|
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;
|
m_wantStereo = chat_setting_get_int(gui_bridge_get_inst(), "radio_capture_stereo", 0) != 0;
|
||||||
updateState();
|
updateState();
|
||||||
m_capture.retryNow();
|
|
||||||
recoverDevices();
|
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; }
|
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_wantStereo = chat_setting_get_int(inst, "radio_capture_stereo", 0) != 0;
|
||||||
m_active = true;
|
m_active = true;
|
||||||
m_capture.retryNow();
|
|
||||||
recoverDevices();
|
recoverDevices();
|
||||||
m_recoveryTimer.start();
|
m_recoveryTimer.start();
|
||||||
emit stateChanged();
|
emit stateChanged();
|
||||||
@@ -225,55 +220,36 @@ int RadioAudioEngine::start(quint64 groupId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void RadioAudioEngine::openCapture() {
|
void RadioAudioEngine::openCapture() {
|
||||||
auto* context = SoundManager::instance()->context();
|
if (!SoundManager::instance()->context()) return;
|
||||||
if (!context) return;
|
m_capture.open(m_captureId, AudioCapture::Role::Radio, m_wantStereo ? 2 : 1, m_groupId.load(), [this](int channels, int) {
|
||||||
for (int channels = m_wantStereo ? 2 : 1; channels >= 1; --channels) {
|
m_captureChannels = channels;
|
||||||
ma_device_config config = ma_device_config_init(ma_device_type_capture);
|
if (radio_audio_capture_start(m_groupId.load(), channels, autoEnabled() ? autoMode() : RADIO_AUTO_OFF) != 0) {
|
||||||
config.capture.format = ma_format_s16;
|
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "RadioAudio: core capture start failed ch=%d mode=%d", channels, autoMode());
|
||||||
config.capture.channels = channels;
|
return false;
|
||||||
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 (!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() {
|
void RadioAudioEngine::recoverDevices() {
|
||||||
if (!m_active) return;
|
if (!m_active) return;
|
||||||
auto* manager = SoundManager::instance();
|
auto* manager = SoundManager::instance();
|
||||||
if (!m_outputAttached && manager->context()) m_outputAttached = manager->attachVoice(&m_voice, m_playbackId);
|
if (!m_outputAttached && manager->context()) m_outputAttached = manager->attachVoice(&m_voice, m_playbackId);
|
||||||
if ((m_capture.initialized() && !m_voice.captureRunning()) || m_capture.needsRecovery()) {
|
if (m_capture.running() && !m_voice.captureRunning()) m_capture.close();
|
||||||
m_capture.close(); m_voice.stopCapture(); radio_audio_capture_stop(); m_capture.retryLater();
|
if (!m_capture.subscribed()) openCapture(); else m_capture.recover();
|
||||||
}
|
|
||||||
if (m_capture.retryReady()) openCapture();
|
|
||||||
m_voice.setAecEnabled(chat_setting_get_int(gui_bridge_get_inst(), "aec_enabled", 1) != 0);
|
m_voice.setAecEnabled(chat_setting_get_int(gui_bridge_get_inst(), "aec_enabled", 1) != 0);
|
||||||
updateState();
|
updateState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Снимок фактического TX включает ручной PTT и VAD; уведомляем GUI только при изменении. */
|
/* Снимок фактического TX включает ручной PTT и VAD; уведомляем GUI только при изменении. */
|
||||||
void RadioAudioEngine::updateState() {
|
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();
|
auto input = radio_audio_get_input_status();
|
||||||
bool valid = capture && input.valid;
|
bool valid = capture && input.valid;
|
||||||
bool vadValid = capture && input.vad_valid;
|
bool vadValid = capture && input.vad_valid;
|
||||||
@@ -295,9 +271,8 @@ void RadioAudioEngine::updateState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void RadioAudioEngine::pauseDevices() {
|
void RadioAudioEngine::pauseDevices() {
|
||||||
m_capture.close(); m_voice.stopCapture();
|
m_capture.close();
|
||||||
SoundManager::instance()->detachVoice(&m_voice); m_outputAttached = false;
|
SoundManager::instance()->detachVoice(&m_voice); m_outputAttached = false;
|
||||||
if (m_active) radio_audio_capture_stop();
|
|
||||||
m_voice.resetPlayback();
|
m_voice.resetPlayback();
|
||||||
updateState();
|
updateState();
|
||||||
}
|
}
|
||||||
@@ -342,6 +317,6 @@ void RadioAudioEngine::updateTalking() {
|
|||||||
emit talkingChanged(talking);
|
emit talkingChanged(talking);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RadioAudioEngine::captureCallback(const void* in, unsigned int frames) {
|
void RadioAudioEngine::captureCallback(const AudioCapture::Frame& frame) {
|
||||||
if (in) m_voice.capture(static_cast<const int16_t*>(in), frames, m_capture.callbackTimeUs(), m_capture.route(), m_capture.captureMissing());
|
m_voice.capture(frame.pcm, frame.frames, frame.timeUs, frame.route, frame.missing);
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include "audio_device.h"
|
#include "audio_capture.h"
|
||||||
#include "audio_capture_lease.h"
|
#include "audio_capture_lease.h"
|
||||||
#include "voice_audio_io.h"
|
#include "voice_audio_io.h"
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
struct ma_device;
|
struct ma_device;
|
||||||
|
|
||||||
/* Desktop-контур рации: GUI владеет capture device и жизненным циклом.
|
/* Desktop-контур рации: GUI владеет подпиской AudioCapture и жизненным циклом.
|
||||||
* Hardware callback копирует PCM в ограниченную очередь VoiceAudioIo.
|
* Hardware callback копирует PCM в ограниченную очередь VoiceAudioIo.
|
||||||
* Capture worker выполняет AEC и упорядочивает PCM/PTT/mute; независимый RX worker
|
* Capture worker выполняет AEC и упорядочивает PCM/PTT/mute; независимый RX worker
|
||||||
* получает готовый голос из сервисного слоя. SoundManager смешивает его со всеми
|
* получает готовый голос из сервисного слоя. SoundManager смешивает его со всеми
|
||||||
@@ -60,7 +60,7 @@ public:
|
|||||||
quint64 groupId() const { return m_groupId.load(); }
|
quint64 groupId() const { return m_groupId.load(); }
|
||||||
|
|
||||||
/* Hardware capture callback: без DSP и растущих буферов. */
|
/* Hardware capture callback: без DSP и растущих буферов. */
|
||||||
void captureCallback(const void* in, unsigned int frames);
|
void captureCallback(const AudioCapture::Frame& frame);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void talkingChanged(bool talking); // GUI-поток, суммарное состояние кнопки и хоткея
|
void talkingChanged(bool talking); // GUI-поток, суммарное состояние кнопки и хоткея
|
||||||
@@ -92,8 +92,8 @@ private:
|
|||||||
std::atomic<bool> m_talking{false};
|
std::atomic<bool> m_talking{false};
|
||||||
std::atomic<uint64_t> m_groupId{0};
|
std::atomic<uint64_t> m_groupId{0};
|
||||||
|
|
||||||
AudioDevice m_capture;
|
AudioCapture m_capture;
|
||||||
AudioCaptureLease m_microphone;
|
AudioCaptureLease m_microphone{AudioCaptureLease::Mode::Radio};
|
||||||
VoiceAudioIo m_voice;
|
VoiceAudioIo m_voice;
|
||||||
bool m_outputAttached = false;
|
bool m_outputAttached = false;
|
||||||
QTimer m_recoveryTimer;
|
QTimer m_recoveryTimer;
|
||||||
|
|||||||
Reference in new issue
Block a user