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.

484 lines
18KB

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