Add live desktop radio VAD settings and transmission state
This commit is contained in:
1 parent
ffd26c56b4
commit
dcb07c9124
2 files changed
+180
-4
No files matched your search
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
Reference in new issue
Block a user