8 changed files with 406 additions and 9 deletions
@ -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(); |
||||
}; |
||||
Loading…
Reference in new issue