The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

522 lines
20KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2020 - Raw Material Software Limited
  5. JUCE is an open source library subject to commercial or open-source
  6. licensing.
  7. By using JUCE, you agree to the terms of both the JUCE 6 End-User License
  8. Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
  9. End User License Agreement: www.juce.com/juce-6-licence
  10. Privacy Policy: www.juce.com/juce-privacy-policy
  11. Or: You may also use this code under the terms of the GPL v3 (see
  12. www.gnu.org/licenses).
  13. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  14. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  15. DISCLAIMED.
  16. ==============================================================================
  17. */
  18. #if JUCE_ENABLE_ALLOCATION_HOOKS
  19. #define JUCE_FAIL_ON_ALLOCATION_IN_SCOPE const UnitTestAllocationChecker checker (*this)
  20. #else
  21. #define JUCE_FAIL_ON_ALLOCATION_IN_SCOPE
  22. #endif
  23. namespace juce
  24. {
  25. namespace dsp
  26. {
  27. namespace
  28. {
  29. class ConvolutionTest : public UnitTest
  30. {
  31. template <typename Callback>
  32. static void nTimes (int n, Callback&& callback)
  33. {
  34. for (auto i = 0; i < n; ++i)
  35. callback();
  36. }
  37. static AudioBuffer<float> makeRamp (int length)
  38. {
  39. AudioBuffer<float> result (1, length);
  40. result.clear();
  41. const auto writePtr = result.getWritePointer (0);
  42. std::fill (writePtr, writePtr + length, 1.0f);
  43. result.applyGainRamp (0, length, 1.0f, 0.0f);
  44. return result;
  45. }
  46. static AudioBuffer<float> makeStereoRamp (int length)
  47. {
  48. AudioBuffer<float> result (2, length);
  49. result.clear();
  50. auto** channels = result.getArrayOfWritePointers();
  51. std::for_each (channels, channels + result.getNumChannels(), [length] (auto* channel)
  52. {
  53. std::fill (channel, channel + length, 1.0f);
  54. });
  55. result.applyGainRamp (0, 0, length, 1.0f, 0.0f);
  56. result.applyGainRamp (1, 0, length, 0.0f, 1.0f);
  57. return result;
  58. }
  59. static void addDiracImpulse (const AudioBlock<float>& block)
  60. {
  61. block.clear();
  62. for (size_t channel = 0; channel != block.getNumChannels(); ++channel)
  63. block.setSample ((int) channel, 0, 1.0f);
  64. }
  65. void checkForNans (const AudioBlock<float>& block)
  66. {
  67. for (size_t channel = 0; channel != block.getNumChannels(); ++channel)
  68. for (size_t sample = 0; sample != block.getNumSamples(); ++sample)
  69. expect (! std::isnan (block.getSample ((int) channel, (int) sample)));
  70. }
  71. template <typename T>
  72. void nonAllocatingExpectWithinAbsoluteError (const T& a, const T& b, const T& error)
  73. {
  74. expect (std::abs (a - b) < error);
  75. }
  76. enum class InitSequence { prepareThenLoad, loadThenPrepare };
  77. void checkLatency (const Convolution& convolution, const Convolution::Latency& latency)
  78. {
  79. const auto reportedLatency = convolution.getLatency();
  80. if (latency.latencyInSamples == 0)
  81. expect (reportedLatency == 0);
  82. expect (reportedLatency >= latency.latencyInSamples);
  83. }
  84. void checkLatency (const Convolution&, const Convolution::NonUniform&) {}
  85. template <typename ConvolutionConfig>
  86. void testConvolution (const ProcessSpec& spec,
  87. const ConvolutionConfig& config,
  88. const AudioBuffer<float>& ir,
  89. double irSampleRate,
  90. Convolution::Stereo stereo,
  91. Convolution::Trim trim,
  92. Convolution::Normalise normalise,
  93. const AudioBlock<const float>& expectedResult,
  94. InitSequence initSequence)
  95. {
  96. AudioBuffer<float> buffer (static_cast<int> (spec.numChannels),
  97. static_cast<int> (spec.maximumBlockSize));
  98. AudioBlock<float> block { buffer };
  99. ProcessContextReplacing<float> context { block };
  100. const auto numBlocksPerSecond = (int) std::ceil (spec.sampleRate / spec.maximumBlockSize);
  101. const auto numBlocksForImpulse = (int) std::ceil ((double) expectedResult.getNumSamples() / spec.maximumBlockSize);
  102. AudioBuffer<float> outBuffer (static_cast<int> (spec.numChannels),
  103. numBlocksForImpulse * static_cast<int> (spec.maximumBlockSize));
  104. Convolution convolution (config);
  105. auto copiedIr = ir;
  106. if (initSequence == InitSequence::loadThenPrepare)
  107. convolution.loadImpulseResponse (std::move (copiedIr), irSampleRate, stereo, trim, normalise);
  108. convolution.prepare (spec);
  109. JUCE_FAIL_ON_ALLOCATION_IN_SCOPE;
  110. if (initSequence == InitSequence::prepareThenLoad)
  111. convolution.loadImpulseResponse (std::move (copiedIr), irSampleRate, stereo, trim, normalise);
  112. checkLatency (convolution, config);
  113. auto processBlocksWithDiracImpulse = [&]
  114. {
  115. for (auto i = 0; i != numBlocksForImpulse; ++i)
  116. {
  117. if (i == 0)
  118. addDiracImpulse (block);
  119. else
  120. block.clear();
  121. convolution.process (context);
  122. for (auto c = 0; c != static_cast<int> (spec.numChannels); ++c)
  123. {
  124. outBuffer.copyFrom (c,
  125. i * static_cast<int> (spec.maximumBlockSize),
  126. block.getChannelPointer (static_cast<size_t> (c)),
  127. static_cast<int> (spec.maximumBlockSize));
  128. }
  129. }
  130. };
  131. const auto time = Time::getMillisecondCounter();
  132. // Wait 10 seconds to load the impulse response
  133. while (Time::getMillisecondCounter() - time < 10'000)
  134. {
  135. processBlocksWithDiracImpulse();
  136. // Check if the impulse response was loaded
  137. if (block.getSample (0, 1) != 0.0f)
  138. break;
  139. }
  140. // At this point, our convolution should be loaded and the current IR size should
  141. // match the expected result size
  142. expect (convolution.getCurrentIRSize() == static_cast<int> (expectedResult.getNumSamples()));
  143. // Make sure we get any smoothing out of the way
  144. nTimes (numBlocksPerSecond, processBlocksWithDiracImpulse);
  145. nTimes (5, [&]
  146. {
  147. processBlocksWithDiracImpulse();
  148. const auto actualLatency = static_cast<size_t> (convolution.getLatency());
  149. // The output should be the same as the IR
  150. for (size_t c = 0; c != static_cast<size_t> (expectedResult.getNumChannels()); ++c)
  151. {
  152. for (size_t i = 0; i != static_cast<size_t> (expectedResult.getNumSamples()); ++i)
  153. {
  154. const auto equivalentSample = i + actualLatency;
  155. if (static_cast<int> (equivalentSample) >= outBuffer.getNumSamples())
  156. continue;
  157. nonAllocatingExpectWithinAbsoluteError (outBuffer.getSample ((int) c, (int) equivalentSample),
  158. expectedResult.getSample ((int) c, (int) i),
  159. 0.01f);
  160. }
  161. }
  162. });
  163. }
  164. template <typename ConvolutionConfig>
  165. void testConvolution (const ProcessSpec& spec,
  166. const ConvolutionConfig& config,
  167. const AudioBuffer<float>& ir,
  168. double irSampleRate,
  169. Convolution::Stereo stereo,
  170. Convolution::Trim trim,
  171. Convolution::Normalise normalise,
  172. const AudioBlock<const float>& expectedResult)
  173. {
  174. for (const auto sequence : { InitSequence::prepareThenLoad, InitSequence::loadThenPrepare })
  175. testConvolution (spec, config, ir, irSampleRate, stereo, trim, normalise, expectedResult, sequence);
  176. }
  177. public:
  178. ConvolutionTest()
  179. : UnitTest ("Convolution", UnitTestCategories::dsp)
  180. {}
  181. void runTest() override
  182. {
  183. const ProcessSpec spec { 44100.0, 512, 2 };
  184. AudioBuffer<float> buffer (static_cast<int> (spec.numChannels),
  185. static_cast<int> (spec.maximumBlockSize));
  186. AudioBlock<float> block { buffer };
  187. ProcessContextReplacing<float> context { block };
  188. const auto impulseData = []
  189. {
  190. Random random;
  191. AudioBuffer<float> result (2, 1000);
  192. for (auto channel = 0; channel != result.getNumChannels(); ++channel)
  193. for (auto sample = 0; sample != result.getNumSamples(); ++sample)
  194. result.setSample (channel, sample, random.nextFloat());
  195. return result;
  196. }();
  197. beginTest ("Impulse responses can be loaded without allocating on the audio thread");
  198. {
  199. Convolution convolution;
  200. convolution.prepare (spec);
  201. auto copy = impulseData;
  202. JUCE_FAIL_ON_ALLOCATION_IN_SCOPE;
  203. nTimes (100, [&]
  204. {
  205. convolution.loadImpulseResponse (std::move (copy),
  206. 1000,
  207. Convolution::Stereo::yes,
  208. Convolution::Trim::yes,
  209. Convolution::Normalise::no);
  210. addDiracImpulse (block);
  211. convolution.process (context);
  212. checkForNans (block);
  213. });
  214. }
  215. beginTest ("Convolution can be reset without allocating on the audio thread");
  216. {
  217. Convolution convolution;
  218. convolution.prepare (spec);
  219. auto copy = impulseData;
  220. convolution.loadImpulseResponse (std::move (copy),
  221. 1000,
  222. Convolution::Stereo::yes,
  223. Convolution::Trim::yes,
  224. Convolution::Normalise::yes);
  225. JUCE_FAIL_ON_ALLOCATION_IN_SCOPE;
  226. nTimes (100, [&]
  227. {
  228. addDiracImpulse (block);
  229. convolution.reset();
  230. convolution.process (context);
  231. convolution.reset();
  232. });
  233. checkForNans (block);
  234. }
  235. beginTest ("Completely empty IRs don't crash");
  236. {
  237. AudioBuffer<float> emptyBuffer;
  238. Convolution convolution;
  239. convolution.prepare (spec);
  240. auto copy = impulseData;
  241. convolution.loadImpulseResponse (std::move (copy),
  242. 2000,
  243. Convolution::Stereo::yes,
  244. Convolution::Trim::yes,
  245. Convolution::Normalise::yes);
  246. JUCE_FAIL_ON_ALLOCATION_IN_SCOPE;
  247. nTimes (100, [&]
  248. {
  249. addDiracImpulse (block);
  250. convolution.reset();
  251. convolution.process (context);
  252. convolution.reset();
  253. });
  254. checkForNans (block);
  255. }
  256. beginTest ("Short uniform convolutions work");
  257. {
  258. const auto ramp = makeRamp (static_cast<int> (spec.maximumBlockSize) / 2);
  259. testConvolution (spec,
  260. Convolution::Latency { 0 },
  261. ramp,
  262. spec.sampleRate,
  263. Convolution::Stereo::yes,
  264. Convolution::Trim::yes,
  265. Convolution::Normalise::no,
  266. ramp);
  267. }
  268. beginTest ("Longer uniform convolutions work");
  269. {
  270. const auto ramp = makeRamp (static_cast<int> (spec.maximumBlockSize) * 8);
  271. testConvolution (spec,
  272. Convolution::Latency { 0 },
  273. ramp,
  274. spec.sampleRate,
  275. Convolution::Stereo::yes,
  276. Convolution::Trim::yes,
  277. Convolution::Normalise::no,
  278. ramp);
  279. }
  280. beginTest ("Normalisation works");
  281. {
  282. const auto ramp = makeRamp (static_cast<int> (spec.maximumBlockSize) * 8);
  283. auto copy = ramp;
  284. const auto channels = copy.getArrayOfWritePointers();
  285. const auto numChannels = copy.getNumChannels();
  286. const auto numSamples = copy.getNumSamples();
  287. const auto factor = 0.125f / std::sqrt (std::accumulate (channels, channels + numChannels, 0.0f,
  288. [numSamples] (auto max, auto* channel)
  289. {
  290. return juce::jmax (max, std::accumulate (channel, channel + numSamples, 0.0f,
  291. [] (auto sum, auto sample)
  292. {
  293. return sum + sample * sample;
  294. }));
  295. }));
  296. std::for_each (channels, channels + numChannels, [factor, numSamples] (auto* channel)
  297. {
  298. FloatVectorOperations::multiply (channel, factor, numSamples);
  299. });
  300. testConvolution (spec,
  301. Convolution::Latency { 0 },
  302. ramp,
  303. spec.sampleRate,
  304. Convolution::Stereo::yes,
  305. Convolution::Trim::yes,
  306. Convolution::Normalise::yes,
  307. copy);
  308. }
  309. beginTest ("Stereo convolutions work");
  310. {
  311. const auto ramp = makeStereoRamp (static_cast<int> (spec.maximumBlockSize) * 5);
  312. testConvolution (spec,
  313. Convolution::Latency { 0 },
  314. ramp,
  315. spec.sampleRate,
  316. Convolution::Stereo::yes,
  317. Convolution::Trim::yes,
  318. Convolution::Normalise::no,
  319. ramp);
  320. }
  321. beginTest ("Stereo IRs only use first channel if stereo is disabled");
  322. {
  323. const auto length = static_cast<int> (spec.maximumBlockSize) * 5;
  324. const auto ramp = makeStereoRamp (length);
  325. const float* channels[] { ramp.getReadPointer (0), ramp.getReadPointer (0) };
  326. testConvolution (spec,
  327. Convolution::Latency { 0 },
  328. ramp,
  329. spec.sampleRate,
  330. Convolution::Stereo::no,
  331. Convolution::Trim::yes,
  332. Convolution::Normalise::no,
  333. AudioBlock<const float> (channels, numElementsInArray (channels), length));
  334. }
  335. beginTest ("IRs with extra silence are trimmed appropriately");
  336. {
  337. const auto length = static_cast<int> (spec.maximumBlockSize) * 3;
  338. const auto ramp = makeRamp (length);
  339. AudioBuffer<float> paddedRamp (ramp.getNumChannels(), ramp.getNumSamples() * 2);
  340. paddedRamp.clear();
  341. const auto offset = (paddedRamp.getNumSamples() - ramp.getNumSamples()) / 2;
  342. for (auto channel = 0; channel != ramp.getNumChannels(); ++channel)
  343. paddedRamp.copyFrom (channel, offset, ramp.getReadPointer (channel), length);
  344. testConvolution (spec,
  345. Convolution::Latency { 0 },
  346. paddedRamp,
  347. spec.sampleRate,
  348. Convolution::Stereo::no,
  349. Convolution::Trim::yes,
  350. Convolution::Normalise::no,
  351. ramp);
  352. }
  353. beginTest ("IRs are resampled if their sample rate is different to the playback rate");
  354. {
  355. for (const auto resampleRatio : { 0.1, 0.5, 2.0, 10.0 })
  356. {
  357. const auto length = static_cast<int> (spec.maximumBlockSize) * 2;
  358. const auto ramp = makeStereoRamp (length);
  359. const auto resampled = [&]
  360. {
  361. AudioBuffer<float> original = ramp;
  362. MemoryAudioSource memorySource (original, false);
  363. ResamplingAudioSource resamplingSource (&memorySource, false, original.getNumChannels());
  364. const auto finalSize = roundToInt (original.getNumSamples() / resampleRatio);
  365. resamplingSource.setResamplingRatio (resampleRatio);
  366. resamplingSource.prepareToPlay (finalSize, spec.sampleRate * resampleRatio);
  367. AudioBuffer<float> result (original.getNumChannels(), finalSize);
  368. resamplingSource.getNextAudioBlock ({ &result, 0, result.getNumSamples() });
  369. return result;
  370. }();
  371. testConvolution (spec,
  372. Convolution::Latency { 0 },
  373. ramp,
  374. spec.sampleRate * resampleRatio,
  375. Convolution::Stereo::yes,
  376. Convolution::Trim::yes,
  377. Convolution::Normalise::no,
  378. resampled);
  379. }
  380. }
  381. beginTest ("Non-uniform convolutions work");
  382. {
  383. const auto ramp = makeRamp (static_cast<int> (spec.maximumBlockSize) * 8);
  384. for (auto headSize : { spec.maximumBlockSize / 2, spec.maximumBlockSize, spec.maximumBlockSize * 9 })
  385. {
  386. testConvolution (spec,
  387. Convolution::NonUniform { static_cast<int> (headSize) },
  388. ramp,
  389. spec.sampleRate,
  390. Convolution::Stereo::yes,
  391. Convolution::Trim::yes,
  392. Convolution::Normalise::no,
  393. ramp);
  394. }
  395. }
  396. beginTest ("Convolutions with latency work");
  397. {
  398. const auto ramp = makeRamp (static_cast<int> (spec.maximumBlockSize) * 8);
  399. using BlockSize = decltype (spec.maximumBlockSize);
  400. for (auto latency : { /*static_cast<BlockSize> (0),
  401. spec.maximumBlockSize / 3,
  402. spec.maximumBlockSize,
  403. spec.maximumBlockSize * 2, */
  404. static_cast<BlockSize> (spec.maximumBlockSize * 2.5) })
  405. {
  406. testConvolution (spec,
  407. Convolution::Latency { static_cast<int> (latency) },
  408. ramp,
  409. spec.sampleRate,
  410. Convolution::Stereo::yes,
  411. Convolution::Trim::yes,
  412. Convolution::Normalise::no,
  413. ramp);
  414. }
  415. }
  416. }
  417. };
  418. ConvolutionTest convolutionUnitTest;
  419. }
  420. }
  421. }
  422. #undef JUCE_FAIL_ON_ALLOCATION_IN_SCOPE