582 lines
22 KiB
C++
582 lines
22 KiB
C++
|
/*
|
||
|
==============================================================================
|
||
|
|
||
|
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.
|
||
|
|
||
|
==============================================================================
|
||
|
*/
|
||
|
|
||
|
#if JUCE_ENABLE_ALLOCATION_HOOKS
|
||
|
#define JUCE_FAIL_ON_ALLOCATION_IN_SCOPE const UnitTestAllocationChecker checker (*this)
|
||
|
#else
|
||
|
#define JUCE_FAIL_ON_ALLOCATION_IN_SCOPE
|
||
|
#endif
|
||
|
|
||
|
namespace juce
|
||
|
{
|
||
|
namespace dsp
|
||
|
{
|
||
|
namespace
|
||
|
{
|
||
|
|
||
|
class ConvolutionTest : public UnitTest
|
||
|
{
|
||
|
template <typename Callback>
|
||
|
static void nTimes (int n, Callback&& callback)
|
||
|
{
|
||
|
for (auto i = 0; i < n; ++i)
|
||
|
callback();
|
||
|
}
|
||
|
|
||
|
static AudioBuffer<float> makeRamp (int length)
|
||
|
{
|
||
|
AudioBuffer<float> result (1, length);
|
||
|
result.clear();
|
||
|
|
||
|
const auto writePtr = result.getWritePointer (0);
|
||
|
std::fill (writePtr, writePtr + length, 1.0f);
|
||
|
result.applyGainRamp (0, length, 1.0f, 0.0f);
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
static AudioBuffer<float> makeStereoRamp (int length)
|
||
|
{
|
||
|
AudioBuffer<float> result (2, length);
|
||
|
result.clear();
|
||
|
|
||
|
auto** channels = result.getArrayOfWritePointers();
|
||
|
std::for_each (channels, channels + result.getNumChannels(), [length] (auto* channel)
|
||
|
{
|
||
|
std::fill (channel, channel + length, 1.0f);
|
||
|
});
|
||
|
|
||
|
result.applyGainRamp (0, 0, length, 1.0f, 0.0f);
|
||
|
result.applyGainRamp (1, 0, length, 0.0f, 1.0f);
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
static void addDiracImpulse (const AudioBlock<float>& block)
|
||
|
{
|
||
|
block.clear();
|
||
|
|
||
|
for (size_t channel = 0; channel != block.getNumChannels(); ++channel)
|
||
|
block.setSample ((int) channel, 0, 1.0f);
|
||
|
}
|
||
|
|
||
|
void checkForNans (const AudioBlock<float>& block)
|
||
|
{
|
||
|
for (size_t channel = 0; channel != block.getNumChannels(); ++channel)
|
||
|
for (size_t sample = 0; sample != block.getNumSamples(); ++sample)
|
||
|
expect (! std::isnan (block.getSample ((int) channel, (int) sample)));
|
||
|
}
|
||
|
|
||
|
void checkAllChannelsNonZero (const AudioBlock<float>& block)
|
||
|
{
|
||
|
for (size_t i = 0; i != block.getNumChannels(); ++i)
|
||
|
{
|
||
|
const auto* channel = block.getChannelPointer (i);
|
||
|
|
||
|
expect (std::any_of (channel, channel + block.getNumSamples(), [] (float sample)
|
||
|
{
|
||
|
return sample != 0.0f;
|
||
|
}));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
template <typename T>
|
||
|
void nonAllocatingExpectWithinAbsoluteError (const T& a, const T& b, const T& error)
|
||
|
{
|
||
|
expect (std::abs (a - b) < error);
|
||
|
}
|
||
|
|
||
|
enum class InitSequence { prepareThenLoad, loadThenPrepare };
|
||
|
|
||
|
void checkLatency (const Convolution& convolution, const Convolution::Latency& latency)
|
||
|
{
|
||
|
const auto reportedLatency = convolution.getLatency();
|
||
|
|
||
|
if (latency.latencyInSamples == 0)
|
||
|
expect (reportedLatency == 0);
|
||
|
|
||
|
expect (reportedLatency >= latency.latencyInSamples);
|
||
|
}
|
||
|
|
||
|
void checkLatency (const Convolution&, const Convolution::NonUniform&) {}
|
||
|
|
||
|
template <typename ConvolutionConfig>
|
||
|
void testConvolution (const ProcessSpec& spec,
|
||
|
const ConvolutionConfig& config,
|
||
|
const AudioBuffer<float>& ir,
|
||
|
double irSampleRate,
|
||
|
Convolution::Stereo stereo,
|
||
|
Convolution::Trim trim,
|
||
|
Convolution::Normalise normalise,
|
||
|
const AudioBlock<const float>& expectedResult,
|
||
|
InitSequence initSequence)
|
||
|
{
|
||
|
AudioBuffer<float> buffer (static_cast<int> (spec.numChannels),
|
||
|
static_cast<int> (spec.maximumBlockSize));
|
||
|
AudioBlock<float> block { buffer };
|
||
|
ProcessContextReplacing<float> context { block };
|
||
|
|
||
|
const auto numBlocksPerSecond = (int) std::ceil (spec.sampleRate / spec.maximumBlockSize);
|
||
|
const auto numBlocksForImpulse = (int) std::ceil ((double) expectedResult.getNumSamples() / spec.maximumBlockSize);
|
||
|
|
||
|
AudioBuffer<float> outBuffer (static_cast<int> (spec.numChannels),
|
||
|
numBlocksForImpulse * static_cast<int> (spec.maximumBlockSize));
|
||
|
|
||
|
Convolution convolution (config);
|
||
|
|
||
|
auto copiedIr = ir;
|
||
|
|
||
|
if (initSequence == InitSequence::loadThenPrepare)
|
||
|
convolution.loadImpulseResponse (std::move (copiedIr), irSampleRate, stereo, trim, normalise);
|
||
|
|
||
|
convolution.prepare (spec);
|
||
|
|
||
|
JUCE_FAIL_ON_ALLOCATION_IN_SCOPE;
|
||
|
|
||
|
if (initSequence == InitSequence::prepareThenLoad)
|
||
|
convolution.loadImpulseResponse (std::move (copiedIr), irSampleRate, stereo, trim, normalise);
|
||
|
|
||
|
checkLatency (convolution, config);
|
||
|
|
||
|
auto processBlocksWithDiracImpulse = [&]
|
||
|
{
|
||
|
for (auto i = 0; i != numBlocksForImpulse; ++i)
|
||
|
{
|
||
|
if (i == 0)
|
||
|
addDiracImpulse (block);
|
||
|
else
|
||
|
block.clear();
|
||
|
|
||
|
convolution.process (context);
|
||
|
|
||
|
for (auto c = 0; c != static_cast<int> (spec.numChannels); ++c)
|
||
|
{
|
||
|
outBuffer.copyFrom (c,
|
||
|
i * static_cast<int> (spec.maximumBlockSize),
|
||
|
block.getChannelPointer (static_cast<size_t> (c)),
|
||
|
static_cast<int> (spec.maximumBlockSize));
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// If we load an IR while the convolution is already running, we'll need to wait
|
||
|
// for it to be loaded on a background thread
|
||
|
if (initSequence == InitSequence::prepareThenLoad)
|
||
|
{
|
||
|
const auto time = Time::getMillisecondCounter();
|
||
|
|
||
|
// Wait 10 seconds to load the impulse response
|
||
|
while (Time::getMillisecondCounter() - time < 10'000)
|
||
|
{
|
||
|
processBlocksWithDiracImpulse();
|
||
|
|
||
|
// Check if the impulse response was loaded
|
||
|
if (block.getSample (0, 1) != 0.0f)
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// At this point, our convolution should be loaded and the current IR size should
|
||
|
// match the expected result size
|
||
|
expect (convolution.getCurrentIRSize() == static_cast<int> (expectedResult.getNumSamples()));
|
||
|
|
||
|
// Make sure we get any smoothing out of the way
|
||
|
nTimes (numBlocksPerSecond, processBlocksWithDiracImpulse);
|
||
|
|
||
|
nTimes (5, [&]
|
||
|
{
|
||
|
processBlocksWithDiracImpulse();
|
||
|
|
||
|
const auto actualLatency = static_cast<size_t> (convolution.getLatency());
|
||
|
|
||
|
// The output should be the same as the IR
|
||
|
for (size_t c = 0; c != static_cast<size_t> (expectedResult.getNumChannels()); ++c)
|
||
|
{
|
||
|
for (size_t i = 0; i != static_cast<size_t> (expectedResult.getNumSamples()); ++i)
|
||
|
{
|
||
|
const auto equivalentSample = i + actualLatency;
|
||
|
|
||
|
if (static_cast<int> (equivalentSample) >= outBuffer.getNumSamples())
|
||
|
continue;
|
||
|
|
||
|
nonAllocatingExpectWithinAbsoluteError (outBuffer.getSample ((int) c, (int) equivalentSample),
|
||
|
expectedResult.getSample ((int) c, (int) i),
|
||
|
0.01f);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
template <typename ConvolutionConfig>
|
||
|
void testConvolution (const ProcessSpec& spec,
|
||
|
const ConvolutionConfig& config,
|
||
|
const AudioBuffer<float>& ir,
|
||
|
double irSampleRate,
|
||
|
Convolution::Stereo stereo,
|
||
|
Convolution::Trim trim,
|
||
|
Convolution::Normalise normalise,
|
||
|
const AudioBlock<const float>& expectedResult)
|
||
|
{
|
||
|
for (const auto sequence : { InitSequence::prepareThenLoad, InitSequence::loadThenPrepare })
|
||
|
testConvolution (spec, config, ir, irSampleRate, stereo, trim, normalise, expectedResult, sequence);
|
||
|
}
|
||
|
|
||
|
public:
|
||
|
ConvolutionTest()
|
||
|
: UnitTest ("Convolution", UnitTestCategories::dsp)
|
||
|
{}
|
||
|
|
||
|
void runTest() override
|
||
|
{
|
||
|
const ProcessSpec spec { 44100.0, 512, 2 };
|
||
|
AudioBuffer<float> buffer (static_cast<int> (spec.numChannels),
|
||
|
static_cast<int> (spec.maximumBlockSize));
|
||
|
AudioBlock<float> block { buffer };
|
||
|
ProcessContextReplacing<float> context { block };
|
||
|
|
||
|
const auto impulseData = []
|
||
|
{
|
||
|
Random random;
|
||
|
AudioBuffer<float> result (2, 1000);
|
||
|
|
||
|
for (auto channel = 0; channel != result.getNumChannels(); ++channel)
|
||
|
for (auto sample = 0; sample != result.getNumSamples(); ++sample)
|
||
|
result.setSample (channel, sample, random.nextFloat());
|
||
|
|
||
|
return result;
|
||
|
}();
|
||
|
|
||
|
beginTest ("Impulse responses can be loaded without allocating on the audio thread");
|
||
|
{
|
||
|
Convolution convolution;
|
||
|
convolution.prepare (spec);
|
||
|
|
||
|
auto copy = impulseData;
|
||
|
|
||
|
JUCE_FAIL_ON_ALLOCATION_IN_SCOPE;
|
||
|
|
||
|
nTimes (100, [&]
|
||
|
{
|
||
|
convolution.loadImpulseResponse (std::move (copy),
|
||
|
1000,
|
||
|
Convolution::Stereo::yes,
|
||
|
Convolution::Trim::yes,
|
||
|
Convolution::Normalise::no);
|
||
|
addDiracImpulse (block);
|
||
|
convolution.process (context);
|
||
|
checkForNans (block);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
beginTest ("Convolution can be reset without allocating on the audio thread");
|
||
|
{
|
||
|
Convolution convolution;
|
||
|
convolution.prepare (spec);
|
||
|
|
||
|
auto copy = impulseData;
|
||
|
|
||
|
convolution.loadImpulseResponse (std::move (copy),
|
||
|
1000,
|
||
|
Convolution::Stereo::yes,
|
||
|
Convolution::Trim::yes,
|
||
|
Convolution::Normalise::yes);
|
||
|
|
||
|
JUCE_FAIL_ON_ALLOCATION_IN_SCOPE;
|
||
|
|
||
|
nTimes (100, [&]
|
||
|
{
|
||
|
addDiracImpulse (block);
|
||
|
convolution.reset();
|
||
|
convolution.process (context);
|
||
|
convolution.reset();
|
||
|
});
|
||
|
|
||
|
checkForNans (block);
|
||
|
}
|
||
|
|
||
|
beginTest ("Completely empty IRs don't crash");
|
||
|
{
|
||
|
AudioBuffer<float> emptyBuffer;
|
||
|
|
||
|
Convolution convolution;
|
||
|
convolution.prepare (spec);
|
||
|
|
||
|
auto copy = impulseData;
|
||
|
|
||
|
convolution.loadImpulseResponse (std::move (copy),
|
||
|
2000,
|
||
|
Convolution::Stereo::yes,
|
||
|
Convolution::Trim::yes,
|
||
|
Convolution::Normalise::yes);
|
||
|
|
||
|
JUCE_FAIL_ON_ALLOCATION_IN_SCOPE;
|
||
|
|
||
|
nTimes (100, [&]
|
||
|
{
|
||
|
addDiracImpulse (block);
|
||
|
convolution.reset();
|
||
|
convolution.process (context);
|
||
|
convolution.reset();
|
||
|
});
|
||
|
|
||
|
checkForNans (block);
|
||
|
}
|
||
|
|
||
|
beginTest ("Convolutions can cope with a change in samplerate and blocksize");
|
||
|
{
|
||
|
Convolution convolution;
|
||
|
|
||
|
auto copy = impulseData;
|
||
|
convolution.loadImpulseResponse (std::move (copy),
|
||
|
2000,
|
||
|
Convolution::Stereo::yes,
|
||
|
Convolution::Trim::no,
|
||
|
Convolution::Normalise::yes);
|
||
|
|
||
|
const dsp::ProcessSpec specs[] = { { 96'000.0, 1024, 2 },
|
||
|
{ 48'000.0, 512, 2 },
|
||
|
{ 44'100.0, 256, 2 } };
|
||
|
|
||
|
for (const auto& thisSpec : specs)
|
||
|
{
|
||
|
convolution.prepare (thisSpec);
|
||
|
|
||
|
expectWithinAbsoluteError ((double) convolution.getCurrentIRSize(),
|
||
|
thisSpec.sampleRate * 0.5,
|
||
|
1.0);
|
||
|
|
||
|
juce::AudioBuffer<float> thisBuffer ((int) thisSpec.numChannels,
|
||
|
(int) thisSpec.maximumBlockSize);
|
||
|
AudioBlock<float> thisBlock { thisBuffer };
|
||
|
ProcessContextReplacing<float> thisContext { thisBlock };
|
||
|
|
||
|
nTimes (100, [&]
|
||
|
{
|
||
|
addDiracImpulse (thisBlock);
|
||
|
convolution.process (thisContext);
|
||
|
|
||
|
checkForNans (thisBlock);
|
||
|
checkAllChannelsNonZero (thisBlock);
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
beginTest ("Short uniform convolutions work");
|
||
|
{
|
||
|
const auto ramp = makeRamp (static_cast<int> (spec.maximumBlockSize) / 2);
|
||
|
testConvolution (spec,
|
||
|
Convolution::Latency { 0 },
|
||
|
ramp,
|
||
|
spec.sampleRate,
|
||
|
Convolution::Stereo::yes,
|
||
|
Convolution::Trim::yes,
|
||
|
Convolution::Normalise::no,
|
||
|
ramp);
|
||
|
}
|
||
|
|
||
|
beginTest ("Longer uniform convolutions work");
|
||
|
{
|
||
|
const auto ramp = makeRamp (static_cast<int> (spec.maximumBlockSize) * 8);
|
||
|
testConvolution (spec,
|
||
|
Convolution::Latency { 0 },
|
||
|
ramp,
|
||
|
spec.sampleRate,
|
||
|
Convolution::Stereo::yes,
|
||
|
Convolution::Trim::yes,
|
||
|
Convolution::Normalise::no,
|
||
|
ramp);
|
||
|
}
|
||
|
|
||
|
beginTest ("Normalisation works");
|
||
|
{
|
||
|
const auto ramp = makeRamp (static_cast<int> (spec.maximumBlockSize) * 8);
|
||
|
|
||
|
auto copy = ramp;
|
||
|
const auto channels = copy.getArrayOfWritePointers();
|
||
|
const auto numChannels = copy.getNumChannels();
|
||
|
const auto numSamples = copy.getNumSamples();
|
||
|
|
||
|
const auto factor = 0.125f / std::sqrt (std::accumulate (channels, channels + numChannels, 0.0f,
|
||
|
[numSamples] (auto max, auto* channel)
|
||
|
{
|
||
|
return juce::jmax (max, std::accumulate (channel, channel + numSamples, 0.0f,
|
||
|
[] (auto sum, auto sample)
|
||
|
{
|
||
|
return sum + sample * sample;
|
||
|
}));
|
||
|
}));
|
||
|
|
||
|
std::for_each (channels, channels + numChannels, [factor, numSamples] (auto* channel)
|
||
|
{
|
||
|
FloatVectorOperations::multiply (channel, factor, numSamples);
|
||
|
});
|
||
|
|
||
|
testConvolution (spec,
|
||
|
Convolution::Latency { 0 },
|
||
|
ramp,
|
||
|
spec.sampleRate,
|
||
|
Convolution::Stereo::yes,
|
||
|
Convolution::Trim::yes,
|
||
|
Convolution::Normalise::yes,
|
||
|
copy);
|
||
|
}
|
||
|
|
||
|
beginTest ("Stereo convolutions work");
|
||
|
{
|
||
|
const auto ramp = makeStereoRamp (static_cast<int> (spec.maximumBlockSize) * 5);
|
||
|
testConvolution (spec,
|
||
|
Convolution::Latency { 0 },
|
||
|
ramp,
|
||
|
spec.sampleRate,
|
||
|
Convolution::Stereo::yes,
|
||
|
Convolution::Trim::yes,
|
||
|
Convolution::Normalise::no,
|
||
|
ramp);
|
||
|
}
|
||
|
|
||
|
beginTest ("Stereo IRs only use first channel if stereo is disabled");
|
||
|
{
|
||
|
const auto length = static_cast<int> (spec.maximumBlockSize) * 5;
|
||
|
const auto ramp = makeStereoRamp (length);
|
||
|
|
||
|
const float* channels[] { ramp.getReadPointer (0), ramp.getReadPointer (0) };
|
||
|
|
||
|
testConvolution (spec,
|
||
|
Convolution::Latency { 0 },
|
||
|
ramp,
|
||
|
spec.sampleRate,
|
||
|
Convolution::Stereo::no,
|
||
|
Convolution::Trim::yes,
|
||
|
Convolution::Normalise::no,
|
||
|
AudioBlock<const float> (channels, numElementsInArray (channels), length));
|
||
|
}
|
||
|
|
||
|
beginTest ("IRs with extra silence are trimmed appropriately");
|
||
|
{
|
||
|
const auto length = static_cast<int> (spec.maximumBlockSize) * 3;
|
||
|
const auto ramp = makeRamp (length);
|
||
|
AudioBuffer<float> paddedRamp (ramp.getNumChannels(), ramp.getNumSamples() * 2);
|
||
|
paddedRamp.clear();
|
||
|
|
||
|
const auto offset = (paddedRamp.getNumSamples() - ramp.getNumSamples()) / 2;
|
||
|
|
||
|
for (auto channel = 0; channel != ramp.getNumChannels(); ++channel)
|
||
|
paddedRamp.copyFrom (channel, offset, ramp.getReadPointer (channel), length);
|
||
|
|
||
|
testConvolution (spec,
|
||
|
Convolution::Latency { 0 },
|
||
|
paddedRamp,
|
||
|
spec.sampleRate,
|
||
|
Convolution::Stereo::no,
|
||
|
Convolution::Trim::yes,
|
||
|
Convolution::Normalise::no,
|
||
|
ramp);
|
||
|
}
|
||
|
|
||
|
beginTest ("IRs are resampled if their sample rate is different to the playback rate");
|
||
|
{
|
||
|
for (const auto resampleRatio : { 0.1, 0.5, 2.0, 10.0 })
|
||
|
{
|
||
|
const auto length = static_cast<int> (spec.maximumBlockSize) * 2;
|
||
|
const auto ramp = makeStereoRamp (length);
|
||
|
|
||
|
const auto resampled = [&]
|
||
|
{
|
||
|
AudioBuffer<float> original = ramp;
|
||
|
MemoryAudioSource memorySource (original, false);
|
||
|
ResamplingAudioSource resamplingSource (&memorySource, false, original.getNumChannels());
|
||
|
|
||
|
const auto finalSize = roundToInt (original.getNumSamples() / resampleRatio);
|
||
|
resamplingSource.setResamplingRatio (resampleRatio);
|
||
|
resamplingSource.prepareToPlay (finalSize, spec.sampleRate * resampleRatio);
|
||
|
|
||
|
AudioBuffer<float> result (original.getNumChannels(), finalSize);
|
||
|
resamplingSource.getNextAudioBlock ({ &result, 0, result.getNumSamples() });
|
||
|
|
||
|
result.applyGain ((float) resampleRatio);
|
||
|
|
||
|
return result;
|
||
|
}();
|
||
|
|
||
|
testConvolution (spec,
|
||
|
Convolution::Latency { 0 },
|
||
|
ramp,
|
||
|
spec.sampleRate * resampleRatio,
|
||
|
Convolution::Stereo::yes,
|
||
|
Convolution::Trim::yes,
|
||
|
Convolution::Normalise::no,
|
||
|
resampled);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
beginTest ("Non-uniform convolutions work");
|
||
|
{
|
||
|
const auto ramp = makeRamp (static_cast<int> (spec.maximumBlockSize) * 8);
|
||
|
|
||
|
for (auto headSize : { spec.maximumBlockSize / 2, spec.maximumBlockSize, spec.maximumBlockSize * 9 })
|
||
|
{
|
||
|
testConvolution (spec,
|
||
|
Convolution::NonUniform { static_cast<int> (headSize) },
|
||
|
ramp,
|
||
|
spec.sampleRate,
|
||
|
Convolution::Stereo::yes,
|
||
|
Convolution::Trim::yes,
|
||
|
Convolution::Normalise::no,
|
||
|
ramp);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
beginTest ("Convolutions with latency work");
|
||
|
{
|
||
|
const auto ramp = makeRamp (static_cast<int> (spec.maximumBlockSize) * 8);
|
||
|
using BlockSize = decltype (spec.maximumBlockSize);
|
||
|
|
||
|
for (auto latency : { static_cast<BlockSize> (0),
|
||
|
spec.maximumBlockSize / 3,
|
||
|
spec.maximumBlockSize,
|
||
|
spec.maximumBlockSize * 2,
|
||
|
static_cast<BlockSize> (spec.maximumBlockSize * 2.5) })
|
||
|
{
|
||
|
testConvolution (spec,
|
||
|
Convolution::Latency { static_cast<int> (latency) },
|
||
|
ramp,
|
||
|
spec.sampleRate,
|
||
|
Convolution::Stereo::yes,
|
||
|
Convolution::Trim::yes,
|
||
|
Convolution::Normalise::no,
|
||
|
ramp);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
ConvolutionTest convolutionUnitTest;
|
||
|
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#undef JUCE_FAIL_ON_ALLOCATION_IN_SCOPE
|