Complete desktop radio UI and audio engine contracts
This commit is contained in:
1 parent
254029be63
commit
cb49b9d4a4
3 files changed
+118
-91
No files matched your search
@@ -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 текущей подписки.
|
||||
};
|
||||
@@ -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.
|
||||
};
|
||||
@@ -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 — запись не активна.
|
||||
};
|
||||
Reference in new issue
Block a user