|
|
|
|
@ -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<const int16_t*>(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); |
|
|
|
|
} |
|
|
|
|
|