#include "radio_audio_engine.h" #include "sound_manager.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 "../../lib/u_async.h" } #include static RadioAudioEngine* g_radioAudioEngine = nullptr; static void radioAudioCaptureCallback(ma_device* dev, void* out, const void* in, ma_uint32 frames) { (void)out; RadioAudioEngine* e = (RadioAudioEngine*)dev->pUserData; if (e) e->captureCallback(in, frames); } static void radioAudioPlaybackCallback(ma_device* dev, void* out, const void* in, ma_uint32 frames) { (void)in; RadioAudioEngine* e = (RadioAudioEngine*)dev->pUserData; if (e) e->playbackCallback(out, frames); } RadioAudioEngine* RadioAudioEngine::instance() { if (!g_radioAudioEngine) g_radioAudioEngine = new RadioAudioEngine(); return g_radioAudioEngine; } int RadioAudioEngine::start(quint64 groupId) { if (m_active) { if (m_groupId == groupId) return 0; stop(); } m_groupId = groupId; struct UTUN_INSTANCE* inst = gui_bridge_get_inst(); if (!inst) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "RadioAudio: no instance, cannot start"); return -1; } if (radio_audio_init(inst) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "RadioAudio: radio_audio_init failed"); return -1; } /* желаемые каналы захвата из настройки; initDevice() при недоступности откатывает на моно */ m_captureChannels = chat_setting_get_int(inst, "radio_capture_stereo", 0) ? 2 : 1; if (initDevice() != 0) { radio_audio_stop(); return -1; } if (radio_audio_start(groupId, m_captureChannels) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "RadioAudio: radio_audio_start failed grp=%016llx", (unsigned long long)groupId); teardownDevice(); radio_audio_stop(); return -1; } m_captureBuf.assign((size_t)RADIO_AUDIO_FRAME_SAMPLES * m_captureChannels, 0); m_capturePos = 0; m_active = true; DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "RadioAudio: started grp=%016llx captureCh=%d", (unsigned long long)groupId, m_captureChannels); return 0; } int RadioAudioEngine::initDevice() { ma_context* ctx = SoundManager::instance()->context(); if (!ctx) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "RadioAudio: no audio context"); return -1; } /* ── capture-устройство (стерео → откат на моно → дефолтное устройство) ── */ const int wantCh = m_captureChannels; int capCh = 0; for (int useDefault = 0; useDefault < 2 && capCh == 0; useDefault++) { for (int ch = wantCh; ch >= 1; ch--) { ma_device* dev = new ma_device; std::memset(dev, 0, sizeof(*dev)); ma_device_config config = ma_device_config_init(ma_device_type_capture); config.capture.format = ma_format_s16; config.capture.channels = ch; config.sampleRate = RADIO_AUDIO_SAMPLE_RATE; config.periodSizeInFrames = 480; config.dataCallback = radioAudioCaptureCallback; config.pUserData = this; if (useDefault) config.capture.pDeviceID = nullptr; if (ma_device_init(ctx, &config, dev) != MA_SUCCESS) { delete dev; continue; } if (ma_device_start(dev) != MA_SUCCESS) { ma_device_uninit(dev); delete dev; continue; } m_captureDevice = dev; m_captureStarted = true; capCh = ch; if (ch != wantCh) DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "RadioAudio: stereo capture unavailable — fallback to mono"); DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "RadioAudio: capture started ch=%d cap='%s'", ch, dev->capture.name ? dev->capture.name : "?"); break; } } if (capCh == 0) { DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "RadioAudio: capture device init failed"); teardownDevice(); return -1; } m_captureChannels = capCh; /* ── playback-устройство (всегда стерео) ── */ m_playbackDevice = new ma_device; std::memset(m_playbackDevice, 0, sizeof(*m_playbackDevice)); { ma_device_config config = ma_device_config_init(ma_device_type_playback); config.playback.format = ma_format_s16; config.playback.channels = 2; config.sampleRate = RADIO_AUDIO_SAMPLE_RATE; config.periodSizeInFrames = 480; config.dataCallback = radioAudioPlaybackCallback; config.pUserData = this; if (ma_device_init(ctx, &config, m_playbackDevice) != MA_SUCCESS) { DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "RadioAudio: playback device init failed"); teardownDevice(); return -1; } if (ma_device_start(m_playbackDevice) != MA_SUCCESS) { DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "RadioAudio: playback device start failed"); teardownDevice(); return -1; } m_playbackStarted = true; } m_capturePos = 0; DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "RadioAudio: playback started ch=2 pb='%s'", m_playbackDevice->playback.name ? m_playbackDevice->playback.name : "?"); return 0; } void RadioAudioEngine::stop() { if (!m_active && !m_captureDevice && !m_playbackDevice && !m_captureStarted && !m_playbackStarted) return; quint64 gid = m_groupId.load(); bool wasTalking = m_talking.exchange(false); if (wasTalking) radio_audio_talk_end(gid); /* честный FIN: пиры дренируют звук, а не ждут таймаут */ m_active = false; teardownDevice(); radio_audio_stop(); m_groupId = 0; DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "RadioAudio: stopped"); } void RadioAudioEngine::talkBegin() { if (!m_active) return; m_talking = true; radio_audio_talk_begin(m_groupId.load()); } void RadioAudioEngine::talkEnd() { if (!m_active) return; m_talking = false; radio_audio_talk_end(m_groupId.load()); } void RadioAudioEngine::teardownDevice() { if (m_captureStarted && m_captureDevice) { ma_device_stop(m_captureDevice); ma_device_uninit(m_captureDevice); m_captureStarted = false; } if (m_captureDevice) { delete m_captureDevice; m_captureDevice = nullptr; } if (m_playbackStarted && m_playbackDevice) { ma_device_stop(m_playbackDevice); ma_device_uninit(m_playbackDevice); m_playbackStarted = false; } if (m_playbackDevice) { delete m_playbackDevice; m_playbackDevice = nullptr; } } void RadioAudioEngine::captureCallback(const void* in, unsigned int frames) { const int16_t* in16 = (const int16_t*)in; if (!in16 || !m_talking.load()) return; /* захват: копим FRAME_SAMPLES * captureChannels → encode+send. Только когда PTT зажата. */ int totalIn = (int)frames * m_captureChannels; for (int i = 0; i < totalIn; i++) { m_captureBuf[m_capturePos++] = in16[i]; if (m_capturePos >= (int)m_captureBuf.size()) { radio_audio_feed_pcm(m_groupId.load(), m_captureBuf.data(), (int)m_captureBuf.size()); m_capturePos = 0; } } } void RadioAudioEngine::playbackCallback(void* out, unsigned int frames) { int16_t* out16 = (int16_t*)out; /* диагностика (DEBUG_CATEGORY_DEBUG): cadence playback-коллбэка */ uint64_t now = get_time_tb(); if (m_dbgFrames == 0) { m_dbgFrames = frames; DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "RadioAudio: playback frames=%u", frames); } m_dbgCbCount++; if (m_dbgPrevCbTb != 0) { uint64_t gap = now - m_dbgPrevCbTb; if (gap > m_dbgGapMax) m_dbgGapMax = gap; } m_dbgPrevCbTb = now; if (m_dbgLastTb == 0) { m_dbgLastTb = now; } else if (now - m_dbgLastTb >= 10000) { DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "RadioAudio: pb cb=%u/s frames=%u gapMax=%.1fms bodyMax=%.1fms", m_dbgCbCount, frames, (double)m_dbgGapMax / 10.0, (double)m_dbgBodyMax / 10.0); m_dbgCbCount = 0; m_dbgLastTb = now; m_dbgGapMax = 0; m_dbgBodyMax = 0; } /* воспроизведение: микшированный PCM из сервисного слоя (всегда стерео) */ int total = (int)frames * 2; int n = radio_audio_pull_pcm(m_groupId.load(), out16, total); if (n < total) std::memset(out16 + n, 0, (size_t)(total - n) * sizeof(int16_t)); uint64_t end = get_time_tb(); uint64_t body = end - now; if (body > m_dbgBodyMax) m_dbgBodyMax = body; }