Skip to content

Commit

Permalink
Updated convolution reverb to use a zero latency strategy
Browse files Browse the repository at this point in the history
  • Loading branch information
cesaref committed Nov 8, 2024
1 parent 308fa8c commit 722cba9
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 19 deletions.
91 changes: 91 additions & 0 deletions examples/patches/ConvolutionReverb/convolution.cmajor
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,99 @@
// EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
// DISCLAIMED.

/// This namespace contains handy implementations of both time domain and frequency domain convolutions
namespace Convolution
{
/// This is an implementation of a convolution reverb with zero latency.
/// The impulse response is chopped into three parts, an initial part which is calculated using
/// a time domain algorithm (and hence introduces no latency), a short FFT based algorithm, which
/// fills in from the end of the time domain convolution, and a long FFT based algorithm that
/// applies the majority of the convolution.
///
/// By using three separate algorithms, and by dividing the impulse between them, we can trade off
/// computational cost against latency, and the result is no introduced latency with good overall
/// convolution performance. Modifying the FFT sizes will alter the performance characteristics
graph ZeroLatencyProcessor (int shortBlockSize, int longBlockSize, int maxImpulseFrames)
{
input stream float in;
output stream float out;
input event float[] impulseData;

event impulseData (float[] impulse)
{
convolution1.impulseData <- impulse[:shortBlockSize/2];
convolutionShort.impulseData <- impulse[shortBlockSize/2:longBlockSize/2];
convolutionLong.impulseData <- impulse[longBlockSize/2:];
}

node convolution1 = TimeDomainProcessor (shortBlockSize/2);
node convolutionShort = BlockProcessor (shortBlockSize, (longBlockSize - shortBlockSize));
node convolutionLong = BlockProcessor (longBlockSize, maxImpulseFrames);

connection
{
in -> convolution1.in, convolutionShort.in, convolutionLong.in;

convolution1.out -> out;
convolutionShort.out -> out;
convolutionLong.out -> out;
}
}

/// This convolution algorithm uses a frequency domain implementation, with the blockSize altering
/// the overall runtime and latency of the algorithm. Latecy is blockSize/2 frames. Larger block sizes
/// will offer lower CPU use
graph BlockProcessor (int blockSize, int maxImpulseFrames)
{
input stream float in;
output stream float out;

input conv.impulseData;

node fft = FFT (blockSize);
node conv = Convolve (blockSize, maxImpulseFrames);
node ifft = iFFT (blockSize);

connection
{
in -> fft -> conv.in;
conv.out -> ifft -> out;
}
}

/// A simple time domain convolution algorithm. This will be costly to execute for longer impulses
processor TimeDomainProcessor (int maxImpulseFrames)
{
input stream float in;
output stream float out;
input event float[] impulseData;

event impulseData (float[] v)
{
impulse = 0.0f;

for (wrap<maxImpulseFrames> i)
if (i < v.size)
impulse[i] = v[i];
}

float<maxImpulseFrames> impulse;

void main()
{
float<maxImpulseFrames> x;

loop
{
x[1:] = x[0:maxImpulseFrames-1];
x[0] = in;

out <- sum (x * impulse);
advance();
}
}
}

processor FFT (int blockSize)
{
input stream float in;
Expand Down
35 changes: 16 additions & 19 deletions examples/patches/ConvolutionReverb/reverb.cmajor
Original file line number Diff line number Diff line change
Expand Up @@ -16,43 +16,39 @@
// EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
// DISCLAIMED.

/// This is an implementation of a simple frequency domain convolution algorithm. The FFT runs at
/// a fixed size, and no attempt is made to avoid latency, so this algorithm runs with half the
/// FFT size as a fixed latency.
///
/// Increasing the fftSize will increase the latency, but reduce the computational cost
graph Convolver [[ main ]]

/// This is a simple algorithm presenting the use of the convolution namespace to implement
/// a convolution reverb. The impulse response is fixed, this is just a demonstrator
graph ConvolutionReverb [[ main ]]
{
input event float32 dryLevel [[ name: "Dry Level", min: -96, max: 6, init: 0, unit: "db" ]];
input event float32 wetLevel [[ name: "Wet Level", min: -96, max: 6, init: -24, unit: "db" ]];
input event float32 dryLevel [[ name: "Dry Level", min: -48, max: 6, init: 0, unit: "db" ]];
input event float32 wetLevel [[ name: "Wet Level", min: -48, max: 6, init: -24, unit: "db" ]];

input stream float in;
output stream float out;

let fftSize = 256;
let maxImpulseLength = 100000;
let shortFFTSize = 32;
let longFFTSize = 1024;

node fft = Convolution::FFT (fftSize);
node conv = Convolution::Convolve (fftSize, maxImpulseLength);
node ifft = Convolution::iFFT (fftSize);
node dryGain = LevelSmoother;
node wetGain = LevelSmoother;

node convolution = Convolution::ZeroLatencyProcessor (shortFFTSize, longFFTSize, maxImpulseLength);

connection
{
ImpulseSource.impulseData -> convolution.impulseData;
dryLevel -> dryGain;
wetLevel -> wetGain;

ImpulseSource -> conv.impulseData;

in -> fft -> conv.in;
conv.out -> ifft;

in -> convolution.in;
in * dryGain.out -> out;
ifft.out * wetGain.out -> out;
convolution.out * wetGain.out -> out;
}
}

/// Processor used to pass the initial impulse data to the convolution algorithm
processor ImpulseSource
{
output event float[] impulseData;
Expand All @@ -62,11 +58,12 @@ processor ImpulseSource
void main()
{
impulseData <- reverb;

advance();
}
}


/// Smoother for gain parameters
processor LevelSmoother
{
input event float db;
Expand Down

0 comments on commit 722cba9

Please sign in to comment.