You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

239 lines
8.7 KiB

#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 <cstring>
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;
}