#include "audiodevicesettingspage.h" #include "sound_manager.h" #include "audiorecorder.h" #include "voicemessageencoder.h" extern "C" { #include "../../lib/audio_compressor.h" } #include "../../lib/debug_config.h" #include "../db/db_manager.h" #include "../transport/gui_bridge.h" #include "../transport/config_updater.h" #include "../../lib/mem.h" extern "C" { #include "chat/chat_core.h" } #include #include #include #include #include #include #include AudioDeviceSettingsPage::AudioDeviceSettingsPage(DbManager* db, AudioRecorder* recorder, QWidget* parent) : QWidget(parent), m_db(db), m_recorder(recorder) { auto* layout = new QVBoxLayout(this); layout->setContentsMargins(0, 12, 0, 0); auto* formLayout = new QFormLayout(); formLayout->setContentsMargins(0, 0, 0, 0); formLayout->setSpacing(10); auto* outLabel = new QLabel("Playback device:", this); m_outputCombo = new QComboBox(this); m_outputCombo->setStyleSheet( "QComboBox { border: 1px solid palette(mid); border-radius: 4px;" " padding: 6px 10px; font-size: 13px; background: palette(base); }"); formLayout->addRow(outLabel, m_outputCombo); auto* inLabel = new QLabel("Recording device:", this); m_inputCombo = new QComboBox(this); m_inputCombo->setStyleSheet(m_outputCombo->styleSheet()); formLayout->addRow(inLabel, m_inputCombo); layout->addLayout(formLayout); /* --- Separator --- */ auto* sep1 = new QFrame(this); sep1->setFrameShape(QFrame::HLine); sep1->setStyleSheet("QFrame { color: palette(mid); }"); layout->addWidget(sep1); /* --- Microphone test --- */ auto* micLabel = new QLabel("Microphone test", this); micLabel->setStyleSheet("font-size: 13px; font-weight: bold;"); layout->addWidget(micLabel); auto* micRow = new QHBoxLayout(); micRow->setSpacing(8); m_micLevelBar = new QProgressBar(this); m_micLevelBar->setRange(0, 100); m_micLevelBar->setValue(0); m_micLevelBar->setTextVisible(false); m_micLevelBar->setFixedHeight(16); micRow->addWidget(m_micLevelBar, 1); m_testMicBtn = new QPushButton("Test Microphone", this); m_testMicBtn->setCheckable(true); m_testMicBtn->setStyleSheet( "QPushButton { border: 1px solid palette(mid); border-radius: 4px;" " padding: 4px 12px; font-size: 12px; background: palette(button); }" "QPushButton:checked { background: #c0392b; color: white; }"); connect(m_testMicBtn, &QPushButton::toggled, this, &AudioDeviceSettingsPage::onTestMicToggled); micRow->addWidget(m_testMicBtn); layout->addLayout(micRow); /* --- Separator --- */ auto* sep2 = new QFrame(this); sep2->setFrameShape(QFrame::HLine); sep2->setStyleSheet("QFrame { color: palette(mid); }"); layout->addWidget(sep2); /* --- Speaker test --- */ auto* spkLabel = new QLabel("Speaker test", this); spkLabel->setStyleSheet("font-size: 13px; font-weight: bold;"); layout->addWidget(spkLabel); m_testSpeakerBtn = new QPushButton("Test Speaker (440Hz)", this); m_testSpeakerBtn->setStyleSheet( "QPushButton { border: 1px solid palette(mid); border-radius: 4px;" " padding: 6px 16px; font-size: 13px; background: palette(button); }" "QPushButton:hover { background: palette(light); }"); connect(m_testSpeakerBtn, &QPushButton::clicked, this, &AudioDeviceSettingsPage::onTestSpeaker); layout->addWidget(m_testSpeakerBtn); /* --- Separator --- */ auto* sep3 = new QFrame(this); sep3->setFrameShape(QFrame::HLine); sep3->setStyleSheet("QFrame { color: palette(mid); }"); layout->addWidget(sep3); /* --- Codec quality --- */ auto* codecLabel = new QLabel("Voice codec quality", this); codecLabel->setStyleSheet("font-size: 13px; font-weight: bold;"); layout->addWidget(codecLabel); m_codecPresetCombo = new QComboBox(this); m_codecPresetCombo->addItem("Low (16 kbps, fast)", 0); m_codecPresetCombo->addItem("Standard (32 kbps)", 1); m_codecPresetCombo->addItem("High (64 kbps, best)", 2); m_codecPresetCombo->setStyleSheet(m_outputCombo->styleSheet()); layout->addWidget(m_codecPresetCombo); /* --- Separator --- */ auto* sep5 = new QFrame(this); sep5->setFrameShape(QFrame::HLine); sep5->setStyleSheet("QFrame { color: palette(mid); }"); layout->addWidget(sep5); /* --- Radio (PTT) Opus --- */ auto* radioLabel = new QLabel("Radio (PTT) — Opus", this); radioLabel->setStyleSheet("font-size: 13px; font-weight: bold;"); layout->addWidget(radioLabel); auto* radioForm = new QFormLayout(); radioForm->setContentsMargins(0, 0, 0, 0); radioForm->setSpacing(10); m_radioOpusModeCombo = new QComboBox(this); m_radioOpusModeCombo->addItem("Voice (VOIP)", 0); m_radioOpusModeCombo->addItem("Music (AUDIO)", 1); m_radioOpusModeCombo->setStyleSheet(m_outputCombo->styleSheet()); radioForm->addRow(new QLabel("Mode:", this), m_radioOpusModeCombo); m_radioOpusBitrateCombo = new QComboBox(this); for (int br : {16, 32, 64, 96, 128}) m_radioOpusBitrateCombo->addItem(QString::number(br) + " kbps", br); m_radioOpusBitrateCombo->setStyleSheet(m_outputCombo->styleSheet()); radioForm->addRow(new QLabel("Bitrate:", this), m_radioOpusBitrateCombo); layout->addLayout(radioForm); m_radioCaptureStereoCb = new QCheckBox("Capture stereo (playback is always stereo)", this); m_radioCaptureStereoCb->setStyleSheet("font-size: 12px;"); layout->addWidget(m_radioCaptureStereoCb); /* --- Separator --- */ auto* sep4 = new QFrame(this); sep4->setFrameShape(QFrame::HLine); sep4->setStyleSheet("QFrame { color: palette(mid); }"); layout->addWidget(sep4); buildCompressorUI(layout); layout->addStretch(); m_levelTimer = new QTimer(this); m_levelTimer->setInterval(25); connect(m_levelTimer, &QTimer::timeout, this, &AudioDeviceSettingsPage::onLevelTimer); } AudioDeviceSettingsPage::~AudioDeviceSettingsPage() { stopMicTest(); } void AudioDeviceSettingsPage::loadFromDb() { auto playbackDevs = SoundManager::enumeratePlaybackDevices(); auto captureDevs = SoundManager::enumerateCaptureDevices(); /* Output devices */ m_outputCombo->clear(); m_outputCombo->addItem("Default (auto)", -1); for (int i = 0; i < playbackDevs.size(); i++) { QString label = playbackDevs[i].name; if (playbackDevs[i].isDefault) label += " (default)"; m_outputCombo->addItem(label, i); } /* Input devices */ m_inputCombo->clear(); m_inputCombo->addItem("Default (auto)", -1); for (int i = 0; i < captureDevs.size(); i++) { QString label = captureDevs[i].name; if (captureDevs[i].isDefault) label += " (default)"; m_inputCombo->addItem(label, i); } if (m_db) { m_savedOutputIdx = m_db->getUiStateInt("audio_output_device", -1); m_savedInputIdx = m_db->getUiStateInt("audio_input_device", -1); m_savedCodecPreset = m_db->getUiStateInt("opus_codec_preset", 1); m_savedRadioOpusMode = m_db->getUiStateInt("radio_opus_mode", 0); m_savedRadioOpusBitrate = m_db->getUiStateInt("radio_opus_bitrate", 32); m_savedRadioCaptureStereo = m_db->getUiStateInt("radio_capture_stereo", 0); m_savedCompressorEnabled = m_db->getUiStateInt("compressor_enabled", 0); m_savedCompressorMaxGainDb = m_db->getUiStateInt("compressor_max_gain_db", 25); m_savedCompressorRiseRate = m_db->getUiStateInt("compressor_rise_rate", 10); } /* Select saved indices */ for (int i = 0; i < m_outputCombo->count(); i++) { if (m_outputCombo->itemData(i).toInt() == m_savedOutputIdx) { m_outputCombo->setCurrentIndex(i); break; } } for (int i = 0; i < m_inputCombo->count(); i++) { if (m_inputCombo->itemData(i).toInt() == m_savedInputIdx) { m_inputCombo->setCurrentIndex(i); break; } } for (int i = 0; i < m_codecPresetCombo->count(); i++) { if (m_codecPresetCombo->itemData(i).toInt() == m_savedCodecPreset) { m_codecPresetCombo->setCurrentIndex(i); break; } } for (int i = 0; i < m_radioOpusModeCombo->count(); i++) { if (m_radioOpusModeCombo->itemData(i).toInt() == m_savedRadioOpusMode) { m_radioOpusModeCombo->setCurrentIndex(i); break; } } for (int i = 0; i < m_radioOpusBitrateCombo->count(); i++) { if (m_radioOpusBitrateCombo->itemData(i).toInt() == m_savedRadioOpusBitrate) { m_radioOpusBitrateCombo->setCurrentIndex(i); break; } } m_radioCaptureStereoCb->setChecked(m_savedRadioCaptureStereo != 0); m_compressorEnabledCb->setChecked(m_savedCompressorEnabled != 0); m_compressorMaxGainSlider->setValue(m_savedCompressorMaxGainDb); m_compressorRiseRateSlider->setValue(m_savedCompressorRiseRate); updateCompressorLabels(); } void AudioDeviceSettingsPage::applyAndSave() { int outIdx = m_outputCombo->currentData().toInt(); int inIdx = m_inputCombo->currentData().toInt(); int codecPreset = m_codecPresetCombo->currentData().toInt(); auto saveKey = [](const char* key, int val) { QByteArray v = QByteArray::number(val); size_t klen = strlen(key), vlen = (size_t)v.size(); size_t total = klen + 1 + vlen + 1; if (total > 256) return; struct save_ui_state_arg* arg = (struct save_ui_state_arg*)u_malloc(sizeof(struct save_ui_state_arg)); if (!arg) return; arg->inst = gui_bridge_get_inst(); memcpy(arg->data, key, klen + 1); memcpy(arg->data + klen + 1, v.constData(), vlen + 1); gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg); }; saveKey("audio_output_device", outIdx); saveKey("audio_input_device", inIdx); saveKey("opus_codec_preset", codecPreset); VoiceEncoder::setPreset(codecPreset); int radioOpusMode = m_radioOpusModeCombo->currentData().toInt(); int radioOpusBitrate = m_radioOpusBitrateCombo->currentData().toInt(); int radioCaptureStereo = m_radioCaptureStereoCb->isChecked() ? 1 : 0; saveKey("radio_opus_mode", radioOpusMode); saveKey("radio_opus_bitrate", radioOpusBitrate); saveKey("radio_capture_stereo", radioCaptureStereo); int compEnabled = m_compressorEnabledCb->isChecked() ? 1 : 0; int compMaxGain = m_compressorMaxGainSlider->value(); int compRiseRate = m_compressorRiseRateSlider->value(); saveKey("compressor_enabled", compEnabled); saveKey("compressor_max_gain_db", compMaxGain); saveKey("compressor_rise_rate", compRiseRate); /* also update [chat] config section + live apply for headless-compat settings */ if (!m_configPath.isEmpty()) { auto postChat = [](const QString& path, const char* name, int val) { config_update_option(path, "chatserver", name, QString::number(val), CONFIG_UPDATE_REPLACE); QByteArray v = QByteArray::number(val); size_t nlen = strlen(name), vlen = (size_t)v.size(), total = nlen + 1 + vlen + 1; if (total > 256) return; struct chat_setting_arg* arg = (struct chat_setting_arg*)u_malloc(sizeof(struct chat_setting_arg)); if (!arg) return; arg->inst = gui_bridge_get_inst(); memcpy(arg->data, name, nlen + 1); memcpy(arg->data + nlen + 1, v.constData(), vlen + 1); gui_bridge_post_uasync_fn(chat_core_set_setting_trampoline, arg); }; postChat(m_configPath, "opus_codec_preset", codecPreset); postChat(m_configPath, "radio_opus_mode", radioOpusMode); postChat(m_configPath, "radio_opus_bitrate", radioOpusBitrate); postChat(m_configPath, "radio_capture_stereo", radioCaptureStereo); postChat(m_configPath, "compressor_enabled", compEnabled); postChat(m_configPath, "compressor_max_gain_db", compMaxGain); postChat(m_configPath, "compressor_rise_rate", compRiseRate); } m_savedOutputIdx = outIdx; m_savedInputIdx = inIdx; m_savedCodecPreset = codecPreset; m_savedRadioOpusMode = radioOpusMode; m_savedRadioOpusBitrate = radioOpusBitrate; m_savedRadioCaptureStereo = radioCaptureStereo; m_savedCompressorEnabled = compEnabled; m_savedCompressorMaxGainDb = compMaxGain; m_savedCompressorRiseRate = compRiseRate; DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "saved output=%d input=%d codec=%d radioOpus=%d/%dkbps stereo=%d compressor=%d gain=%ddB rise=%.1fx", outIdx, inIdx, codecPreset, radioOpusMode, radioOpusBitrate, radioCaptureStereo, compEnabled, compMaxGain, (float)compRiseRate); if (m_recorder) { m_recorder->setCompressorEnabled(compEnabled != 0); if (m_recorder->compressor() && compEnabled) { audio_compressor_config_t compCfg = {0}; compCfg.sample_rate = 48000; compCfg.channels = 1; compCfg.max_gain_db = (float)compMaxGain; compCfg.lookback_ms = 200; compCfg.lookahead_ms = 100; compCfg.rise_rate_per_sec = (float)compRiseRate; compCfg.target_level = 1.0f; audio_compressor_configure(m_recorder->compressor(), &compCfg); } } } void AudioDeviceSettingsPage::onTestMicToggled() { if (m_testMicBtn->isChecked()) { m_testRecorder = new AudioRecorder(this); m_testRecorder->init(); int inIdx = m_inputCombo->currentData().toInt(); m_testRecorder->setCaptureDeviceIndex(inIdx); m_testRecorder->startRecording(); m_micTestActive = true; m_levelTimer->start(); m_testMicBtn->setText("Stop Test"); m_micLevelBar->setValue(0); } else { stopMicTest(); m_testMicBtn->setText("Test Microphone"); } } void AudioDeviceSettingsPage::stopMicTest() { m_levelTimer->stop(); if (m_testRecorder) { m_testRecorder->stopRecording(); m_testRecorder->shutdown(); delete m_testRecorder; m_testRecorder = nullptr; } m_micTestActive = false; m_micLevelBar->setValue(0); m_testMicBtn->setChecked(false); } void AudioDeviceSettingsPage::onLevelTimer() { if (!m_micTestActive || !m_testRecorder || !m_testRecorder->isRecording()) { stopMicTest(); return; } float peak = m_testRecorder->peakLevel(); /* Linear to dB: 20*log10(peak), map -60..0 dB to 0..100 */ float dbVal = peak > 0.0001f ? 20.0f * std::log10(peak) : -70.0f; int pct = (int)((dbVal + 70.0f) / 70.0f * 100.0f); if (pct < 0) pct = 0; if (pct > 100) pct = 100; m_micLevelBar->setValue(pct); const char* color = pct < 60 ? "#27ae60" : pct < 85 ? "#f1c40f" : "#e74c3c"; m_micLevelBar->setStyleSheet(QString( "QProgressBar { border: 1px solid palette(mid); border-radius: 4px; background: #333; }" "QProgressBar::chunk { background: %1; border-radius: 3px; }").arg(color)); } void AudioDeviceSettingsPage::onTestSpeaker() { int sampleRate = 48000; int samples = sampleRate / 4; /* 250ms */ std::vector pcm(samples); for (int i = 0; i < samples; i++) pcm[i] = (int16_t)(12000.0 * sin(2.0 * M_PI * 440.0 * i / sampleRate)); SoundManager::instance()->playRawPcm(pcm.data(), samples, sampleRate, 1); } void AudioDeviceSettingsPage::buildCompressorUI(QVBoxLayout* layout) { auto* compLabel = new QLabel("Voice Compressor", this); compLabel->setStyleSheet("font-size: 13px; font-weight: bold;"); layout->addWidget(compLabel); m_compressorEnabledCb = new QCheckBox("Enable compressor (leveler/normalizer)", this); m_compressorEnabledCb->setStyleSheet("font-size: 12px;"); layout->addWidget(m_compressorEnabledCb); auto makeSliderRow = [&](const QString& name, QSlider*& slider, QLabel*& lbl, int min, int max, int step) { auto* row = new QHBoxLayout(); row->setSpacing(6); auto* textLabel = new QLabel(name, this); textLabel->setFixedWidth(180); textLabel->setStyleSheet("font-size: 12px;"); row->addWidget(textLabel); slider = new QSlider(Qt::Horizontal, this); slider->setRange(min, max); slider->setSingleStep(step); slider->setStyleSheet( "QSlider::groove:horizontal { height: 4px; background: palette(mid); border-radius: 2px; }" "QSlider::handle:horizontal { width: 12px; margin: -5px 0; background: palette(highlight); border-radius: 6px; }"); row->addWidget(slider, 1); lbl = new QLabel(this); lbl->setFixedWidth(60); lbl->setStyleSheet("font-size: 12px;"); lbl->setAlignment(Qt::AlignRight | Qt::AlignVCenter); row->addWidget(lbl); layout->addLayout(row); }; makeSliderRow("Max gain:", m_compressorMaxGainSlider, m_compressorMaxGainLabel, 1, 50, 1); makeSliderRow("Rise rate (x/sec):", m_compressorRiseRateSlider, m_compressorRiseRateLabel, 2, 10, 1); auto connectSlider = [this](QSlider* slider) { connect(slider, &QSlider::valueChanged, this, &AudioDeviceSettingsPage::updateCompressorLabels); }; connectSlider(m_compressorMaxGainSlider); connectSlider(m_compressorRiseRateSlider); } void AudioDeviceSettingsPage::updateCompressorLabels() { if (m_compressorMaxGainLabel) m_compressorMaxGainLabel->setText(QString::number(m_compressorMaxGainSlider->value()) + " dB"); if (m_compressorRiseRateLabel) { int v = m_compressorRiseRateSlider->value(); m_compressorRiseRateLabel->setText(QString::number(v) + "x"); } }