From d38817a7a12efac0153bf1746d3c3beea3d29d5d Mon Sep 17 00:00:00 2001 From: "Steinar H. Gunderson" Date: Sat, 7 Nov 2015 00:25:43 +0100 Subject: [PATCH] Add a compressor (kindly relicensed by Rune Holm). Fixed for now, and only to get the overall levels right. --- Makefile | 2 +- README | 1 + mixer.cpp | 30 +++++++++++++++++++- mixer.h | 3 ++ stereocompressor.cpp | 67 ++++++++++++++++++++++++++++++++++++++++++++ stereocompressor.h | 45 +++++++++++++++++++++++++++++ 6 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 stereocompressor.cpp create mode 100644 stereocompressor.h diff --git a/Makefile b/Makefile index 0b88d60..422fe0a 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ OBJS=glwidget.o main.o mainwindow.o vumeter.o lrameter.o vu_common.o OBJS += glwidget.moc.o mainwindow.moc.o vumeter.moc.o lrameter.moc.o # Mixer objects -OBJS += h264encode.o mixer.o bmusb/bmusb.o pbo_frame_allocator.o context.o ref_counted_frame.o theme.o resampler.o httpd.o ebu_r128_proc.o flags.o image_input.o +OBJS += h264encode.o mixer.o bmusb/bmusb.o pbo_frame_allocator.o context.o ref_counted_frame.o theme.o resampler.o httpd.o ebu_r128_proc.o flags.o image_input.o stereocompressor.o %.o: %.cpp $(CXX) -MMD -MP $(CPPFLAGS) $(CXXFLAGS) -o $@ -c $< diff --git a/README b/README index beee722..6c880dc 100644 --- a/README +++ b/README @@ -80,6 +80,7 @@ Intel's copyright license at h264encode.h. Nageru is Copyright (C) 2015 Steinar H. Gunderson . +Portions Copyright (C) 2003 Rune Holm. Portions Copyright (C) 2010-2011 Fons Adriaensen . Portions Copyright (C) 2012-2015 Fons Adriaensen . Portions Copyright (c) 2007-2013 Intel Corporation. All Rights Reserved. diff --git a/mixer.cpp b/mixer.cpp index c0e8de4..43e39b6 100644 --- a/mixer.cpp +++ b/mixer.cpp @@ -68,7 +68,8 @@ Mixer::Mixer(const QSurfaceFormat &format, unsigned num_cards) : httpd("test.ts", WIDTH, HEIGHT), num_cards(num_cards), mixer_surface(create_surface(format)), - h264_encoder_surface(create_surface(format)) + h264_encoder_surface(create_surface(format)), + compressor(48000.0f) { httpd.start(9095); @@ -536,6 +537,33 @@ void Mixer::process_audio_one_frame() } } + // Apply a level compressor to get the general level right. + // Basically, if it's over about -40 dBFS, we squeeze it down to that level + // (or more precisely, near it, since we don't use infinite ratio), + // then apply a makeup gain to get it to -12 dBFS. -12 dBFS is, of course, + // entirely arbitrary, but from practical tests with speech, it seems to + // put ut around -23 LUFS, so it's a reasonable starting point for later use. + // + // TODO: Hook this up to a UI, so we can see the effects, and/or turn it off + // to control the gain manually instead. For now, there's only the #if-ed out + // code below. + // + // TODO: Add the actual compressors/limiters (for taking care of transients) + // later in the chain. + float threshold = 0.01f; // -40 dBFS. + float ratio = 20.0f; + float attack_time = 0.1f; + float release_time = 10.0f; + float makeup_gain = pow(10.0f, 28.0f / 20.0f); // +28 dB takes us to -12 dBFS. + compressor.process(samples_out.data(), samples_out.size() / 2, threshold, ratio, attack_time, release_time, makeup_gain); + +#if 0 + printf("level=%f (%+5.2f dBFS) attenuation=%f (%+5.2f dB) end_result=%+5.2f dB\n", + compressor.get_level(), 20.0 * log10(compressor.get_level()), + compressor.get_attenuation(), 20.0 * log10(compressor.get_attenuation()), + 20.0 * log10(compressor.get_level() * compressor.get_attenuation() * makeup_gain)); +#endif + // Find peak and R128 levels. peak = std::max(peak, find_peak(samples_out)); vector left, right; diff --git a/mixer.h b/mixer.h index 33f3a98..8ac322f 100644 --- a/mixer.h +++ b/mixer.h @@ -29,6 +29,7 @@ #include "resampler.h" #include "theme.h" #include "timebase.h" +#include "stereocompressor.h" class H264Encoder; class QSurface; @@ -203,6 +204,8 @@ private: // TODO: Implement oversampled peak detection. float peak = 0.0f; + + StereoCompressor compressor; }; extern Mixer *global_mixer; diff --git a/stereocompressor.cpp b/stereocompressor.cpp new file mode 100644 index 0000000..0d87aab --- /dev/null +++ b/stereocompressor.cpp @@ -0,0 +1,67 @@ +#include +#include +#include + +#include "stereocompressor.h" + +namespace { + +inline float compressor_knee(float x, float threshold, float inv_threshold, float inv_ratio_minus_one, float postgain) +{ + assert(inv_ratio_minus_one <= 0.0f); + if (x > threshold && inv_ratio_minus_one < 0.0f) { + return postgain * pow(x * inv_threshold, inv_ratio_minus_one); + } else { + return postgain; + } +} + +} // namespace + +void StereoCompressor::process(float *buf, size_t num_samples, float threshold, float ratio, + float attack_time, float release_time, float makeup_gain) +{ + float attack_increment = float(pow(2.0f, 1.0f / (attack_time * sample_rate + 1))); + if (attack_time == 0.0f) attack_increment = 100000; // For instant attack reaction. + + const float release_increment = float(pow(2.0f, -1.0f / (release_time * sample_rate + 1))); + const float peak_increment = float(pow(2.0f, -1.0f / (0.003f * sample_rate + 1))); + + float inv_ratio_minus_one = 1.0f / ratio - 1.0f; + if (ratio > 63) inv_ratio_minus_one = -1.0f; // Infinite ratio. + float inv_threshold = 1.0f / threshold; + + float *left_ptr = buf; + float *right_ptr = buf + 1; + + float peak_level = this->peak_level; + float compr_level = this->compr_level; + + for (size_t i = 0; i < num_samples; ++i) { + if (fabs(*left_ptr) > peak_level) peak_level = float(fabs(*left_ptr)); + if (fabs(*right_ptr) > peak_level) peak_level = float(fabs(*right_ptr)); + + if (peak_level > compr_level) { + compr_level = std::min(compr_level * attack_increment, peak_level); + } else { + compr_level = std::max(compr_level * release_increment, 0.0001f); + } + + float scalefactor_with_gain = compressor_knee(compr_level, threshold, inv_threshold, inv_ratio_minus_one, makeup_gain); + + *left_ptr *= scalefactor_with_gain; + left_ptr += 2; + + *right_ptr *= scalefactor_with_gain; + right_ptr += 2; + + peak_level = std::max(peak_level * peak_increment, 0.0001f); + } + + // Store attenuation level for debug/visualization. + scalefactor = compressor_knee(compr_level, threshold, inv_threshold, inv_ratio_minus_one, 1.0f); + + this->peak_level = peak_level; + this->compr_level = compr_level; +} + diff --git a/stereocompressor.h b/stereocompressor.h new file mode 100644 index 0000000..239f284 --- /dev/null +++ b/stereocompressor.h @@ -0,0 +1,45 @@ +#ifndef _STEREOCOMPRESSOR_H +#define _STEREOCOMPRESSOR_H 1 + +// A simple compressor based on absolute values, with independent +// attack/release times. There is no sidechain or lookahead, but the +// peak value is shared between both channels. +// +// The compressor was originally written by, and is copyrighted by, Rune Holm. +// It has been adapted and relicensed under GPLv3 (or, at your option, +// any later version) for Nageru, so that its license matches the rest of the code. + +class StereoBuffer; + +class StereoCompressor { +public: + StereoCompressor(float sample_rate) + : sample_rate(sample_rate) { + reset(); + } + + void reset() { + peak_level = compr_level = 0.1f; + scalefactor = 0.0f; + } + + // Process interleaved stereo data in-place. + // Attack and release times are in seconds. + void process(float *buf, size_t num_samples, float threshold, float ratio, + float attack_time, float release_time, float makeup_gain); + + // Last level estimated (after attack/decay applied). + float get_level() { return compr_level; } + + // Last attenuation factor applied, e.g. if 5x compression is currently applied, + // this number will be 0.2. + float get_attenuation() { return scalefactor; } + +private: + float sample_rate; + float peak_level; + float compr_level; + float scalefactor; +}; + +#endif /* !defined(_STEREOCOMPRESSOR_H) */ -- 2.39.2