radio: add activation controls, live VAD meters and signal diagnostics
This commit is contained in:
1 parent
fddbba217d
commit
7f3247bfcc
23 files changed
+818
-182
No files matched your search
@@ -1,6 +1,7 @@
|
||||
#include <QCoreApplication>
|
||||
#include "radiopanel.h"
|
||||
#include "radio_audio_engine.h"
|
||||
#include "radiosettingspage.h"
|
||||
#include "sound_manager.h"
|
||||
#include "../transport/gui_bridge.h"
|
||||
extern "C" {
|
||||
@@ -17,7 +18,32 @@ extern "C" {
|
||||
#include <QSignalBlocker>
|
||||
#include <QStyle>
|
||||
#include <QMouseEvent>
|
||||
#include <QWidgetAction>
|
||||
#include <QScrollArea>
|
||||
#include <QVBoxLayout>
|
||||
#include <QPainter>
|
||||
#include <QScreen>
|
||||
#include <QGuiApplication>
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
|
||||
/* Полоса показывает текущую вероятность речи, не установленный порог срабатывания. */
|
||||
class RadioAutoButton final : public QPushButton {
|
||||
public:
|
||||
explicit RadioAutoButton(QWidget* parent) : QPushButton(QStringLiteral("VAD"), parent) {}
|
||||
protected:
|
||||
void paintEvent(QPaintEvent* event) override {
|
||||
QPushButton::paintEvent(event);
|
||||
auto* engine = RadioAudioEngine::instance();
|
||||
if (engine->autoMode() != 1) return;
|
||||
QPainter painter(this);
|
||||
QRectF track(6, height() - 7, width() - 12, 3);
|
||||
painter.fillRect(track, isChecked() ? QColor(0, 0, 0, 65) : palette().color(QPalette::Mid));
|
||||
if (!engine->vadValid()) return;
|
||||
track.setWidth(track.width() * std::clamp(engine->vadProbability(), 0.0f, 1.0f));
|
||||
painter.fillRect(track, QColor("#4cdb88"));
|
||||
}
|
||||
};
|
||||
|
||||
RadioPanel::RadioPanel(QWidget* parent) : QWidget(parent) {
|
||||
setObjectName(QStringLiteral("radioPanel"));
|
||||
@@ -43,12 +69,21 @@ RadioPanel::RadioPanel(QWidget* parent) : QWidget(parent) {
|
||||
m_status->setMinimumWidth(0);
|
||||
m_status->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
|
||||
row->addWidget(m_status, 1);
|
||||
m_auto = new QPushButton(QStringLiteral("VAD"), this);
|
||||
m_auto = new RadioAutoButton(this);
|
||||
m_auto->setObjectName(QStringLiteral("radioAutoButton"));
|
||||
m_auto->setAccessibleName(QCoreApplication::translate("ChatGui", "Automatic voice transmission"));
|
||||
m_auto->setCheckable(true);
|
||||
m_auto->setMinimumWidth(72);
|
||||
m_auto->setFixedHeight(34);
|
||||
m_auto->installEventFilter(this);
|
||||
m_auto->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(m_auto, &QWidget::customContextMenuRequested, this, [this]() { showActivationSettings(); });
|
||||
m_autoHold.setSingleShot(true); m_autoHold.setInterval(500);
|
||||
connect(&m_autoHold, &QTimer::timeout, this, [this]() {
|
||||
if (!m_auto->isDown() || !m_auto->isEnabled()) return;
|
||||
m_autoLongPressed = true; m_auto->setDown(false);
|
||||
showActivationSettings();
|
||||
});
|
||||
m_auto->setStyleSheet("QPushButton:checked { background: #3390EC; color: white; }");
|
||||
row->addWidget(m_auto);
|
||||
m_ptt = new QPushButton(QStringLiteral("PTT"), this);
|
||||
@@ -67,10 +102,13 @@ RadioPanel::RadioPanel(QWidget* parent) : QWidget(parent) {
|
||||
if (!m_pttLatched) engine->talkEnd();
|
||||
});
|
||||
connect(m_auto, &QPushButton::clicked, this, [this](bool checked) {
|
||||
if (m_autoLongPressed) { updateState(); return; }
|
||||
RadioAudioEngine::instance()->applySettings({{QStringLiteral("radio_auto_enabled"), checked ? 1 : 0}});
|
||||
updateState();
|
||||
});
|
||||
connect(engine, &RadioAudioEngine::stateChanged, this, &RadioPanel::updateState);
|
||||
connect(engine, &RadioAudioEngine::inputLevelChanged, m_auto, [this]() { m_auto->update(); });
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "RadioPanel: VAD button meter uses live speech probability (0..100%%), refresh <=10Hz");
|
||||
connect(SoundManager::instance(), &SoundManager::devicesChanged, this, &RadioPanel::updateDevices);
|
||||
updateState();
|
||||
updateDevices();
|
||||
@@ -82,6 +120,16 @@ RadioPanel::~RadioPanel() {
|
||||
|
||||
/* Второе нажатие Qt приходит как DoubleClick: не отдаём его обычному переключателю pressed. */
|
||||
bool RadioPanel::eventFilter(QObject* watched, QEvent* event) {
|
||||
if (watched == m_auto) {
|
||||
if (event->type() == QEvent::MouseButtonPress && static_cast<QMouseEvent*>(event)->button() == Qt::LeftButton) {
|
||||
m_autoLongPressed = false; m_autoHold.start();
|
||||
} else if (event->type() == QEvent::MouseButtonRelease && static_cast<QMouseEvent*>(event)->button() == Qt::LeftButton) {
|
||||
m_autoHold.stop();
|
||||
if (m_autoLongPressed) { m_autoLongPressed = false; m_auto->setDown(false); return true; }
|
||||
} else if (event->type() == QEvent::Leave || event->type() == QEvent::Hide || event->type() == QEvent::WindowDeactivate) {
|
||||
m_autoHold.stop();
|
||||
} else if (event->type() == QEvent::KeyPress) m_autoLongPressed = false;
|
||||
}
|
||||
if (watched == m_ptt && event->type() == QEvent::MouseButtonDblClick && m_ptt->isEnabled()) {
|
||||
auto* mouse = static_cast<QMouseEvent*>(event);
|
||||
if (mouse->button() == Qt::LeftButton && m_ptt->rect().contains(mouse->pos())) {
|
||||
@@ -93,6 +141,42 @@ bool RadioPanel::eventFilter(QObject* watched, QEvent* event) {
|
||||
return QWidget::eventFilter(watched, event);
|
||||
}
|
||||
|
||||
void RadioPanel::showActivationSettings() {
|
||||
if (!m_auto->isEnabled()) return;
|
||||
m_autoHold.stop(); m_auto->setDown(false);
|
||||
if (!m_activationMenu) {
|
||||
m_activationMenu = new QMenu(this);
|
||||
m_activationMenu->setObjectName(QStringLiteral("radioActivationPopup"));
|
||||
auto* action = new QWidgetAction(m_activationMenu);
|
||||
auto* contents = new QWidget(m_activationMenu);
|
||||
auto* layout = new QVBoxLayout(contents);
|
||||
auto* scroll = new QScrollArea(contents);
|
||||
scroll->setFrameShape(QFrame::NoFrame); scroll->setWidgetResizable(true);
|
||||
m_activationSettings = new RadioActivationSettings(true);
|
||||
scroll->setWidget(m_activationSettings);
|
||||
int height = QGuiApplication::primaryScreen()->availableGeometry().height();
|
||||
int contentHeight = m_activationSettings->heightForWidth(420);
|
||||
if (contentHeight < 0) contentHeight = m_activationSettings->sizeHint().height();
|
||||
scroll->setFixedSize(420, std::min(contentHeight + 24, std::min(480, std::max(160, height - 120))));
|
||||
layout->addWidget(scroll);
|
||||
auto* apply = new QPushButton(QCoreApplication::translate("ChatGui", "Apply"), contents);
|
||||
apply->setObjectName(QStringLiteral("radioActivationApply"));
|
||||
layout->addWidget(apply);
|
||||
connect(apply, &QPushButton::clicked, m_activationSettings, &RadioActivationSettings::apply);
|
||||
connect(m_activationSettings, &RadioActivationSettings::changed, apply, [this, apply]() {
|
||||
apply->setEnabled(m_activationSettings->canApply());
|
||||
});
|
||||
action->setDefaultWidget(contents); m_activationMenu->addAction(action);
|
||||
connect(m_activationMenu, &QMenu::aboutToHide, this, [this]() {
|
||||
m_autoLongPressed = false;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "RadioPanel: close activation quick settings");
|
||||
});
|
||||
}
|
||||
m_activationSettings->loadSettings();
|
||||
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "RadioPanel: open activation quick settings mode=%d", RadioAudioEngine::instance()->autoMode());
|
||||
m_activationMenu->popup(m_auto->mapToGlobal(QPoint(0, m_auto->height())));
|
||||
}
|
||||
|
||||
/* Фиксация владеет только кнопочным PTT; хоткей и автоматическая передача остаются независимыми. */
|
||||
void RadioPanel::setPttLatched(bool latched, const char* reason) {
|
||||
if (m_pttLatched == latched) return;
|
||||
@@ -109,9 +193,19 @@ void RadioPanel::updateState() {
|
||||
if (m_pttLatched && (!engine->isActive() || !engine->captureAvailable()))
|
||||
setPttLatched(false, engine->isActive() ? "microphone-unavailable" : "radio-stopped");
|
||||
QSignalBlocker blocker(m_auto);
|
||||
m_auto->setText(engine->autoMode() == 2 ? QCoreApplication::translate("ChatGui", "Level") : QStringLiteral("VAD"));
|
||||
m_auto->setText(engine->autoMode() == 2 ? QString() : QStringLiteral("VAD"));
|
||||
QIcon levelIcon;
|
||||
if (engine->autoMode() == 2) {
|
||||
QPixmap pixmap(24, 24); pixmap.fill(Qt::transparent);
|
||||
QPainter painter(&pixmap);
|
||||
painter.setPen(QPen(engine->autoEnabled() ? Qt::white : palette().color(QPalette::ButtonText), 3, Qt::SolidLine, Qt::RoundCap));
|
||||
painter.drawLine(5, 10, 5, 19); painter.drawLine(12, 4, 12, 19); painter.drawLine(19, 7, 19, 19);
|
||||
painter.end(); levelIcon = QIcon(pixmap);
|
||||
}
|
||||
m_auto->setIcon(levelIcon); m_auto->setIconSize(QSize(24, 24));
|
||||
m_auto->setChecked(engine->autoEnabled());
|
||||
m_auto->setEnabled(engine->isActive() && engine->settingsReady() && !engine->settingsBusy());
|
||||
if (!engine->isActive() && m_activationMenu) m_activationMenu->hide();
|
||||
m_ptt->setEnabled(engine->isActive() && engine->captureAvailable());
|
||||
m_ptt->setText(m_pttLatched ? QStringLiteral("PTT \U0001F512") : QStringLiteral("PTT"));
|
||||
m_ptt->setToolTip(m_pttLatched ? QCoreApplication::translate("ChatGui", "PTT locked. Click to release.")
|
||||
@@ -137,8 +231,13 @@ void RadioPanel::updateState() {
|
||||
: QCoreApplication::translate("ChatGui", "VAD listening");
|
||||
m_status->setText(state);
|
||||
m_status->setToolTip(engine->settingsError().isEmpty() ? state : engine->settingsError());
|
||||
m_auto->setToolTip(engine->autoEnabled() ? QCoreApplication::translate("ChatGui", "Disable automatic voice transmission")
|
||||
: QCoreApplication::translate("ChatGui", "Enable automatic voice transmission"));
|
||||
const QString toggle = engine->autoEnabled() ? QCoreApplication::translate("ChatGui", "Disable automatic voice transmission")
|
||||
: QCoreApplication::translate("ChatGui", "Enable automatic voice transmission");
|
||||
const QString mode = engine->autoMode() == 2 ? QCoreApplication::translate("ChatGui", "By signal level") : QStringLiteral("VAD");
|
||||
const QString hint = QCoreApplication::translate("ChatGui", "Hold or right-click to adjust activation mode and thresholds");
|
||||
m_auto->setToolTip(mode + "\n" + toggle + "\n" + hint);
|
||||
m_auto->setAccessibleName(mode + ". " + toggle); m_auto->setAccessibleDescription(hint);
|
||||
m_auto->update();
|
||||
}
|
||||
|
||||
/* Список перечитывается при открытии меню: подключённые устройства видны без перезапуска. */
|
||||
|
||||
Reference in new issue
Block a user