OpenShot Audio Library | OpenShotAudio 0.4.0
juce_Convolution_test.cpp
1/*
2 ==============================================================================
3
4 This file is part of the JUCE library.
5 Copyright (c) 2022 - Raw Material Software Limited
6
7 JUCE is an open source library subject to commercial or open-source
8 licensing.
9
10 By using JUCE, you agree to the terms of both the JUCE 7 End-User License
11 Agreement and JUCE Privacy Policy.
12
13 End User License Agreement: www.juce.com/juce-7-licence
14 Privacy Policy: www.juce.com/juce-privacy-policy
15
16 Or: You may also use this code under the terms of the GPL v3 (see
17 www.gnu.org/licenses).
18
19 JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
20 EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
21 DISCLAIMED.
22
23 ==============================================================================
24*/
25
26#if JUCE_ENABLE_ALLOCATION_HOOKS
27#define JUCE_FAIL_ON_ALLOCATION_IN_SCOPE const UnitTestAllocationChecker checker (*this)
28#else
29#define JUCE_FAIL_ON_ALLOCATION_IN_SCOPE
30#endif
31
32namespace juce::dsp
33{
34namespace
35{
36
37class ConvolutionTest final : public UnitTest
38{
39 template <typename Callback>
40 static void nTimes (int n, Callback&& callback)
41 {
42 for (auto i = 0; i < n; ++i)
43 callback();
44 }
45
46 static AudioBuffer<float> makeRamp (int length)
47 {
48 AudioBuffer<float> result (1, length);
49 result.clear();
50
51 const auto writePtr = result.getWritePointer (0);
52 std::fill (writePtr, writePtr + length, 1.0f);
53 result.applyGainRamp (0, length, 1.0f, 0.0f);
54
55 return result;
56 }
57
58 static AudioBuffer<float> makeStereoRamp (int length)
59 {
60 AudioBuffer<float> result (2, length);
61 result.clear();
62
63 auto* const* channels = result.getArrayOfWritePointers();
64 std::for_each (channels, channels + result.getNumChannels(), [length] (auto* channel)
65 {
66 std::fill (channel, channel + length, 1.0f);
67 });
68
69 result.applyGainRamp (0, 0, length, 1.0f, 0.0f);
70 result.applyGainRamp (1, 0, length, 0.0f, 1.0f);
71
72 return result;
73 }
74
75 static void addDiracImpulse (const AudioBlock<float>& block)
76 {
77 block.clear();
78
79 for (size_t channel = 0; channel != block.getNumChannels(); ++channel)
80 block.setSample ((int) channel, 0, 1.0f);
81 }
82
83 void checkForNans (const AudioBlock<float>& block)
84 {
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)));
88 }
89
90 void checkAllChannelsNonZero (const AudioBlock<float>& block)
91 {
92 for (size_t i = 0; i != block.getNumChannels(); ++i)
93 {
94 const auto* channel = block.getChannelPointer (i);
95
96 expect (std::any_of (channel, channel + block.getNumSamples(), [] (float sample)
97 {
98 return ! approximatelyEqual (sample, 0.0f);
99 }));
100 }
101 }
102
103 template <typename T>
104 void nonAllocatingExpectWithinAbsoluteError (const T& a, const T& b, const T& error)
105 {
106 expect (std::abs (a - b) < error);
107 }
108
109 enum class InitSequence { prepareThenLoad, loadThenPrepare };
110
111 void checkLatency (const Convolution& convolution, const Convolution::Latency& latency)
112 {
113 const auto reportedLatency = convolution.getLatency();
114
115 if (latency.latencyInSamples == 0)
116 expect (reportedLatency == 0);
117
118 expect (reportedLatency >= latency.latencyInSamples);
119 }
120
121 void checkLatency (const Convolution&, const Convolution::NonUniform&) {}
122
123 template <typename ConvolutionConfig>
124 void testConvolution (const ProcessSpec& spec,
125 const ConvolutionConfig& config,
126 const AudioBuffer<float>& ir,
127 double irSampleRate,
128 Convolution::Stereo stereo,
129 Convolution::Trim trim,
130 Convolution::Normalise normalise,
131 const AudioBlock<const float>& expectedResult,
132 InitSequence initSequence)
133 {
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 };
138
139 const auto numBlocksPerSecond = (int) std::ceil (spec.sampleRate / spec.maximumBlockSize);
140 const auto numBlocksForImpulse = (int) std::ceil ((double) expectedResult.getNumSamples() / spec.maximumBlockSize);
141
142 AudioBuffer<float> outBuffer (static_cast<int> (spec.numChannels),
143 numBlocksForImpulse * static_cast<int> (spec.maximumBlockSize));
144
145 Convolution convolution (config);
146
147 auto copiedIr = ir;
148
149 if (initSequence == InitSequence::loadThenPrepare)
150 convolution.loadImpulseResponse (std::move (copiedIr), irSampleRate, stereo, trim, normalise);
151
152 convolution.prepare (spec);
153
154 JUCE_FAIL_ON_ALLOCATION_IN_SCOPE;
155
156 if (initSequence == InitSequence::prepareThenLoad)
157 convolution.loadImpulseResponse (std::move (copiedIr), irSampleRate, stereo, trim, normalise);
158
159 checkLatency (convolution, config);
160
161 auto processBlocksWithDiracImpulse = [&]
162 {
163 for (auto i = 0; i != numBlocksForImpulse; ++i)
164 {
165 if (i == 0)
166 addDiracImpulse (block);
167 else
168 block.clear();
169
170 convolution.process (context);
171
172 for (auto c = 0; c != static_cast<int> (spec.numChannels); ++c)
173 {
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));
178 }
179 }
180 };
181
182 // If we load an IR while the convolution is already running, we'll need to wait
183 // for it to be loaded on a background thread
184 if (initSequence == InitSequence::prepareThenLoad)
185 {
186 const auto time = Time::getMillisecondCounter();
187
188 // Wait 10 seconds to load the impulse response
189 while (Time::getMillisecondCounter() - time < 10'000)
190 {
191 processBlocksWithDiracImpulse();
192
193 // Check if the impulse response was loaded
194 if (! approximatelyEqual (block.getSample (0, 1), 0.0f))
195 break;
196 }
197 }
198
199 // At this point, our convolution should be loaded and the current IR size should
200 // match the expected result size
201 expect (convolution.getCurrentIRSize() == static_cast<int> (expectedResult.getNumSamples()));
202
203 // Make sure we get any smoothing out of the way
204 nTimes (numBlocksPerSecond, processBlocksWithDiracImpulse);
205
206 nTimes (5, [&]
207 {
208 processBlocksWithDiracImpulse();
209
210 const auto actualLatency = static_cast<size_t> (convolution.getLatency());
211
212 // The output should be the same as the IR
213 for (size_t c = 0; c != static_cast<size_t> (expectedResult.getNumChannels()); ++c)
214 {
215 for (size_t i = 0; i != static_cast<size_t> (expectedResult.getNumSamples()); ++i)
216 {
217 const auto equivalentSample = i + actualLatency;
218
219 if (static_cast<int> (equivalentSample) >= outBuffer.getNumSamples())
220 continue;
221
222 nonAllocatingExpectWithinAbsoluteError (outBuffer.getSample ((int) c, (int) equivalentSample),
223 expectedResult.getSample ((int) c, (int) i),
224 0.01f);
225 }
226 }
227 });
228 }
229
230 template <typename ConvolutionConfig>
231 void testConvolution (const ProcessSpec& spec,
232 const ConvolutionConfig& config,
233 const AudioBuffer<float>& ir,
234 double irSampleRate,
235 Convolution::Stereo stereo,
236 Convolution::Trim trim,
237 Convolution::Normalise normalise,
238 const AudioBlock<const float>& expectedResult)
239 {
240 for (const auto sequence : { InitSequence::prepareThenLoad, InitSequence::loadThenPrepare })
241 testConvolution (spec, config, ir, irSampleRate, stereo, trim, normalise, expectedResult, sequence);
242 }
243
244public:
245 ConvolutionTest()
246 : UnitTest ("Convolution", UnitTestCategories::dsp)
247 {}
248
249 void runTest() override
250 {
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 };
256
257 const auto impulseData = []
258 {
259 Random random;
260 AudioBuffer<float> result (2, 1000);
261
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());
265
266 return result;
267 }();
268
269 beginTest ("Impulse responses can be loaded without allocating on the audio thread");
270 {
271 Convolution convolution;
272 convolution.prepare (spec);
273
274 auto copy = impulseData;
275
276 JUCE_FAIL_ON_ALLOCATION_IN_SCOPE;
277
278 nTimes (100, [&]
279 {
280 convolution.loadImpulseResponse (std::move (copy),
281 1000,
282 Convolution::Stereo::yes,
283 Convolution::Trim::yes,
284 Convolution::Normalise::no);
285 addDiracImpulse (block);
286 convolution.process (context);
287 checkForNans (block);
288 });
289 }
290
291 beginTest ("Convolution can be reset without allocating on the audio thread");
292 {
293 Convolution convolution;
294 convolution.prepare (spec);
295
296 auto copy = impulseData;
297
298 convolution.loadImpulseResponse (std::move (copy),
299 1000,
300 Convolution::Stereo::yes,
301 Convolution::Trim::yes,
302 Convolution::Normalise::yes);
303
304 JUCE_FAIL_ON_ALLOCATION_IN_SCOPE;
305
306 nTimes (100, [&]
307 {
308 addDiracImpulse (block);
309 convolution.reset();
310 convolution.process (context);
311 convolution.reset();
312 });
313
314 checkForNans (block);
315 }
316
317 beginTest ("Completely empty IRs don't crash");
318 {
319 AudioBuffer<float> emptyBuffer;
320
321 Convolution convolution;
322 convolution.prepare (spec);
323
324 auto copy = impulseData;
325
326 convolution.loadImpulseResponse (std::move (copy),
327 2000,
328 Convolution::Stereo::yes,
329 Convolution::Trim::yes,
330 Convolution::Normalise::yes);
331
332 JUCE_FAIL_ON_ALLOCATION_IN_SCOPE;
333
334 nTimes (100, [&]
335 {
336 addDiracImpulse (block);
337 convolution.reset();
338 convolution.process (context);
339 convolution.reset();
340 });
341
342 checkForNans (block);
343 }
344
345 beginTest ("Convolutions can cope with a change in samplerate and blocksize");
346 {
347 Convolution convolution;
348
349 auto copy = impulseData;
350 convolution.loadImpulseResponse (std::move (copy),
351 2000,
352 Convolution::Stereo::yes,
353 Convolution::Trim::no,
354 Convolution::Normalise::yes);
355
356 const dsp::ProcessSpec specs[] = { { 96'000.0, 1024, 2 },
357 { 48'000.0, 512, 2 },
358 { 44'100.0, 256, 2 } };
359
360 for (const auto& thisSpec : specs)
361 {
362 convolution.prepare (thisSpec);
363
364 expectWithinAbsoluteError ((double) convolution.getCurrentIRSize(),
365 thisSpec.sampleRate * 0.5,
366 1.0);
367
368 juce::AudioBuffer<float> thisBuffer ((int) thisSpec.numChannels,
369 (int) thisSpec.maximumBlockSize);
370 AudioBlock<float> thisBlock { thisBuffer };
371 ProcessContextReplacing<float> thisContext { thisBlock };
372
373 nTimes (100, [&]
374 {
375 addDiracImpulse (thisBlock);
376 convolution.process (thisContext);
377
378 checkForNans (thisBlock);
379 checkAllChannelsNonZero (thisBlock);
380 });
381 }
382 }
383
384 beginTest ("Short uniform convolutions work");
385 {
386 const auto ramp = makeRamp (static_cast<int> (spec.maximumBlockSize) / 2);
387 testConvolution (spec,
388 Convolution::Latency { 0 },
389 ramp,
390 spec.sampleRate,
391 Convolution::Stereo::yes,
392 Convolution::Trim::yes,
393 Convolution::Normalise::no,
394 ramp);
395 }
396
397 beginTest ("Longer uniform convolutions work");
398 {
399 const auto ramp = makeRamp (static_cast<int> (spec.maximumBlockSize) * 8);
400 testConvolution (spec,
401 Convolution::Latency { 0 },
402 ramp,
403 spec.sampleRate,
404 Convolution::Stereo::yes,
405 Convolution::Trim::yes,
406 Convolution::Normalise::no,
407 ramp);
408 }
409
410 beginTest ("Normalisation works");
411 {
412 const auto ramp = makeRamp (static_cast<int> (spec.maximumBlockSize) * 8);
413
414 auto copy = ramp;
415 const auto channels = copy.getArrayOfWritePointers();
416 const auto numChannels = copy.getNumChannels();
417 const auto numSamples = copy.getNumSamples();
418
419 const auto factor = 0.125f / std::sqrt (std::accumulate (channels, channels + numChannels, 0.0f,
420 [numSamples] (auto max, auto* channel)
421 {
422 return juce::jmax (max, std::accumulate (channel, channel + numSamples, 0.0f,
423 [] (auto sum, auto sample)
424 {
425 return sum + sample * sample;
426 }));
427 }));
428
429 std::for_each (channels, channels + numChannels, [factor, numSamples] (auto* channel)
430 {
431 FloatVectorOperations::multiply (channel, factor, numSamples);
432 });
433
434 testConvolution (spec,
435 Convolution::Latency { 0 },
436 ramp,
437 spec.sampleRate,
438 Convolution::Stereo::yes,
439 Convolution::Trim::yes,
440 Convolution::Normalise::yes,
441 copy);
442 }
443
444 beginTest ("Stereo convolutions work");
445 {
446 const auto ramp = makeStereoRamp (static_cast<int> (spec.maximumBlockSize) * 5);
447 testConvolution (spec,
448 Convolution::Latency { 0 },
449 ramp,
450 spec.sampleRate,
451 Convolution::Stereo::yes,
452 Convolution::Trim::yes,
453 Convolution::Normalise::no,
454 ramp);
455 }
456
457 beginTest ("Stereo IRs only use first channel if stereo is disabled");
458 {
459 const auto length = static_cast<int> (spec.maximumBlockSize) * 5;
460 const auto ramp = makeStereoRamp (length);
461
462 const float* channels[] { ramp.getReadPointer (0), ramp.getReadPointer (0) };
463
464 testConvolution (spec,
465 Convolution::Latency { 0 },
466 ramp,
467 spec.sampleRate,
468 Convolution::Stereo::no,
469 Convolution::Trim::yes,
470 Convolution::Normalise::no,
471 AudioBlock<const float> (channels, numElementsInArray (channels), (size_t) length));
472 }
473
474 beginTest ("IRs with extra silence are trimmed appropriately");
475 {
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);
479 paddedRamp.clear();
480
481 const auto offset = (paddedRamp.getNumSamples() - ramp.getNumSamples()) / 2;
482
483 for (auto channel = 0; channel != ramp.getNumChannels(); ++channel)
484 paddedRamp.copyFrom (channel, offset, ramp.getReadPointer (channel), length);
485
486 testConvolution (spec,
487 Convolution::Latency { 0 },
488 paddedRamp,
489 spec.sampleRate,
490 Convolution::Stereo::no,
491 Convolution::Trim::yes,
492 Convolution::Normalise::no,
493 ramp);
494 }
495
496 beginTest ("IRs are resampled if their sample rate is different to the playback rate");
497 {
498 for (const auto resampleRatio : { 0.1, 0.5, 2.0, 10.0 })
499 {
500 const auto length = static_cast<int> (spec.maximumBlockSize) * 2;
501 const auto ramp = makeStereoRamp (length);
502
503 const auto resampled = [&]
504 {
505 AudioBuffer<float> original = ramp;
506 MemoryAudioSource memorySource (original, false);
507 ResamplingAudioSource resamplingSource (&memorySource, false, original.getNumChannels());
508
509 const auto finalSize = roundToInt (original.getNumSamples() / resampleRatio);
510 resamplingSource.setResamplingRatio (resampleRatio);
511 resamplingSource.prepareToPlay (finalSize, spec.sampleRate * resampleRatio);
512
513 AudioBuffer<float> result (original.getNumChannels(), finalSize);
514 resamplingSource.getNextAudioBlock ({ &result, 0, result.getNumSamples() });
515
516 result.applyGain ((float) resampleRatio);
517
518 return result;
519 }();
520
521 testConvolution (spec,
522 Convolution::Latency { 0 },
523 ramp,
524 spec.sampleRate * resampleRatio,
525 Convolution::Stereo::yes,
526 Convolution::Trim::yes,
527 Convolution::Normalise::no,
528 resampled);
529 }
530 }
531
532 beginTest ("Non-uniform convolutions work");
533 {
534 const auto ramp = makeRamp (static_cast<int> (spec.maximumBlockSize) * 8);
535
536 for (auto headSize : { spec.maximumBlockSize / 2, spec.maximumBlockSize, spec.maximumBlockSize * 9 })
537 {
538 testConvolution (spec,
539 Convolution::NonUniform { static_cast<int> (headSize) },
540 ramp,
541 spec.sampleRate,
542 Convolution::Stereo::yes,
543 Convolution::Trim::yes,
544 Convolution::Normalise::no,
545 ramp);
546 }
547 }
548
549 beginTest ("Convolutions with latency work");
550 {
551 const auto ramp = makeRamp (static_cast<int> (spec.maximumBlockSize) * 8);
552 using BlockSize = decltype (spec.maximumBlockSize);
553
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) })
559 {
560 testConvolution (spec,
561 Convolution::Latency { static_cast<int> (latency) },
562 ramp,
563 spec.sampleRate,
564 Convolution::Stereo::yes,
565 Convolution::Trim::yes,
566 Convolution::Normalise::no,
567 ramp);
568 }
569 }
570 }
571};
572
573ConvolutionTest convolutionUnitTest;
574
575}
576} // namespace juce::dsp
577
578#undef JUCE_FAIL_ON_ALLOCATION_IN_SCOPE
static uint32 getMillisecondCounter() noexcept
Definition: juce_Time.cpp:241