1230 lines
47 KiB
C++
1230 lines
47 KiB
C++
/*
|
|
==============================================================================
|
|
|
|
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.
|
|
|
|
==============================================================================
|
|
*/
|
|
|
|
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
|
|
{
|
|
enum class SourceType
|
|
{
|
|
sourceBinaryData,
|
|
sourceAudioFile,
|
|
sourceAudioBuffer,
|
|
sourceNone
|
|
};
|
|
|
|
SourceType sourceType = SourceType::sourceNone;
|
|
|
|
const void* sourceData;
|
|
int sourceDataSize;
|
|
File fileImpulseResponse;
|
|
|
|
double originalSampleRate;
|
|
int originalSize = 0;
|
|
int originalNumChannels = 1;
|
|
|
|
AudioBuffer<float>* buffer;
|
|
|
|
bool wantsStereo = true;
|
|
bool wantsTrimming = true;
|
|
bool wantsNormalization = true;
|
|
int64 wantedSize = 0;
|
|
int finalSize = 0;
|
|
|
|
double sampleRate = 0;
|
|
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.finalSize) / (FFTSize - blockSize) + 1u;
|
|
|
|
numInputSegments = (blockSize > 128 ? numSegments : 3 * numSegments);
|
|
|
|
FFTobject.reset (new FFT (roundToInt (std::log2 (FFTSize))));
|
|
|
|
bufferInput.setSize (1, static_cast<int> (FFTSize));
|
|
bufferOutput.setSize (1, static_cast<int> (FFTSize * 2));
|
|
bufferTempOutput.setSize (1, static_cast<int> (FFTSize * 2));
|
|
bufferOverlap.setSize (1, static_cast<int> (FFTSize));
|
|
|
|
buffersInputSegments.clear();
|
|
buffersImpulseSegments.clear();
|
|
|
|
for (size_t i = 0; i < numInputSegments; ++i)
|
|
{
|
|
AudioBuffer<float> newInputSegment;
|
|
newInputSegment.setSize (1, static_cast<int> (FFTSize * 2));
|
|
buffersInputSegments.add (newInputSegment);
|
|
}
|
|
|
|
for (auto i = 0u; i < numSegments; ++i)
|
|
{
|
|
AudioBuffer<float> newImpulseSegment;
|
|
newImpulseSegment.setSize (1, static_cast<int> (FFTSize * 2));
|
|
buffersImpulseSegments.add (newImpulseSegment);
|
|
}
|
|
|
|
std::unique_ptr<FFT> 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<int> (n)).clear();
|
|
|
|
auto* impulseResponse = buffersImpulseSegments.getReference (static_cast<int> (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;
|
|
|
|
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<int> (numSamplesToProcess));
|
|
|
|
auto* inputSegmentData = buffersInputSegments.getReference (static_cast<int> (currentSegment)).getWritePointer (0);
|
|
FloatVectorOperations::copy (inputSegmentData, inputData, static_cast<int> (FFTSize));
|
|
|
|
// Forward FFT
|
|
FFTobject->performRealOnlyForwardTransform (inputSegmentData);
|
|
prepareForConvolution (inputSegmentData);
|
|
|
|
// Complex multiplication
|
|
if (inputDataWasEmpty)
|
|
{
|
|
FloatVectorOperations::fill (outputTempData, 0, static_cast<int> (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<int> (index)).getWritePointer (0),
|
|
buffersImpulseSegments.getReference (static_cast<int> (i)).getWritePointer (0),
|
|
outputTempData);
|
|
}
|
|
}
|
|
|
|
FloatVectorOperations::copy (outputData, outputTempData, static_cast<int> (FFTSize + 1));
|
|
|
|
convolutionProcessingAndAccumulate (buffersInputSegments.getReference (static_cast<int> (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<int> (FFTSize));
|
|
|
|
inputDataPos = 0;
|
|
|
|
// Extra step for segSize > blockSize
|
|
FloatVectorOperations::add (&(outputData[blockSize]), &(overlapData[blockSize]), static_cast<int> (FFTSize - 2 * blockSize));
|
|
|
|
// Save the overlap
|
|
FloatVectorOperations::copy (overlapData, &(outputData[blockSize]), static_cast<int> (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<int> (FFTSizeDiv2));
|
|
FloatVectorOperations::subtractWithMultiply (output, &(input[FFTSizeDiv2]), &(impulse[FFTSizeDiv2]), static_cast<int> (FFTSizeDiv2));
|
|
|
|
FloatVectorOperations::addWithMultiply (&(output[FFTSizeDiv2]), input, &(impulse[FFTSizeDiv2]), static_cast<int> (FFTSizeDiv2));
|
|
FloatVectorOperations::addWithMultiply (&(output[FFTSizeDiv2]), &(input[FFTSizeDiv2]), impulse, static_cast<int> (FFTSizeDiv2));
|
|
|
|
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.
|
|
*/
|
|
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];
|
|
}
|
|
}
|
|
|
|
//==============================================================================
|
|
std::unique_ptr<FFT> FFTobject;
|
|
|
|
size_t FFTSize = 0;
|
|
size_t currentSegment = 0, numInputSegments = 0, numSegments = 0, blockSize = 0, inputDataPos = 0;
|
|
|
|
AudioBuffer<float> bufferInput, bufferOutput, bufferTempOutput, bufferOverlap;
|
|
Array<AudioBuffer<float>> 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
|
|
{
|
|
enum class ChangeRequest
|
|
{
|
|
changeEngine = 0,
|
|
changeSampleRate,
|
|
changeMaximumBufferSize,
|
|
changeSource,
|
|
changeImpulseResponseSize,
|
|
changeStereo,
|
|
changeTrimming,
|
|
changeNormalization,
|
|
changeIgnore,
|
|
numChangeRequestTypes
|
|
};
|
|
|
|
using SourceType = ConvolutionEngine::ProcessingInformation::SourceType;
|
|
|
|
//==============================================================================
|
|
Pimpl() : Thread ("Convolution"), abstractFifo (fifoSize)
|
|
{
|
|
abstractFifo.reset();
|
|
fifoRequestsType.resize (fifoSize);
|
|
fifoRequestsParameter.resize (fifoSize);
|
|
|
|
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<int> (maximumTimeInSamples), false, false, true);
|
|
impulseResponseOriginal.setSize (2, static_cast<int> (maximumTimeInSamples), false, false, true);
|
|
impulseResponse.setSize (2, static_cast<int> (maximumTimeInSamples), false, false, true);
|
|
}
|
|
|
|
~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)
|
|
{
|
|
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 (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)
|
|
{
|
|
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<SourceType> (static_cast<int> (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::changeNormalization:
|
|
{
|
|
bool newWantsNormalization = requestsParameter[n];
|
|
|
|
if (currentInfo.wantsNormalization != newWantsNormalization)
|
|
changeLevel = jmax (1, changeLevel);
|
|
|
|
currentInfo.wantsNormalization = newWantsNormalization;
|
|
}
|
|
break;
|
|
|
|
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<float> 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<int> (currentInfo.maximumBufferSize), false, false, true);
|
|
|
|
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 (dsp::AudioBlock<float> 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();
|
|
}
|
|
|
|
/** Convolution processing handling interpolation between previous and new states
|
|
of the convolution engines.
|
|
*/
|
|
void processSamples (const AudioBlock<float>& input, AudioBlock<float>& 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 = dsp::AudioBlock<float> (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 (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).copy (output.getSingleChannelBlock (0));
|
|
}
|
|
|
|
//==============================================================================
|
|
const int64 maximumTimeInSamples = 10 * 96000;
|
|
|
|
private:
|
|
//==============================================================================
|
|
/** This the thread run function which does the preparation of data depending
|
|
on the requested change level.
|
|
*/
|
|
void run() override
|
|
{
|
|
if (changeLevel == 2)
|
|
{
|
|
loadImpulseResponse();
|
|
|
|
if (isThreadRunning() && threadShouldExit())
|
|
return;
|
|
}
|
|
|
|
processImpulseResponse();
|
|
|
|
if (isThreadRunning() && threadShouldExit())
|
|
return;
|
|
|
|
initializeConvolutionEngines();
|
|
}
|
|
|
|
/** Loads the impulse response from the requested audio source. */
|
|
void loadImpulseResponse()
|
|
{
|
|
if (currentInfo.sourceType == SourceType::sourceBinaryData)
|
|
{
|
|
if (! (copyAudioStreamInAudioBuffer (new MemoryInputStream (currentInfo.sourceData, (size_t) currentInfo.sourceDataSize, false))))
|
|
return;
|
|
}
|
|
else if (currentInfo.sourceType == SourceType::sourceAudioFile)
|
|
{
|
|
if (! (copyAudioStreamInAudioBuffer (new FileInputStream (currentInfo.fileImpulseResponse))))
|
|
return;
|
|
}
|
|
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.wantsNormalization)
|
|
{
|
|
if (currentInfo.originalNumChannels > 1)
|
|
{
|
|
normalizeImpulseResponse (currentInfo.buffer->getWritePointer (0), (int) currentInfo.finalSize, 1.0);
|
|
normalizeImpulseResponse (currentInfo.buffer->getWritePointer (1), (int) currentInfo.finalSize, 1.0);
|
|
}
|
|
else
|
|
{
|
|
normalizeImpulseResponse (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 (InputStream* stream)
|
|
{
|
|
AudioFormatManager manager;
|
|
manager.registerBasicFormats();
|
|
std::unique_ptr<AudioFormatReader> formatReader (manager.createReaderFor (stream));
|
|
|
|
if (formatReader != nullptr)
|
|
{
|
|
currentInfo.originalNumChannels = formatReader->numChannels > 1 ? 2 : 1;
|
|
currentInfo.originalSampleRate = formatReader->sampleRate;
|
|
currentInfo.originalSize = static_cast<int> (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<int> (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<int> (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);
|
|
}
|
|
|
|
/** Normalization of the impulse response based on its energy. */
|
|
void normalizeImpulseResponse (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 <float> (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].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<ChangeRequest> fifoRequestsType; // an array of ChangeRequest
|
|
Array<juce::var> fifoRequestsParameter; // an array of change parameters
|
|
|
|
Array<ChangeRequest> requestsType; // an array of ChangeRequest
|
|
Array<juce::var> 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<float> 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<float> impulseResponseOriginal; // a buffer with the original impulse response
|
|
AudioBuffer<float> impulseResponse; // a buffer with the impulse response trimmed, resampled, resized and normalized
|
|
|
|
//==============================================================================
|
|
OwnedArray<ConvolutionEngine> engines; // the 4 convolution engines being used
|
|
|
|
AudioBuffer<float> interpolationBuffer; // a buffer to do the interpolation between the convolution engines 0-1 and 2-3
|
|
LinearSmoothedValue<float> 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.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 wantsNormalization)
|
|
{
|
|
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::changeNormalization };
|
|
|
|
Array<juce::var> 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<int64> (wantedSize)),
|
|
juce::var (wantsStereo),
|
|
juce::var (wantsTrimming),
|
|
juce::var (wantsNormalization) };
|
|
|
|
pimpl->addToFifo (types, parameters, 5);
|
|
}
|
|
|
|
void Convolution::loadImpulseResponse (const File& fileImpulseResponse, bool wantsStereo,
|
|
bool wantsTrimming, size_t size, bool wantsNormalization)
|
|
{
|
|
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::changeNormalization };
|
|
|
|
Array<juce::var> 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<int64> (wantedSize)),
|
|
juce::var (wantsStereo),
|
|
juce::var (wantsTrimming),
|
|
juce::var (wantsNormalization) };
|
|
|
|
pimpl->addToFifo (types, parameters, 5);
|
|
}
|
|
|
|
void Convolution::copyAndLoadImpulseResponseFromBuffer (AudioBuffer<float>& buffer,
|
|
double bufferSampleRate, bool wantsStereo, bool wantsTrimming, bool wantsNormalization, size_t size)
|
|
{
|
|
copyAndLoadImpulseResponseFromBlock (AudioBlock<float> (buffer), bufferSampleRate,
|
|
wantsStereo, wantsTrimming, wantsNormalization, size);
|
|
}
|
|
|
|
void Convolution::copyAndLoadImpulseResponseFromBlock (AudioBlock<float> block, double bufferSampleRate,
|
|
bool wantsStereo, bool wantsTrimming, bool wantsNormalization, 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::changeNormalization };
|
|
|
|
Array<juce::var> 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<int64> (wantedSize)),
|
|
juce::var (wantsStereo),
|
|
juce::var (wantsTrimming),
|
|
juce::var (wantsNormalization) };
|
|
|
|
pimpl->addToFifo (types, parameters, 5);
|
|
}
|
|
|
|
void Convolution::prepare (const ProcessSpec& spec)
|
|
{
|
|
jassert (isPositiveAndBelow (spec.numChannels, static_cast<uint32> (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<int> (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<float> (dryBufferStorage,
|
|
jmin (spec.numChannels, 2u),
|
|
spec.maximumBlockSize);
|
|
|
|
isActive = true;
|
|
}
|
|
|
|
void Convolution::reset() noexcept
|
|
{
|
|
dryBuffer.clear();
|
|
pimpl->reset();
|
|
}
|
|
|
|
void Convolution::processSamples (const AudioBlock<float>& input, AudioBlock<float>& output, bool isBypassed) noexcept
|
|
{
|
|
if (! isActive)
|
|
return;
|
|
|
|
jassert (input.getNumChannels() == output.getNumChannels());
|
|
jassert (isPositiveAndBelow (input.getNumChannels(), static_cast<size_t> (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);
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
} // namespace dsp
|
|
} // namespace juce
|