Complete desktop radio UI and audio engine contracts

This commit is contained in:
evgeny committed 2026-10-27 14:24:46 +01:00
1 parent 254029be63
commit cb49b9d4a4
3 files changed
+118 -91

No files matched your search

+59 -53
View File
@@ -1,3 +1,9 @@
/* Desktop-аудио одной активной рации: AudioCapture поставляет PCM, VoiceAudioIo выполняет AEC/кодирование и RX,
* SoundManager смешивает голос с остальными звуками приложения и возвращает итоговый stereo reference.
* Управление/обычные свойства/сигналы — GUI-поток; captureCallback — аппаратный поток. isTalking/isActive/groupId атомарны.
* После DB_READY загрузить настройки; start вызывается после включения сетевой подписки рации отдельной командой ядру.
* До уничтожения ядра вызвать stop: снять capture, join capture worker, отсоединить output, join RX, освободить core DSP.
* Уже запущенное закрытие backend может завершиться позже; singleton живёт до конца процесса. */
#pragma once
#include <QObject>
@@ -13,24 +19,20 @@
struct ma_device;
/* Desktop-контур рации: GUI владеет подпиской AudioCapture и жизненным циклом.
* Hardware callback копирует PCM в ограниченную очередь VoiceAudioIo.
* Capture worker выполняет AEC и упорядочивает PCM/PTT/mute; независимый RX worker
* получает готовый голос из сервисного слоя. SoundManager смешивает его со всеми
* звуками приложения на том же выходе и возвращает итоговый stereo reference.
* Stop: close capture → join capture worker → detach output → join RX → release core. */
class RadioAudioEngine : public QObject {
Q_OBJECT
public:
static RadioAudioEngine* instance();
static RadioAudioEngine* instance(); // Заимствованный singleton; первый вызов — в GUI при живом SoundManager.
/* Включить прослушивание канала (после toggle рации). 0 = успех. */
/* Запускает DSP/RX и recovery для groupId: 0 — сессия запущена, -1 — core/worker/закрывающийся capture мешают запуску.
* Аппаратная готовность не гарантируется. Тот же активный ID — без изменений; другой сначала останавливает прежний. */
int start(quint64 groupId);
/* Выключить прослушивание канала (toggle рации выключен). */
/* Идемпотентно останавливает аудиосессию и снимает обоих ручных владельцев PTT; настройки сохраняются. */
void stop();
/* PTT: зажата / отпущена (только когда радио канала включено). */
/* Кнопочный владелец PTT, без счётчика вложенности: повтор безопасен. Begin требует активную рацию без captureError.
* End снимает только кнопку; хоткей и автоматическая передача независимы. */
void talkBegin();
void talkEnd();
void setHotkeyTalking(bool pressed); // независимый владелец PTT — глобальная клавиатура
@@ -38,75 +40,79 @@ public:
/* GUI-поток: начать новое чтение настроек из ядра, в том числе после DB_READY.
* configPath — INI для последующего applySettings; loadSettings сам файл не читает.
* Снимает прежнее ожидание, даже если ядро удалило задачу без callback; поздний результат прежнего поколения игнорируется.
* Физически уже поставленные задачи остаются в очереди ядра; новый снимок читается после них. */
* Уже стоящие задачи живого ядра не отменяются; новое чтение следует за ними. */
void loadSettings(const QString& configPath);
/* Применение в uasync; true — принято в очередь либо значения уже совпадают при выключенной рации.
/* values — подмножество девяти поддержанных radio_* настроек; проверяет имена/диапазоны и level_off < level_on.
* Изменения сначала атомарно сохраняются в INI (если configPath непустой), затем применяются по одному в uasync;
* общей транзакции INI/core нет. true — принято в очередь либо значения совпадают при выключенной рации.
* После подтверждения GUI перезапускает только capture; готовность/ошибка приходят через stateChanged. */
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 autoEnabled() const { return m_settings[0] != 0; }
int autoMode() const { return m_settings[6]; }
int setting(const QString& name) const; // Последний снимок; неизвестное имя логируется и возвращает 0.
bool settingsReady() const { return m_settingsReady; } // Получен снимок текущего поколения, не гарантия отсутствия settingsError.
bool settingsBusy() const { return m_settingsBusy; } // Ожидание GUI completion текущего поколения.
bool autoEnabled() const { return m_settings[0] != 0; } // Запрошенное включение автоматики, независимо от готовности capture.
int autoMode() const { return m_settings[6]; } // Запрошенный режим: 1=VAD, 2=уровень сигнала; выключение — через autoEnabled.
/* Последние измерения: уровень dBFS, вероятность речи 0..1 и усиление VAD в dB; проверять соответствующий Valid. */
float inputLevel() const { return m_inputLevel; }
bool inputLevelValid() const { return m_inputLevelValid; }
float vadProbability() const { return m_vadProbability; }
bool vadValid() const { return m_vadValid; }
float vadGain() const { return m_vadGain; }
bool vadCompressorEnabled() const { return m_vadCompressorEnabled; }
bool autoBlocked() const { return m_autoBlocked; }
bool autoAvailable() const { return m_autoAvailable; }
bool captureAvailable() const { return m_captureAvailable; }
bool isTransmitting() const { return m_transmitting; }
bool autoBlocked() const { return m_autoBlocked; } // Детектор временно заблокирован занятым каналом при готовом capture.
bool autoAvailable() const { return m_autoAvailable; } // Core auto mode активен при готовом capture, включая режим уровня.
bool captureAvailable() const { return m_captureAvailable; } // Работают и AudioCapture, и capture worker.
bool isTransmitting() const { return m_transmitting; } // Последний фактический TX ядра, включая автоматику.
QString settingsError() const { return m_settingsError; }
QString captureError() const { return m_captureError; }
bool isTalking() const { return m_talking.load(); }
bool isActive() const { return m_active.load(); }
quint64 groupId() const { return m_groupId.load(); }
bool isTalking() const { return m_talking.load(); } // Суммарный ручной запрос кнопки/хоткея, не фактический TX.
bool isActive() const { return m_active.load(); } // Аудиосессия, независимо от наличия устройств.
quint64 groupId() const { return m_groupId.load(); } // ID активной группы, 0 после stop.
/* Hardware capture callback: без DSP и растущих буферов. */
/* Заимствованный Frame аппаратного callback: синхронно копирует PCM/время/route/missing в ограниченную очередь, без DSP. */
void captureCallback(const AudioCapture::Frame& frame);
signals:
void talkingChanged(bool talking); // GUI-поток, суммарное состояние кнопки и хоткея
void inputLevelChanged(); // снимок измерителя, не чаще 10Гц
void inputLevelChanged(); // Изменился снимок измерителя: обычно раз в 100мс, также при переходах состояния.
void stateChanged(); // фактический TX, доступность capture/VAD и настройки; GUI-поток
private:
RadioAudioEngine();
void updateTalking();
void updateTalking(); // Объединяет два ручных флага, посылает упорядоченную PTT-команду и talkingChanged при изменении.
bool m_buttonTalking = false, m_hotkeyTalking = false; // GUI-поток
void pauseDevices();
void recoverDevices();
void openCapture();
void updateState();
void reconfigureCapture();
void postSettings(const std::array<int, 9>& values, bool apply);
void pauseDevices(); // Перед reset контекста SoundManager закрывает capture, отсоединяет output, сбрасывает playback.
void recoverDevices(); // При активной сессии восстанавливает устройства/AEC; отсутствие PCM 1с блокирует capture до смены входа/start.
void openCapture(); // Подписка Role::Radio; ready/stopped запускают/останавливают core capture и его worker.
void updateState(); // Снимает статус ядра и готовность устройств, уведомляет GUI при изменениях.
void reconfigureCapture(); // Переоткрывает только capture, сохраняя RX/output и ручной запрос PTT.
void postSettings(const std::array<int, 9>& values, bool apply); // Публикует read/apply в uasync с возвратом снимка в GUI.
/* auto_enabled, vad_threshold (%), hangover (мс), compressor_enabled, TX/VAD gain (dB), auto_mode, on/off (dBFS). */
std::array<int, 9> m_settings{0, 50, 200, 1, 15, 15, 1, -35, -45};
QString m_configPath;
QString m_settingsError;
QString m_configPath; // INI для последующего сохранения изменённых полей в [chatserver].
QString m_settingsError; // Ошибка загрузки/валидации/сохранения/применения настроек.
uint64_t m_settingsGeneration = 0; // Только GUI; каждый loadSettings инвалидирует старые queued completions.
bool m_settingsReady = false, m_settingsBusy = false;
bool m_transmitting = false, m_autoAvailable = false, m_captureAvailable = false;
bool m_settingsReady = false, m_settingsBusy = false; // Готовность снимка и ожидание операции текущего поколения.
bool m_transmitting = false, m_autoAvailable = false, m_captureAvailable = false; // Последний GUI-снимок готовности/TX.
float m_inputLevel = -120;
bool m_inputLevelValid = false, m_autoBlocked = false;
float m_vadProbability = 0, m_vadGain = 0;
bool m_vadValid = false, m_vadCompressorEnabled = false;
float m_inputLevel = -120; // RMS детектора в dBFS; значение использовать только при inputLevelValid.
bool m_inputLevelValid = false, m_autoBlocked = false; // Валидность уровня и занятость канала для автоматики.
float m_vadProbability = 0, m_vadGain = 0; // Вероятность речи 0..1 и усиление VAD-компрессора в dB.
bool m_vadValid = false, m_vadCompressorEnabled = false; // Валидность VAD и фактическое включение его компрессора.
std::atomic<bool> m_active{false};
std::atomic<bool> m_talking{false};
std::atomic<uint64_t> m_groupId{0};
std::atomic<bool> m_active{false}; // Аудиосессия запущена.
std::atomic<bool> m_talking{false}; // Ручной PTT: кнопка либо хоткей.
std::atomic<uint64_t> m_groupId{0}; // ID сессии, не признак готовности сети/аппаратуры.
AudioCapture m_capture;
AudioCaptureLease m_microphone{AudioCaptureLease::Mode::Radio};
VoiceAudioIo m_voice;
bool m_outputAttached = false;
QTimer m_recoveryTimer;
bool m_wantStereo = false;
QString m_captureId, m_playbackId;
QString m_captureError;
AudioCapture m_capture; // GUI-подписка на общую с трансляцией capture-сессию.
AudioCaptureLease m_microphone{AudioCaptureLease::Mode::Radio}; // Radio не участвует в блокировке publisher/call/recorder.
VoiceAudioIo m_voice; // Capture/RX workers и ограниченные очереди.
bool m_outputAttached = false; // Источник зарегистрирован в SoundManager, не гарантия готовности выхода.
QTimer m_recoveryTimer; // Опрос/восстановление и обновление снимков каждые 100мс при активной сессии.
bool m_wantStereo = false; // radio_capture_stereo из ядра при start/переконфигурации.
QString m_captureId, m_playbackId; // Запрошенные общие device ID из SoundManager.
QString m_captureError; // Fatal отсутствие PCM; смена входа либо новая сессия снимают блокировку.
int m_captureChannels = 1;
int m_captureChannels = 1; // Число каналов, подтверждённое ready текущей подписки.
};
+21 -15
View File
@@ -1,3 +1,7 @@
/* GUI-панель активной рации, сама аудиосессию не запускает: отображает singleton RadioAudioEngine.
* PTT удерживается кнопкой либо фиксируется двойным левым кликом; следующая кнопка/остановка рации/потеря capture снимают фиксацию.
* Короткий клик автоматики включает/выключает выбранный VAD/уровень; удержание 500мс либо правый клик открывают редактор.
* Устройства общие через SoundManager, их ID сохраняются через core ui_state; режим/пороги — через настройки движка. */
#pragma once
#include <QWidget>
@@ -9,27 +13,29 @@ class QLabel;
class QMenu;
class RadioActivationSettings;
/* Верхняя панель активной рации: общие устройства SoundManager, VAD и удерживаемая PTT.
* Состояние передачи берётся из ядра; включение VAD хранится в конфигурации приложения. */
class RadioPanel : public QWidget {
Q_OBJECT
public:
/* Создаёт дочерние контролы и подписки на stateChanged/devicesChanged, сразу обновляет снимок UI. */
explicit RadioPanel(QWidget* parent = nullptr);
/* Снимает принадлежащую панели фиксацию кнопочного PTT; аудиодвижок не останавливает. */
~RadioPanel() override;
private:
/* Различает удержание автоматики и фиксацию PTT; скрытие/деактивация кнопки автоматики отменяют только её таймер. */
bool eventFilter(QObject* watched, QEvent* event) override;
/* Меняет только кнопочного владельца движка; reason заимствуется на вызов для лога. */
void setPttLatched(bool latched, const char* reason);
void updateState();
void showActivationSettings();
void updateDevices();
void populateDevices(QToolButton* button, bool capture);
void selectDevice(const QString& id, bool capture);
QToolButton *m_input, *m_output;
QPushButton *m_auto, *m_ptt;
QLabel* m_status;
bool m_pttLatched = false;
QTimer m_autoHold;
bool m_autoLongPressed = false;
QMenu* m_activationMenu = nullptr;
RadioActivationSettings* m_activationSettings = nullptr;
void updateState(); // Готовность контролов, ошибки, режим и фактический TX; VAD-кнопка рисует текущую вероятность речи.
void showActivationSettings(); // Лениво создаёт popup, каждый раз загружает настройки; закрытый черновик не сохраняется.
void updateDevices(); // Обновляет подписи/tooltip из выбранных ID SoundManager.
void populateDevices(QToolButton* button, bool capture); // Перечитывает список при открытии меню; пустой ID — default.
void selectDevice(const QString& id, bool capture); // Планирует сохранение ui_state и сразу меняет общие устройства.
QToolButton *m_input, *m_output; // Дочерние кнопки с меню устройств ввода/вывода.
QPushButton *m_auto, *m_ptt; // Автоматика с измерителем VAD и кнопочный PTT.
QLabel* m_status; // Ошибка/состояние рации с tooltip подробностей настроек.
bool m_pttLatched = false; // Фиксация кнопочного владельца до снятия/уничтожения панели.
QTimer m_autoHold; // Однократный порог долгого нажатия автоматики, 500мс.
bool m_autoLongPressed = false; // Подавляет обычное переключение при отпускании после долгого нажатия.
QMenu* m_activationMenu = nullptr; // Дочерний popup, создаётся при первом открытии.
RadioActivationSettings* m_activationSettings = nullptr; // Компактный редактор, принадлежит содержимому popup.
};
+38 -23
View File
@@ -1,3 +1,7 @@
/* GUI-настройки рации: общий редактор автоматики и вкладка глобальных PTT-сочетаний.
* Черновик режима/порогов хранится в контролах; измерения/ошибки обновляются отдельно и не заменяют его.
* Физические hotkeys применяются в памяти, вызывающий сохраняет hotkeys().join('|') в ui_state radio_ptt_hotkeys.
* Wayland-портал сохраняет назначения сам; видимая вкладка временно запрещает portal-PTT. */
#pragma once
#include "ptt_key_chord.h"
@@ -15,36 +19,47 @@ class QCheckBox;
class QButtonGroup;
class RadioLevelMeter;
/* Общий редактор вкладки и мини-блока; метрики не перезаписывают редактируемые настройки. */
/* Один редактор для полной вкладки и компактного popup RadioPanel. */
class RadioActivationSettings : public QWidget {
Q_OBJECT
public:
/* compact скрывает TX-компрессор/его усиление, остальные поля общие; дочерние контролы принадлежат виджету. */
explicit RadioActivationSettings(bool compact, QWidget* parent = nullptr);
/* Заменяет черновик снимком движка, когда settingsReady; иначе оставляет поля до первого готового снимка. */
void loadSettings();
/* Передаёт карту полей applySettings движка; true — принятие/совпадение, завершение операции асинхронно. */
bool apply();
/* Есть изменения, снимок готов, нет pending и stop threshold ниже start; не проверяет доступ к INI/ядру. */
bool canApply() const;
signals:
/* Черновик/ошибка/доступность изменились; получатель заново вычисляет canApply. */
void changed();
private:
QMap<QString, int> values() const;
void updateControls();
void updateMeters();
bool m_compact, m_loaded = false;
QButtonGroup* m_mode;
QSpinBox *m_threshold, *m_hangover, *m_txGain = nullptr, *m_vadGain, *m_levelOn, *m_levelOff;
QCheckBox* m_compressor = nullptr;
RadioLevelMeter *m_levelMeter, *m_vadMeter;
QLabel *m_levelText, *m_vadText, *m_gainText, *m_error;
QMap<QString, int> values() const; // Черновик radio_* полей; compact не включает TX-компрессор и его усиление.
void updateControls(); // Проверяет пороги/ошибку и обновляет отметки шкал, не загружает заново настройки.
void updateMeters(); // Только текущие измерения/подписи из движка, без изменения черновика.
bool m_compact, m_loaded = false; // Режим отображения и первичная загрузка снимка.
QButtonGroup* m_mode; // Дочерняя группа: 1=VAD, 2=уровень сигнала.
QSpinBox *m_threshold, *m_hangover, *m_txGain = nullptr, *m_vadGain, *m_levelOn, *m_levelOff; // %, мс, TX/VAD dB, on/off dBFS.
QCheckBox* m_compressor = nullptr; // Общий флаг компрессора, отсутствует в compact.
RadioLevelMeter *m_levelMeter, *m_vadMeter; // Дочерние шкалы уровня/вероятности с отметками черновых порогов.
QLabel *m_levelText, *m_vadText, *m_gainText, *m_error; // Дочерние подписи измерений и ошибки.
};
// Запись глобальных сочетаний PTT, включая отдельные левые/правые модификаторы.
/* Вкладка редактирует три физических сочетания Windows/X11 либо открывает системное назначение Wayland. */
class RadioSettingsPage : public QWidget {
Q_OBJECT
public:
/* db заимствуется, может быть nullptr; нужна только при loadFromDb. Менеджер hotkeys должен быть инициализирован отдельно. */
explicit RadioSettingsPage(DbManager* db, QWidget* parent = nullptr);
/* Отменяет запись клавиш; видимая portal-вкладка снимает свой запрет portal-PTT. */
~RadioSettingsPage() override;
/* Перезагружает редактор и три позиции из БД, отменяя физическую запись; portal только перечитывает подписи. */
void loadFromDb();
/* Три кодированных физических позиции, включая пустые; в режиме portal — пустой список. */
QStringList hotkeys() const;
/* Сначала apply редактора; при успехе вне portal отменяет запись и применяет hotkeys в памяти менеджера.
* БД hotkeys не сохраняет, завершения uasync-настроек не ждёт; false оставляет сочетания неприменёнными. */
bool apply();
protected:
@@ -53,16 +68,16 @@ protected:
void hideEvent(QHideEvent* event) override;
private:
void updatePortalLabels();
RadioActivationSettings* m_activation;
QLabel* m_portalStatus = nullptr;
void startCapture(int index);
void finishCapture(bool save);
void updateLabel(int index);
DbManager* m_db;
QVector<PttKeyChord> m_chords;
QVector<QPushButton*> m_buttons;
QVector<QLabel*> m_labels;
PttChordCapture m_capture;
int m_captureIndex = -1;
void updatePortalLabels(); // Три описания/статус и доступность системной кнопки, только portal-ветка.
RadioActivationSettings* m_activation; // Дочерний полный редактор автоматики.
QLabel* m_portalStatus = nullptr; // Дочерняя подпись; ненулевая только в portal-ветке.
void startCapture(int index); // Отменяет прежнюю запись и начинает физическую запись позиции 0..2.
void finishCapture(bool save); // Сохраняет candidate только в черновик либо отменяет; снимает eventFilter/режим записи.
void updateLabel(int index); // Показывает физическое сочетание позиции 0..2 через менеджер.
DbManager* m_db; // Заимствованная БД, записью hotkeys владеет SettingsDialog.
QVector<PttKeyChord> m_chords; // Три физических черновика либо пусто для portal.
QVector<QPushButton*> m_buttons; // Три кнопки записи либо одна системная кнопка, дочерние.
QVector<QLabel*> m_labels; // Три дочерние подписи сочетаний.
PttChordCapture m_capture; // Самый большой одновременный снимок текущей физической записи.
int m_captureIndex = -1; // 0..2 во время записи, -1 — запись не активна.
};