radio: PTT-радио в каналах (src/radio + chatgui + chatgui-android)
Новый модуль src/radio (radio/radio_audio/radio_headless) — push-to-talk радио поверх topo-групп, доставка аудио в канал, headless-режим и тесты. Интеграция в chatgui (ptt_hotkey_manager, radio_audio_engine, радионастройки, вызовы) и chatgui-android (RadioAudioEngine, RadioOverlayService, UI).
This commit is contained in:
1 parent
398078a28f
commit
b96ed8bd00
69 files changed
+5372
-51
No files matched your search
@@ -0,0 +1,153 @@
|
||||
#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 "../../lib/debug_config.h"
|
||||
}
|
||||
|
||||
#include <cstring>
|
||||
|
||||
static RadioAudioEngine* g_radioAudioEngine = nullptr;
|
||||
|
||||
static void radioAudioDataCallback(ma_device* dev, void* out, const void* in, ma_uint32 frames) {
|
||||
RadioAudioEngine* e = (RadioAudioEngine*)dev->pUserData;
|
||||
if (e) e->dataCallback(in, 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;
|
||||
}
|
||||
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;
|
||||
|
||||
if (initDevice() != 0) {
|
||||
radio_audio_stop();
|
||||
return -1;
|
||||
}
|
||||
|
||||
m_active = true;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "RadioAudio: started grp=%016llx", (unsigned long long)groupId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int RadioAudioEngine::initDevice() {
|
||||
ma_context* ctx = SoundManager::instance()->context();
|
||||
if (!ctx) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "RadioAudio: no audio context");
|
||||
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);
|
||||
config.capture.format = ma_format_s16;
|
||||
config.capture.channels = 1;
|
||||
config.playback.format = ma_format_s16;
|
||||
config.playback.channels = 1;
|
||||
config.sampleRate = RADIO_AUDIO_SAMPLE_RATE;
|
||||
config.periodSizeInFrames = 480;
|
||||
config.dataCallback = radioAudioDataCallback;
|
||||
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;
|
||||
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;
|
||||
return -1;
|
||||
}
|
||||
m_deviceStarted = true;
|
||||
m_capturePos = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void RadioAudioEngine::stop() {
|
||||
if (!m_active && !m_device && !m_deviceStarted) return;
|
||||
m_active = false;
|
||||
m_talking = false;
|
||||
quint64 gid = m_groupId.load();
|
||||
|
||||
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_deviceStarted) {
|
||||
ma_device_stop(m_device);
|
||||
ma_device_uninit(m_device);
|
||||
m_deviceStarted = false;
|
||||
}
|
||||
if (m_device) { delete m_device; m_device = nullptr; }
|
||||
}
|
||||
|
||||
void RadioAudioEngine::dataCallback(const void* in, void* out, unsigned int frames) {
|
||||
const int16_t* in16 = (const int16_t*)in;
|
||||
int16_t* out16 = (int16_t*)out;
|
||||
|
||||
/* захват: копим 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));
|
||||
}
|
||||
Reference in new issue
Block a user