feat: voice compressor with RMS+peak leveling, sliding window envelope, rate-limited gain
This commit is contained in:
1 parent
7004b110ae
commit
f1b599f0c0
8 files changed
+406
-9
No files matched your search
@@ -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
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
#include "audiocompressor.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
#include <cstdint>
|
||||
|
||||
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<int16_t>& 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<BlockLevel> m_blockLevels;
|
||||
|
||||
struct PendingBlock {
|
||||
std::vector<int16_t> samples;
|
||||
size_t levelIndex;
|
||||
};
|
||||
std::deque<PendingBlock> m_pending;
|
||||
std::vector<int16_t> m_accumulator;
|
||||
|
||||
std::vector<int16_t> 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();
|
||||
};
|
||||
@@ -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");
|
||||
}
|
||||
@@ -3,9 +3,13 @@
|
||||
#include <QComboBox>
|
||||
#include <QProgressBar>
|
||||
#include <QPushButton>
|
||||
#include <QCheckBox>
|
||||
#include <QSlider>
|
||||
#include <QLabel>
|
||||
#include <QTimer>
|
||||
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;
|
||||
};
|
||||
@@ -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<int16_t>& 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");
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <cstdint>
|
||||
|
||||
struct ma_device;
|
||||
class AudioCompressor;
|
||||
|
||||
class AudioRecorder : public QObject {
|
||||
Q_OBJECT
|
||||
@@ -27,10 +28,14 @@ public:
|
||||
|
||||
float peakLevel() const;
|
||||
|
||||
const std::vector<int16_t>& buffer() const { return m_pcmBuffer; }
|
||||
const std::vector<int16_t>& buffer() const;
|
||||
const std::vector<float>& 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);
|
||||
};
|
||||
@@ -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 <QFile>
|
||||
#include <QTextStream>
|
||||
#include <QDir>
|
||||
#include <cmath>
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user