Browse Source

Add live desktop radio VAD settings and transmission state

master
evgeny 3 days ago
parent
commit
dcb07c9124
  1. 160
      tools/chatgui/src/radio_audio_engine.cpp
  2. 24
      tools/chatgui/src/radio_audio_engine.h

160
tools/chatgui/src/radio_audio_engine.cpp

@ -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);
}

24
tools/chatgui/src/radio_audio_engine.h

@ -7,6 +7,8 @@
#include "audio_device.h"
#include "voice_audio_io.h"
#include <QTimer>
#include <QMap>
#include <array>
struct ma_device;
@ -32,6 +34,18 @@ public:
void talkEnd();
void setHotkeyTalking(bool pressed); // независимый владелец PTT — глобальная клавиатура
/* Настройки читаются/применяются в uasync; после подтверждения GUI перезапускает только capture. */
void loadSettings(const QString& configPath);
bool applySettings(const QMap<QString, int>& values);
int setting(const QString& name) const;
bool settingsReady() const { return m_settingsReady; }
bool settingsBusy() const { return m_settingsBusy; }
bool vadEnabled() const { return m_settings[0] != 0; }
bool vadAvailable() const { return m_vadAvailable; }
bool captureAvailable() const { return m_captureAvailable; }
bool isTransmitting() const { return m_transmitting; }
QString settingsError() const { return m_settingsError; }
bool isTalking() const { return m_talking.load(); }
bool isActive() const { return m_active.load(); }
quint64 groupId() const { return m_groupId.load(); }
@ -41,6 +55,7 @@ public:
signals:
void talkingChanged(bool talking); // GUI-поток, суммарное состояние кнопки и хоткея
void stateChanged(); // фактический TX, доступность capture/VAD и настройки; GUI-поток
private:
RadioAudioEngine();
@ -49,7 +64,14 @@ private:
void pauseDevices();
void recoverDevices();
void openCapture();
void updateState();
void reconfigureCapture();
void postSettings(const std::array<int, 6>& values, bool apply);
std::array<int, 6> m_settings{0, 50, 200, 1, 15, 15};
QString m_configPath;
QString m_settingsError;
bool m_settingsReady = false, m_settingsBusy = false;
bool m_transmitting = false, m_vadAvailable = false, m_captureAvailable = false;
std::atomic<bool> m_active{false};
std::atomic<bool> m_talking{false};

Loading…
Cancel
Save