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.

378 lines
13KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2017 - ROLI Ltd.
  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 5 End-User License
  8. Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
  9. 27th April 2017).
  10. End User License Agreement: www.juce.com/juce-5-licence
  11. Privacy Policy: www.juce.com/juce-5-privacy-policy
  12. Or: You may also use this code under the terms of the GPL v3 (see
  13. www.gnu.org/licenses).
  14. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  15. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  16. DISCLAIMED.
  17. ==============================================================================
  18. */
  19. #pragma once
  20. #include "../JuceLibraryCode/JuceHeader.h"
  21. #include "Audio.h"
  22. #include "WaveshapeProgram.h"
  23. //==============================================================================
  24. /**
  25. A struct that handles the setup and layout of the DrumPadGridProgram
  26. */
  27. struct SynthGrid
  28. {
  29. SynthGrid (int cols, int rows)
  30. : numColumns (cols),
  31. numRows (rows)
  32. {
  33. constructGridFillArray();
  34. }
  35. /** Creates a GridFill object for each pad in the grid and sets its colour
  36. and fill before adding it to an array of GridFill objects
  37. */
  38. void constructGridFillArray()
  39. {
  40. gridFillArray.clear();
  41. for (auto i = 0; i < numRows; ++i)
  42. {
  43. for (auto j = 0; j < numColumns; ++j)
  44. {
  45. DrumPadGridProgram::GridFill fill;
  46. auto padNum = (i * 5) + j;
  47. fill.colour = notes.contains (padNum) ? baseGridColour
  48. : tonics.contains (padNum) ? Colours::white
  49. : Colours::black;
  50. fill.fillType = DrumPadGridProgram::GridFill::FillType::gradient;
  51. gridFillArray.add (fill);
  52. }
  53. }
  54. }
  55. int getNoteNumberForPad (int x, int y) const
  56. {
  57. auto xIndex = x / 3;
  58. auto yIndex = y / 3;
  59. return 60 + ((4 - yIndex) * 5) + xIndex;
  60. }
  61. //==============================================================================
  62. int numColumns, numRows;
  63. float width, height;
  64. Array<DrumPadGridProgram::GridFill> gridFillArray;
  65. Colour baseGridColour = Colours::green;
  66. Colour touchColour = Colours::red;
  67. Array<int> tonics = { 4, 12, 20 };
  68. Array<int> notes = { 1, 3, 6, 7, 9, 11, 14, 15, 17, 19, 22, 24 };
  69. //==============================================================================
  70. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SynthGrid)
  71. };
  72. //==============================================================================
  73. /**
  74. The main component
  75. */
  76. class MainComponent : public Component,
  77. public TopologySource::Listener,
  78. private TouchSurface::Listener,
  79. private ControlButton::Listener,
  80. #if JUCE_IOS
  81. private Button::Listener,
  82. #endif
  83. private Timer
  84. {
  85. public:
  86. MainComponent()
  87. {
  88. setSize (600, 400);
  89. // Register MainContentComponent as a listener to the PhysicalTopologySource object
  90. topologySource.addListener (this);
  91. #if JUCE_IOS
  92. connectButton.setButtonText ("Connect");
  93. connectButton.addListener (this);
  94. addAndMakeVisible (connectButton);
  95. #endif
  96. };
  97. ~MainComponent()
  98. {
  99. if (activeBlock != nullptr)
  100. detachActiveBlock();
  101. }
  102. void paint (Graphics& g) override
  103. {
  104. g.setColour (getLookAndFeel().findColour (Label::textColourId));
  105. g.drawText ("Connect a Lightpad Block to play.",
  106. getLocalBounds(), Justification::centred, false);
  107. }
  108. void resized() override
  109. {
  110. #if JUCE_IOS
  111. connectButton.setBounds (getRight() - 100, 20, 80, 30);
  112. #endif
  113. }
  114. /** Overridden from TopologySource::Listener, called when the topology changes */
  115. void topologyChanged() override
  116. {
  117. // Reset the activeBlock object
  118. if (activeBlock != nullptr)
  119. detachActiveBlock();
  120. // Get the array of currently connected Block objects from the PhysicalTopologySource
  121. auto blocks = topologySource.getCurrentTopology().blocks;
  122. // Iterate over the array of Block objects
  123. for (auto b : blocks)
  124. {
  125. // Find the first Lightpad
  126. if (b->getType() == Block::Type::lightPadBlock)
  127. {
  128. activeBlock = b;
  129. // Register MainContentComponent as a listener to the touch surface
  130. if (auto surface = activeBlock->getTouchSurface())
  131. surface->addListener (this);
  132. // Register MainContentComponent as a listener to any buttons
  133. for (auto button : activeBlock->getButtons())
  134. button->addListener (this);
  135. // Get the LEDGrid object from the Lightpad and set its program to the program for the current mode
  136. if (auto grid = activeBlock->getLEDGrid())
  137. {
  138. // Work out scale factors to translate X and Y touches to LED indexes
  139. scaleX = static_cast<float> (grid->getNumColumns() - 1) / activeBlock->getWidth();
  140. scaleY = static_cast<float> (grid->getNumRows() - 1) / activeBlock->getHeight();
  141. setLEDProgram (*activeBlock);
  142. }
  143. break;
  144. }
  145. }
  146. }
  147. private:
  148. /** Overridden from TouchSurface::Listener. Called when a Touch is received on the Lightpad */
  149. void touchChanged (TouchSurface&, const TouchSurface::Touch& touch) override
  150. {
  151. if (currentMode == waveformSelectionMode && touch.isTouchStart && allowTouch)
  152. {
  153. if (auto* waveshapeProgram = getWaveshapeProgram())
  154. {
  155. // Change the displayed waveshape to the next one
  156. ++waveshapeMode;
  157. if (waveshapeMode > 3)
  158. waveshapeMode = 0;
  159. waveshapeProgram->setWaveshapeType (static_cast<uint8> (waveshapeMode));
  160. allowTouch = false;
  161. startTimer (250);
  162. }
  163. }
  164. else if (currentMode == playMode)
  165. {
  166. if (auto* gridProgram = getGridProgram())
  167. {
  168. // Translate X and Y touch events to LED indexes
  169. auto xLed = roundToInt (touch.startX * scaleX);
  170. auto yLed = roundToInt (touch.startY * scaleY);
  171. // Limit the number of touches per second
  172. constexpr auto maxNumTouchMessagesPerSecond = 100;
  173. auto now = Time::getCurrentTime();
  174. clearOldTouchTimes (now);
  175. auto midiChannel = waveshapeMode + 1;
  176. // Send the touch event to the DrumPadGridProgram and Audio class
  177. if (touch.isTouchStart)
  178. {
  179. gridProgram->startTouch (touch.startX, touch.startY);
  180. audio.noteOn (midiChannel, layout.getNoteNumberForPad (xLed, yLed), touch.z);
  181. }
  182. else if (touch.isTouchEnd)
  183. {
  184. gridProgram->endTouch (touch.startX, touch.startY);
  185. audio.noteOff (midiChannel, layout.getNoteNumberForPad (xLed, yLed), 1.0);
  186. }
  187. else
  188. {
  189. if (touchMessageTimesInLastSecond.size() > maxNumTouchMessagesPerSecond / 3)
  190. return;
  191. gridProgram->sendTouch (touch.x, touch.y, touch.z,
  192. layout.touchColour);
  193. // Send pitch change and pressure values to the Audio class
  194. audio.pitchChange (midiChannel, (touch.x - touch.startX) / activeBlock->getWidth());
  195. audio.pressureChange (midiChannel, touch.z);
  196. }
  197. touchMessageTimesInLastSecond.add (now);
  198. }
  199. }
  200. }
  201. /** Overridden from ControlButton::Listener. Called when a button on the Lightpad is pressed */
  202. void buttonPressed (ControlButton&, Block::Timestamp) override {}
  203. /** Overridden from ControlButton::Listener. Called when a button on the Lightpad is released */
  204. void buttonReleased (ControlButton&, Block::Timestamp) override
  205. {
  206. // Turn any active synthesiser notes off
  207. audio.allNotesOff();
  208. // Switch modes
  209. if (currentMode == waveformSelectionMode)
  210. currentMode = playMode;
  211. else if (currentMode == playMode)
  212. currentMode = waveformSelectionMode;
  213. // Set the LEDGrid program to the new mode
  214. setLEDProgram (*activeBlock);
  215. }
  216. #if JUCE_IOS
  217. void buttonClicked (Button* b) override
  218. {
  219. if (b == &connectButton)
  220. BluetoothMidiDevicePairingDialogue::open();
  221. }
  222. #endif
  223. /** Clears the old touch times */
  224. void clearOldTouchTimes (const Time now)
  225. {
  226. for (auto i = touchMessageTimesInLastSecond.size(); --i >= 0;)
  227. if (touchMessageTimesInLastSecond.getReference(i) < now - juce::RelativeTime::seconds (0.33))
  228. touchMessageTimesInLastSecond.remove (i);
  229. }
  230. /** Removes TouchSurface and ControlButton listeners and sets activeBlock to nullptr */
  231. void detachActiveBlock()
  232. {
  233. if (auto surface = activeBlock->getTouchSurface())
  234. surface->removeListener (this);
  235. for (auto button : activeBlock->getButtons())
  236. button->removeListener (this);
  237. activeBlock = nullptr;
  238. }
  239. /** Sets the LEDGrid Program for the selected mode */
  240. void setLEDProgram (Block& block)
  241. {
  242. if (currentMode == waveformSelectionMode)
  243. {
  244. // Set the LEDGrid program
  245. block.setProgram (new WaveshapeProgram (block));
  246. // Initialise the program
  247. if (auto* waveshapeProgram = getWaveshapeProgram())
  248. {
  249. waveshapeProgram->setWaveshapeType (static_cast<uint8> (waveshapeMode));
  250. waveshapeProgram->generateWaveshapes();
  251. }
  252. }
  253. else if (currentMode == playMode)
  254. {
  255. // Set the LEDGrid program
  256. auto error = block.setProgram (new DrumPadGridProgram (block));
  257. if (error.failed())
  258. {
  259. DBG (error.getErrorMessage());
  260. jassertfalse;
  261. }
  262. // Setup the grid layout
  263. if (auto* gridProgram = getGridProgram())
  264. gridProgram->setGridFills (layout.numColumns, layout.numRows, layout.gridFillArray);
  265. }
  266. }
  267. /** Stops touch events from triggering multiple waveshape mode changes */
  268. void timerCallback() override { allowTouch = true; }
  269. //==============================================================================
  270. DrumPadGridProgram* getGridProgram()
  271. {
  272. if (activeBlock != nullptr)
  273. return dynamic_cast<DrumPadGridProgram*> (activeBlock->getProgram());
  274. return nullptr;
  275. }
  276. WaveshapeProgram* getWaveshapeProgram()
  277. {
  278. if (activeBlock != nullptr)
  279. return dynamic_cast<WaveshapeProgram*> (activeBlock->getProgram());
  280. return nullptr;
  281. }
  282. //==============================================================================
  283. enum BlocksSynthMode
  284. {
  285. waveformSelectionMode = 0,
  286. playMode
  287. };
  288. BlocksSynthMode currentMode = playMode;
  289. //==============================================================================
  290. Audio audio;
  291. SynthGrid layout { 5, 5 };
  292. PhysicalTopologySource topologySource;
  293. Block::Ptr activeBlock;
  294. Array<juce::Time> touchMessageTimesInLastSecond;
  295. int waveshapeMode = 0;
  296. float scaleX = 0.0;
  297. float scaleY = 0.0;
  298. bool allowTouch = true;
  299. //==============================================================================
  300. #if JUCE_IOS
  301. TextButton connectButton;
  302. #endif
  303. //==============================================================================
  304. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
  305. };