]> git.sesse.net Git - nageru/commitdiff
Add a compressor (kindly relicensed by Rune Holm). Fixed for now, and only to get...
authorSteinar H. Gunderson <sgunderson@bigfoot.com>
Fri, 6 Nov 2015 23:25:43 +0000 (00:25 +0100)
committerSteinar H. Gunderson <sgunderson@bigfoot.com>
Fri, 6 Nov 2015 23:26:29 +0000 (00:26 +0100)
Makefile
README
mixer.cpp
mixer.h
stereocompressor.cpp [new file with mode: 0644]
stereocompressor.h [new file with mode: 0644]

index 0b88d60bc7add19b045fae225467b963ecb42031..422fe0a7219368bac28424c0a07397a50828efae 100644 (file)
--- 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 beee722608e55ebc221953ce9e89597c310ed2ae..6c880dc096d59b97b8cc0ff8d0110bf09e7f1aca 100644 (file)
--- a/README
+++ b/README
@@ -80,6 +80,7 @@ Intel's copyright license at h264encode.h.
 
 
 Nageru is Copyright (C) 2015 Steinar H. Gunderson <sgunderson@bigfoot.com>.
+Portions Copyright (C) 2003 Rune Holm.
 Portions Copyright (C) 2010-2011 Fons Adriaensen <fons@linuxaudio.org>.
 Portions Copyright (C) 2012-2015 Fons Adriaensen <fons@linuxaudio.org>.
 Portions Copyright (c) 2007-2013 Intel Corporation. All Rights Reserved.
index c0e8de46d18c558e6469a50cf8a10b9d085967de..43e39b6c18e8518b8b0ecee7df91ebf0d2a0b269 100644 (file)
--- 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<float> left, right;
diff --git a/mixer.h b/mixer.h
index 33f3a982cbb0519241224bbe67aa605d3b373f56..8ac322fd921f66e902d02fc76202e1de957674a9 100644 (file)
--- 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 (file)
index 0000000..0d87aab
--- /dev/null
@@ -0,0 +1,67 @@
+#include <math.h>
+#include <assert.h>
+#include <algorithm>
+
+#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 (file)
index 0000000..239f284
--- /dev/null
@@ -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 <num_samples> 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) */