|
|
|
|
@ -2,6 +2,7 @@
|
|
|
|
|
#include "sound_manager.h" |
|
|
|
|
#include "../transport/audio_diagnostics.h" |
|
|
|
|
#include "../transport/gui_bridge.h" |
|
|
|
|
#include "../transport/config_updater.h" |
|
|
|
|
|
|
|
|
|
#include "miniaudio.h" |
|
|
|
|
|
|
|
|
|
@ -9,10 +10,26 @@ extern "C" {
|
|
|
|
|
#include "radio/radio_audio.h" |
|
|
|
|
#include "chat/chat_setting.h" |
|
|
|
|
#include "../../lib/debug_config.h" |
|
|
|
|
#include "../../lib/mem.h" |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#include <cstring> |
|
|
|
|
#include <algorithm> |
|
|
|
|
#include <QFile> |
|
|
|
|
#include <QSaveFile> |
|
|
|
|
#include <new> |
|
|
|
|
|
|
|
|
|
namespace { |
|
|
|
|
constexpr const char* settingKeys[] = {"radio_vad_enabled", "radio_vad_threshold", "radio_vad_hangover_ms", |
|
|
|
|
"radio_compressor_enabled", "radio_compressor_max_gain_db", "radio_vad_compressor_max_gain_db"}; |
|
|
|
|
constexpr int settingMin[] = {0, 10, 50, 0, 1, 1}, settingMax[] = {1, 95, 2000, 1, 60, 60}; |
|
|
|
|
struct SettingsRequest { |
|
|
|
|
RadioAudioEngine* engine; |
|
|
|
|
UTUN_INSTANCE* inst; |
|
|
|
|
std::array<int, 6> values; |
|
|
|
|
bool apply; |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
RadioAudioEngine* RadioAudioEngine::instance() { |
|
|
|
|
static auto* engine = new RadioAudioEngine(); |
|
|
|
|
@ -34,7 +51,7 @@ RadioAudioEngine::RadioAudioEngine() {
|
|
|
|
|
} |
|
|
|
|
recoverDevices(); |
|
|
|
|
}); |
|
|
|
|
m_recoveryTimer.setInterval(200); |
|
|
|
|
m_recoveryTimer.setInterval(100); |
|
|
|
|
connect(&m_recoveryTimer, &QTimer::timeout, this, &RadioAudioEngine::recoverDevices); |
|
|
|
|
connect(SoundManager::instance(), &SoundManager::contextAboutToReset, this, &RadioAudioEngine::pauseDevices); |
|
|
|
|
connect(SoundManager::instance(), &SoundManager::contextRestored, this, [this]() { |
|
|
|
|
@ -43,6 +60,127 @@ RadioAudioEngine::RadioAudioEngine() {
|
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
int RadioAudioEngine::setting(const QString& name) const { |
|
|
|
|
for (size_t i = 0; i < m_settings.size(); ++i) if (name == QLatin1String(settingKeys[i])) return m_settings[i]; |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "RadioAudio: unknown setting %s", qPrintable(name)); |
|
|
|
|
return 0; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void RadioAudioEngine::loadSettings(const QString& configPath) { |
|
|
|
|
if (m_settingsBusy) { |
|
|
|
|
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "RadioAudio: settings load deferred: update pending"); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
m_configPath = configPath; |
|
|
|
|
m_settingsReady = false; |
|
|
|
|
postSettings(m_settings, false); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* Проверяем значения и атомарно сохраняем INI до отправки команды в ядро. */ |
|
|
|
|
bool RadioAudioEngine::applySettings(const QMap<QString, int>& values) { |
|
|
|
|
if (!m_settingsReady || m_settingsBusy || !gui_bridge_is_uasync_ready() || !gui_bridge_get_inst()) { |
|
|
|
|
m_settingsError = QStringLiteral("Настройки рации ещё не готовы"); |
|
|
|
|
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "RadioAudio: cannot apply settings ready=%d busy=%d", m_settingsReady, m_settingsBusy); |
|
|
|
|
emit stateChanged(); |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
auto next = m_settings; |
|
|
|
|
for (auto it = values.cbegin(); it != values.cend(); ++it) { |
|
|
|
|
size_t index = 0; |
|
|
|
|
while (index < next.size() && it.key() != QLatin1String(settingKeys[index])) ++index; |
|
|
|
|
if (index == next.size() || it.value() < settingMin[index] || it.value() > settingMax[index]) { |
|
|
|
|
m_settingsError = QStringLiteral("Некорректная настройка рации: %1").arg(it.key()); |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "RadioAudio: invalid setting %s=%d", qPrintable(it.key()), it.value()); |
|
|
|
|
emit stateChanged(); |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
next[index] = it.value(); |
|
|
|
|
} |
|
|
|
|
if (next == m_settings) return true; |
|
|
|
|
if (!m_configPath.isEmpty()) { |
|
|
|
|
QFile input(m_configPath); |
|
|
|
|
if (!input.open(QIODevice::ReadOnly)) { |
|
|
|
|
m_settingsError = QStringLiteral("Не удалось прочитать конфигурацию: %1").arg(input.errorString()); |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "RadioAudio: read config failed: %s", qPrintable(input.errorString())); |
|
|
|
|
emit stateChanged(); |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
QStringList lines = QString::fromUtf8(input.readAll()).split('\n'); |
|
|
|
|
input.close(); |
|
|
|
|
for (size_t i = 0; i < next.size(); ++i) |
|
|
|
|
if (next[i] != m_settings[i]) config_update_in_lines(lines, "chatserver", settingKeys[i], QString::number(next[i]), true); |
|
|
|
|
QByteArray data = lines.join('\n').toUtf8(); |
|
|
|
|
if (!data.endsWith('\n')) data.append('\n'); |
|
|
|
|
QSaveFile output(m_configPath); |
|
|
|
|
if (!output.open(QIODevice::WriteOnly) || output.write(data) != data.size() || !output.commit()) { |
|
|
|
|
m_settingsError = QStringLiteral("Не удалось сохранить конфигурацию: %1").arg(output.errorString()); |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "RadioAudio: save config failed: %s", qPrintable(output.errorString())); |
|
|
|
|
emit stateChanged(); |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
postSettings(next, true); |
|
|
|
|
return m_settingsBusy; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* Возврат в GUI происходит после применения всей группы настроек в uasync. */ |
|
|
|
|
void RadioAudioEngine::postSettings(const std::array<int, 6>& values, bool apply) { |
|
|
|
|
auto* inst = gui_bridge_get_inst(); |
|
|
|
|
void* memory = inst && gui_bridge_is_uasync_ready() ? u_malloc(sizeof(SettingsRequest)) : nullptr; |
|
|
|
|
if (!memory) { |
|
|
|
|
m_settingsError = QStringLiteral("Не удалось отправить настройки рации в ядро"); |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "RadioAudio: settings request unavailable inst=%d uasync=%d", |
|
|
|
|
inst != nullptr, gui_bridge_is_uasync_ready()); |
|
|
|
|
emit stateChanged(); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
auto* request = new (memory) SettingsRequest{this, inst, values, apply}; |
|
|
|
|
m_settingsBusy = true; |
|
|
|
|
m_settingsError.clear(); |
|
|
|
|
emit stateChanged(); |
|
|
|
|
gui_bridge_post_uasync_fn([](void* arg) { |
|
|
|
|
auto* request = static_cast<SettingsRequest*>(arg); |
|
|
|
|
bool success = true; |
|
|
|
|
for (size_t i = 0; i < request->values.size(); ++i) { |
|
|
|
|
if (request->apply) { |
|
|
|
|
char value[16]; |
|
|
|
|
snprintf(value, sizeof(value), "%d", request->values[i]); |
|
|
|
|
if (chat_setting_set(request->inst, settingKeys[i], value) != 0) { |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "RadioAudio: core rejected %s=%s", settingKeys[i], value); |
|
|
|
|
success = false; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
request->values[i] = chat_setting_get_int(request->inst, settingKeys[i], request->values[i]); |
|
|
|
|
} |
|
|
|
|
auto* engine = request->engine; |
|
|
|
|
auto snapshot = request->values; |
|
|
|
|
QMetaObject::invokeMethod(engine, [engine, snapshot, success]() { |
|
|
|
|
bool changed = engine->m_settings != snapshot; |
|
|
|
|
engine->m_settings = snapshot; |
|
|
|
|
engine->m_settingsReady = true; |
|
|
|
|
engine->m_settingsBusy = false; |
|
|
|
|
if (!success) engine->m_settingsError = QStringLiteral("Ядро отклонило настройки рации"); |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "RadioAudio: settings applied=%d vad=%d threshold=%d hangover=%dms TX_AGC=%d " |
|
|
|
|
"TX_gain=%ddB VAD_gain=%ddB", success, snapshot[0], snapshot[1], snapshot[2], snapshot[3], snapshot[4], snapshot[5]); |
|
|
|
|
if (changed && engine->m_active) engine->reconfigureCapture(); |
|
|
|
|
emit engine->stateChanged(); |
|
|
|
|
}, Qt::QueuedConnection); |
|
|
|
|
request->~SettingsRequest(); |
|
|
|
|
u_free(request); |
|
|
|
|
}, request); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void RadioAudioEngine::reconfigureCapture() { |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "RadioAudio: reconfigure capture group=%016llx manual=%d vad=%d", |
|
|
|
|
(unsigned long long)m_groupId.load(), m_talking.load(), vadEnabled()); |
|
|
|
|
m_capture.close(); |
|
|
|
|
m_voice.stopCapture(); |
|
|
|
|
radio_audio_capture_stop(); |
|
|
|
|
updateState(); |
|
|
|
|
m_capture.retryNow(); |
|
|
|
|
recoverDevices(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
int RadioAudioEngine::start(quint64 groupId) { |
|
|
|
|
if (m_active && m_groupId == groupId) return 0; |
|
|
|
|
stop(); |
|
|
|
|
@ -80,11 +218,12 @@ void RadioAudioEngine::openCapture() {
|
|
|
|
|
m_captureChannels = channels; |
|
|
|
|
|
|
|
|
|
auto* inst = gui_bridge_get_inst(); |
|
|
|
|
int vad = chat_setting_get_int(inst, "radio_vad_enabled", 0); |
|
|
|
|
if (radio_audio_capture_start(m_groupId.load(), channels, vad) != 0) { |
|
|
|
|
if (radio_audio_capture_start(m_groupId.load(), channels, vadEnabled()) != 0) { |
|
|
|
|
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "RadioAudio: core capture start failed ch=%d vad=%d", channels, vadEnabled()); |
|
|
|
|
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); |
|
|
|
|
@ -104,6 +243,19 @@ void RadioAudioEngine::recoverDevices() {
|
|
|
|
|
} |
|
|
|
|
if (m_capture.retryReady()) openCapture(); |
|
|
|
|
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 vad = capture && radio_audio_vad_mode(); |
|
|
|
|
bool transmitting = m_active && radio_audio_transmitting(); |
|
|
|
|
if (capture == m_captureAvailable && vad == m_vadAvailable && transmitting == m_transmitting) return; |
|
|
|
|
m_captureAvailable = capture; m_vadAvailable = vad; m_transmitting = transmitting; |
|
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "RadioAudio: state group=%016llx capture=%d vad_requested=%d vad_ready=%d TX=%d manual=%d", |
|
|
|
|
(unsigned long long)m_groupId.load(), capture, vadEnabled(), vad, transmitting, m_talking.load()); |
|
|
|
|
emit stateChanged(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void RadioAudioEngine::pauseDevices() { |
|
|
|
|
@ -111,6 +263,7 @@ void RadioAudioEngine::pauseDevices() {
|
|
|
|
|
SoundManager::instance()->detachVoice(&m_voice); m_outputAttached = false; |
|
|
|
|
if (m_active) radio_audio_capture_stop(); |
|
|
|
|
m_voice.resetPlayback(); |
|
|
|
|
updateState(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void RadioAudioEngine::stop() { |
|
|
|
|
@ -121,6 +274,7 @@ void RadioAudioEngine::stop() {
|
|
|
|
|
m_buttonTalking = m_hotkeyTalking = false; |
|
|
|
|
m_active = false; |
|
|
|
|
if (group) radio_audio_stop(); |
|
|
|
|
updateState(); |
|
|
|
|
if (talking) emit talkingChanged(false); |
|
|
|
|
if (group) DEBUG_INFO(DEBUG_CATEGORY_RADIO, "RadioAudio: stopped group=%016llx", (unsigned long long)group); |
|
|
|
|
} |
|
|
|
|
|