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

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_ImpulseResponsePlugin.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
11
12namespace tracktion { inline namespace engine
13{
14
15//==============================================================================
17 : Plugin (info)
18{
19 auto um = getUndoManager();
20
21 name.referTo (state, IDs::name, um);
22
23 const juce::NormalisableRange frequencyRange { frequencyToMidiNote (10.0f), frequencyToMidiNote (20'000.0f) };
24
25 highPassCutoffValue.referTo (state, IDs::highPassFrequency, um, frequencyRange.start);
26 lowPassCutoffValue.referTo (state, IDs::lowPassFrequency, um, frequencyRange.end);
27 gainValue.referTo (state, IDs::gain, um, 0.0f);
28 mixValue.referTo (state, IDs::mix, um, 1.0f);
29 qValue.referTo (state, IDs::filterSlope, um, 1.0f / juce::MathConstants<float>::sqrt2);
30
31 normalise.referTo (state, IDs::normalise, um, true);
32 trimSilence.referTo (state, IDs::trimSilence, um, false);
33
34 juce::NormalisableRange volumeRange { -12.0f, 6.0f };
35 volumeRange.setSkewForCentre (0.0f);
36
37 // Initialises parameter and attaches to value
38 gainParam = addParam (IDs::gain.toString(), TRANS ("Gain"), volumeRange,
39 [] (float value) { return juce::Decibels::toString (value); },
40 [] (const juce::String& s) { return s.getFloatValue(); });
41 gainParam->attachToCurrentValue (gainValue);
42
43 highPassCutoffParam = addParam (IDs::highPassMidiNoteNumber.toString(), TRANS ("Low Cut"), frequencyRange,
44 [] (float value) { return juce::String (midiNoteToFrequency (value), 1) + " Hz"; },
45 [] (const juce::String& s) { return frequencyToMidiNote (s.getFloatValue()); });
46 highPassCutoffParam->attachToCurrentValue (highPassCutoffValue);
47
48 lowPassCutoffParam = addParam (IDs::lowPassMidiNoteNumber.toString(), TRANS ("High Cut"), frequencyRange,
49 [] (float value) { return juce::String (midiNoteToFrequency (value), 1) + " Hz"; },
50 [] (const juce::String& s) { return frequencyToMidiNote (s.getFloatValue()); });
51 lowPassCutoffParam->attachToCurrentValue (lowPassCutoffValue);
52
53 mixParam = addParam (IDs::mix.toString(), TRANS("Mix"), { 0.0f, 1.0f, 0.0f },
54 [] (float value) { return juce::String (juce::roundToInt (value * 100.0f)) + "%"; },
55 [] (const juce::String& s) { return s.getFloatValue() / 100.0f; });
56 mixParam->attachToCurrentValue (mixValue);
57
58 filterQParam = addParam (IDs::filterQ.toString(), TRANS("Filter Q"), { 0.1f, 14.0f, 0.0f },
59 [] (float value) { return juce::String (value); },
60 [] (const juce::String& s) { return s.getFloatValue(); });
61 filterQParam->attachToCurrentValue (qValue);
62
63 loadImpulseResponseFromState();
64}
65
67{
68 notifyListenersOfDeletion();
69 gainParam->detachFromCurrentValue();
70 highPassCutoffParam->detachFromCurrentValue();
71 lowPassCutoffParam->detachFromCurrentValue();
72 mixParam->detachFromCurrentValue();
73 filterQParam->detachFromCurrentValue();
74}
75
76const char* ImpulseResponsePlugin::getPluginName() { return NEEDS_TRANS ("Impulse Response"); }
77
78//==============================================================================
79bool ImpulseResponsePlugin::loadImpulseResponse (const void* sourceData, size_t sourceDataSize)
80{
81 auto is = std::make_unique<juce::MemoryInputStream> (sourceData, sourceDataSize, false);
82 auto& formatManager = engine.getAudioFileFormatManager().readFormatManager;
83
84 if (auto reader = std::unique_ptr<juce::AudioFormatReader> (formatManager.createReaderFor (std::move (is))))
85 {
86 juce::AudioBuffer<float> buffer ((int) reader->numChannels, (int) reader->lengthInSamples);
87 reader->read (&buffer, 0, buffer.getNumSamples(), 0, true, true);
88
89 return loadImpulseResponse (std::move (buffer), reader->sampleRate, (int) reader->bitsPerSample);
90 }
91
92 return false;
93}
94
96{
97 juce::MemoryBlock fileDataMemoryBlock;
98
99 if (fileImpulseResponse.loadFileAsData (fileDataMemoryBlock))
100 return loadImpulseResponse (fileDataMemoryBlock.getData(), fileDataMemoryBlock.getSize());
101
102 return false;
103}
104
106 double sampleRateToStore,
107 int bitDepthToStore)
108{
109 juce::MemoryBlock fileDataMemoryBlock;
110
112 .createWriterFor (new juce::MemoryOutputStream (fileDataMemoryBlock, false),
113 sampleRateToStore,
114 (unsigned int) bufferImpulseResponse.getNumChannels(),
115 std::min (24, bitDepthToStore),
116 {},
117 0)))
118 {
119 if (writer->writeFromAudioSampleBuffer (bufferImpulseResponse, 0, bufferImpulseResponse.getNumSamples()))
120 {
121 writer.reset(); // Delete the writer to flush the block before moving the buffer on
122 state.setProperty (IDs::irFileData, juce::var (std::move (fileDataMemoryBlock)), getUndoManager());
123 return true;
124 }
125 }
126
127 return false;
128}
129
130
131//==============================================================================
132juce::String ImpulseResponsePlugin::getName() const { return getPluginName(); }
133juce::String ImpulseResponsePlugin::getShortName (int) { return "IR"; }
134juce::String ImpulseResponsePlugin::getPluginType() { return xmlTypeName; }
135bool ImpulseResponsePlugin::needsConstantBufferSize() { return false; }
137
138double ImpulseResponsePlugin::getLatencySeconds()
139{
140 return processorChain.get<convolutionIndex>().getLatency() / sampleRate;
141}
142
143void ImpulseResponsePlugin::initialise (const PluginInitialisationInfo& info)
144{
145 juce::dsp::ProcessSpec processSpec;
146 processSpec.sampleRate = info.sampleRate;
147 processSpec.maximumBlockSize = (uint32_t) info.blockSizeSamples;
148 processSpec.numChannels = 2;
149 processorChain.prepare (processSpec);
150
151 // Update smoothers
152 lowFreqSmoother.setTargetValue (midiNoteToFrequency (lowPassCutoffParam->getCurrentValue()));
153 highFreqSmoother.setTargetValue (midiNoteToFrequency (highPassCutoffParam->getCurrentValue()));
154 gainSmoother.setTargetValue (gainParam->getCurrentValue());
155 qSmoother.setTargetValue (filterQParam->getCurrentValue());
156
157 const auto wetDry = getWetDryLevels (mixParam->getCurrentValue());
158 wetGainSmoother.setTargetValue (wetDry.wet);
159 dryGainSmoother.setTargetValue (wetDry.dry);
160
161 const double smoothTime = 0.01;
162 lowFreqSmoother.reset (info.sampleRate, smoothTime);
163 highFreqSmoother.reset (info.sampleRate, smoothTime);
164 gainSmoother.reset (info.sampleRate, smoothTime);
165 qSmoother.reset (info.sampleRate, smoothTime);
166 wetGainSmoother.reset (info.sampleRate, smoothTime);
167 dryGainSmoother.reset (info.sampleRate, smoothTime);
168}
169
172
174{
175 processorChain.reset();
176}
177
179{
180 // Update smoothers
181 lowFreqSmoother.setTargetValue (midiNoteToFrequency (lowPassCutoffParam->getCurrentValue()));
182 highFreqSmoother.setTargetValue (midiNoteToFrequency (highPassCutoffParam->getCurrentValue()));
183 gainSmoother.setTargetValue (gainParam->getCurrentValue());
184 qSmoother.setTargetValue (filterQParam->getCurrentValue());
185
186 const auto wetDryGain = getWetDryLevels (mixParam->getCurrentValue());
187 wetGainSmoother.setTargetValue (wetDryGain.wet);
188 dryGainSmoother.setTargetValue (wetDryGain.dry);
189
190 // Update gains and filter params
191 auto hpf = processorChain.get<HPFIndex>().state;
192 auto& lpf = processorChain.get<LPFIndex>().state;
193 auto& gain = processorChain.get<gainIndex>();
194
195 AudioScratchBuffer dryBuffer (*fc.destBuffer);
196
197 if (gainSmoother.isSmoothing() || lowFreqSmoother.isSmoothing() || highFreqSmoother.isSmoothing() || qSmoother.isSmoothing())
198 {
199 const int blockSize = 32;
200 int numSamplesLeft = fc.bufferNumSamples;
201 int numSamplesDone = 0;
202
203 for (;;)
204 {
205 const int numThisTime = std::min (blockSize, numSamplesLeft);
206
207 // Process sub-block
208 auto inOutBlock = juce::dsp::AudioBlock<float> (*fc.destBuffer).getSubBlock (size_t (numSamplesDone + fc.bufferStartSample),
209 size_t (numThisTime));
210 juce::dsp::ProcessContextReplacing <float> context (inOutBlock);
211 processorChain.process (context);
212
213 // Update params
214 const auto qFactor = qSmoother.skip (numThisTime);
215 *hpf = juce::dsp::IIR::ArrayCoefficients<float>::makeHighPass (sampleRate, highFreqSmoother.skip (numThisTime), qFactor);
216 *lpf = juce::dsp::IIR::ArrayCoefficients<float>::makeLowPass (sampleRate, lowFreqSmoother.skip (numThisTime), qFactor);
217 gain.setGainLinear (juce::Decibels::decibelsToGain (gainSmoother.skip (numThisTime)));
218
219 numSamplesDone += numThisTime;
220 numSamplesLeft -= blockSize;
221
222 if (numSamplesDone == fc.bufferNumSamples)
223 break;
224 }
225 }
226 else
227 {
228 // Update params
229 const auto qFactor = qSmoother.getCurrentValue();
230 *hpf = juce::dsp::IIR::ArrayCoefficients<float>::makeHighPass (sampleRate, highFreqSmoother.getCurrentValue(), qFactor);
231 *lpf = juce::dsp::IIR::ArrayCoefficients<float>::makeLowPass (sampleRate, lowFreqSmoother.getCurrentValue(), qFactor);
232 gain.setGainLinear (juce::Decibels::decibelsToGain (gainSmoother.getCurrentValue()));
233
235 juce::dsp::ProcessContextReplacing <float> context (inOutBlock);
236 processorChain.process (context);
237 }
238
239 const bool isMixed = wetGainSmoother.getCurrentValue() < 1.0f;
240 jassert (fc.bufferStartSample == 0); // This assumes bufferStartSample is always 0 which should be the case
241 wetGainSmoother.applyGain (*fc.destBuffer, fc.bufferNumSamples);
242 dryGainSmoother.applyGain (dryBuffer.buffer, dryBuffer.buffer.getNumSamples());
243 jassert (fc.bufferNumSamples == dryBuffer.buffer.getNumSamples());
244 jassert (fc.destBuffer->getNumChannels() == dryBuffer.buffer.getNumChannels());
245
246 if (isMixed)
247 {
248 for (int c = 0; c < fc.destBuffer->getNumChannels(); ++c)
249 fc.destBuffer->addFrom (c, fc.bufferStartSample, dryBuffer.buffer,
251 }
252}
253
254void ImpulseResponsePlugin::restorePluginStateFromValueTree (const juce::ValueTree& v)
255{
256 copyPropertiesToCachedValues (v, gainValue, highPassCutoffValue, lowPassCutoffValue, mixValue, qValue);
257
258 state.setProperty (IDs::name, v[IDs::name], getUndoManager());
259
260 if (auto irFileData = v.getProperty (IDs::irFileData).getBinaryData())
261 state.setProperty (IDs::irFileData, juce::var (juce::MemoryBlock (*irFileData)), getUndoManager());
262
263 for (auto p : getAutomatableParameters())
264 p->updateFromAttachedValue();
265}
266
267//==============================================================================
268void ImpulseResponsePlugin::loadImpulseResponseFromState()
269{
270 if (auto irFileData = state.getProperty (IDs::irFileData).getBinaryData())
271 {
272 auto is = std::make_unique<juce::MemoryInputStream> (*irFileData, false);
273
275 .createReaderFor (is.release(), true)))
276 {
277 juce::AudioBuffer<float> loadIRBuffer ((int) reader->numChannels, (int) reader->lengthInSamples);
278 reader->read (&loadIRBuffer, 0, (int) reader->lengthInSamples, 0, true, true);
279
280 jassert (reader->numChannels > 0);
281 processorChain.get<convolutionIndex>().loadImpulseResponse (std::move (loadIRBuffer),
282 reader->sampleRate,
283 reader->numChannels > 1 ? juce::dsp::Convolution::Stereo::yes
284 : juce::dsp::Convolution::Stereo::no,
285 trimSilence.get() ? juce::dsp::Convolution::Trim::yes
286 : juce::dsp::Convolution::Trim::no,
287 normalise.get() ? juce::dsp::Convolution::Normalise::yes
288 : juce::dsp::Convolution::Normalise::no);
289 }
290 }
291}
292
293void ImpulseResponsePlugin::valueTreePropertyChanged (juce::ValueTree& v, const juce::Identifier& id)
294{
295 if (v == state)
296 {
297 if (id == IDs::irFileData || id == IDs::normalise || id == IDs::trimSilence)
298 loadImpulseResponseFromState();
299 }
300 else
301 {
302 Plugin::valueTreePropertyChanged (v, id);
303 }
304}
305
306}} // namespace tracktion { inline namespace engine
int getNumChannels() const noexcept
int getNumSamples() const noexcept
void addFrom(int destChannel, int destStartSample, const AudioBuffer &source, int sourceChannel, int sourceStartSample, int numSamples, Type gainToApplyToSource=Type(1)) noexcept
void referTo(ValueTree &tree, const Identifier &property, UndoManager *um)
Type get() const noexcept
static Type decibelsToGain(Type decibels, Type minusInfinityDb=Type(defaultMinusInfinitydB))
static String toString(Type decibels, int decimalPlaces=2, Type minusInfinityDb=Type(defaultMinusInfinitydB), bool shouldIncludeSuffix=true, StringRef customMinusInfinityString={})
bool loadFileAsData(MemoryBlock &result) const
void * getData() noexcept
size_t getSize() const noexcept
void setSkewForCentre(ValueType centrePointValue) noexcept
bool isSmoothing() const noexcept
void applyGain(FloatType *samples, int numSamples) noexcept
FloatType getCurrentValue() const noexcept
FloatType skip(int numSamples) noexcept
void reset(double sampleRate, double rampLengthInSeconds) noexcept
void setTargetValue(FloatType newValue) noexcept
float getFloatValue() const noexcept
ValueTree & setProperty(const Identifier &name, const var &newValue, UndoManager *undoManager)
const var & getProperty(const Identifier &name) const noexcept
Iterator end() const noexcept
AudioBlock getSubBlock(size_t newOffset, size_t newLength) const noexcept
void prepare(const ProcessSpec &spec)
auto & get() noexcept
void process(const ProcessContext &context) noexcept
MemoryBlock * getBinaryData() const noexcept
An audio scratch buffer that has pooled storage.
AudioFileFormatManager & getAudioFileFormatManager() const
Returns the AudioFileFormatManager that maintains a list of available audio file formats.
bool loadImpulseResponse(const void *sourceData, size_t sourceDataSize)
Loads an impulse from binary audio file data i.e.
AutomatableParameter::Ptr gainParam
Parameter for the gain to apply.
AutomatableParameter::Ptr mixParam
Parameter for the mix control, 0.0 = dry, 1.0 = wet.
juce::CachedValue< juce::String > name
A name property.
juce::String getSelectableDescription() override
Subclasses must return a description of what they are.
juce::String getName() const override
The name of the type, e.g.
void applyToBuffer(const PluginRenderContext &) override
Process the next block of data.
ImpulseResponsePlugin(PluginCreationInfo)
Creates an ImpulseResponsePlugin.
void deinitialise() override
Called after play stops to release resources.
AutomatableParameter::Ptr lowPassCutoffParam
Cutoff frequency for the low pass filter to applied after the IR.
AutomatableParameter::Ptr filterQParam
Parameter for the Q factor of the high pass and low pass filters.
juce::CachedValue< bool > normalise
Normalise the IR file when loading from the state.
AutomatableParameter::Ptr highPassCutoffParam
Cutoff frequency for the high pass filter to applied after the IR.
juce::CachedValue< bool > trimSilence
Trim silence from the IR file when loading from the state.
void reset() override
Should reset synth voices, tails, clear delay buffers, etc.
T is_pointer_v
#define TRANS(stringLiteral)
#define NEEDS_TRANS(stringLiteral)
#define jassert(expression)
T min(T... args)
int roundToInt(const FloatType value) noexcept
Passed into Plugins when they are being initialised, to give them useful contextual information that ...
typedef uint32_t
static std::array< NumericType, 6 > makeHighPass(double sampleRate, NumericType frequency)
static std::array< NumericType, 6 > makeLowPass(double sampleRate, NumericType frequency)
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.
typedef size_t