/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2017 - ROLI Ltd. JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 5 End-User License Agreement and JUCE 5 Privacy Policy (both updated and effective as of the 27th April 2017). End User License Agreement: www.juce.com/juce-5-licence Privacy Policy: www.juce.com/juce-5-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ /** This class is the convolution engine itself, processing only one channel at a time of input signal. */ struct ConvolutionEngine { ConvolutionEngine() = default; //============================================================================== struct ProcessingInformation { enum class SourceType { sourceBinaryData, sourceAudioFile, sourceAudioBuffer, sourceNone }; SourceType sourceType = SourceType::sourceNone; const void* sourceData; size_t sourceDataSize; File fileImpulseResponse; double bufferSampleRate; AudioBuffer* buffer; double sampleRate = 0; bool wantsStereo; size_t impulseResponseSize; size_t maximumBufferSize = 0; }; //============================================================================== void reset() { bufferInput.clear(); bufferOverlap.clear(); bufferTempOutput.clear(); for (auto i = 0; i < buffersInputSegments.size(); ++i) buffersInputSegments.getReference (i).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.buffer->getNumSamples()) / (FFTSize - blockSize) + 1; numInputSegments = (blockSize > 128 ? numSegments : 3 * numSegments); FFTobject = new FFT (roundDoubleToInt (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(); 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); } ScopedPointer FFTTempObject = new FFT (roundDoubleToInt (log2 (FFTSize))); auto numChannels = (info.wantsStereo && info.buffer->getNumChannels() >= 2 ? 2 : 1); if (channel < numChannels) { 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.buffer->getNumSamples()) 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 = new FFT (roundDoubleToInt (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; 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) { 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)); // Forward FFT FFTobject->performRealOnlyForwardTransform (inputSegmentData); prepareForConvolution (inputSegmentData); // Complex multiplication if (inputDataWasEmpty) { 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.getReference (static_cast (index)).getWritePointer (0), buffersImpulseSegments.getReference (static_cast (i)).getWritePointer (0), outputTempData); } } FloatVectorOperations::copy (outputData, outputTempData, static_cast (FFTSize + 1)); convolutionProcessingAndAccumulate (buffersInputSegments.getReference (static_cast (currentSegment)).getWritePointer (0), buffersImpulseSegments.getReference (0).getWritePointer (0), outputData); // Inverse FFT updateSymmetricFrequencyDomainData (outputData); FFTobject->performRealOnlyInverseTransform (outputData); // Add overlap for (size_t i = 0; i < numSamplesToProcess; ++i) output[i + numSamplesProcessed] = outputData[inputDataPos + i] + overlapData[inputDataPos + i]; // Input buffer full => Next block inputDataPos += numSamplesToProcess; if (inputDataPos == blockSize) { // Input buffer is empty again now 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)); // Save the overlap FloatVectorOperations::copy (overlapData, &(outputData[blockSize]), static_cast (FFTSize - blockSize)); // Update current segment currentSegment = (currentSegment > 0) ? (currentSegment - 1) : (numInputSegments - 1); } numSamplesProcessed += numSamplesToProcess; } } /** 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; for (size_t i = 0; i < FFTSizeDiv2; i++) samples[i] = samples[2 * i]; samples[FFTSizeDiv2] = 0; for (size_t i = 1; i < FFTSizeDiv2; i++) samples[i + FFTSizeDiv2] = -samples[2 * (FFTSize - i) + 1]; } /** 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; FloatVectorOperations::addWithMultiply (output, input, impulse, static_cast (FFTSizeDiv2)); FloatVectorOperations::subtractWithMultiply (output, &(input[FFTSizeDiv2]), &(impulse[FFTSizeDiv2]), static_cast (FFTSizeDiv2)); FloatVectorOperations::addWithMultiply (&(output[FFTSizeDiv2]), input, &(impulse[FFTSizeDiv2]), static_cast (FFTSizeDiv2)); FloatVectorOperations::addWithMultiply (&(output[FFTSizeDiv2]), &(input[FFTSizeDiv2]), impulse, static_cast (FFTSizeDiv2)); } /** 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. */ void updateSymmetricFrequencyDomainData (float* samples) noexcept { 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[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]; } } //============================================================================== ScopedPointer FFTobject; size_t FFTSize = 0; size_t currentSegment = 0, numInputSegments = 0, numSegments = 0, blockSize = 0, inputDataPos = 0; AudioBuffer bufferInput, bufferOutput, bufferTempOutput, bufferOverlap; Array> buffersInputSegments, buffersImpulseSegments; bool isReady = false; //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ConvolutionEngine) }; //============================================================================== /** 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 { public: enum class ChangeRequest { changeEngine = 0, changeSampleRate, changeMaximumBufferSize, changeSource, changeImpulseResponseSize, changeStereo, numChangeRequestTypes }; using SourceType = ConvolutionEngine::ProcessingInformation::SourceType; //============================================================================== Pimpl() : Thread ("Convolution"), abstractFifo (fifoSize) { abstractFifo.reset(); requestsType.resize (fifoSize); requestsParameter.resize (fifoSize); for (auto i = 0u; i < 4; ++i) engines.add (new ConvolutionEngine()); currentInfo.maximumBufferSize = 0; currentInfo.buffer = &impulseResponse; } ~Pimpl() { stopThread (10000); } //============================================================================== /** 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 (size1 > 0) { requestsType.setUnchecked (start1, type); requestsParameter.setUnchecked (start1, parameter); } if (size2 > 0) { requestsType.setUnchecked (start2, type); requestsParameter.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 (size1 > 0) { for (int i = 0; i < size1; ++i) { requestsType.setUnchecked (start1 + i, types[i]); requestsParameter.setUnchecked (start1 + i, parameters[i]); } } if (size2 > 0) { for (int i = 0; i < size2; ++i) { requestsType.setUnchecked (start2 + i, types[i + size1]); requestsParameter.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 = requestsType[start1]; parameter = requestsParameter[start1]; } if (size2 > 0) { type = requestsType[start2]; parameter = requestsParameter[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; // retrieve the information from the FIFO for processing Array requests; Array requestParameters; while (getNumRemainingEntries() > 0) { ChangeRequest type = ChangeRequest::changeEngine; juce::var parameter; readFromFifo (type, parameter); requests.add (type); requestParameters.add (parameter); } // remove any useless messages for (int i = 0; i < (int) ChangeRequest::numChangeRequestTypes; ++i) { bool exists = false; for (int n = requests.size(); --n >= 0;) { if (requests[n] == (ChangeRequest) i) { if (! exists) { exists = true; } else { requests.remove (n); requestParameters.remove (n); } } } } changeLevel = 0; for (int n = 0; n < requests.size(); ++n) { switch (requests[n]) { case ChangeRequest::changeEngine: changeLevel = 3; break; case ChangeRequest::changeSampleRate: { double newSampleRate = requestParameters[n]; if (currentInfo.sampleRate != newSampleRate) changeLevel = 3; currentInfo.sampleRate = newSampleRate; } break; case ChangeRequest::changeMaximumBufferSize: { int newMaximumBufferSize = requestParameters[n]; if (currentInfo.maximumBufferSize != (size_t) newMaximumBufferSize) changeLevel = 3; currentInfo.maximumBufferSize = (size_t) newMaximumBufferSize; } break; case ChangeRequest::changeSource: { auto* arrayParameters = requestParameters[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 = 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 bufferSampleRate (arrayParameters->getUnchecked (1)); changeLevel = jmax (2, changeLevel); currentInfo.sourceType = SourceType::sourceAudioBuffer; currentInfo.bufferSampleRate = bufferSampleRate; currentInfo.fileImpulseResponse = File(); currentInfo.sourceData = nullptr; currentInfo.sourceDataSize = 0; } } break; case ChangeRequest::changeImpulseResponseSize: { int64 newSize = requestParameters[n]; if (currentInfo.impulseResponseSize != (size_t) newSize) changeLevel = jmax (1, changeLevel); currentInfo.impulseResponseSize = (size_t) newSize; } break; case ChangeRequest::changeStereo: { bool newWantsStereo = requestParameters[n]; if (currentInfo.wantsStereo != newWantsStereo) changeLevel = jmax (1, changeLevel); currentInfo.wantsStereo = newWantsStereo; } 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.bufferSampleRate = currentInfo.sampleRate; currentInfo.impulseResponseSize = 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) { interpolationBuffer.setSize (2, static_cast (currentInfo.maximumBufferSize)); processImpulseResponse(); initializeConvolutionEngines(); } else if (changeLevel == 2) { startThread(); } else if (changeLevel == 1) { startThread(); } } //============================================================================== void copyBufferToTemporaryLocation (const AudioBuffer& buffer) { const SpinLock::ScopedLockType sl (processLock); auto numChannels = buffer.getNumChannels() > 1 ? 2 : 1; temporaryBuffer.setSize (numChannels, buffer.getNumSamples(), false, false, true); for (auto channel = 0; channel < numChannels; ++channel) temporaryBuffer.copyFrom (channel, 0, buffer, channel, 0, buffer.getNumSamples()); } /** Copies a buffer from a temporary location to the impulseResponseOriginal buffer for the sourceAudioBuffer. */ void copyBufferFromTemporaryLocation() { const SpinLock::ScopedLockType sl (processLock); impulseResponseOriginal.setSize (2, temporaryBuffer.getNumSamples(), false, false, true); for (auto channel = 0; channel < temporaryBuffer.getNumChannels(); ++channel) impulseResponseOriginal.copyFrom (channel, 0, temporaryBuffer, channel, 0, temporaryBuffer.getNumSamples()); } //============================================================================== void reset() { for (auto* e : engines) e->reset(); } /** Convolution processing handling interpolation between previous and new states of the convolution engines. */ void processSamples (const AudioBlock& input, AudioBlock& output) { processFifo(); size_t numChannels = input.getNumChannels(); 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 ((int) channel, 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 ((int) channel); engines[(int) channel + 2]->processSamples (interPtr, interPtr, numSamples); changeVolumes[channel + 2].applyGain (interPtr, (int) numSamples); buffer += interpolated.getSingleChannelBlock (channel); } if (changeVolumes[0].isSmoothing() == false) { mustInterpolate = false; for (auto channel = 0; channel < 2; ++channel) engines[channel]->copyStateFromOtherEngine (*engines[channel + 2]); } } } private: //============================================================================== void run() override { if (changeLevel == 2) { processImpulseResponse(); if (isThreadRunning() && threadShouldExit()) return; initializeConvolutionEngines(); } else if (changeLevel == 1) { initializeConvolutionEngines(); } } void processImpulseResponse() { if (currentInfo.sourceType == SourceType::sourceBinaryData) { copyAudioStreamInAudioBuffer (new MemoryInputStream (currentInfo.sourceData, currentInfo.sourceDataSize, false)); } else if (currentInfo.sourceType == SourceType::sourceAudioFile) { copyAudioStreamInAudioBuffer (new FileInputStream (currentInfo.fileImpulseResponse)); } else if (currentInfo.sourceType == SourceType::sourceAudioBuffer) { copyBufferFromTemporaryLocation(); trimAndResampleImpulseResponse (temporaryBuffer.getNumChannels(), currentInfo.bufferSampleRate); } if (isThreadRunning() && threadShouldExit()) return; if (currentInfo.wantsStereo) { normalizeImpulseResponse (currentInfo.buffer->getWritePointer(0), currentInfo.buffer->getNumSamples()); normalizeImpulseResponse (currentInfo.buffer->getWritePointer(1), currentInfo.buffer->getNumSamples()); } else { normalizeImpulseResponse (currentInfo.buffer->getWritePointer (0), currentInfo.buffer->getNumSamples()); } } /** Converts the data from an audio file into a stereo audio buffer of floats, and performs resampling if necessary. */ void copyAudioStreamInAudioBuffer (InputStream* stream) { AudioFormatManager manager; manager.registerBasicFormats(); if (ScopedPointer formatReader = manager.createReaderFor (stream)) { auto maximumTimeInSeconds = 10.0; int64 maximumLength = static_cast (roundDoubleToInt (maximumTimeInSeconds * formatReader->sampleRate)); auto numChannels = formatReader->numChannels > 1 ? 2 : 1; impulseResponseOriginal.setSize (2, static_cast (jmin (maximumLength, formatReader->lengthInSamples)), false, false, true); impulseResponseOriginal.clear(); formatReader->read (&(impulseResponseOriginal), 0, impulseResponseOriginal.getNumSamples(), 0, true, numChannels > 1); trimAndResampleImpulseResponse (numChannels, formatReader->sampleRate); } } void trimAndResampleImpulseResponse (int numChannels, double bufferSampleRate) { auto thresholdTrim = Decibels::decibelsToGain (-80.0f); auto indexStart = impulseResponseOriginal.getNumSamples() - 1; auto indexEnd = 0; for (auto channel = 0; channel < numChannels; ++channel) { auto localIndexStart = 0; auto localIndexEnd = impulseResponseOriginal.getNumSamples() - 1; auto* channelData = impulseResponseOriginal.getReadPointer (channel); while (localIndexStart < impulseResponseOriginal.getNumSamples() - 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 < impulseResponseOriginal.getNumSamples() - 1; ++i) channelData[i] = 0.0f; } } if (currentInfo.sampleRate == bufferSampleRate) { // No resampling auto impulseSize = jmin (static_cast (currentInfo.impulseResponseSize), indexEnd - indexStart + 1); impulseResponse.setSize (2, impulseSize); impulseResponse.clear(); for (auto channel = 0; channel < numChannels; ++channel) impulseResponse.copyFrom (channel, 0, impulseResponseOriginal, channel, 0, impulseSize); } else { // Resampling auto factorReading = bufferSampleRate / currentInfo.sampleRate; auto impulseSize = jmin (static_cast (currentInfo.impulseResponseSize), roundDoubleToInt ((indexEnd - indexStart + 1) / factorReading)); impulseResponse.setSize (2, impulseSize); impulseResponse.clear(); MemoryAudioSource memorySource (impulseResponseOriginal, false); ResamplingAudioSource resamplingSource (&memorySource, false, numChannels); resamplingSource.setResamplingRatio (factorReading); resamplingSource.prepareToPlay (impulseSize, currentInfo.sampleRate); AudioSourceChannelInfo info; info.startSample = 0; info.numSamples = impulseSize; 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, impulseResponse.getNumSamples()); } void normalizeImpulseResponse (float* samples, int numSamples) const { auto magnitude = 0.0f; for (int i = 0; i < numSamples; ++i) magnitude += samples[i] * samples[i]; auto magnitudeInv = 1.0f / (4.0f * std::sqrt (magnitude)); for (int i = 0; i < numSamples; ++i) samples[i] *= magnitudeInv; } void initializeConvolutionEngines() { if (currentInfo.maximumBufferSize == 0) return; auto numChannels = (currentInfo.wantsStereo ? 2 : 1); if (changeLevel == 3) { for (int i = 0; i < numChannels; ++i) engines[i]->initializeConvolutionEngine (currentInfo, i); if (numChannels == 1) engines[1]->copyStateFromOtherEngine (*engines[0]); mustInterpolate = false; } else { for (int i = 0; i < numChannels; ++i) { engines[i + 2]->initializeConvolutionEngine (currentInfo, i); engines[i + 2]->reset(); if (isThreadRunning() && threadShouldExit()) return; } if (numChannels == 1) engines[3]->copyStateFromOtherEngine (*engines[2]); for (size_t i = 0; i < 2; ++i) { changeVolumes[i].setValue (1.0f); changeVolumes[i].reset (currentInfo.sampleRate, 0.05); changeVolumes[i].setValue (0.0f); changeVolumes[i + 2].setValue (0.0f); changeVolumes[i + 2].reset (currentInfo.sampleRate, 0.05); changeVolumes[i + 2].setValue (1.0f); } mustInterpolate = true; } } //============================================================================== static constexpr int fifoSize = 256; // the size of the fifo which handles all the change requests AbstractFifo abstractFifo; // the abstract fifo 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 normalized //============================================================================== 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 LinearSmoothedValue 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) }; //============================================================================== Convolution::Convolution() { pimpl = new Pimpl(); pimpl->addToFifo (Convolution::Pimpl::ChangeRequest::changeEngine, juce::var (0)); } Convolution::~Convolution() { } void Convolution::loadImpulseResponse (const void* sourceData, size_t sourceDataSize, bool wantsStereo, size_t size) { if (sourceData == nullptr) return; Pimpl::ChangeRequest types[] = { Pimpl::ChangeRequest::changeSource, Pimpl::ChangeRequest::changeImpulseResponseSize, Pimpl::ChangeRequest::changeStereo }; 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 (size)), juce::var (wantsStereo) }; pimpl->addToFifo (types, parameters, 3); } void Convolution::loadImpulseResponse (const File& fileImpulseResponse, bool wantsStereo, size_t size) { if (! fileImpulseResponse.existsAsFile()) return; Pimpl::ChangeRequest types[] = { Pimpl::ChangeRequest::changeSource, Pimpl::ChangeRequest::changeImpulseResponseSize, Pimpl::ChangeRequest::changeStereo }; 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 (size)), juce::var (wantsStereo) }; pimpl->addToFifo (types, parameters, 3); } void Convolution::copyAndLoadImpulseResponseFromBuffer (const AudioBuffer& buffer, double bufferSampleRate, bool wantsStereo, size_t size) { jassert (bufferSampleRate > 0); if (buffer.getNumSamples() == 0) return; pimpl->copyBufferToTemporaryLocation (buffer); Pimpl::ChangeRequest types[] = { Pimpl::ChangeRequest::changeSource, Pimpl::ChangeRequest::changeImpulseResponseSize, Pimpl::ChangeRequest::changeStereo }; 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 (size)), juce::var (wantsStereo) }; pimpl->addToFifo (types, parameters, 3); } 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); for (size_t channel = 0; channel < spec.numChannels; ++channel) { volumeDry[channel].reset (spec.sampleRate, 0.05); volumeWet[channel].reset (spec.sampleRate, 0.05); } sampleRate = spec.sampleRate; dryBuffer = AudioBlock (dryBufferStorage, jmin (spec.numChannels, 2u), spec.maximumBlockSize); } void Convolution::reset() noexcept { dryBuffer.clear(); pimpl->reset(); } void Convolution::processSamples (const AudioBlock& input, AudioBlock& output, bool isBypassed) noexcept { jassert (input.getNumChannels() == output.getNumChannels()); jassert (isPositiveAndBelow (input.getNumChannels(), static_cast (3))); // only mono and stereo is supported auto numChannels = input.getNumChannels(); auto numSamples = jmin (input.getNumSamples(), output.getNumSamples()); auto dry = dryBuffer.getSubsetChannelBlock (0, numChannels); if (volumeDry[0].isSmoothing()) { dry.copy (input); for (size_t channel = 0; channel < numChannels; ++channel) volumeDry[channel].applyGain (dry.getChannelPointer (channel), (int) numSamples); pimpl->processSamples (input, output); for (size_t channel = 0; channel < numChannels; ++channel) volumeWet[channel].applyGain (output.getChannelPointer (channel), (int) numSamples); output += dry; } else { if (! currentIsBypassed) pimpl->processSamples (input, output); if (isBypassed != currentIsBypassed) { currentIsBypassed = isBypassed; for (size_t channel = 0; channel < numChannels; ++channel) { volumeDry[channel].setValue (isBypassed ? 0.0f : 1.0f); volumeDry[channel].reset (sampleRate, 0.05); volumeDry[channel].setValue (isBypassed ? 1.0f : 0.0f); volumeWet[channel].setValue (isBypassed ? 1.0f : 0.0f); volumeWet[channel].reset (sampleRate, 0.05); volumeWet[channel].setValue (isBypassed ? 0.0f : 1.0f); } } } }