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.

579 lines
22KB

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