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.

676 lines
22KB

  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. #pragma once
  16. using namespace dsp;
  17. //==============================================================================
  18. struct DSPDemoParameterBase : public ChangeBroadcaster
  19. {
  20. DSPDemoParameterBase (const String& labelName) : name (labelName) {}
  21. virtual ~DSPDemoParameterBase() = default;
  22. virtual Component* getComponent() = 0;
  23. virtual int getPreferredHeight() = 0;
  24. virtual int getPreferredWidth() = 0;
  25. String name;
  26. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DSPDemoParameterBase)
  27. };
  28. //==============================================================================
  29. struct SliderParameter : public DSPDemoParameterBase
  30. {
  31. SliderParameter (Range<double> range, double skew, double initialValue,
  32. const String& labelName, const String& suffix = {})
  33. : DSPDemoParameterBase (labelName)
  34. {
  35. slider.setRange (range.getStart(), range.getEnd(), 0.01);
  36. slider.setSkewFactor (skew);
  37. slider.setValue (initialValue);
  38. if (suffix.isNotEmpty())
  39. slider.setTextValueSuffix (suffix);
  40. slider.onValueChange = [this] { sendChangeMessage(); };
  41. }
  42. Component* getComponent() override { return &slider; }
  43. int getPreferredHeight() override { return 40; }
  44. int getPreferredWidth() override { return 500; }
  45. double getCurrentValue() const { return slider.getValue(); }
  46. private:
  47. Slider slider;
  48. };
  49. //==============================================================================
  50. struct ChoiceParameter : public DSPDemoParameterBase
  51. {
  52. ChoiceParameter (const StringArray& options, int initialId, const String& labelName)
  53. : DSPDemoParameterBase (labelName)
  54. {
  55. parameterBox.addItemList (options, 1);
  56. parameterBox.onChange = [this] { sendChangeMessage(); };
  57. parameterBox.setSelectedId (initialId);
  58. }
  59. Component* getComponent() override { return &parameterBox; }
  60. int getPreferredHeight() override { return 25; }
  61. int getPreferredWidth() override { return 250; }
  62. int getCurrentSelectedID() const { return parameterBox.getSelectedId(); }
  63. private:
  64. ComboBox parameterBox;
  65. };
  66. //==============================================================================
  67. class AudioThumbnailComponent : public Component,
  68. public FileDragAndDropTarget,
  69. public ChangeBroadcaster,
  70. private ChangeListener,
  71. private Timer
  72. {
  73. public:
  74. AudioThumbnailComponent (AudioDeviceManager& adm, AudioFormatManager& afm)
  75. : audioDeviceManager (adm),
  76. thumbnailCache (5),
  77. thumbnail (128, afm, thumbnailCache)
  78. {
  79. thumbnail.addChangeListener (this);
  80. }
  81. ~AudioThumbnailComponent() override
  82. {
  83. thumbnail.removeChangeListener (this);
  84. }
  85. void paint (Graphics& g) override
  86. {
  87. g.fillAll (Colour (0xff495358));
  88. g.setColour (Colours::white);
  89. if (thumbnail.getTotalLength() > 0.0)
  90. {
  91. thumbnail.drawChannels (g, getLocalBounds().reduced (2),
  92. 0.0, thumbnail.getTotalLength(), 1.0f);
  93. g.setColour (Colours::black);
  94. g.fillRect (static_cast<float> (currentPosition * getWidth()), 0.0f,
  95. 1.0f, static_cast<float> (getHeight()));
  96. }
  97. else
  98. {
  99. g.drawFittedText ("No audio file loaded.\nDrop a file here or click the \"Load File...\" button.", getLocalBounds(),
  100. Justification::centred, 2);
  101. }
  102. }
  103. bool isInterestedInFileDrag (const StringArray&) override { return true; }
  104. void filesDropped (const StringArray& files, int, int) override { loadURL (URL (File (files[0])), true); }
  105. void setCurrentURL (const URL& u)
  106. {
  107. if (currentURL == u)
  108. return;
  109. loadURL (u);
  110. }
  111. URL getCurrentURL() const { return currentURL; }
  112. void setTransportSource (AudioTransportSource* newSource)
  113. {
  114. transportSource = newSource;
  115. struct ResetCallback : public CallbackMessage
  116. {
  117. ResetCallback (AudioThumbnailComponent& o) : owner (o) {}
  118. void messageCallback() override { owner.reset(); }
  119. AudioThumbnailComponent& owner;
  120. };
  121. (new ResetCallback (*this))->post();
  122. }
  123. private:
  124. AudioDeviceManager& audioDeviceManager;
  125. AudioThumbnailCache thumbnailCache;
  126. AudioThumbnail thumbnail;
  127. AudioTransportSource* transportSource = nullptr;
  128. URL currentURL;
  129. double currentPosition = 0.0;
  130. //==============================================================================
  131. void changeListenerCallback (ChangeBroadcaster*) override { repaint(); }
  132. void reset()
  133. {
  134. currentPosition = 0.0;
  135. repaint();
  136. if (transportSource == nullptr)
  137. stopTimer();
  138. else
  139. startTimerHz (25);
  140. }
  141. void loadURL (const URL& u, bool notify = false)
  142. {
  143. if (currentURL == u)
  144. return;
  145. currentURL = u;
  146. thumbnail.setSource (makeInputSource (u).release());
  147. if (notify)
  148. sendChangeMessage();
  149. }
  150. void timerCallback() override
  151. {
  152. if (transportSource != nullptr)
  153. {
  154. currentPosition = transportSource->getCurrentPosition() / thumbnail.getTotalLength();
  155. repaint();
  156. }
  157. }
  158. void mouseDrag (const MouseEvent& e) override
  159. {
  160. if (transportSource != nullptr)
  161. {
  162. const ScopedLock sl (audioDeviceManager.getAudioCallbackLock());
  163. transportSource->setPosition ((jmax (static_cast<double> (e.x), 0.0) / getWidth())
  164. * thumbnail.getTotalLength());
  165. }
  166. }
  167. };
  168. //==============================================================================
  169. class DemoParametersComponent : public Component
  170. {
  171. public:
  172. DemoParametersComponent (const std::vector<DSPDemoParameterBase*>& demoParams)
  173. {
  174. parameters = demoParams;
  175. for (auto demoParameter : parameters)
  176. {
  177. addAndMakeVisible (demoParameter->getComponent());
  178. auto* paramLabel = new Label ({}, demoParameter->name);
  179. paramLabel->attachToComponent (demoParameter->getComponent(), true);
  180. paramLabel->setJustificationType (Justification::centredLeft);
  181. addAndMakeVisible (paramLabel);
  182. labels.add (paramLabel);
  183. }
  184. }
  185. void resized() override
  186. {
  187. auto bounds = getLocalBounds();
  188. bounds.removeFromLeft (100);
  189. for (auto* p : parameters)
  190. {
  191. auto* comp = p->getComponent();
  192. comp->setSize (jmin (bounds.getWidth(), p->getPreferredWidth()), p->getPreferredHeight());
  193. auto compBounds = bounds.removeFromTop (p->getPreferredHeight());
  194. comp->setCentrePosition (compBounds.getCentre());
  195. }
  196. }
  197. int getHeightNeeded()
  198. {
  199. auto height = 0;
  200. for (auto* p : parameters)
  201. height += p->getPreferredHeight();
  202. return height + 10;
  203. }
  204. private:
  205. std::vector<DSPDemoParameterBase*> parameters;
  206. OwnedArray<Label> labels;
  207. };
  208. //==============================================================================
  209. template <class DemoType>
  210. struct DSPDemo : public AudioSource,
  211. public ProcessorWrapper<DemoType>,
  212. private ChangeListener
  213. {
  214. DSPDemo (AudioSource& input)
  215. : inputSource (&input)
  216. {
  217. for (auto* p : getParameters())
  218. p->addChangeListener (this);
  219. }
  220. void prepareToPlay (int blockSize, double sampleRate) override
  221. {
  222. inputSource->prepareToPlay (blockSize, sampleRate);
  223. this->prepare ({ sampleRate, (uint32) blockSize, 2 });
  224. }
  225. void releaseResources() override
  226. {
  227. inputSource->releaseResources();
  228. }
  229. void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override
  230. {
  231. if (bufferToFill.buffer == nullptr)
  232. {
  233. jassertfalse;
  234. return;
  235. }
  236. inputSource->getNextAudioBlock (bufferToFill);
  237. AudioBlock<float> block (*bufferToFill.buffer,
  238. (size_t) bufferToFill.startSample);
  239. ScopedLock audioLock (audioCallbackLock);
  240. this->process (ProcessContextReplacing<float> (block));
  241. }
  242. const std::vector<DSPDemoParameterBase*>& getParameters()
  243. {
  244. return this->processor.parameters;
  245. }
  246. void changeListenerCallback (ChangeBroadcaster*) override
  247. {
  248. ScopedLock audioLock (audioCallbackLock);
  249. static_cast<DemoType&> (this->processor).updateParameters();
  250. }
  251. CriticalSection audioCallbackLock;
  252. AudioSource* inputSource;
  253. };
  254. //==============================================================================
  255. template <class DemoType>
  256. class AudioFileReaderComponent : public Component,
  257. private TimeSliceThread,
  258. private Value::Listener,
  259. private ChangeListener
  260. {
  261. public:
  262. //==============================================================================
  263. AudioFileReaderComponent()
  264. : TimeSliceThread ("Audio File Reader Thread"),
  265. header (audioDeviceManager, formatManager, *this)
  266. {
  267. loopState.addListener (this);
  268. formatManager.registerBasicFormats();
  269. audioDeviceManager.addAudioCallback (&audioSourcePlayer);
  270. #ifndef JUCE_DEMO_RUNNER
  271. audioDeviceManager.initialiseWithDefaultDevices (0, 2);
  272. #endif
  273. init();
  274. startThread();
  275. setOpaque (true);
  276. addAndMakeVisible (header);
  277. setSize (800, 250);
  278. }
  279. ~AudioFileReaderComponent() override
  280. {
  281. signalThreadShouldExit();
  282. stop();
  283. audioDeviceManager.removeAudioCallback (&audioSourcePlayer);
  284. waitForThreadToExit (10000);
  285. }
  286. void paint (Graphics& g) override
  287. {
  288. g.setColour (getLookAndFeel().findColour (ResizableWindow::backgroundColourId));
  289. g.fillRect (getLocalBounds());
  290. }
  291. void resized() override
  292. {
  293. auto r = getLocalBounds();
  294. header.setBounds (r.removeFromTop (120));
  295. r.removeFromTop (20);
  296. if (parametersComponent.get() != nullptr)
  297. parametersComponent->setBounds (r.removeFromTop (parametersComponent->getHeightNeeded()).reduced (20, 0));
  298. }
  299. //==============================================================================
  300. bool loadURL (const URL& fileToPlay)
  301. {
  302. stop();
  303. audioSourcePlayer.setSource (nullptr);
  304. getThumbnailComponent().setTransportSource (nullptr);
  305. transportSource.reset();
  306. readerSource.reset();
  307. auto source = makeInputSource (fileToPlay);
  308. if (source == nullptr)
  309. return false;
  310. auto stream = rawToUniquePtr (source->createInputStream());
  311. if (stream == nullptr)
  312. return false;
  313. reader = rawToUniquePtr (formatManager.createReaderFor (std::move (stream)));
  314. if (reader == nullptr)
  315. return false;
  316. readerSource.reset (new AudioFormatReaderSource (reader.get(), false));
  317. readerSource->setLooping (loopState.getValue());
  318. init();
  319. return true;
  320. }
  321. void togglePlay()
  322. {
  323. if (playState.getValue())
  324. stop();
  325. else
  326. play();
  327. }
  328. void stop()
  329. {
  330. playState = false;
  331. if (transportSource.get() != nullptr)
  332. {
  333. transportSource->stop();
  334. transportSource->setPosition (0);
  335. }
  336. }
  337. void init()
  338. {
  339. if (transportSource.get() == nullptr)
  340. {
  341. transportSource.reset (new AudioTransportSource());
  342. transportSource->addChangeListener (this);
  343. if (readerSource.get() != nullptr)
  344. {
  345. if (auto* device = audioDeviceManager.getCurrentAudioDevice())
  346. {
  347. transportSource->setSource (readerSource.get(), roundToInt (device->getCurrentSampleRate()), this, reader->sampleRate);
  348. getThumbnailComponent().setTransportSource (transportSource.get());
  349. }
  350. }
  351. }
  352. audioSourcePlayer.setSource (nullptr);
  353. currentDemo.reset();
  354. if (currentDemo.get() == nullptr)
  355. currentDemo.reset (new DSPDemo<DemoType> (*transportSource));
  356. audioSourcePlayer.setSource (currentDemo.get());
  357. initParameters();
  358. }
  359. void play()
  360. {
  361. if (readerSource.get() == nullptr)
  362. return;
  363. if (transportSource->getCurrentPosition() >= transportSource->getLengthInSeconds()
  364. || transportSource->getCurrentPosition() < 0)
  365. transportSource->setPosition (0);
  366. transportSource->start();
  367. playState = true;
  368. }
  369. void setLooping (bool shouldLoop)
  370. {
  371. if (readerSource.get() != nullptr)
  372. readerSource->setLooping (shouldLoop);
  373. }
  374. AudioThumbnailComponent& getThumbnailComponent() { return header.thumbnailComp; }
  375. void initParameters()
  376. {
  377. auto& parameters = currentDemo->getParameters();
  378. parametersComponent.reset();
  379. if (parameters.size() > 0)
  380. {
  381. parametersComponent.reset (new DemoParametersComponent (parameters));
  382. addAndMakeVisible (parametersComponent.get());
  383. }
  384. resized();
  385. }
  386. private:
  387. //==============================================================================
  388. class AudioPlayerHeader : public Component,
  389. private ChangeListener,
  390. private Value::Listener
  391. {
  392. public:
  393. AudioPlayerHeader (AudioDeviceManager& adm,
  394. AudioFormatManager& afm,
  395. AudioFileReaderComponent& afr)
  396. : thumbnailComp (adm, afm),
  397. audioFileReader (afr)
  398. {
  399. setOpaque (true);
  400. addAndMakeVisible (loadButton);
  401. addAndMakeVisible (playButton);
  402. addAndMakeVisible (loopButton);
  403. playButton.setColour (TextButton::buttonColourId, Colour (0xff79ed7f));
  404. playButton.setColour (TextButton::textColourOffId, Colours::black);
  405. loadButton.setColour (TextButton::buttonColourId, Colour (0xff797fed));
  406. loadButton.setColour (TextButton::textColourOffId, Colours::black);
  407. loadButton.onClick = [this] { openFile(); };
  408. playButton.onClick = [this] { audioFileReader.togglePlay(); };
  409. addAndMakeVisible (thumbnailComp);
  410. thumbnailComp.addChangeListener (this);
  411. audioFileReader.playState.addListener (this);
  412. loopButton.getToggleStateValue().referTo (audioFileReader.loopState);
  413. }
  414. ~AudioPlayerHeader() override
  415. {
  416. audioFileReader.playState.removeListener (this);
  417. }
  418. void paint (Graphics& g) override
  419. {
  420. g.setColour (getLookAndFeel().findColour (ResizableWindow::backgroundColourId).darker());
  421. g.fillRect (getLocalBounds());
  422. }
  423. void resized() override
  424. {
  425. auto bounds = getLocalBounds();
  426. auto buttonBounds = bounds.removeFromLeft (jmin (250, bounds.getWidth() / 4));
  427. auto loopBounds = buttonBounds.removeFromBottom (30);
  428. loadButton.setBounds (buttonBounds.removeFromTop (buttonBounds.getHeight() / 2));
  429. playButton.setBounds (buttonBounds);
  430. loopButton.setSize (0, 25);
  431. loopButton.changeWidthToFitText();
  432. loopButton.setCentrePosition (loopBounds.getCentre());
  433. thumbnailComp.setBounds (bounds);
  434. }
  435. AudioThumbnailComponent thumbnailComp;
  436. private:
  437. //==============================================================================
  438. void openFile()
  439. {
  440. audioFileReader.stop();
  441. if (fileChooser != nullptr)
  442. return;
  443. if (! RuntimePermissions::isGranted (RuntimePermissions::readExternalStorage))
  444. {
  445. SafePointer<AudioPlayerHeader> safeThis (this);
  446. RuntimePermissions::request (RuntimePermissions::readExternalStorage,
  447. [safeThis] (bool granted) mutable
  448. {
  449. if (safeThis != nullptr && granted)
  450. safeThis->openFile();
  451. });
  452. return;
  453. }
  454. fileChooser.reset (new FileChooser ("Select an audio file...", File(), "*.wav;*.mp3;*.aif"));
  455. fileChooser->launchAsync (FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles,
  456. [this] (const FileChooser& fc) mutable
  457. {
  458. if (fc.getURLResults().size() > 0)
  459. {
  460. const auto u = fc.getURLResult();
  461. if (! audioFileReader.loadURL (u))
  462. NativeMessageBox::showAsync (MessageBoxOptions()
  463. .withIconType (MessageBoxIconType::WarningIcon)
  464. .withTitle ("Error loading file")
  465. .withMessage ("Unable to load audio file"),
  466. nullptr);
  467. else
  468. thumbnailComp.setCurrentURL (u);
  469. }
  470. fileChooser = nullptr;
  471. }, nullptr);
  472. }
  473. void changeListenerCallback (ChangeBroadcaster*) override
  474. {
  475. if (audioFileReader.playState.getValue())
  476. audioFileReader.stop();
  477. audioFileReader.loadURL (thumbnailComp.getCurrentURL());
  478. }
  479. void valueChanged (Value& v) override
  480. {
  481. playButton.setButtonText (v.getValue() ? "Stop" : "Play");
  482. playButton.setColour (TextButton::buttonColourId, v.getValue() ? Colour (0xffed797f) : Colour (0xff79ed7f));
  483. }
  484. //==============================================================================
  485. TextButton loadButton { "Load File..." }, playButton { "Play" };
  486. ToggleButton loopButton { "Loop File" };
  487. AudioFileReaderComponent& audioFileReader;
  488. std::unique_ptr<FileChooser> fileChooser;
  489. };
  490. //==============================================================================
  491. void valueChanged (Value& v) override
  492. {
  493. if (readerSource.get() != nullptr)
  494. readerSource->setLooping (v.getValue());
  495. }
  496. void changeListenerCallback (ChangeBroadcaster*) override
  497. {
  498. if (playState.getValue() && ! transportSource->isPlaying())
  499. stop();
  500. }
  501. //==============================================================================
  502. // if this PIP is running inside the demo runner, we'll use the shared device manager instead
  503. #ifndef JUCE_DEMO_RUNNER
  504. AudioDeviceManager audioDeviceManager;
  505. #else
  506. AudioDeviceManager& audioDeviceManager { getSharedAudioDeviceManager (0, 2) };
  507. #endif
  508. AudioFormatManager formatManager;
  509. Value playState { var (false) };
  510. Value loopState { var (false) };
  511. double currentSampleRate = 44100.0;
  512. uint32 currentBlockSize = 512;
  513. uint32 currentNumChannels = 2;
  514. std::unique_ptr<AudioFormatReader> reader;
  515. std::unique_ptr<AudioFormatReaderSource> readerSource;
  516. std::unique_ptr<AudioTransportSource> transportSource;
  517. std::unique_ptr<DSPDemo<DemoType>> currentDemo;
  518. AudioSourcePlayer audioSourcePlayer;
  519. AudioPlayerHeader header;
  520. AudioBuffer<float> fileReadBuffer;
  521. std::unique_ptr<DemoParametersComponent> parametersComponent;
  522. };