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