You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

161 lines
8.3 KiB

#include <QCoreApplication>
#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(QCoreApplication::translate("ChatGui", "Microphone"));
m_output->setText(QCoreApplication::translate("ChatGui", "Audio output"));
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(QCoreApplication::translate("ChatGui", "Automatic voice transmission"));
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(QCoreApplication::translate("ChatGui", "Hold to talk"));
m_ptt->setToolTip(QCoreApplication::translate("ChatGui", "Hold the button or a global shortcut to transmit"));
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 = QCoreApplication::translate("ChatGui", "Settings error");
else if (engine->settingsBusy()) state = QCoreApplication::translate("ChatGui", "Applying settings…");
else if (!engine->captureAvailable()) state = QCoreApplication::translate("ChatGui", "Microphone unavailable");
else if (engine->vadEnabled() && !engine->vadAvailable()) state = QCoreApplication::translate("ChatGui", "VAD unavailable");
else if (transmitting) state = QCoreApplication::translate("ChatGui", "Transmitting");
else state = engine->vadEnabled() ? QCoreApplication::translate("ChatGui", "VAD listening") : QCoreApplication::translate("ChatGui", "Receiving");
m_status->setText(state);
m_status->setToolTip(engine->settingsError().isEmpty() ? state : engine->settingsError());
m_vad->setToolTip(engine->vadEnabled() ? QCoreApplication::translate("ChatGui", "Disable automatic voice transmission")
: QCoreApplication::translate("ChatGui", "Enable automatic voice transmission"));
}
/* Список перечитывается при открытии меню: подключённые устройства видны без перезапуска. */
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(QCoreApplication::translate("ChatGui", "Default (auto)"), 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(QCoreApplication::translate("ChatGui", "Core unavailable"));
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(QCoreApplication::translate("ChatGui", "Failed to select device"));
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(QCoreApplication::translate("ChatGui", "Failed to select device"));
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 = QCoreApplication::translate("ChatGui", "Default (auto)");
if (!id.isEmpty()) {
name = QCoreApplication::translate("ChatGui", "Selected device unavailable; using an available device");
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);
}
}