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.

660 lines
20KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE examples.
  4. Copyright (c) 2022 - Raw Material Software Limited
  5. The code included in this file is provided under the terms of the ISC license
  6. http://www.isc.org/downloads/software-support-policy/isc-license. Permission
  7. To use, copy, modify, and/or distribute this software for any purpose with or
  8. without fee is hereby granted provided that the above copyright notice and
  9. this permission notice appear in all copies.
  10. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
  11. WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
  12. PURPOSE, ARE DISCLAIMED.
  13. ==============================================================================
  14. */
  15. /*******************************************************************************
  16. The block below describes the properties of this PIP. A PIP is a short snippet
  17. of code that can be read by the Projucer and used to generate a JUCE project.
  18. BEGIN_JUCE_PIP_METADATA
  19. name: AudioWorkgroupDemo
  20. version: 1.0.0
  21. vendor: JUCE
  22. website: http://juce.com
  23. description: Simple audio workgroup demo application.
  24. dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats,
  25. juce_audio_processors, juce_audio_utils, juce_core,
  26. juce_data_structures, juce_events, juce_graphics,
  27. juce_gui_basics, juce_gui_extra
  28. exporters: xcode_mac, xcode_iphone
  29. moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
  30. type: Component
  31. mainClass: AudioWorkgroupDemo
  32. useLocalCopy: 1
  33. END_JUCE_PIP_METADATA
  34. *******************************************************************************/
  35. #pragma once
  36. #include "../Assets/DemoUtilities.h"
  37. #include "../Assets/AudioLiveScrollingDisplay.h"
  38. #include "../Assets/ADSRComponent.h"
  39. constexpr auto NumWorkerThreads = 4;
  40. //==============================================================================
  41. class ThreadBarrier : public ReferenceCountedObject
  42. {
  43. public:
  44. using Ptr = ReferenceCountedObjectPtr<ThreadBarrier>;
  45. static Ptr make (int numThreadsToSynchronise)
  46. {
  47. return { new ThreadBarrier { numThreadsToSynchronise } };
  48. }
  49. void arriveAndWait()
  50. {
  51. std::unique_lock lk { mutex };
  52. [[maybe_unused]] const auto c = ++blockCount;
  53. // You've tried to synchronise too many threads!!
  54. jassert (c <= threadCount);
  55. if (blockCount == threadCount)
  56. {
  57. blockCount = 0;
  58. cv.notify_all();
  59. return;
  60. }
  61. cv.wait (lk, [this] { return blockCount == 0; });
  62. }
  63. private:
  64. std::mutex mutex;
  65. std::condition_variable cv;
  66. int blockCount{};
  67. const int threadCount{};
  68. explicit ThreadBarrier (int numThreadsToSynchronise)
  69. : threadCount (numThreadsToSynchronise) {}
  70. JUCE_DECLARE_NON_COPYABLE (ThreadBarrier)
  71. JUCE_DECLARE_NON_MOVEABLE (ThreadBarrier)
  72. };
  73. struct Voice
  74. {
  75. struct Oscillator
  76. {
  77. float getNextSample()
  78. {
  79. const auto s = (2.f * phase - 1.f);
  80. phase += delta;
  81. if (phase >= 1.f)
  82. phase -= 1.f;
  83. return s;
  84. }
  85. float delta = 0;
  86. float phase = 0;
  87. };
  88. Voice (int numSamples, double newSampleRate)
  89. : sampleRate (newSampleRate),
  90. workBuffer (2, numSamples)
  91. {
  92. }
  93. bool isActive() const { return adsr.isActive(); }
  94. void startNote (int midiNoteNumber, float detuneAmount, ADSR::Parameters env)
  95. {
  96. constexpr float superSawDetuneValues[] = { -1.f, -0.8f, -0.6f, 0.f, 0.5f, 0.7f, 1.f };
  97. const auto freq = 440.f * std::pow (2.f, ((float) midiNoteNumber - 69.f) / 12.f);
  98. for (size_t i = 0; i < 7; i++)
  99. {
  100. auto& osc = oscillators[i];
  101. const auto detune = superSawDetuneValues[i] * detuneAmount;
  102. osc.delta = (freq + detune) / (float) sampleRate;
  103. osc.phase = wobbleGenerator.nextFloat();
  104. }
  105. currentNote = midiNoteNumber;
  106. adsr.setParameters (env);
  107. adsr.setSampleRate (sampleRate);
  108. adsr.noteOn();
  109. }
  110. void stopNote()
  111. {
  112. adsr.noteOff();
  113. }
  114. void run()
  115. {
  116. workBuffer.clear();
  117. constexpr auto oscillatorCount = 7;
  118. constexpr float superSawPanValues[] = { -1.f, -0.7f, -0.3f, 0.f, 0.3f, 0.7f, 1.f };
  119. constexpr auto spread = 0.8f;
  120. constexpr auto mix = 1 / 7.f;
  121. auto* l = workBuffer.getWritePointer (0);
  122. auto* r = workBuffer.getWritePointer (1);
  123. for (int i = 0; i < workBuffer.getNumSamples(); i++)
  124. {
  125. const auto a = adsr.getNextSample();
  126. float left = 0;
  127. float right = 0;
  128. for (size_t o = 0; o < oscillatorCount; o++)
  129. {
  130. auto& osc = oscillators[o];
  131. const auto s = a * osc.getNextSample();
  132. left += s * (1.f - (superSawPanValues[o] * spread));
  133. right += s * (1.f + (superSawPanValues[o] * spread));
  134. }
  135. l[i] += left * mix;
  136. r[i] += right * mix;
  137. }
  138. workBuffer.applyGain (0.25f);
  139. }
  140. const AudioSampleBuffer& getWorkBuffer() const { return workBuffer; }
  141. ADSR adsr;
  142. double sampleRate;
  143. std::array<Oscillator, 7> oscillators;
  144. int currentNote = 0;
  145. Random wobbleGenerator;
  146. private:
  147. AudioSampleBuffer workBuffer;
  148. JUCE_DECLARE_NON_COPYABLE (Voice)
  149. JUCE_DECLARE_NON_MOVEABLE (Voice)
  150. };
  151. struct AudioWorkerThreadOptions
  152. {
  153. int numChannels;
  154. int numSamples;
  155. double sampleRate;
  156. AudioWorkgroup workgroup;
  157. ThreadBarrier::Ptr completionBarrier;
  158. };
  159. class AudioWorkerThread final : private Thread
  160. {
  161. public:
  162. using Ptr = std::unique_ptr<AudioWorkerThread>;
  163. using Options = AudioWorkerThreadOptions;
  164. explicit AudioWorkerThread (const Options& workerOptions)
  165. : Thread ("AudioWorkerThread"),
  166. options (workerOptions)
  167. {
  168. jassert (options.completionBarrier != nullptr);
  169. #if defined (JUCE_MAC)
  170. jassert (options.workgroup);
  171. #endif
  172. startRealtimeThread (RealtimeOptions{}.withApproximateAudioProcessingTime (options.numSamples, options.sampleRate));
  173. }
  174. ~AudioWorkerThread() override { stop(); }
  175. using Thread::notify;
  176. using Thread::signalThreadShouldExit;
  177. using Thread::isThreadRunning;
  178. int getJobCount() const { return lastJobCount; }
  179. int queueAudioJobs (Span<Voice*> jobs)
  180. {
  181. size_t spanIndex = 0;
  182. const auto write = jobQueueFifo.write ((int) jobs.size());
  183. write.forEach ([&, jobs] (int dstIndex)
  184. {
  185. jobQueue[(size_t) dstIndex] = jobs[spanIndex++];
  186. });
  187. return write.blockSize1 + write.blockSize2;
  188. }
  189. private:
  190. void stop()
  191. {
  192. signalThreadShouldExit();
  193. stopThread (-1);
  194. }
  195. void run() override
  196. {
  197. WorkgroupToken token;
  198. options.workgroup.join (token);
  199. while (wait (-1) && ! threadShouldExit())
  200. {
  201. const auto numReady = jobQueueFifo.getNumReady();
  202. lastJobCount = numReady;
  203. if (numReady > 0)
  204. {
  205. jobQueueFifo.read (jobQueueFifo.getNumReady())
  206. .forEach ([this] (int srcIndex)
  207. {
  208. jobQueue[(size_t) srcIndex]->run();
  209. });
  210. }
  211. // Wait for all our threads to get to this point.
  212. options.completionBarrier->arriveAndWait();
  213. }
  214. }
  215. static constexpr auto numJobs = 128;
  216. Options options;
  217. std::array<Voice*, numJobs> jobQueue;
  218. AbstractFifo jobQueueFifo { numJobs };
  219. std::atomic<int> lastJobCount = 0;
  220. private:
  221. JUCE_DECLARE_NON_COPYABLE (AudioWorkerThread)
  222. JUCE_DECLARE_NON_MOVEABLE (AudioWorkerThread)
  223. };
  224. template <typename ValueType, typename LockType>
  225. struct SharedThreadValue
  226. {
  227. SharedThreadValue (LockType& lockRef, ValueType initialValue = {})
  228. : lock (lockRef),
  229. preSyncValue (initialValue),
  230. postSyncValue (initialValue)
  231. {
  232. }
  233. void set (const ValueType& newValue)
  234. {
  235. const typename LockType::ScopedLockType sl { lock };
  236. preSyncValue = newValue;
  237. }
  238. ValueType get() const
  239. {
  240. {
  241. const typename LockType::ScopedTryLockType sl { lock, true };
  242. if (sl.isLocked())
  243. postSyncValue = preSyncValue;
  244. }
  245. return postSyncValue;
  246. }
  247. private:
  248. LockType& lock;
  249. ValueType preSyncValue{};
  250. mutable ValueType postSyncValue{};
  251. JUCE_DECLARE_NON_COPYABLE (SharedThreadValue)
  252. JUCE_DECLARE_NON_MOVEABLE (SharedThreadValue)
  253. };
  254. //==============================================================================
  255. class SuperSynth
  256. {
  257. public:
  258. SuperSynth() = default;
  259. void setEnvelope (ADSR::Parameters params)
  260. {
  261. envelope.set (params);
  262. }
  263. void setThickness (float newThickness)
  264. {
  265. thickness.set (newThickness);
  266. }
  267. void prepareToPlay (int numSamples, double sampleRate)
  268. {
  269. activeVoices.reserve (128);
  270. for (auto& voice : voices)
  271. voice.reset (new Voice { numSamples, sampleRate });
  272. }
  273. void process (ThreadBarrier::Ptr barrier, Span<AudioWorkerThread*> workers,
  274. AudioSampleBuffer& buffer, MidiBuffer& midiBuffer)
  275. {
  276. const auto blockThickness = thickness.get();
  277. const auto blockEnvelope = envelope.get();
  278. // We're not trying to be sample accurate.. handle the on/off events in a single block.
  279. for (auto event : midiBuffer)
  280. {
  281. const auto message = event.getMessage();
  282. if (message.isNoteOn())
  283. {
  284. for (auto& voice : voices)
  285. {
  286. if (! voice->isActive())
  287. {
  288. voice->startNote (message.getNoteNumber(), blockThickness, blockEnvelope);
  289. break;
  290. }
  291. }
  292. continue;
  293. }
  294. if (message.isNoteOff())
  295. {
  296. for (auto& voice : voices)
  297. {
  298. if (voice->currentNote == message.getNoteNumber())
  299. voice->stopNote();
  300. }
  301. continue;
  302. }
  303. }
  304. // Queue up all active voices
  305. for (auto& voice : voices)
  306. if (voice->isActive())
  307. activeVoices.push_back (voice.get());
  308. constexpr auto jobsPerThread = 1;
  309. // Try and split the voices evenly just for demonstration purposes.
  310. // You could also do some of the work on this thread instead of waiting.
  311. for (int i = 0; i < (int) activeVoices.size();)
  312. {
  313. for (auto worker : workers)
  314. {
  315. if (i >= (int) activeVoices.size())
  316. break;
  317. const auto jobCount = jmin (jobsPerThread, (int) activeVoices.size() - i);
  318. i += worker->queueAudioJobs ({ activeVoices.data() + i, (size_t) jobCount });
  319. }
  320. }
  321. // kick off the work.
  322. for (auto& worker : workers)
  323. worker->notify();
  324. // Wait for our jobs to complete.
  325. barrier->arriveAndWait();
  326. // mix the jobs into the main audio thread buffer.
  327. for (auto* voice : activeVoices)
  328. {
  329. buffer.addFrom (0, 0, voice->getWorkBuffer(), 0, 0, buffer.getNumSamples());
  330. buffer.addFrom (1, 0, voice->getWorkBuffer(), 1, 0, buffer.getNumSamples());
  331. }
  332. // Abuse std::vector not reallocating on clear.
  333. activeVoices.clear();
  334. }
  335. private:
  336. std::array<std::unique_ptr<Voice>, 128> voices;
  337. std::vector<Voice*> activeVoices;
  338. template <typename T>
  339. using ThreadValue = SharedThreadValue<T, SpinLock>;
  340. SpinLock paramLock;
  341. ThreadValue<ADSR::Parameters> envelope { paramLock, { 0.f, 0.3f, 1.f, 0.3f } };
  342. ThreadValue<float> thickness { paramLock, 1.f };
  343. JUCE_DECLARE_NON_COPYABLE (SuperSynth)
  344. JUCE_DECLARE_NON_MOVEABLE (SuperSynth)
  345. };
  346. //==============================================================================
  347. class AudioWorkgroupDemo : public Component,
  348. private Timer,
  349. private AudioSource,
  350. private MidiInputCallback
  351. {
  352. public:
  353. AudioWorkgroupDemo()
  354. {
  355. addAndMakeVisible (keyboardComponent);
  356. addAndMakeVisible (liveAudioDisplayComp);
  357. addAndMakeVisible (envelopeComponent);
  358. addAndMakeVisible (keyboardComponent);
  359. addAndMakeVisible (thicknessSlider);
  360. addAndMakeVisible (voiceCountLabel);
  361. std::generate (threadLabels.begin(), threadLabels.end(), &std::make_unique<Label>);
  362. for (auto& label : threadLabels)
  363. {
  364. addAndMakeVisible (*label);
  365. label->setEditable (false);
  366. }
  367. thicknessSlider.textFromValueFunction = [] (double) { return "Phatness"; };
  368. thicknessSlider.onValueChange = [this] { synthesizer.setThickness ((float) thicknessSlider.getValue()); };
  369. thicknessSlider.setRange (0.5, 15, 0.1);
  370. thicknessSlider.setValue (7, dontSendNotification);
  371. thicknessSlider.setTextBoxIsEditable (false);
  372. envelopeComponent.onChange = [this] { synthesizer.setEnvelope (envelopeComponent.getParameters()); };
  373. voiceCountLabel.setEditable (false);
  374. audioSourcePlayer.setSource (this);
  375. #ifndef JUCE_DEMO_RUNNER
  376. audioDeviceManager.initialise (0, 2, nullptr, true, {}, nullptr);
  377. #endif
  378. audioDeviceManager.addAudioCallback (&audioSourcePlayer);
  379. audioDeviceManager.addMidiInputDeviceCallback ({}, this);
  380. setOpaque (true);
  381. setSize (640, 480);
  382. startTimerHz (10);
  383. }
  384. ~AudioWorkgroupDemo() override
  385. {
  386. audioSourcePlayer.setSource (nullptr);
  387. audioDeviceManager.removeMidiInputDeviceCallback ({}, this);
  388. audioDeviceManager.removeAudioCallback (&audioSourcePlayer);
  389. }
  390. //==============================================================================
  391. void paint (Graphics& g) override
  392. {
  393. g.fillAll (getUIColourIfAvailable (LookAndFeel_V4::ColourScheme::UIColour::windowBackground));
  394. }
  395. void resized() override
  396. {
  397. auto bounds = getLocalBounds();
  398. liveAudioDisplayComp.setBounds (bounds.removeFromTop (60));
  399. keyboardComponent.setBounds (bounds.removeFromBottom (150));
  400. envelopeComponent.setBounds (bounds.removeFromBottom (150));
  401. thicknessSlider.setBounds (bounds.removeFromTop (30));
  402. voiceCountLabel.setBounds (bounds.removeFromTop (30));
  403. const auto maxLabelWidth = bounds.getWidth() / 4;
  404. auto currentBounds = bounds.removeFromLeft (maxLabelWidth);
  405. for (auto& l : threadLabels)
  406. {
  407. if (currentBounds.getHeight() < 30)
  408. currentBounds = bounds.removeFromLeft (maxLabelWidth);
  409. l->setBounds (currentBounds.removeFromTop (30));
  410. }
  411. }
  412. void timerCallback() override
  413. {
  414. String text;
  415. int totalVoices = 0;
  416. {
  417. const SpinLock::ScopedLockType sl { threadArrayUiLock };
  418. for (size_t i = 0; i < NumWorkerThreads; i++)
  419. {
  420. const auto& thread = workerThreads[i];
  421. auto& label = threadLabels[i];
  422. if (thread != nullptr)
  423. {
  424. const auto count = thread->getJobCount();
  425. text = "Thread ";
  426. text << (int) i << ": " << count << " jobs";
  427. label->setText (text, dontSendNotification);
  428. totalVoices += count;
  429. }
  430. }
  431. }
  432. text = {};
  433. text << "Voices: " << totalVoices << " (" << totalVoices * 7 << " oscs)";
  434. voiceCountLabel.setText (text, dontSendNotification);
  435. }
  436. //==============================================================================
  437. void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override
  438. {
  439. completionBarrier = ThreadBarrier::make ((int) NumWorkerThreads + 1);
  440. const auto numChannels = 2;
  441. const auto workerOptions = AudioWorkerThreadOptions
  442. {
  443. numChannels,
  444. samplesPerBlockExpected,
  445. sampleRate,
  446. audioDeviceManager.getDeviceAudioWorkgroup(),
  447. completionBarrier,
  448. };
  449. {
  450. const SpinLock::ScopedLockType sl { threadArrayUiLock };
  451. for (auto& worker : workerThreads)
  452. worker.reset (new AudioWorkerThread { workerOptions });
  453. }
  454. synthesizer.prepareToPlay (samplesPerBlockExpected, sampleRate);
  455. liveAudioDisplayComp.audioDeviceAboutToStart (audioDeviceManager.getCurrentAudioDevice());
  456. waveformBuffer.setSize (1, samplesPerBlockExpected);
  457. }
  458. void releaseResources() override
  459. {
  460. {
  461. const SpinLock::ScopedLockType sl { threadArrayUiLock };
  462. for (auto& thread : workerThreads)
  463. thread.reset();
  464. }
  465. liveAudioDisplayComp.audioDeviceStopped();
  466. }
  467. void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override
  468. {
  469. midiBuffer.clear();
  470. bufferToFill.clearActiveBufferRegion();
  471. keyboardState.processNextMidiBuffer (midiBuffer, bufferToFill.startSample, bufferToFill.numSamples, true);
  472. AudioWorkerThread* workers[NumWorkerThreads]{};
  473. std::transform (workerThreads.begin(), workerThreads.end(), workers,
  474. [] (auto& worker) { return worker.get(); });
  475. synthesizer.process (completionBarrier, Span { workers }, *bufferToFill.buffer, midiBuffer);
  476. // LiveAudioScrollingDisplay applies a 10x gain to the input signal, we need to reduce the gain on our signal.
  477. waveformBuffer.copyFrom (0, 0,
  478. bufferToFill.buffer->getReadPointer (0),
  479. bufferToFill.numSamples,
  480. 1 / 10.f);
  481. liveAudioDisplayComp.audioDeviceIOCallbackWithContext (waveformBuffer.getArrayOfReadPointers(), 1,
  482. nullptr, 0, bufferToFill.numSamples, {});
  483. }
  484. void handleIncomingMidiMessage (MidiInput*, const MidiMessage& message) override
  485. {
  486. if (message.isNoteOn())
  487. keyboardState.noteOn (message.getChannel(), message.getNoteNumber(), 1);
  488. else if (message.isNoteOff())
  489. keyboardState.noteOff (message.getChannel(), message.getNoteNumber(), 1);
  490. }
  491. private:
  492. // if this PIP is running inside the demo runner, we'll use the shared device manager instead
  493. #ifndef JUCE_DEMO_RUNNER
  494. AudioDeviceManager audioDeviceManager;
  495. #else
  496. AudioDeviceManager& audioDeviceManager { getSharedAudioDeviceManager (0, 2) };
  497. #endif
  498. MidiBuffer midiBuffer;
  499. MidiKeyboardState keyboardState;
  500. AudioSourcePlayer audioSourcePlayer;
  501. SuperSynth synthesizer;
  502. AudioSampleBuffer waveformBuffer;
  503. MidiKeyboardComponent keyboardComponent { keyboardState, MidiKeyboardComponent::horizontalKeyboard };
  504. LiveScrollingAudioDisplay liveAudioDisplayComp;
  505. ADSRComponent envelopeComponent;
  506. Slider thicknessSlider { Slider::SliderStyle::LinearHorizontal, Slider::TextBoxLeft };
  507. Label voiceCountLabel;
  508. SpinLock threadArrayUiLock;
  509. ThreadBarrier::Ptr completionBarrier;
  510. std::array<std::unique_ptr<Label>, NumWorkerThreads> threadLabels;
  511. std::array<AudioWorkerThread::Ptr, NumWorkerThreads> workerThreads;
  512. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioWorkgroupDemo)
  513. };