26#if JUCE_ENABLE_ALLOCATION_HOOKS
27#define JUCE_FAIL_ON_ALLOCATION_IN_SCOPE const UnitTestAllocationChecker checker (*this)
29#define JUCE_FAIL_ON_ALLOCATION_IN_SCOPE
37class ConvolutionTest final :
public UnitTest
39 template <
typename Callback>
40 static void nTimes (
int n, Callback&& callback)
42 for (
auto i = 0; i < n; ++i)
46 static AudioBuffer<float> makeRamp (
int length)
48 AudioBuffer<float> result (1, length);
51 const auto writePtr = result.getWritePointer (0);
52 std::fill (writePtr, writePtr + length, 1.0f);
53 result.applyGainRamp (0, length, 1.0f, 0.0f);
58 static AudioBuffer<float> makeStereoRamp (
int length)
60 AudioBuffer<float> result (2, length);
63 auto*
const* channels = result.getArrayOfWritePointers();
64 std::for_each (channels, channels + result.getNumChannels(), [length] (
auto* channel)
66 std::fill (channel, channel + length, 1.0f);
69 result.applyGainRamp (0, 0, length, 1.0f, 0.0f);
70 result.applyGainRamp (1, 0, length, 0.0f, 1.0f);
75 static void addDiracImpulse (
const AudioBlock<float>& block)
79 for (
size_t channel = 0; channel != block.getNumChannels(); ++channel)
80 block.setSample ((
int) channel, 0, 1.0f);
83 void checkForNans (
const AudioBlock<float>& block)
85 for (
size_t channel = 0; channel != block.getNumChannels(); ++channel)
86 for (
size_t sample = 0; sample != block.getNumSamples(); ++sample)
87 expect (! std::isnan (block.getSample ((
int) channel, (
int) sample)));
90 void checkAllChannelsNonZero (
const AudioBlock<float>& block)
92 for (
size_t i = 0; i != block.getNumChannels(); ++i)
94 const auto* channel = block.getChannelPointer (i);
96 expect (std::any_of (channel, channel + block.getNumSamples(), [] (
float sample)
98 return ! approximatelyEqual (sample, 0.0f);
103 template <
typename T>
104 void nonAllocatingExpectWithinAbsoluteError (
const T& a,
const T& b,
const T& error)
106 expect (std::abs (a - b) < error);
109 enum class InitSequence { prepareThenLoad, loadThenPrepare };
111 void checkLatency (
const Convolution& convolution,
const Convolution::Latency& latency)
113 const auto reportedLatency = convolution.getLatency();
115 if (latency.latencyInSamples == 0)
116 expect (reportedLatency == 0);
118 expect (reportedLatency >= latency.latencyInSamples);
121 void checkLatency (
const Convolution&,
const Convolution::NonUniform&) {}
123 template <
typename ConvolutionConfig>
124 void testConvolution (
const ProcessSpec& spec,
125 const ConvolutionConfig& config,
126 const AudioBuffer<float>& ir,
128 Convolution::Stereo stereo,
129 Convolution::Trim trim,
130 Convolution::Normalise normalise,
131 const AudioBlock<const float>& expectedResult,
132 InitSequence initSequence)
134 AudioBuffer<float> buffer (
static_cast<int> (spec.numChannels),
135 static_cast<int> (spec.maximumBlockSize));
136 AudioBlock<float> block { buffer };
137 ProcessContextReplacing<float> context { block };
139 const auto numBlocksPerSecond = (int) std::ceil (spec.sampleRate / spec.maximumBlockSize);
140 const auto numBlocksForImpulse = (int) std::ceil ((
double) expectedResult.getNumSamples() / spec.maximumBlockSize);
142 AudioBuffer<float> outBuffer (
static_cast<int> (spec.numChannels),
143 numBlocksForImpulse *
static_cast<int> (spec.maximumBlockSize));
145 Convolution convolution (config);
149 if (initSequence == InitSequence::loadThenPrepare)
150 convolution.loadImpulseResponse (std::move (copiedIr), irSampleRate, stereo, trim, normalise);
152 convolution.prepare (spec);
154 JUCE_FAIL_ON_ALLOCATION_IN_SCOPE;
156 if (initSequence == InitSequence::prepareThenLoad)
157 convolution.loadImpulseResponse (std::move (copiedIr), irSampleRate, stereo, trim, normalise);
159 checkLatency (convolution, config);
161 auto processBlocksWithDiracImpulse = [&]
163 for (
auto i = 0; i != numBlocksForImpulse; ++i)
166 addDiracImpulse (block);
170 convolution.process (context);
172 for (
auto c = 0; c !=
static_cast<int> (spec.numChannels); ++c)
174 outBuffer.copyFrom (c,
175 i *
static_cast<int> (spec.maximumBlockSize),
176 block.getChannelPointer (
static_cast<size_t> (c)),
177 static_cast<int> (spec.maximumBlockSize));
184 if (initSequence == InitSequence::prepareThenLoad)
191 processBlocksWithDiracImpulse();
194 if (! approximatelyEqual (block.getSample (0, 1), 0.0f))
201 expect (convolution.getCurrentIRSize() ==
static_cast<int> (expectedResult.getNumSamples()));
204 nTimes (numBlocksPerSecond, processBlocksWithDiracImpulse);
208 processBlocksWithDiracImpulse();
210 const auto actualLatency =
static_cast<size_t> (convolution.getLatency());
213 for (
size_t c = 0; c !=
static_cast<size_t> (expectedResult.getNumChannels()); ++c)
215 for (
size_t i = 0; i !=
static_cast<size_t> (expectedResult.getNumSamples()); ++i)
217 const auto equivalentSample = i + actualLatency;
219 if (
static_cast<int> (equivalentSample) >= outBuffer.getNumSamples())
222 nonAllocatingExpectWithinAbsoluteError (outBuffer.getSample ((
int) c, (
int) equivalentSample),
223 expectedResult.getSample ((
int) c, (
int) i),
230 template <
typename ConvolutionConfig>
231 void testConvolution (
const ProcessSpec& spec,
232 const ConvolutionConfig& config,
233 const AudioBuffer<float>& ir,
235 Convolution::Stereo stereo,
236 Convolution::Trim trim,
237 Convolution::Normalise normalise,
238 const AudioBlock<const float>& expectedResult)
240 for (
const auto sequence : { InitSequence::prepareThenLoad, InitSequence::loadThenPrepare })
241 testConvolution (spec, config, ir, irSampleRate, stereo, trim, normalise, expectedResult, sequence);
246 : UnitTest (
"Convolution", UnitTestCategories::dsp)
249 void runTest()
override
251 const ProcessSpec spec { 44100.0, 512, 2 };
252 AudioBuffer<float> buffer (
static_cast<int> (spec.numChannels),
253 static_cast<int> (spec.maximumBlockSize));
254 AudioBlock<float> block { buffer };
255 ProcessContextReplacing<float> context { block };
257 const auto impulseData = []
260 AudioBuffer<float> result (2, 1000);
262 for (
auto channel = 0; channel != result.getNumChannels(); ++channel)
263 for (
auto sample = 0; sample != result.getNumSamples(); ++sample)
264 result.setSample (channel, sample, random.nextFloat());
269 beginTest (
"Impulse responses can be loaded without allocating on the audio thread");
271 Convolution convolution;
272 convolution.prepare (spec);
274 auto copy = impulseData;
276 JUCE_FAIL_ON_ALLOCATION_IN_SCOPE;
280 convolution.loadImpulseResponse (std::move (copy),
282 Convolution::Stereo::yes,
283 Convolution::Trim::yes,
284 Convolution::Normalise::no);
285 addDiracImpulse (block);
286 convolution.process (context);
287 checkForNans (block);
291 beginTest (
"Convolution can be reset without allocating on the audio thread");
293 Convolution convolution;
294 convolution.prepare (spec);
296 auto copy = impulseData;
298 convolution.loadImpulseResponse (std::move (copy),
300 Convolution::Stereo::yes,
301 Convolution::Trim::yes,
302 Convolution::Normalise::yes);
304 JUCE_FAIL_ON_ALLOCATION_IN_SCOPE;
308 addDiracImpulse (block);
310 convolution.process (context);
314 checkForNans (block);
317 beginTest (
"Completely empty IRs don't crash");
319 AudioBuffer<float> emptyBuffer;
321 Convolution convolution;
322 convolution.prepare (spec);
324 auto copy = impulseData;
326 convolution.loadImpulseResponse (std::move (copy),
328 Convolution::Stereo::yes,
329 Convolution::Trim::yes,
330 Convolution::Normalise::yes);
332 JUCE_FAIL_ON_ALLOCATION_IN_SCOPE;
336 addDiracImpulse (block);
338 convolution.process (context);
342 checkForNans (block);
345 beginTest (
"Convolutions can cope with a change in samplerate and blocksize");
347 Convolution convolution;
349 auto copy = impulseData;
350 convolution.loadImpulseResponse (std::move (copy),
352 Convolution::Stereo::yes,
353 Convolution::Trim::no,
354 Convolution::Normalise::yes);
356 const dsp::ProcessSpec specs[] = { { 96'000.0, 1024, 2 },
357 { 48'000.0, 512, 2 },
358 { 44'100.0, 256, 2 } };
360 for (
const auto& thisSpec : specs)
362 convolution.prepare (thisSpec);
364 expectWithinAbsoluteError ((
double) convolution.getCurrentIRSize(),
365 thisSpec.sampleRate * 0.5,
368 juce::AudioBuffer<float> thisBuffer ((
int) thisSpec.numChannels,
369 (
int) thisSpec.maximumBlockSize);
370 AudioBlock<float> thisBlock { thisBuffer };
371 ProcessContextReplacing<float> thisContext { thisBlock };
375 addDiracImpulse (thisBlock);
376 convolution.process (thisContext);
378 checkForNans (thisBlock);
379 checkAllChannelsNonZero (thisBlock);
384 beginTest (
"Short uniform convolutions work");
386 const auto ramp = makeRamp (
static_cast<int> (spec.maximumBlockSize) / 2);
387 testConvolution (spec,
388 Convolution::Latency { 0 },
391 Convolution::Stereo::yes,
392 Convolution::Trim::yes,
393 Convolution::Normalise::no,
397 beginTest (
"Longer uniform convolutions work");
399 const auto ramp = makeRamp (
static_cast<int> (spec.maximumBlockSize) * 8);
400 testConvolution (spec,
401 Convolution::Latency { 0 },
404 Convolution::Stereo::yes,
405 Convolution::Trim::yes,
406 Convolution::Normalise::no,
410 beginTest (
"Normalisation works");
412 const auto ramp = makeRamp (
static_cast<int> (spec.maximumBlockSize) * 8);
415 const auto channels = copy.getArrayOfWritePointers();
416 const auto numChannels = copy.getNumChannels();
417 const auto numSamples = copy.getNumSamples();
419 const auto factor = 0.125f / std::sqrt (std::accumulate (channels, channels + numChannels, 0.0f,
420 [numSamples] (
auto max,
auto* channel)
422 return juce::jmax (max, std::accumulate (channel, channel + numSamples, 0.0f,
423 [] (
auto sum,
auto sample)
425 return sum + sample * sample;
429 std::for_each (channels, channels + numChannels, [factor, numSamples] (
auto* channel)
431 FloatVectorOperations::multiply (channel, factor, numSamples);
434 testConvolution (spec,
435 Convolution::Latency { 0 },
438 Convolution::Stereo::yes,
439 Convolution::Trim::yes,
440 Convolution::Normalise::yes,
444 beginTest (
"Stereo convolutions work");
446 const auto ramp = makeStereoRamp (
static_cast<int> (spec.maximumBlockSize) * 5);
447 testConvolution (spec,
448 Convolution::Latency { 0 },
451 Convolution::Stereo::yes,
452 Convolution::Trim::yes,
453 Convolution::Normalise::no,
457 beginTest (
"Stereo IRs only use first channel if stereo is disabled");
459 const auto length =
static_cast<int> (spec.maximumBlockSize) * 5;
460 const auto ramp = makeStereoRamp (length);
462 const float* channels[] { ramp.getReadPointer (0), ramp.getReadPointer (0) };
464 testConvolution (spec,
465 Convolution::Latency { 0 },
468 Convolution::Stereo::no,
469 Convolution::Trim::yes,
470 Convolution::Normalise::no,
471 AudioBlock<const float> (channels, numElementsInArray (channels), (
size_t) length));
474 beginTest (
"IRs with extra silence are trimmed appropriately");
476 const auto length =
static_cast<int> (spec.maximumBlockSize) * 3;
477 const auto ramp = makeRamp (length);
478 AudioBuffer<float> paddedRamp (ramp.getNumChannels(), ramp.getNumSamples() * 2);
481 const auto offset = (paddedRamp.getNumSamples() - ramp.getNumSamples()) / 2;
483 for (
auto channel = 0; channel != ramp.getNumChannels(); ++channel)
484 paddedRamp.copyFrom (channel, offset, ramp.getReadPointer (channel), length);
486 testConvolution (spec,
487 Convolution::Latency { 0 },
490 Convolution::Stereo::no,
491 Convolution::Trim::yes,
492 Convolution::Normalise::no,
496 beginTest (
"IRs are resampled if their sample rate is different to the playback rate");
498 for (
const auto resampleRatio : { 0.1, 0.5, 2.0, 10.0 })
500 const auto length =
static_cast<int> (spec.maximumBlockSize) * 2;
501 const auto ramp = makeStereoRamp (length);
503 const auto resampled = [&]
505 AudioBuffer<float> original = ramp;
506 MemoryAudioSource memorySource (original,
false);
507 ResamplingAudioSource resamplingSource (&memorySource,
false, original.getNumChannels());
509 const auto finalSize = roundToInt (original.getNumSamples() / resampleRatio);
510 resamplingSource.setResamplingRatio (resampleRatio);
511 resamplingSource.prepareToPlay (finalSize, spec.sampleRate * resampleRatio);
513 AudioBuffer<float> result (original.getNumChannels(), finalSize);
514 resamplingSource.getNextAudioBlock ({ &result, 0, result.getNumSamples() });
516 result.applyGain ((
float) resampleRatio);
521 testConvolution (spec,
522 Convolution::Latency { 0 },
524 spec.sampleRate * resampleRatio,
525 Convolution::Stereo::yes,
526 Convolution::Trim::yes,
527 Convolution::Normalise::no,
532 beginTest (
"Non-uniform convolutions work");
534 const auto ramp = makeRamp (
static_cast<int> (spec.maximumBlockSize) * 8);
536 for (
auto headSize : { spec.maximumBlockSize / 2, spec.maximumBlockSize, spec.maximumBlockSize * 9 })
538 testConvolution (spec,
539 Convolution::NonUniform {
static_cast<int> (headSize) },
542 Convolution::Stereo::yes,
543 Convolution::Trim::yes,
544 Convolution::Normalise::no,
549 beginTest (
"Convolutions with latency work");
551 const auto ramp = makeRamp (
static_cast<int> (spec.maximumBlockSize) * 8);
552 using BlockSize =
decltype (spec.maximumBlockSize);
554 for (
auto latency : {
static_cast<BlockSize
> (0),
555 spec.maximumBlockSize / 3,
556 spec.maximumBlockSize,
557 spec.maximumBlockSize * 2,
558 static_cast<BlockSize
> (spec.maximumBlockSize * 2.5) })
560 testConvolution (spec,
561 Convolution::Latency {
static_cast<int> (latency) },
564 Convolution::Stereo::yes,
565 Convolution::Trim::yes,
566 Convolution::Normalise::no,
573ConvolutionTest convolutionUnitTest;
578#undef JUCE_FAIL_ON_ALLOCATION_IN_SCOPE
static uint32 getMillisecondCounter() noexcept