/* ============================================================================== This file is part of the JUCE 7 technical preview. Copyright (c) 2022 - Raw Material Software Limited You may use this code under the terms of the GPL v3 (see www.gnu.org/licenses). For the technical preview this file cannot be licensed commercially. 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