tracktion-engine 3.0-10-g034fdde4aa5
Tracktion Engine — High level data model for audio applications

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_EnvelopeFollowerModifier.cpp
Go to the documentation of this file.
1 /*
2 ,--. ,--. ,--. ,--.
3 ,-' '-.,--.--.,--,--.,---.| |,-.,-' '-.`--' ,---. ,--,--, Copyright 2024
4 '-. .-'| .--' ,-. | .--'| /'-. .-',--.| .-. || \ Tracktion Software
5 | | | | \ '-' \ `--.| \ \ | | | |' '-' '| || | Corporation
6 `---' `--' `--`--'`---'`--'`--' `---' `--' `---' `--''--' www.tracktion.com
7
8 Tracktion Engine uses a GPL/commercial licence - see LICENCE.md for details.
9*/
10
11namespace tracktion { inline namespace engine
12{
13
14//==============================================================================
20{
21public:
23 RunningRMS() = default;
24
26 inline void setSampleRate (float newSampleRate) noexcept
27 {
28 sampleRate = newSampleRate;
29 update();
30 }
31
33 inline float processSingleSample (float in) noexcept
34 {
35 state += (juce::square (in) - state) * coefficient;
36 return std::sqrt (state);
37 }
38
39private:
40 float state = 0.0f;
41 float coefficient = 1.0f;
42 float sampleRate = 44100.0f;
43
44 void update() noexcept
45 {
46 coefficient = 1.0f - std::exp (-2.0f * juce::MathConstants<float>::pi * 100.0f / sampleRate);
47 state = 0.0f;
48 }
49};
50
51//==============================================================================
57{
58public:
59 //==============================================================================
62 {
63 peakMode = 0,
64 msMode,
65 rmsMode
66 };
67
70 {
71 digitalTC = 0,
72 slowDigitalTC,
73 analogTC
74 };
75
76 //==============================================================================
78 EnvelopeFollower() = default;
79
83 void setSampleRate (float newSampleRate) noexcept
84 {
85 sampleRate = newSampleRate;
86 rms.setSampleRate (newSampleRate);
87
88 attackCoeff = getDelta (attackTimeMs);
89 releaseCoeff = getDelta (releaseTimeMs);
90 holdSamples = (int) msToSamples (holdTimeMs, sampleRate);
91
92 reset();
93 }
94
96 void reset() noexcept
97 {
98 envelope = 0.0f;
99 }
100
101 //==============================================================================
103 void setAttackTime (float attackMs)
104 {
105 attackTimeMs = attackMs;
106 attackCoeff = getDelta (attackTimeMs);
107 }
108
110 void setHoldTime (float holdMs)
111 {
112 holdTimeMs = holdMs;
113 holdSamples = (int) msToSamples (holdMs, sampleRate);
114 }
115
117 void setReleaseTime (float releaseMs)
118 {
119 releaseTimeMs = releaseMs;
120 releaseCoeff = getDelta (releaseTimeMs);
121 }
122
124 void setDetectMode (DetectionMode newDetectionMode)
125 {
126 detectMode = newDetectionMode;
127 }
128
131 {
132 if (! setIfDifferent (timeConstantMode, newTC))
133 return;
134
135 switch (timeConstantMode)
136 {
137 case digitalTC: timeConstant = -2.0f; break; // log (1%)
138 case slowDigitalTC: timeConstant = -1.0f; break; // log (10%)
139 case analogTC: timeConstant = -0.43533393574791066201247090699309f; break; // log (36.7%)
140 }
141
142 attackCoeff = getDelta (attackTimeMs);
143 releaseCoeff = getDelta (releaseTimeMs);
144 }
145
146 //==============================================================================
148 float processSingleSample (float input)
149 {
150 switch (detectMode)
151 {
152 case peakMode:
153 {
154 input = std::abs (input);
155 break;
156 }
157 case msMode:
158 {
159 input = juce::square (std::abs (input));
160 break;
161 }
162 case rmsMode:
163 {
164 input = rms.processSingleSample (input);
165 break;
166 }
167 }
168
169 if (input > envelope)
170 {
171 envelope = attackCoeff * (envelope - input) + input;
172 holdSamplesLeft = holdSamples;
173 }
174 else if (holdSamplesLeft > 0)
175 {
176 --holdSamplesLeft;
177 }
178 else
179 {
180 envelope = releaseCoeff * (envelope - input) + input;
181 }
182
183 jassert (! std::isnan (envelope));
184 jassert (envelope == 0.0f || std::isnormal (envelope));
185 envelope = juce::jlimit (0.0f, 1.0f, envelope);
186
187 return envelope;
188 }
189
190protected:
191 float attackCoeff = 0.0f;
192 float releaseCoeff = 0.0f;
193 float envelope = 0.0f;
194 float attackTimeMs = 0.0f;
195 float holdTimeMs = 0.0f;
196 float releaseTimeMs = 0.0f;
197 float sampleRate = 44100.0f;
198 float timeConstant = -2.0f;
199 int holdSamples = 0, holdSamplesLeft = 0;
200 DetectionMode detectMode = peakMode;
201 TimeConstantMode timeConstantMode = digitalTC;
202 RunningRMS rms;
203
204 inline float getDelta (float timeMs)
205 {
206 return std::exp (timeConstant / (timeMs * sampleRate * 0.001f));
207 }
208
209 inline double msToSamples (double numMiliseconds, double sr)
210 {
211 return numMiliseconds * 0.001 * sr;
212 }
213
215};
216
217
218//==============================================================================
219EnvelopeFollowerModifier::EnvelopeFollowerModifier (Edit& e, const juce::ValueTree& v)
220 : Modifier (e, v)
221{
222 auto um = &edit.getUndoManager();
223
224 gainDb.referTo (state, IDs::gainDb, um, 0.0f);
225 attack.referTo (state, IDs::attack, um, 100.0f);
226 hold.referTo (state, IDs::hold, um, 0.0f);
227 release.referTo (state, IDs::release, um, 500.0f);
228 depth.referTo (state, IDs::depth, um, 1.0f);
229 offset.referTo (state, IDs::offset, um, 0.0f);
230 lowPassEnabled.referTo (state, IDs::lowPassEnabled, um, false);
231 highPassEnabled.referTo (state, IDs::highPassEnabled, um, false);
232 lowPassFrequency.referTo (state, IDs::lowPassFrequency, um, 2000.0f);
233 highPassFrequency.referTo (state, IDs::highPassFrequency, um, 500.0f);
234
235 auto addDiscreteParam = [this] (const juce::String& paramID, const juce::String& name,
237 const juce::StringArray& labels) -> AutomatableParameter*
238 {
239 auto* p = new DiscreteLabelledParameter (paramID, name, *this, valueRange, labels.size(), labels);
240 addAutomatableParameter (p);
241 p->attachToCurrentValue (val);
242
243 return p;
244 };
245
246 auto addParam = [this] (const juce::String& paramID, const juce::String& name,
247 juce::NormalisableRange<float> valueRange, float centreVal,
248 juce::CachedValue<float>& val, const juce::String& suffix) -> AutomatableParameter*
249 {
250 valueRange.setSkewForCentre (centreVal);
251 auto* p = new SuffixedParameter (paramID, name, *this, valueRange, suffix);
252 addAutomatableParameter (p);
253 p->attachToCurrentValue (val);
254
255 return p;
256 };
257
258 const juce::NormalisableRange<float> freqRange (20.0f, 20000.0f);
259
260 gainDbParam = addParam ("gainDb", TRANS("Gain"), { -20.0f, 20.0f, 0.001f }, 0.0f, gainDb, "dB");
261 attackParam = addParam ("attack", TRANS("Attack"), { 1.0f, 5000.0f }, 50.0f, attack, "ms");
262 holdParam = addParam ("hold", TRANS("Hold"), { 0.0f, 5000.0f }, 50.0f, hold, "ms");
263 releaseParam = addParam ("release", TRANS("Release"), { 1.0f, 5000.0f }, 50.0f, release, "ms");
264 depthParam = addParam ("depth", TRANS("Depth"), { -1.0f, 1.0f }, 0.0f, depth, {});
265 offsetParam = addParam ("offset", TRANS("Offset"), { 0.0f, 1.0f }, 0.5f, offset, {});
266 lowPassEnabledParam = addDiscreteParam ("lowPassEnabled", TRANS("Low-pass Enabled"), { 0.0f, 1.0f }, lowPassEnabled, modifier::getEnabledNames());
267 highPassEnabledParam = addDiscreteParam ("highPassEnabled", TRANS("High-pass Enabled"), { 0.0f, 1.0f }, highPassEnabled, modifier::getEnabledNames());
268 lowPassFrequencyParam = addParam ("lowPassFrequency", TRANS("Low-pass Frequency"), freqRange, 700.0f, lowPassFrequency, "Hz");
269 highPassFrequencyParam = addParam ("highPassFrequency", TRANS("High-pass Frequency"), freqRange, 700.0f, highPassFrequency, "Hz");
270
271 changedTimer.setCallback ([this]
272 {
273 changedTimer.stopTimer();
274 changed();
275 });
276
277 state.addListener (this);
278
279 envelopeFollower = std::make_unique<EnvelopeFollower>();
280}
281
282EnvelopeFollowerModifier::~EnvelopeFollowerModifier()
283{
284 state.removeListener (this);
285 notifyListenersOfDeletion();
286
287 for (auto p : getAutomatableParameters())
288 p->detachFromCurrentValue();
289
290 deleteAutomatableParameters();
291}
292
293//==============================================================================
295{
296 return (getEnvelopeValue() * depthParam->getCurrentValue()) + offsetParam->getCurrentValue();
297}
298
303
305{
306 return { TRANS("Audio input left"),
307 TRANS("Audio input right") };
308}
309
310void EnvelopeFollowerModifier::initialise (double newSampleRate, int)
311{
312 prepareToPlay (newSampleRate);
313}
314
316{
317 reset();
318}
319
321{
322 setEditTime (pc.editTime.getStart());
323
324 if (pc.destBuffer == nullptr)
325 return;
326
327 updateParameterStreams (pc.editTime.getStart());
328
333 processBlock (ab);
334}
335
336//==============================================================================
337EnvelopeFollowerModifier::Assignment::Assignment (const juce::ValueTree& v, const EnvelopeFollowerModifier& evm)
338 : AutomatableParameter::ModifierAssignment (evm.edit, v),
339 envelopeFollowerModifierID (evm.itemID)
340{
341}
342
343bool EnvelopeFollowerModifier::Assignment::isForModifierSource (const ModifierSource& source) const
344{
345 if (auto* mod = dynamic_cast<const EnvelopeFollowerModifier*> (&source))
346 return mod->itemID == envelopeFollowerModifierID;
347
348 return false;
349}
350
351EnvelopeFollowerModifier::Ptr EnvelopeFollowerModifier::Assignment::getEnvelopeFollowerModifier() const
352{
353 if (auto mod = findModifierTypeForID<EnvelopeFollowerModifier> (edit, envelopeFollowerModifierID))
354 return mod;
355
356 return {};
357}
358
359//==============================================================================
360void EnvelopeFollowerModifier::prepareToPlay (double newSampleRate)
361{
362 envelopeFollower->setSampleRate ((float) newSampleRate);
363}
364
365void EnvelopeFollowerModifier::processBlock (const juce::AudioBuffer<float>& ab)
366{
367 envelopeFollower->setAttackTime (attackParam->getCurrentValue());
368 envelopeFollower->setHoldTime (holdParam->getCurrentValue());
369 envelopeFollower->setReleaseTime (releaseParam->getCurrentValue());
370 auto gainVal = juce::Decibels::decibelsToGain (gainDbParam->getCurrentValue());
371 const bool lowPass = getBoolParamValue (*lowPassEnabledParam);
372 const bool highPass = getBoolParamValue (*highPassEnabledParam);
373
374 const int numChannels = ab.getNumChannels();
375 const int numSamples = ab.getNumSamples();
376 const float* const* samples = ab.getArrayOfReadPointers();
377 float envelope = 0.0f;
378
379 AudioScratchBuffer scratch (1, numSamples);
380 float* scratchData = scratch.buffer.getWritePointer (0);
381
382 // Find max of all channels
383 for (int i = 0; i < numSamples; ++i)
384 {
385 float sample = 0.0f;
386
387 for (int c = 0; c < numChannels; ++c)
388 sample = std::max (sample, std::abs (samples[c][i]));
389
390 scratchData[i] = sample * gainVal;
391 }
392
393 // Filter if required
394 if (setIfDifferent (currentLowPassFrequency, lowPassFrequencyParam->getCurrentValue()))
395 lowPassFilter.setCoefficients (juce::IIRCoefficients::makeLowPass (getSampleRate(), currentLowPassFrequency));
396
397 if (setIfDifferent (currentHighPassFrequency, highPassFrequencyParam->getCurrentValue()))
398 highPassFilter.setCoefficients (juce::IIRCoefficients::makeHighPass (getSampleRate(), currentHighPassFrequency));
399
400 if (lowPass)
401 lowPassFilter.processSamples (scratchData, numSamples);
402
403 if (highPass)
404 highPassFilter.processSamples (scratchData, numSamples);
405
406 // Process envelope
407 for (int i = 0; i < numSamples; ++i)
408 envelope = envelopeFollower->processSingleSample (scratchData[i]);
409
410 jassert (! std::isnan (envelope));
411 envelopeValue.store (envelope, std::memory_order_release);
412}
413
414void EnvelopeFollowerModifier::reset()
415{
416 envelopeFollower->reset();
417}
418
419void EnvelopeFollowerModifier::valueTreeChanged()
420{
421 if (! changedTimer.isTimerRunning())
422 changedTimer.startTimerHz (30);
423}
424
425}} // namespace tracktion { inline namespace engine
int getNumChannels() const noexcept
int getNumSamples() const noexcept
const Type *const * getArrayOfReadPointers() const noexcept
Type *const * getArrayOfWritePointers() noexcept
static Type decibelsToGain(Type decibels, Type minusInfinityDb=Type(defaultMinusInfinitydB))
static IIRCoefficients makeLowPass(double sampleRate, double frequency) noexcept
static IIRCoefficients makeHighPass(double sampleRate, double frequency) noexcept
void setCoefficients(const IIRCoefficients &newCoefficients) noexcept
void processSamples(float *samples, int numSamples) noexcept
void startTimerHz(int timerFrequencyHz) noexcept
bool isTimerRunning() const noexcept
void addListener(Listener *listener)
void removeListener(Listener *listener)
void updateParameterStreams(TimePosition)
Updates all the parameter streams to their positions at this time.
const EditItemID itemID
Every EditItem has an ID which is unique within the edit.
Envelope follower with adjustable attack/release parameters as well as several detection and time con...
EnvelopeFollower()=default
Creates a default EnvelopeFollower.
void setDetectMode(DetectionMode newDetectionMode)
Sets the detection mode to use.
void setSampleRate(float newSampleRate) noexcept
Sets the sample rate to use.
void setTimeConstantMode(TimeConstantMode newTC)
Sets the time constant to be used.
void reset() noexcept
Resets the detection and current envelope.
float processSingleSample(float input)
Returns the envelope for a new sample.
void initialise() override
Call this once after construction to connect it to the audio graph.
float getCurrentValue() override
Must return the current value of the modifier.
void applyToBuffer(const PluginRenderContext &) override
Sub classes should implement this to process the Modifier.
void deinitialise() override
Sub classes should implement this to deinitialise the Modifier.
AutomatableParameter::ModifierAssignment * createAssignment(const juce::ValueTree &) override
Must return a new ModifierAssignment for a given state.
juce::StringArray getAudioInputNames() override
Can return an array of names represeting audio inputs.
Calculates the RMS of a continuous signal.
void setSampleRate(float newSampleRate) noexcept
Sets the sample rate to use for detection.
RunningRMS()=default
Creates an empty RunningRMS.
float processSingleSample(float in) noexcept
Returns the current RMS for a new input sample.
virtual void changed()
This should be called to send a change notification to any SelectableListeners that are registered wi...
T exp(T... args)
T is_pointer_v
T isnan(T... args)
T isnormal(T... args)
#define TRANS(stringLiteral)
#define jassert(expression)
#define JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(className)
typedef int
T max(T... args)
Type jlimit(Type lowerLimit, Type upperLimit, Type valueToConstrain) noexcept
constexpr NumericType square(NumericType n) noexcept
bool getBoolParamValue(const AutomatableParameter &ap)
Returns a bool version of an AutomatableParameter.
T sample(T... args)
T sqrt(T... args)
T store(T... args)
Connects a modifier source to an AutomatableParameter.
juce::ValueTree state
Modifier internal state.
void setEditTime(TimePosition newEditTime)
Subclasses can call this to update the edit time of the current value.
double getSampleRate() const
Returns the sample rate the Modifier has been initialised with.
The context passed to plugin render methods to provide it with buffers to fill.
int bufferNumSamples
The number of samples to write into the audio buffer.
juce::AudioBuffer< float > * destBuffer
The target audio buffer which needs to be filled.
int bufferStartSample
The index of the start point in the audio buffer from which data must be written.
TimeRange editTime
The edit time range this context represents.