#include #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 #include #include #include #include #include #include #include #include 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(); 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(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); } }