/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2022 - 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 7 End-User License Agreement and JUCE Privacy Policy. End User License Agreement: www.juce.com/juce-7-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 class Queue { public: explicit Queue (int size) : fifo (size), storage (static_cast (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 (writer.startIndex1)] = std::move (element); else if (writer.blockSize2 != 0) storage[static_cast (writer.startIndex2)] = std::move (element); return true; } template void pop (Fn&& fn) { popN (1, std::forward (fn)); } template void popAll (Fn&& fn) { popN (fifo.getNumReady(), std::forward (fn)); } bool hasPendingMessages() const noexcept { return fifo.getNumReady() > 0; } private: template void popN (int n, Fn&& fn) { fifo.read (n).forEach ([&] (int index) { fn (storage[static_cast (index)]); }); } AbstractFifo fifo; std::vector 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 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 (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 (roundToInt (std::log2 (fftSize)))), numSegments (numSamples / (fftSize - blockSize) + 1u), numInputSegments ((blockSize > 128 ? numSegments : 3 * numSegments)), bufferInput (1, static_cast (fftSize)), bufferOutput (1, static_cast (fftSize * 2)), bufferTempOutput (1, static_cast (fftSize * 2)), bufferOverlap (1, static_cast (fftSize)) { bufferOutput.clear(); auto updateSegmentsIfNecessary = [this] (size_t numSegmentsToUpdate, std::vector>& 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 (fftSize * 2) }); } }; updateSegmentsIfNecessary (numInputSegments, buffersInputSegments); updateSegmentsIfNecessary (numSegments, buffersImpulseSegments); auto FFTTempObject = std::make_unique (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 (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 (numSamplesToProcess)); auto* inputSegmentData = buffersInputSegments[currentSegment].getWritePointer (0); FloatVectorOperations::copy (inputSegmentData, inputData, static_cast (fftSize)); 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[index].getWritePointer (0), buffersImpulseSegments[i].getWritePointer (0), outputTempData); } } FloatVectorOperations::copy (outputData, outputTempData, static_cast (fftSize + 1)); convolutionProcessingAndAccumulate (inputSegmentData, buffersImpulseSegments.front().getWritePointer (0), outputData); updateSymmetricFrequencyDomainData (outputData); fftObject->performRealOnlyInverseTransform (outputData); // Add overlap FloatVectorOperations::add (&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 (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)); 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 (numSamplesToProcess)); FloatVectorOperations::copy (output + numSamplesProcessed, outputData + inputDataPos, static_cast (numSamplesToProcess)); numSamplesProcessed += numSamplesToProcess; inputDataPos += numSamplesToProcess; // processing itself when needed (with latency) if (inputDataPos == blockSize) { // Copy input data in input segment auto* inputSegmentData = buffersInputSegments[currentSegment].getWritePointer (0); FloatVectorOperations::copy (inputSegmentData, inputData, static_cast (fftSize)); fftObject->performRealOnlyForwardTransform (inputSegmentData); prepareForConvolution (inputSegmentData); // Complex multiplication FloatVectorOperations::fill (outputTempData, 0, static_cast (fftSize + 1)); auto index = currentSegment; for (size_t i = 1; i < numSegments; ++i) { index += indexStep; if (index >= numInputSegments) index -= numInputSegments; convolutionProcessingAndAccumulate (buffersInputSegments[index].getWritePointer (0), buffersImpulseSegments[i].getWritePointer (0), outputTempData); } FloatVectorOperations::copy (outputData, outputTempData, static_cast (fftSize + 1)); convolutionProcessingAndAccumulate (inputSegmentData, buffersImpulseSegments.front().getWritePointer (0), outputData); updateSymmetricFrequencyDomainData (outputData); fftObject->performRealOnlyInverseTransform (outputData); // Add overlap FloatVectorOperations::add (outputData, overlapData, static_cast (blockSize)); // Input buffer is empty again now FloatVectorOperations::fill (inputData, 0.0f, static_cast (fftSize)); // Extra step for segSize > blockSize FloatVectorOperations::add (&(outputData[blockSize]), &(overlapData[blockSize]), static_cast (fftSize - 2 * blockSize)); // Save the overlap FloatVectorOperations::copy (overlapData, &(outputData[blockSize]), static_cast (fftSize - blockSize)); currentSegment = (currentSegment > 0) ? (currentSegment - 1) : (numInputSegments - 1); inputDataPos = 0; } } } // After each FFT, this function is called to allow convolution to be performed with only 4 SIMD functions calls. void prepareForConvolution (float *samples) noexcept { auto FFTSizeDiv2 = fftSize / 2; 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 (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)); 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 fftObject; const size_t numSegments; const size_t numInputSegments; size_t currentSegment = 0, inputDataPos = 0; AudioBuffer bufferInput, bufferOutput, bufferTempOutput, bufferOverlap; std::vector> buffersInputSegments, buffersImpulseSegments; }; //============================================================================== class MultichannelEngine { public: MultichannelEngine (const AudioBuffer& buf, int maxBlockSize, int maxBufferSize, Convolution::NonUniform headSizeIn, bool isZeroDelayIn) : tailBuffer (1, maxBlockSize), latency (isZeroDelayIn ? 0 : maxBufferSize), irSize (buf.getNumSamples()), blockSize (maxBlockSize), isZeroDelay (isZeroDelayIn) { constexpr auto numChannels = 2; const auto makeEngine = [&] (int channel, int offset, int length, uint32 thisBlockSize) { return std::make_unique (buf.getReadPointer (jmin (buf.getNumChannels() - 1, channel), offset), length, static_cast (thisBlockSize)); }; if (headSizeIn.headSizeInSamples == 0) { for (int i = 0; i < numChannels; ++i) head.emplace_back (makeEngine (i, 0, buf.getNumSamples(), static_cast (maxBufferSize))); } else { const auto size = jmin (buf.getNumSamples(), headSizeIn.headSizeInSamples); for (int i = 0; i < numChannels; ++i) head.emplace_back (makeEngine (i, 0, size, static_cast (maxBufferSize))); const auto tailBufferSize = static_cast (headSizeIn.headSizeInSamples + (isZeroDelay ? 0 : maxBufferSize)); if (size != buf.getNumSamples()) for (int i = 0; i < numChannels; ++i) tail.emplace_back (makeEngine (i, size, buf.getNumSamples() - size, tailBufferSize)); } } void reset() { for (const auto& e : head) e->reset(); for (const auto& e : tail) e->reset(); } void processSamples (const AudioBlock& input, AudioBlock& output) { const auto numChannels = jmin (head.size(), input.getNumChannels(), output.getNumChannels()); const auto numSamples = jmin (input.getNumSamples(), output.getNumSamples()); const AudioBlock fullTailBlock (tailBuffer); const auto tailBlock = fullTailBlock.getSubBlock (0, (size_t) numSamples); const auto isUniform = tail.empty(); for (size_t channel = 0; channel < numChannels; ++channel) { if (! isUniform) tail[channel]->processSamplesWithAddedLatency (input.getChannelPointer (channel), tailBlock.getChannelPointer (0), numSamples); if (isZeroDelay) head[channel]->processSamples (input.getChannelPointer (channel), output.getChannelPointer (channel), numSamples); else head[channel]->processSamplesWithAddedLatency (input.getChannelPointer (channel), output.getChannelPointer (channel), numSamples); if (! isUniform) output.getSingleChannelBlock (channel) += tailBlock; } const auto numOutputChannels = output.getNumChannels(); for (auto i = numChannels; i < numOutputChannels; ++i) output.getSingleChannelBlock (i).copyFrom (output.getSingleChannelBlock (0)); } int getIRSize() const noexcept { return irSize; } int getLatency() const noexcept { return latency; } int getBlockSize() const noexcept { return blockSize; } private: std::vector> head, tail; AudioBuffer tailBuffer; const int latency; const int irSize; const int blockSize; const bool isZeroDelay; }; static AudioBuffer fixNumChannels (const AudioBuffer& buf, Convolution::Stereo stereo) { const auto numChannels = jmin (buf.getNumChannels(), stereo == Convolution::Stereo::yes ? 2 : 1); const auto numSamples = buf.getNumSamples(); AudioBuffer result (numChannels, buf.getNumSamples()); for (auto channel = 0; channel != numChannels; ++channel) result.copyFrom (channel, 0, buf.getReadPointer (channel), numSamples); if (result.getNumSamples() == 0 || result.getNumChannels() == 0) { result.setSize (1, 1); result.setSample (0, 0, 1.0f); } return result; } static AudioBuffer trimImpulseResponse (const AudioBuffer& buf) { const auto thresholdTrim = Decibels::decibelsToGain (-80.0f); const auto numChannels = buf.getNumChannels(); const auto numSamples = buf.getNumSamples(); std::ptrdiff_t offsetBegin = numSamples; std::ptrdiff_t offsetEnd = numSamples; for (auto channel = 0; channel < numChannels; ++channel) { const auto indexAboveThreshold = [&] (auto begin, auto end) { return std::distance (begin, std::find_if (begin, end, [&] (float sample) { return std::abs (sample) >= thresholdTrim; })); }; const auto channelBegin = buf.getReadPointer (channel); const auto channelEnd = channelBegin + numSamples; const auto itStart = indexAboveThreshold (channelBegin, channelEnd); const auto itEnd = indexAboveThreshold (std::make_reverse_iterator (channelEnd), std::make_reverse_iterator (channelBegin)); offsetBegin = jmin (offsetBegin, itStart); offsetEnd = jmin (offsetEnd, itEnd); } if (offsetBegin == numSamples) { auto result = AudioBuffer (numChannels, 1); result.clear(); return result; } const auto newLength = jmax (1, numSamples - static_cast (offsetBegin + offsetEnd)); AudioBuffer result (numChannels, newLength); for (auto channel = 0; channel < numChannels; ++channel) { result.copyFrom (channel, 0, buf.getReadPointer (channel, static_cast (offsetBegin)), result.getNumSamples()); } return result; } static float calculateNormalisationFactor (float sumSquaredMagnitude) { if (sumSquaredMagnitude < 1e-8f) return 1.0f; return 0.125f / std::sqrt (sumSquaredMagnitude); } static void normaliseImpulseResponse (AudioBuffer& buf) { const auto numChannels = buf.getNumChannels(); const auto numSamples = buf.getNumSamples(); const auto channelPtrs = buf.getArrayOfWritePointers(); const auto maxSumSquaredMag = std::accumulate (channelPtrs, channelPtrs + numChannels, 0.0f, [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 resampleImpulseResponse (const AudioBuffer& buf, const double srcSampleRate, const double destSampleRate) { if (srcSampleRate == destSampleRate) return buf; const auto factorReading = srcSampleRate / destSampleRate; AudioBuffer original = buf; MemoryAudioSource memorySource (original, false); ResamplingAudioSource resamplingSource (&memorySource, false, buf.getNumChannels()); const auto finalSize = roundToInt (jmax (1.0, buf.getNumSamples() / factorReading)); resamplingSource.setResamplingRatio (factorReading); resamplingSource.prepareToPlay (finalSize, srcSampleRate); AudioBuffer result (buf.getNumChannels(), finalSize); resamplingSource.getNextAudioBlock ({ &result, 0, result.getNumSamples() }); return result; } //============================================================================== template class TryLockedPtr { public: void set (std::unique_ptr p) { const SpinLock::ScopedLockType lock (mutex); ptr = std::move (p); } std::unique_ptr get() { const SpinLock::ScopedTryLockType lock (mutex); return lock.isLocked() ? std::move (ptr) : nullptr; } private: std::unique_ptr ptr; SpinLock mutex; }; struct BufferWithSampleRate { BufferWithSampleRate() = default; BufferWithSampleRate (AudioBuffer&& bufferIn, double sampleRateIn) : buffer (std::move (bufferIn)), sampleRate (sampleRateIn) {} AudioBuffer buffer; double sampleRate = 0.0; }; static BufferWithSampleRate loadStreamToBuffer (std::unique_ptr stream, size_t maxLength) { AudioFormatManager manager; manager.registerBasicFormats(); std::unique_ptr formatReader (manager.createReaderFor (std::move (stream))); if (formatReader == nullptr) return {}; const auto fileLength = static_cast (formatReader->lengthInSamples); const auto lengthToLoad = maxLength == 0 ? fileLength : jmin (maxLength, fileLength); BufferWithSampleRate result { { jlimit (1, 2, static_cast (formatReader->numChannels)), static_cast (lengthToLoad) }, formatReader->sampleRate }; formatReader->read (result.buffer.getArrayOfWritePointers(), result.buffer.getNumChannels(), 0, result.buffer.getNumSamples()); return result; } // This class caches the data required to build a new convolution engine // (in particular, impulse response data and a ProcessSpec). // Calls to `setProcessSpec` and `setImpulseResponse` construct a // new engine, which can be retrieved by calling `getEngine`. class ConvolutionEngineFactory { public: ConvolutionEngineFactory (Convolution::Latency requiredLatency, Convolution::NonUniform requiredHeadSize) : latency { (requiredLatency.latencyInSamples <= 0) ? 0 : jmax (64, nextPowerOfTwo (requiredLatency.latencyInSamples)) }, headSize { (requiredHeadSize.headSizeInSamples <= 0) ? 0 : jmax (64, nextPowerOfTwo (requiredHeadSize.headSizeInSamples)) }, shouldBeZeroLatency (requiredLatency.latencyInSamples == 0) {} // It is safe to call this method simultaneously with other public // member functions. void setProcessSpec (const ProcessSpec& spec) { const std::lock_guard lock (mutex); processSpec = spec; engine.set (makeEngine()); } // It is safe to call this method simultaneously with other public // member functions. void setImpulseResponse (BufferWithSampleRate&& buf, Convolution::Stereo stereo, Convolution::Trim trim, Convolution::Normalise normalise) { const std::lock_guard lock (mutex); wantsNormalise = normalise; originalSampleRate = buf.sampleRate; impulseResponse = [&] { auto corrected = fixNumChannels (buf.buffer, stereo); return trim == Convolution::Trim::yes ? trimImpulseResponse (corrected) : corrected; }(); engine.set (makeEngine()); } // Returns the most recently-created engine, or nullptr // if there is no pending engine, or if the engine is currently // being updated by one of the setter methods. // It is safe to call this simultaneously with other public // member functions. std::unique_ptr getEngine() { return engine.get(); } private: std::unique_ptr makeEngine() { auto resampled = resampleImpulseResponse (impulseResponse, originalSampleRate, processSpec.sampleRate); if (wantsNormalise == Convolution::Normalise::yes) normaliseImpulseResponse (resampled); else resampled.applyGain ((float) (originalSampleRate / processSpec.sampleRate)); const auto currentLatency = jmax (processSpec.maximumBlockSize, (uint32) latency.latencyInSamples); const auto maxBufferSize = shouldBeZeroLatency ? static_cast (processSpec.maximumBlockSize) : nextPowerOfTwo (static_cast (currentLatency)); return std::make_unique (resampled, processSpec.maximumBlockSize, maxBufferSize, headSize, shouldBeZeroLatency); } static AudioBuffer makeImpulseBuffer() { AudioBuffer result (1, 1); result.setSample (0, 0, 1.0f); return result; } ProcessSpec processSpec { 44100.0, 128, 2 }; AudioBuffer impulseResponse = makeImpulseBuffer(); double originalSampleRate = processSpec.sampleRate; Convolution::Normalise wantsNormalise = Convolution::Normalise::no; const Convolution::Latency latency; const Convolution::NonUniform headSize; const bool shouldBeZeroLatency; TryLockedPtr engine; mutable std::mutex mutex; }; static void setImpulseResponse (ConvolutionEngineFactory& factory, const void* sourceData, size_t sourceDataSize, Convolution::Stereo stereo, Convolution::Trim trim, size_t size, Convolution::Normalise normalise) { factory.setImpulseResponse (loadStreamToBuffer (std::make_unique (sourceData, sourceDataSize, false), size), stereo, trim, normalise); } static void setImpulseResponse (ConvolutionEngineFactory& factory, const File& fileImpulseResponse, Convolution::Stereo stereo, Convolution::Trim trim, size_t size, Convolution::Normalise normalise) { factory.setImpulseResponse (loadStreamToBuffer (std::make_unique (fileImpulseResponse), size), stereo, trim, normalise); } // This class acts as a destination for convolution engines which are loaded on // a background thread. // Deriving from `enable_shared_from_this` allows us to capture a reference to // this object when adding commands to the background message queue. // That way, we can avoid dangling references in the background thread in the case // that a Convolution instance is deleted before the background message queue. class ConvolutionEngineQueue : public std::enable_shared_from_this { public: ConvolutionEngineQueue (BackgroundMessageQueue& queue, Convolution::Latency latencyIn, Convolution::NonUniform headSizeIn) : messageQueue (queue), factory (latencyIn, headSizeIn) {} void loadImpulseResponse (AudioBuffer&& buffer, double sr, Convolution::Stereo stereo, Convolution::Trim trim, Convolution::Normalise normalise) { callLater ([b = std::move (buffer), sr, stereo, trim, normalise] (ConvolutionEngineFactory& f) mutable { f.setImpulseResponse ({ std::move (b), sr }, stereo, trim, normalise); }); } void loadImpulseResponse (const void* sourceData, size_t sourceDataSize, Convolution::Stereo stereo, Convolution::Trim trim, size_t size, Convolution::Normalise normalise) { callLater ([sourceData, sourceDataSize, stereo, trim, size, normalise] (ConvolutionEngineFactory& f) mutable { setImpulseResponse (f, sourceData, sourceDataSize, stereo, trim, size, normalise); }); } void loadImpulseResponse (const File& fileImpulseResponse, Convolution::Stereo stereo, Convolution::Trim trim, size_t size, Convolution::Normalise normalise) { callLater ([fileImpulseResponse, stereo, trim, size, normalise] (ConvolutionEngineFactory& f) mutable { setImpulseResponse (f, fileImpulseResponse, stereo, trim, size, normalise); }); } void prepare (const ProcessSpec& spec) { factory.setProcessSpec (spec); } // Call this regularly to try to resend any pending message. // This allows us to always apply the most recently requested // state (eventually), even if the message queue fills up. void postPendingCommand() { if (pendingCommand == nullptr) return; if (messageQueue.push (pendingCommand)) pendingCommand = nullptr; } std::unique_ptr getEngine() { return factory.getEngine(); } private: template void callLater (Fn&& fn) { // If there was already a pending command (because the queue was full) we'll end up deleting it here. // Not much we can do about that! pendingCommand = [weak = weakFromThis(), callback = std::forward (fn)]() mutable { if (auto t = weak.lock()) callback (t->factory); }; postPendingCommand(); } std::weak_ptr weakFromThis() { return shared_from_this(); } BackgroundMessageQueue& messageQueue; ConvolutionEngineFactory factory; BackgroundMessageQueue::IncomingCommand pendingCommand; }; class CrossoverMixer { public: void reset() { smoother.setCurrentAndTargetValue (1.0f); } void prepare (const ProcessSpec& spec) { smoother.reset (spec.sampleRate, 0.05); smootherBuffer.setSize (1, static_cast (spec.maximumBlockSize)); mixBuffer.setSize (static_cast (spec.numChannels), static_cast (spec.maximumBlockSize)); reset(); } template void processSamples (const AudioBlock& input, AudioBlock& output, ProcessCurrent&& current, ProcessPrevious&& previous, NotifyDone&& notifyDone) { if (smoother.isSmoothing()) { const auto numSamples = static_cast (input.getNumSamples()); for (auto sample = 0; sample != numSamples; ++sample) smootherBuffer.setSample (0, sample, smoother.getNextValue()); AudioBlock mixBlock (mixBuffer); mixBlock.clear(); previous (input, mixBlock); for (size_t channel = 0; channel != output.getNumChannels(); ++channel) { FloatVectorOperations::multiply (mixBlock.getChannelPointer (channel), smootherBuffer.getReadPointer (0), numSamples); } FloatVectorOperations::multiply (smootherBuffer.getWritePointer (0), -1.0f, numSamples); FloatVectorOperations::add (smootherBuffer.getWritePointer (0), 1.0f, numSamples); current (input, output); for (size_t channel = 0; channel != output.getNumChannels(); ++channel) { FloatVectorOperations::multiply (output.getChannelPointer (channel), smootherBuffer.getReadPointer (0), numSamples); FloatVectorOperations::add (output.getChannelPointer (channel), mixBlock.getChannelPointer (channel), numSamples); } if (! smoother.isSmoothing()) notifyDone(); } else { current (input, output); } } void beginTransition() { smoother.setCurrentAndTargetValue (1.0f); smoother.setTargetValue (0.0f); } private: LinearSmoothedValue smoother; AudioBuffer smootherBuffer; AudioBuffer mixBuffer; }; using OptionalQueue = OptionalScopedPointer; class Convolution::Impl { public: Impl (Latency requiredLatency, NonUniform requiredHeadSize, OptionalQueue&& queue) : messageQueue (std::move (queue)), engineQueue (std::make_shared (*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& input, AudioBlock& output) { engineQueue->postPendingCommand(); if (previousEngine == nullptr) installPendingEngine(); mixer.processSamples (input, output, [this] (const AudioBlock& in, AudioBlock& out) { currentEngine->processSamples (in, out); }, [this] (const AudioBlock& in, AudioBlock& out) { if (previousEngine != nullptr) previousEngine->processSamples (in, out); else out.copyFrom (in); }, [this] { destroyPreviousEngine(); }); } int getCurrentIRSize() const { return currentEngine != nullptr ? currentEngine->getIRSize() : 0; } int getLatency() const { return currentEngine != nullptr ? currentEngine->getLatency() : 0; } void loadImpulseResponse (AudioBuffer&& buffer, double originalSampleRate, Stereo stereo, Trim trim, Normalise normalise) { engineQueue->loadImpulseResponse (std::move (buffer), originalSampleRate, stereo, trim, normalise); } void loadImpulseResponse (const void* sourceData, size_t sourceDataSize, Stereo stereo, Trim trim, size_t size, Normalise normalise) { engineQueue->loadImpulseResponse (sourceData, sourceDataSize, stereo, trim, size, normalise); } void loadImpulseResponse (const File& fileImpulseResponse, Stereo stereo, Trim trim, size_t size, Normalise normalise) { engineQueue->loadImpulseResponse (fileImpulseResponse, stereo, trim, size, normalise); } private: void destroyPreviousEngine() { // If the queue is full, we'll destroy this straight away BackgroundMessageQueue::IncomingCommand command = [p = std::move (previousEngine)]() mutable { p = nullptr; }; messageQueue->pimpl->push (command); } void installNewEngine (std::unique_ptr newEngine) { destroyPreviousEngine(); previousEngine = std::move (currentEngine); currentEngine = std::move (newEngine); mixer.beginTransition(); } void installPendingEngine() { if (auto newEngine = engineQueue->getEngine()) installNewEngine (std::move (newEngine)); } OptionalQueue messageQueue; std::shared_ptr engineQueue; std::unique_ptr previousEngine, currentEngine; CrossoverMixer mixer; }; //============================================================================== void Convolution::Mixer::prepare (const ProcessSpec& spec) { for (auto& dry : volumeDry) dry.reset (spec.sampleRate, 0.05); for (auto& wet : volumeWet) wet.reset (spec.sampleRate, 0.05); sampleRate = spec.sampleRate; dryBlock = AudioBlock (dryBlockStorage, jmin (spec.numChannels, 2u), spec.maximumBlockSize); } template void Convolution::Mixer::processSamples (const AudioBlock& input, AudioBlock& 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() }) {} Convolution::Convolution (const NonUniform& nonUniform) : Convolution ({}, nonUniform, OptionalQueue { std::make_unique() }) {} 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 (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&& buffer, double originalSampleRate, Stereo stereo, Trim trim, Normalise normalise) { pimpl->loadImpulseResponse (std::move (buffer), originalSampleRate, stereo, trim, normalise); } void Convolution::prepare (const ProcessSpec& spec) { mixer.prepare (spec); pimpl->prepare (spec); isActive = true; } void Convolution::reset() noexcept { mixer.reset(); pimpl->reset(); } void Convolution::processSamples (const AudioBlock& input, AudioBlock& output, bool isBypassed) noexcept { if (! isActive) return; jassert (input.getNumChannels() == output.getNumChannels()); jassert (isPositiveAndBelow (input.getNumChannels(), static_cast (3))); // only mono and stereo is supported mixer.processSamples (input, output, isBypassed, [this] (const auto& in, auto& out) { pimpl->processSamples (in, out); }); } int Convolution::getCurrentIRSize() const { return pimpl->getCurrentIRSize(); } int Convolution::getLatency() const { return pimpl->getLatency(); } } // namespace dsp } // namespace juce