Files
utun2/tools/chatgui/src/audiodevicesettingspage.cpp
T

460 lines
21 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include <QCoreApplication>
#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"
extern "C" {
#include "call/call_video_rate.h"
}
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QFormLayout>
#include <QFrame>
#include <QDebug>
#include <QDoubleSpinBox>
#include <QSpinBox>
#include <QMessageBox>
#include <cmath>
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(QCoreApplication::translate("ChatGui", "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(QCoreApplication::translate("ChatGui", "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(QCoreApplication::translate("ChatGui", "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(QCoreApplication::translate("ChatGui", "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(QCoreApplication::translate("ChatGui", "Speaker test"), this);
spkLabel->setStyleSheet("font-size: 13px; font-weight: bold;");
layout->addWidget(spkLabel);
m_testSpeakerBtn = new QPushButton(QCoreApplication::translate("ChatGui", "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(QCoreApplication::translate("ChatGui", "Voice codec quality"), this);
codecLabel->setStyleSheet("font-size: 13px; font-weight: bold;");
layout->addWidget(codecLabel);
m_codecPresetCombo = new QComboBox(this);
m_codecPresetCombo->addItem(QCoreApplication::translate("ChatGui", "Low (16 kbps, fast)"), 0);
m_codecPresetCombo->addItem(QCoreApplication::translate("ChatGui", "Standard (32 kbps)"), 1);
m_codecPresetCombo->addItem(QCoreApplication::translate("ChatGui", "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(QCoreApplication::translate("ChatGui", "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(QCoreApplication::translate("ChatGui", "Voice (VOIP)"), 0);
m_radioOpusModeCombo->addItem(QCoreApplication::translate("ChatGui", "Music (AUDIO)"), 1);
m_radioOpusModeCombo->setStyleSheet(m_outputCombo->styleSheet());
radioForm->addRow(new QLabel(QCoreApplication::translate("ChatGui", "Mode:"), this), m_radioOpusModeCombo);
m_radioOpusBitrateCombo = new QComboBox(this);
for (int br : {16, 32, 64, 96, 128})
m_radioOpusBitrateCombo->addItem(QString::number(br) + QCoreApplication::translate("ChatGui", " kbps"), br);
m_radioOpusBitrateCombo->setStyleSheet(m_outputCombo->styleSheet());
radioForm->addRow(new QLabel(QCoreApplication::translate("ChatGui", "Bitrate:"), this), m_radioOpusBitrateCombo);
layout->addLayout(radioForm);
m_radioCaptureStereoCb = new QCheckBox(QCoreApplication::translate("ChatGui", "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);
auto* callForm = new QFormLayout();
m_jitterReserveSpin = new QDoubleSpinBox(this);
m_jitterReserveSpin->setRange(0.5, 2.0);
m_jitterReserveSpin->setDecimals(1);
m_jitterReserveSpin->setSingleStep(0.1);
m_jitterReserveSpin->setValue(1.0);
m_jitterReserveSpin->setSuffix(QCoreApplication::translate("ChatGui", " s"));
m_jitterReserveSpin->setToolTip(QCoreApplication::translate("ChatGui", "Maximum adaptive jitter reserve for calls. Applies to the next call."));
callForm->addRow(QCoreApplication::translate("ChatGui", "Call: maximum jitter reserve"), m_jitterReserveSpin);
m_videoMinimumSpin = new QSpinBox(this);
m_videoMaximumSpin = new QSpinBox(this);
for (auto* spin : {m_videoMinimumSpin, m_videoMaximumSpin}) {
spin->setRange(CALL_VIDEO_RATE_MIN_BPS / 1000, CALL_VIDEO_RATE_MAX_BPS / 1000);
spin->setSingleStep(100);
spin->setSuffix(QCoreApplication::translate("ChatGui", " kbit/s"));
}
m_videoMinimumSpin->setValue(CALL_VIDEO_RATE_MIN_BPS / 1000);
m_videoMaximumSpin->setValue(CALL_VIDEO_RATE_MAX_BPS / 1000);
connect(m_videoMinimumSpin, QOverload<int>::of(&QSpinBox::valueChanged), m_videoMaximumSpin, &QSpinBox::setMinimum);
connect(m_videoMaximumSpin, QOverload<int>::of(&QSpinBox::valueChanged), m_videoMinimumSpin, &QSpinBox::setMaximum);
callForm->addRow(QCoreApplication::translate("ChatGui", "Call video: minimum bitrate"), m_videoMinimumSpin);
callForm->addRow(QCoreApplication::translate("ChatGui", "Call video: maximum bitrate"), m_videoMaximumSpin);
layout->addLayout(callForm);
auto* videoHint = new QLabel(QCoreApplication::translate("ChatGui",
"Video bitrate adapts to the connection within this range. Default: 300–4000 kbit/s."), this);
videoHint->setWordWrap(true);
layout->addWidget(videoHint);
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(QCoreApplication::translate("ChatGui", "Default (auto)"), QString());
for (int i = 0; i < playbackDevs.size(); i++) {
QString label = playbackDevs[i].name;
if (playbackDevs[i].isDefault) label += QCoreApplication::translate("ChatGui", " (default)");
m_outputCombo->addItem(label, playbackDevs[i].id);
}
/* Input devices */
m_inputCombo->clear();
m_inputCombo->addItem(QCoreApplication::translate("ChatGui", "Default (auto)"), QString());
for (int i = 0; i < captureDevs.size(); i++) {
QString label = captureDevs[i].name;
if (captureDevs[i].isDefault) label += QCoreApplication::translate("ChatGui", " (default)");
m_inputCombo->addItem(label, captureDevs[i].id);
}
if (m_db) {
m_savedOutputId = m_db->getUiState("audio_output_id");
m_savedInputId = m_db->getUiState("audio_input_id");
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);
m_jitterReserveSpin->setValue(m_db->getUiStateInt("call_jitter_max_reserve_ms", 1000) / 1000.0);
QByteArray range = m_db->getUiState("call_video_bitrate_range_kbps").toUtf8();
uint32_t minimum = CALL_VIDEO_RATE_MIN_BPS, maximum = CALL_VIDEO_RATE_MAX_BPS;
if (!range.isEmpty()) call_video_rate_parse_range(range.constData(), &minimum, &maximum);
m_videoMinimumSpin->setValue(CALL_VIDEO_RATE_MIN_BPS / 1000);
m_videoMaximumSpin->setValue(maximum / 1000);
m_videoMinimumSpin->setValue(minimum / 1000);
m_savedAecEnabled = m_db->getUiStateInt("aec_enabled", 1);
}
/* Select saved indices */
for (int i = 0; i < m_outputCombo->count(); i++) {
if (m_outputCombo->itemData(i).toString() == m_savedOutputId)
{ m_outputCombo->setCurrentIndex(i); break; }
}
for (int i = 0; i < m_inputCombo->count(); i++) {
if (m_inputCombo->itemData(i).toString() == m_savedInputId)
{ 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);
m_aecEnabledCb->setChecked(m_savedAecEnabled != 0);
updateCompressorLabels();
}
void AudioDeviceSettingsPage::applyAndSave() {
const char* rangeKey = "call_video_bitrate_range_kbps";
QString range = QStringLiteral("%1:%2").arg(m_videoMinimumSpin->value()).arg(m_videoMaximumSpin->value());
if (!m_configPath.isEmpty() && !config_update_option(m_configPath, "chatserver", rangeKey, range)) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "video bitrate range config save failed path=%s", qPrintable(m_configPath));
QMessageBox::warning(this, QCoreApplication::translate("ChatGui", "Settings"),
QCoreApplication::translate("ChatGui", "Could not save the call video bitrate range."));
return;
}
gui_bridge_post_setting(GUI_SETTING_CHAT, rangeKey, range.toUtf8().constData());
QString outputId = m_outputCombo->currentData().toString();
QString inputId = m_inputCombo->currentData().toString();
int codecPreset = m_codecPresetCombo->currentData().toInt();
const QPair<const char*, QString> textSettings[] = {{"audio_output_id", outputId}, {"audio_input_id", inputId}, {rangeKey, range}};
for (const auto& setting : textSettings)
gui_bridge_post_setting(GUI_SETTING_UI_STATE, setting.first, setting.second.toUtf8().constData());
SoundManager::instance()->setDevices(outputId, inputId);
VoiceEncoder::setPreset(codecPreset);
int radioOpusMode = m_radioOpusModeCombo->currentData().toInt();
int radioOpusBitrate = m_radioOpusBitrateCombo->currentData().toInt();
int radioCaptureStereo = m_radioCaptureStereoCb->isChecked() ? 1 : 0;
int compEnabled = m_compressorEnabledCb->isChecked() ? 1 : 0;
int compMaxGain = m_compressorMaxGainSlider->value();
int compRiseRate = m_compressorRiseRateSlider->value();
int jitterReserveMs = (int)std::lround(m_jitterReserveSpin->value() * 1000);
int aecEnabled = m_aecEnabledCb->isChecked() ? 1 : 0;
const QPair<const char*, int> numericSettings[] = {
{"opus_codec_preset", codecPreset}, {"radio_opus_mode", radioOpusMode}, {"radio_opus_bitrate", radioOpusBitrate},
{"radio_capture_stereo", radioCaptureStereo}, {"compressor_enabled", compEnabled}, {"compressor_max_gain_db", compMaxGain},
{"compressor_rise_rate", compRiseRate}, {"call_jitter_max_reserve_ms", jitterReserveMs}, {"aec_enabled", aecEnabled}
};
for (const auto& setting : numericSettings) {
const QByteArray value = QByteArray::number(setting.second);
gui_bridge_post_setting(GUI_SETTING_UI_STATE, setting.first, value.constData());
if (m_configPath.isEmpty()) continue;
if (!config_update_option(m_configPath, "chatserver", setting.first, QString::fromLatin1(value))) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Audio settings: config save failed key=%s", setting.first); continue;
}
gui_bridge_post_setting(GUI_SETTING_CHAT, setting.first, value.constData());
}
m_savedOutputId = outputId;
m_savedInputId = inputId;
m_savedCodecPreset = codecPreset;
m_savedRadioOpusMode = radioOpusMode;
m_savedRadioOpusBitrate = radioOpusBitrate;
m_savedRadioCaptureStereo = radioCaptureStereo;
m_savedCompressorEnabled = compEnabled;
m_savedCompressorMaxGainDb = compMaxGain;
m_savedCompressorRiseRate = compRiseRate;
m_savedAecEnabled = aecEnabled;
DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG,
"requested output_default=%d input_default=%d codec=%d radioOpus=%d/%dkbps stereo=%d compressor=%d gain=%ddB rise=%.1fx aec=%d",
outputId.isEmpty(), inputId.isEmpty(), codecPreset, radioOpusMode, radioOpusBitrate, radioCaptureStereo,
compEnabled, compMaxGain, (float)compRiseRate, aecEnabled);
if (m_recorder) {
m_recorder->setCompressorEnabled(compEnabled != 0);
{
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;
m_recorder->configureCompressor(compCfg);
}
}
}
void AudioDeviceSettingsPage::onTestMicToggled() {
if (m_testMicBtn->isChecked()) {
m_testRecorder = new AudioRecorder(this);
m_testRecorder->init();
QString id = m_inputCombo->currentData().toString();
m_testRecorder->setCaptureDevice(id);
m_testRecorder->startRecording(true);
m_micTestActive = true;
m_levelTimer->start();
m_testMicBtn->setText(QCoreApplication::translate("ChatGui", "Stop Test"));
m_micLevelBar->setValue(0);
} else {
stopMicTest();
m_testMicBtn->setText(QCoreApplication::translate("ChatGui", "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<int16_t> 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, {}, true);
}
void AudioDeviceSettingsPage::buildCompressorUI(QVBoxLayout* layout) {
auto* compLabel = new QLabel(QCoreApplication::translate("ChatGui", "Voice Compressor"), this);
compLabel->setStyleSheet("font-size: 13px; font-weight: bold;");
layout->addWidget(compLabel);
m_compressorEnabledCb = new QCheckBox(QCoreApplication::translate("ChatGui", "Enable compressor (leveler/normalizer)"), this);
m_compressorEnabledCb->setStyleSheet("font-size: 12px;");
layout->addWidget(m_compressorEnabledCb);
m_aecEnabledCb = new QCheckBox(QCoreApplication::translate("ChatGui", "Enable echo cancellation (AEC) in calls"), this);
m_aecEnabledCb->setStyleSheet("font-size: 12px;");
layout->addWidget(m_aecEnabledCb);
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->setMinimumWidth(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(QCoreApplication::translate("ChatGui", "Max gain:"), m_compressorMaxGainSlider, m_compressorMaxGainLabel,
1, 50, 1);
makeSliderRow(QCoreApplication::translate("ChatGui", "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()) + QCoreApplication::translate("ChatGui", " dB"));
if (m_compressorRiseRateLabel) {
int v = m_compressorRiseRateSlider->value();
m_compressorRiseRateLabel->setText(QString::number(v) + "x");
}
}