161 lines
8.2 KiB
C++
161 lines
8.2 KiB
C++
#include "radiopanel.h"
|
|
#include "radio_audio_engine.h"
|
|
#include "sound_manager.h"
|
|
#include "../transport/gui_bridge.h"
|
|
extern "C" {
|
|
#include "chat/chat_core.h"
|
|
#include "../../lib/debug_config.h"
|
|
#include "../../lib/mem.h"
|
|
}
|
|
#include <QHBoxLayout>
|
|
#include <QToolButton>
|
|
#include <QPushButton>
|
|
#include <QLabel>
|
|
#include <QMenu>
|
|
#include <QActionGroup>
|
|
#include <QSignalBlocker>
|
|
#include <QStyle>
|
|
#include <cstring>
|
|
|
|
RadioPanel::RadioPanel(QWidget* parent) : QWidget(parent) {
|
|
setObjectName(QStringLiteral("radioPanel"));
|
|
auto* row = new QHBoxLayout(this);
|
|
row->setContentsMargins(10, 6, 10, 6);
|
|
row->setSpacing(8);
|
|
m_input = new QToolButton(this);
|
|
m_output = new QToolButton(this);
|
|
m_input->setText(QStringLiteral("Микрофон"));
|
|
m_output->setText(QStringLiteral("Выход звука"));
|
|
m_input->setObjectName(QStringLiteral("radioInputButton"));
|
|
m_output->setObjectName(QStringLiteral("radioOutputButton"));
|
|
for (auto* button : {m_input, m_output}) {
|
|
button->setPopupMode(QToolButton::InstantPopup);
|
|
button->setMenu(new QMenu(button));
|
|
auto* group = new QActionGroup(button->menu());
|
|
group->setExclusive(true);
|
|
row->addWidget(button);
|
|
connect(button->menu(), &QMenu::aboutToShow, this, [this, button]() { populateDevices(button, button == m_input); });
|
|
}
|
|
m_status = new QLabel(this);
|
|
m_status->setObjectName(QStringLiteral("radioStatus"));
|
|
m_status->setMinimumWidth(0);
|
|
m_status->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
|
|
row->addWidget(m_status, 1);
|
|
m_vad = new QPushButton(QStringLiteral("VAD"), this);
|
|
m_vad->setObjectName(QStringLiteral("radioVadButton"));
|
|
m_vad->setAccessibleName(QStringLiteral("Автоматическая передача по голосу"));
|
|
m_vad->setCheckable(true);
|
|
m_vad->setFixedSize(52, 34);
|
|
m_vad->setStyleSheet("QPushButton:checked { background: #3390EC; color: white; }");
|
|
row->addWidget(m_vad);
|
|
m_ptt = new QPushButton(QStringLiteral("PTT"), this);
|
|
m_ptt->setObjectName(QStringLiteral("radioPttButton"));
|
|
m_ptt->setAccessibleName(QStringLiteral("Держите и говорите"));
|
|
m_ptt->setToolTip(QStringLiteral("Держите кнопку или глобальный хоткей для передачи"));
|
|
m_ptt->setFixedSize(64, 34);
|
|
m_ptt->setStyleSheet("QPushButton[transmitting=\"true\"] { background: #27ae60; color: white; }");
|
|
row->addWidget(m_ptt);
|
|
auto* engine = RadioAudioEngine::instance();
|
|
connect(m_ptt, &QPushButton::pressed, engine, &RadioAudioEngine::talkBegin);
|
|
connect(m_ptt, &QPushButton::released, engine, &RadioAudioEngine::talkEnd);
|
|
connect(m_vad, &QPushButton::clicked, this, [this](bool checked) {
|
|
RadioAudioEngine::instance()->applySettings({{QStringLiteral("radio_vad_enabled"), checked ? 1 : 0}});
|
|
updateState();
|
|
});
|
|
connect(engine, &RadioAudioEngine::stateChanged, this, &RadioPanel::updateState);
|
|
connect(SoundManager::instance(), &SoundManager::devicesChanged, this, &RadioPanel::updateDevices);
|
|
updateState();
|
|
updateDevices();
|
|
}
|
|
|
|
void RadioPanel::updateState() {
|
|
auto* engine = RadioAudioEngine::instance();
|
|
QSignalBlocker blocker(m_vad);
|
|
m_vad->setChecked(engine->vadEnabled());
|
|
m_vad->setEnabled(engine->isActive() && engine->settingsReady() && !engine->settingsBusy());
|
|
m_ptt->setEnabled(engine->isActive() && engine->captureAvailable());
|
|
bool transmitting = engine->isTransmitting();
|
|
if (m_ptt->property("transmitting").toBool() != transmitting || !m_ptt->property("transmitting").isValid()) {
|
|
m_ptt->setProperty("transmitting", transmitting);
|
|
m_ptt->style()->unpolish(m_ptt);
|
|
m_ptt->style()->polish(m_ptt);
|
|
m_ptt->update();
|
|
}
|
|
QString state;
|
|
if (!engine->settingsError().isEmpty()) state = QStringLiteral("Ошибка настройки");
|
|
else if (engine->settingsBusy()) state = QStringLiteral("Применение настроек…");
|
|
else if (!engine->captureAvailable()) state = QStringLiteral("Микрофон недоступен");
|
|
else if (engine->vadEnabled() && !engine->vadAvailable()) state = QStringLiteral("VAD недоступен");
|
|
else if (transmitting) state = QStringLiteral("Передача");
|
|
else state = engine->vadEnabled() ? QStringLiteral("VAD слушает") : QStringLiteral("Приём");
|
|
m_status->setText(state);
|
|
m_status->setToolTip(engine->settingsError().isEmpty() ? state : engine->settingsError());
|
|
m_vad->setToolTip(engine->vadEnabled() ? QStringLiteral("Выключить автоматическую передачу по голосу")
|
|
: QStringLiteral("Включить автоматическую передачу по голосу"));
|
|
}
|
|
|
|
/* Список перечитывается при открытии меню: подключённые устройства видны без перезапуска. */
|
|
void RadioPanel::populateDevices(QToolButton* button, bool capture) {
|
|
auto* menu = button->menu();
|
|
menu->clear();
|
|
auto* group = menu->findChild<QActionGroup*>();
|
|
QString selected = capture ? SoundManager::instance()->captureDevice() : SoundManager::instance()->playbackDevice();
|
|
auto add = [&](const QString& name, const QString& id) {
|
|
auto* action = menu->addAction(name);
|
|
action->setCheckable(true);
|
|
action->setChecked(id == selected);
|
|
group->addAction(action);
|
|
connect(action, &QAction::triggered, this, [this, id, capture]() { selectDevice(id, capture); });
|
|
};
|
|
add(QStringLiteral("По умолчанию (авто)"), QString());
|
|
auto devices = capture ? SoundManager::enumerateCaptureDevices() : SoundManager::enumeratePlaybackDevices();
|
|
for (const auto& device : devices) add(device.name, device.id);
|
|
}
|
|
|
|
void RadioPanel::selectDevice(const QString& id, bool capture) {
|
|
auto* inst = gui_bridge_get_inst();
|
|
if (!inst || !gui_bridge_is_uasync_ready()) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "RadioPanel: cannot save device: core unavailable");
|
|
m_status->setText(QStringLiteral("Ядро недоступно"));
|
|
return;
|
|
}
|
|
const char* key = capture ? "audio_input_id" : "audio_output_id";
|
|
QByteArray value = id.toUtf8();
|
|
auto* request = static_cast<save_ui_state_arg*>(u_calloc(1, sizeof(save_ui_state_arg)));
|
|
if (!request) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "RadioPanel: device settings allocation failed");
|
|
m_status->setText(QStringLiteral("Не удалось выбрать устройство"));
|
|
return;
|
|
}
|
|
size_t keyLen = strlen(key), valueLen = value.size();
|
|
if (keyLen + valueLen + 2 > sizeof(request->data)) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "RadioPanel: device ID too long bytes=%zu", valueLen);
|
|
u_free(request);
|
|
m_status->setText(QStringLiteral("Не удалось выбрать устройство"));
|
|
return;
|
|
}
|
|
request->inst = inst;
|
|
memcpy(request->data, key, keyLen + 1);
|
|
memcpy(request->data + keyLen + 1, value.constData(), valueLen + 1);
|
|
gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, request);
|
|
auto* manager = SoundManager::instance();
|
|
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "RadioPanel: selected %s default=%d", capture ? "input" : "output", id.isEmpty());
|
|
manager->setDevices(capture ? manager->playbackDevice() : id, capture ? id : manager->captureDevice());
|
|
}
|
|
|
|
void RadioPanel::updateDevices() {
|
|
auto* manager = SoundManager::instance();
|
|
for (auto* button : {m_input, m_output}) {
|
|
bool capture = button == m_input;
|
|
QString id = capture ? manager->captureDevice() : manager->playbackDevice();
|
|
QString name = QStringLiteral("По умолчанию (авто)");
|
|
if (!id.isEmpty()) {
|
|
name = QStringLiteral("Выбранное устройство недоступно; используется доступное");
|
|
auto devices = capture ? manager->enumerateCaptureDevices() : manager->enumeratePlaybackDevices();
|
|
for (const auto& device : devices) if (device.id == id) { name = device.name; break; }
|
|
}
|
|
button->setToolTip(name);
|
|
button->setAccessibleName(button->text() + ": " + name);
|
|
}
|
|
}
|