diff --git a/BREAKING-CHANGES.txt b/BREAKING-CHANGES.txt index 54f92e61bc..cc82dd8114 100644 --- a/BREAKING-CHANGES.txt +++ b/BREAKING-CHANGES.txt @@ -4,6 +4,40 @@ JUCE breaking changes Develop ======= +Change +------ +The Convolution class interface was changed: +- `loadImpulseResponse` member functions now take `enum class` parameters + instead of `bool`. +- `copyAndLoadImpulseResponseFromBlock` and + `copyAndLoadImpulseResponseFromBuffer` were replaced by a new + `loadImpulseResponse` overload. + +Possible Issues +--------------- +Code using the old interface will no longer compile, and will need to be +updated. + +Workaround +---------- +Code that was previously loading impulse responses from binary data or from +files can substitute old `bool` parameters with the newer `enum class` +equivalents. Code that was previously passing buffers or blocks will need +reworking so that the Convolution instance can take ownership of the buffer +containing the impulse response. + +Rationale +--------- +The newer `enum class` parameters make user code much more readable, e.g. +`loadImpulseResponse (file, Stereo::yes, Trim::yes, 0, Normalise::yes)` rather +than `loadImpulseResponse (file, true, true, 0, true);`. By taking ownership of +the passed buffer, the Convolution can avoid preallocating a large internal +buffer, reducing memory usage when short impulse responses are used. Changing +the ownership semantics of the buffer also makes it easier for users to avoid +copies/allocations on the audio thread, and gives more flexibility to the +implementation to run initialisation tasks on a background thread. + + Change ------ All references to ROLI Ltd. (ROLI) have been changed to Raw Material Software diff --git a/examples/DSP/ConvolutionDemo.h b/examples/DSP/ConvolutionDemo.h index 384b7d3b32..dcec0e6677 100644 --- a/examples/DSP/ConvolutionDemo.h +++ b/examples/DSP/ConvolutionDemo.h @@ -66,6 +66,15 @@ struct ConvolutionDemoDSP void process (ProcessContextReplacing context) { context.isBypassed = bypass; + + // Load a new IR if there's one available. Note that this doesn't lock or allocate! + if (auto buffer = bufferTransfer.get()) + convolution.loadImpulseResponse (std::move (buffer->buffer), + buffer->sampleRate, + Convolution::Stereo::yes, + Convolution::Trim::yes, + Convolution::Normalise::yes); + convolution.process (context); } @@ -76,39 +85,90 @@ struct ConvolutionDemoDSP void updateParameters() { - if (auto* cabinetTypeParameter = dynamic_cast (parameters[0])) + auto* cabinetTypeParameter = dynamic_cast (parameters[0]); + + if (cabinetTypeParameter == nullptr) { - if (cabinetTypeParameter->getCurrentSelectedID() == 1) + jassertfalse; + return; + } + + if (cabinetTypeParameter->getCurrentSelectedID() == 1) + { + bypass = true; + } + else + { + bypass = false; + + auto selectedType = cabinetTypeParameter->getCurrentSelectedID(); + auto assetName = (selectedType == 2 ? "guitar_amp.wav" : "cassette_recorder.wav"); + + auto assetInputStream = createAssetInputStream (assetName); + + if (assetInputStream == nullptr) { - bypass = true; + jassertfalse; + return; } - else + + AudioFormatManager manager; + manager.registerBasicFormats(); + std::unique_ptr reader { manager.createReaderFor (std::move (assetInputStream)) }; + + if (reader == nullptr) { - bypass = false; - - auto maxSize = static_cast (roundToInt (sampleRate * (8192.0 / 44100.0))); - auto selectedType = cabinetTypeParameter->getCurrentSelectedID(); - auto assetName = (selectedType == 2 ? "guitar_amp.wav" : "cassette_recorder.wav"); - - if (auto assetInputStream = createAssetInputStream (assetName)) - { - currentCabinetData.reset(); - assetInputStream->readIntoMemoryBlock (currentCabinetData); - - convolution.loadImpulseResponse (currentCabinetData.getData(), currentCabinetData.getSize(), - false, true, maxSize); - } + jassertfalse; + return; } + + AudioBuffer buffer (static_cast (reader->numChannels), + static_cast (reader->lengthInSamples)); + reader->read (buffer.getArrayOfWritePointers(), buffer.getNumChannels(), 0, buffer.getNumSamples()); + + bufferTransfer.set (std::make_unique (std::move (buffer), + reader->sampleRate)); } } //============================================================================== + struct BufferWithSampleRate + { + BufferWithSampleRate (AudioBuffer&& bufferIn, double sampleRateIn) + : buffer (std::move (bufferIn)), sampleRate (sampleRateIn) {} + + AudioBuffer buffer; + double sampleRate = 0.0; + }; + + class BufferTransfer + { + public: + void set (std::unique_ptr p) + { + const SpinLock::ScopedLockType lock (mutex); + ptr = std::move (p); + } + + std::unique_ptr get() + { + const SpinLock::ScopedTryLockType lock (mutex); + return lock.isLocked() ? std::move (ptr) : nullptr; + } + + private: + std::unique_ptr ptr; + SpinLock mutex; + }; + double sampleRate = 0.0; bool bypass = false; MemoryBlock currentCabinetData; Convolution convolution; + BufferTransfer bufferTransfer; + ChoiceParameter cabinetParam { { "Bypass", "Guitar amplifier 8''", "Cassette recorder" }, 1, "Cabinet Type" }; std::vector parameters { &cabinetParam }; diff --git a/examples/DemoRunner/Builds/Android/app/CMakeLists.txt b/examples/DemoRunner/Builds/Android/app/CMakeLists.txt index 7a30259853..529abf0469 100644 --- a/examples/DemoRunner/Builds/Android/app/CMakeLists.txt +++ b/examples/DemoRunner/Builds/Android/app/CMakeLists.txt @@ -828,6 +828,8 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_core/maths/juce_Random.h" "../../../../../modules/juce_core/maths/juce_Range.h" "../../../../../modules/juce_core/maths/juce_StatisticsAccumulator.h" + "../../../../../modules/juce_core/memory/juce_AllocationHooks.cpp" + "../../../../../modules/juce_core/memory/juce_AllocationHooks.h" "../../../../../modules/juce_core/memory/juce_Atomic.h" "../../../../../modules/juce_core/memory/juce_ByteOrder.h" "../../../../../modules/juce_core/memory/juce_ContainerDeletePolicy.h" @@ -1048,6 +1050,8 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_data_structures/juce_data_structures.h" "../../../../../modules/juce_dsp/containers/juce_AudioBlock.h" "../../../../../modules/juce_dsp/containers/juce_AudioBlock_test.cpp" + "../../../../../modules/juce_dsp/containers/juce_FixedSizeFunction.h" + "../../../../../modules/juce_dsp/containers/juce_FixedSizeFunction_test.cpp" "../../../../../modules/juce_dsp/containers/juce_SIMDRegister.h" "../../../../../modules/juce_dsp/containers/juce_SIMDRegister_Impl.h" "../../../../../modules/juce_dsp/containers/juce_SIMDRegister_test.cpp" @@ -1055,6 +1059,7 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_dsp/filter_design/juce_FilterDesign.h" "../../../../../modules/juce_dsp/frequency/juce_Convolution.cpp" "../../../../../modules/juce_dsp/frequency/juce_Convolution.h" + "../../../../../modules/juce_dsp/frequency/juce_Convolution_test.cpp" "../../../../../modules/juce_dsp/frequency/juce_FFT.cpp" "../../../../../modules/juce_dsp/frequency/juce_FFT.h" "../../../../../modules/juce_dsp/frequency/juce_FFT_test.cpp" @@ -2577,6 +2582,8 @@ set_source_files_properties("../../../../../modules/juce_core/maths/juce_Random. set_source_files_properties("../../../../../modules/juce_core/maths/juce_Random.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/maths/juce_Range.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/maths/juce_StatisticsAccumulator.h" PROPERTIES HEADER_FILE_ONLY TRUE) +set_source_files_properties("../../../../../modules/juce_core/memory/juce_AllocationHooks.cpp" PROPERTIES HEADER_FILE_ONLY TRUE) +set_source_files_properties("../../../../../modules/juce_core/memory/juce_AllocationHooks.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/memory/juce_Atomic.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/memory/juce_ByteOrder.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/memory/juce_ContainerDeletePolicy.h" PROPERTIES HEADER_FILE_ONLY TRUE) @@ -2797,6 +2804,8 @@ set_source_files_properties("../../../../../modules/juce_data_structures/juce_da set_source_files_properties("../../../../../modules/juce_data_structures/juce_data_structures.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_dsp/containers/juce_AudioBlock.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_dsp/containers/juce_AudioBlock_test.cpp" PROPERTIES HEADER_FILE_ONLY TRUE) +set_source_files_properties("../../../../../modules/juce_dsp/containers/juce_FixedSizeFunction.h" PROPERTIES HEADER_FILE_ONLY TRUE) +set_source_files_properties("../../../../../modules/juce_dsp/containers/juce_FixedSizeFunction_test.cpp" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_dsp/containers/juce_SIMDRegister.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_dsp/containers/juce_SIMDRegister_Impl.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_dsp/containers/juce_SIMDRegister_test.cpp" PROPERTIES HEADER_FILE_ONLY TRUE) @@ -2804,6 +2813,7 @@ set_source_files_properties("../../../../../modules/juce_dsp/filter_design/juce_ set_source_files_properties("../../../../../modules/juce_dsp/filter_design/juce_FilterDesign.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_dsp/frequency/juce_Convolution.cpp" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_dsp/frequency/juce_Convolution.h" PROPERTIES HEADER_FILE_ONLY TRUE) +set_source_files_properties("../../../../../modules/juce_dsp/frequency/juce_Convolution_test.cpp" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_dsp/frequency/juce_FFT.cpp" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_dsp/frequency/juce_FFT.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_dsp/frequency/juce_FFT_test.cpp" PROPERTIES HEADER_FILE_ONLY TRUE) diff --git a/examples/DemoRunner/Builds/VisualStudio2015/DemoRunner_App.vcxproj b/examples/DemoRunner/Builds/VisualStudio2015/DemoRunner_App.vcxproj index 0b86ac9981..3fc438bdc4 100644 --- a/examples/DemoRunner/Builds/VisualStudio2015/DemoRunner_App.vcxproj +++ b/examples/DemoRunner/Builds/VisualStudio2015/DemoRunner_App.vcxproj @@ -1109,6 +1109,9 @@ true + + true + true @@ -1388,6 +1391,9 @@ true + + true + true @@ -1397,6 +1403,9 @@ true + + true + true @@ -2887,6 +2896,7 @@ + @@ -3005,6 +3015,7 @@ + diff --git a/examples/DemoRunner/Builds/VisualStudio2015/DemoRunner_App.vcxproj.filters b/examples/DemoRunner/Builds/VisualStudio2015/DemoRunner_App.vcxproj.filters index 1ec8388045..e339270961 100644 --- a/examples/DemoRunner/Builds/VisualStudio2015/DemoRunner_App.vcxproj.filters +++ b/examples/DemoRunner/Builds/VisualStudio2015/DemoRunner_App.vcxproj.filters @@ -1576,6 +1576,9 @@ JUCE Modules\juce_core\maths + + JUCE Modules\juce_core\memory + JUCE Modules\juce_core\memory @@ -1879,6 +1882,9 @@ JUCE Modules\juce_dsp\containers + + JUCE Modules\juce_dsp\containers + JUCE Modules\juce_dsp\containers @@ -1888,6 +1894,9 @@ JUCE Modules\juce_dsp\frequency + + JUCE Modules\juce_dsp\frequency + JUCE Modules\juce_dsp\frequency @@ -4368,6 +4377,9 @@ JUCE Modules\juce_core\maths + + JUCE Modules\juce_core\memory + JUCE Modules\juce_core\memory @@ -4722,6 +4734,9 @@ JUCE Modules\juce_dsp\containers + + JUCE Modules\juce_dsp\containers + JUCE Modules\juce_dsp\containers diff --git a/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj b/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj index 55dc65aab8..6bff47b077 100644 --- a/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj +++ b/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj @@ -1109,6 +1109,9 @@ true + + true + true @@ -1388,6 +1391,9 @@ true + + true + true @@ -1397,6 +1403,9 @@ true + + true + true @@ -2887,6 +2896,7 @@ + @@ -3005,6 +3015,7 @@ + diff --git a/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj.filters b/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj.filters index cbab95c68b..1afe319803 100644 --- a/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj.filters +++ b/examples/DemoRunner/Builds/VisualStudio2017/DemoRunner_App.vcxproj.filters @@ -1576,6 +1576,9 @@ JUCE Modules\juce_core\maths + + JUCE Modules\juce_core\memory + JUCE Modules\juce_core\memory @@ -1879,6 +1882,9 @@ JUCE Modules\juce_dsp\containers + + JUCE Modules\juce_dsp\containers + JUCE Modules\juce_dsp\containers @@ -1888,6 +1894,9 @@ JUCE Modules\juce_dsp\frequency + + JUCE Modules\juce_dsp\frequency + JUCE Modules\juce_dsp\frequency @@ -4368,6 +4377,9 @@ JUCE Modules\juce_core\maths + + JUCE Modules\juce_core\memory + JUCE Modules\juce_core\memory @@ -4722,6 +4734,9 @@ JUCE Modules\juce_dsp\containers + + JUCE Modules\juce_dsp\containers + JUCE Modules\juce_dsp\containers diff --git a/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj b/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj index 2039a25a0d..83b3be2fed 100644 --- a/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj +++ b/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj @@ -1109,6 +1109,9 @@ true + + true + true @@ -1388,6 +1391,9 @@ true + + true + true @@ -1397,6 +1403,9 @@ true + + true + true @@ -2887,6 +2896,7 @@ + @@ -3005,6 +3015,7 @@ + diff --git a/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj.filters b/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj.filters index b9f848a1eb..8c076de43f 100644 --- a/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj.filters +++ b/examples/DemoRunner/Builds/VisualStudio2019/DemoRunner_App.vcxproj.filters @@ -1576,6 +1576,9 @@ JUCE Modules\juce_core\maths + + JUCE Modules\juce_core\memory + JUCE Modules\juce_core\memory @@ -1879,6 +1882,9 @@ JUCE Modules\juce_dsp\containers + + JUCE Modules\juce_dsp\containers + JUCE Modules\juce_dsp\containers @@ -1888,6 +1894,9 @@ JUCE Modules\juce_dsp\frequency + + JUCE Modules\juce_dsp\frequency + JUCE Modules\juce_dsp\frequency @@ -4368,6 +4377,9 @@ JUCE Modules\juce_core\maths + + JUCE Modules\juce_core\memory + JUCE Modules\juce_core\memory @@ -4722,6 +4734,9 @@ JUCE Modules\juce_dsp\containers + + JUCE Modules\juce_dsp\containers + JUCE Modules\juce_dsp\containers diff --git a/examples/Plugins/DSPModulePluginDemo.h b/examples/Plugins/DSPModulePluginDemo.h index 1c0f6e6ffb..874983c1d5 100644 --- a/examples/Plugins/DSPModulePluginDemo.h +++ b/examples/Plugins/DSPModulePluginDemo.h @@ -65,6 +65,9 @@ namespace ID PARAMETER_ID (distortionInGain) PARAMETER_ID (distortionCompGain) PARAMETER_ID (distortionMix) + PARAMETER_ID (convolutionCabEnabled) + PARAMETER_ID (convolutionReverbEnabled) + PARAMETER_ID (convolutionReverbMix) PARAMETER_ID (multiBandEnabled) PARAMETER_ID (multiBandFreq) PARAMETER_ID (multiBandLowVolume) @@ -146,23 +149,17 @@ class DspModulePluginDemo : public AudioProcessor, { public: DspModulePluginDemo() - { - apvts.state.addListener (this); - - forEach ([] (dsp::Gain& gain) { gain.setRampDurationSeconds (0.05); }, - dsp::get (chain), - dsp::get (chain)); - - dsp::get (chain).setRule (dsp::PannerRule::linear); - } + : DspModulePluginDemo (AudioProcessorValueTreeState::ParameterLayout{}) {} //============================================================================== void prepareToPlay (double sampleRate, int samplesPerBlock) override { - if (jmin (getTotalNumInputChannels(), getTotalNumOutputChannels()) == 0) + const auto channels = jmax (getTotalNumInputChannels(), getTotalNumOutputChannels()); + + if (channels == 0) return; - chain.prepare ({ sampleRate, (uint32) samplesPerBlock, (uint32) getTotalNumOutputChannels() }); + chain.prepare ({ sampleRate, (uint32) samplesPerBlock, (uint32) channels }); reset(); } @@ -177,7 +174,7 @@ public: void processBlock (AudioBuffer& buffer, MidiBuffer&) override { - if (jmin (getTotalNumInputChannels(), getTotalNumOutputChannels()) == 0) + if (jmax (getTotalNumInputChannels(), getTotalNumOutputChannels()) == 0) return; ScopedNoDenormals noDenormals; @@ -185,9 +182,14 @@ public: if (requiresUpdate.load()) update(); + irSize = dsp::get (chain).reverb.getCurrentIRSize(); + const auto totalNumInputChannels = getTotalNumInputChannels(); const auto totalNumOutputChannels = getTotalNumOutputChannels(); + setLatencySamples (dsp::get (chain).getLatency() + + (dsp::isBypassed (chain) ? 0 : roundToInt (dsp::get (chain).getLatency()))); + const auto numChannels = jmax (totalNumInputChannels, totalNumOutputChannels); auto inoutBlock = dsp::AudioBlock (buffer).getSubsetChannelBlock (0, (size_t) numChannels); @@ -228,279 +230,702 @@ public: apvts.replaceState (ValueTree::fromXml (*getXmlFromBinary (data, sizeInBytes))); } - //============================================================================== - AudioProcessorValueTreeState apvts { *this, nullptr, "state", createParameters() }; + int getCurrentIRSize() const { return irSize; } + using Parameter = AudioProcessorValueTreeState::Parameter; + + // This struct holds references to the raw parameters, so that we don't have to search + // the APVTS (involving string comparisons and map lookups!) every time a parameter + // changes. + struct ParameterReferences + { + template + static Param& addToLayout (AudioProcessorValueTreeState::ParameterLayout& layout, + std::unique_ptr param) + { + auto& ref = *param; + layout.add (std::move (param)); + return ref; + } + + static String valueToTextFunction (float x) { return String (x, 2); } + static float textToValueFunction (const String& str) { return str.getFloatValue(); } + + static String valueToTextPanFunction (float x) { return getPanningTextForValue ((x + 100.0f) / 200.0f); } + static float textToValuePanFunction (const String& str) { return getPanningValueForText (str) * 200.0f - 100.0f; } + + // Creates parameters, adds them to the layout, and stores references to the parameters + // in this struct. + explicit ParameterReferences (AudioProcessorValueTreeState::ParameterLayout& layout) + : inputGain (addToLayout (layout, + std::make_unique (ID::inputGain, + "Input", + "dB", + NormalisableRange (-40.0f, 40.0f), + 0.0f, + valueToTextFunction, + textToValueFunction))), + outputGain (addToLayout (layout, + std::make_unique (ID::outputGain, + "Output", + "dB", + NormalisableRange (-40.0f, 40.0f), + 0.0f, + valueToTextFunction, + textToValueFunction))), + pan (addToLayout (layout, + std::make_unique (ID::pan, + "Panning", + "", + NormalisableRange (-100.0f, 100.0f), + 0.0f, + valueToTextPanFunction, + textToValuePanFunction))), + distortionEnabled (addToLayout (layout, + std::make_unique (ID::distortionEnabled, + "Distortion", + true, + ""))), + distortionType (addToLayout (layout, + std::make_unique (ID::distortionType, + "Waveshaper", + StringArray { "std::tanh", "Approx. tanh" }, + 0))), + distortionInGain (addToLayout (layout, + std::make_unique (ID::distortionInGain, + "Gain", + "dB", + NormalisableRange (-40.0f, 40.0f), + 0.0f, + valueToTextFunction, + textToValueFunction))), + distortionLowpass (addToLayout (layout, + std::make_unique (ID::distortionLowpass, + "Post Low-pass", + "Hz", + NormalisableRange (20.0f, 22000.0f, 0.0f, 0.25f), + 22000.0f, + valueToTextFunction, + textToValueFunction))), + distortionHighpass (addToLayout (layout, + std::make_unique (ID::distortionHighpass, + "Pre High-pass", + "Hz", + NormalisableRange (20.0f, 22000.0f, 0.0f, 0.25f), + 20.0f, + valueToTextFunction, + textToValueFunction))), + distortionCompGain (addToLayout (layout, + std::make_unique (ID::distortionCompGain, + "Compensat.", + "dB", + NormalisableRange (-40.0f, 40.0f), + 0.0f, + valueToTextFunction, + textToValueFunction))), + distortionMix (addToLayout (layout, + std::make_unique (ID::distortionMix, + "Mix", + "%", + NormalisableRange (0.0f, 100.0f), + 100.0f, + valueToTextFunction, + textToValueFunction))), + distortionOversampler (addToLayout (layout, + std::make_unique (ID::distortionOversampler, + "Oversampling", + StringArray { "2X", "4X", "8X", "2X compensated", "4X compensated", "8X compensated" }, + 1))), + multiBandEnabled (addToLayout (layout, + std::make_unique (ID::multiBandEnabled, + "Multi-band", + false, + ""))), + multiBandFreq (addToLayout (layout, + std::make_unique (ID::multiBandFreq, + "Sep. Freq.", + "Hz", + NormalisableRange (20.0f, 22000.0f, 0.0f, 0.25f), + 2000.0f, + valueToTextFunction, + textToValueFunction))), + multiBandLowVolume (addToLayout (layout, + std::make_unique (ID::multiBandLowVolume, + "Low volume", + "dB", + NormalisableRange (-40.0f, 40.0f), + 0.0f, + valueToTextFunction, + textToValueFunction))), + multiBandHighVolume (addToLayout (layout, + std::make_unique (ID::multiBandHighVolume, + "High volume", + "dB", + NormalisableRange (-40.0f, 40.0f), + 0.0f, + valueToTextFunction, + textToValueFunction))), + convolutionCabEnabled (addToLayout (layout, + std::make_unique (ID::convolutionCabEnabled, + "Cabinet", + false, + ""))), + convolutionReverbEnabled (addToLayout (layout, + std::make_unique (ID::convolutionReverbEnabled, + "Reverb", + false, + ""))), + convolutionReverbMix (addToLayout (layout, + std::make_unique (ID::convolutionReverbMix, + "Reverb Mix", + "%", + NormalisableRange (0.0f, 100.0f), + 50.0f, + valueToTextFunction, + textToValueFunction))), + compressorEnabled (addToLayout (layout, + std::make_unique (ID::compressorEnabled, + "Comp.", + false, + ""))), + compressorThreshold (addToLayout (layout, + std::make_unique (ID::compressorThreshold, + "Threshold", + "dB", + NormalisableRange (-100.0f, 0.0f), + 0.0f, + valueToTextFunction, + textToValueFunction))), + compressorRatio (addToLayout (layout, + std::make_unique (ID::compressorRatio, + "Ratio", + ":1", + NormalisableRange (1.0f, 100.0f, 0.0f, 0.25f), + 1.0f, + valueToTextFunction, + textToValueFunction))), + compressorAttack (addToLayout (layout, + std::make_unique (ID::compressorAttack, + "Attack", + "ms", + NormalisableRange (0.01f, 1000.0f, 0.0f, 0.25f), + 1.0f, + valueToTextFunction, + textToValueFunction))), + compressorRelease (addToLayout (layout, + std::make_unique (ID::compressorRelease, + "Release", + "ms", + NormalisableRange (10.0f, 10000.0f, 0.0f, 0.25f), + 100.0f, + valueToTextFunction, + textToValueFunction))), + noiseGateEnabled (addToLayout (layout, + std::make_unique (ID::noiseGateEnabled, + "Gate", + false, + ""))), + noiseGateThreshold (addToLayout (layout, + std::make_unique (ID::noiseGateThreshold, + "Threshold", + "dB", + NormalisableRange (-100.0f, 0.0f), + -100.0f, + valueToTextFunction, + textToValueFunction))), + noiseGateRatio (addToLayout (layout, + std::make_unique (ID::noiseGateRatio, + "Ratio", + ":1", + NormalisableRange (1.0f, 100.0f, 0.0f, 0.25f), + 10.0f, + valueToTextFunction, + textToValueFunction))), + noiseGateAttack (addToLayout (layout, + std::make_unique (ID::noiseGateAttack, + "Attack", + "ms", + NormalisableRange (0.01f, 1000.0f, 0.0f, 0.25f), + 1.0f, + valueToTextFunction, + textToValueFunction))), + noiseGateRelease (addToLayout (layout, + std::make_unique (ID::noiseGateRelease, + "Release", + "ms", + NormalisableRange (10.0f, 10000.0f, 0.0f, 0.25f), + 100.0f, + valueToTextFunction, + textToValueFunction))), + limiterEnabled (addToLayout (layout, + std::make_unique (ID::limiterEnabled, + "Limiter", + false, + ""))), + limiterThreshold (addToLayout (layout, + std::make_unique (ID::limiterThreshold, + "Threshold", + "dB", + NormalisableRange (-40.0f, 0.0f), + 0.0f, + valueToTextFunction, + textToValueFunction))), + limiterRelease (addToLayout (layout, + std::make_unique (ID::limiterRelease, + "Release", + "ms", + NormalisableRange (10.0f, 10000.0f, 0.0f, 0.25f), + 100.0f, + valueToTextFunction, + textToValueFunction))), + directDelayEnabled (addToLayout (layout, + std::make_unique (ID::directDelayEnabled, + "DL Dir.", + false, + ""))), + directDelayType (addToLayout (layout, + std::make_unique (ID::directDelayType, + "DL Type", + StringArray { "None", "Linear", "Lagrange", "Thiran" }, + 1))), + directDelayValue (addToLayout (layout, + std::make_unique (ID::directDelayValue, + "Delay", + "smps", + NormalisableRange (0.0f, 44100.0f), + 0.0f, + valueToTextFunction, + textToValueFunction))), + directDelaySmoothing (addToLayout (layout, + std::make_unique (ID::directDelaySmoothing, + "Smooth", + "ms", + NormalisableRange (20.0f, 10000.0f, 0.0f, 0.25f), + 200.0f, + valueToTextFunction, + textToValueFunction))), + directDelayMix (addToLayout (layout, + std::make_unique (ID::directDelayMix, + "Delay Mix", + "%", + NormalisableRange (0.0f, 100.0f), + 50.0f, + valueToTextFunction, + textToValueFunction))), + delayEffectEnabled (addToLayout (layout, + std::make_unique (ID::delayEffectEnabled, + "DL Effect", + false, + ""))), + delayEffectType (addToLayout (layout, + std::make_unique (ID::delayEffectType, + "DL Type", + StringArray { "None", "Linear", "Lagrange", "Thiran" }, + 1))), + delayEffectValue (addToLayout (layout, + std::make_unique (ID::delayEffectValue, + "Delay", + "ms", + NormalisableRange (0.01f, 1000.0f), + 100.0f, + valueToTextFunction, + textToValueFunction))), + delayEffectSmoothing (addToLayout (layout, + std::make_unique (ID::delayEffectSmoothing, + "Smooth", + "ms", + NormalisableRange (20.0f, 10000.0f, 0.0f, 0.25f), + 400.0f, + valueToTextFunction, + textToValueFunction))), + delayEffectLowpass (addToLayout (layout, + std::make_unique (ID::delayEffectLowpass, + "Low-pass", + "Hz", + NormalisableRange (20.0f, 22000.0f, 0.0f, 0.25f), + 22000.0f, + valueToTextFunction, + textToValueFunction))), + delayEffectMix (addToLayout (layout, + std::make_unique (ID::delayEffectMix, + "Delay Mix", + "%", + NormalisableRange (0.0f, 100.0f), + 50.0f, + valueToTextFunction, + textToValueFunction))), + delayEffectFeedback (addToLayout (layout, + std::make_unique (ID::delayEffectFeedback, + "Feedback", + "dB", + NormalisableRange (-100.0f, 0.0f), + -100.0f, + valueToTextFunction, + textToValueFunction))), + phaserEnabled (addToLayout (layout, + std::make_unique (ID::phaserEnabled, + "Phaser", + false, + ""))), + phaserRate (addToLayout (layout, + std::make_unique (ID::phaserRate, + "Rate", + "Hz", + NormalisableRange (0.05f, 20.0f, 0.0f, 0.25f), + 1.0f, + valueToTextFunction, + textToValueFunction))), + phaserDepth (addToLayout (layout, + std::make_unique (ID::phaserDepth, + "Depth", + "%", + NormalisableRange (0.0f, 100.0f), + 50.0f, + valueToTextFunction, + textToValueFunction))), + phaserCentreFrequency (addToLayout (layout, + std::make_unique (ID::phaserCentreFrequency, + "Center", + "Hz", + NormalisableRange (20.0f, 20000.0f, 0.0f, 0.25f), + 600.0f, + valueToTextFunction, + textToValueFunction))), + phaserFeedback (addToLayout (layout, + std::make_unique (ID::phaserFeedback, + "Feedback", + "%", + NormalisableRange (0.0f, 100.0f), + 50.0f, + valueToTextFunction, + textToValueFunction))), + phaserMix (addToLayout (layout, + std::make_unique (ID::phaserMix, + "Mix", + "%", + NormalisableRange (0.0f, 100.0f), + 50.0f, + valueToTextFunction, + textToValueFunction))), + chorusEnabled (addToLayout (layout, + std::make_unique (ID::chorusEnabled, + "Chorus", + false, + ""))), + chorusRate (addToLayout (layout, + std::make_unique (ID::chorusRate, + "Rate", + "Hz", + NormalisableRange (0.05f, 20.0f, 0.0f, 0.25f), + 1.0f, + valueToTextFunction, + textToValueFunction))), + chorusDepth (addToLayout (layout, + std::make_unique (ID::chorusDepth, + "Depth", + "%", + NormalisableRange (0.0f, 100.0f), + 50.0f, + valueToTextFunction, + textToValueFunction))), + chorusCentreDelay (addToLayout (layout, + std::make_unique (ID::chorusCentreDelay, + "Center", + "ms", + NormalisableRange (1.0f, 100.0f, 0.0f, 0.25f), + 7.0f, + valueToTextFunction, + textToValueFunction))), + chorusFeedback (addToLayout (layout, + std::make_unique (ID::chorusFeedback, + "Feedback", + "%", + NormalisableRange (0.0f, 100.0f), + 50.0f, + valueToTextFunction, + textToValueFunction))), + chorusMix (addToLayout (layout, + std::make_unique (ID::chorusMix, + "Mix", + "%", + NormalisableRange (0.0f, 100.0f), + 50.0f, + valueToTextFunction, + textToValueFunction))), + ladderEnabled (addToLayout (layout, + std::make_unique (ID::ladderEnabled, + "Ladder", + false, + ""))), + ladderMode (addToLayout (layout, + std::make_unique (ID::ladderMode, + "Mode", + StringArray { "LP12", "LP24", "HP12", "HP24", "BP12", "BP24" }, + 1))), + ladderCutoff (addToLayout (layout, + std::make_unique (ID::ladderCutoff, + "Frequency", + "Hz", + NormalisableRange (10.0f, 22000.0f, 0.0f, 0.25f), + 1000.0f, + valueToTextFunction, + textToValueFunction))), + ladderResonance (addToLayout (layout, + std::make_unique (ID::ladderResonance, + "Resonance", + "%", + NormalisableRange (0.0f, 100.0f), + 0.0f, + valueToTextFunction, + textToValueFunction))), + ladderDrive (addToLayout (layout, + std::make_unique (ID::ladderDrive, + "Drive", + "dB", + NormalisableRange (0.0f, 40.0f), + 0.0f, + valueToTextFunction, + textToValueFunction))) + {} + + Parameter& inputGain; + Parameter& outputGain; + Parameter& pan; + + AudioParameterBool& distortionEnabled; + AudioParameterChoice& distortionType; + Parameter& distortionInGain; + Parameter& distortionLowpass; + Parameter& distortionHighpass; + Parameter& distortionCompGain; + Parameter& distortionMix; + AudioParameterChoice& distortionOversampler; + + AudioParameterBool& multiBandEnabled; + Parameter& multiBandFreq; + Parameter& multiBandLowVolume; + Parameter& multiBandHighVolume; + + AudioParameterBool& convolutionCabEnabled; + AudioParameterBool& convolutionReverbEnabled; + Parameter& convolutionReverbMix; + + AudioParameterBool& compressorEnabled; + Parameter& compressorThreshold; + Parameter& compressorRatio; + Parameter& compressorAttack; + Parameter& compressorRelease; + + AudioParameterBool& noiseGateEnabled; + Parameter& noiseGateThreshold; + Parameter& noiseGateRatio; + Parameter& noiseGateAttack; + Parameter& noiseGateRelease; + + AudioParameterBool& limiterEnabled; + Parameter& limiterThreshold; + Parameter& limiterRelease; + + AudioParameterBool& directDelayEnabled; + AudioParameterChoice& directDelayType; + Parameter& directDelayValue; + Parameter& directDelaySmoothing; + Parameter& directDelayMix; + + AudioParameterBool& delayEffectEnabled; + AudioParameterChoice& delayEffectType; + Parameter& delayEffectValue; + Parameter& delayEffectSmoothing; + Parameter& delayEffectLowpass; + Parameter& delayEffectMix; + Parameter& delayEffectFeedback; + + AudioParameterBool& phaserEnabled; + Parameter& phaserRate; + Parameter& phaserDepth; + Parameter& phaserCentreFrequency; + Parameter& phaserFeedback; + Parameter& phaserMix; + + AudioParameterBool& chorusEnabled; + Parameter& chorusRate; + Parameter& chorusDepth; + Parameter& chorusCentreDelay; + Parameter& chorusFeedback; + Parameter& chorusMix; + + AudioParameterBool& ladderEnabled; + AudioParameterChoice& ladderMode; + Parameter& ladderCutoff; + Parameter& ladderResonance; + Parameter& ladderDrive; + }; + + const ParameterReferences& getParameterValues() const noexcept { return parameters; } + + //============================================================================== // We store this here so that the editor retains its state if it is closed and reopened int indexTab = 0; private: + struct LayoutAndReferences + { + AudioProcessorValueTreeState::ParameterLayout layout; + ParameterReferences references; + }; + + explicit DspModulePluginDemo (AudioProcessorValueTreeState::ParameterLayout layout) + : AudioProcessor (BusesProperties().withInput ("In", AudioChannelSet::stereo()) + .withOutput ("Out", AudioChannelSet::stereo())), + parameters { layout }, + apvts { *this, nullptr, "state", std::move (layout) } + { + apvts.state.addListener (this); + + forEach ([] (dsp::Gain& gain) { gain.setRampDurationSeconds (0.05); }, + dsp::get (chain), + dsp::get (chain)); + + dsp::get (chain).setRule (dsp::PannerRule::linear); + } + //============================================================================== void valueTreePropertyChanged (ValueTree&, const Identifier&) override { requiresUpdate.store (true); } - // This struct holds references to the raw parameter values, so that we don't have to look up - // the parameters (involving string comparisons and map lookups!) every time a parameter - // changes. - struct ParameterValues - { - explicit ParameterValues (AudioProcessorValueTreeState& state) - : inputGain (*state.getRawParameterValue (ID::inputGain)), - outputGain (*state.getRawParameterValue (ID::outputGain)), - pan (*state.getRawParameterValue (ID::pan)), - distortionEnabled (*state.getRawParameterValue (ID::distortionEnabled)), - distortionType (*state.getRawParameterValue (ID::distortionType)), - distortionOversampler (*state.getRawParameterValue (ID::distortionOversampler)), - distortionLowpass (*state.getRawParameterValue (ID::distortionLowpass)), - distortionHighpass (*state.getRawParameterValue (ID::distortionHighpass)), - distortionInGain (*state.getRawParameterValue (ID::distortionInGain)), - distortionCompGain (*state.getRawParameterValue (ID::distortionCompGain)), - distortionMix (*state.getRawParameterValue (ID::distortionMix)), - multiBandEnabled (*state.getRawParameterValue (ID::multiBandEnabled)), - multiBandFreq (*state.getRawParameterValue (ID::multiBandFreq)), - multiBandLowVolume (*state.getRawParameterValue (ID::multiBandLowVolume)), - multiBandHighVolume (*state.getRawParameterValue (ID::multiBandHighVolume)), - compressorEnabled (*state.getRawParameterValue (ID::compressorEnabled)), - compressorThreshold (*state.getRawParameterValue (ID::compressorThreshold)), - compressorRatio (*state.getRawParameterValue (ID::compressorRatio)), - compressorAttack (*state.getRawParameterValue (ID::compressorAttack)), - compressorRelease (*state.getRawParameterValue (ID::compressorRelease)), - noiseGateEnabled (*state.getRawParameterValue (ID::noiseGateEnabled)), - noiseGateThreshold (*state.getRawParameterValue (ID::noiseGateThreshold)), - noiseGateRatio (*state.getRawParameterValue (ID::noiseGateRatio)), - noiseGateAttack (*state.getRawParameterValue (ID::noiseGateAttack)), - noiseGateRelease (*state.getRawParameterValue (ID::noiseGateRelease)), - limiterEnabled (*state.getRawParameterValue (ID::limiterEnabled)), - limiterThreshold (*state.getRawParameterValue (ID::limiterThreshold)), - limiterRelease (*state.getRawParameterValue (ID::limiterRelease)), - directDelayEnabled (*state.getRawParameterValue (ID::directDelayEnabled)), - directDelayType (*state.getRawParameterValue (ID::directDelayType)), - directDelayValue (*state.getRawParameterValue (ID::directDelayValue)), - directDelaySmoothing (*state.getRawParameterValue (ID::directDelaySmoothing)), - directDelayMix (*state.getRawParameterValue (ID::directDelayMix)), - delayEffectEnabled (*state.getRawParameterValue (ID::delayEffectEnabled)), - delayEffectType (*state.getRawParameterValue (ID::delayEffectType)), - delayEffectValue (*state.getRawParameterValue (ID::delayEffectValue)), - delayEffectSmoothing (*state.getRawParameterValue (ID::delayEffectSmoothing)), - delayEffectLowpass (*state.getRawParameterValue (ID::delayEffectLowpass)), - delayEffectFeedback (*state.getRawParameterValue (ID::delayEffectFeedback)), - delayEffectMix (*state.getRawParameterValue (ID::delayEffectMix)), - phaserEnabled (*state.getRawParameterValue (ID::phaserEnabled)), - phaserRate (*state.getRawParameterValue (ID::phaserRate)), - phaserDepth (*state.getRawParameterValue (ID::phaserDepth)), - phaserCentreFrequency (*state.getRawParameterValue (ID::phaserCentreFrequency)), - phaserFeedback (*state.getRawParameterValue (ID::phaserFeedback)), - phaserMix (*state.getRawParameterValue (ID::phaserMix)), - chorusEnabled (*state.getRawParameterValue (ID::chorusEnabled)), - chorusRate (*state.getRawParameterValue (ID::chorusRate)), - chorusDepth (*state.getRawParameterValue (ID::chorusDepth)), - chorusCentreDelay (*state.getRawParameterValue (ID::chorusCentreDelay)), - chorusFeedback (*state.getRawParameterValue (ID::chorusFeedback)), - chorusMix (*state.getRawParameterValue (ID::chorusMix)), - ladderEnabled (*state.getRawParameterValue (ID::ladderEnabled)), - ladderCutoff (*state.getRawParameterValue (ID::ladderCutoff)), - ladderResonance (*state.getRawParameterValue (ID::ladderResonance)), - ladderDrive (*state.getRawParameterValue (ID::ladderDrive)), - ladderMode (*state.getRawParameterValue (ID::ladderMode)) - {} - - std::atomic& inputGain; - std::atomic& outputGain; - std::atomic& pan; - - std::atomic& distortionEnabled; - std::atomic& distortionType; - std::atomic& distortionOversampler; - std::atomic& distortionLowpass; - std::atomic& distortionHighpass; - std::atomic& distortionInGain; - std::atomic& distortionCompGain; - std::atomic& distortionMix; - - std::atomic& multiBandEnabled; - std::atomic& multiBandFreq; - std::atomic& multiBandLowVolume; - std::atomic& multiBandHighVolume; - - std::atomic& compressorEnabled; - std::atomic& compressorThreshold; - std::atomic& compressorRatio; - std::atomic& compressorAttack; - std::atomic& compressorRelease; - - std::atomic& noiseGateEnabled; - std::atomic& noiseGateThreshold; - std::atomic& noiseGateRatio; - std::atomic& noiseGateAttack; - std::atomic& noiseGateRelease; - - std::atomic& limiterEnabled; - std::atomic& limiterThreshold; - std::atomic& limiterRelease; - - std::atomic& directDelayEnabled; - std::atomic& directDelayType; - std::atomic& directDelayValue; - std::atomic& directDelaySmoothing; - std::atomic& directDelayMix; - - std::atomic& delayEffectEnabled; - std::atomic& delayEffectType; - std::atomic& delayEffectValue; - std::atomic& delayEffectSmoothing; - std::atomic& delayEffectLowpass; - std::atomic& delayEffectFeedback; - std::atomic& delayEffectMix; - - std::atomic& phaserEnabled; - std::atomic& phaserRate; - std::atomic& phaserDepth; - std::atomic& phaserCentreFrequency; - std::atomic& phaserFeedback; - std::atomic& phaserMix; - - std::atomic& chorusEnabled; - std::atomic& chorusRate; - std::atomic& chorusDepth; - std::atomic& chorusCentreDelay; - std::atomic& chorusFeedback; - std::atomic& chorusMix; - - std::atomic& ladderEnabled; - std::atomic& ladderCutoff; - std::atomic& ladderResonance; - std::atomic& ladderDrive; - std::atomic& ladderMode; - }; - - ParameterValues parameters { apvts }; - //============================================================================== void update() { { DistortionProcessor& distortion = dsp::get (chain); - if (distortion.currentIndexOversampling != parameters.distortionOversampler.load()) + if (distortion.currentIndexOversampling != parameters.distortionOversampler.getIndex()) { - distortion.currentIndexOversampling = roundToInt (parameters.distortionOversampler.load()); + distortion.currentIndexOversampling = parameters.distortionOversampler.getIndex(); prepareToPlay (getSampleRate(), getBlockSize()); return; } - distortion.currentIndexWaveshaper = roundToInt (parameters.distortionType.load()); - distortion.lowpass .setCutoffFrequency (parameters.distortionLowpass); - distortion.highpass.setCutoffFrequency (parameters.distortionHighpass); - distortion.distGain.setGainDecibels (parameters.distortionInGain); - distortion.compGain.setGainDecibels (parameters.distortionCompGain); - distortion.mixer.setWetMixProportion (parameters.distortionMix / 100.0f); - dsp::setBypassed (chain, parameters.distortionEnabled.load() == 0.0f); + distortion.currentIndexWaveshaper = parameters.distortionType.getIndex(); + distortion.lowpass .setCutoffFrequency (parameters.distortionLowpass.get()); + distortion.highpass.setCutoffFrequency (parameters.distortionHighpass.get()); + distortion.distGain.setGainDecibels (parameters.distortionInGain.get()); + distortion.compGain.setGainDecibels (parameters.distortionCompGain.get()); + distortion.mixer.setWetMixProportion (parameters.distortionMix.get() / 100.0f); + dsp::setBypassed (chain, ! parameters.distortionEnabled); } - dsp::get (chain).setGainDecibels (parameters.inputGain); - dsp::get (chain).setGainDecibels (parameters.outputGain); - dsp::get (chain).setPan (parameters.pan / 100.0f); + { + ConvolutionProcessor& convolution = dsp::get (chain); + convolution.cabEnabled = parameters.convolutionCabEnabled; + convolution.reverbEnabled = parameters.convolutionReverbEnabled; + convolution.mixer.setWetMixProportion (parameters.convolutionReverbMix.get() / 100.0f); + } + + dsp::get (chain).setGainDecibels (parameters.inputGain.get()); + dsp::get (chain).setGainDecibels (parameters.outputGain.get()); + dsp::get (chain).setPan (parameters.pan.get() / 100.0f); { MultiBandProcessor& multiband = dsp::get (chain); - const auto multibandFreq = parameters.multiBandFreq.load(); + const auto multibandFreq = parameters.multiBandFreq.get(); multiband.lowpass .setCutoffFrequency (multibandFreq); multiband.highpass.setCutoffFrequency (multibandFreq); - const auto enabled = parameters.multiBandEnabled.load() != 0.0f; - multiband.lowVolume .setGainDecibels (enabled ? parameters.multiBandLowVolume .load() : 0.0f); - multiband.highVolume.setGainDecibels (enabled ? parameters.multiBandHighVolume.load() : 0.0f); + const bool enabled = parameters.multiBandEnabled; + multiband.lowVolume .setGainDecibels (enabled ? parameters.multiBandLowVolume .get() : 0.0f); + multiband.highVolume.setGainDecibels (enabled ? parameters.multiBandHighVolume.get() : 0.0f); dsp::setBypassed (chain, ! enabled); } { dsp::Compressor& compressor = dsp::get (chain); - compressor.setThreshold (parameters.compressorThreshold); - compressor.setRatio (parameters.compressorRatio); - compressor.setAttack (parameters.compressorAttack); - compressor.setRelease (parameters.compressorRelease); - dsp::setBypassed (chain, parameters.compressorEnabled.load() == 0.0f); + compressor.setThreshold (parameters.compressorThreshold.get()); + compressor.setRatio (parameters.compressorRatio.get()); + compressor.setAttack (parameters.compressorAttack.get()); + compressor.setRelease (parameters.compressorRelease.get()); + dsp::setBypassed (chain, ! parameters.compressorEnabled); } { dsp::NoiseGate& noiseGate = dsp::get (chain); - noiseGate.setThreshold (parameters.noiseGateThreshold); - noiseGate.setRatio (parameters.noiseGateRatio); - noiseGate.setAttack (parameters.noiseGateAttack); - noiseGate.setRelease (parameters.noiseGateRelease); - dsp::setBypassed (chain, parameters.noiseGateEnabled.load() == 0.0f); + noiseGate.setThreshold (parameters.noiseGateThreshold.get()); + noiseGate.setRatio (parameters.noiseGateRatio.get()); + noiseGate.setAttack (parameters.noiseGateAttack.get()); + noiseGate.setRelease (parameters.noiseGateRelease.get()); + dsp::setBypassed (chain, ! parameters.noiseGateEnabled); } { dsp::Limiter& limiter = dsp::get (chain); - limiter.setThreshold (parameters.limiterThreshold); - limiter.setRelease (parameters.limiterRelease); - dsp::setBypassed (chain, parameters.limiterEnabled.load() == 0.0f); + limiter.setThreshold (parameters.limiterThreshold.get()); + limiter.setRelease (parameters.limiterRelease.get()); + dsp::setBypassed (chain, ! parameters.limiterEnabled); } { DirectDelayProcessor& delay = dsp::get (chain); - delay.delayLineDirectType = roundToInt (parameters.directDelayType.load()); + delay.delayLineDirectType = parameters.directDelayType.getIndex(); std::fill (delay.delayDirectValue.begin(), delay.delayDirectValue.end(), - (double) parameters.directDelayValue); + (double) parameters.directDelayValue.get()); - delay.smoothFilter.setCutoffFrequency (1000.0 / parameters.directDelaySmoothing); - delay.mixer.setWetMixProportion (parameters.directDelayMix / 100.0f); - dsp::setBypassed (chain, parameters.directDelayEnabled.load() == 0.0f); + delay.smoothFilter.setCutoffFrequency (1000.0 / parameters.directDelaySmoothing.get()); + delay.mixer.setWetMixProportion (parameters.directDelayMix.get() / 100.0f); + dsp::setBypassed (chain, ! parameters.directDelayEnabled); } { DelayEffectProcessor& delay = dsp::get (chain); - delay.delayEffectType = roundToInt (parameters.delayEffectType.load()); + delay.delayEffectType = parameters.delayEffectType.getIndex(); std::fill (delay.delayEffectValue.begin(), delay.delayEffectValue.end(), - (double) parameters.delayEffectValue / 1000.0 * getSampleRate()); + (double) parameters.delayEffectValue.get() / 1000.0 * getSampleRate()); - const auto feedbackGain = Decibels::decibelsToGain (parameters.delayEffectFeedback.load(), -100.0f); + const auto feedbackGain = Decibels::decibelsToGain (parameters.delayEffectFeedback.get(), -100.0f); for (auto& volume : delay.delayFeedbackVolume) volume.setTargetValue (feedbackGain); - delay.smoothFilter.setCutoffFrequency (1000.0 / parameters.delayEffectSmoothing); - delay.lowpass.setCutoffFrequency (parameters.delayEffectLowpass); - delay.mixer.setWetMixProportion (parameters.delayEffectMix / 100.0f); - dsp::setBypassed (chain, parameters.delayEffectEnabled.load() == 0.0f); + delay.smoothFilter.setCutoffFrequency (1000.0 / parameters.delayEffectSmoothing.get()); + delay.lowpass.setCutoffFrequency (parameters.delayEffectLowpass.get()); + delay.mixer.setWetMixProportion (parameters.delayEffectMix.get() / 100.0f); + dsp::setBypassed (chain, ! parameters.delayEffectEnabled); } { dsp::Phaser& phaser = dsp::get (chain); - phaser.setRate (parameters.phaserRate); - phaser.setDepth (parameters.phaserDepth / 100.0f); - phaser.setCentreFrequency (parameters.phaserCentreFrequency); - phaser.setFeedback (parameters.phaserFeedback / 100.0f * 0.95f); - phaser.setMix (parameters.phaserMix / 100.0f); - dsp::setBypassed (chain, parameters.phaserEnabled.load() == 0.0f); + phaser.setRate (parameters.phaserRate.get()); + phaser.setDepth (parameters.phaserDepth.get() / 100.0f); + phaser.setCentreFrequency (parameters.phaserCentreFrequency.get()); + phaser.setFeedback (parameters.phaserFeedback.get() / 100.0f * 0.95f); + phaser.setMix (parameters.phaserMix.get() / 100.0f); + dsp::setBypassed (chain, ! parameters.phaserEnabled); } { dsp::Chorus& chorus = dsp::get (chain); - chorus.setRate (parameters.chorusRate); - chorus.setDepth (parameters.chorusDepth / 100.0f); - chorus.setCentreDelay (parameters.chorusCentreDelay); - chorus.setFeedback (parameters.chorusFeedback / 100.0f * 0.95f); - chorus.setMix (parameters.chorusMix / 100.0f); - dsp::setBypassed (chain, parameters.chorusEnabled.load() == 0.0f); + chorus.setRate (parameters.chorusRate.get()); + chorus.setDepth (parameters.chorusDepth.get() / 100.0f); + chorus.setCentreDelay (parameters.chorusCentreDelay.get()); + chorus.setFeedback (parameters.chorusFeedback.get() / 100.0f * 0.95f); + chorus.setMix (parameters.chorusMix.get() / 100.0f); + dsp::setBypassed (chain, ! parameters.chorusEnabled); } { dsp::LadderFilter& ladder = dsp::get (chain); - ladder.setCutoffFrequencyHz (parameters.ladderCutoff); - ladder.setResonance (parameters.ladderResonance / 100.0f); - ladder.setDrive (Decibels::decibelsToGain (parameters.ladderDrive.load())); + ladder.setCutoffFrequencyHz (parameters.ladderCutoff.get()); + ladder.setResonance (parameters.ladderResonance.get() / 100.0f); + ladder.setDrive (Decibels::decibelsToGain (parameters.ladderDrive.get())); ladder.setMode ([&] { - switch (roundToInt (parameters.ladderMode.load())) + switch (parameters.ladderMode.getIndex()) { case 0: return dsp::LadderFilterMode::LPF12; case 1: return dsp::LadderFilterMode::LPF24; @@ -514,7 +939,7 @@ private: return dsp::LadderFilterMode::BPF24; }()); - dsp::setBypassed (chain, parameters.ladderEnabled.load() == 0.0f); + dsp::setBypassed (chain, ! parameters.ladderEnabled); } requiresUpdate.store (false); @@ -554,418 +979,6 @@ private: return 0.5f; } - static AudioProcessorValueTreeState::ParameterLayout createParameters() - { - using Parameter = AudioProcessorValueTreeState::Parameter; - - auto valueToTextFunction = [] (float x) { return String (x, 2); }; - auto textToValueFunction = [] (const String& str) { return str.getFloatValue(); }; - - auto valueToTextPanFunction = [] (float x) { return getPanningTextForValue ((x + 100.0f) / 200.0f); }; - auto textToValuePanFunction = [] (const String& str) { return getPanningValueForText (str) * 200.0f - 100.0f; }; - - AudioProcessorValueTreeState::ParameterLayout layout; - - layout.add (std::make_unique (ID::inputGain, - "Input", - "dB", - NormalisableRange (-40.0f, 40.0f), - 0.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::outputGain, - "Output", - "dB", - NormalisableRange (-40.0f, 40.0f), - 0.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::pan, - "Panning", - "", - NormalisableRange (-100.0f, 100.0f), - 0.0f, - valueToTextPanFunction, - textToValuePanFunction)); - - layout.add (std::make_unique (ID::distortionEnabled, "Distortion", true, "")); - - layout.add (std::make_unique (ID::distortionType, - "Waveshaper", - StringArray { "std::tanh", "Approx. tanh" }, - 0)); - - layout.add (std::make_unique (ID::distortionInGain, - "Gain", - "dB", - NormalisableRange (-40.0f, 40.0f), - 0.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::distortionLowpass, - "Post Low-pass", - "Hz", - NormalisableRange (20.0f, 22000.0f, 0.0f, 0.25f), - 22000.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::distortionHighpass, - "Pre High-pass", - "Hz", - NormalisableRange (20.0f, 22000.0f, 0.0f, 0.25f), - 20.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::distortionCompGain, - "Compensat.", - "dB", - NormalisableRange (-40.0f, 40.0f), - 0.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::distortionMix, - "Mix", - "%", - NormalisableRange (0.0f, 100.0f), - 100.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::distortionOversampler, - "Oversampling", - StringArray { "2X", - "4X", - "8X", - "2X compensated", - "4X compensated", - "8X compensated" }, - 1)); - - layout.add (std::make_unique (ID::multiBandEnabled, "Multi-band", false, "")); - - layout.add (std::make_unique (ID::multiBandFreq, - "Sep. Freq.", - "Hz", - NormalisableRange (20.0f, 22000.0f, 0.0f, 0.25f), - 2000.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::multiBandLowVolume, - "Low volume", - "dB", - NormalisableRange (-40.0f, 40.0f), - 0.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::multiBandHighVolume, - "High volume", - "dB", - NormalisableRange (-40.0f, 40.0f), - 0.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::compressorEnabled, "Comp.", false, "")); - - layout.add (std::make_unique (ID::compressorThreshold, - "Threshold", - "dB", - NormalisableRange (-100.0f, 0.0f), - 0.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::compressorRatio, - "Ratio", - ":1", - NormalisableRange (1.0f, 100.0f, 0.0f, 0.25f), - 1.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::compressorAttack, - "Attack", - "ms", - NormalisableRange (0.01f, 1000.0f, 0.0f, 0.25f), - 1.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::compressorRelease, - "Release", - "ms", - NormalisableRange (10.0f, 10000.0f, 0.0f, 0.25f), - 100.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::noiseGateEnabled, "Gate", false, "")); - - layout.add (std::make_unique (ID::noiseGateThreshold, - "Threshold", - "dB", - NormalisableRange (-100.0f, 0.0f), - -100.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::noiseGateRatio, - "Ratio", - ":1", - NormalisableRange (1.0f, 100.0f, 0.0f, 0.25f), - 10.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::noiseGateAttack, - "Attack", - "ms", - NormalisableRange (0.01f, 1000.0f, 0.0f, 0.25f), - 1.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::noiseGateRelease, - "Release", - "ms", - NormalisableRange (10.0f, 10000.0f, 0.0f, 0.25f), - 100.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::limiterEnabled, "Limiter", false, "")); - - layout.add (std::make_unique (ID::limiterThreshold, - "Threshold", - "dB", - NormalisableRange (-40.0f, 0.0f), - 0.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::limiterRelease, - "Release", - "ms", - NormalisableRange (10.0f, 10000.0f, 0.0f, 0.25f), - 100.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::directDelayEnabled, "DL Dir.", false, "")); - - layout.add (std::make_unique (ID::directDelayType, - "DL Type", - StringArray { "None", - "Linear", - "Lagrange", - "Thiran" }, - 1)); - - layout.add (std::make_unique (ID::directDelayValue, - "Delay", - "smps", - NormalisableRange (0.0f, 44100.0f), - 0.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::directDelaySmoothing, - "Smooth", - "ms", - NormalisableRange (20.0f, 10000.0f, 0.0f, 0.25f), - 200.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::directDelayMix, - "Delay Mix", - "%", - NormalisableRange (0.0f, 100.0f), - 50.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::delayEffectEnabled, "DL Effect", false, "")); - - layout.add (std::make_unique (ID::delayEffectType, - "DL Type", - StringArray { "None", - "Linear", - "Lagrange", - "Thiran" }, - 1)); - - layout.add (std::make_unique (ID::delayEffectValue, - "Delay", - "ms", - NormalisableRange (0.01f, 1000.0f), - 100.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::delayEffectSmoothing, - "Smooth", - "ms", - NormalisableRange (20.0f, 10000.0f, 0.0f, 0.25f), - 400.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::delayEffectLowpass, - "Low-pass", - "Hz", - NormalisableRange (20.0f, 22000.0f, 0.0f, 0.25f), - 22000.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::delayEffectMix, - "Delay Mix", - "%", - NormalisableRange (0.0f, 100.0f), - 50.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::delayEffectFeedback, - "Feedback", - "dB", - NormalisableRange (-100.0f, 0.0f), - -100.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::phaserEnabled, "Phaser", false, "")); - - layout.add (std::make_unique (ID::phaserRate, - "Rate", - "Hz", - NormalisableRange (0.05f, 20.0f, 0.0f, 0.25f), - 1.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::phaserDepth, - "Depth", - "%", - NormalisableRange (0.0f, 100.0f), - 50.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::phaserCentreFrequency, - "Center", - "Hz", - NormalisableRange (20.0f, 20000.0f, 0.0f, 0.25f), - 600.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::phaserFeedback, - "Feedback", - "%", - NormalisableRange (0.0f, 100.0f), - 50.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::phaserMix, - "Mix", - "%", - NormalisableRange (0.0f, 100.0f), - 50.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::chorusEnabled, "Chorus", false, "")); - - layout.add (std::make_unique (ID::chorusRate, - "Rate", - "Hz", - NormalisableRange (0.05f, 20.0f, 0.0f, 0.25f), - 1.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::chorusDepth, - "Depth", - "%", - NormalisableRange (0.0f, 100.0f), - 50.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::chorusCentreDelay, - "Center", - "ms", - NormalisableRange (1.0f, 100.0f, 0.0f, 0.25f), - 7.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::chorusFeedback, - "Feedback", - "%", - NormalisableRange (0.0f, 100.0f), - 50.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::chorusMix, - "Mix", - "%", - NormalisableRange (0.0f, 100.0f), - 50.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::ladderEnabled, "Ladder", false, "")); - - layout.add (std::make_unique (ID::ladderMode, - "Mode", - StringArray { "LP12", - "LP24", - "HP12", - "HP24", - "BP12", - "BP24" }, - 1)); - - layout.add (std::make_unique (ID::ladderCutoff, - "Frequency", - "Hz", - NormalisableRange (10.0f, 22000.0f, 0.0f, 0.25f), - 1000.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::ladderResonance, - "Resonance", - "%", - NormalisableRange (0.0f, 100.0f), - 0.0f, - valueToTextFunction, - textToValueFunction)); - - layout.add (std::make_unique (ID::ladderDrive, - "Drive", - "dB", - NormalisableRange (0.0f, 40.0f), - 0.0f, - valueToTextFunction, - textToValueFunction)); - - return layout; - } - //============================================================================== struct DistortionProcessor { @@ -1048,16 +1061,112 @@ private: { 2, 3, dsp::Oversampling::filterHalfBandPolyphaseIIR, true, true }, } }; + static float clip (float in) { return juce::jlimit (-1.0f, 1.0f, in); } + dsp::FirstOrderTPTFilter lowpass, highpass; dsp::Gain distGain, compGain; dsp::DryWetMixer mixer { 10 }; std::array, 2> waveShapers { { { std::tanh }, { dsp::FastMathApproximations::tanh } } }; - dsp::WaveShaper clipping; + dsp::WaveShaper clipping { clip }; int currentIndexOversampling = 0; int currentIndexWaveshaper = 0; }; + struct ConvolutionProcessor + { + ConvolutionProcessor() + { + loadImpulseResponse (cabinet, "guitar_amp.wav"); + loadImpulseResponse (reverb, "reverb_ir.wav"); + mixer.setMixingRule (dsp::DryWetMixingRule::balanced); + } + + void prepare (const dsp::ProcessSpec& spec) + { + prepareAll (spec, cabinet, reverb, mixer); + } + + void reset() + { + resetAll (cabinet, reverb, mixer); + } + + template + void process (Context& context) + { + auto contextConv = context; + contextConv.isBypassed = (! cabEnabled) || context.isBypassed; + cabinet.process (contextConv); + + if (cabEnabled) + context.getOutputBlock().multiplyBy (4.0f); + + if (reverbEnabled) + mixer.pushDrySamples (context.getInputBlock()); + + contextConv.isBypassed = (! reverbEnabled) || context.isBypassed; + reverb.process (contextConv); + + if (reverbEnabled) + { + const auto& outputBlock = context.getOutputBlock(); + outputBlock.multiplyBy (4.0f); + mixer.mixWetSamples (outputBlock); + } + } + + int getLatency() const + { + auto latency = 0; + + if (cabEnabled) + latency += cabinet.getLatency(); + + if (reverbEnabled) + latency += reverb.getLatency(); + + return latency; + } + + dsp::Convolution cabinet { dsp::Convolution::NonUniform { 512 } }; + dsp::Convolution reverb { dsp::Convolution::NonUniform { 512 } }; + dsp::DryWetMixer mixer; + bool cabEnabled = false, reverbEnabled = false; + + private: + static void loadImpulseResponse (dsp::Convolution& convolution, const char* filename) + { + auto stream = createAssetInputStream (filename); + + if (stream == nullptr) + { + jassertfalse; + return; + } + + AudioFormatManager manager; + manager.registerBasicFormats(); + std::unique_ptr reader { manager.createReaderFor (std::move (stream)) }; + + if (reader == nullptr) + { + jassertfalse; + return; + } + + AudioBuffer buffer (static_cast (reader->numChannels), + static_cast (reader->lengthInSamples)); + reader->read (buffer.getArrayOfWritePointers(), buffer.getNumChannels(), 0, buffer.getNumSamples()); + + convolution.loadImpulseResponse (std::move (buffer), + reader->sampleRate, + dsp::Convolution::Stereo::yes, + dsp::Convolution::Trim::yes, + dsp::Convolution::Normalise::yes); + } + }; + struct MultiBandProcessor { MultiBandProcessor() @@ -1320,6 +1429,9 @@ private: int delayEffectType = 1; }; + ParameterReferences parameters; + AudioProcessorValueTreeState apvts; + using Chain = dsp::ProcessorChain, dsp::Gain, DirectDelayProcessor, @@ -1330,6 +1442,7 @@ private: DistortionProcessor, dsp::LadderFilter, DelayEffectProcessor, + ConvolutionProcessor, dsp::Limiter, dsp::Gain, dsp::Panner>; @@ -1348,6 +1461,7 @@ private: distortionIndex, ladderIndex, delayEffectIndex, + convolutionIndex, limiterIndex, outputGainIndex, pannerIndex @@ -1355,6 +1469,7 @@ private: //============================================================================== std::atomic requiresUpdate { true }; + std::atomic irSize { 0 }; //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DspModulePluginDemo) @@ -1370,6 +1485,7 @@ public: { comboEffect.addSectionHeading ("Main"); comboEffect.addItem ("Distortion", TabDistortion); + comboEffect.addItem ("Convolution", TabConvolution); comboEffect.addItem ("Multi-band", TabMultiBand); comboEffect.addSectionHeading ("Dynamics"); @@ -1398,6 +1514,7 @@ public: labelEffect, basicControls, distortionControls, + convolutionControls, multibandControls, compressorControls, noiseGateControls, @@ -1440,6 +1557,10 @@ public: g.setColour (Colours::white); g.setFont (Font (20.0f).italicised().withExtraKerningFactor (0.1f)); g.drawFittedText ("DSP MODULE DEMO", rectTop.reduced (10, 0), Justification::centredLeft, 1); + + g.setFont (Font (14.0f)); + String strText = "IR length (reverb): " + String (proc.getCurrentIRSize()) + " samples"; + g.drawFittedText (strText, rectBottom.reduced (10, 0), Justification::centredRight, 1); } void resized() override @@ -1460,6 +1581,7 @@ public: forEach ([&] (Component& comp) { comp.setBounds (rectEffects); }, distortionControls, + convolutionControls, multibandControls, compressorControls, noiseGateControls, @@ -1475,13 +1597,13 @@ private: class AttachedSlider : public Component { public: - AttachedSlider (AudioProcessorValueTreeState& state, StringRef strID) - : label ("", state.getParameter (strID)->name), - attachment (state, strID, slider) + explicit AttachedSlider (RangedAudioParameter& param) + : label ("", param.name), + attachment (param, slider) { addAllAndMakeVisible (*this, slider, label); - slider.setTextValueSuffix (" " + state.getParameter (strID)->label); + slider.setTextValueSuffix (" " + param.label); label.attachToComponent (&slider, false); label.setJustificationType (Justification::centred); @@ -1492,15 +1614,15 @@ private: private: Slider slider { Slider::RotaryVerticalDrag, Slider::TextBoxBelow }; Label label; - AudioProcessorValueTreeState::SliderAttachment attachment; + SliderParameterAttachment attachment; }; class AttachedToggle : public Component { public: - AttachedToggle (AudioProcessorValueTreeState& state, StringRef strID) - : toggle (state.getParameter (strID)->name), - attachment (state, strID, toggle) + explicit AttachedToggle (RangedAudioParameter& param) + : toggle (param.name), + attachment (param, toggle) { addAndMakeVisible (toggle); } @@ -1509,16 +1631,16 @@ private: private: ToggleButton toggle; - AudioProcessorValueTreeState::ButtonAttachment attachment; + ButtonParameterAttachment attachment; }; class AttachedCombo : public Component { public: - AttachedCombo (AudioProcessorValueTreeState& state, StringRef strID) - : combo (state, strID), - label ("", state.getParameter (strID)->name), - attachment (state, strID, combo) + explicit AttachedCombo (RangedAudioParameter& param) + : combo (param), + label ("", param.name), + attachment (param, combo) { addAllAndMakeVisible (*this, combo, label); @@ -1534,17 +1656,17 @@ private: private: struct ComboWithItems : public ComboBox { - ComboWithItems (AudioProcessorValueTreeState& state, StringRef strID) + explicit ComboWithItems (RangedAudioParameter& param) { // Adding the list here in the constructor means that the combo // is already populated when we construct the attachment below - addItemList (dynamic_cast (state.getParameter (strID))->choices, 1); + addItemList (dynamic_cast (param).choices, 1); } }; ComboWithItems combo; Label label; - AudioProcessorValueTreeState::ComboBoxAttachment attachment; + ComboBoxParameterAttachment attachment; }; //============================================================================== @@ -1561,6 +1683,7 @@ private: forEach (op, std::forward_as_tuple (distortionControls, TabDistortion), + std::forward_as_tuple (convolutionControls, TabConvolution), std::forward_as_tuple (multibandControls, TabMultiBand), std::forward_as_tuple (compressorControls, TabCompressor), std::forward_as_tuple (noiseGateControls, TabNoiseGate), @@ -1575,6 +1698,7 @@ private: enum EffectsTabs { TabDistortion = 1, + TabConvolution, TabMultiBand, TabCompressor, TabNoiseGate, @@ -1622,10 +1746,10 @@ private: struct BasicControls : public Component { - explicit BasicControls (AudioProcessorValueTreeState& state) - : pan (state, ID::pan), - input (state, ID::inputGain), - output (state, ID::outputGain) + explicit BasicControls (const DspModulePluginDemo::ParameterReferences& state) + : pan (state.pan), + input (state.inputGain), + output (state.outputGain) { addAllAndMakeVisible (*this, pan, input, output); } @@ -1640,15 +1764,15 @@ private: struct DistortionControls : public Component { - explicit DistortionControls (AudioProcessorValueTreeState& state) - : toggle (state, ID::distortionEnabled), - lowpass (state, ID::distortionLowpass), - highpass (state, ID::distortionHighpass), - mix (state, ID::distortionMix), - gain (state, ID::distortionInGain), - compv (state, ID::distortionCompGain), - type (state, ID::distortionType), - oversampling (state, ID::distortionOversampler) + explicit DistortionControls (const DspModulePluginDemo::ParameterReferences& state) + : toggle (state.distortionEnabled), + lowpass (state.distortionLowpass), + highpass (state.distortionHighpass), + mix (state.distortionMix), + gain (state.distortionInGain), + compv (state.distortionCompGain), + type (state.distortionType), + oversampling (state.distortionOversampler) { addAllAndMakeVisible (*this, toggle, type, lowpass, highpass, mix, gain, compv, oversampling); } @@ -1663,13 +1787,32 @@ private: AttachedCombo type, oversampling; }; + struct ConvolutionControls : public Component + { + explicit ConvolutionControls (const DspModulePluginDemo::ParameterReferences& state) + : cab (state.convolutionCabEnabled), + reverb (state.convolutionReverbEnabled), + mix (state.convolutionReverbMix) + { + addAllAndMakeVisible (*this, cab, reverb, mix); + } + + void resized() override + { + performLayout (getLocalBounds(), cab, reverb, mix); + } + + AttachedToggle cab, reverb; + AttachedSlider mix; + }; + struct MultiBandControls : public Component { - explicit MultiBandControls (AudioProcessorValueTreeState& state) - : toggle (state, ID::multiBandEnabled), - low (state, ID::multiBandLowVolume), - high (state, ID::multiBandHighVolume), - lRFreq (state, ID::multiBandFreq) + explicit MultiBandControls (const DspModulePluginDemo::ParameterReferences& state) + : toggle (state.multiBandEnabled), + low (state.multiBandLowVolume), + high (state.multiBandHighVolume), + lRFreq (state.multiBandFreq) { addAllAndMakeVisible (*this, toggle, low, high, lRFreq); } @@ -1685,12 +1828,12 @@ private: struct CompressorControls : public Component { - explicit CompressorControls (AudioProcessorValueTreeState& state) - : toggle (state, ID::compressorEnabled), - threshold (state, ID::compressorThreshold), - ratio (state, ID::compressorRatio), - attack (state, ID::compressorAttack), - release (state, ID::compressorRelease) + explicit CompressorControls (const DspModulePluginDemo::ParameterReferences& state) + : toggle (state.compressorEnabled), + threshold (state.compressorThreshold), + ratio (state.compressorRatio), + attack (state.compressorAttack), + release (state.compressorRelease) { addAllAndMakeVisible (*this, toggle, threshold, ratio, attack, release); } @@ -1706,12 +1849,12 @@ private: struct NoiseGateControls : public Component { - explicit NoiseGateControls (AudioProcessorValueTreeState& state) - : toggle (state, ID::noiseGateEnabled), - threshold (state, ID::noiseGateThreshold), - ratio (state, ID::noiseGateRatio), - attack (state, ID::noiseGateAttack), - release (state, ID::noiseGateRelease) + explicit NoiseGateControls (const DspModulePluginDemo::ParameterReferences& state) + : toggle (state.noiseGateEnabled), + threshold (state.noiseGateThreshold), + ratio (state.noiseGateRatio), + attack (state.noiseGateAttack), + release (state.noiseGateRelease) { addAllAndMakeVisible (*this, toggle, threshold, ratio, attack, release); } @@ -1727,10 +1870,10 @@ private: struct LimiterControls : public Component { - explicit LimiterControls (AudioProcessorValueTreeState& state) - : toggle (state, ID::limiterEnabled), - threshold (state, ID::limiterThreshold), - release (state, ID::limiterRelease) + explicit LimiterControls (const DspModulePluginDemo::ParameterReferences& state) + : toggle (state.limiterEnabled), + threshold (state.limiterThreshold), + release (state.limiterRelease) { addAllAndMakeVisible (*this, toggle, threshold, release); } @@ -1746,12 +1889,12 @@ private: struct DirectDelayControls : public Component { - explicit DirectDelayControls (AudioProcessorValueTreeState& state) - : toggle (state, ID::directDelayEnabled), - type (state, ID::directDelayType), - delay (state, ID::directDelayValue), - smooth (state, ID::directDelaySmoothing), - mix (state, ID::directDelayMix) + explicit DirectDelayControls (const DspModulePluginDemo::ParameterReferences& state) + : toggle (state.directDelayEnabled), + type (state.directDelayType), + delay (state.directDelayValue), + smooth (state.directDelaySmoothing), + mix (state.directDelayMix) { addAllAndMakeVisible (*this, toggle, type, delay, smooth, mix); } @@ -1768,14 +1911,14 @@ private: struct DelayEffectControls : public Component { - explicit DelayEffectControls (AudioProcessorValueTreeState& state) - : toggle (state, ID::delayEffectEnabled), - type (state, ID::delayEffectType), - value (state, ID::delayEffectValue), - smooth (state, ID::delayEffectSmoothing), - lowpass (state, ID::delayEffectLowpass), - feedback (state, ID::delayEffectFeedback), - mix (state, ID::delayEffectMix) + explicit DelayEffectControls (const DspModulePluginDemo::ParameterReferences& state) + : toggle (state.delayEffectEnabled), + type (state.delayEffectType), + value (state.delayEffectValue), + smooth (state.delayEffectSmoothing), + lowpass (state.delayEffectLowpass), + feedback (state.delayEffectFeedback), + mix (state.delayEffectMix) { addAllAndMakeVisible (*this, toggle, type, value, smooth, lowpass, feedback, mix); } @@ -1792,13 +1935,13 @@ private: struct PhaserControls : public Component { - explicit PhaserControls (AudioProcessorValueTreeState& state) - : toggle (state, ID::phaserEnabled), - rate (state, ID::phaserRate), - depth (state, ID::phaserDepth), - centre (state, ID::phaserCentreFrequency), - feedback (state, ID::phaserFeedback), - mix (state, ID::phaserMix) + explicit PhaserControls (const DspModulePluginDemo::ParameterReferences& state) + : toggle (state.phaserEnabled), + rate (state.phaserRate), + depth (state.phaserDepth), + centre (state.phaserCentreFrequency), + feedback (state.phaserFeedback), + mix (state.phaserMix) { addAllAndMakeVisible (*this, toggle, rate, depth, centre, feedback, mix); } @@ -1814,13 +1957,13 @@ private: struct ChorusControls : public Component { - explicit ChorusControls (AudioProcessorValueTreeState& state) - : toggle (state, ID::chorusEnabled), - rate (state, ID::chorusRate), - depth (state, ID::chorusDepth), - centre (state, ID::chorusCentreDelay), - feedback (state, ID::chorusFeedback), - mix (state, ID::chorusMix) + explicit ChorusControls (const DspModulePluginDemo::ParameterReferences& state) + : toggle (state.chorusEnabled), + rate (state.chorusRate), + depth (state.chorusDepth), + centre (state.chorusCentreDelay), + feedback (state.chorusFeedback), + mix (state.chorusMix) { addAllAndMakeVisible (*this, toggle, rate, depth, centre, feedback, mix); } @@ -1836,12 +1979,12 @@ private: struct LadderControls : public Component { - explicit LadderControls (AudioProcessorValueTreeState& state) - : toggle (state, ID::ladderEnabled), - mode (state, ID::ladderMode), - freq (state, ID::ladderCutoff), - resonance (state, ID::ladderResonance), - drive (state, ID::ladderDrive) + explicit LadderControls (const DspModulePluginDemo::ParameterReferences& state) + : toggle (state.ladderEnabled), + mode (state.ladderMode), + freq (state.ladderCutoff), + resonance (state.ladderResonance), + drive (state.ladderDrive) { addAllAndMakeVisible (*this, toggle, mode, freq, resonance, drive); } @@ -1865,17 +2008,18 @@ private: //============================================================================== DspModulePluginDemo& proc; - BasicControls basicControls { proc.apvts }; - DistortionControls distortionControls { proc.apvts }; - MultiBandControls multibandControls { proc.apvts }; - CompressorControls compressorControls { proc.apvts }; - NoiseGateControls noiseGateControls { proc.apvts }; - LimiterControls limiterControls { proc.apvts }; - DirectDelayControls directDelayControls { proc.apvts }; - DelayEffectControls delayEffectControls { proc.apvts }; - PhaserControls phaserControls { proc.apvts }; - ChorusControls chorusControls { proc.apvts }; - LadderControls ladderControls { proc.apvts }; + BasicControls basicControls { proc.getParameterValues() }; + DistortionControls distortionControls { proc.getParameterValues() }; + ConvolutionControls convolutionControls { proc.getParameterValues() }; + MultiBandControls multibandControls { proc.getParameterValues() }; + CompressorControls compressorControls { proc.getParameterValues() }; + NoiseGateControls noiseGateControls { proc.getParameterValues() }; + LimiterControls limiterControls { proc.getParameterValues() }; + DirectDelayControls directDelayControls { proc.getParameterValues() }; + DelayEffectControls delayEffectControls { proc.getParameterValues() }; + PhaserControls phaserControls { proc.getParameterValues() }; + ChorusControls chorusControls { proc.getParameterValues() }; + LadderControls ladderControls { proc.getParameterValues() }; //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DspModulePluginDemoEditor) diff --git a/extras/AudioPerformanceTest/Builds/Android/app/CMakeLists.txt b/extras/AudioPerformanceTest/Builds/Android/app/CMakeLists.txt index 5fd6cbb869..a92d48385e 100644 --- a/extras/AudioPerformanceTest/Builds/Android/app/CMakeLists.txt +++ b/extras/AudioPerformanceTest/Builds/Android/app/CMakeLists.txt @@ -667,6 +667,8 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_core/maths/juce_Random.h" "../../../../../modules/juce_core/maths/juce_Range.h" "../../../../../modules/juce_core/maths/juce_StatisticsAccumulator.h" + "../../../../../modules/juce_core/memory/juce_AllocationHooks.cpp" + "../../../../../modules/juce_core/memory/juce_AllocationHooks.h" "../../../../../modules/juce_core/memory/juce_Atomic.h" "../../../../../modules/juce_core/memory/juce_ByteOrder.h" "../../../../../modules/juce_core/memory/juce_ContainerDeletePolicy.h" @@ -2076,6 +2078,8 @@ set_source_files_properties("../../../../../modules/juce_core/maths/juce_Random. set_source_files_properties("../../../../../modules/juce_core/maths/juce_Random.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/maths/juce_Range.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/maths/juce_StatisticsAccumulator.h" PROPERTIES HEADER_FILE_ONLY TRUE) +set_source_files_properties("../../../../../modules/juce_core/memory/juce_AllocationHooks.cpp" PROPERTIES HEADER_FILE_ONLY TRUE) +set_source_files_properties("../../../../../modules/juce_core/memory/juce_AllocationHooks.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/memory/juce_Atomic.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/memory/juce_ByteOrder.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/memory/juce_ContainerDeletePolicy.h" PROPERTIES HEADER_FILE_ONLY TRUE) diff --git a/extras/AudioPerformanceTest/Builds/VisualStudio2019/AudioPerformanceTest_App.vcxproj b/extras/AudioPerformanceTest/Builds/VisualStudio2019/AudioPerformanceTest_App.vcxproj index e8b4cf4e80..d66715e8c6 100644 --- a/extras/AudioPerformanceTest/Builds/VisualStudio2019/AudioPerformanceTest_App.vcxproj +++ b/extras/AudioPerformanceTest/Builds/VisualStudio2019/AudioPerformanceTest_App.vcxproj @@ -895,6 +895,9 @@ true + + true + true @@ -2372,6 +2375,7 @@ + diff --git a/extras/AudioPerformanceTest/Builds/VisualStudio2019/AudioPerformanceTest_App.vcxproj.filters b/extras/AudioPerformanceTest/Builds/VisualStudio2019/AudioPerformanceTest_App.vcxproj.filters index ca6aa84887..de03a91c54 100644 --- a/extras/AudioPerformanceTest/Builds/VisualStudio2019/AudioPerformanceTest_App.vcxproj.filters +++ b/extras/AudioPerformanceTest/Builds/VisualStudio2019/AudioPerformanceTest_App.vcxproj.filters @@ -1210,6 +1210,9 @@ JUCE Modules\juce_core\maths + + JUCE Modules\juce_core\memory + JUCE Modules\juce_core\memory @@ -3504,6 +3507,9 @@ JUCE Modules\juce_core\maths + + JUCE Modules\juce_core\memory + JUCE Modules\juce_core\memory diff --git a/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt b/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt index 0b936e6459..92fff7f8c5 100644 --- a/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt +++ b/extras/AudioPluginHost/Builds/Android/app/CMakeLists.txt @@ -684,6 +684,8 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_core/maths/juce_Random.h" "../../../../../modules/juce_core/maths/juce_Range.h" "../../../../../modules/juce_core/maths/juce_StatisticsAccumulator.h" + "../../../../../modules/juce_core/memory/juce_AllocationHooks.cpp" + "../../../../../modules/juce_core/memory/juce_AllocationHooks.h" "../../../../../modules/juce_core/memory/juce_Atomic.h" "../../../../../modules/juce_core/memory/juce_ByteOrder.h" "../../../../../modules/juce_core/memory/juce_ContainerDeletePolicy.h" @@ -904,6 +906,8 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_data_structures/juce_data_structures.h" "../../../../../modules/juce_dsp/containers/juce_AudioBlock.h" "../../../../../modules/juce_dsp/containers/juce_AudioBlock_test.cpp" + "../../../../../modules/juce_dsp/containers/juce_FixedSizeFunction.h" + "../../../../../modules/juce_dsp/containers/juce_FixedSizeFunction_test.cpp" "../../../../../modules/juce_dsp/containers/juce_SIMDRegister.h" "../../../../../modules/juce_dsp/containers/juce_SIMDRegister_Impl.h" "../../../../../modules/juce_dsp/containers/juce_SIMDRegister_test.cpp" @@ -911,6 +915,7 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_dsp/filter_design/juce_FilterDesign.h" "../../../../../modules/juce_dsp/frequency/juce_Convolution.cpp" "../../../../../modules/juce_dsp/frequency/juce_Convolution.h" + "../../../../../modules/juce_dsp/frequency/juce_Convolution_test.cpp" "../../../../../modules/juce_dsp/frequency/juce_FFT.cpp" "../../../../../modules/juce_dsp/frequency/juce_FFT.h" "../../../../../modules/juce_dsp/frequency/juce_FFT_test.cpp" @@ -2248,6 +2253,8 @@ set_source_files_properties("../../../../../modules/juce_core/maths/juce_Random. set_source_files_properties("../../../../../modules/juce_core/maths/juce_Random.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/maths/juce_Range.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/maths/juce_StatisticsAccumulator.h" PROPERTIES HEADER_FILE_ONLY TRUE) +set_source_files_properties("../../../../../modules/juce_core/memory/juce_AllocationHooks.cpp" PROPERTIES HEADER_FILE_ONLY TRUE) +set_source_files_properties("../../../../../modules/juce_core/memory/juce_AllocationHooks.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/memory/juce_Atomic.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/memory/juce_ByteOrder.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/memory/juce_ContainerDeletePolicy.h" PROPERTIES HEADER_FILE_ONLY TRUE) @@ -2468,6 +2475,8 @@ set_source_files_properties("../../../../../modules/juce_data_structures/juce_da set_source_files_properties("../../../../../modules/juce_data_structures/juce_data_structures.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_dsp/containers/juce_AudioBlock.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_dsp/containers/juce_AudioBlock_test.cpp" PROPERTIES HEADER_FILE_ONLY TRUE) +set_source_files_properties("../../../../../modules/juce_dsp/containers/juce_FixedSizeFunction.h" PROPERTIES HEADER_FILE_ONLY TRUE) +set_source_files_properties("../../../../../modules/juce_dsp/containers/juce_FixedSizeFunction_test.cpp" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_dsp/containers/juce_SIMDRegister.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_dsp/containers/juce_SIMDRegister_Impl.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_dsp/containers/juce_SIMDRegister_test.cpp" PROPERTIES HEADER_FILE_ONLY TRUE) @@ -2475,6 +2484,7 @@ set_source_files_properties("../../../../../modules/juce_dsp/filter_design/juce_ set_source_files_properties("../../../../../modules/juce_dsp/filter_design/juce_FilterDesign.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_dsp/frequency/juce_Convolution.cpp" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_dsp/frequency/juce_Convolution.h" PROPERTIES HEADER_FILE_ONLY TRUE) +set_source_files_properties("../../../../../modules/juce_dsp/frequency/juce_Convolution_test.cpp" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_dsp/frequency/juce_FFT.cpp" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_dsp/frequency/juce_FFT.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_dsp/frequency/juce_FFT_test.cpp" PROPERTIES HEADER_FILE_ONLY TRUE) diff --git a/extras/AudioPluginHost/Builds/VisualStudio2015/AudioPluginHost_App.vcxproj b/extras/AudioPluginHost/Builds/VisualStudio2015/AudioPluginHost_App.vcxproj index 07d54ed376..f752538570 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2015/AudioPluginHost_App.vcxproj +++ b/extras/AudioPluginHost/Builds/VisualStudio2015/AudioPluginHost_App.vcxproj @@ -902,6 +902,9 @@ true + + true + true @@ -1181,6 +1184,9 @@ true + + true + true @@ -1190,6 +1196,9 @@ true + + true + true @@ -2551,6 +2560,7 @@ + @@ -2669,6 +2679,7 @@ + diff --git a/extras/AudioPluginHost/Builds/VisualStudio2015/AudioPluginHost_App.vcxproj.filters b/extras/AudioPluginHost/Builds/VisualStudio2015/AudioPluginHost_App.vcxproj.filters index 38fdba80ef..548c33a731 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2015/AudioPluginHost_App.vcxproj.filters +++ b/extras/AudioPluginHost/Builds/VisualStudio2015/AudioPluginHost_App.vcxproj.filters @@ -1291,6 +1291,9 @@ JUCE Modules\juce_core\maths + + JUCE Modules\juce_core\memory + JUCE Modules\juce_core\memory @@ -1594,6 +1597,9 @@ JUCE Modules\juce_dsp\containers + + JUCE Modules\juce_dsp\containers + JUCE Modules\juce_dsp\containers @@ -1603,6 +1609,9 @@ JUCE Modules\juce_dsp\frequency + + JUCE Modules\juce_dsp\frequency + JUCE Modules\juce_dsp\frequency @@ -3789,6 +3798,9 @@ JUCE Modules\juce_core\maths + + JUCE Modules\juce_core\memory + JUCE Modules\juce_core\memory @@ -4143,6 +4155,9 @@ JUCE Modules\juce_dsp\containers + + JUCE Modules\juce_dsp\containers + JUCE Modules\juce_dsp\containers diff --git a/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj b/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj index 858a4fd42a..7abd8e9fe4 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj +++ b/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj @@ -902,6 +902,9 @@ true + + true + true @@ -1181,6 +1184,9 @@ true + + true + true @@ -1190,6 +1196,9 @@ true + + true + true @@ -2551,6 +2560,7 @@ + @@ -2669,6 +2679,7 @@ + diff --git a/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj.filters b/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj.filters index b3ba2bee37..b66624de82 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj.filters +++ b/extras/AudioPluginHost/Builds/VisualStudio2017/AudioPluginHost_App.vcxproj.filters @@ -1291,6 +1291,9 @@ JUCE Modules\juce_core\maths + + JUCE Modules\juce_core\memory + JUCE Modules\juce_core\memory @@ -1594,6 +1597,9 @@ JUCE Modules\juce_dsp\containers + + JUCE Modules\juce_dsp\containers + JUCE Modules\juce_dsp\containers @@ -1603,6 +1609,9 @@ JUCE Modules\juce_dsp\frequency + + JUCE Modules\juce_dsp\frequency + JUCE Modules\juce_dsp\frequency @@ -3789,6 +3798,9 @@ JUCE Modules\juce_core\maths + + JUCE Modules\juce_core\memory + JUCE Modules\juce_core\memory @@ -4143,6 +4155,9 @@ JUCE Modules\juce_dsp\containers + + JUCE Modules\juce_dsp\containers + JUCE Modules\juce_dsp\containers diff --git a/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj b/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj index aa757ea964..2d48837bf0 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj +++ b/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj @@ -902,6 +902,9 @@ true + + true + true @@ -1181,6 +1184,9 @@ true + + true + true @@ -1190,6 +1196,9 @@ true + + true + true @@ -2551,6 +2560,7 @@ + @@ -2669,6 +2679,7 @@ + diff --git a/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj.filters b/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj.filters index 3f20abf9e6..cf86e5851a 100644 --- a/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj.filters +++ b/extras/AudioPluginHost/Builds/VisualStudio2019/AudioPluginHost_App.vcxproj.filters @@ -1291,6 +1291,9 @@ JUCE Modules\juce_core\maths + + JUCE Modules\juce_core\memory + JUCE Modules\juce_core\memory @@ -1594,6 +1597,9 @@ JUCE Modules\juce_dsp\containers + + JUCE Modules\juce_dsp\containers + JUCE Modules\juce_dsp\containers @@ -1603,6 +1609,9 @@ JUCE Modules\juce_dsp\frequency + + JUCE Modules\juce_dsp\frequency + JUCE Modules\juce_dsp\frequency @@ -3789,6 +3798,9 @@ JUCE Modules\juce_core\maths + + JUCE Modules\juce_core\memory + JUCE Modules\juce_core\memory @@ -4143,6 +4155,9 @@ JUCE Modules\juce_dsp\containers + + JUCE Modules\juce_dsp\containers + JUCE Modules\juce_dsp\containers diff --git a/extras/BinaryBuilder/Builds/VisualStudio2019/BinaryBuilder_ConsoleApp.vcxproj b/extras/BinaryBuilder/Builds/VisualStudio2019/BinaryBuilder_ConsoleApp.vcxproj index dec29cd699..166b503305 100644 --- a/extras/BinaryBuilder/Builds/VisualStudio2019/BinaryBuilder_ConsoleApp.vcxproj +++ b/extras/BinaryBuilder/Builds/VisualStudio2019/BinaryBuilder_ConsoleApp.vcxproj @@ -217,6 +217,9 @@ true + + true + true @@ -486,6 +489,7 @@ + diff --git a/extras/BinaryBuilder/Builds/VisualStudio2019/BinaryBuilder_ConsoleApp.vcxproj.filters b/extras/BinaryBuilder/Builds/VisualStudio2019/BinaryBuilder_ConsoleApp.vcxproj.filters index 9750fee0f0..57ad38db1d 100644 --- a/extras/BinaryBuilder/Builds/VisualStudio2019/BinaryBuilder_ConsoleApp.vcxproj.filters +++ b/extras/BinaryBuilder/Builds/VisualStudio2019/BinaryBuilder_ConsoleApp.vcxproj.filters @@ -157,6 +157,9 @@ JUCE Modules\juce_core\maths + + JUCE Modules\juce_core\memory + JUCE Modules\juce_core\memory @@ -522,6 +525,9 @@ JUCE Modules\juce_core\maths + + JUCE Modules\juce_core\memory + JUCE Modules\juce_core\memory diff --git a/extras/NetworkGraphicsDemo/Builds/Android/app/CMakeLists.txt b/extras/NetworkGraphicsDemo/Builds/Android/app/CMakeLists.txt index 7086feb335..92bf168f00 100644 --- a/extras/NetworkGraphicsDemo/Builds/Android/app/CMakeLists.txt +++ b/extras/NetworkGraphicsDemo/Builds/Android/app/CMakeLists.txt @@ -671,6 +671,8 @@ add_library( ${BINARY_NAME} "../../../../../modules/juce_core/maths/juce_Random.h" "../../../../../modules/juce_core/maths/juce_Range.h" "../../../../../modules/juce_core/maths/juce_StatisticsAccumulator.h" + "../../../../../modules/juce_core/memory/juce_AllocationHooks.cpp" + "../../../../../modules/juce_core/memory/juce_AllocationHooks.h" "../../../../../modules/juce_core/memory/juce_Atomic.h" "../../../../../modules/juce_core/memory/juce_ByteOrder.h" "../../../../../modules/juce_core/memory/juce_ContainerDeletePolicy.h" @@ -2155,6 +2157,8 @@ set_source_files_properties("../../../../../modules/juce_core/maths/juce_Random. set_source_files_properties("../../../../../modules/juce_core/maths/juce_Random.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/maths/juce_Range.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/maths/juce_StatisticsAccumulator.h" PROPERTIES HEADER_FILE_ONLY TRUE) +set_source_files_properties("../../../../../modules/juce_core/memory/juce_AllocationHooks.cpp" PROPERTIES HEADER_FILE_ONLY TRUE) +set_source_files_properties("../../../../../modules/juce_core/memory/juce_AllocationHooks.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/memory/juce_Atomic.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/memory/juce_ByteOrder.h" PROPERTIES HEADER_FILE_ONLY TRUE) set_source_files_properties("../../../../../modules/juce_core/memory/juce_ContainerDeletePolicy.h" PROPERTIES HEADER_FILE_ONLY TRUE) diff --git a/extras/NetworkGraphicsDemo/Builds/VisualStudio2019/NetworkGraphicsDemo_App.vcxproj b/extras/NetworkGraphicsDemo/Builds/VisualStudio2019/NetworkGraphicsDemo_App.vcxproj index ff2a57d1f2..e298e479d5 100644 --- a/extras/NetworkGraphicsDemo/Builds/VisualStudio2019/NetworkGraphicsDemo_App.vcxproj +++ b/extras/NetworkGraphicsDemo/Builds/VisualStudio2019/NetworkGraphicsDemo_App.vcxproj @@ -895,6 +895,9 @@ true + + true + true @@ -2457,6 +2460,7 @@ + diff --git a/extras/NetworkGraphicsDemo/Builds/VisualStudio2019/NetworkGraphicsDemo_App.vcxproj.filters b/extras/NetworkGraphicsDemo/Builds/VisualStudio2019/NetworkGraphicsDemo_App.vcxproj.filters index 1713ecfff3..849ef13a1d 100644 --- a/extras/NetworkGraphicsDemo/Builds/VisualStudio2019/NetworkGraphicsDemo_App.vcxproj.filters +++ b/extras/NetworkGraphicsDemo/Builds/VisualStudio2019/NetworkGraphicsDemo_App.vcxproj.filters @@ -1240,6 +1240,9 @@ JUCE Modules\juce_core\maths + + JUCE Modules\juce_core\memory + JUCE Modules\juce_core\memory @@ -3639,6 +3642,9 @@ JUCE Modules\juce_core\maths + + JUCE Modules\juce_core\memory + JUCE Modules\juce_core\memory diff --git a/extras/Projucer/Builds/VisualStudio2015/Projucer_App.vcxproj b/extras/Projucer/Builds/VisualStudio2015/Projucer_App.vcxproj index 4261d77d16..3bc83332ab 100644 --- a/extras/Projucer/Builds/VisualStudio2015/Projucer_App.vcxproj +++ b/extras/Projucer/Builds/VisualStudio2015/Projucer_App.vcxproj @@ -344,6 +344,9 @@ true + + true + true @@ -1721,6 +1724,7 @@ + diff --git a/extras/Projucer/Builds/VisualStudio2015/Projucer_App.vcxproj.filters b/extras/Projucer/Builds/VisualStudio2015/Projucer_App.vcxproj.filters index 97ce38359d..f5f78ed704 100644 --- a/extras/Projucer/Builds/VisualStudio2015/Projucer_App.vcxproj.filters +++ b/extras/Projucer/Builds/VisualStudio2015/Projucer_App.vcxproj.filters @@ -622,6 +622,9 @@ JUCE Modules\juce_core\maths + + JUCE Modules\juce_core\memory + JUCE Modules\juce_core\memory @@ -2577,6 +2580,9 @@ JUCE Modules\juce_core\maths + + JUCE Modules\juce_core\memory + JUCE Modules\juce_core\memory diff --git a/extras/Projucer/Builds/VisualStudio2017/Projucer_App.vcxproj b/extras/Projucer/Builds/VisualStudio2017/Projucer_App.vcxproj index d805b54f56..b861f033d5 100644 --- a/extras/Projucer/Builds/VisualStudio2017/Projucer_App.vcxproj +++ b/extras/Projucer/Builds/VisualStudio2017/Projucer_App.vcxproj @@ -344,6 +344,9 @@ true + + true + true @@ -1721,6 +1724,7 @@ + diff --git a/extras/Projucer/Builds/VisualStudio2017/Projucer_App.vcxproj.filters b/extras/Projucer/Builds/VisualStudio2017/Projucer_App.vcxproj.filters index 4ff591457d..92e7b79ad6 100644 --- a/extras/Projucer/Builds/VisualStudio2017/Projucer_App.vcxproj.filters +++ b/extras/Projucer/Builds/VisualStudio2017/Projucer_App.vcxproj.filters @@ -622,6 +622,9 @@ JUCE Modules\juce_core\maths + + JUCE Modules\juce_core\memory + JUCE Modules\juce_core\memory @@ -2577,6 +2580,9 @@ JUCE Modules\juce_core\maths + + JUCE Modules\juce_core\memory + JUCE Modules\juce_core\memory diff --git a/extras/Projucer/Builds/VisualStudio2019/Projucer_App.vcxproj b/extras/Projucer/Builds/VisualStudio2019/Projucer_App.vcxproj index f31f3e70ce..bca3289aa1 100644 --- a/extras/Projucer/Builds/VisualStudio2019/Projucer_App.vcxproj +++ b/extras/Projucer/Builds/VisualStudio2019/Projucer_App.vcxproj @@ -344,6 +344,9 @@ true + + true + true @@ -1721,6 +1724,7 @@ + diff --git a/extras/Projucer/Builds/VisualStudio2019/Projucer_App.vcxproj.filters b/extras/Projucer/Builds/VisualStudio2019/Projucer_App.vcxproj.filters index 694adb5e80..3df5574f89 100644 --- a/extras/Projucer/Builds/VisualStudio2019/Projucer_App.vcxproj.filters +++ b/extras/Projucer/Builds/VisualStudio2019/Projucer_App.vcxproj.filters @@ -622,6 +622,9 @@ JUCE Modules\juce_core\maths + + JUCE Modules\juce_core\memory + JUCE Modules\juce_core\memory @@ -2577,6 +2580,9 @@ JUCE Modules\juce_core\maths + + JUCE Modules\juce_core\memory + JUCE Modules\juce_core\memory diff --git a/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj b/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj index 0cc5d787be..2130d24725 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj +++ b/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj @@ -965,6 +965,9 @@ true + + true + true @@ -1244,6 +1247,9 @@ true + + true + true @@ -1253,6 +1259,9 @@ true + + true + true @@ -2689,6 +2698,7 @@ + @@ -2807,6 +2817,7 @@ + diff --git a/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj.filters b/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj.filters index 88b9ebed4e..a9b5b8ca47 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj.filters +++ b/extras/UnitTestRunner/Builds/VisualStudio2017/UnitTestRunner_ConsoleApp.vcxproj.filters @@ -1384,6 +1384,9 @@ JUCE Modules\juce_core\maths + + JUCE Modules\juce_core\memory + JUCE Modules\juce_core\memory @@ -1687,6 +1690,9 @@ JUCE Modules\juce_dsp\containers + + JUCE Modules\juce_dsp\containers + JUCE Modules\juce_dsp\containers @@ -1696,6 +1702,9 @@ JUCE Modules\juce_dsp\frequency + + JUCE Modules\juce_dsp\frequency + JUCE Modules\juce_dsp\frequency @@ -4014,6 +4023,9 @@ JUCE Modules\juce_core\maths + + JUCE Modules\juce_core\memory + JUCE Modules\juce_core\memory @@ -4368,6 +4380,9 @@ JUCE Modules\juce_dsp\containers + + JUCE Modules\juce_dsp\containers + JUCE Modules\juce_dsp\containers diff --git a/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj b/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj index a27581ad11..d727c0cb14 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj +++ b/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj @@ -965,6 +965,9 @@ true + + true + true @@ -1244,6 +1247,9 @@ true + + true + true @@ -1253,6 +1259,9 @@ true + + true + true @@ -2689,6 +2698,7 @@ + @@ -2807,6 +2817,7 @@ + diff --git a/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj.filters b/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj.filters index e64d57000f..6313e316b5 100644 --- a/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj.filters +++ b/extras/UnitTestRunner/Builds/VisualStudio2019/UnitTestRunner_ConsoleApp.vcxproj.filters @@ -1384,6 +1384,9 @@ JUCE Modules\juce_core\maths + + JUCE Modules\juce_core\memory + JUCE Modules\juce_core\memory @@ -1687,6 +1690,9 @@ JUCE Modules\juce_dsp\containers + + JUCE Modules\juce_dsp\containers + JUCE Modules\juce_dsp\containers @@ -1696,6 +1702,9 @@ JUCE Modules\juce_dsp\frequency + + JUCE Modules\juce_dsp\frequency + JUCE Modules\juce_dsp\frequency @@ -4014,6 +4023,9 @@ JUCE Modules\juce_core\maths + + JUCE Modules\juce_core\memory + JUCE Modules\juce_core\memory @@ -4368,6 +4380,9 @@ JUCE Modules\juce_dsp\containers + + JUCE Modules\juce_dsp\containers + JUCE Modules\juce_dsp\containers diff --git a/extras/WindowsDLL/Builds/VisualStudio2019/WindowsDLL_StaticLibrary.vcxproj b/extras/WindowsDLL/Builds/VisualStudio2019/WindowsDLL_StaticLibrary.vcxproj index fb0a6b9fbc..4698ad5c4f 100644 --- a/extras/WindowsDLL/Builds/VisualStudio2019/WindowsDLL_StaticLibrary.vcxproj +++ b/extras/WindowsDLL/Builds/VisualStudio2019/WindowsDLL_StaticLibrary.vcxproj @@ -894,6 +894,9 @@ true + + true + true @@ -2433,6 +2436,7 @@ + diff --git a/extras/WindowsDLL/Builds/VisualStudio2019/WindowsDLL_StaticLibrary.vcxproj.filters b/extras/WindowsDLL/Builds/VisualStudio2019/WindowsDLL_StaticLibrary.vcxproj.filters index 46cbea29ed..f0da9bd116 100644 --- a/extras/WindowsDLL/Builds/VisualStudio2019/WindowsDLL_StaticLibrary.vcxproj.filters +++ b/extras/WindowsDLL/Builds/VisualStudio2019/WindowsDLL_StaticLibrary.vcxproj.filters @@ -1237,6 +1237,9 @@ JUCE Modules\juce_core\maths + + JUCE Modules\juce_core\memory + JUCE Modules\juce_core\memory @@ -3606,6 +3609,9 @@ JUCE Modules\juce_core\maths + + JUCE Modules\juce_core\memory + JUCE Modules\juce_core\memory diff --git a/modules/juce_core/juce_core.cpp b/modules/juce_core/juce_core.cpp index 2a24d8b827..914cae6806 100644 --- a/modules/juce_core/juce_core.cpp +++ b/modules/juce_core/juce_core.cpp @@ -136,6 +136,7 @@ #include "maths/juce_Expression.cpp" #include "maths/juce_Random.cpp" #include "memory/juce_MemoryBlock.cpp" +#include "memory/juce_AllocationHooks.cpp" #include "misc/juce_RuntimePermissions.cpp" #include "misc/juce_Result.cpp" #include "misc/juce_Uuid.cpp" diff --git a/modules/juce_core/juce_core.h b/modules/juce_core/juce_core.h index 178d043bf8..ebcd7271f0 100644 --- a/modules/juce_core/juce_core.h +++ b/modules/juce_core/juce_core.h @@ -177,6 +177,13 @@ #define JUCE_STRICT_REFCOUNTEDPOINTER 0 #endif +/** Config: JUCE_ENABLE_ALLOCATION_HOOKS + If enabled, this will add global allocation functions with built-in assertions, which may + help when debugging allocations in unit tests. +*/ +#ifndef JUCE_ENABLE_ALLOCATION_HOOKS + #define JUCE_ENABLE_ALLOCATION_HOOKS 0 +#endif #ifndef JUCE_STRING_UTF_TYPE #define JUCE_STRING_UTF_TYPE 8 @@ -330,6 +337,7 @@ JUCE_END_IGNORE_WARNINGS_MSVC #include "zip/juce_ZipFile.h" #include "containers/juce_PropertySet.h" #include "memory/juce_SharedResourcePointer.h" +#include "memory/juce_AllocationHooks.h" #if JUCE_CORE_INCLUDE_OBJC_HELPERS && (JUCE_MAC || JUCE_IOS) #include "native/juce_osx_ObjCHelpers.h" diff --git a/modules/juce_core/memory/juce_AllocationHooks.cpp b/modules/juce_core/memory/juce_AllocationHooks.cpp new file mode 100644 index 0000000000..7d85c7e3c2 --- /dev/null +++ b/modules/juce_core/memory/juce_AllocationHooks.cpp @@ -0,0 +1,100 @@ +/* + ============================================================================== + + This file is part of the JUCE 6 technical preview. + Copyright (c) 2020 - Raw Material Software Limited + + You may use this code under the terms of the GPL v3 + (see www.gnu.org/licenses). + + For this technical preview, this file is not subject to commercial licensing. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#if JUCE_ENABLE_ALLOCATION_HOOKS + +namespace juce +{ + +static AllocationHooks& getAllocationHooksForThread() +{ + thread_local AllocationHooks hooks; + return hooks; +} + +void notifyAllocationHooksForThread() +{ + getAllocationHooksForThread().listenerList.call ([] (AllocationHooks::Listener& l) + { + l.newOrDeleteCalled(); + }); +} + +} + +void* operator new (size_t s) +{ + juce::notifyAllocationHooksForThread(); + return std::malloc (s); +} + +void* operator new[] (size_t s) +{ + juce::notifyAllocationHooksForThread(); + return std::malloc (s); +} + +void operator delete (void* p) noexcept +{ + juce::notifyAllocationHooksForThread(); + std::free (p); +} + +void operator delete[] (void* p) noexcept +{ + juce::notifyAllocationHooksForThread(); + std::free (p); +} + +#if JUCE_CXX14_IS_AVAILABLE + +void operator delete (void* p, size_t) noexcept +{ + juce::notifyAllocationHooksForThread(); + std::free (p); +} + +void operator delete[] (void* p, size_t) noexcept +{ + juce::notifyAllocationHooksForThread(); + std::free (p); +} + +#endif + +namespace juce +{ + +//============================================================================== +UnitTestAllocationChecker::UnitTestAllocationChecker (UnitTest& test) + : unitTest (test) +{ + getAllocationHooksForThread().addListener (this); +} + +UnitTestAllocationChecker::~UnitTestAllocationChecker() noexcept +{ + getAllocationHooksForThread().removeListener (this); + unitTest.expectEquals (calls, (size_t) 0, "new or delete was incorrectly called while allocation checker was active"); +} + +void UnitTestAllocationChecker::newOrDeleteCalled() noexcept { ++calls; } + +} + +#endif diff --git a/modules/juce_core/memory/juce_AllocationHooks.h b/modules/juce_core/memory/juce_AllocationHooks.h new file mode 100644 index 0000000000..d61e06cf45 --- /dev/null +++ b/modules/juce_core/memory/juce_AllocationHooks.h @@ -0,0 +1,69 @@ +/* + ============================================================================== + + This file is part of the JUCE 6 technical preview. + Copyright (c) 2020 - Raw Material Software Limited + + You may use this code under the terms of the GPL v3 + (see www.gnu.org/licenses). + + For this technical preview, this file is not subject to commercial licensing. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#if JUCE_ENABLE_ALLOCATION_HOOKS + +namespace juce +{ + +class AllocationHooks +{ +public: + struct Listener + { + virtual ~Listener() noexcept = default; + virtual void newOrDeleteCalled() noexcept = 0; + }; + + void addListener (Listener* l) { listenerList.add (l); } + void removeListener (Listener* l) noexcept { listenerList.remove (l); } + +private: + friend void notifyAllocationHooksForThread(); + ListenerList listenerList; +}; + +//============================================================================== +/** Scoped checker which will cause a unit test failure if any new/delete calls + are made during the lifetime of the UnitTestAllocationChecker. +*/ +class UnitTestAllocationChecker : private AllocationHooks::Listener +{ +public: + /** Create a checker which will log a failure to the passed test if + any calls to new/delete are made. + + Remember to call `UnitTest::beginTest` before constructing this checker! + */ + explicit UnitTestAllocationChecker (UnitTest& test); + + /** Will add a failure to the test if the number of new/delete calls during + this object's lifetime was greater than zero. + */ + ~UnitTestAllocationChecker() noexcept override; + +private: + void newOrDeleteCalled() noexcept override; + + UnitTest& unitTest; + size_t calls = 0; +}; + +} + +#endif diff --git a/modules/juce_dsp/containers/juce_AudioBlock.h b/modules/juce_dsp/containers/juce_AudioBlock.h index 1cc6f704e1..06821b40a5 100644 --- a/modules/juce_dsp/containers/juce_AudioBlock.h +++ b/modules/juce_dsp/containers/juce_AudioBlock.h @@ -143,6 +143,19 @@ public: { } + /** Creates an AudioBlock that points to the data in an AudioBuffer. + AudioBlock does not copy nor own the memory pointed to by dataToUse. + Therefore it is the user's responsibility to ensure that the buffer is retained + throughout the life-time of the AudioBlock without being modified. + */ + template + constexpr AudioBlock (const AudioBuffer& buffer) noexcept + : channels (buffer.getArrayOfReadPointers()), + numChannels (static_cast (buffer.getNumChannels())), + numSamples (static_cast (buffer.getNumSamples())) + { + } + /** Creates an AudioBlock that points to the data in an AudioBuffer. AudioBlock does not copy nor own the memory pointed to by dataToUse. Therefore it is the user's responsibility to ensure that the buffer is retained diff --git a/modules/juce_dsp/containers/juce_FixedSizeFunction.h b/modules/juce_dsp/containers/juce_FixedSizeFunction.h new file mode 100644 index 0000000000..4a59f3739e --- /dev/null +++ b/modules/juce_dsp/containers/juce_FixedSizeFunction.h @@ -0,0 +1,230 @@ +/* + ============================================================================== + + This file is part of the JUCE 6 technical preview. + Copyright (c) 2020 - Raw Material Software Limited + + You may use this code under the terms of the GPL v3 + (see www.gnu.org/licenses). + + For this technical preview, this file is not subject to commercial licensing. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ +namespace dsp +{ + +namespace detail +{ + template + struct Vtable + { + using Storage = void*; + + using Move = void (*) (Storage, Storage); + using Call = Ret (*) (Storage, Args...); + using Clear = void (*) (Storage); + + constexpr Vtable (Move moveIn, Call callIn, Clear clearIn) noexcept + : move (moveIn), call (callIn), clear (clearIn) {} + + Move move = nullptr; + Call call = nullptr; + Clear clear = nullptr; + }; + + template + void move (void* from, void* to) + { + new (to) Fn (std::move (*reinterpret_cast (from))); + } + + template + typename std::enable_if::value, Ret>::type call (void* s, Args... args) + { + (*reinterpret_cast (s)) (args...); + } + + template + typename std::enable_if::value, Ret>::type call (void* s, Args... args) + { + return (*reinterpret_cast (s)) (std::forward (args)...); + } + + template + void clear (void* s) + { + auto& fn = *reinterpret_cast (s); + fn.~Fn(); + // I know this looks insane, for some reason MSVC 14 sometimes thinks fn is unreferenced + juce::ignoreUnused (fn); + } + + template + constexpr Vtable makeVtable() + { + return { move , call , clear }; + } +} // namespace detail + +template +class FixedSizeFunction; + +/** + A type similar to `std::function` that holds a callable object. + + Unlike `std::function`, the callable object will always be stored in + a buffer of size `len` that is internal to the FixedSizeFunction instance. + This in turn means that creating a FixedSizeFunction instance will never allocate, + making FixedSizeFunctions suitable for use in realtime contexts. +*/ +template +class FixedSizeFunction +{ +private: + using Storage = typename std::aligned_storage::type; + + template + using Decay = typename std::decay::type; + + template > + using IntIfValidConversion = typename std::enable_if::value, + int>::type; + +public: + /** Create an empty function. */ + FixedSizeFunction() noexcept = default; + + /** Create an empty function. */ + FixedSizeFunction (std::nullptr_t) noexcept + : FixedSizeFunction() {} + + FixedSizeFunction (const FixedSizeFunction&) = delete; + + /** Forwards the passed Callable into the internal storage buffer. */ + template , + IntIfValidConversion = 0> + FixedSizeFunction (Callable&& callable) + { + static_assert (sizeof (Fn) <= len, + "The requested function cannot fit in this FixedSizeFunction"); + static_assert (alignof (Fn) <= alignof (Storage), + "FixedSizeFunction cannot accommodate the requested alignment requirements"); + + static constexpr auto vtableForCallable = detail::makeVtable(); + vtable = &vtableForCallable; + + auto* ptr = new (&storage) Fn (std::forward (callable)); + jassert ((void*) ptr == (void*) &storage); + juce::ignoreUnused (ptr); + } + + /** Move constructor. */ + FixedSizeFunction (FixedSizeFunction&& other) noexcept + : vtable (other.vtable) + { + move (std::move (other)); + } + + /** Converting constructor from smaller FixedSizeFunctions. */ + template ::type = 0> + FixedSizeFunction (FixedSizeFunction&& other) noexcept + : vtable (other.vtable) + { + move (std::move (other)); + } + + /** Nulls this instance. */ + FixedSizeFunction& operator= (std::nullptr_t) noexcept + { + return *this = FixedSizeFunction(); + } + + FixedSizeFunction& operator= (const FixedSizeFunction&) = delete; + + /** Assigns a new callable to this instance. */ + template = 0> + FixedSizeFunction& operator= (Callable&& callable) + { + return *this = FixedSizeFunction (std::forward (callable)); + } + + /** Move assignment from smaller FixedSizeFunctions. */ + template ::type = 0> + FixedSizeFunction& operator= (FixedSizeFunction&& other) noexcept + { + return *this = FixedSizeFunction (std::move (other)); + } + + /** Move assignment operator. */ + FixedSizeFunction& operator= (FixedSizeFunction&& other) noexcept + { + clear(); + vtable = other.vtable; + move (std::move (other)); + return *this; + } + + /** Destructor. */ + ~FixedSizeFunction() noexcept { clear(); } + + /** If this instance is currently storing a callable object, calls that object, + otherwise throws `std::bad_function_call`. + */ + Ret operator() (Args... args) const + { + if (vtable != nullptr) + return vtable->call (&storage, std::forward (args)...); + + throw std::bad_function_call(); + } + + /** Returns true if this instance currently holds a callable. */ + explicit operator bool() const noexcept { return vtable != nullptr; } + +private: + template + friend class FixedSizeFunction; + + void clear() noexcept + { + if (vtable != nullptr) + vtable->clear (&storage); + } + + template + void move (FixedSizeFunction&& other) noexcept + { + if (vtable != nullptr) + vtable->move (&other.storage, &storage); + } + + const detail::Vtable* vtable = nullptr; + mutable Storage storage; +}; + +template +bool operator!= (const FixedSizeFunction& fn, std::nullptr_t) { return bool (fn); } + +template +bool operator!= (std::nullptr_t, const FixedSizeFunction& fn) { return bool (fn); } + +template +bool operator== (const FixedSizeFunction& fn, std::nullptr_t) { return ! (fn != nullptr); } + +template +bool operator== (std::nullptr_t, const FixedSizeFunction& fn) { return ! (fn != nullptr); } + + +} +} diff --git a/modules/juce_dsp/containers/juce_FixedSizeFunction_test.cpp b/modules/juce_dsp/containers/juce_FixedSizeFunction_test.cpp new file mode 100644 index 0000000000..4651bdc2f9 --- /dev/null +++ b/modules/juce_dsp/containers/juce_FixedSizeFunction_test.cpp @@ -0,0 +1,345 @@ +/* + ============================================================================== + + This file is part of the JUCE 6 technical preview. + Copyright (c) 2020 - Raw Material Software Limited + + You may use this code under the terms of the GPL v3 + (see www.gnu.org/licenses). + + For this technical preview, this file is not subject to commercial licensing. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#if JUCE_ENABLE_ALLOCATION_HOOKS +#define JUCE_FAIL_ON_ALLOCATION_IN_SCOPE const UnitTestAllocationChecker checker (*this) +#else +#define JUCE_FAIL_ON_ALLOCATION_IN_SCOPE +#endif + +namespace juce +{ +namespace dsp +{ +namespace +{ + +class ConstructCounts +{ + auto tie() const noexcept { return std::tie (constructions, copies, moves, calls, destructions); } + +public: + int constructions = 0; + int copies = 0; + int moves = 0; + int calls = 0; + int destructions = 0; + + ConstructCounts withConstructions (int i) const noexcept { auto c = *this; c.constructions = i; return c; } + ConstructCounts withCopies (int i) const noexcept { auto c = *this; c.copies = i; return c; } + ConstructCounts withMoves (int i) const noexcept { auto c = *this; c.moves = i; return c; } + ConstructCounts withCalls (int i) const noexcept { auto c = *this; c.calls = i; return c; } + ConstructCounts withDestructions (int i) const noexcept { auto c = *this; c.destructions = i; return c; } + + bool operator== (const ConstructCounts& other) const noexcept { return tie() == other.tie(); } + bool operator!= (const ConstructCounts& other) const noexcept { return tie() != other.tie(); } +}; + +String& operator<< (String& str, const ConstructCounts& c) +{ + return str << "{ constructions: " << c.constructions + << ", copies: " << c.copies + << ", moves: " << c.moves + << ", calls: " << c.calls + << ", destructions: " << c.destructions + << " }"; +} + +class FixedSizeFunctionTest : public UnitTest +{ + static void toggleBool (bool& b) { b = ! b; } + + struct ConstructCounter + { + explicit ConstructCounter (ConstructCounts& countsIn) + : counts (countsIn) {} + + ConstructCounter (const ConstructCounter& c) + : counts (c.counts) + { + counts.copies += 1; + } + + ConstructCounter (ConstructCounter&& c) noexcept + : counts (c.counts) + { + counts.moves += 1; + } + + ~ConstructCounter() noexcept { counts.destructions += 1; } + + void operator()() const noexcept { counts.calls += 1; } + + ConstructCounts& counts; + }; + +public: + FixedSizeFunctionTest() + : UnitTest ("Fixed Size Function", UnitTestCategories::dsp) + {} + + void runTest() override + { + beginTest ("Can be constructed and called from a lambda"); + { + JUCE_FAIL_ON_ALLOCATION_IN_SCOPE; + + const auto result = 5; + bool wasCalled = false; + const auto lambda = [&] { wasCalled = true; return result; }; + + const FixedSizeFunction fn (lambda); + const auto out = fn(); + + expect (wasCalled); + expectEquals (result, out); + } + + beginTest ("void fn can be constructed from function with return value"); + { + JUCE_FAIL_ON_ALLOCATION_IN_SCOPE; + + bool wasCalled = false; + const auto lambda = [&] { wasCalled = true; return 5; }; + const FixedSizeFunction fn (lambda); + + fn(); + expect (wasCalled); + } + + beginTest ("Can be constructed and called from a function pointer"); + { + JUCE_FAIL_ON_ALLOCATION_IN_SCOPE; + + bool state = false; + + const FixedSizeFunction fn (toggleBool); + + fn (state); + expect (state); + + fn (state); + expect (! state); + + fn (state); + expect (state); + } + + beginTest ("Default constructed functions throw if called"); + { + const auto a = FixedSizeFunction<8, void()>(); + expectThrowsType (a(), std::bad_function_call) + + const auto b = FixedSizeFunction<8, void()> (nullptr); + expectThrowsType (b(), std::bad_function_call) + } + + beginTest ("Functions can be moved"); + { + JUCE_FAIL_ON_ALLOCATION_IN_SCOPE; + + ConstructCounts counts; + + auto a = FixedSizeFunction (ConstructCounter { counts }); + expectEquals (counts, ConstructCounts().withMoves (1).withDestructions (1)); // The temporary gets destroyed + + a(); + expectEquals (counts, ConstructCounts().withMoves (1).withDestructions (1).withCalls (1)); + + const auto b = std::move (a); + expectEquals (counts, ConstructCounts().withMoves (2).withDestructions (1).withCalls (1)); + + b(); + expectEquals (counts, ConstructCounts().withMoves (2).withDestructions (1).withCalls (2)); + + b(); + expectEquals (counts, ConstructCounts().withMoves (2).withDestructions (1).withCalls (3)); + } + + beginTest ("Functions are destructed properly"); + { + JUCE_FAIL_ON_ALLOCATION_IN_SCOPE; + + ConstructCounts counts; + const ConstructCounter toCopy { counts }; + + { + auto a = FixedSizeFunction (toCopy); + expectEquals (counts, ConstructCounts().withCopies (1)); + } + + expectEquals (counts, ConstructCounts().withCopies (1).withDestructions (1)); + } + + beginTest ("Avoid destructing functions that fail to construct"); + { + struct BadConstructor + { + explicit BadConstructor (ConstructCounts& c) + : counts (c) + { + counts.constructions += 1; + throw std::runtime_error { "this was meant to happen" }; + } + + ~BadConstructor() noexcept { counts.destructions += 1; } + + void operator()() const noexcept { counts.calls += 1; } + + ConstructCounts& counts; + }; + + ConstructCounts counts; + + expectThrowsType ((FixedSizeFunction (BadConstructor { counts })), + std::runtime_error) + + expectEquals (counts, ConstructCounts().withConstructions (1)); + } + + beginTest ("Equality checks work"); + { + JUCE_FAIL_ON_ALLOCATION_IN_SCOPE; + + FixedSizeFunction<8, void()> a; + expect (! bool (a)); + expect (a == nullptr); + expect (nullptr == a); + expect (! (a != nullptr)); + expect (! (nullptr != a)); + + FixedSizeFunction<8, void()> b ([] {}); + expect (bool (b)); + expect (b != nullptr); + expect (nullptr != b); + expect (! (b == nullptr)); + expect (! (nullptr == b)); + } + + beginTest ("Functions can be cleared"); + { + JUCE_FAIL_ON_ALLOCATION_IN_SCOPE; + + FixedSizeFunction<8, void()> fn ([] {}); + expect (bool (fn)); + + fn = nullptr; + expect (! bool (fn)); + } + + beginTest ("Functions can be assigned"); + { + JUCE_FAIL_ON_ALLOCATION_IN_SCOPE; + + using Fn = FixedSizeFunction<8, void()>; + + int numCallsA = 0; + int numCallsB = 0; + + Fn x; + Fn y; + expect (! bool (x)); + expect (! bool (y)); + + x = [&] { numCallsA += 1; }; + y = [&] { numCallsB += 1; }; + expect (bool (x)); + expect (bool (y)); + + x(); + expectEquals (numCallsA, 1); + expectEquals (numCallsB, 0); + + y(); + expectEquals (numCallsA, 1); + expectEquals (numCallsB, 1); + + x = std::move (y); + expectEquals (numCallsA, 1); + expectEquals (numCallsB, 1); + + x(); + expectEquals (numCallsA, 1); + expectEquals (numCallsB, 2); + } + + beginTest ("Functions may mutate internal state"); + { + JUCE_FAIL_ON_ALLOCATION_IN_SCOPE; + + using Fn = FixedSizeFunction<64, void()>; + + Fn x; + expect (! bool (x)); + + int numCalls = 0; + x = [&numCalls, counter = 0]() mutable { counter += 1; numCalls = counter; }; + expect (bool (x)); + + expectEquals (numCalls, 0); + + x(); + expectEquals (numCalls, 1); + + x(); + expectEquals (numCalls, 2); + } + + beginTest ("Functions can sink move-only parameters"); + { + JUCE_FAIL_ON_ALLOCATION_IN_SCOPE; + + using Fn = FixedSizeFunction<64, int (std::unique_ptr)>; + + auto value = 5; + auto ptr = std::make_unique (value); + + Fn fn = [] (std::unique_ptr p) { return *p; }; + + expect (value == fn (std::move (ptr))); + } + + beginTest ("Functions be converted from smaller functions"); + { + JUCE_FAIL_ON_ALLOCATION_IN_SCOPE; + + using SmallFn = FixedSizeFunction<20, void()>; + using LargeFn = FixedSizeFunction<21, void()>; + + bool smallCalled = false; + bool largeCalled = false; + + SmallFn small = [&smallCalled, a = std::array{}] { smallCalled = true; juce::ignoreUnused (a); }; + LargeFn large = [&largeCalled, a = std::array{}] { largeCalled = true; juce::ignoreUnused (a); }; + + large = std::move (small); + + large(); + + expect (smallCalled); + expect (! largeCalled); + } + } +}; + +FixedSizeFunctionTest fixedSizedFunctionTest; + +} +} +} +#undef JUCE_FAIL_ON_ALLOCATION_IN_SCOPE diff --git a/modules/juce_dsp/frequency/juce_Convolution.cpp b/modules/juce_dsp/frequency/juce_Convolution.cpp index dcfef0a00e..af9c45d705 100644 --- a/modules/juce_dsp/frequency/juce_Convolution.cpp +++ b/modules/juce_dsp/frequency/juce_Convolution.cpp @@ -21,155 +21,81 @@ namespace juce namespace dsp { -/** This class is the convolution engine itself, processing only one channel at - a time of input signal. -*/ struct ConvolutionEngine { - ConvolutionEngine() = default; - - //============================================================================== - struct ProcessingInformation + ConvolutionEngine (const float* samples, + size_t numSamples, + size_t maxBlockSize) + : blockSize ((size_t) nextPowerOfTwo ((int) maxBlockSize)), + fftSize (blockSize > 128 ? 2 * blockSize : 4 * blockSize), + fftObject (std::make_unique (roundToInt (std::log2 (fftSize)))), + numSegments (numSamples / (fftSize - blockSize) + 1u), + numInputSegments ((blockSize > 128 ? numSegments : 3 * numSegments)), + bufferInput (1, static_cast (fftSize)), + bufferOutput (1, static_cast (fftSize * 2)), + bufferTempOutput (1, static_cast (fftSize * 2)), + bufferOverlap (1, static_cast (fftSize)) { - enum class SourceType + bufferOutput.clear(); + + auto updateSegmentsIfNecessary = [this] (size_t numSegmentsToUpdate, + std::vector>& segments) { - sourceBinaryData, - sourceAudioFile, - sourceAudioBuffer, - sourceNone + if (numSegmentsToUpdate == 0 + || numSegmentsToUpdate != (size_t) segments.size() + || (size_t) segments[0].getNumSamples() != fftSize * 2) + { + segments.clear(); + + for (size_t i = 0; i < numSegmentsToUpdate; ++i) + segments.push_back ({ 1, static_cast (fftSize * 2) }); + } }; - SourceType sourceType = SourceType::sourceNone; + updateSegmentsIfNecessary (numInputSegments, buffersInputSegments); + updateSegmentsIfNecessary (numSegments, buffersImpulseSegments); - const void* sourceData; - int sourceDataSize; - File fileImpulseResponse; + auto FFTTempObject = std::make_unique (roundToInt (std::log2 (fftSize))); + size_t currentPtr = 0; - double originalSampleRate; - int originalSize = 0; - int originalNumChannels = 1; + for (auto& buf : buffersImpulseSegments) + { + buf.clear(); - AudioBuffer* buffer; + auto* impulseResponse = buf.getWritePointer (0); - bool wantsStereo = true; - bool wantsTrimming = true; - bool wantsNormalisation = true; - int64 wantedSize = 0; - int finalSize = 0; + if (&buf == &buffersImpulseSegments.front()) + impulseResponse[0] = 1.0f; - double sampleRate = 0; - size_t maximumBufferSize = 0; - }; + FloatVectorOperations::copy (impulseResponse, + samples + currentPtr, + static_cast (jmin (fftSize - blockSize, numSamples - currentPtr))); + + FFTTempObject->performRealOnlyForwardTransform (impulseResponse); + prepareForConvolution (impulseResponse); + + currentPtr += (fftSize - blockSize); + } + + reset(); + } - //============================================================================== void reset() { bufferInput.clear(); bufferOverlap.clear(); bufferTempOutput.clear(); + bufferOutput.clear(); - for (auto i = 0; i < buffersInputSegments.size(); ++i) - buffersInputSegments.getReference (i).clear(); + for (auto& buf : buffersInputSegments) + buf.clear(); currentSegment = 0; inputDataPos = 0; } - /** Initalize all the states and objects to perform the convolution. */ - void initializeConvolutionEngine (ProcessingInformation& info, int channel) - { - blockSize = (size_t) nextPowerOfTwo ((int) info.maximumBufferSize); - - FFTSize = blockSize > 128 ? 2 * blockSize - : 4 * blockSize; - - numSegments = ((size_t) info.finalSize) / (FFTSize - blockSize) + 1u; - - numInputSegments = (blockSize > 128 ? numSegments : 3 * numSegments); - - FFTobject = std::make_unique (roundToInt (std::log2 (FFTSize))); - - bufferInput.setSize (1, static_cast (FFTSize)); - bufferOutput.setSize (1, static_cast (FFTSize * 2)); - bufferTempOutput.setSize (1, static_cast (FFTSize * 2)); - bufferOverlap.setSize (1, static_cast (FFTSize)); - - buffersInputSegments.clear(); - buffersImpulseSegments.clear(); - bufferOutput.clear(); - - for (size_t i = 0; i < numInputSegments; ++i) - { - AudioBuffer newInputSegment; - newInputSegment.setSize (1, static_cast (FFTSize * 2)); - buffersInputSegments.add (newInputSegment); - } - - for (auto i = 0u; i < numSegments; ++i) - { - AudioBuffer newImpulseSegment; - newImpulseSegment.setSize (1, static_cast (FFTSize * 2)); - buffersImpulseSegments.add (newImpulseSegment); - } - - std::unique_ptr FFTTempObject (new FFT (roundToInt (std::log2 (FFTSize)))); - - auto* channelData = info.buffer->getWritePointer (channel); - - for (size_t n = 0; n < numSegments; ++n) - { - buffersImpulseSegments.getReference (static_cast (n)).clear(); - - auto* impulseResponse = buffersImpulseSegments.getReference (static_cast (n)).getWritePointer (0); - - if (n == 0) - impulseResponse[0] = 1.0f; - - for (size_t i = 0; i < FFTSize - blockSize; ++i) - if (i + n * (FFTSize - blockSize) < (size_t) info.finalSize) - impulseResponse[i] = channelData[i + n * (FFTSize - blockSize)]; - - FFTTempObject->performRealOnlyForwardTransform (impulseResponse); - prepareForConvolution (impulseResponse); - } - - reset(); - - isReady = true; - } - - /** Copy the states of another engine. */ - void copyStateFromOtherEngine (const ConvolutionEngine& other) - { - if (FFTSize != other.FFTSize) - { - FFTobject.reset (new FFT (roundToInt (std::log2 (other.FFTSize)))); - FFTSize = other.FFTSize; - } - - currentSegment = other.currentSegment; - numInputSegments = other.numInputSegments; - numSegments = other.numSegments; - blockSize = other.blockSize; - inputDataPos = other.inputDataPos; - - bufferInput = other.bufferInput; - bufferTempOutput = other.bufferTempOutput; - bufferOutput = other.bufferOutput; - - buffersInputSegments = other.buffersInputSegments; - buffersImpulseSegments = other.buffersImpulseSegments; - bufferOverlap = other.bufferOverlap; - - isReady = true; - } - - /** Performs the uniform partitioned convolution using FFT. */ void processSamples (const float* input, float* output, size_t numSamples) { - if (! isReady) - return; - // Overlap-add, zero latency convolution algorithm with uniform partitioning size_t numSamplesProcessed = 0; @@ -185,20 +111,18 @@ struct ConvolutionEngine const bool inputDataWasEmpty = (inputDataPos == 0); auto numSamplesToProcess = jmin (numSamples - numSamplesProcessed, blockSize - inputDataPos); - // copy the input samples FloatVectorOperations::copy (inputData + inputDataPos, input + numSamplesProcessed, static_cast (numSamplesToProcess)); - auto* inputSegmentData = buffersInputSegments.getReference (static_cast (currentSegment)).getWritePointer (0); - FloatVectorOperations::copy (inputSegmentData, inputData, static_cast (FFTSize)); + auto* inputSegmentData = buffersInputSegments[currentSegment].getWritePointer (0); + FloatVectorOperations::copy (inputSegmentData, inputData, static_cast (fftSize)); - // Forward FFT - FFTobject->performRealOnlyForwardTransform (inputSegmentData); + fftObject->performRealOnlyForwardTransform (inputSegmentData); prepareForConvolution (inputSegmentData); // Complex multiplication if (inputDataWasEmpty) { - FloatVectorOperations::fill (outputTempData, 0, static_cast (FFTSize + 1)); + FloatVectorOperations::fill (outputTempData, 0, static_cast (fftSize + 1)); auto index = currentSegment; @@ -209,25 +133,23 @@ struct ConvolutionEngine if (index >= numInputSegments) index -= numInputSegments; - convolutionProcessingAndAccumulate (buffersInputSegments.getReference (static_cast (index)).getWritePointer (0), - buffersImpulseSegments.getReference (static_cast (i)).getWritePointer (0), + convolutionProcessingAndAccumulate (buffersInputSegments[index].getWritePointer (0), + buffersImpulseSegments[i].getWritePointer (0), outputTempData); } } - FloatVectorOperations::copy (outputData, outputTempData, static_cast (FFTSize + 1)); + FloatVectorOperations::copy (outputData, outputTempData, static_cast (fftSize + 1)); - convolutionProcessingAndAccumulate (buffersInputSegments.getReference (static_cast (currentSegment)).getWritePointer (0), - buffersImpulseSegments.getReference (0).getWritePointer (0), + convolutionProcessingAndAccumulate (inputSegmentData, + buffersImpulseSegments.front().getWritePointer (0), outputData); - // Inverse FFT updateSymmetricFrequencyDomainData (outputData); - FFTobject->performRealOnlyInverseTransform (outputData); + fftObject->performRealOnlyInverseTransform (outputData); // Add overlap - for (size_t i = 0; i < numSamplesToProcess; ++i) - output[i + numSamplesProcessed] = outputData[inputDataPos + i] + overlapData[inputDataPos + i]; + FloatVectorOperations::add (&output[numSamplesProcessed], &outputData[inputDataPos], &overlapData[inputDataPos], (int) numSamplesToProcess); // Input buffer full => Next block inputDataPos += numSamplesToProcess; @@ -235,17 +157,16 @@ struct ConvolutionEngine if (inputDataPos == blockSize) { // Input buffer is empty again now - FloatVectorOperations::fill (inputData, 0.0f, static_cast (FFTSize)); + FloatVectorOperations::fill (inputData, 0.0f, static_cast (fftSize)); inputDataPos = 0; // Extra step for segSize > blockSize - FloatVectorOperations::add (&(outputData[blockSize]), &(overlapData[blockSize]), static_cast (FFTSize - 2 * blockSize)); + FloatVectorOperations::add (&(outputData[blockSize]), &(overlapData[blockSize]), static_cast (fftSize - 2 * blockSize)); // Save the overlap - FloatVectorOperations::copy (overlapData, &(outputData[blockSize]), static_cast (FFTSize - blockSize)); + FloatVectorOperations::copy (overlapData, &(outputData[blockSize]), static_cast (fftSize - blockSize)); - // Update current segment currentSegment = (currentSegment > 0) ? (currentSegment - 1) : (numInputSegments - 1); } @@ -253,24 +174,102 @@ struct ConvolutionEngine } } - /** After each FFT, this function is called to allow convolution to be performed with only 4 SIMD functions calls. */ + void processSamplesWithAddedLatency (const float* input, float* output, size_t numSamples) + { + // Overlap-add, zero latency convolution algorithm with uniform partitioning + size_t numSamplesProcessed = 0; + + auto indexStep = numInputSegments / numSegments; + + auto* inputData = bufferInput.getWritePointer (0); + auto* outputTempData = bufferTempOutput.getWritePointer (0); + auto* outputData = bufferOutput.getWritePointer (0); + auto* overlapData = bufferOverlap.getWritePointer (0); + + while (numSamplesProcessed < numSamples) + { + auto numSamplesToProcess = jmin (numSamples - numSamplesProcessed, blockSize - inputDataPos); + + FloatVectorOperations::copy (inputData + inputDataPos, input + numSamplesProcessed, static_cast (numSamplesToProcess)); + + FloatVectorOperations::copy (output + numSamplesProcessed, outputData + inputDataPos, static_cast (numSamplesToProcess)); + + numSamplesProcessed += numSamplesToProcess; + inputDataPos += numSamplesToProcess; + + // processing itself when needed (with latency) + if (inputDataPos == blockSize) + { + // Copy input data in input segment + auto* inputSegmentData = buffersInputSegments[currentSegment].getWritePointer (0); + FloatVectorOperations::copy (inputSegmentData, inputData, static_cast (fftSize)); + + fftObject->performRealOnlyForwardTransform (inputSegmentData); + prepareForConvolution (inputSegmentData); + + // Complex multiplication + FloatVectorOperations::fill (outputTempData, 0, static_cast (fftSize + 1)); + + auto index = currentSegment; + + for (size_t i = 1; i < numSegments; ++i) + { + index += indexStep; + + if (index >= numInputSegments) + index -= numInputSegments; + + convolutionProcessingAndAccumulate (buffersInputSegments[index].getWritePointer (0), + buffersImpulseSegments[i].getWritePointer (0), + outputTempData); + } + + FloatVectorOperations::copy (outputData, outputTempData, static_cast (fftSize + 1)); + + convolutionProcessingAndAccumulate (inputSegmentData, + buffersImpulseSegments.front().getWritePointer (0), + outputData); + + updateSymmetricFrequencyDomainData (outputData); + fftObject->performRealOnlyInverseTransform (outputData); + + // Add overlap + FloatVectorOperations::add (outputData, overlapData, static_cast (blockSize)); + + // Input buffer is empty again now + FloatVectorOperations::fill (inputData, 0.0f, static_cast (fftSize)); + + // Extra step for segSize > blockSize + FloatVectorOperations::add (&(outputData[blockSize]), &(overlapData[blockSize]), static_cast (fftSize - 2 * blockSize)); + + // Save the overlap + FloatVectorOperations::copy (overlapData, &(outputData[blockSize]), static_cast (fftSize - blockSize)); + + currentSegment = (currentSegment > 0) ? (currentSegment - 1) : (numInputSegments - 1); + + inputDataPos = 0; + } + } + } + + // After each FFT, this function is called to allow convolution to be performed with only 4 SIMD functions calls. void prepareForConvolution (float *samples) noexcept { - auto FFTSizeDiv2 = FFTSize / 2; + auto FFTSizeDiv2 = fftSize / 2; for (size_t i = 0; i < FFTSizeDiv2; i++) - samples[i] = samples[2 * i]; + samples[i] = samples[i << 1]; samples[FFTSizeDiv2] = 0; for (size_t i = 1; i < FFTSizeDiv2; i++) - samples[i + FFTSizeDiv2] = -samples[2 * (FFTSize - i) + 1]; + samples[i + FFTSizeDiv2] = -samples[((fftSize - i) << 1) + 1]; } - /** Does the convolution operation itself only on half of the frequency domain samples. */ + // Does the convolution operation itself only on half of the frequency domain samples. void convolutionProcessingAndAccumulate (const float *input, const float *impulse, float *output) { - auto FFTSizeDiv2 = FFTSize / 2; + auto FFTSizeDiv2 = fftSize / 2; FloatVectorOperations::addWithMultiply (output, input, impulse, static_cast (FFTSizeDiv2)); FloatVectorOperations::subtractWithMultiply (output, &(input[FFTSizeDiv2]), &(impulse[FFTSizeDiv2]), static_cast (FFTSizeDiv2)); @@ -278,928 +277,827 @@ struct ConvolutionEngine FloatVectorOperations::addWithMultiply (&(output[FFTSizeDiv2]), input, &(impulse[FFTSizeDiv2]), static_cast (FFTSizeDiv2)); FloatVectorOperations::addWithMultiply (&(output[FFTSizeDiv2]), &(input[FFTSizeDiv2]), impulse, static_cast (FFTSizeDiv2)); - output[FFTSize] += input[FFTSize] * impulse[FFTSize]; + output[fftSize] += input[fftSize] * impulse[fftSize]; } - /** Undo the re-organization of samples from the function prepareForConvolution. - Then, takes the conjugate of the frequency domain first half of samples, to fill the - second half, so that the inverse transform will return real samples in the time domain. - */ + // Undoes the re-organization of samples from the function prepareForConvolution. + // Then takes the conjugate of the frequency domain first half of samples to fill the + // second half, so that the inverse transform will return real samples in the time domain. void updateSymmetricFrequencyDomainData (float* samples) noexcept { - auto FFTSizeDiv2 = FFTSize / 2; + auto FFTSizeDiv2 = fftSize / 2; for (size_t i = 1; i < FFTSizeDiv2; i++) { - samples[2 * (FFTSize - i)] = samples[i]; - samples[2 * (FFTSize - i) + 1] = -samples[FFTSizeDiv2 + i]; + samples[(fftSize - i) << 1] = samples[i]; + samples[((fftSize - i) << 1) + 1] = -samples[FFTSizeDiv2 + i]; } samples[1] = 0.f; for (size_t i = 1; i < FFTSizeDiv2; i++) { - samples[2 * i] = samples[2 * (FFTSize - i)]; - samples[2 * i + 1] = -samples[2 * (FFTSize - i) + 1]; + samples[i << 1] = samples[(fftSize - i) << 1]; + samples[(i << 1) + 1] = -samples[((fftSize - i) << 1) + 1]; } } //============================================================================== - std::unique_ptr FFTobject; - - size_t FFTSize = 0; - size_t currentSegment = 0, numInputSegments = 0, numSegments = 0, blockSize = 0, inputDataPos = 0; + const size_t blockSize; + const size_t fftSize; + const std::unique_ptr fftObject; + const size_t numSegments; + const size_t numInputSegments; + size_t currentSegment = 0, inputDataPos = 0; AudioBuffer bufferInput, bufferOutput, bufferTempOutput, bufferOverlap; - Array> buffersInputSegments, buffersImpulseSegments; - - bool isReady = false; - - //============================================================================== - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ConvolutionEngine) + std::vector> buffersInputSegments, buffersImpulseSegments; }; - - //============================================================================== -/** Manages all the changes requested by the main convolution engine, to minimize - the number of calls of the convolution engine initialization, and the potential - consequences of multiple quick calls to the function Convolution::loadImpulseResponse. -*/ -struct Convolution::Pimpl : private Thread +template +class Queue { - enum class ChangeRequest +public: + explicit Queue (int size) + : fifo (size), storage (static_cast (size)) {} + + bool push (Element& element) noexcept { - changeEngine = 0, - changeSampleRate, - changeMaximumBufferSize, - changeSource, - changeImpulseResponseSize, - changeStereo, - changeTrimming, - changeNormalisation, - changeIgnore, - numChangeRequestTypes - }; + if (fifo.getFreeSpace() == 0) + return false; - using SourceType = ConvolutionEngine::ProcessingInformation::SourceType; + const auto writer = fifo.write (1); - //============================================================================== - Pimpl() : Thread ("Convolution"), abstractFifo (fifoSize) - { - abstractFifo.reset(); - fifoRequestsType.resize (fifoSize); - fifoRequestsParameter.resize (fifoSize); + if (writer.blockSize1 != 0) + storage[static_cast (writer.startIndex1)] = std::move (element); + else if (writer.blockSize2 != 0) + storage[static_cast (writer.startIndex2)] = std::move (element); - requestsType.resize (fifoSize); - requestsParameter.resize (fifoSize); - - for (auto i = 0; i < 4; ++i) - engines.add (new ConvolutionEngine()); - - currentInfo.maximumBufferSize = 0; - currentInfo.buffer = &impulseResponse; - - temporaryBuffer.setSize (2, static_cast (maximumTimeInSamples), false, false, true); - impulseResponseOriginal.setSize (2, static_cast (maximumTimeInSamples), false, false, true); - impulseResponse.setSize (2, static_cast (maximumTimeInSamples), false, false, true); + return true; } - ~Pimpl() override - { - stopThread (10000); - } + template + void pop (Fn&& fn) { popN (1, std::forward (fn)); } - //============================================================================== - /** Inits the size of the interpolation buffer. */ - void initProcessing (int maximumBufferSize) - { - stopThread (1000); + template + void popAll (Fn&& fn) { popN (fifo.getNumReady(), std::forward (fn)); } - interpolationBuffer.setSize (1, maximumBufferSize, false, false, true); - mustInterpolate = false; - } - - //============================================================================== - /** Adds a new change request. */ - void addToFifo (ChangeRequest type, juce::var parameter) - { - int start1, size1, start2, size2; - abstractFifo.prepareToWrite (1, start1, size1, start2, size2); - - // If you hit this assertion then you have requested more impulse response - // changes than the Convolution class can handle. - jassert (size1 + size2 > 0); - - if (size1 > 0) - { - fifoRequestsType.setUnchecked (start1, type); - fifoRequestsParameter.setUnchecked (start1, parameter); - } - - if (size2 > 0) - { - fifoRequestsType.setUnchecked (start2, type); - fifoRequestsParameter.setUnchecked (start2, parameter); - } - - abstractFifo.finishedWrite (size1 + size2); - } - - /** Adds a new array of change requests. */ - void addToFifo (ChangeRequest* types, juce::var* parameters, int numEntries) - { - int start1, size1, start2, size2; - abstractFifo.prepareToWrite (numEntries, start1, size1, start2, size2); - - // If you hit this assertion then you have requested more impulse response - // changes than the Convolution class can handle. - jassert (numEntries > 0 && size1 + size2 > 0); - - if (size1 > 0) - { - for (auto i = 0; i < size1; ++i) - { - fifoRequestsType.setUnchecked (start1 + i, types[i]); - fifoRequestsParameter.setUnchecked (start1 + i, parameters[i]); - } - } - - if (size2 > 0) - { - for (auto i = 0; i < size2; ++i) - { - fifoRequestsType.setUnchecked (start2 + i, types[i + size1]); - fifoRequestsParameter.setUnchecked (start2 + i, parameters[i + size1]); - } - } - - abstractFifo.finishedWrite (size1 + size2); - } - - /** Reads requests from the fifo. */ - void readFromFifo (ChangeRequest& type, juce::var& parameter) - { - int start1, size1, start2, size2; - abstractFifo.prepareToRead (1, start1, size1, start2, size2); - - if (size1 > 0) - { - type = fifoRequestsType[start1]; - parameter = fifoRequestsParameter[start1]; - } - - if (size2 > 0) - { - type = fifoRequestsType[start2]; - parameter = fifoRequestsParameter[start2]; - } - - abstractFifo.finishedRead (size1 + size2); - } - - /** Returns the number of requests that still need to be processed. */ - int getNumRemainingEntries() const noexcept - { - return abstractFifo.getNumReady(); - } - - //============================================================================== - /** This function processes all the change requests to remove all the the - redundant ones, and to tell what kind of initialization must be done. - - Depending on the results, the convolution engines might be reset, or - simply updated, or they might not need any change at all. - */ - void processFifo() - { - if (getNumRemainingEntries() == 0 || isThreadRunning() || mustInterpolate) - return; - - auto numRequests = 0; - - // retrieve the information from the FIFO for processing - while (getNumRemainingEntries() > 0 && numRequests < fifoSize) - { - ChangeRequest type = ChangeRequest::changeEngine; - juce::var parameter; - - readFromFifo (type, parameter); - - requestsType.setUnchecked (numRequests, type); - requestsParameter.setUnchecked (numRequests, parameter); - - numRequests++; - } - - // remove any useless messages - for (auto i = 0; i < (int) ChangeRequest::numChangeRequestTypes; ++i) - { - bool exists = false; - - for (auto n = numRequests; --n >= 0;) - { - if (requestsType[n] == (ChangeRequest) i) - { - if (! exists) - exists = true; - else - requestsType.setUnchecked (n, ChangeRequest::changeIgnore); - } - } - } - - changeLevel = 0; - - for (auto n = 0; n < numRequests; ++n) - { - switch (requestsType[n]) - { - case ChangeRequest::changeEngine: - changeLevel = 3; - break; - - case ChangeRequest::changeSampleRate: - { - double newSampleRate = requestsParameter[n]; - - if (currentInfo.sampleRate != newSampleRate) - changeLevel = 3; - - currentInfo.sampleRate = newSampleRate; - } - break; - - case ChangeRequest::changeMaximumBufferSize: - { - int newMaximumBufferSize = requestsParameter[n]; - - if (currentInfo.maximumBufferSize != (size_t) newMaximumBufferSize) - changeLevel = 3; - - currentInfo.maximumBufferSize = (size_t) newMaximumBufferSize; - } - break; - - case ChangeRequest::changeSource: - { - auto* arrayParameters = requestsParameter[n].getArray(); - auto newSourceType = static_cast (static_cast (arrayParameters->getUnchecked (0))); - - if (currentInfo.sourceType != newSourceType) - changeLevel = jmax (2, changeLevel); - - if (newSourceType == SourceType::sourceBinaryData) - { - auto& prm = arrayParameters->getRawDataPointer()[1]; - auto* newMemoryBlock = prm.getBinaryData(); - - auto* newPtr = newMemoryBlock->getData(); - auto newSize = (int) newMemoryBlock->getSize(); - - if (currentInfo.sourceData != newPtr || currentInfo.sourceDataSize != newSize) - changeLevel = jmax (2, changeLevel); - - currentInfo.sourceType = SourceType::sourceBinaryData; - currentInfo.sourceData = newPtr; - currentInfo.sourceDataSize = newSize; - currentInfo.fileImpulseResponse = File(); - } - else if (newSourceType == SourceType::sourceAudioFile) - { - File newFile (arrayParameters->getUnchecked (1).toString()); - - if (currentInfo.fileImpulseResponse != newFile) - changeLevel = jmax (2, changeLevel); - - currentInfo.sourceType = SourceType::sourceAudioFile; - currentInfo.fileImpulseResponse = newFile; - currentInfo.sourceData = nullptr; - currentInfo.sourceDataSize = 0; - } - else if (newSourceType == SourceType::sourceAudioBuffer) - { - double originalSampleRate (arrayParameters->getUnchecked (1)); - changeLevel = jmax (2, changeLevel); - - currentInfo.sourceType = SourceType::sourceAudioBuffer; - currentInfo.originalSampleRate = originalSampleRate; - currentInfo.fileImpulseResponse = File(); - currentInfo.sourceData = nullptr; - currentInfo.sourceDataSize = 0; - } - } - break; - - case ChangeRequest::changeImpulseResponseSize: - { - int64 newSize = requestsParameter[n]; - - if (currentInfo.wantedSize != newSize) - changeLevel = jmax (1, changeLevel); - - currentInfo.wantedSize = newSize; - } - break; - - case ChangeRequest::changeStereo: - { - bool newWantsStereo = requestsParameter[n]; - - if (currentInfo.wantsStereo != newWantsStereo) - changeLevel = jmax (0, changeLevel); - - currentInfo.wantsStereo = newWantsStereo; - } - break; - - case ChangeRequest::changeTrimming: - { - bool newWantsTrimming = requestsParameter[n]; - - if (currentInfo.wantsTrimming != newWantsTrimming) - changeLevel = jmax (1, changeLevel); - - currentInfo.wantsTrimming = newWantsTrimming; - } - break; - - case ChangeRequest::changeNormalisation: - { - bool newWantsNormalisation = requestsParameter[n]; - - if (currentInfo.wantsNormalisation != newWantsNormalisation) - changeLevel = jmax (1, changeLevel); - - currentInfo.wantsNormalisation = newWantsNormalisation; - } - break; - - case ChangeRequest::numChangeRequestTypes: - case ChangeRequest::changeIgnore: - break; - - default: - jassertfalse; - break; - } - } - - if (currentInfo.sourceType == SourceType::sourceNone) - { - currentInfo.sourceType = SourceType::sourceAudioBuffer; - - if (currentInfo.sampleRate == 0) - currentInfo.sampleRate = 44100; - - if (currentInfo.maximumBufferSize == 0) - currentInfo.maximumBufferSize = 128; - - currentInfo.originalSampleRate = currentInfo.sampleRate; - currentInfo.wantedSize = 1; - currentInfo.fileImpulseResponse = File(); - currentInfo.sourceData = nullptr; - currentInfo.sourceDataSize = 0; - - AudioBuffer newBuffer; - newBuffer.setSize (1, 1); - newBuffer.setSample (0, 0, 1.f); - - copyBufferToTemporaryLocation (newBuffer); - } - - // action depending on the change level - if (changeLevel == 3) - { - loadImpulseResponse(); - processImpulseResponse(); - initializeConvolutionEngines(); - } - else if (changeLevel > 0) - { - startThread(); - } - } - - //============================================================================== - /** This function copies a buffer to a temporary location, so that any external - audio source can be processed then in the dedicated thread. - */ - void copyBufferToTemporaryLocation (AudioBlock block) - { - const SpinLock::ScopedLockType sl (processLock); - - currentInfo.originalNumChannels = (block.getNumChannels() > 1 ? 2 : 1); - currentInfo.originalSize = (int) jmin ((size_t) maximumTimeInSamples, block.getNumSamples()); - - for (auto channel = 0; channel < currentInfo.originalNumChannels; ++channel) - temporaryBuffer.copyFrom (channel, 0, block.getChannelPointer ((size_t) channel), (int) currentInfo.originalSize); - } - - //============================================================================== - /** Resets the convolution engines states. */ - void reset() - { - for (auto* e : engines) - e->reset(); - - mustInterpolate = false; - - processFifo(); - } - - /** Convolution processing handling interpolation between previous and new states - of the convolution engines. - */ - void processSamples (const AudioBlock& input, AudioBlock& output) - { - processFifo(); - - size_t numChannels = jmin (input.getNumChannels(), (size_t) (currentInfo.wantsStereo ? 2 : 1)); - size_t numSamples = jmin (input.getNumSamples(), output.getNumSamples()); - - if (mustInterpolate == false) - { - for (size_t channel = 0; channel < numChannels; ++channel) - engines[(int) channel]->processSamples (input.getChannelPointer (channel), output.getChannelPointer (channel), numSamples); - } - else - { - auto interpolated = AudioBlock (interpolationBuffer).getSubBlock (0, numSamples); - - for (size_t channel = 0; channel < numChannels; ++channel) - { - auto&& buffer = output.getSingleChannelBlock (channel); - - interpolationBuffer.copyFrom (0, 0, input.getChannelPointer (channel), (int) numSamples); - - engines[(int) channel]->processSamples (input.getChannelPointer (channel), buffer.getChannelPointer (0), numSamples); - changeVolumes[channel].applyGain (buffer.getChannelPointer (0), (int) numSamples); - - auto* interPtr = interpolationBuffer.getWritePointer (0); - engines[(int) channel + 2]->processSamples (interPtr, interPtr, numSamples); - changeVolumes[channel + 2].applyGain (interPtr, (int) numSamples); - - buffer += interpolated; - } - - if (input.getNumChannels() > 1 && currentInfo.wantsStereo == false) - { - auto&& buffer = output.getSingleChannelBlock (1); - - changeVolumes[1].applyGain (buffer.getChannelPointer (0), (int) numSamples); - changeVolumes[3].applyGain (buffer.getChannelPointer (0), (int) numSamples); - } - - if (changeVolumes[0].isSmoothing() == false) - { - mustInterpolate = false; - - for (auto channel = 0; channel < 2; ++channel) - engines[channel]->copyStateFromOtherEngine (*engines[channel + 2]); - } - } - - if (input.getNumChannels() > 1 && currentInfo.wantsStereo == false) - output.getSingleChannelBlock (1).copyFrom (output.getSingleChannelBlock (0)); - } - - //============================================================================== - const int64 maximumTimeInSamples = 10 * 96000; + bool hasPendingMessages() const noexcept { return fifo.getNumReady() > 0; } private: - //============================================================================== - /** This the thread run function which does the preparation of data depending - on the requested change level. - */ - void run() override + template + void popN (int n, Fn&& fn) { - if (changeLevel == 2) + fifo.read (n).forEach ([&] (int index) { - loadImpulseResponse(); - - if (isThreadRunning() && threadShouldExit()) - return; - } - - processImpulseResponse(); - - if (isThreadRunning() && threadShouldExit()) - return; - - initializeConvolutionEngines(); + fn (storage[static_cast (index)]); + }); } - /** Loads the impulse response from the requested audio source. */ - void loadImpulseResponse() - { - if (currentInfo.sourceType == SourceType::sourceBinaryData) - { - copyAudioStreamInAudioBuffer (std::make_unique (currentInfo.sourceData, (size_t) currentInfo.sourceDataSize, false)); - } - else if (currentInfo.sourceType == SourceType::sourceAudioFile) - { - copyAudioStreamInAudioBuffer (std::make_unique (currentInfo.fileImpulseResponse)); - } - else if (currentInfo.sourceType == SourceType::sourceAudioBuffer) - { - copyBufferFromTemporaryLocation(); - } - } - - /** Processes the impulse response data with the requested treatments - and resampling if needed. - */ - void processImpulseResponse() - { - trimAndResampleImpulseResponse (currentInfo.originalNumChannels, currentInfo.originalSampleRate, currentInfo.wantsTrimming); - - if (isThreadRunning() && threadShouldExit()) - return; - - if (currentInfo.wantsNormalisation) - { - if (currentInfo.originalNumChannels > 1) - { - normaliseImpulseResponse (currentInfo.buffer->getWritePointer (0), (int) currentInfo.finalSize, 1.0); - normaliseImpulseResponse (currentInfo.buffer->getWritePointer (1), (int) currentInfo.finalSize, 1.0); - } - else - { - normaliseImpulseResponse (currentInfo.buffer->getWritePointer (0), (int) currentInfo.finalSize, 1.0); - } - } - - if (currentInfo.originalNumChannels == 1) - currentInfo.buffer->copyFrom (1, 0, *currentInfo.buffer, 0, 0, (int) currentInfo.finalSize); - } - - /** Converts the data from an audio file into a stereo audio buffer of floats, and - performs resampling if necessary. - */ - bool copyAudioStreamInAudioBuffer (std::unique_ptr stream) - { - AudioFormatManager manager; - manager.registerBasicFormats(); - std::unique_ptr formatReader (manager.createReaderFor (std::move (stream))); - - if (formatReader != nullptr) - { - currentInfo.originalNumChannels = formatReader->numChannels > 1 ? 2 : 1; - currentInfo.originalSampleRate = formatReader->sampleRate; - currentInfo.originalSize = static_cast (jmin (maximumTimeInSamples, formatReader->lengthInSamples)); - - impulseResponseOriginal.clear(); - formatReader->read (&(impulseResponseOriginal), 0, (int) currentInfo.originalSize, 0, true, currentInfo.originalNumChannels > 1); - - return true; - } - - return false; - } - - /** Copies a buffer from a temporary location to the impulseResponseOriginal - buffer for the sourceAudioBuffer. - */ - void copyBufferFromTemporaryLocation() - { - const SpinLock::ScopedLockType sl (processLock); - - for (auto channel = 0; channel < currentInfo.originalNumChannels; ++channel) - impulseResponseOriginal.copyFrom (channel, 0, temporaryBuffer, channel, 0, (int) currentInfo.originalSize); - } - - /** Trim and resample the impulse response if needed. */ - void trimAndResampleImpulseResponse (int numChannels, double srcSampleRate, bool mustTrim) - { - auto thresholdTrim = Decibels::decibelsToGain (-80.0f); - auto indexStart = 0; - auto indexEnd = currentInfo.originalSize - 1; - - if (mustTrim) - { - indexStart = currentInfo.originalSize - 1; - indexEnd = 0; - - for (auto channel = 0; channel < numChannels; ++channel) - { - auto localIndexStart = 0; - auto localIndexEnd = currentInfo.originalSize - 1; - - auto* channelData = impulseResponseOriginal.getReadPointer (channel); - - while (localIndexStart < currentInfo.originalSize - 1 - && channelData[localIndexStart] <= thresholdTrim - && channelData[localIndexStart] >= -thresholdTrim) - ++localIndexStart; - - while (localIndexEnd >= 0 - && channelData[localIndexEnd] <= thresholdTrim - && channelData[localIndexEnd] >= -thresholdTrim) - --localIndexEnd; - - indexStart = jmin (indexStart, localIndexStart); - indexEnd = jmax (indexEnd, localIndexEnd); - } - - if (indexStart > 0) - { - for (auto channel = 0; channel < numChannels; ++channel) - { - auto* channelData = impulseResponseOriginal.getWritePointer (channel); - - for (auto i = 0; i < indexEnd - indexStart + 1; ++i) - channelData[i] = channelData[i + indexStart]; - - for (auto i = indexEnd - indexStart + 1; i < currentInfo.originalSize - 1; ++i) - channelData[i] = 0.0f; - } - } - } - - if (currentInfo.sampleRate == srcSampleRate) - { - // No resampling - currentInfo.finalSize = jmin (static_cast (currentInfo.wantedSize), indexEnd - indexStart + 1); - - impulseResponse.clear(); - - for (auto channel = 0; channel < numChannels; ++channel) - impulseResponse.copyFrom (channel, 0, impulseResponseOriginal, channel, 0, (int) currentInfo.finalSize); - } - else - { - // Resampling - auto factorReading = srcSampleRate / currentInfo.sampleRate; - currentInfo.finalSize = jmin (static_cast (currentInfo.wantedSize), roundToInt ((indexEnd - indexStart + 1) / factorReading)); - - impulseResponse.clear(); - - MemoryAudioSource memorySource (impulseResponseOriginal, false); - ResamplingAudioSource resamplingSource (&memorySource, false, (int) numChannels); - - resamplingSource.setResamplingRatio (factorReading); - resamplingSource.prepareToPlay ((int) currentInfo.finalSize, currentInfo.sampleRate); - - AudioSourceChannelInfo info; - info.startSample = 0; - info.numSamples = (int) currentInfo.finalSize; - info.buffer = &impulseResponse; - - resamplingSource.getNextAudioBlock (info); - } - - // Filling the second channel with the first if necessary - if (numChannels == 1) - impulseResponse.copyFrom (1, 0, impulseResponse, 0, 0, (int) currentInfo.finalSize); - } - - /** Normalisation of the impulse response based on its energy. */ - void normaliseImpulseResponse (float* samples, int numSamples, double factorResampling) const - { - auto magnitude = 0.0f; - - for (auto i = 0; i < numSamples; ++i) - magnitude += samples[i] * samples[i]; - - auto magnitudeInv = 1.0f / (4.0f * std::sqrt (magnitude)) * 0.5f * static_cast (factorResampling); - - for (auto i = 0; i < numSamples; ++i) - samples[i] *= magnitudeInv; - } - - // ================================================================================================================ - /** Initializes the convolution engines depending on the provided sizes - and performs the FFT on the impulse responses. - */ - void initializeConvolutionEngines() - { - if (currentInfo.maximumBufferSize == 0) - return; - - if (changeLevel == 3) - { - for (auto i = 0; i < 2; ++i) - engines[i]->initializeConvolutionEngine (currentInfo, i); - - mustInterpolate = false; - } - else - { - for (auto i = 0; i < 2; ++i) - { - engines[i + 2]->initializeConvolutionEngine (currentInfo, i); - engines[i + 2]->reset(); - - if (isThreadRunning() && threadShouldExit()) - return; - } - - for (auto i = 0; i < 2; ++i) - { - changeVolumes[i].setTargetValue (1.0f); - changeVolumes[i].reset (currentInfo.sampleRate, 0.05); - changeVolumes[i].setTargetValue (0.0f); - - changeVolumes[i + 2].setTargetValue (0.0f); - changeVolumes[i + 2].reset (currentInfo.sampleRate, 0.05); - changeVolumes[i + 2].setTargetValue (1.0f); - - } - - mustInterpolate = true; - } - } - - - //============================================================================== - static constexpr int fifoSize = 1024; // the size of the fifo which handles all the change requests - AbstractFifo abstractFifo; // the abstract fifo - - Array fifoRequestsType; // an array of ChangeRequest - Array fifoRequestsParameter; // an array of change parameters - - Array requestsType; // an array of ChangeRequest - Array requestsParameter; // an array of change parameters - - int changeLevel = 0; // the current level of requested change in the convolution engine - - //============================================================================== - ConvolutionEngine::ProcessingInformation currentInfo; // the information about the impulse response to load - - AudioBuffer temporaryBuffer; // a temporary buffer that is used when the function copyAndLoadImpulseResponse is called in the main API - SpinLock processLock; // a necessary lock to use with this temporary buffer - - AudioBuffer impulseResponseOriginal; // a buffer with the original impulse response - AudioBuffer impulseResponse; // a buffer with the impulse response trimmed, resampled, resized and normalised - - //============================================================================== - OwnedArray engines; // the 4 convolution engines being used - - AudioBuffer interpolationBuffer; // a buffer to do the interpolation between the convolution engines 0-1 and 2-3 - LogRampedValue changeVolumes[4]; // the volumes for each convolution engine during interpolation - - bool mustInterpolate = false; // tells if the convolution engines outputs must be currently interpolated - - //============================================================================== - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl) + AbstractFifo fifo; + std::vector storage; }; - //============================================================================== -Convolution::Convolution() +class MultichannelEngine { - pimpl.reset (new Pimpl()); - pimpl->addToFifo (Convolution::Pimpl::ChangeRequest::changeEngine, juce::var (0)); -} - -Convolution::~Convolution() -{ -} - -void Convolution::loadImpulseResponse (const void* sourceData, size_t sourceDataSize, - bool wantsStereo, bool wantsTrimming, size_t size, - bool wantsNormalisation) -{ - if (sourceData == nullptr) - return; - - auto maximumSamples = (size_t) pimpl->maximumTimeInSamples; - auto wantedSize = (size == 0 ? maximumSamples : jmin (size, maximumSamples)); - - Pimpl::ChangeRequest types[] = { Pimpl::ChangeRequest::changeSource, - Pimpl::ChangeRequest::changeImpulseResponseSize, - Pimpl::ChangeRequest::changeStereo, - Pimpl::ChangeRequest::changeTrimming, - Pimpl::ChangeRequest::changeNormalisation }; - - Array sourceParameter; - - sourceParameter.add (juce::var ((int) ConvolutionEngine::ProcessingInformation::SourceType::sourceBinaryData)); - sourceParameter.add (juce::var (sourceData, sourceDataSize)); - - juce::var parameters[] = { juce::var (sourceParameter), - juce::var (static_cast (wantedSize)), - juce::var (wantsStereo), - juce::var (wantsTrimming), - juce::var (wantsNormalisation) }; - - pimpl->addToFifo (types, parameters, 5); -} - -void Convolution::loadImpulseResponse (const File& fileImpulseResponse, bool wantsStereo, - bool wantsTrimming, size_t size, bool wantsNormalisation) -{ - if (! fileImpulseResponse.existsAsFile()) - return; - - auto maximumSamples = (size_t) pimpl->maximumTimeInSamples; - auto wantedSize = (size == 0 ? maximumSamples : jmin (size, maximumSamples)); - - Pimpl::ChangeRequest types[] = { Pimpl::ChangeRequest::changeSource, - Pimpl::ChangeRequest::changeImpulseResponseSize, - Pimpl::ChangeRequest::changeStereo, - Pimpl::ChangeRequest::changeTrimming, - Pimpl::ChangeRequest::changeNormalisation }; - - Array sourceParameter; - - sourceParameter.add (juce::var ((int) ConvolutionEngine::ProcessingInformation::SourceType::sourceAudioFile)); - sourceParameter.add (juce::var (fileImpulseResponse.getFullPathName())); - - juce::var parameters[] = { juce::var (sourceParameter), - juce::var (static_cast (wantedSize)), - juce::var (wantsStereo), - juce::var (wantsTrimming), - juce::var (wantsNormalisation) }; - - pimpl->addToFifo (types, parameters, 5); -} - -void Convolution::copyAndLoadImpulseResponseFromBuffer (AudioBuffer& buffer, - double bufferSampleRate, bool wantsStereo, bool wantsTrimming, bool wantsNormalisation, size_t size) -{ - copyAndLoadImpulseResponseFromBlock (AudioBlock (buffer), bufferSampleRate, - wantsStereo, wantsTrimming, wantsNormalisation, size); -} - -void Convolution::copyAndLoadImpulseResponseFromBlock (AudioBlock block, double bufferSampleRate, - bool wantsStereo, bool wantsTrimming, bool wantsNormalisation, size_t size) -{ - jassert (bufferSampleRate > 0); - - if (block.getNumSamples() == 0) - return; - - auto maximumSamples = (size_t) pimpl->maximumTimeInSamples; - auto wantedSize = (size == 0 ? maximumSamples : jmin (size, maximumSamples)); - - pimpl->copyBufferToTemporaryLocation (block); - - Pimpl::ChangeRequest types[] = { Pimpl::ChangeRequest::changeSource, - Pimpl::ChangeRequest::changeImpulseResponseSize, - Pimpl::ChangeRequest::changeStereo, - Pimpl::ChangeRequest::changeTrimming, - Pimpl::ChangeRequest::changeNormalisation }; - - Array sourceParameter; - sourceParameter.add (juce::var ((int) ConvolutionEngine::ProcessingInformation::SourceType::sourceAudioBuffer)); - sourceParameter.add (juce::var (bufferSampleRate)); - - juce::var parameters[] = { juce::var (sourceParameter), - juce::var (static_cast (wantedSize)), - juce::var (wantsStereo), - juce::var (wantsTrimming), - juce::var (wantsNormalisation) }; - - pimpl->addToFifo (types, parameters, 5); -} - -void Convolution::prepare (const ProcessSpec& spec) -{ - jassert (isPositiveAndBelow (spec.numChannels, static_cast (3))); // only mono and stereo is supported - - Pimpl::ChangeRequest types[] = { Pimpl::ChangeRequest::changeSampleRate, - Pimpl::ChangeRequest::changeMaximumBufferSize }; - - juce::var parameters[] = { juce::var (spec.sampleRate), - juce::var (static_cast (spec.maximumBlockSize)) }; - - pimpl->addToFifo (types, parameters, 2); - pimpl->initProcessing (static_cast (spec.maximumBlockSize)); - - for (size_t channel = 0; channel < spec.numChannels; ++channel) +public: + MultichannelEngine (const AudioBuffer& buf, + int maxBlockSize, + int maxBufferSize, + Convolution::NonUniform headSizeIn, + bool isZeroDelayIn) + : tailBuffer (1, maxBlockSize), + latency (isZeroDelayIn ? 0 : maxBufferSize), + irSize (buf.getNumSamples()), + blockSize (maxBlockSize), + isZeroDelay (isZeroDelayIn) { - volumeDry[channel].reset (spec.sampleRate, 0.05); - volumeWet[channel].reset (spec.sampleRate, 0.05); + constexpr auto numChannels = 2; + + const auto makeEngine = [&] (int channel, int offset, int length, uint32 thisBlockSize) + { + return std::make_unique (buf.getReadPointer (jmin (buf.getNumChannels() - 1, channel), offset), + length, + static_cast (thisBlockSize)); + }; + + if (headSizeIn.headSizeInSamples == 0) + { + for (int i = 0; i < numChannels; ++i) + head.emplace_back (makeEngine (i, 0, buf.getNumSamples(), static_cast (maxBufferSize))); + } + else + { + const auto size = jmin (buf.getNumSamples(), headSizeIn.headSizeInSamples); + + for (int i = 0; i < numChannels; ++i) + head.emplace_back (makeEngine (i, 0, size, static_cast (maxBufferSize))); + + const auto tailBufferSize = static_cast (headSizeIn.headSizeInSamples + (isZeroDelay ? 0 : maxBufferSize)); + + if (size != buf.getNumSamples()) + for (int i = 0; i < numChannels; ++i) + tail.emplace_back (makeEngine (i, size, buf.getNumSamples() - size, tailBufferSize)); + } } + void reset() + { + for (const auto& e : head) + e->reset(); + + for (const auto& e : tail) + e->reset(); + } + + void processSamples (const AudioBlock& input, AudioBlock& output) + { + const auto numChannels = juce::jmin (head.size(), input.getNumChannels(), output.getNumChannels()); + const auto numSamples = jmin (input.getNumSamples(), output.getNumSamples()); + + const AudioBlock fullTailBlock (tailBuffer); + const auto tailBlock = fullTailBlock.getSubBlock (0, (size_t) numSamples); + + const auto isUniform = tail.empty(); + + for (size_t channel = 0; channel < numChannels; ++channel) + { + if (! isUniform) + tail[channel]->processSamplesWithAddedLatency (input.getChannelPointer (channel), + tailBlock.getChannelPointer (0), + numSamples); + + if (isZeroDelay) + head[channel]->processSamples (input.getChannelPointer (channel), + output.getChannelPointer (channel), + numSamples); + else + head[channel]->processSamplesWithAddedLatency (input.getChannelPointer (channel), + output.getChannelPointer (channel), + numSamples); + + if (! isUniform) + output.getSingleChannelBlock (channel) += tailBlock; + } + + const auto numOutputChannels = output.getNumChannels(); + + for (auto i = numChannels; i < numOutputChannels; ++i) + output.getSingleChannelBlock (i).copyFrom (output.getSingleChannelBlock (0)); + } + + int getIRSize() const noexcept { return irSize; } + int getLatency() const noexcept { return latency; } + int getBlockSize() const noexcept { return blockSize; } + +private: + std::vector> head, tail; + AudioBuffer tailBuffer; + + const int latency; + const int irSize; + const int blockSize; + const bool isZeroDelay; +}; + +static AudioBuffer fixNumChannels (const AudioBuffer& buf, Convolution::Stereo stereo) +{ + const auto numChannels = jmin (buf.getNumChannels(), stereo == Convolution::Stereo::yes ? 2 : 1); + const auto numSamples = buf.getNumSamples(); + + AudioBuffer result (numChannels, buf.getNumSamples()); + + for (auto channel = 0; channel != numChannels; ++channel) + result.copyFrom (channel, 0, buf.getReadPointer (channel), numSamples); + + if (result.getNumSamples() == 0 || result.getNumChannels() == 0) + { + result.setSize (1, 1); + result.setSample (0, 0, 1.0f); + } + + return result; +} + +static AudioBuffer trimImpulseResponse (const AudioBuffer& buf) +{ + const auto thresholdTrim = Decibels::decibelsToGain (-80.0f); + + const auto numChannels = buf.getNumChannels(); + const auto numSamples = buf.getNumSamples(); + + std::ptrdiff_t offsetBegin = numSamples; + std::ptrdiff_t offsetEnd = numSamples; + + for (auto channel = 0; channel < numChannels; ++channel) + { + const auto indexAboveThreshold = [&] (auto begin, auto end) + { + return std::distance (begin, std::find_if (begin, end, [&] (float sample) + { + return std::abs (sample) >= thresholdTrim; + })); + }; + + const auto channelBegin = buf.getReadPointer (channel); + const auto channelEnd = channelBegin + numSamples; + const auto itStart = indexAboveThreshold (channelBegin, channelEnd); + const auto itEnd = indexAboveThreshold (std::make_reverse_iterator (channelEnd), + std::make_reverse_iterator (channelBegin)); + + offsetBegin = jmin (offsetBegin, itStart); + offsetEnd = jmin (offsetEnd, itEnd); + } + + if (offsetBegin == numSamples) + { + auto result = AudioBuffer (numChannels, 1); + result.clear(); + return result; + } + + const auto newLength = juce::jmax (1, numSamples - static_cast (offsetBegin + offsetEnd)); + + AudioBuffer result (numChannels, newLength); + + for (auto channel = 0; channel < numChannels; ++channel) + { + result.copyFrom (channel, + 0, + buf.getReadPointer (channel, static_cast (offsetBegin)), + result.getNumSamples()); + } + + return result; +} + +static float calculateNormalisationFactor (float sumSquaredMagnitude) +{ + if (sumSquaredMagnitude < 1e-8f) + return 1.0f; + + return 0.125f / std::sqrt (sumSquaredMagnitude); +} + +static void normaliseImpulseResponse (AudioBuffer& buf) +{ + const auto numChannels = buf.getNumChannels(); + const auto numSamples = buf.getNumSamples(); + const auto channelPtrs = buf.getArrayOfWritePointers(); + + const auto maxSumSquaredMag = std::accumulate (channelPtrs, channelPtrs + numChannels, 0.0f, [&] (auto max, auto* channel) + { + return juce::jmax (max, std::accumulate (channel, channel + numSamples, 0.0f, [] (auto sum, auto samp) + { + return sum + (samp * samp); + })); + }); + + const auto normalisationFactor = calculateNormalisationFactor (maxSumSquaredMag); + + std::for_each (channelPtrs, channelPtrs + numChannels, [&] (auto* channel) + { + FloatVectorOperations::multiply (channel, normalisationFactor, numSamples); + }); +} + +static AudioBuffer resampleImpulseResponse (const AudioBuffer& buf, + const double srcSampleRate, + const double destSampleRate) +{ + if (srcSampleRate == destSampleRate) + return buf; + + const auto factorReading = srcSampleRate / destSampleRate; + + AudioBuffer original = buf; + MemoryAudioSource memorySource (original, false); + ResamplingAudioSource resamplingSource (&memorySource, false, buf.getNumChannels()); + + const auto finalSize = roundToInt (juce::jmax (1.0, buf.getNumSamples() / factorReading)); + resamplingSource.setResamplingRatio (factorReading); + resamplingSource.prepareToPlay (finalSize, srcSampleRate); + + AudioBuffer result (buf.getNumChannels(), finalSize); + resamplingSource.getNextAudioBlock ({ &result, 0, result.getNumSamples() }); + + return result; +} + +//============================================================================== +class BackgroundMessageQueue : private Thread +{ +public: + BackgroundMessageQueue() + : Thread ("Convolution background loader"), queue (1000) + { + startThread(); + } + + ~BackgroundMessageQueue() override + { + stopThread (-1); + } + + using IncomingCommand = FixedSizeFunction<400, void()>; + + // Push functions here, and they'll be called later on a background thread. + // This function is wait-free. + // This function is only safe to call from a single thread at a time. + bool push (IncomingCommand& command) { return queue.push (command); } + +private: + void run() override + { + while (! threadShouldExit()) + { + if (queue.hasPendingMessages()) + queue.pop ([] (IncomingCommand& command) { command(); command = nullptr;}); + else + sleep (10); + } + } + + Queue queue; +}; + +template +class TryLockedPtr +{ +public: + void set (std::unique_ptr p) + { + const SpinLock::ScopedLockType lock (mutex); + ptr = std::move (p); + } + + std::unique_ptr get() + { + const SpinLock::ScopedTryLockType lock (mutex); + return lock.isLocked() ? std::move (ptr) : nullptr; + } + +private: + std::unique_ptr ptr; + SpinLock mutex; +}; + +struct BufferWithSampleRate +{ + BufferWithSampleRate() = default; + + BufferWithSampleRate (AudioBuffer&& bufferIn, double sampleRateIn) + : buffer (std::move (bufferIn)), sampleRate (sampleRateIn) {} + + AudioBuffer buffer; + double sampleRate = 0.0; +}; + +static BufferWithSampleRate loadStreamToBuffer (std::unique_ptr stream, size_t maxLength) +{ + AudioFormatManager manager; + manager.registerBasicFormats(); + std::unique_ptr formatReader (manager.createReaderFor (std::move (stream))); + + if (formatReader == nullptr) + return {}; + + const auto fileLength = static_cast (formatReader->lengthInSamples); + const auto lengthToLoad = maxLength == 0 ? fileLength : jmin (maxLength, fileLength); + + BufferWithSampleRate result { { jlimit (1, 2, static_cast (formatReader->numChannels)), + static_cast (lengthToLoad) }, + formatReader->sampleRate }; + + formatReader->read (result.buffer.getArrayOfWritePointers(), + result.buffer.getNumChannels(), + 0, + result.buffer.getNumSamples()); + + return result; +} + +// This class caches the data required to build a new convolution engine +// (in particular, impulse response data and a ProcessSpec). +// Calls to `setProcessSpec` and `setImpulseResponse` construct a +// new engine, which can be retrieved by calling `getEngine`. +class ConvolutionEngineFactory +{ +public: + ConvolutionEngineFactory (Convolution::Latency requiredLatency, + Convolution::NonUniform requiredHeadSize) + : latency { (requiredLatency.latencyInSamples <= 0) ? 0 : jmax (64, nextPowerOfTwo (requiredLatency.latencyInSamples)) }, + headSize { (requiredHeadSize.headSizeInSamples <= 0) ? 0 : jmax (64, nextPowerOfTwo (requiredHeadSize.headSizeInSamples)) }, + shouldBeZeroLatency (requiredLatency.latencyInSamples == 0) + {} + + // It is safe to call this method simultaneously with other public + // member functions. + void setProcessSpec (const ProcessSpec& spec) + { + const std::lock_guard lock (mutex); + processSpec = spec; + + engine.set (makeEngine()); + } + + // It is safe to call this method simultaneously with other public + // member functions. + void setImpulseResponse (BufferWithSampleRate&& buf, + Convolution::Stereo stereo, + Convolution::Trim trim, + Convolution::Normalise normalise) + { + const std::lock_guard lock (mutex); + wantsNormalise = normalise; + originalSampleRate = buf.sampleRate; + + impulseResponse = [&] + { + auto corrected = fixNumChannels (buf.buffer, stereo); + return trim == Convolution::Trim::yes ? trimImpulseResponse (corrected) : corrected; + }(); + + engine.set (makeEngine()); + } + + // Returns the most recently-created engine, or nullptr + // if there is no pending engine, or if the engine is currently + // being updated by one of the setter methods. + // It is safe to call this simultaneously with other public + // member functions. + std::unique_ptr getEngine() { return engine.get(); } + +private: + std::unique_ptr makeEngine() + { + auto resampled = resampleImpulseResponse (impulseResponse, originalSampleRate, processSpec.sampleRate); + + if (wantsNormalise == Convolution::Normalise::yes) + normaliseImpulseResponse (resampled); + + const auto currentLatency = jmax (processSpec.maximumBlockSize, (uint32) latency.latencyInSamples); + const auto maxBufferSize = shouldBeZeroLatency ? static_cast (processSpec.maximumBlockSize) + : nextPowerOfTwo (static_cast (currentLatency)); + + return std::make_unique (resampled, + processSpec.maximumBlockSize, + maxBufferSize, + headSize, + shouldBeZeroLatency); + } + + static AudioBuffer makeImpulseBuffer() + { + AudioBuffer result (1, 1); + result.setSample (0, 0, 1.0f); + return result; + } + + ProcessSpec processSpec { 44100.0, 128, 2 }; + AudioBuffer impulseResponse = makeImpulseBuffer(); + double originalSampleRate = processSpec.sampleRate; + Convolution::Normalise wantsNormalise = Convolution::Normalise::no; + const Convolution::Latency latency; + const Convolution::NonUniform headSize; + const bool shouldBeZeroLatency; + + TryLockedPtr engine; + + mutable std::mutex mutex; +}; + +static void setImpulseResponse (ConvolutionEngineFactory& factory, + const void* sourceData, + size_t sourceDataSize, + Convolution::Stereo stereo, + Convolution::Trim trim, + size_t size, + Convolution::Normalise normalise) +{ + factory.setImpulseResponse (loadStreamToBuffer (std::make_unique (sourceData, sourceDataSize, false), size), + stereo, trim, normalise); +} + +static void setImpulseResponse (ConvolutionEngineFactory& factory, + const File& fileImpulseResponse, + Convolution::Stereo stereo, + Convolution::Trim trim, + size_t size, + Convolution::Normalise normalise) +{ + factory.setImpulseResponse (loadStreamToBuffer (std::make_unique (fileImpulseResponse), size), + stereo, trim, normalise); +} + +// This class acts as a destination for convolution engines which are loaded on +// a background thread. + +// Deriving from `enable_shared_from_this` allows us to capture a reference to +// this object when adding commands to the background message queue. +// That way, we can avoid dangling references in the background thread in the case +// that a Convolution instance is deleted before the background message queue. +class ConvolutionEngineQueue : public std::enable_shared_from_this +{ +public: + ConvolutionEngineQueue (BackgroundMessageQueue& queue, + Convolution::Latency latencyIn, + Convolution::NonUniform headSizeIn) + : messageQueue (queue), factory (latencyIn, headSizeIn) {} + + void loadImpulseResponse (AudioBuffer&& buffer, + double sr, + Convolution::Stereo stereo, + Convolution::Trim trim, + Convolution::Normalise normalise) + { + callLater ([b = std::move (buffer), sr, stereo, trim, normalise] (ConvolutionEngineFactory& f) mutable + { + f.setImpulseResponse ({ std::move (b), sr }, stereo, trim, normalise); + }); + } + + void loadImpulseResponse (const void* sourceData, + size_t sourceDataSize, + Convolution::Stereo stereo, + Convolution::Trim trim, + size_t size, + Convolution::Normalise normalise) + { + callLater ([sourceData, sourceDataSize, stereo, trim, size, normalise] (ConvolutionEngineFactory& f) mutable + { + setImpulseResponse (f, sourceData, sourceDataSize, stereo, trim, size, normalise); + }); + } + + void loadImpulseResponse (const File& fileImpulseResponse, + Convolution::Stereo stereo, + Convolution::Trim trim, + size_t size, + Convolution::Normalise normalise) + { + callLater ([fileImpulseResponse, stereo, trim, size, normalise] (ConvolutionEngineFactory& f) mutable + { + setImpulseResponse (f, fileImpulseResponse, stereo, trim, size, normalise); + }); + } + + void prepare (const ProcessSpec& spec) + { + factory.setProcessSpec (spec); + } + + // Call this regularly to try to resend any pending message. + // This allows us to always apply the most recently requested + // state (eventually), even if the message queue fills up. + void postPendingCommand() + { + if (pendingCommand == nullptr) + return; + + if (messageQueue.push (pendingCommand)) + pendingCommand = nullptr; + } + + std::unique_ptr getEngine() { return factory.getEngine(); } + +private: + + template + void callLater (Fn&& fn) + { + // If there was already a pending command (because the queue was full) we'll end up deleting it here. + // Not much we can do about that! + pendingCommand = [weak = weakFromThis(), callback = std::forward (fn)]() mutable + { + if (auto t = weak.lock()) + callback (t->factory); + }; + + postPendingCommand(); + } + + std::weak_ptr weakFromThis() { return shared_from_this(); } + + BackgroundMessageQueue& messageQueue; + ConvolutionEngineFactory factory; + BackgroundMessageQueue::IncomingCommand pendingCommand; +}; + +class CrossoverMixer +{ +public: + void reset() + { + smoother.setCurrentAndTargetValue (1.0f); + } + + void prepare (const ProcessSpec& spec) + { + smoother.reset (spec.sampleRate, 0.05); + smootherBuffer.setSize (1, static_cast (spec.maximumBlockSize)); + mixBuffer.setSize (static_cast (spec.numChannels), static_cast (spec.maximumBlockSize)); + reset(); + } + + template + void processSamples (const AudioBlock& input, + AudioBlock& output, + ProcessCurrent&& current, + ProcessPrevious&& previous, + NotifyDone&& notifyDone) + { + if (smoother.isSmoothing()) + { + const auto numSamples = static_cast (input.getNumSamples()); + + for (auto sample = 0; sample != numSamples; ++sample) + smootherBuffer.setSample (0, sample, smoother.getNextValue()); + + AudioBlock mixBlock (mixBuffer); + mixBlock.clear(); + previous (input, mixBlock); + + for (size_t channel = 0; channel != output.getNumChannels(); ++channel) + { + FloatVectorOperations::multiply (mixBlock.getChannelPointer (channel), + smootherBuffer.getReadPointer (0), + numSamples); + } + + FloatVectorOperations::multiply (smootherBuffer.getWritePointer (0), -1.0f, numSamples); + FloatVectorOperations::add (smootherBuffer.getWritePointer (0), 1.0f, numSamples); + + current (input, output); + + for (size_t channel = 0; channel != output.getNumChannels(); ++channel) + { + FloatVectorOperations::multiply (output.getChannelPointer (channel), + smootherBuffer.getReadPointer (0), + numSamples); + FloatVectorOperations::add (output.getChannelPointer (channel), + mixBlock.getChannelPointer (channel), + numSamples); + } + + if (! smoother.isSmoothing()) + notifyDone(); + } + else + { + current (input, output); + } + } + + void beginTransition() + { + smoother.setCurrentAndTargetValue (1.0f); + smoother.setTargetValue (0.0f); + } + +private: + LinearSmoothedValue smoother; + AudioBuffer smootherBuffer; + AudioBuffer mixBuffer; +}; + +class Convolution::Impl +{ +public: + Impl (Latency requiredLatency, NonUniform requiredHeadSize) + : engineQueue (std::make_shared (messageQueue, requiredLatency, requiredHeadSize)) + {} + + void reset() + { + mixer.reset(); + + if (currentEngine != nullptr) + currentEngine->reset(); + + destroyPreviousEngine(); + } + + void prepare (const ProcessSpec& spec) + { + mixer.prepare (spec); + engineQueue->prepare (spec); + installPendingEngine(); + jassert (currentEngine != nullptr); + } + + void processSamples (const AudioBlock& input, AudioBlock& output) + { + engineQueue->postPendingCommand(); + + if (previousEngine == nullptr) + installPendingEngine(); + + mixer.processSamples (input, + output, + [this] (const AudioBlock& in, AudioBlock& out) + { + currentEngine->processSamples (in, out); + }, + [this] (const AudioBlock& in, AudioBlock& out) + { + if (previousEngine != nullptr) + previousEngine->processSamples (in, out); + else + out.copyFrom (in); + }, + [this] { destroyPreviousEngine(); }); + } + + int getCurrentIRSize() const { return currentEngine != nullptr ? currentEngine->getIRSize() : 0; } + + int getLatency() const { return currentEngine != nullptr ? currentEngine->getLatency() : 0; } + + void loadImpulseResponse (AudioBuffer&& buffer, + double originalSampleRate, + Stereo stereo, + Trim trim, + Normalise normalise) + { + engineQueue->loadImpulseResponse (std::move (buffer), originalSampleRate, stereo, trim, normalise); + } + + void loadImpulseResponse (const void* sourceData, + size_t sourceDataSize, + Stereo stereo, + Trim trim, + size_t size, + Normalise normalise) + { + engineQueue->loadImpulseResponse (sourceData, sourceDataSize, stereo, trim, size, normalise); + } + + void loadImpulseResponse (const File& fileImpulseResponse, + Stereo stereo, + Trim trim, + size_t size, + Normalise normalise) + { + engineQueue->loadImpulseResponse (fileImpulseResponse, stereo, trim, size, normalise); + } + +private: + void destroyPreviousEngine() + { + // If the queue is full, we'll destroy this straight away + BackgroundMessageQueue::IncomingCommand command = [p = std::move (previousEngine)]() mutable { p = nullptr; }; + messageQueue.push (command); + } + + void installNewEngine (std::unique_ptr newEngine) + { + destroyPreviousEngine(); + previousEngine = std::move (currentEngine); + currentEngine = std::move (newEngine); + mixer.beginTransition(); + } + + void installPendingEngine() + { + if (auto newEngine = engineQueue->getEngine()) + installNewEngine (std::move (newEngine)); + } + + BackgroundMessageQueue messageQueue; + std::shared_ptr engineQueue; + std::unique_ptr previousEngine, currentEngine; + CrossoverMixer mixer; +}; + +//============================================================================== +void Convolution::Mixer::prepare (const ProcessSpec& spec) +{ + for (auto& dry : volumeDry) + dry.reset (spec.sampleRate, 0.05); + + for (auto& wet : volumeWet) + wet.reset (spec.sampleRate, 0.05); + sampleRate = spec.sampleRate; - dryBuffer = AudioBlock (dryBufferStorage, - jmin (spec.numChannels, 2u), - spec.maximumBlockSize); - isActive = true; + dryBlock = AudioBlock (dryBlockStorage, + jmin (spec.numChannels, 2u), + spec.maximumBlockSize); + } -void Convolution::reset() noexcept +template +void Convolution::Mixer::processSamples (const AudioBlock& input, + AudioBlock& output, + bool isBypassed, + ProcessWet&& processWet) noexcept { - dryBuffer.clear(); - pimpl->reset(); -} + const auto numChannels = jmin (input.getNumChannels(), volumeDry.size()); + const auto numSamples = jmin (input.getNumSamples(), output.getNumSamples()); -void Convolution::processSamples (const AudioBlock& input, AudioBlock& output, bool isBypassed) noexcept -{ - if (! isActive) - return; - - jassert (input.getNumChannels() == output.getNumChannels()); - jassert (isPositiveAndBelow (input.getNumChannels(), static_cast (3))); // only mono and stereo is supported - - auto numChannels = jmin (input.getNumChannels(), (size_t) 2); - auto numSamples = jmin (input.getNumSamples(), output.getNumSamples()); - - auto dry = dryBuffer.getSubsetChannelBlock (0, numChannels); + auto dry = dryBlock.getSubsetChannelBlock (0, numChannels); if (volumeDry[0].isSmoothing()) { @@ -1208,7 +1106,7 @@ void Convolution::processSamples (const AudioBlock& input, AudioBlo for (size_t channel = 0; channel < numChannels; ++channel) volumeDry[channel].applyGain (dry.getChannelPointer (channel), (int) numSamples); - pimpl->processSamples (input, output); + processWet (input, output); for (size_t channel = 0; channel < numChannels; ++channel) volumeWet[channel].applyGain (output.getChannelPointer (channel), (int) numSamples); @@ -1218,7 +1116,7 @@ void Convolution::processSamples (const AudioBlock& input, AudioBlo else { if (! currentIsBypassed) - pimpl->processSamples (input, output); + processWet (input, output); if (isBypassed != currentIsBypassed) { @@ -1238,5 +1136,82 @@ void Convolution::processSamples (const AudioBlock& input, AudioBlo } } +void Convolution::Mixer::reset() { dryBlock.clear(); } + +//============================================================================== +Convolution::Convolution() + : Convolution (Latency { 0 }) {} + +Convolution::Convolution (const Latency& requiredLatency) + : pimpl (std::make_unique (requiredLatency, NonUniform{})) +{} + +Convolution::Convolution (const NonUniform& nonUniform) + : pimpl (std::make_unique (Latency{}, nonUniform)) +{} + +Convolution::~Convolution() noexcept = default; + +void Convolution::loadImpulseResponse (const void* sourceData, + size_t sourceDataSize, + Stereo stereo, + Trim trim, + size_t size, + Normalise normalise) +{ + pimpl->loadImpulseResponse (sourceData, sourceDataSize, stereo, trim, size, normalise); +} + +void Convolution::loadImpulseResponse (const File& fileImpulseResponse, + Stereo stereo, + Trim trim, + size_t size, + Normalise normalise) +{ + pimpl->loadImpulseResponse (fileImpulseResponse, stereo, trim, size, normalise); +} + +void Convolution::loadImpulseResponse (AudioBuffer&& buffer, + double originalSampleRate, + Stereo stereo, + Trim trim, + Normalise normalise) +{ + pimpl->loadImpulseResponse (std::move (buffer), originalSampleRate, stereo, trim, normalise); +} + +void Convolution::prepare (const ProcessSpec& spec) +{ + mixer.prepare (spec); + pimpl->prepare (spec); + isActive = true; +} + +void Convolution::reset() noexcept +{ + mixer.reset(); + pimpl->reset(); +} + +void Convolution::processSamples (const AudioBlock& input, + AudioBlock& output, + bool isBypassed) noexcept +{ + if (! isActive) + return; + + jassert (input.getNumChannels() == output.getNumChannels()); + jassert (isPositiveAndBelow (input.getNumChannels(), static_cast (3))); // only mono and stereo is supported + + mixer.processSamples (input, output, isBypassed, [this] (const auto& in, auto& out) + { + pimpl->processSamples (in, out); + }); +} + +int Convolution::getCurrentIRSize() const { return pimpl->getCurrentIRSize(); } + +int Convolution::getLatency() const { return pimpl->getLatency(); } + } // namespace dsp } // namespace juce diff --git a/modules/juce_dsp/frequency/juce_Convolution.h b/modules/juce_dsp/frequency/juce_Convolution.h index a0494cd732..d07210521c 100644 --- a/modules/juce_dsp/frequency/juce_Convolution.h +++ b/modules/juce_dsp/frequency/juce_Convolution.h @@ -22,18 +22,32 @@ namespace dsp { /** - Performs stereo uniform-partitioned convolution of an input signal with an - impulse response in the frequency domain, using the juce FFT class. + Performs stereo partitioned convolution of an input signal with an + impulse response in the frequency domain, using the JUCE FFT class. - It provides some thread-safe functions to load impulse responses as well, - from audio files or memory on the fly without any noticeable artefacts, + This class provides some thread-safe functions to load impulse responses + from audio files or memory on-the-fly without noticeable artefacts, performing resampling and trimming if necessary. - The processing is equivalent to the time domain convolution done in the - class FIRFilter, with a FIRFilter::Coefficients object having as - coefficients the samples of the impulse response. However, it is more - efficient in general to do frequency domain convolution when the size of - the impulse response is higher than 64 samples. + The processing performed by this class is equivalent to the time domain + convolution done in the FIRFilter class, with a FIRFilter::Coefficients + object having the samples of the impulse response as its coefficients. + However, in general it is more efficient to do frequency domain + convolution when the size of the impulse response is 64 samples or + greater. + + Note: The default operation of this class uses zero latency and a uniform + partitioned algorithm. If the impulse response size is large, or if the + algorithm is too CPU intensive, it is possible to use either a fixed + latency version of the algorithm, or a simple non-uniform partitioned + convolution algorithm. + + Threading: It is not safe to interleave calls to the methods of this + class. If you need to load new impulse responses during processing the + `load` calls must be synchronised with `process` calls, which in practice + means making the `load` call from the audio thread. The + `loadImpulseResponse` functions *are* wait-free and are therefore + suitable for use in a realtime context. @see FIRFilter, FIRFilter::Coefficients, FFT @@ -46,117 +60,164 @@ public: /** Initialises an object for performing convolution in the frequency domain. */ Convolution(); - /** Destructor. */ - ~Convolution(); + /** Contains configuration information for a convolution with a fixed latency. */ + struct Latency { int latencyInSamples; }; + + /** Initialises an object for performing convolution with a fixed latency. + + If the requested latency is zero, the actual latency will also be zero. + For requested latencies greater than zero, the actual latency will + always at least as large as the requested latency. Using a fixed + non-zero latency can reduce the CPU consumption of the convolution + algorithm. + + @param requiredLatency the minimum latency + */ + explicit Convolution (const Latency& requiredLatency); + + /** Contains configuration information for a non-uniform convolution. */ + struct NonUniform { int headSizeInSamples; }; + + /** Initialises an object for performing convolution in the frequency domain + using a non-uniform partitioned algorithm. + + A requiredHeadSize of 256 samples or greater will improve the + efficiency of the processing for IR sizes of 4096 samples or greater + (recommended for reverberation IRs). + + @param requiredHeadSize the head IR size for two stage non-uniform + partitioned convolution + */ + explicit Convolution (const NonUniform& requiredHeadSize); + + ~Convolution() noexcept; //============================================================================== - /** Must be called before loading any impulse response, to provide to the - convolution the maximumBufferSize to handle, and the sample rate useful for - optional resampling. + /** Must be called before loading any impulse response. This provides the + maximumBufferSize and the sample rate required for any resampling. */ void prepare (const ProcessSpec&); - /** Resets the processing pipeline, ready to start a new stream of data. */ + /** Resets the processing pipeline ready to start a new stream of data. */ void reset() noexcept; - /** Performs the filter operation on the given set of samples, with optional + /** Performs the filter operation on the given set of samples with optional stereo processing. */ - template + template ::value, int> = 0> void process (const ProcessContext& context) noexcept { - static_assert (std::is_same::value, - "Convolution engine only supports single precision floating point data"); - processSamples (context.getInputBlock(), context.getOutputBlock(), context.isBypassed); } + //============================================================================== + enum class Stereo { yes, no }; + enum class Trim { yes, no }; + enum class Normalise { yes, no }; + //============================================================================== /** This function loads an impulse response audio file from memory, added in a JUCE project with the Projucer as binary data. It can load any of the audio formats registered in JUCE, and performs some resampling and pre-processing as well if needed. - Note: Obviously, don't try to use this function on float samples, since the - data is supposed to be an audio file in its binary format, and be sure that - the original data is not going to move at all its memory location during the - process !! + Note: Don't try to use this function on float samples, since the data is + expected to be an audio file in its binary format. Be sure that the original + data remains constant throughout the lifetime of the Convolution object, as + the loading process will happen on a background thread once this function has + returned. @param sourceData the block of data to use as the stream's source @param sourceDataSize the number of bytes in the source data block - @param wantsStereo requests to process both stereo channels or only one mono channel - @param wantsTrimming requests to trim the start and the end of the impulse response + @param isStereo selects either stereo or mono + @param requiresTrimming optionally trim the start and the end of the impulse response @param size the expected size for the impulse response after loading, can be - set to 0 for requesting maximum original impulse response size - @param wantsNormalisation requests to normalise the impulse response amplitude + set to 0 to requesting the original impulse response size + @param requiresNormalisation optionally normalise the impulse response amplitude */ void loadImpulseResponse (const void* sourceData, size_t sourceDataSize, - bool wantsStereo, bool wantsTrimming, size_t size, - bool wantsNormalisation = true); + Stereo isStereo, Trim requiresTrimming, size_t size, + Normalise requiresNormalisation = Normalise::yes); - /** This function loads an impulse response from an audio file on any drive. It - can load any of the audio formats registered in JUCE, and performs some - resampling and pre-processing as well if needed. + /** This function loads an impulse response from an audio file. It can load any + of the audio formats registered in JUCE, and performs some resampling and + pre-processing as well if needed. @param fileImpulseResponse the location of the audio file - @param wantsStereo requests to process both stereo channels or only one mono channel - @param wantsTrimming requests to trim the start and the end of the impulse response + @param isStereo selects either stereo or mono + @param requiresTrimming optionally trim the start and the end of the impulse response @param size the expected size for the impulse response after loading, can be - set to 0 for requesting maximum original impulse response size - @param wantsNormalisation requests to normalise the impulse response amplitude + set to 0 to requesting the original impulse response size + @param requiresNormalisation optionally normalise the impulse response amplitude */ void loadImpulseResponse (const File& fileImpulseResponse, - bool wantsStereo, bool wantsTrimming, size_t size, - bool wantsNormalisation = true); + Stereo isStereo, Trim requiresTrimming, size_t size, + Normalise requiresNormalisation = Normalise::yes); - /** This function loads an impulse response from an audio buffer, which is - copied before doing anything else. Performs some resampling and - pre-processing as well if needed. + /** This function loads an impulse response from an audio buffer. + To avoid memory allocation on the audio thread, this function takes + ownership of the buffer passed in. + + If calling this function during processing, make sure that the buffer is + not allocated on the audio thread (be careful of accidental copies!). + If you need to pass arbitrary/generated buffers it's recommended to + create these buffers on a separate thread and to use some wait-free + construct (a lock-free queue or a SpinLock/GenericScopedTryLock combination) + to transfer ownership to the audio thread without allocating. @param buffer the AudioBuffer to use @param bufferSampleRate the sampleRate of the data in the AudioBuffer - @param wantsStereo requests to process both stereo channels or only one mono channel - @param wantsTrimming requests to trim the start and the end of the impulse response - @param wantsNormalisation requests to normalise the impulse response amplitude - @param size the expected size for the impulse response after loading, can be - set to 0 for requesting maximum original impulse response size + @param isStereo selects either stereo or mono + @param requiresTrimming optionally trim the start and the end of the impulse response + @param requiresNormalisation optionally normalise the impulse response amplitude */ - void copyAndLoadImpulseResponseFromBuffer (AudioBuffer& buffer, double bufferSampleRate, - bool wantsStereo, bool wantsTrimming, bool wantsNormalisation, - size_t size); + void loadImpulseResponse (AudioBuffer&& buffer, double bufferSampleRate, + Stereo isStereo, Trim requiresTrimming, Normalise requiresNormalisation); - /** This function loads an impulse response from an audio block, which is - copied before doing anything else. Performs some resampling and - pre-processing as well if needed. + /** This function returns the size of the current IR in samples. */ + int getCurrentIRSize() const; - @param block the AudioBlock to use - @param bufferSampleRate the sampleRate of the data in the AudioBuffer - @param wantsStereo requests to process both stereo channels or only one channel - @param wantsTrimming requests to trim the start and the end of the impulse response - @param wantsNormalisation requests to normalise the impulse response amplitude - @param size the expected size for the impulse response after loading, - -1 for maximum length + /** This function returns the current latency of the process in samples. + + Note: This is the latency of the convolution engine, not the latency + associated with the current impulse response choice that has to be + considered separately (linear phase filters, for eaxmple). */ - void copyAndLoadImpulseResponseFromBlock (AudioBlock block, double bufferSampleRate, - bool wantsStereo, bool wantsTrimming, bool wantsNormalisation, - size_t size); - + int getLatency() const; private: - //============================================================================== - struct Pimpl; - std::unique_ptr pimpl; - //============================================================================== void processSamples (const AudioBlock&, AudioBlock&, bool isBypassed) noexcept; + class Mixer + { + public: + void prepare (const ProcessSpec&); + + template + void processSamples (const AudioBlock&, + AudioBlock&, + bool isBypassed, + ProcessWet&&) noexcept; + + void reset(); + + private: + std::array, 2> volumeDry, volumeWet; + AudioBlock dryBlock; + HeapBlock dryBlockStorage; + double sampleRate = 0; + bool currentIsBypassed = false; + }; + //============================================================================== - double sampleRate; - bool currentIsBypassed = false; + class Impl; + std::unique_ptr pimpl; + + //============================================================================== + Mixer mixer; bool isActive = false; - SmoothedValue volumeDry[2], volumeWet[2]; - AudioBlock dryBuffer; - HeapBlock dryBufferStorage; //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Convolution) diff --git a/modules/juce_dsp/frequency/juce_Convolution_test.cpp b/modules/juce_dsp/frequency/juce_Convolution_test.cpp new file mode 100644 index 0000000000..3f536e39c5 --- /dev/null +++ b/modules/juce_dsp/frequency/juce_Convolution_test.cpp @@ -0,0 +1,512 @@ +/* + ============================================================================== + + This file is part of the JUCE 6 technical preview. + Copyright (c) 2020 - Raw Material Software Limited + + You may use this code under the terms of the GPL v3 + (see www.gnu.org/licenses). + + For this technical preview, this file is not subject to commercial licensing. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#if JUCE_ENABLE_ALLOCATION_HOOKS +#define JUCE_FAIL_ON_ALLOCATION_IN_SCOPE const UnitTestAllocationChecker checker (*this) +#else +#define JUCE_FAIL_ON_ALLOCATION_IN_SCOPE +#endif + +namespace juce +{ +namespace dsp +{ +namespace +{ + +class ConvolutionTest : public UnitTest +{ + template + static void nTimes (int n, Callback&& callback) + { + for (auto i = 0; i < n; ++i) + callback(); + } + + static AudioBuffer makeRamp (int length) + { + AudioBuffer result (1, length); + result.clear(); + + const auto writePtr = result.getWritePointer (0); + std::fill (writePtr, writePtr + length, 1.0f); + result.applyGainRamp (0, length, 1.0f, 0.0f); + + return result; + } + + static AudioBuffer makeStereoRamp (int length) + { + AudioBuffer result (2, length); + result.clear(); + + auto** channels = result.getArrayOfWritePointers(); + std::for_each (channels, channels + result.getNumChannels(), [&] (auto* channel) + { + std::fill (channel, channel + length, 1.0f); + }); + + result.applyGainRamp (0, 0, length, 1.0f, 0.0f); + result.applyGainRamp (1, 0, length, 0.0f, 1.0f); + + return result; + } + + static void addDiracImpulse (const AudioBlock& block) + { + block.clear(); + + for (size_t channel = 0; channel != block.getNumChannels(); ++channel) + block.setSample ((int) channel, 0, 1.0f); + } + + void checkForNans (const AudioBlock& block) + { + for (size_t channel = 0; channel != block.getNumChannels(); ++channel) + for (size_t sample = 0; sample != block.getNumSamples(); ++sample) + expect (! std::isnan (block.getSample ((int) channel, (int) sample))); + } + + template + void nonAllocatingExpectWithinAbsoluteError (const T& a, const T& b, const T& error) + { + expect (std::abs (a - b) < error); + } + + enum class InitSequence { prepareThenLoad, loadThenPrepare }; + + void checkLatency (const Convolution& convolution, const Convolution::Latency& latency) + { + const auto reportedLatency = convolution.getLatency(); + + if (latency.latencyInSamples == 0) + expect (reportedLatency == 0); + + expect (reportedLatency >= latency.latencyInSamples); + } + + void checkLatency (const Convolution&, const Convolution::NonUniform&) {} + + template + void testConvolution (const ProcessSpec& spec, + const ConvolutionConfig& config, + const AudioBuffer& ir, + double irSampleRate, + Convolution::Stereo stereo, + Convolution::Trim trim, + Convolution::Normalise normalise, + const AudioBlock& expectedResult, + InitSequence initSequence) + { + AudioBuffer buffer (static_cast (spec.numChannels), + static_cast (spec.maximumBlockSize)); + AudioBlock block { buffer }; + ProcessContextReplacing context { block }; + + const auto numBlocksPerSecond = (int) std::ceil (spec.sampleRate / spec.maximumBlockSize); + const auto numBlocksForImpulse = (int) std::ceil ((double) expectedResult.getNumSamples() / spec.maximumBlockSize); + + AudioBuffer outBuffer (static_cast (spec.numChannels), + numBlocksForImpulse * static_cast (spec.maximumBlockSize)); + + Convolution convolution (config); + + auto copiedIr = ir; + + if (initSequence == InitSequence::loadThenPrepare) + convolution.loadImpulseResponse (std::move (copiedIr), irSampleRate, stereo, trim, normalise); + + convolution.prepare (spec); + + JUCE_FAIL_ON_ALLOCATION_IN_SCOPE; + + if (initSequence == InitSequence::prepareThenLoad) + convolution.loadImpulseResponse (std::move (copiedIr), irSampleRate, stereo, trim, normalise); + + checkLatency (convolution, config); + + auto processBlocksWithDiracImpulse = [&] + { + for (auto i = 0; i != numBlocksForImpulse; ++i) + { + if (i == 0) + addDiracImpulse (block); + else + block.clear(); + + convolution.process (context); + + for (auto c = 0; c != static_cast (spec.numChannels); ++c) + { + outBuffer.copyFrom (c, + i * static_cast (spec.maximumBlockSize), + block.getChannelPointer (static_cast (c)), + static_cast (spec.maximumBlockSize)); + } + } + }; + + const auto time = Time::getMillisecondCounter(); + + // Wait 10 seconds to load the impulse response + while (Time::getMillisecondCounter() - time < 10'000) + { + processBlocksWithDiracImpulse(); + + // Check if the impulse response was loaded + if (block.getSample (0, 1) != 0.0f) + break; + } + + // At this point, our convolution should be loaded and the current IR size should + // match the expected result size + expect (convolution.getCurrentIRSize() == static_cast (expectedResult.getNumSamples())); + + // Make sure we get any smoothing out of the way + nTimes (numBlocksPerSecond, processBlocksWithDiracImpulse); + + nTimes (5, [&] + { + processBlocksWithDiracImpulse(); + + const auto actualLatency = static_cast (convolution.getLatency()); + + // The output should be the same as the IR + for (size_t c = 0; c != static_cast (expectedResult.getNumChannels()); ++c) + { + for (size_t i = 0; i != static_cast (expectedResult.getNumSamples()); ++i) + { + const auto equivalentSample = i + actualLatency; + + if (static_cast (equivalentSample) >= outBuffer.getNumSamples()) + continue; + + nonAllocatingExpectWithinAbsoluteError (outBuffer.getSample ((int) c, (int) equivalentSample), + expectedResult.getSample ((int) c, (int) i), + 0.01f); + } + } + }); + } + + template + void testConvolution (const ProcessSpec& spec, + const ConvolutionConfig& config, + const AudioBuffer& ir, + double irSampleRate, + Convolution::Stereo stereo, + Convolution::Trim trim, + Convolution::Normalise normalise, + const AudioBlock& expectedResult) + { + for (const auto sequence : { InitSequence::prepareThenLoad, InitSequence::loadThenPrepare }) + testConvolution (spec, config, ir, irSampleRate, stereo, trim, normalise, expectedResult, sequence); + } + +public: + ConvolutionTest() + : UnitTest ("Convolution", UnitTestCategories::dsp) + {} + + void runTest() override + { + const ProcessSpec spec { 44100.0, 512, 2 }; + AudioBuffer buffer (static_cast (spec.numChannels), + static_cast (spec.maximumBlockSize)); + AudioBlock block { buffer }; + ProcessContextReplacing context { block }; + + const auto impulseData = [&] + { + Random random; + AudioBuffer result (2, 1000); + + for (auto channel = 0; channel != result.getNumChannels(); ++channel) + for (auto sample = 0; sample != result.getNumSamples(); ++sample) + result.setSample (channel, sample, random.nextFloat()); + + return result; + }(); + + beginTest ("Impulse responses can be loaded without allocating on the audio thread"); + { + Convolution convolution; + convolution.prepare (spec); + + auto copy = impulseData; + + JUCE_FAIL_ON_ALLOCATION_IN_SCOPE; + + nTimes (100, [&] + { + convolution.loadImpulseResponse (std::move (copy), + 1000, + Convolution::Stereo::yes, + Convolution::Trim::yes, + Convolution::Normalise::no); + addDiracImpulse (block); + convolution.process (context); + checkForNans (block); + }); + } + + beginTest ("Convolution can be reset without allocating on the audio thread"); + { + Convolution convolution; + convolution.prepare (spec); + + auto copy = impulseData; + + convolution.loadImpulseResponse (std::move (copy), + 1000, + Convolution::Stereo::yes, + Convolution::Trim::yes, + Convolution::Normalise::yes); + + JUCE_FAIL_ON_ALLOCATION_IN_SCOPE; + + nTimes (100, [&] + { + addDiracImpulse (block); + convolution.reset(); + convolution.process (context); + convolution.reset(); + }); + + checkForNans (block); + } + + beginTest ("Completely empty IRs don't crash"); + { + AudioBuffer emptyBuffer; + + Convolution convolution; + convolution.prepare (spec); + + auto copy = impulseData; + + convolution.loadImpulseResponse (std::move (copy), + 2000, + Convolution::Stereo::yes, + Convolution::Trim::yes, + Convolution::Normalise::yes); + + JUCE_FAIL_ON_ALLOCATION_IN_SCOPE; + + nTimes (100, [&] + { + addDiracImpulse (block); + convolution.reset(); + convolution.process (context); + convolution.reset(); + }); + + checkForNans (block); + } + + beginTest ("Short uniform convolutions work"); + { + const auto ramp = makeRamp (static_cast (spec.maximumBlockSize) / 2); + testConvolution (spec, + Convolution::Latency { 0 }, + ramp, + spec.sampleRate, + Convolution::Stereo::yes, + Convolution::Trim::yes, + Convolution::Normalise::no, + ramp); + } + + beginTest ("Longer uniform convolutions work"); + { + const auto ramp = makeRamp (static_cast (spec.maximumBlockSize) * 8); + testConvolution (spec, + Convolution::Latency { 0 }, + ramp, + spec.sampleRate, + Convolution::Stereo::yes, + Convolution::Trim::yes, + Convolution::Normalise::no, + ramp); + } + + beginTest ("Normalisation works"); + { + const auto ramp = makeRamp (static_cast (spec.maximumBlockSize) * 8); + + auto copy = ramp; + const auto channels = copy.getArrayOfWritePointers(); + const auto numChannels = copy.getNumChannels(); + const auto numSamples = copy.getNumSamples(); + + const auto factor = 0.125f / std::sqrt (std::accumulate (channels, channels + numChannels, 0.0f, [&] (auto max, auto* channel) + { + return juce::jmax (max, std::accumulate (channel, channel + numSamples, 0.0f, [] (auto sum, auto sample) + { + return sum + sample * sample; + })); + })); + + std::for_each (channels, channels + numChannels, [&] (auto* channel) + { + FloatVectorOperations::multiply (channel, factor, numSamples); + }); + + testConvolution (spec, + Convolution::Latency { 0 }, + ramp, + spec.sampleRate, + Convolution::Stereo::yes, + Convolution::Trim::yes, + Convolution::Normalise::yes, + copy); + } + + beginTest ("Stereo convolutions work"); + { + const auto ramp = makeStereoRamp (static_cast (spec.maximumBlockSize) * 5); + testConvolution (spec, + Convolution::Latency { 0 }, + ramp, + spec.sampleRate, + Convolution::Stereo::yes, + Convolution::Trim::yes, + Convolution::Normalise::no, + ramp); + } + + beginTest ("Stereo IRs only use first channel if stereo is disabled"); + { + const auto length = static_cast (spec.maximumBlockSize) * 5; + const auto ramp = makeStereoRamp (length); + + const float* channels[] { ramp.getReadPointer (0), ramp.getReadPointer (0) }; + + testConvolution (spec, + Convolution::Latency { 0 }, + ramp, + spec.sampleRate, + Convolution::Stereo::no, + Convolution::Trim::yes, + Convolution::Normalise::no, + AudioBlock (channels, numElementsInArray (channels), length)); + } + + beginTest ("IRs with extra silence are trimmed appropriately"); + { + const auto length = static_cast (spec.maximumBlockSize) * 3; + const auto ramp = makeRamp (length); + AudioBuffer paddedRamp (ramp.getNumChannels(), ramp.getNumSamples() * 2); + paddedRamp.clear(); + + const auto offset = (paddedRamp.getNumSamples() - ramp.getNumSamples()) / 2; + + for (auto channel = 0; channel != ramp.getNumChannels(); ++channel) + paddedRamp.copyFrom (channel, offset, ramp.getReadPointer (channel), length); + + testConvolution (spec, + Convolution::Latency { 0 }, + paddedRamp, + spec.sampleRate, + Convolution::Stereo::no, + Convolution::Trim::yes, + Convolution::Normalise::no, + ramp); + } + + beginTest ("IRs are resampled if their sample rate is different to the playback rate"); + { + for (const auto resampleRatio : { 0.1, 0.5, 2.0, 10.0 }) + { + const auto length = static_cast (spec.maximumBlockSize) * 2; + const auto ramp = makeStereoRamp (length); + + const auto resampled = [&] + { + AudioBuffer original = ramp; + MemoryAudioSource memorySource (original, false); + ResamplingAudioSource resamplingSource (&memorySource, false, original.getNumChannels()); + + const auto finalSize = roundToInt (original.getNumSamples() / resampleRatio); + resamplingSource.setResamplingRatio (resampleRatio); + resamplingSource.prepareToPlay (finalSize, spec.sampleRate * resampleRatio); + + AudioBuffer result (original.getNumChannels(), finalSize); + resamplingSource.getNextAudioBlock ({ &result, 0, result.getNumSamples() }); + return result; + }(); + + testConvolution (spec, + Convolution::Latency { 0 }, + ramp, + spec.sampleRate * resampleRatio, + Convolution::Stereo::yes, + Convolution::Trim::yes, + Convolution::Normalise::no, + resampled); + } + } + + beginTest ("Non-uniform convolutions work"); + { + const auto ramp = makeRamp (static_cast (spec.maximumBlockSize) * 8); + + for (auto headSize : { spec.maximumBlockSize / 2, spec.maximumBlockSize, spec.maximumBlockSize * 9 }) + { + testConvolution (spec, + Convolution::NonUniform { static_cast (headSize) }, + ramp, + spec.sampleRate, + Convolution::Stereo::yes, + Convolution::Trim::yes, + Convolution::Normalise::no, + ramp); + } + } + + beginTest ("Convolutions with latency work"); + { + const auto ramp = makeRamp (static_cast (spec.maximumBlockSize) * 8); + using BlockSize = decltype (spec.maximumBlockSize); + + for (auto latency : { /*static_cast (0), + spec.maximumBlockSize / 3, + spec.maximumBlockSize, + spec.maximumBlockSize * 2, */ + static_cast (spec.maximumBlockSize * 2.5) }) + { + testConvolution (spec, + Convolution::Latency { static_cast (latency) }, + ramp, + spec.sampleRate, + Convolution::Stereo::yes, + Convolution::Trim::yes, + Convolution::Normalise::no, + ramp); + } + } + } +}; + +ConvolutionTest convolutionUnitTest; + +} +} +} + +#undef JUCE_FAIL_ON_ALLOCATION_IN_SCOPE diff --git a/modules/juce_dsp/juce_dsp.cpp b/modules/juce_dsp/juce_dsp.cpp index ebe502e312..34f7766ad6 100644 --- a/modules/juce_dsp/juce_dsp.cpp +++ b/modules/juce_dsp/juce_dsp.cpp @@ -47,6 +47,8 @@ #define JUCE_IPP_AVAILABLE 1 #endif +#include "containers/juce_FixedSizeFunction.h" + #include "processors/juce_FIRFilter.cpp" #include "processors/juce_IIRFilter.cpp" #include "processors/juce_FirstOrderTPTFilter.cpp" @@ -94,6 +96,8 @@ #endif #include "containers/juce_AudioBlock_test.cpp" + #include "containers/juce_FixedSizeFunction_test.cpp" + #include "frequency/juce_Convolution_test.cpp" #include "frequency/juce_FFT_test.cpp" #include "processors/juce_FIRFilter_test.cpp" #include "processors/juce_ProcessorChain_test.cpp"