15 #include "alnumeric.h"
17 #include "opthelpers.h"
20 /* These structures assume BufferLineSize is a power of 2. */
21 static_assert((BufferLineSize
& (BufferLineSize
-1)) == 0, "BufferLineSize is not a power of 2");
24 alignas(16) float mValues
[BufferLineSize
];
25 uint mExpiries
[BufferLineSize
];
34 using namespace std::placeholders
;
36 /* This sliding hold follows the input level with an instant attack and a
37 * fixed duration hold before an instant release to the next highest level.
38 * It is a sliding window maximum (descending maxima) implementation based on
39 * Richard Harter's ascending minima algorithm available at:
41 * http://www.richardhartersworld.com/cri/2001/slidingmin.html
43 float UpdateSlidingHold(SlidingHold
*Hold
, const uint i
, const float in
)
45 static constexpr uint mask
{BufferLineSize
- 1};
46 const uint length
{Hold
->mLength
};
47 float (&values
)[BufferLineSize
] = Hold
->mValues
;
48 uint (&expiries
)[BufferLineSize
] = Hold
->mExpiries
;
49 uint lowerIndex
{Hold
->mLowerIndex
};
50 uint upperIndex
{Hold
->mUpperIndex
};
52 if(i
>= expiries
[upperIndex
])
53 upperIndex
= (upperIndex
+ 1) & mask
;
55 if(in
>= values
[upperIndex
])
57 values
[upperIndex
] = in
;
58 expiries
[upperIndex
] = i
+ length
;
59 lowerIndex
= upperIndex
;
65 if(!(in
>= values
[lowerIndex
]))
67 } while(lowerIndex
--);
72 lowerIndex
= (lowerIndex
+ 1) & mask
;
73 values
[lowerIndex
] = in
;
74 expiries
[lowerIndex
] = i
+ length
;
77 Hold
->mLowerIndex
= lowerIndex
;
78 Hold
->mUpperIndex
= upperIndex
;
80 return values
[upperIndex
];
83 void ShiftSlidingHold(SlidingHold
*Hold
, const uint n
)
85 auto exp_begin
= std::begin(Hold
->mExpiries
) + Hold
->mUpperIndex
;
86 auto exp_last
= std::begin(Hold
->mExpiries
) + Hold
->mLowerIndex
;
87 if(exp_last
-exp_begin
< 0)
89 std::transform(exp_begin
, std::end(Hold
->mExpiries
), exp_begin
,
90 std::bind(std::minus
<>{}, _1
, n
));
91 exp_begin
= std::begin(Hold
->mExpiries
);
93 std::transform(exp_begin
, exp_last
+1, exp_begin
, std::bind(std::minus
<>{}, _1
, n
));
97 /* Multichannel compression is linked via the absolute maximum of all
100 void LinkChannels(Compressor
*Comp
, const uint SamplesToDo
, const FloatBufferLine
*OutBuffer
)
102 const size_t numChans
{Comp
->mNumChans
};
104 ASSUME(SamplesToDo
> 0);
105 ASSUME(numChans
> 0);
107 auto side_begin
= std::begin(Comp
->mSideChain
) + Comp
->mLookAhead
;
108 std::fill(side_begin
, side_begin
+SamplesToDo
, 0.0f
);
110 auto fill_max
= [SamplesToDo
,side_begin
](const FloatBufferLine
&input
) -> void
112 const float *RESTRICT buffer
{al::assume_aligned
<16>(input
.data())};
113 auto max_abs
= std::bind(maxf
, _1
, std::bind(static_cast<float(&)(float)>(std::fabs
), _2
));
114 std::transform(side_begin
, side_begin
+SamplesToDo
, buffer
, side_begin
, max_abs
);
116 std::for_each(OutBuffer
, OutBuffer
+numChans
, fill_max
);
119 /* This calculates the squared crest factor of the control signal for the
120 * basic automation of the attack/release times. As suggested by the paper,
121 * it uses an instantaneous squared peak detector and a squared RMS detector
122 * both with 200ms release times.
124 static void CrestDetector(Compressor
*Comp
, const uint SamplesToDo
)
126 const float a_crest
{Comp
->mCrestCoeff
};
127 float y2_peak
{Comp
->mLastPeakSq
};
128 float y2_rms
{Comp
->mLastRmsSq
};
130 ASSUME(SamplesToDo
> 0);
132 auto calc_crest
= [&y2_rms
,&y2_peak
,a_crest
](const float x_abs
) noexcept
-> float
134 const float x2
{clampf(x_abs
* x_abs
, 0.000001f
, 1000000.0f
)};
136 y2_peak
= maxf(x2
, lerpf(x2
, y2_peak
, a_crest
));
137 y2_rms
= lerpf(x2
, y2_rms
, a_crest
);
138 return y2_peak
/ y2_rms
;
140 auto side_begin
= std::begin(Comp
->mSideChain
) + Comp
->mLookAhead
;
141 std::transform(side_begin
, side_begin
+SamplesToDo
, std::begin(Comp
->mCrestFactor
), calc_crest
);
143 Comp
->mLastPeakSq
= y2_peak
;
144 Comp
->mLastRmsSq
= y2_rms
;
147 /* The side-chain starts with a simple peak detector (based on the absolute
148 * value of the incoming signal) and performs most of its operations in the
151 void PeakDetector(Compressor
*Comp
, const uint SamplesToDo
)
153 ASSUME(SamplesToDo
> 0);
155 /* Clamp the minimum amplitude to near-zero and convert to logarithm. */
156 auto side_begin
= std::begin(Comp
->mSideChain
) + Comp
->mLookAhead
;
157 std::transform(side_begin
, side_begin
+SamplesToDo
, side_begin
,
158 [](const float s
) -> float { return std::log(maxf(0.000001f
, s
)); });
161 /* An optional hold can be used to extend the peak detector so it can more
162 * solidly detect fast transients. This is best used when operating as a
165 void PeakHoldDetector(Compressor
*Comp
, const uint SamplesToDo
)
167 ASSUME(SamplesToDo
> 0);
169 SlidingHold
*hold
{Comp
->mHold
};
171 auto detect_peak
= [&i
,hold
](const float x_abs
) -> float
173 const float x_G
{std::log(maxf(0.000001f
, x_abs
))};
174 return UpdateSlidingHold(hold
, i
++, x_G
);
176 auto side_begin
= std::begin(Comp
->mSideChain
) + Comp
->mLookAhead
;
177 std::transform(side_begin
, side_begin
+SamplesToDo
, side_begin
, detect_peak
);
179 ShiftSlidingHold(hold
, SamplesToDo
);
182 /* This is the heart of the feed-forward compressor. It operates in the log
183 * domain (to better match human hearing) and can apply some basic automation
184 * to knee width, attack/release times, make-up/post gain, and clipping
187 void GainCompressor(Compressor
*Comp
, const uint SamplesToDo
)
189 const bool autoKnee
{Comp
->mAuto
.Knee
};
190 const bool autoAttack
{Comp
->mAuto
.Attack
};
191 const bool autoRelease
{Comp
->mAuto
.Release
};
192 const bool autoPostGain
{Comp
->mAuto
.PostGain
};
193 const bool autoDeclip
{Comp
->mAuto
.Declip
};
194 const uint lookAhead
{Comp
->mLookAhead
};
195 const float threshold
{Comp
->mThreshold
};
196 const float slope
{Comp
->mSlope
};
197 const float attack
{Comp
->mAttack
};
198 const float release
{Comp
->mRelease
};
199 const float c_est
{Comp
->mGainEstimate
};
200 const float a_adp
{Comp
->mAdaptCoeff
};
201 const float *crestFactor
{Comp
->mCrestFactor
};
202 float postGain
{Comp
->mPostGain
};
203 float knee
{Comp
->mKnee
};
205 float t_rel
{release
- attack
};
206 float a_att
{std::exp(-1.0f
/ t_att
)};
207 float a_rel
{std::exp(-1.0f
/ t_rel
)};
208 float y_1
{Comp
->mLastRelease
};
209 float y_L
{Comp
->mLastAttack
};
210 float c_dev
{Comp
->mLastGainDev
};
212 ASSUME(SamplesToDo
> 0);
214 for(float &sideChain
: al::span
<float>{Comp
->mSideChain
, SamplesToDo
})
217 knee
= maxf(0.0f
, 2.5f
* (c_dev
+ c_est
));
218 const float knee_h
{0.5f
* knee
};
220 /* This is the gain computer. It applies a static compression curve
221 * to the control signal.
223 const float x_over
{std::addressof(sideChain
)[lookAhead
] - threshold
};
225 (x_over
<= -knee_h
) ? 0.0f
:
226 (std::fabs(x_over
) < knee_h
) ? (x_over
+ knee_h
) * (x_over
+ knee_h
) / (2.0f
* knee
) :
229 const float y2_crest
{*(crestFactor
++)};
232 t_att
= 2.0f
*attack
/y2_crest
;
233 a_att
= std::exp(-1.0f
/ t_att
);
237 t_rel
= 2.0f
*release
/y2_crest
- t_att
;
238 a_rel
= std::exp(-1.0f
/ t_rel
);
241 /* Gain smoothing (ballistics) is done via a smooth decoupled peak
242 * detector. The attack time is subtracted from the release time
243 * above to compensate for the chained operating mode.
245 const float x_L
{-slope
* y_G
};
246 y_1
= maxf(x_L
, lerpf(x_L
, y_1
, a_rel
));
247 y_L
= lerpf(y_1
, y_L
, a_att
);
249 /* Knee width and make-up gain automation make use of a smoothed
250 * measurement of deviation between the control signal and estimate.
251 * The estimate is also used to bias the measurement to hot-start its
254 c_dev
= lerpf(-(y_L
+c_est
), c_dev
, a_adp
);
258 /* Clipping reduction is only viable when make-up gain is being
259 * automated. It modifies the deviation to further attenuate the
260 * control signal when clipping is detected. The adaptation time
261 * is sufficiently long enough to suppress further clipping at the
265 c_dev
= maxf(c_dev
, sideChain
- y_L
- threshold
- c_est
);
267 postGain
= -(c_dev
+ c_est
);
270 sideChain
= std::exp(postGain
- y_L
);
273 Comp
->mLastRelease
= y_1
;
274 Comp
->mLastAttack
= y_L
;
275 Comp
->mLastGainDev
= c_dev
;
278 /* Combined with the hold time, a look-ahead delay can improve handling of
279 * fast transients by allowing the envelope time to converge prior to
280 * reaching the offending impulse. This is best used when operating as a
283 void SignalDelay(Compressor
*Comp
, const uint SamplesToDo
, FloatBufferLine
*OutBuffer
)
285 const size_t numChans
{Comp
->mNumChans
};
286 const uint lookAhead
{Comp
->mLookAhead
};
288 ASSUME(SamplesToDo
> 0);
289 ASSUME(numChans
> 0);
290 ASSUME(lookAhead
> 0);
292 for(size_t c
{0};c
< numChans
;c
++)
294 float *inout
{al::assume_aligned
<16>(OutBuffer
[c
].data())};
295 float *delaybuf
{al::assume_aligned
<16>(Comp
->mDelay
[c
].data())};
297 auto inout_end
= inout
+ SamplesToDo
;
298 if(SamplesToDo
>= lookAhead
) [[likely
]]
300 auto delay_end
= std::rotate(inout
, inout_end
- lookAhead
, inout_end
);
301 std::swap_ranges(inout
, delay_end
, delaybuf
);
305 auto delay_start
= std::swap_ranges(inout
, inout_end
, delaybuf
);
306 std::rotate(delaybuf
, delay_start
, delaybuf
+ lookAhead
);
314 std::unique_ptr
<Compressor
> Compressor::Create(const size_t NumChans
, const float SampleRate
,
315 const bool AutoKnee
, const bool AutoAttack
, const bool AutoRelease
, const bool AutoPostGain
,
316 const bool AutoDeclip
, const float LookAheadTime
, const float HoldTime
, const float PreGainDb
,
317 const float PostGainDb
, const float ThresholdDb
, const float Ratio
, const float KneeDb
,
318 const float AttackTime
, const float ReleaseTime
)
320 const auto lookAhead
= static_cast<uint
>(
321 clampf(std::round(LookAheadTime
*SampleRate
), 0.0f
, BufferLineSize
-1));
322 const auto hold
= static_cast<uint
>(
323 clampf(std::round(HoldTime
*SampleRate
), 0.0f
, BufferLineSize
-1));
325 size_t size
{sizeof(Compressor
)};
328 size
+= sizeof(*Compressor::mDelay
) * NumChans
;
329 /* The sliding hold implementation doesn't handle a length of 1. A 1-
330 * sample hold is useless anyway, it would only ever give back what was
334 size
+= sizeof(*Compressor::mHold
);
337 auto Comp
= CompressorPtr
{al::construct_at(static_cast<Compressor
*>(al_calloc(16, size
)))};
338 Comp
->mNumChans
= NumChans
;
339 Comp
->mAuto
.Knee
= AutoKnee
;
340 Comp
->mAuto
.Attack
= AutoAttack
;
341 Comp
->mAuto
.Release
= AutoRelease
;
342 Comp
->mAuto
.PostGain
= AutoPostGain
;
343 Comp
->mAuto
.Declip
= AutoPostGain
&& AutoDeclip
;
344 Comp
->mLookAhead
= lookAhead
;
345 Comp
->mPreGain
= std::pow(10.0f
, PreGainDb
/ 20.0f
);
346 Comp
->mPostGain
= PostGainDb
* std::log(10.0f
) / 20.0f
;
347 Comp
->mThreshold
= ThresholdDb
* std::log(10.0f
) / 20.0f
;
348 Comp
->mSlope
= 1.0f
/ maxf(1.0f
, Ratio
) - 1.0f
;
349 Comp
->mKnee
= maxf(0.0f
, KneeDb
* std::log(10.0f
) / 20.0f
);
350 Comp
->mAttack
= maxf(1.0f
, AttackTime
* SampleRate
);
351 Comp
->mRelease
= maxf(1.0f
, ReleaseTime
* SampleRate
);
353 /* Knee width automation actually treats the compressor as a limiter. By
354 * varying the knee width, it can effectively be seen as applying
355 * compression over a wide range of ratios.
358 Comp
->mSlope
= -1.0f
;
364 Comp
->mHold
= al::construct_at(reinterpret_cast<SlidingHold
*>(Comp
.get() + 1));
365 Comp
->mHold
->mValues
[0] = -std::numeric_limits
<float>::infinity();
366 Comp
->mHold
->mExpiries
[0] = hold
;
367 Comp
->mHold
->mLength
= hold
;
368 Comp
->mDelay
= reinterpret_cast<FloatBufferLine
*>(Comp
->mHold
+ 1);
371 Comp
->mDelay
= reinterpret_cast<FloatBufferLine
*>(Comp
.get() + 1);
372 std::uninitialized_fill_n(Comp
->mDelay
, NumChans
, FloatBufferLine
{});
375 Comp
->mCrestCoeff
= std::exp(-1.0f
/ (0.200f
* SampleRate
)); // 200ms
376 Comp
->mGainEstimate
= Comp
->mThreshold
* -0.5f
* Comp
->mSlope
;
377 Comp
->mAdaptCoeff
= std::exp(-1.0f
/ (2.0f
* SampleRate
)); // 2s
382 Compressor::~Compressor()
385 al::destroy_at(mHold
);
388 al::destroy_n(mDelay
, mNumChans
);
393 void Compressor::process(const uint SamplesToDo
, FloatBufferLine
*OutBuffer
)
395 const size_t numChans
{mNumChans
};
397 ASSUME(SamplesToDo
> 0);
398 ASSUME(numChans
> 0);
400 const float preGain
{mPreGain
};
403 auto apply_gain
= [SamplesToDo
,preGain
](FloatBufferLine
&input
) noexcept
-> void
405 float *buffer
{al::assume_aligned
<16>(input
.data())};
406 std::transform(buffer
, buffer
+SamplesToDo
, buffer
,
407 std::bind(std::multiplies
<float>{}, _1
, preGain
));
409 std::for_each(OutBuffer
, OutBuffer
+numChans
, apply_gain
);
412 LinkChannels(this, SamplesToDo
, OutBuffer
);
414 if(mAuto
.Attack
|| mAuto
.Release
)
415 CrestDetector(this, SamplesToDo
);
418 PeakHoldDetector(this, SamplesToDo
);
420 PeakDetector(this, SamplesToDo
);
422 GainCompressor(this, SamplesToDo
);
425 SignalDelay(this, SamplesToDo
, OutBuffer
);
427 const float (&sideChain
)[BufferLineSize
*2] = mSideChain
;
428 auto apply_comp
= [SamplesToDo
,&sideChain
](FloatBufferLine
&input
) noexcept
-> void
430 float *buffer
{al::assume_aligned
<16>(input
.data())};
431 const float *gains
{al::assume_aligned
<16>(&sideChain
[0])};
432 std::transform(gains
, gains
+SamplesToDo
, buffer
, buffer
,
433 std::bind(std::multiplies
<float>{}, _1
, _2
));
435 std::for_each(OutBuffer
, OutBuffer
+numChans
, apply_comp
);
437 auto side_begin
= std::begin(mSideChain
) + SamplesToDo
;
438 std::copy(side_begin
, side_begin
+mLookAhead
, std::begin(mSideChain
));