From f1b599f0c0df91c2fdeb58b8e094d44273748dab Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 27 Jul 2026 11:24:47 +0300 Subject: [PATCH] feat: voice compressor with RMS+peak leveling, sliding window envelope, rate-limited gain --- tools/chatgui/CMakeLists.txt | 1 + tools/chatgui/src/audiocompressor.cpp | 147 ++++++++++++++++++ tools/chatgui/src/audiocompressor.h | 59 +++++++ tools/chatgui/src/audiodevicesettingspage.cpp | 115 +++++++++++++- tools/chatgui/src/audiodevicesettingspage.h | 24 +++ tools/chatgui/src/audiorecorder.cpp | 40 ++++- tools/chatgui/src/audiorecorder.h | 10 +- tools/chatgui/src/mainwindow.cpp | 19 ++- 8 files changed, 406 insertions(+), 9 deletions(-) create mode 100644 tools/chatgui/src/audiocompressor.cpp create mode 100644 tools/chatgui/src/audiocompressor.h diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index b2c33aca..899084e6 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -77,6 +77,7 @@ add_executable(chatgui src/soundsettingspage.cpp src/sound_manager.cpp src/audiorecorder.cpp + src/audiocompressor.cpp src/voicemessageencoder.cpp src/voiceplayback.cpp src/audiodevicesettingspage.cpp diff --git a/tools/chatgui/src/audiocompressor.cpp b/tools/chatgui/src/audiocompressor.cpp new file mode 100644 index 00000000..5ec0d738 --- /dev/null +++ b/tools/chatgui/src/audiocompressor.cpp @@ -0,0 +1,147 @@ +#include "audiocompressor.h" + +#include +#include +#include +#include + +void AudioCompressor::configure(const Config& cfg) { + m_cfg = cfg; + m_blockSamples = m_cfg.sampleRate * m_cfg.blockDurationMs / 1000 * m_cfg.channels; + m_lookbackBlocks = m_cfg.lookbackMs / m_cfg.blockDurationMs; + m_lookaheadBlocks = m_cfg.lookaheadMs / m_cfg.blockDurationMs; + m_riseFactorPerBlock = powf(m_cfg.riseRatePer500ms, (float)m_cfg.blockDurationMs / 500.0f); + m_maxGain = powf(10.0f, m_cfg.maxGainDb / 20.0f); + qDebug("AudioCompressor: config rate=%d ch=%d block=%dms window=%dms+%dms maxGain=%.0fdB riseRate=%.1fx/500ms target=%.0fdBFS", + m_cfg.sampleRate, m_cfg.channels, m_cfg.blockDurationMs, + m_cfg.lookbackMs, m_cfg.lookaheadMs, m_cfg.maxGainDb, + m_cfg.riseRatePer500ms, 20.0f * log10f(m_cfg.targetLevel)); +} + +void AudioCompressor::reset() { + m_blockLevels.clear(); + m_pending.clear(); + m_accumulator.clear(); + m_output.clear(); + m_gainSmoothed = 1.0f; + m_dbgCounter = 0; +} + +void AudioCompressor::push(const int16_t* samples, size_t count) { + if (!m_enabled) { + m_output.insert(m_output.end(), samples, samples + count); + return; + } + + size_t remaining = count; + const int16_t* src = samples; + while (remaining > 0) { + size_t need = (size_t)m_blockSamples - m_accumulator.size(); + size_t take = std::min(remaining, need); + m_accumulator.insert(m_accumulator.end(), src, src + take); + src += take; + remaining -= take; + if (m_accumulator.size() == (size_t)m_blockSamples) { + float level = computeBlockLevel(m_accumulator.data(), m_accumulator.size()); + + BlockLevel bl; + bl.level = level; + m_blockLevels.push_back(bl); + + PendingBlock pb; + pb.samples = std::move(m_accumulator); + pb.levelIndex = m_blockLevels.size() - 1; + m_pending.push_back(std::move(pb)); + + m_accumulator.clear(); + } + } + + while (m_pending.size() > (size_t)m_lookaheadBlocks) { + processPendingBlock(); + } +} + +float AudioCompressor::computeBlockLevel(const int16_t* samples, size_t count) const { + float sumSq = 0.0f; + float peak = 0.0f; + for (size_t i = 0; i < count; i++) { + float v = (float)samples[i] / 32768.0f; + sumSq += v * v; + float av = fabsf(v); + if (av > peak) peak = av; + } + float rms = sqrtf(sumSq / (float)count); + return (rms * 2.0f + peak) / 2.0f; +} + +void AudioCompressor::processPendingBlock() { + PendingBlock& block = m_pending.front(); + size_t idx = block.levelIndex; + + size_t winStart = idx >= (size_t)m_lookbackBlocks ? idx - (size_t)m_lookbackBlocks : 0; + size_t winEnd = std::min(idx + (size_t)m_lookaheadBlocks, m_blockLevels.size() - 1); + + float envelope = 0.0f; + for (size_t i = winStart; i <= winEnd; i++) { + if (m_blockLevels[i].level > envelope) envelope = m_blockLevels[i].level; + } + if (envelope < 0.0001f) envelope = 0.0001f; + + float G_raw = m_cfg.targetLevel / envelope; + + float G_prev = m_gainSmoothed; + if (G_raw > m_gainSmoothed) { + float maxRise = m_gainSmoothed * m_riseFactorPerBlock; + m_gainSmoothed = std::min(G_raw, maxRise); + } else { + m_gainSmoothed = G_raw; + } + + m_gainSmoothed = std::min(m_gainSmoothed, m_maxGain); + + static int dbgCounter = 0; + float gainDb = 20.0f * log10f(m_gainSmoothed); + float prevDb = 20.0f * log10f(G_prev > 0.0001f ? G_prev : 0.0001f); + if (m_dbgCounter % 25 == 0 || fabsf(gainDb - prevDb) > 3.0f) { + qDebug("AudioCompressor: block %zu level=%.4f envelope=%.4f gainRaw=%.1fdB gain=%.1fdB", + idx, m_blockLevels[idx].level, envelope, 20.0f * log10f(G_raw), gainDb); + } + m_dbgCounter++; + + for (size_t i = 0; i < block.samples.size(); i++) { + float v = (float)block.samples[i] * m_gainSmoothed; + if (v > 32767.0f) v = 32767.0f; + if (v < -32768.0f) v = -32768.0f; + m_output.push_back((int16_t)(int)v); + } + + m_pending.pop_front(); +} + +void AudioCompressor::flush() { + if (!m_enabled) return; + + if (!m_accumulator.empty()) { + float level = computeBlockLevel(m_accumulator.data(), m_accumulator.size()); + + BlockLevel bl; + bl.level = level; + m_blockLevels.push_back(bl); + + PendingBlock pb; + pb.samples = std::move(m_accumulator); + pb.levelIndex = m_blockLevels.size() - 1; + m_pending.push_back(std::move(pb)); + + m_accumulator.clear(); + } + + int remain = (int)m_pending.size(); + qDebug("AudioCompressor: flush %d pending blocks (gainSmoothed=%.1fdB)", remain, + 20.0f * log10f(m_gainSmoothed)); + + while (!m_pending.empty()) { + processPendingBlock(); + } +} diff --git a/tools/chatgui/src/audiocompressor.h b/tools/chatgui/src/audiocompressor.h new file mode 100644 index 00000000..4e7f6073 --- /dev/null +++ b/tools/chatgui/src/audiocompressor.h @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include + +class AudioCompressor { +public: + struct Config { + int sampleRate = 48000; + int channels = 1; + int blockDurationMs = 20; + int lookbackMs = 200; + int lookaheadMs = 100; + float maxGainDb = 30.0f; + float riseRatePer500ms = 2.0f; + float targetLevel = 0.25f; + }; + + void configure(const Config& cfg); + void reset(); + + void setEnabled(bool enabled) { m_enabled = enabled; } + bool isEnabled() const { return m_enabled; } + + void push(const int16_t* samples, size_t count); + const std::vector& outputBuffer() const { return m_output; } + bool hasPending() const { return !m_pending.empty() || !m_accumulator.empty(); } + void flush(); + +private: + Config m_cfg; + bool m_enabled = true; + + struct BlockLevel { + float level; + }; + std::deque m_blockLevels; + + struct PendingBlock { + std::vector samples; + size_t levelIndex; + }; + std::deque m_pending; + std::vector m_accumulator; + + std::vector m_output; + + int m_blockSamples = 960; + int m_lookbackBlocks = 10; + int m_lookaheadBlocks = 5; + float m_riseFactorPerBlock = 1.0f; + float m_maxGain = 31.62f; + float m_gainSmoothed = 1.0f; + int m_dbgCounter = 0; + + float computeBlockLevel(const int16_t* samples, size_t count) const; + void processPendingBlock(); +}; diff --git a/tools/chatgui/src/audiodevicesettingspage.cpp b/tools/chatgui/src/audiodevicesettingspage.cpp index 773b1406..278ffb12 100644 --- a/tools/chatgui/src/audiodevicesettingspage.cpp +++ b/tools/chatgui/src/audiodevicesettingspage.cpp @@ -1,6 +1,7 @@ #include "audiodevicesettingspage.h" #include "sound_manager.h" #include "audiorecorder.h" +#include "audiocompressor.h" #include "voicemessageencoder.h" #include "../db/db_manager.h" #include "../transport/gui_bridge.h" @@ -111,6 +112,14 @@ AudioDeviceSettingsPage::AudioDeviceSettingsPage(DbManager* db, QWidget* parent) 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); @@ -148,6 +157,12 @@ void AudioDeviceSettingsPage::loadFromDb() { 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 */ @@ -163,6 +178,14 @@ void AudioDeviceSettingsPage::loadFromDb() { 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() { @@ -186,10 +209,32 @@ void AudioDeviceSettingsPage::applyAndSave() { 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; - qDebug("AudioDeviceSettings: saved output=%d input=%d codec=%d", outIdx, inIdx, codecPreset); + m_savedCompressorEnabled = compEnabled; + m_savedCompressorMaxGainDb = compMaxGain; + m_savedCompressorLookbackMs = compLookback; + m_savedCompressorLookaheadMs = compLookahead; + m_savedCompressorRiseRateTenths = compRiseRateTenths; + m_savedCompressorTargetLevelDb = compTargetLevel; + qDebug("AudioDeviceSettings: 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); } void AudioDeviceSettingsPage::onTestMicToggled() { @@ -244,3 +289,71 @@ void AudioDeviceSettingsPage::onTestSpeaker() { 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"); +} diff --git a/tools/chatgui/src/audiodevicesettingspage.h b/tools/chatgui/src/audiodevicesettingspage.h index 3bef10ba..296144e0 100644 --- a/tools/chatgui/src/audiodevicesettingspage.h +++ b/tools/chatgui/src/audiodevicesettingspage.h @@ -3,9 +3,13 @@ #include #include #include +#include +#include +#include #include class DbManager; class AudioRecorder; +class AudioCompressor; class AudioDeviceSettingsPage : public QWidget { Q_OBJECT @@ -22,6 +26,8 @@ private slots: private: void stopMicTest(); + void buildCompressorUI(class QVBoxLayout* layout); + void updateCompressorLabels(); DbManager* m_db; AudioRecorder* m_testRecorder = nullptr; @@ -36,4 +42,22 @@ private: int m_savedOutputIdx = -1; int m_savedInputIdx = -1; int m_savedCodecPreset = 1; + + QCheckBox* m_compressorEnabledCb; + QSlider* m_compressorMaxGainSlider; + QSlider* m_compressorLookbackSlider; + QSlider* m_compressorLookaheadSlider; + QSlider* m_compressorRiseRateSlider; + QSlider* m_compressorTargetLevelSlider; + QLabel* m_compressorMaxGainLabel; + QLabel* m_compressorLookbackLabel; + QLabel* m_compressorLookaheadLabel; + QLabel* m_compressorRiseRateLabel; + QLabel* m_compressorTargetLevelLabel; + int m_savedCompressorEnabled = 0; + int m_savedCompressorMaxGainDb = 30; + int m_savedCompressorLookbackMs = 200; + int m_savedCompressorLookaheadMs = 100; + int m_savedCompressorRiseRateTenths = 20; + int m_savedCompressorTargetLevelDb = -12; }; diff --git a/tools/chatgui/src/audiorecorder.cpp b/tools/chatgui/src/audiorecorder.cpp index 8bfb09a7..f608abde 100644 --- a/tools/chatgui/src/audiorecorder.cpp +++ b/tools/chatgui/src/audiorecorder.cpp @@ -1,4 +1,5 @@ #include "audiorecorder.h" +#include "audiocompressor.h" #include "miniaudio.h" #include "sound_manager.h" @@ -16,10 +17,15 @@ void audioCaptureCallback(ma_device* pDevice, void* pOutput, const void* pInput, if (!pInput) return; const int16_t* src = (const int16_t*)pInput; - size_t cur = self->m_pcmBuffer.size(); size_t add = (size_t)frameCount * (size_t)self->m_channels; - self->m_pcmBuffer.resize(cur + add); - std::memcpy(self->m_pcmBuffer.data() + cur, src, add * sizeof(int16_t)); + + if (self->m_compressor && self->m_compressorEnabled) { + self->m_compressor->push(src, add); + } else { + size_t cur = self->m_pcmBuffer.size(); + self->m_pcmBuffer.resize(cur + add); + std::memcpy(self->m_pcmBuffer.data() + cur, src, add * sizeof(int16_t)); + } float sumSq = 0.0f; for (unsigned int i = 0; i < frameCount * (unsigned int)self->m_channels; i++) { @@ -42,6 +48,11 @@ void audioCaptureCallback(ma_device* pDevice, void* pOutput, const void* pInput, AudioRecorder::AudioRecorder(QObject* parent) : QObject(parent) { g_recorder = this; + m_compressor = new AudioCompressor; + AudioCompressor::Config cfg; + cfg.sampleRate = m_sampleRate; + cfg.channels = m_channels; + m_compressor->configure(cfg); m_durationTimer.setInterval(100); connect(&m_durationTimer, &QTimer::timeout, this, [this]() { if (m_recording) { @@ -53,6 +64,8 @@ AudioRecorder::AudioRecorder(QObject* parent) AudioRecorder::~AudioRecorder() { shutdown(); + delete m_compressor; + m_compressor = nullptr; if (g_recorder == this) g_recorder = nullptr; } @@ -86,6 +99,8 @@ void AudioRecorder::startRecording() { m_rmsIdx = 0; m_elapsedMs = 0; + if (m_compressor) m_compressor->reset(); + ma_context* ctx = SoundManager::instance()->context(); if (!ctx) { qWarning("AudioRecorder: no audio context"); return; } @@ -142,10 +157,14 @@ void AudioRecorder::stopRecording() { ma_device_uninit(m_device); m_deviceStarted = false; } - m_recording = false; + + if (m_compressor && m_compressorEnabled) m_compressor->flush(); + int duration = m_elapsedMs; - qDebug("AudioRecorder: recording stopped, duration=%dms, pcmSamples=%zu", duration, m_pcmBuffer.size()); + size_t bufSize = (m_compressor && m_compressorEnabled) ? m_compressor->outputBuffer().size() : m_pcmBuffer.size(); + qDebug("AudioRecorder: recording stopped, duration=%dms, pcmSamples=%zu, compressor=%d", + duration, bufSize, m_compressorEnabled ? 1 : 0); emit recordingStopped(duration); emit durationChanged(0); } @@ -159,3 +178,14 @@ float AudioRecorder::peakLevel() const { m_peakLevel = 0.0f; return v; } + +const std::vector& AudioRecorder::buffer() const { + if (m_compressor && m_compressorEnabled) return m_compressor->outputBuffer(); + return m_pcmBuffer; +} + +void AudioRecorder::setCompressorEnabled(bool enabled) { + m_compressorEnabled = enabled; + if (m_compressor) m_compressor->setEnabled(enabled); + qDebug("AudioRecorder: compressor %s", enabled ? "enabled" : "disabled"); +} diff --git a/tools/chatgui/src/audiorecorder.h b/tools/chatgui/src/audiorecorder.h index e2b56466..2901c9e4 100644 --- a/tools/chatgui/src/audiorecorder.h +++ b/tools/chatgui/src/audiorecorder.h @@ -7,6 +7,7 @@ #include struct ma_device; +class AudioCompressor; class AudioRecorder : public QObject { Q_OBJECT @@ -27,10 +28,14 @@ public: float peakLevel() const; - const std::vector& buffer() const { return m_pcmBuffer; } + const std::vector& buffer() const; const std::vector& waveformLevels() const { return m_waveformLevels; } int durationMs() const; + AudioCompressor* compressor() const { return m_compressor; } + void setCompressorEnabled(bool enabled); + bool isCompressorEnabled() const { return m_compressorEnabled; } + signals: void recordingStarted(); void recordingStopped(int durationMs); @@ -53,5 +58,8 @@ private: QTimer m_durationTimer; int m_elapsedMs = 0; + AudioCompressor* m_compressor = nullptr; + bool m_compressorEnabled = false; + friend void audioCaptureCallback(ma_device* pDevice, void* pOutput, const void* pInput, unsigned int frameCount); }; diff --git a/tools/chatgui/src/mainwindow.cpp b/tools/chatgui/src/mainwindow.cpp index 133b0bd7..2906118e 100644 --- a/tools/chatgui/src/mainwindow.cpp +++ b/tools/chatgui/src/mainwindow.cpp @@ -9,6 +9,7 @@ #include "invite_link.h" #include "sound_manager.h" #include "audiorecorder.h" +#include "audiocompressor.h" #include "voicemessageencoder.h" #include "animtimer.h" #include "../db/db_manager.h" @@ -25,6 +26,7 @@ #include #include #include +#include extern "C" { #include "../transport/chat_core.h" @@ -207,9 +209,22 @@ void MainWindow::setupSoundFromConfig() { if (m_db && m_db->isOpen()) { m_recorder->setCaptureDeviceIndex(m_db->getUiStateInt("audio_input_device", -1)); VoiceEncoder::setPreset(m_db->getUiStateInt("opus_codec_preset", 1)); + + int compEnabled = m_db->getUiStateInt("compressor_enabled", 0); + m_recorder->setCompressorEnabled(compEnabled != 0); + if (m_recorder->compressor()) { + AudioCompressor::Config compCfg; + compCfg.maxGainDb = (float)m_db->getUiStateInt("compressor_max_gain_db", 30); + compCfg.lookbackMs = m_db->getUiStateInt("compressor_lookback_ms", 200); + compCfg.lookaheadMs = m_db->getUiStateInt("compressor_lookahead_ms", 100); + compCfg.riseRatePer500ms = (float)m_db->getUiStateInt("compressor_rise_rate_tenths", 20) / 10.0f; + compCfg.targetLevel = powf(10.0f, (float)m_db->getUiStateInt("compressor_target_level_db", -12) / 20.0f); + m_recorder->compressor()->configure(compCfg); + } } - qDebug("MainWindow: AudioRecorder initialized (captureDevice=%d, opusPreset=%d)", - m_recorder->captureDeviceIndex(), VoiceEncoder::preset()); + qDebug("MainWindow: AudioRecorder initialized (captureDevice=%d, opusPreset=%d, compressor=%d)", + m_recorder->captureDeviceIndex(), VoiceEncoder::preset(), + m_recorder->isCompressorEnabled() ? 1 : 0); } }