paulxstretch/deps/juce/modules/juce_dsp/frequency/juce_Convolution.cpp

1298 lines
48 KiB
C++
Raw Permalink Normal View History

/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
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 6 End-User License
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
End User License Agreement: www.juce.com/juce-6-licence
Privacy Policy: www.juce.com/juce-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
{
template <typename Element>
class Queue
{
public:
explicit Queue (int size)
: fifo (size), storage (static_cast<size_t> (size)) {}
bool push (Element& element) noexcept
{
if (fifo.getFreeSpace() == 0)
return false;
const auto writer = fifo.write (1);
if (writer.blockSize1 != 0)
storage[static_cast<size_t> (writer.startIndex1)] = std::move (element);
else if (writer.blockSize2 != 0)
storage[static_cast<size_t> (writer.startIndex2)] = std::move (element);
return true;
}
template <typename Fn>
void pop (Fn&& fn) { popN (1, std::forward<Fn> (fn)); }
template <typename Fn>
void popAll (Fn&& fn) { popN (fifo.getNumReady(), std::forward<Fn> (fn)); }
bool hasPendingMessages() const noexcept { return fifo.getNumReady() > 0; }
private:
template <typename Fn>
void popN (int n, Fn&& fn)
{
fifo.read (n).forEach ([&] (int index)
{
fn (storage[static_cast<size_t> (index)]);
});
}
AbstractFifo fifo;
std::vector<Element> storage;
};
class BackgroundMessageQueue : private Thread
{
public:
explicit BackgroundMessageQueue (int entries)
: Thread ("Convolution background loader"), queue (entries)
{}
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); }
void popAll()
{
const ScopedLock lock (popMutex);
queue.popAll ([] (IncomingCommand& command) { command(); command = nullptr; });
}
using Thread::startThread;
using Thread::stopThread;
private:
void run() override
{
while (! threadShouldExit())
{
const auto tryPop = [&]
{
const ScopedLock lock (popMutex);
if (! queue.hasPendingMessages())
return false;
queue.pop ([] (IncomingCommand& command) { command(); command = nullptr;});
return true;
};
if (! tryPop())
sleep (10);
}
}
CriticalSection popMutex;
Queue<IncomingCommand> queue;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BackgroundMessageQueue)
};
struct ConvolutionMessageQueue::Impl : public BackgroundMessageQueue
{
using BackgroundMessageQueue::BackgroundMessageQueue;
};
ConvolutionMessageQueue::ConvolutionMessageQueue()
: ConvolutionMessageQueue (1000)
{}
ConvolutionMessageQueue::ConvolutionMessageQueue (int entries)
: pimpl (std::make_unique<Impl> (entries))
{
pimpl->startThread();
}
ConvolutionMessageQueue::~ConvolutionMessageQueue() noexcept
{
pimpl->stopThread (-1);
}
ConvolutionMessageQueue::ConvolutionMessageQueue (ConvolutionMessageQueue&&) noexcept = default;
ConvolutionMessageQueue& ConvolutionMessageQueue::operator= (ConvolutionMessageQueue&&) noexcept = default;
//==============================================================================
struct ConvolutionEngine
{
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<FFT> (roundToInt (std::log2 (fftSize)))),
numSegments (numSamples / (fftSize - blockSize) + 1u),
numInputSegments ((blockSize > 128 ? numSegments : 3 * numSegments)),
bufferInput (1, static_cast<int> (fftSize)),
bufferOutput (1, static_cast<int> (fftSize * 2)),
bufferTempOutput (1, static_cast<int> (fftSize * 2)),
bufferOverlap (1, static_cast<int> (fftSize))
{
bufferOutput.clear();
auto updateSegmentsIfNecessary = [this] (size_t numSegmentsToUpdate,
std::vector<AudioBuffer<float>>& segments)
{
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<int> (fftSize * 2) });
}
};
updateSegmentsIfNecessary (numInputSegments, buffersInputSegments);
updateSegmentsIfNecessary (numSegments, buffersImpulseSegments);
auto FFTTempObject = std::make_unique<FFT> (roundToInt (std::log2 (fftSize)));
size_t currentPtr = 0;
for (auto& buf : buffersImpulseSegments)
{
buf.clear();
auto* impulseResponse = buf.getWritePointer (0);
if (&buf == &buffersImpulseSegments.front())
impulseResponse[0] = 1.0f;
FloatVectorOperations::copy (impulseResponse,
samples + currentPtr,
static_cast<int> (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& buf : buffersInputSegments)
buf.clear();
currentSegment = 0;
inputDataPos = 0;
}
void processSamples (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)
{
const bool inputDataWasEmpty = (inputDataPos == 0);
auto numSamplesToProcess = jmin (numSamples - numSamplesProcessed, blockSize - inputDataPos);
FloatVectorOperations::copy (inputData + inputDataPos, input + numSamplesProcessed, static_cast<int> (numSamplesToProcess));
auto* inputSegmentData = buffersInputSegments[currentSegment].getWritePointer (0);
FloatVectorOperations::copy (inputSegmentData, inputData, static_cast<int> (fftSize));
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[index].getWritePointer (0),
buffersImpulseSegments[i].getWritePointer (0),
outputTempData);
}
}
FloatVectorOperations::copy (outputData, outputTempData, static_cast<int> (fftSize + 1));
convolutionProcessingAndAccumulate (inputSegmentData,
buffersImpulseSegments.front().getWritePointer (0),
outputData);
updateSymmetricFrequencyDomainData (outputData);
fftObject->performRealOnlyInverseTransform (outputData);
// Add overlap
FloatVectorOperations::add (&output[numSamplesProcessed], &outputData[inputDataPos], &overlapData[inputDataPos], (int) numSamplesToProcess);
// 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));
currentSegment = (currentSegment > 0) ? (currentSegment - 1) : (numInputSegments - 1);
}
numSamplesProcessed += numSamplesToProcess;
}
}
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<int> (numSamplesToProcess));
FloatVectorOperations::copy (output + numSamplesProcessed, outputData + inputDataPos, static_cast<int> (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<int> (fftSize));
fftObject->performRealOnlyForwardTransform (inputSegmentData);
prepareForConvolution (inputSegmentData);
// Complex multiplication
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[index].getWritePointer (0),
buffersImpulseSegments[i].getWritePointer (0),
outputTempData);
}
FloatVectorOperations::copy (outputData, outputTempData, static_cast<int> (fftSize + 1));
convolutionProcessingAndAccumulate (inputSegmentData,
buffersImpulseSegments.front().getWritePointer (0),
outputData);
updateSymmetricFrequencyDomainData (outputData);
fftObject->performRealOnlyInverseTransform (outputData);
// Add overlap
FloatVectorOperations::add (outputData, overlapData, static_cast<int> (blockSize));
// Input buffer is empty again now
FloatVectorOperations::fill (inputData, 0.0f, static_cast<int> (fftSize));
// 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));
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;
for (size_t i = 0; i < FFTSizeDiv2; i++)
samples[i] = samples[i << 1];
samples[FFTSizeDiv2] = 0;
for (size_t i = 1; i < FFTSizeDiv2; i++)
samples[i + FFTSizeDiv2] = -samples[((fftSize - i) << 1) + 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];
}
// 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;
for (size_t i = 1; i < 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[i << 1] = samples[(fftSize - i) << 1];
samples[(i << 1) + 1] = -samples[((fftSize - i) << 1) + 1];
}
}
//==============================================================================
const size_t blockSize;
const size_t fftSize;
const std::unique_ptr<FFT> fftObject;
const size_t numSegments;
const size_t numInputSegments;
size_t currentSegment = 0, inputDataPos = 0;
AudioBuffer<float> bufferInput, bufferOutput, bufferTempOutput, bufferOverlap;
std::vector<AudioBuffer<float>> buffersInputSegments, buffersImpulseSegments;
};
//==============================================================================
class MultichannelEngine
{
public:
MultichannelEngine (const AudioBuffer<float>& buf,
int maxBlockSize,
int maxBufferSize,
Convolution::NonUniform headSizeIn,
bool isZeroDelayIn)
: tailBuffer (1, maxBlockSize),
latency (isZeroDelayIn ? 0 : maxBufferSize),
irSize (buf.getNumSamples()),
blockSize (maxBlockSize),
isZeroDelay (isZeroDelayIn)
{
constexpr auto numChannels = 2;
const auto makeEngine = [&] (int channel, int offset, int length, uint32 thisBlockSize)
{
return std::make_unique<ConvolutionEngine> (buf.getReadPointer (jmin (buf.getNumChannels() - 1, channel), offset),
length,
static_cast<size_t> (thisBlockSize));
};
if (headSizeIn.headSizeInSamples == 0)
{
for (int i = 0; i < numChannels; ++i)
head.emplace_back (makeEngine (i, 0, buf.getNumSamples(), static_cast<uint32> (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<uint32> (maxBufferSize)));
const auto tailBufferSize = static_cast<uint32> (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<const float>& input, AudioBlock<float>& output)
{
const auto numChannels = jmin (head.size(), input.getNumChannels(), output.getNumChannels());
const auto numSamples = jmin (input.getNumSamples(), output.getNumSamples());
const AudioBlock<float> 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<std::unique_ptr<ConvolutionEngine>> head, tail;
AudioBuffer<float> tailBuffer;
const int latency;
const int irSize;
const int blockSize;
const bool isZeroDelay;
};
static AudioBuffer<float> fixNumChannels (const AudioBuffer<float>& buf, Convolution::Stereo stereo)
{
const auto numChannels = jmin (buf.getNumChannels(), stereo == Convolution::Stereo::yes ? 2 : 1);
const auto numSamples = buf.getNumSamples();
AudioBuffer<float> 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<float> trimImpulseResponse (const AudioBuffer<float>& 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<float> (numChannels, 1);
result.clear();
return result;
}
const auto newLength = jmax (1, numSamples - static_cast<int> (offsetBegin + offsetEnd));
AudioBuffer<float> result (numChannels, newLength);
for (auto channel = 0; channel < numChannels; ++channel)
{
result.copyFrom (channel,
0,
buf.getReadPointer (channel, static_cast<int> (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<float>& 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, [numSamples] (auto max, auto* channel)
{
return 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, [normalisationFactor, numSamples] (auto* channel)
{
FloatVectorOperations::multiply (channel, normalisationFactor, numSamples);
});
}
static AudioBuffer<float> resampleImpulseResponse (const AudioBuffer<float>& buf,
const double srcSampleRate,
const double destSampleRate)
{
if (srcSampleRate == destSampleRate)
return buf;
const auto factorReading = srcSampleRate / destSampleRate;
AudioBuffer<float> original = buf;
MemoryAudioSource memorySource (original, false);
ResamplingAudioSource resamplingSource (&memorySource, false, buf.getNumChannels());
const auto finalSize = roundToInt (jmax (1.0, buf.getNumSamples() / factorReading));
resamplingSource.setResamplingRatio (factorReading);
resamplingSource.prepareToPlay (finalSize, srcSampleRate);
AudioBuffer<float> result (buf.getNumChannels(), finalSize);
resamplingSource.getNextAudioBlock ({ &result, 0, result.getNumSamples() });
return result;
}
//==============================================================================
template <typename Element>
class TryLockedPtr
{
public:
void set (std::unique_ptr<Element> p)
{
const SpinLock::ScopedLockType lock (mutex);
ptr = std::move (p);
}
std::unique_ptr<MultichannelEngine> get()
{
const SpinLock::ScopedTryLockType lock (mutex);
return lock.isLocked() ? std::move (ptr) : nullptr;
}
private:
std::unique_ptr<Element> ptr;
SpinLock mutex;
};
struct BufferWithSampleRate
{
BufferWithSampleRate() = default;
BufferWithSampleRate (AudioBuffer<float>&& bufferIn, double sampleRateIn)
: buffer (std::move (bufferIn)), sampleRate (sampleRateIn) {}
AudioBuffer<float> buffer;
double sampleRate = 0.0;
};
static BufferWithSampleRate loadStreamToBuffer (std::unique_ptr<InputStream> stream, size_t maxLength)
{
AudioFormatManager manager;
manager.registerBasicFormats();
std::unique_ptr<AudioFormatReader> formatReader (manager.createReaderFor (std::move (stream)));
if (formatReader == nullptr)
return {};
const auto fileLength = static_cast<size_t> (formatReader->lengthInSamples);
const auto lengthToLoad = maxLength == 0 ? fileLength : jmin (maxLength, fileLength);
BufferWithSampleRate result { { jlimit (1, 2, static_cast<int> (formatReader->numChannels)),
static_cast<int> (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<std::mutex> 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<std::mutex> 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<MultichannelEngine> getEngine() { return engine.get(); }
private:
std::unique_ptr<MultichannelEngine> makeEngine()
{
auto resampled = resampleImpulseResponse (impulseResponse, originalSampleRate, processSpec.sampleRate);
if (wantsNormalise == Convolution::Normalise::yes)
normaliseImpulseResponse (resampled);
else
resampled.applyGain ((float) (originalSampleRate / processSpec.sampleRate));
const auto currentLatency = jmax (processSpec.maximumBlockSize, (uint32) latency.latencyInSamples);
const auto maxBufferSize = shouldBeZeroLatency ? static_cast<int> (processSpec.maximumBlockSize)
: nextPowerOfTwo (static_cast<int> (currentLatency));
return std::make_unique<MultichannelEngine> (resampled,
processSpec.maximumBlockSize,
maxBufferSize,
headSize,
shouldBeZeroLatency);
}
static AudioBuffer<float> makeImpulseBuffer()
{
AudioBuffer<float> result (1, 1);
result.setSample (0, 0, 1.0f);
return result;
}
ProcessSpec processSpec { 44100.0, 128, 2 };
AudioBuffer<float> impulseResponse = makeImpulseBuffer();
double originalSampleRate = processSpec.sampleRate;
Convolution::Normalise wantsNormalise = Convolution::Normalise::no;
const Convolution::Latency latency;
const Convolution::NonUniform headSize;
const bool shouldBeZeroLatency;
TryLockedPtr<MultichannelEngine> 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<MemoryInputStream> (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<FileInputStream> (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<ConvolutionEngineQueue>
{
public:
ConvolutionEngineQueue (BackgroundMessageQueue& queue,
Convolution::Latency latencyIn,
Convolution::NonUniform headSizeIn)
: messageQueue (queue), factory (latencyIn, headSizeIn) {}
void loadImpulseResponse (AudioBuffer<float>&& 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<MultichannelEngine> getEngine() { return factory.getEngine(); }
private:
template <typename Fn>
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> (fn)]() mutable
{
if (auto t = weak.lock())
callback (t->factory);
};
postPendingCommand();
}
std::weak_ptr<ConvolutionEngineQueue> 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<int> (spec.maximumBlockSize));
mixBuffer.setSize (static_cast<int> (spec.numChannels), static_cast<int> (spec.maximumBlockSize));
reset();
}
template <typename ProcessCurrent, typename ProcessPrevious, typename NotifyDone>
void processSamples (const AudioBlock<const float>& input,
AudioBlock<float>& output,
ProcessCurrent&& current,
ProcessPrevious&& previous,
NotifyDone&& notifyDone)
{
if (smoother.isSmoothing())
{
const auto numSamples = static_cast<int> (input.getNumSamples());
for (auto sample = 0; sample != numSamples; ++sample)
smootherBuffer.setSample (0, sample, smoother.getNextValue());
AudioBlock<float> 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<float> smoother;
AudioBuffer<float> smootherBuffer;
AudioBuffer<float> mixBuffer;
};
using OptionalQueue = OptionalScopedPointer<ConvolutionMessageQueue>;
class Convolution::Impl
{
public:
Impl (Latency requiredLatency,
NonUniform requiredHeadSize,
OptionalQueue&& queue)
: messageQueue (std::move (queue)),
engineQueue (std::make_shared<ConvolutionEngineQueue> (*messageQueue->pimpl,
requiredLatency,
requiredHeadSize))
{}
void reset()
{
mixer.reset();
if (currentEngine != nullptr)
currentEngine->reset();
destroyPreviousEngine();
}
void prepare (const ProcessSpec& spec)
{
messageQueue->pimpl->popAll();
mixer.prepare (spec);
engineQueue->prepare (spec);
if (auto newEngine = engineQueue->getEngine())
currentEngine = std::move (newEngine);
previousEngine = nullptr;
jassert (currentEngine != nullptr);
}
void processSamples (const AudioBlock<const float>& input, AudioBlock<float>& output)
{
engineQueue->postPendingCommand();
if (previousEngine == nullptr)
installPendingEngine();
mixer.processSamples (input,
output,
[this] (const AudioBlock<const float>& in, AudioBlock<float>& out)
{
currentEngine->processSamples (in, out);
},
[this] (const AudioBlock<const float>& in, AudioBlock<float>& 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<float>&& 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->pimpl->push (command);
}
void installNewEngine (std::unique_ptr<MultichannelEngine> newEngine)
{
destroyPreviousEngine();
previousEngine = std::move (currentEngine);
currentEngine = std::move (newEngine);
mixer.beginTransition();
}
void installPendingEngine()
{
if (auto newEngine = engineQueue->getEngine())
installNewEngine (std::move (newEngine));
}
OptionalQueue messageQueue;
std::shared_ptr<ConvolutionEngineQueue> engineQueue;
std::unique_ptr<MultichannelEngine> 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;
dryBlock = AudioBlock<float> (dryBlockStorage,
jmin (spec.numChannels, 2u),
spec.maximumBlockSize);
}
template <typename ProcessWet>
void Convolution::Mixer::processSamples (const AudioBlock<const float>& input,
AudioBlock<float>& output,
bool isBypassed,
ProcessWet&& processWet) noexcept
{
const auto numChannels = jmin (input.getNumChannels(), volumeDry.size());
const auto numSamples = jmin (input.getNumSamples(), output.getNumSamples());
auto dry = dryBlock.getSubsetChannelBlock (0, numChannels);
if (volumeDry[0].isSmoothing())
{
dry.copyFrom (input);
for (size_t channel = 0; channel < numChannels; ++channel)
volumeDry[channel].applyGain (dry.getChannelPointer (channel), (int) numSamples);
processWet (input, output);
for (size_t channel = 0; channel < numChannels; ++channel)
volumeWet[channel].applyGain (output.getChannelPointer (channel), (int) numSamples);
output += dry;
}
else
{
if (! currentIsBypassed)
processWet (input, output);
if (isBypassed != currentIsBypassed)
{
currentIsBypassed = isBypassed;
for (size_t channel = 0; channel < numChannels; ++channel)
{
volumeDry[channel].setTargetValue (isBypassed ? 0.0f : 1.0f);
volumeDry[channel].reset (sampleRate, 0.05);
volumeDry[channel].setTargetValue (isBypassed ? 1.0f : 0.0f);
volumeWet[channel].setTargetValue (isBypassed ? 1.0f : 0.0f);
volumeWet[channel].reset (sampleRate, 0.05);
volumeWet[channel].setTargetValue (isBypassed ? 0.0f : 1.0f);
}
}
}
}
void Convolution::Mixer::reset() { dryBlock.clear(); }
//==============================================================================
Convolution::Convolution()
: Convolution (Latency { 0 })
{}
Convolution::Convolution (ConvolutionMessageQueue& queue)
: Convolution (Latency { 0 }, queue)
{}
Convolution::Convolution (const Latency& requiredLatency)
: Convolution (requiredLatency,
{},
OptionalQueue { std::make_unique<ConvolutionMessageQueue>() })
{}
Convolution::Convolution (const NonUniform& nonUniform)
: Convolution ({},
nonUniform,
OptionalQueue { std::make_unique<ConvolutionMessageQueue>() })
{}
Convolution::Convolution (const Latency& requiredLatency, ConvolutionMessageQueue& queue)
: Convolution (requiredLatency, {}, OptionalQueue { queue })
{}
Convolution::Convolution (const NonUniform& nonUniform, ConvolutionMessageQueue& queue)
: Convolution ({}, nonUniform, OptionalQueue { queue })
{}
Convolution::Convolution (const Latency& latency,
const NonUniform& nonUniform,
OptionalQueue&& queue)
: pimpl (std::make_unique<Impl> (latency, nonUniform, std::move (queue)))
{}
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<float>&& 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<const 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
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