OpenShot Audio Library | OpenShotAudio  0.6.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 
32 namespace juce::dsp
33 {
34 namespace
35 {
36 
37 class 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 
244 public:
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 
573 ConvolutionTest 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