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.
 
 
 
 
 
 

375 lines
15 KiB

#include "audiodevicesettingspage.h"
#include "sound_manager.h"
#include "audiorecorder.h"
#include "audiocompressor.h"
#include "voicemessageencoder.h"
#include "../../lib/debug_config.h"
#include "../db/db_manager.h"
#include "../transport/gui_bridge.h"
#include "../../lib/mem.h"
extern "C" {
#include "chat/chat_core.h"
}
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QFormLayout>
#include <QFrame>
#include <QDebug>
#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("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);
m_micLevelBar->setStyleSheet(
"QProgressBar { border: 1px solid palette(mid); border-radius: 4px; background: #333; }"
"QProgressBar::chunk { background: qlineargradient(x1:0, y1:0, x2:1, y2:0,"
" stop:0 #27ae60, stop:0.6 #f1c40f, stop:1 #e74c3c); border-radius: 3px; }");
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* 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_savedCompressorEnabled = m_db->getUiStateInt("compressor_enabled", 0);
m_savedCompressorMaxGainDb = m_db->getUiStateInt("compressor_max_gain_db", 30);
m_savedCompressorLookbackMs = m_db->getUiStateInt("compressor_lookback_ms", 200);
m_savedCompressorLookaheadMs = m_db->getUiStateInt("compressor_lookahead_ms", 100);
m_savedCompressorRiseRateTenths = m_db->getUiStateInt("compressor_rise_rate_tenths", 20);
m_savedCompressorTargetLevelDb = m_db->getUiStateInt("compressor_target_level_db", -12);
}
/* 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; }
}
m_compressorEnabledCb->setChecked(m_savedCompressorEnabled != 0);
m_compressorMaxGainSlider->setValue(m_savedCompressorMaxGainDb);
m_compressorLookbackSlider->setValue(m_savedCompressorLookbackMs);
m_compressorLookaheadSlider->setValue(m_savedCompressorLookaheadMs);
m_compressorRiseRateSlider->setValue(m_savedCompressorRiseRateTenths);
m_compressorTargetLevelSlider->setValue(m_savedCompressorTargetLevelDb);
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;
void* arg = u_malloc(total);
memcpy(arg, key, klen + 1);
memcpy((char*)arg + 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 compEnabled = m_compressorEnabledCb->isChecked() ? 1 : 0;
int compMaxGain = m_compressorMaxGainSlider->value();
int compLookback = m_compressorLookbackSlider->value();
int compLookahead = m_compressorLookaheadSlider->value();
int compRiseRateTenths = m_compressorRiseRateSlider->value();
int compTargetLevel = m_compressorTargetLevelSlider->value();
saveKey("compressor_enabled", compEnabled);
saveKey("compressor_max_gain_db", compMaxGain);
saveKey("compressor_lookback_ms", compLookback);
saveKey("compressor_lookahead_ms", compLookahead);
saveKey("compressor_rise_rate_tenths", compRiseRateTenths);
saveKey("compressor_target_level_db", compTargetLevel);
m_savedOutputIdx = outIdx;
m_savedInputIdx = inIdx;
m_savedCodecPreset = codecPreset;
m_savedCompressorEnabled = compEnabled;
m_savedCompressorMaxGainDb = compMaxGain;
m_savedCompressorLookbackMs = compLookback;
m_savedCompressorLookaheadMs = compLookahead;
m_savedCompressorRiseRateTenths = compRiseRateTenths;
m_savedCompressorTargetLevelDb = compTargetLevel;
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "saved output=%d input=%d codec=%d compressor=%d gain=%ddB lookback=%d lookahead=%d rise=%.1fx target=%ddB",
outIdx, inIdx, codecPreset, compEnabled, compMaxGain, compLookback, compLookahead,
(float)compRiseRateTenths / 10.0f, compTargetLevel);
if (m_recorder) {
m_recorder->setCompressorEnabled(compEnabled != 0);
if (m_recorder->compressor() && compEnabled) {
AudioCompressor::Config compCfg;
compCfg.sampleRate = 48000;
compCfg.channels = 1;
compCfg.maxGainDb = (float)compMaxGain;
compCfg.lookbackMs = compLookback;
compCfg.lookaheadMs = compLookahead;
compCfg.riseRatePer500ms = (float)compRiseRateTenths / 10.0f;
compCfg.targetLevel = powf(10.0f, (float)compTargetLevel / 20.0f);
m_recorder->compressor()->configure(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);
}
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);
}
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,
0, 30, 1);
makeSliderRow("Lookback time:", m_compressorLookbackSlider, m_compressorLookbackLabel,
50, 500, 10);
makeSliderRow("Lookahead time:", m_compressorLookaheadSlider, m_compressorLookaheadLabel,
20, 500, 10);
makeSliderRow("Rise rate (x/500ms):", m_compressorRiseRateSlider, m_compressorRiseRateLabel,
11, 100, 1);
makeSliderRow("Target level:", m_compressorTargetLevelSlider, m_compressorTargetLevelLabel,
-40, 0, 1);
auto connectSlider = [this](QSlider* slider) {
connect(slider, &QSlider::valueChanged, this, &AudioDeviceSettingsPage::updateCompressorLabels);
};
connectSlider(m_compressorMaxGainSlider);
connectSlider(m_compressorLookbackSlider);
connectSlider(m_compressorLookaheadSlider);
connectSlider(m_compressorRiseRateSlider);
connectSlider(m_compressorTargetLevelSlider);
}
void AudioDeviceSettingsPage::updateCompressorLabels() {
if (m_compressorMaxGainLabel)
m_compressorMaxGainLabel->setText(QString::number(m_compressorMaxGainSlider->value()) + " dB");
if (m_compressorLookbackLabel)
m_compressorLookbackLabel->setText(QString::number(m_compressorLookbackSlider->value()) + " ms");
if (m_compressorLookaheadLabel)
m_compressorLookaheadLabel->setText(QString::number(m_compressorLookaheadSlider->value()) + " ms");
if (m_compressorRiseRateLabel) {
float v = (float)m_compressorRiseRateSlider->value() / 10.0f;
m_compressorRiseRateLabel->setText(QString::number(v, 'f', 1) + "x");
}
if (m_compressorTargetLevelLabel)
m_compressorTargetLevelLabel->setText(QString::number(m_compressorTargetLevelSlider->value()) + " dBFS");
}