aboutsummaryrefslogtreecommitdiffstats
path: root/core
diff options
context:
space:
mode:
Diffstat (limited to 'core')
-rw-r--r--core/mastering.cpp441
-rw-r--r--core/mastering.h104
2 files changed, 545 insertions, 0 deletions
diff --git a/core/mastering.cpp b/core/mastering.cpp
new file mode 100644
index 00000000..e0cb2ca7
--- /dev/null
+++ b/core/mastering.cpp
@@ -0,0 +1,441 @@
+
+#include "config.h"
+
+#include "mastering.h"
+
+#include <algorithm>
+#include <cmath>
+#include <cstddef>
+#include <functional>
+#include <iterator>
+#include <limits>
+#include <new>
+
+#include "almalloc.h"
+#include "alnumeric.h"
+#include "alspan.h"
+#include "opthelpers.h"
+
+
+/* These structures assume BufferLineSize is a power of 2. */
+static_assert((BufferLineSize & (BufferLineSize-1)) == 0, "BufferLineSize is not a power of 2");
+
+struct SlidingHold {
+ alignas(16) float mValues[BufferLineSize];
+ uint mExpiries[BufferLineSize];
+ uint mLowerIndex;
+ uint mUpperIndex;
+ uint mLength;
+};
+
+
+namespace {
+
+using namespace std::placeholders;
+
+/* This sliding hold follows the input level with an instant attack and a
+ * fixed duration hold before an instant release to the next highest level.
+ * It is a sliding window maximum (descending maxima) implementation based on
+ * Richard Harter's ascending minima algorithm available at:
+ *
+ * http://www.richardhartersworld.com/cri/2001/slidingmin.html
+ */
+float UpdateSlidingHold(SlidingHold *Hold, const uint i, const float in)
+{
+ static constexpr uint mask{BufferLineSize - 1};
+ const uint length{Hold->mLength};
+ float (&values)[BufferLineSize] = Hold->mValues;
+ uint (&expiries)[BufferLineSize] = Hold->mExpiries;
+ uint lowerIndex{Hold->mLowerIndex};
+ uint upperIndex{Hold->mUpperIndex};
+
+ if(i >= expiries[upperIndex])
+ upperIndex = (upperIndex + 1) & mask;
+
+ if(in >= values[upperIndex])
+ {
+ values[upperIndex] = in;
+ expiries[upperIndex] = i + length;
+ lowerIndex = upperIndex;
+ }
+ else
+ {
+ do {
+ do {
+ if(!(in >= values[lowerIndex]))
+ goto found_place;
+ } while(lowerIndex--);
+ lowerIndex = mask;
+ } while(1);
+ found_place:
+
+ lowerIndex = (lowerIndex + 1) & mask;
+ values[lowerIndex] = in;
+ expiries[lowerIndex] = i + length;
+ }
+
+ Hold->mLowerIndex = lowerIndex;
+ Hold->mUpperIndex = upperIndex;
+
+ return values[upperIndex];
+}
+
+void ShiftSlidingHold(SlidingHold *Hold, const uint n)
+{
+ auto exp_begin = std::begin(Hold->mExpiries) + Hold->mUpperIndex;
+ auto exp_last = std::begin(Hold->mExpiries) + Hold->mLowerIndex;
+ if(exp_last-exp_begin < 0)
+ {
+ std::transform(exp_begin, std::end(Hold->mExpiries), exp_begin,
+ std::bind(std::minus<>{}, _1, n));
+ exp_begin = std::begin(Hold->mExpiries);
+ }
+ std::transform(exp_begin, exp_last+1, exp_begin, std::bind(std::minus<>{}, _1, n));
+}
+
+
+/* Multichannel compression is linked via the absolute maximum of all
+ * channels.
+ */
+void LinkChannels(Compressor *Comp, const uint SamplesToDo, const FloatBufferLine *OutBuffer)
+{
+ const size_t numChans{Comp->mNumChans};
+
+ ASSUME(SamplesToDo > 0);
+ ASSUME(numChans > 0);
+
+ auto side_begin = std::begin(Comp->mSideChain) + Comp->mLookAhead;
+ std::fill(side_begin, side_begin+SamplesToDo, 0.0f);
+
+ auto fill_max = [SamplesToDo,side_begin](const FloatBufferLine &input) -> void
+ {
+ const float *RESTRICT buffer{al::assume_aligned<16>(input.data())};
+ auto max_abs = std::bind(maxf, _1, std::bind(static_cast<float(&)(float)>(std::fabs), _2));
+ std::transform(side_begin, side_begin+SamplesToDo, buffer, side_begin, max_abs);
+ };
+ std::for_each(OutBuffer, OutBuffer+numChans, fill_max);
+}
+
+/* This calculates the squared crest factor of the control signal for the
+ * basic automation of the attack/release times. As suggested by the paper,
+ * it uses an instantaneous squared peak detector and a squared RMS detector
+ * both with 200ms release times.
+ */
+static void CrestDetector(Compressor *Comp, const uint SamplesToDo)
+{
+ const float a_crest{Comp->mCrestCoeff};
+ float y2_peak{Comp->mLastPeakSq};
+ float y2_rms{Comp->mLastRmsSq};
+
+ ASSUME(SamplesToDo > 0);
+
+ auto calc_crest = [&y2_rms,&y2_peak,a_crest](const float x_abs) noexcept -> float
+ {
+ const float x2{clampf(x_abs * x_abs, 0.000001f, 1000000.0f)};
+
+ y2_peak = maxf(x2, lerp(x2, y2_peak, a_crest));
+ y2_rms = lerp(x2, y2_rms, a_crest);
+ return y2_peak / y2_rms;
+ };
+ auto side_begin = std::begin(Comp->mSideChain) + Comp->mLookAhead;
+ std::transform(side_begin, side_begin+SamplesToDo, std::begin(Comp->mCrestFactor), calc_crest);
+
+ Comp->mLastPeakSq = y2_peak;
+ Comp->mLastRmsSq = y2_rms;
+}
+
+/* The side-chain starts with a simple peak detector (based on the absolute
+ * value of the incoming signal) and performs most of its operations in the
+ * log domain.
+ */
+void PeakDetector(Compressor *Comp, const uint SamplesToDo)
+{
+ ASSUME(SamplesToDo > 0);
+
+ /* Clamp the minimum amplitude to near-zero and convert to logarithm. */
+ auto side_begin = std::begin(Comp->mSideChain) + Comp->mLookAhead;
+ std::transform(side_begin, side_begin+SamplesToDo, side_begin,
+ [](const float s) -> float { return std::log(maxf(0.000001f, s)); });
+}
+
+/* An optional hold can be used to extend the peak detector so it can more
+ * solidly detect fast transients. This is best used when operating as a
+ * limiter.
+ */
+void PeakHoldDetector(Compressor *Comp, const uint SamplesToDo)
+{
+ ASSUME(SamplesToDo > 0);
+
+ SlidingHold *hold{Comp->mHold};
+ uint i{0};
+ auto detect_peak = [&i,hold](const float x_abs) -> float
+ {
+ const float x_G{std::log(maxf(0.000001f, x_abs))};
+ return UpdateSlidingHold(hold, i++, x_G);
+ };
+ auto side_begin = std::begin(Comp->mSideChain) + Comp->mLookAhead;
+ std::transform(side_begin, side_begin+SamplesToDo, side_begin, detect_peak);
+
+ ShiftSlidingHold(hold, SamplesToDo);
+}
+
+/* This is the heart of the feed-forward compressor. It operates in the log
+ * domain (to better match human hearing) and can apply some basic automation
+ * to knee width, attack/release times, make-up/post gain, and clipping
+ * reduction.
+ */
+void GainCompressor(Compressor *Comp, const uint SamplesToDo)
+{
+ const bool autoKnee{Comp->mAuto.Knee};
+ const bool autoAttack{Comp->mAuto.Attack};
+ const bool autoRelease{Comp->mAuto.Release};
+ const bool autoPostGain{Comp->mAuto.PostGain};
+ const bool autoDeclip{Comp->mAuto.Declip};
+ const uint lookAhead{Comp->mLookAhead};
+ const float threshold{Comp->mThreshold};
+ const float slope{Comp->mSlope};
+ const float attack{Comp->mAttack};
+ const float release{Comp->mRelease};
+ const float c_est{Comp->mGainEstimate};
+ const float a_adp{Comp->mAdaptCoeff};
+ const float *crestFactor{Comp->mCrestFactor};
+ float postGain{Comp->mPostGain};
+ float knee{Comp->mKnee};
+ float t_att{attack};
+ float t_rel{release - attack};
+ float a_att{std::exp(-1.0f / t_att)};
+ float a_rel{std::exp(-1.0f / t_rel)};
+ float y_1{Comp->mLastRelease};
+ float y_L{Comp->mLastAttack};
+ float c_dev{Comp->mLastGainDev};
+
+ ASSUME(SamplesToDo > 0);
+
+ for(float &sideChain : al::span<float>{Comp->mSideChain, SamplesToDo})
+ {
+ if(autoKnee)
+ knee = maxf(0.0f, 2.5f * (c_dev + c_est));
+ const float knee_h{0.5f * knee};
+
+ /* This is the gain computer. It applies a static compression curve
+ * to the control signal.
+ */
+ const float x_over{std::addressof(sideChain)[lookAhead] - threshold};
+ const float y_G{
+ (x_over <= -knee_h) ? 0.0f :
+ (std::fabs(x_over) < knee_h) ? (x_over + knee_h) * (x_over + knee_h) / (2.0f * knee) :
+ x_over};
+
+ const float y2_crest{*(crestFactor++)};
+ if(autoAttack)
+ {
+ t_att = 2.0f*attack/y2_crest;
+ a_att = std::exp(-1.0f / t_att);
+ }
+ if(autoRelease)
+ {
+ t_rel = 2.0f*release/y2_crest - t_att;
+ a_rel = std::exp(-1.0f / t_rel);
+ }
+
+ /* Gain smoothing (ballistics) is done via a smooth decoupled peak
+ * detector. The attack time is subtracted from the release time
+ * above to compensate for the chained operating mode.
+ */
+ const float x_L{-slope * y_G};
+ y_1 = maxf(x_L, lerp(x_L, y_1, a_rel));
+ y_L = lerp(y_1, y_L, a_att);
+
+ /* Knee width and make-up gain automation make use of a smoothed
+ * measurement of deviation between the control signal and estimate.
+ * The estimate is also used to bias the measurement to hot-start its
+ * average.
+ */
+ c_dev = lerp(-(y_L+c_est), c_dev, a_adp);
+
+ if(autoPostGain)
+ {
+ /* Clipping reduction is only viable when make-up gain is being
+ * automated. It modifies the deviation to further attenuate the
+ * control signal when clipping is detected. The adaptation time
+ * is sufficiently long enough to suppress further clipping at the
+ * same output level.
+ */
+ if(autoDeclip)
+ c_dev = maxf(c_dev, sideChain - y_L - threshold - c_est);
+
+ postGain = -(c_dev + c_est);
+ }
+
+ sideChain = std::exp(postGain - y_L);
+ }
+
+ Comp->mLastRelease = y_1;
+ Comp->mLastAttack = y_L;
+ Comp->mLastGainDev = c_dev;
+}
+
+/* Combined with the hold time, a look-ahead delay can improve handling of
+ * fast transients by allowing the envelope time to converge prior to
+ * reaching the offending impulse. This is best used when operating as a
+ * limiter.
+ */
+void SignalDelay(Compressor *Comp, const uint SamplesToDo, FloatBufferLine *OutBuffer)
+{
+ const size_t numChans{Comp->mNumChans};
+ const uint lookAhead{Comp->mLookAhead};
+
+ ASSUME(SamplesToDo > 0);
+ ASSUME(numChans > 0);
+ ASSUME(lookAhead > 0);
+
+ for(size_t c{0};c < numChans;c++)
+ {
+ float *inout{al::assume_aligned<16>(OutBuffer[c].data())};
+ float *delaybuf{al::assume_aligned<16>(Comp->mDelay[c].data())};
+
+ auto inout_end = inout + SamplesToDo;
+ if LIKELY(SamplesToDo >= lookAhead)
+ {
+ auto delay_end = std::rotate(inout, inout_end - lookAhead, inout_end);
+ std::swap_ranges(inout, delay_end, delaybuf);
+ }
+ else
+ {
+ auto delay_start = std::swap_ranges(inout, inout_end, delaybuf);
+ std::rotate(delaybuf, delay_start, delaybuf + lookAhead);
+ }
+ }
+}
+
+} // namespace
+
+
+std::unique_ptr<Compressor> Compressor::Create(const size_t NumChans, const float SampleRate,
+ const bool AutoKnee, const bool AutoAttack, const bool AutoRelease, const bool AutoPostGain,
+ const bool AutoDeclip, const float LookAheadTime, const float HoldTime, const float PreGainDb,
+ const float PostGainDb, const float ThresholdDb, const float Ratio, const float KneeDb,
+ const float AttackTime, const float ReleaseTime)
+{
+ const auto lookAhead = static_cast<uint>(
+ clampf(std::round(LookAheadTime*SampleRate), 0.0f, BufferLineSize-1));
+ const auto hold = static_cast<uint>(
+ clampf(std::round(HoldTime*SampleRate), 0.0f, BufferLineSize-1));
+
+ size_t size{sizeof(Compressor)};
+ if(lookAhead > 0)
+ {
+ size += sizeof(*Compressor::mDelay) * NumChans;
+ /* The sliding hold implementation doesn't handle a length of 1. A 1-
+ * sample hold is useless anyway, it would only ever give back what was
+ * just given to it.
+ */
+ if(hold > 1)
+ size += sizeof(*Compressor::mHold);
+ }
+
+ auto Comp = std::unique_ptr<Compressor>{new (al_calloc(16, size)) Compressor{}};
+ Comp->mNumChans = NumChans;
+ Comp->mAuto.Knee = AutoKnee;
+ Comp->mAuto.Attack = AutoAttack;
+ Comp->mAuto.Release = AutoRelease;
+ Comp->mAuto.PostGain = AutoPostGain;
+ Comp->mAuto.Declip = AutoPostGain && AutoDeclip;
+ Comp->mLookAhead = lookAhead;
+ Comp->mPreGain = std::pow(10.0f, PreGainDb / 20.0f);
+ Comp->mPostGain = PostGainDb * std::log(10.0f) / 20.0f;
+ Comp->mThreshold = ThresholdDb * std::log(10.0f) / 20.0f;
+ Comp->mSlope = 1.0f / maxf(1.0f, Ratio) - 1.0f;
+ Comp->mKnee = maxf(0.0f, KneeDb * std::log(10.0f) / 20.0f);
+ Comp->mAttack = maxf(1.0f, AttackTime * SampleRate);
+ Comp->mRelease = maxf(1.0f, ReleaseTime * SampleRate);
+
+ /* Knee width automation actually treats the compressor as a limiter. By
+ * varying the knee width, it can effectively be seen as applying
+ * compression over a wide range of ratios.
+ */
+ if(AutoKnee)
+ Comp->mSlope = -1.0f;
+
+ if(lookAhead > 0)
+ {
+ if(hold > 1)
+ {
+ Comp->mHold = ::new (static_cast<void*>(Comp.get() + 1)) SlidingHold{};
+ Comp->mHold->mValues[0] = -std::numeric_limits<float>::infinity();
+ Comp->mHold->mExpiries[0] = hold;
+ Comp->mHold->mLength = hold;
+ Comp->mDelay = ::new(static_cast<void*>(Comp->mHold + 1)) FloatBufferLine[NumChans];
+ }
+ else
+ {
+ Comp->mDelay = ::new(static_cast<void*>(Comp.get() + 1)) FloatBufferLine[NumChans];
+ }
+ std::fill_n(Comp->mDelay, NumChans, FloatBufferLine{});
+ }
+
+ Comp->mCrestCoeff = std::exp(-1.0f / (0.200f * SampleRate)); // 200ms
+ Comp->mGainEstimate = Comp->mThreshold * -0.5f * Comp->mSlope;
+ Comp->mAdaptCoeff = std::exp(-1.0f / (2.0f * SampleRate)); // 2s
+
+ return Comp;
+}
+
+Compressor::~Compressor()
+{
+ if(mHold)
+ al::destroy_at(mHold);
+ mHold = nullptr;
+ if(mDelay)
+ al::destroy_n(mDelay, mNumChans);
+ mDelay = nullptr;
+}
+
+
+void Compressor::process(const uint SamplesToDo, FloatBufferLine *OutBuffer)
+{
+ const size_t numChans{mNumChans};
+
+ ASSUME(SamplesToDo > 0);
+ ASSUME(numChans > 0);
+
+ const float preGain{mPreGain};
+ if(preGain != 1.0f)
+ {
+ auto apply_gain = [SamplesToDo,preGain](FloatBufferLine &input) noexcept -> void
+ {
+ float *buffer{al::assume_aligned<16>(input.data())};
+ std::transform(buffer, buffer+SamplesToDo, buffer,
+ std::bind(std::multiplies<float>{}, _1, preGain));
+ };
+ std::for_each(OutBuffer, OutBuffer+numChans, apply_gain);
+ }
+
+ LinkChannels(this, SamplesToDo, OutBuffer);
+
+ if(mAuto.Attack || mAuto.Release)
+ CrestDetector(this, SamplesToDo);
+
+ if(mHold)
+ PeakHoldDetector(this, SamplesToDo);
+ else
+ PeakDetector(this, SamplesToDo);
+
+ GainCompressor(this, SamplesToDo);
+
+ if(mDelay)
+ SignalDelay(this, SamplesToDo, OutBuffer);
+
+ const float (&sideChain)[BufferLineSize*2] = mSideChain;
+ auto apply_comp = [SamplesToDo,&sideChain](FloatBufferLine &input) noexcept -> void
+ {
+ float *buffer{al::assume_aligned<16>(input.data())};
+ const float *gains{al::assume_aligned<16>(&sideChain[0])};
+ std::transform(gains, gains+SamplesToDo, buffer, buffer,
+ std::bind(std::multiplies<float>{}, _1, _2));
+ };
+ std::for_each(OutBuffer, OutBuffer+numChans, apply_comp);
+
+ auto side_begin = std::begin(mSideChain) + SamplesToDo;
+ std::copy(side_begin, side_begin+mLookAhead, std::begin(mSideChain));
+}
diff --git a/core/mastering.h b/core/mastering.h
new file mode 100644
index 00000000..322d3654
--- /dev/null
+++ b/core/mastering.h
@@ -0,0 +1,104 @@
+#ifndef CORE_MASTERING_H
+#define CORE_MASTERING_H
+
+#include <memory>
+
+#include "almalloc.h"
+#include "bufferline.h"
+
+struct SlidingHold;
+
+using uint = unsigned int;
+
+
+/* General topology and basic automation was based on the following paper:
+ *
+ * D. Giannoulis, M. Massberg and J. D. Reiss,
+ * "Parameter Automation in a Dynamic Range Compressor,"
+ * Journal of the Audio Engineering Society, v61 (10), Oct. 2013
+ *
+ * Available (along with supplemental reading) at:
+ *
+ * http://c4dm.eecs.qmul.ac.uk/audioengineering/compressors/
+ */
+struct Compressor {
+ size_t mNumChans{0u};
+
+ struct {
+ bool Knee : 1;
+ bool Attack : 1;
+ bool Release : 1;
+ bool PostGain : 1;
+ bool Declip : 1;
+ } mAuto{};
+
+ uint mLookAhead{0};
+
+ float mPreGain{0.0f};
+ float mPostGain{0.0f};
+
+ float mThreshold{0.0f};
+ float mSlope{0.0f};
+ float mKnee{0.0f};
+
+ float mAttack{0.0f};
+ float mRelease{0.0f};
+
+ alignas(16) float mSideChain[2*BufferLineSize]{};
+ alignas(16) float mCrestFactor[BufferLineSize]{};
+
+ SlidingHold *mHold{nullptr};
+ FloatBufferLine *mDelay{nullptr};
+
+ float mCrestCoeff{0.0f};
+ float mGainEstimate{0.0f};
+ float mAdaptCoeff{0.0f};
+
+ float mLastPeakSq{0.0f};
+ float mLastRmsSq{0.0f};
+ float mLastRelease{0.0f};
+ float mLastAttack{0.0f};
+ float mLastGainDev{0.0f};
+
+
+ ~Compressor();
+ void process(const uint SamplesToDo, FloatBufferLine *OutBuffer);
+ int getLookAhead() const noexcept { return static_cast<int>(mLookAhead); }
+
+ DEF_PLACE_NEWDEL()
+
+ /**
+ * The compressor is initialized with the following settings:
+ *
+ * \param NumChans Number of channels to process.
+ * \param SampleRate Sample rate to process.
+ * \param AutoKnee Whether to automate the knee width parameter.
+ * \param AutoAttack Whether to automate the attack time parameter.
+ * \param AutoRelease Whether to automate the release time parameter.
+ * \param AutoPostGain Whether to automate the make-up (post) gain
+ * parameter.
+ * \param AutoDeclip Whether to automate clipping reduction. Ignored
+ * when not automating make-up gain.
+ * \param LookAheadTime Look-ahead time (in seconds).
+ * \param HoldTime Peak hold-time (in seconds).
+ * \param PreGainDb Gain applied before detection (in dB).
+ * \param PostGainDb Make-up gain applied after compression (in dB).
+ * \param ThresholdDb Triggering threshold (in dB).
+ * \param Ratio Compression ratio (x:1). Set to INFINIFTY for true
+ * limiting. Ignored when automating knee width.
+ * \param KneeDb Knee width (in dB). Ignored when automating knee
+ * width.
+ * \param AttackTime Attack time (in seconds). Acts as a maximum when
+ * automating attack time.
+ * \param ReleaseTime Release time (in seconds). Acts as a maximum when
+ * automating release time.
+ */
+ static std::unique_ptr<Compressor> Create(const size_t NumChans, const float SampleRate,
+ const bool AutoKnee, const bool AutoAttack, const bool AutoRelease,
+ const bool AutoPostGain, const bool AutoDeclip, const float LookAheadTime,
+ const float HoldTime, const float PreGainDb, const float PostGainDb,
+ const float ThresholdDb, const float Ratio, const float KneeDb, const float AttackTime,
+ const float ReleaseTime);
+};
+
+#endif /* CORE_MASTERING_H */