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.

488 lines
18KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE examples.
  4. Copyright (c) 2017 - ROLI Ltd.
  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: MidiDemo
  20. version: 1.0.0
  21. vendor: JUCE
  22. website: http://juce.com
  23. description: Handles incoming and outcoming midi messages.
  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, vs2017, linux_make, xcode_iphone, androidstudio
  29. type: Component
  30. mainClass: MidiDemo
  31. useLocalCopy: 1
  32. END_JUCE_PIP_METADATA
  33. *******************************************************************************/
  34. #pragma once
  35. //==============================================================================
  36. struct MidiDeviceListEntry : ReferenceCountedObject
  37. {
  38. MidiDeviceListEntry (const String& deviceName) : name (deviceName) {}
  39. String name;
  40. std::unique_ptr<MidiInput> inDevice;
  41. std::unique_ptr<MidiOutput> outDevice;
  42. typedef ReferenceCountedObjectPtr<MidiDeviceListEntry> Ptr;
  43. };
  44. //==============================================================================
  45. struct MidiCallbackMessage : public Message
  46. {
  47. MidiCallbackMessage (const MidiMessage& msg) : message (msg) {}
  48. MidiMessage message;
  49. };
  50. //==============================================================================
  51. class MidiDemo : public Component,
  52. private Timer,
  53. private MidiKeyboardStateListener,
  54. private MidiInputCallback,
  55. private MessageListener
  56. {
  57. public:
  58. //==============================================================================
  59. MidiDemo()
  60. : midiKeyboard (keyboardState, MidiKeyboardComponent::horizontalKeyboard),
  61. midiInputSelector (new MidiDeviceListBox ("Midi Input Selector", *this, true)),
  62. midiOutputSelector (new MidiDeviceListBox ("Midi Output Selector", *this, false))
  63. {
  64. addLabelAndSetStyle (midiInputLabel);
  65. addLabelAndSetStyle (midiOutputLabel);
  66. addLabelAndSetStyle (incomingMidiLabel);
  67. addLabelAndSetStyle (outgoingMidiLabel);
  68. midiKeyboard.setName ("MIDI Keyboard");
  69. addAndMakeVisible (midiKeyboard);
  70. midiMonitor.setMultiLine (true);
  71. midiMonitor.setReturnKeyStartsNewLine (false);
  72. midiMonitor.setReadOnly (true);
  73. midiMonitor.setScrollbarsShown (true);
  74. midiMonitor.setCaretVisible (false);
  75. midiMonitor.setPopupMenuEnabled (false);
  76. midiMonitor.setText ({});
  77. addAndMakeVisible (midiMonitor);
  78. if (! BluetoothMidiDevicePairingDialogue::isAvailable())
  79. pairButton.setEnabled (false);
  80. addAndMakeVisible (pairButton);
  81. pairButton.onClick = []
  82. {
  83. RuntimePermissions::request (RuntimePermissions::bluetoothMidi,
  84. [] (bool wasGranted)
  85. {
  86. if (wasGranted)
  87. BluetoothMidiDevicePairingDialogue::open();
  88. });
  89. };
  90. keyboardState.addListener (this);
  91. addAndMakeVisible (midiInputSelector .get());
  92. addAndMakeVisible (midiOutputSelector.get());
  93. setSize (732, 520);
  94. startTimer (500);
  95. }
  96. ~MidiDemo()
  97. {
  98. stopTimer();
  99. midiInputs .clear();
  100. midiOutputs.clear();
  101. keyboardState.removeListener (this);
  102. midiInputSelector .reset();
  103. midiOutputSelector.reset();
  104. }
  105. //==============================================================================
  106. void timerCallback() override
  107. {
  108. updateDeviceList (true);
  109. updateDeviceList (false);
  110. }
  111. void handleNoteOn (MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) override
  112. {
  113. MidiMessage m (MidiMessage::noteOn (midiChannel, midiNoteNumber, velocity));
  114. m.setTimeStamp (Time::getMillisecondCounterHiRes() * 0.001);
  115. sendToOutputs (m);
  116. }
  117. void handleNoteOff (MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) override
  118. {
  119. MidiMessage m (MidiMessage::noteOff (midiChannel, midiNoteNumber, velocity));
  120. m.setTimeStamp (Time::getMillisecondCounterHiRes() * 0.001);
  121. sendToOutputs (m);
  122. }
  123. void handleMessage (const Message& msg) override
  124. {
  125. // This is called on the message loop
  126. auto& mm = dynamic_cast<const MidiCallbackMessage&> (msg).message;
  127. String midiString;
  128. midiString << (mm.isNoteOn() ? String ("Note on: ") : String ("Note off: "));
  129. midiString << (MidiMessage::getMidiNoteName (mm.getNoteNumber(), true, true, true));
  130. midiString << (String (" vel = "));
  131. midiString << static_cast<int> (mm.getVelocity());
  132. midiString << "\n";
  133. midiMonitor.insertTextAtCaret (midiString);
  134. }
  135. void paint (Graphics&) override {}
  136. void resized() override
  137. {
  138. auto margin = 10;
  139. midiInputLabel.setBounds (margin, margin,
  140. (getWidth() / 2) - (2 * margin), 24);
  141. midiOutputLabel.setBounds ((getWidth() / 2) + margin, margin,
  142. (getWidth() / 2) - (2 * margin), 24);
  143. midiInputSelector->setBounds (margin, (2 * margin) + 24,
  144. (getWidth() / 2) - (2 * margin),
  145. (getHeight() / 2) - ((4 * margin) + 24 + 24));
  146. midiOutputSelector->setBounds ((getWidth() / 2) + margin, (2 * margin) + 24,
  147. (getWidth() / 2) - (2 * margin),
  148. (getHeight() / 2) - ((4 * margin) + 24 + 24));
  149. pairButton.setBounds (margin, (getHeight() / 2) - (margin + 24),
  150. getWidth() - (2 * margin), 24);
  151. outgoingMidiLabel.setBounds (margin, getHeight() / 2, getWidth() - (2 * margin), 24);
  152. midiKeyboard.setBounds (margin, (getHeight() / 2) + (24 + margin), getWidth() - (2 * margin), 64);
  153. incomingMidiLabel.setBounds (margin, (getHeight() / 2) + (24 + (2 * margin) + 64),
  154. getWidth() - (2 * margin), 24);
  155. auto y = (getHeight() / 2) + ((2 * 24) + (3 * margin) + 64);
  156. midiMonitor.setBounds (margin, y,
  157. getWidth() - (2 * margin), getHeight() - y - margin);
  158. }
  159. void openDevice (bool isInput, int index)
  160. {
  161. if (isInput)
  162. {
  163. jassert (midiInputs[index]->inDevice.get() == nullptr);
  164. midiInputs[index]->inDevice.reset (MidiInput::openDevice (index, this));
  165. if (midiInputs[index]->inDevice.get() == nullptr)
  166. {
  167. DBG ("MidiDemo::openDevice: open input device for index = " << index << " failed!");
  168. return;
  169. }
  170. midiInputs[index]->inDevice->start();
  171. }
  172. else
  173. {
  174. jassert (midiOutputs[index]->outDevice.get() == nullptr);
  175. midiOutputs[index]->outDevice.reset (MidiOutput::openDevice (index));
  176. if (midiOutputs[index]->outDevice.get() == nullptr)
  177. {
  178. DBG ("MidiDemo::openDevice: open output device for index = " << index << " failed!");
  179. }
  180. }
  181. }
  182. void closeDevice (bool isInput, int index)
  183. {
  184. if (isInput)
  185. {
  186. jassert (midiInputs[index]->inDevice.get() != nullptr);
  187. midiInputs[index]->inDevice->stop();
  188. midiInputs[index]->inDevice.reset();
  189. }
  190. else
  191. {
  192. jassert (midiOutputs[index]->outDevice.get() != nullptr);
  193. midiOutputs[index]->outDevice.reset();
  194. }
  195. }
  196. int getNumMidiInputs() const noexcept
  197. {
  198. return midiInputs.size();
  199. }
  200. int getNumMidiOutputs() const noexcept
  201. {
  202. return midiOutputs.size();
  203. }
  204. ReferenceCountedObjectPtr<MidiDeviceListEntry> getMidiDevice (int index, bool isInput) const noexcept
  205. {
  206. return isInput ? midiInputs[index] : midiOutputs[index];
  207. }
  208. private:
  209. //==============================================================================
  210. class MidiDeviceListBox : public ListBox,
  211. private ListBoxModel
  212. {
  213. public:
  214. //==============================================================================
  215. MidiDeviceListBox (const String& name,
  216. MidiDemo& contentComponent,
  217. bool isInputDeviceList)
  218. : ListBox (name, this),
  219. parent (contentComponent),
  220. isInput (isInputDeviceList)
  221. {
  222. setOutlineThickness (1);
  223. setMultipleSelectionEnabled (true);
  224. setClickingTogglesRowSelection (true);
  225. }
  226. //==============================================================================
  227. int getNumRows() override
  228. {
  229. return isInput ? parent.getNumMidiInputs()
  230. : parent.getNumMidiOutputs();
  231. }
  232. //==============================================================================
  233. void paintListBoxItem (int rowNumber, Graphics& g,
  234. int width, int height, bool rowIsSelected) override
  235. {
  236. auto textColour = getLookAndFeel().findColour (ListBox::textColourId);
  237. if (rowIsSelected)
  238. g.fillAll (textColour.interpolatedWith (getLookAndFeel().findColour (ListBox::backgroundColourId), 0.5));
  239. g.setColour (textColour);
  240. g.setFont (height * 0.7f);
  241. if (isInput)
  242. {
  243. if (rowNumber < parent.getNumMidiInputs())
  244. g.drawText (parent.getMidiDevice (rowNumber, true)->name,
  245. 5, 0, width, height,
  246. Justification::centredLeft, true);
  247. }
  248. else
  249. {
  250. if (rowNumber < parent.getNumMidiOutputs())
  251. g.drawText (parent.getMidiDevice (rowNumber, false)->name,
  252. 5, 0, width, height,
  253. Justification::centredLeft, true);
  254. }
  255. }
  256. //==============================================================================
  257. void selectedRowsChanged (int) override
  258. {
  259. auto newSelectedItems = getSelectedRows();
  260. if (newSelectedItems != lastSelectedItems)
  261. {
  262. for (auto i = 0; i < lastSelectedItems.size(); ++i)
  263. {
  264. if (! newSelectedItems.contains (lastSelectedItems[i]))
  265. parent.closeDevice (isInput, lastSelectedItems[i]);
  266. }
  267. for (auto i = 0; i < newSelectedItems.size(); ++i)
  268. {
  269. if (! lastSelectedItems.contains (newSelectedItems[i]))
  270. parent.openDevice (isInput, newSelectedItems[i]);
  271. }
  272. lastSelectedItems = newSelectedItems;
  273. }
  274. }
  275. //==============================================================================
  276. void syncSelectedItemsWithDeviceList (const ReferenceCountedArray<MidiDeviceListEntry>& midiDevices)
  277. {
  278. SparseSet<int> selectedRows;
  279. for (auto i = 0; i < midiDevices.size(); ++i)
  280. if (midiDevices[i]->inDevice.get() != nullptr || midiDevices[i]->outDevice.get() != nullptr)
  281. selectedRows.addRange (Range<int> (i, i + 1));
  282. lastSelectedItems = selectedRows;
  283. updateContent();
  284. setSelectedRows (selectedRows, dontSendNotification);
  285. }
  286. private:
  287. //==============================================================================
  288. MidiDemo& parent;
  289. bool isInput;
  290. SparseSet<int> lastSelectedItems;
  291. };
  292. //==============================================================================
  293. void handleIncomingMidiMessage (MidiInput* /*source*/, const MidiMessage& message) override
  294. {
  295. // This is called on the MIDI thread
  296. if (message.isNoteOnOrOff())
  297. postMessage (new MidiCallbackMessage (message));
  298. }
  299. void sendToOutputs(const MidiMessage& msg)
  300. {
  301. for (auto midiOutput : midiOutputs)
  302. if (midiOutput->outDevice.get() != nullptr)
  303. midiOutput->outDevice->sendMessageNow (msg);
  304. }
  305. //==============================================================================
  306. bool hasDeviceListChanged (const StringArray& deviceNames, bool isInputDevice)
  307. {
  308. ReferenceCountedArray<MidiDeviceListEntry>& midiDevices = isInputDevice ? midiInputs
  309. : midiOutputs;
  310. if (deviceNames.size() != midiDevices.size())
  311. return true;
  312. for (auto i = 0; i < deviceNames.size(); ++i)
  313. if (deviceNames[i] != midiDevices[i]->name)
  314. return true;
  315. return false;
  316. }
  317. ReferenceCountedObjectPtr<MidiDeviceListEntry> findDeviceWithName (const String& name, bool isInputDevice) const
  318. {
  319. const ReferenceCountedArray<MidiDeviceListEntry>& midiDevices = isInputDevice ? midiInputs
  320. : midiOutputs;
  321. for (auto midiDevice : midiDevices)
  322. if (midiDevice->name == name)
  323. return midiDevice;
  324. return nullptr;
  325. }
  326. void closeUnpluggedDevices (StringArray& currentlyPluggedInDevices, bool isInputDevice)
  327. {
  328. ReferenceCountedArray<MidiDeviceListEntry>& midiDevices = isInputDevice ? midiInputs
  329. : midiOutputs;
  330. for (auto i = midiDevices.size(); --i >= 0;)
  331. {
  332. auto& d = *midiDevices[i];
  333. if (! currentlyPluggedInDevices.contains (d.name))
  334. {
  335. if (isInputDevice ? d.inDevice .get() != nullptr
  336. : d.outDevice.get() != nullptr)
  337. closeDevice (isInputDevice, i);
  338. midiDevices.remove (i);
  339. }
  340. }
  341. }
  342. void updateDeviceList (bool isInputDeviceList)
  343. {
  344. auto newDeviceNames = isInputDeviceList ? MidiInput::getDevices()
  345. : MidiOutput::getDevices();
  346. if (hasDeviceListChanged (newDeviceNames, isInputDeviceList))
  347. {
  348. ReferenceCountedArray<MidiDeviceListEntry>& midiDevices
  349. = isInputDeviceList ? midiInputs : midiOutputs;
  350. closeUnpluggedDevices (newDeviceNames, isInputDeviceList);
  351. ReferenceCountedArray<MidiDeviceListEntry> newDeviceList;
  352. // add all currently plugged-in devices to the device list
  353. for (auto newDeviceName : newDeviceNames)
  354. {
  355. MidiDeviceListEntry::Ptr entry = findDeviceWithName (newDeviceName, isInputDeviceList);
  356. if (entry == nullptr)
  357. entry = new MidiDeviceListEntry (newDeviceName);
  358. newDeviceList.add (entry);
  359. }
  360. // actually update the device list
  361. midiDevices = newDeviceList;
  362. // update the selection status of the combo-box
  363. if (auto* midiSelector = isInputDeviceList ? midiInputSelector.get() : midiOutputSelector.get())
  364. midiSelector->syncSelectedItemsWithDeviceList (midiDevices);
  365. }
  366. }
  367. //==============================================================================
  368. void addLabelAndSetStyle (Label& label)
  369. {
  370. label.setFont (Font (15.00f, Font::plain));
  371. label.setJustificationType (Justification::centredLeft);
  372. label.setEditable (false, false, false);
  373. label.setColour (TextEditor::textColourId, Colours::black);
  374. label.setColour (TextEditor::backgroundColourId, Colour (0x00000000));
  375. addAndMakeVisible (label);
  376. }
  377. //==============================================================================
  378. Label midiInputLabel { "Midi Input Label", "MIDI Input:" };
  379. Label midiOutputLabel { "Midi Output Label", "MIDI Output:" };
  380. Label incomingMidiLabel { "Incoming Midi Label", "Received MIDI messages:" };
  381. Label outgoingMidiLabel { "Outgoing Midi Label", "Play the keyboard to send MIDI messages..." };
  382. MidiKeyboardState keyboardState;
  383. MidiKeyboardComponent midiKeyboard;
  384. TextEditor midiMonitor { "MIDI Monitor" };
  385. TextButton pairButton { "MIDI Bluetooth devices..." };
  386. std::unique_ptr<MidiDeviceListBox> midiInputSelector;
  387. std::unique_ptr<MidiDeviceListBox> midiOutputSelector;
  388. ReferenceCountedArray<MidiDeviceListEntry> midiInputs;
  389. ReferenceCountedArray<MidiDeviceListEntry> midiOutputs;
  390. //==============================================================================
  391. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiDemo)
  392. };