|
|
|
|
@ -6,6 +6,7 @@
|
|
|
|
|
|
|
|
|
|
extern "C" { |
|
|
|
|
#include "radio/radio_audio.h" |
|
|
|
|
#include "chat/chat_setting.h" |
|
|
|
|
#include "../../lib/debug_config.h" |
|
|
|
|
#include "../../lib/u_async.h" |
|
|
|
|
} |
|
|
|
|
@ -14,9 +15,16 @@ extern "C" {
|
|
|
|
|
|
|
|
|
|
static RadioAudioEngine* g_radioAudioEngine = nullptr; |
|
|
|
|
|
|
|
|
|
static void radioAudioDataCallback(ma_device* dev, void* out, const void* in, ma_uint32 frames) { |
|
|
|
|
static void radioAudioCaptureCallback(ma_device* dev, void* out, const void* in, ma_uint32 frames) { |
|
|
|
|
(void)out; |
|
|
|
|
RadioAudioEngine* e = (RadioAudioEngine*)dev->pUserData; |
|
|
|
|
if (e) e->dataCallback(in, out, frames); |
|
|
|
|
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() { |
|
|
|
|
@ -40,21 +48,27 @@ int RadioAudioEngine::start(quint64 groupId) {
|
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "RadioAudio: radio_audio_init failed"); |
|
|
|
|
return -1; |
|
|
|
|
} |
|
|
|
|
if (radio_audio_start(groupId) != 0) { |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "RadioAudio: radio_audio_start failed grp=%016llx", (unsigned long long)groupId); |
|
|
|
|
return -1; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
m_captureBuf.assign(RADIO_AUDIO_FRAME_SAMPLES, 0); |
|
|
|
|
m_capturePos = 0; |
|
|
|
|
/* желаемые каналы захвата из настройки; 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", (unsigned long long)groupId); |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "RadioAudio: started grp=%016llx captureCh=%d", (unsigned long long)groupId, m_captureChannels); |
|
|
|
|
return 0; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -65,48 +79,74 @@ int RadioAudioEngine::initDevice() {
|
|
|
|
|
return -1; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
m_device = new ma_device; |
|
|
|
|
std::memset(m_device, 0, sizeof(*m_device)); |
|
|
|
|
ma_device_config config = ma_device_config_init(ma_device_type_duplex); |
|
|
|
|
/* ── 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 = 1; |
|
|
|
|
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 = 1; |
|
|
|
|
config.playback.channels = 2; |
|
|
|
|
config.sampleRate = RADIO_AUDIO_SAMPLE_RATE; |
|
|
|
|
config.periodSizeInFrames = 480; |
|
|
|
|
config.dataCallback = radioAudioDataCallback; |
|
|
|
|
config.dataCallback = radioAudioPlaybackCallback; |
|
|
|
|
config.pUserData = this; |
|
|
|
|
|
|
|
|
|
if (ma_device_init(ctx, &config, m_device) != MA_SUCCESS) { |
|
|
|
|
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "RadioAudio: device init failed, trying default"); |
|
|
|
|
config.playback.pDeviceID = nullptr; |
|
|
|
|
config.capture.pDeviceID = nullptr; |
|
|
|
|
if (ma_device_init(ctx, &config, m_device) != MA_SUCCESS) { |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "RadioAudio: default device init also failed"); |
|
|
|
|
delete m_device; m_device = nullptr; |
|
|
|
|
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_device) != MA_SUCCESS) { |
|
|
|
|
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "RadioAudio: device start failed"); |
|
|
|
|
ma_device_uninit(m_device); |
|
|
|
|
delete m_device; m_device = nullptr; |
|
|
|
|
if (ma_device_start(m_playbackDevice) != MA_SUCCESS) { |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "RadioAudio: playback device start failed"); |
|
|
|
|
teardownDevice(); |
|
|
|
|
return -1; |
|
|
|
|
} |
|
|
|
|
m_deviceStarted = true; |
|
|
|
|
m_playbackStarted = true; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
m_capturePos = 0; |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, |
|
|
|
|
"RadioAudio: device started sr=%u cap='%s' fmt=%d internalSr=%u pb='%s' fmt=%d internalSr=%u", |
|
|
|
|
m_device->sampleRate, |
|
|
|
|
m_device->capture.name ? m_device->capture.name : "?", |
|
|
|
|
(int)m_device->capture.format, m_device->capture.internalSampleRate, |
|
|
|
|
m_device->playback.name ? m_device->playback.name : "?", |
|
|
|
|
(int)m_device->playback.format, m_device->playback.internalSampleRate); |
|
|
|
|
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_device && !m_deviceStarted) return; |
|
|
|
|
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: пиры дренируют звук, а не ждут таймаут */ |
|
|
|
|
@ -132,25 +172,43 @@ void RadioAudioEngine::talkEnd() {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void RadioAudioEngine::teardownDevice() { |
|
|
|
|
if (m_deviceStarted) { |
|
|
|
|
ma_device_stop(m_device); |
|
|
|
|
ma_device_uninit(m_device); |
|
|
|
|
m_deviceStarted = false; |
|
|
|
|
} |
|
|
|
|
if (m_device) { delete m_device; m_device = nullptr; } |
|
|
|
|
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::dataCallback(const void* in, void* out, unsigned int frames) { |
|
|
|
|
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 коллбэка — gap между вызовами (снаружи)
|
|
|
|
|
* и длительность тела (внутри), чтобы отличить «девайс не дёргает» от «заблокирован внутри». */ |
|
|
|
|
/* диагностика (DEBUG_CATEGORY_DEBUG): cadence playback-коллбэка */ |
|
|
|
|
uint64_t now = get_time_tb(); |
|
|
|
|
if (m_dbgFrames == 0) { |
|
|
|
|
m_dbgFrames = frames; |
|
|
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "RadioAudio: callback frames=%u in=%s out=%s", |
|
|
|
|
frames, in16 ? "yes" : "no", out16 ? "yes" : "no"); |
|
|
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "RadioAudio: playback frames=%u", frames); |
|
|
|
|
} |
|
|
|
|
m_dbgCbCount++; |
|
|
|
|
if (m_dbgPrevCbTb != 0) { |
|
|
|
|
@ -161,7 +219,7 @@ void RadioAudioEngine::dataCallback(const void* in, void* out, unsigned int fram
|
|
|
|
|
if (m_dbgLastTb == 0) { |
|
|
|
|
m_dbgLastTb = now; |
|
|
|
|
} else if (now - m_dbgLastTb >= 10000) { |
|
|
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "RadioAudio: cb=%u/s frames=%u gapMax=%.1fms bodyMax=%.1fms", |
|
|
|
|
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; |
|
|
|
|
@ -169,21 +227,11 @@ void RadioAudioEngine::dataCallback(const void* in, void* out, unsigned int fram
|
|
|
|
|
m_dbgBodyMax = 0; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* захват: копим 960 samples → encode+send (сервисный слой). Только когда PTT зажата. */ |
|
|
|
|
if (in16 && m_talking.load()) { |
|
|
|
|
for (unsigned int i = 0; i < frames; 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; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* воспроизведение: микшированный PCM из сервисного слоя */ |
|
|
|
|
int n = radio_audio_pull_pcm(m_groupId.load(), out16, (int)frames); |
|
|
|
|
if (n < (int)frames) |
|
|
|
|
std::memset(out16 + n, 0, (size_t)(frames - n) * sizeof(int16_t)); |
|
|
|
|
/* воспроизведение: микшированный 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; |
|
|
|
|
|