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.

575 lines
22KB

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