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.

487 lines
18KB

  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: 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, vs2022, 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. explicit 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. void stopAndReset()
  45. {
  46. if (inDevice != nullptr)
  47. inDevice->stop();
  48. inDevice .reset();
  49. outDevice.reset();
  50. }
  51. };
  52. //==============================================================================
  53. class MidiDemo : public Component,
  54. private MidiKeyboardState::Listener,
  55. private MidiInputCallback,
  56. private AsyncUpdater
  57. {
  58. public:
  59. //==============================================================================
  60. MidiDemo()
  61. : midiKeyboard (keyboardState, MidiKeyboardComponent::horizontalKeyboard),
  62. midiInputSelector (new MidiDeviceListBox ("Midi Input Selector", *this, true)),
  63. midiOutputSelector (new MidiDeviceListBox ("Midi Output Selector", *this, false))
  64. {
  65. addLabelAndSetStyle (midiInputLabel);
  66. addLabelAndSetStyle (midiOutputLabel);
  67. addLabelAndSetStyle (incomingMidiLabel);
  68. addLabelAndSetStyle (outgoingMidiLabel);
  69. midiKeyboard.setName ("MIDI Keyboard");
  70. addAndMakeVisible (midiKeyboard);
  71. midiMonitor.setMultiLine (true);
  72. midiMonitor.setReturnKeyStartsNewLine (false);
  73. midiMonitor.setReadOnly (true);
  74. midiMonitor.setScrollbarsShown (true);
  75. midiMonitor.setCaretVisible (false);
  76. midiMonitor.setPopupMenuEnabled (false);
  77. midiMonitor.setText ({});
  78. addAndMakeVisible (midiMonitor);
  79. if (! BluetoothMidiDevicePairingDialogue::isAvailable())
  80. pairButton.setEnabled (false);
  81. addAndMakeVisible (pairButton);
  82. pairButton.onClick = []
  83. {
  84. RuntimePermissions::request (RuntimePermissions::bluetoothMidi,
  85. [] (bool wasGranted)
  86. {
  87. if (wasGranted)
  88. BluetoothMidiDevicePairingDialogue::open();
  89. });
  90. };
  91. keyboardState.addListener (this);
  92. addAndMakeVisible (midiInputSelector .get());
  93. addAndMakeVisible (midiOutputSelector.get());
  94. setSize (732, 520);
  95. updateDeviceLists();
  96. }
  97. ~MidiDemo() override
  98. {
  99. midiInputs .clear();
  100. midiOutputs.clear();
  101. keyboardState.removeListener (this);
  102. midiInputSelector .reset();
  103. midiOutputSelector.reset();
  104. }
  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. auto& list = isInput ? midiInputs : midiOutputs;
  168. list[index]->stopAndReset();
  169. }
  170. int getNumMidiInputs() const noexcept
  171. {
  172. return midiInputs.size();
  173. }
  174. int getNumMidiOutputs() const noexcept
  175. {
  176. return midiOutputs.size();
  177. }
  178. ReferenceCountedObjectPtr<MidiDeviceListEntry> getMidiDevice (int index, bool isInput) const noexcept
  179. {
  180. return isInput ? midiInputs[index] : midiOutputs[index];
  181. }
  182. private:
  183. //==============================================================================
  184. struct MidiDeviceListBox : private ListBoxModel,
  185. public ListBox
  186. {
  187. MidiDeviceListBox (const String& name,
  188. MidiDemo& contentComponent,
  189. bool isInputDeviceList)
  190. : ListBox (name),
  191. parent (contentComponent),
  192. isInput (isInputDeviceList)
  193. {
  194. setModel (this);
  195. setOutlineThickness (1);
  196. setMultipleSelectionEnabled (true);
  197. setClickingTogglesRowSelection (true);
  198. }
  199. //==============================================================================
  200. int getNumRows() override
  201. {
  202. return isInput ? parent.getNumMidiInputs()
  203. : parent.getNumMidiOutputs();
  204. }
  205. void paintListBoxItem (int rowNumber, Graphics& g,
  206. int width, int height, bool rowIsSelected) override
  207. {
  208. auto textColour = getLookAndFeel().findColour (ListBox::textColourId);
  209. if (rowIsSelected)
  210. g.fillAll (textColour.interpolatedWith (getLookAndFeel().findColour (ListBox::backgroundColourId), 0.5));
  211. g.setColour (textColour);
  212. g.setFont ((float) height * 0.7f);
  213. if (isInput)
  214. {
  215. if (rowNumber < parent.getNumMidiInputs())
  216. g.drawText (parent.getMidiDevice (rowNumber, true)->deviceInfo.name,
  217. 5, 0, width, height,
  218. Justification::centredLeft, true);
  219. }
  220. else
  221. {
  222. if (rowNumber < parent.getNumMidiOutputs())
  223. g.drawText (parent.getMidiDevice (rowNumber, false)->deviceInfo.name,
  224. 5, 0, width, height,
  225. Justification::centredLeft, true);
  226. }
  227. }
  228. //==============================================================================
  229. void selectedRowsChanged (int) override
  230. {
  231. auto newSelectedItems = getSelectedRows();
  232. if (newSelectedItems != lastSelectedItems)
  233. {
  234. for (auto i = 0; i < lastSelectedItems.size(); ++i)
  235. {
  236. if (! newSelectedItems.contains (lastSelectedItems[i]))
  237. parent.closeDevice (isInput, lastSelectedItems[i]);
  238. }
  239. for (auto i = 0; i < newSelectedItems.size(); ++i)
  240. {
  241. if (! lastSelectedItems.contains (newSelectedItems[i]))
  242. parent.openDevice (isInput, newSelectedItems[i]);
  243. }
  244. lastSelectedItems = newSelectedItems;
  245. }
  246. }
  247. //==============================================================================
  248. void syncSelectedItemsWithDeviceList (const ReferenceCountedArray<MidiDeviceListEntry>& midiDevices)
  249. {
  250. SparseSet<int> selectedRows;
  251. for (auto i = 0; i < midiDevices.size(); ++i)
  252. if (midiDevices[i]->inDevice.get() != nullptr || midiDevices[i]->outDevice.get() != nullptr)
  253. selectedRows.addRange (Range<int> (i, i + 1));
  254. lastSelectedItems = selectedRows;
  255. updateContent();
  256. setSelectedRows (selectedRows, dontSendNotification);
  257. }
  258. private:
  259. //==============================================================================
  260. MidiDemo& parent;
  261. bool isInput;
  262. SparseSet<int> lastSelectedItems;
  263. };
  264. //==============================================================================
  265. void handleIncomingMidiMessage (MidiInput* /*source*/, const MidiMessage& message) override
  266. {
  267. // This is called on the MIDI thread
  268. const ScopedLock sl (midiMonitorLock);
  269. incomingMessages.add (message);
  270. triggerAsyncUpdate();
  271. }
  272. void handleAsyncUpdate() override
  273. {
  274. // This is called on the message loop
  275. Array<MidiMessage> messages;
  276. {
  277. const ScopedLock sl (midiMonitorLock);
  278. messages.swapWith (incomingMessages);
  279. }
  280. String messageText;
  281. for (auto& m : messages)
  282. messageText << m.getDescription() << "\n";
  283. midiMonitor.insertTextAtCaret (messageText);
  284. }
  285. void sendToOutputs (const MidiMessage& msg)
  286. {
  287. for (auto midiOutput : midiOutputs)
  288. if (midiOutput->outDevice.get() != nullptr)
  289. midiOutput->outDevice->sendMessageNow (msg);
  290. }
  291. //==============================================================================
  292. bool hasDeviceListChanged (const Array<MidiDeviceInfo>& availableDevices, bool isInputDevice)
  293. {
  294. ReferenceCountedArray<MidiDeviceListEntry>& midiDevices = isInputDevice ? midiInputs
  295. : midiOutputs;
  296. if (availableDevices.size() != midiDevices.size())
  297. return true;
  298. for (auto i = 0; i < availableDevices.size(); ++i)
  299. if (availableDevices[i] != midiDevices[i]->deviceInfo)
  300. return true;
  301. return false;
  302. }
  303. ReferenceCountedObjectPtr<MidiDeviceListEntry> findDevice (MidiDeviceInfo device, bool isInputDevice) const
  304. {
  305. const ReferenceCountedArray<MidiDeviceListEntry>& midiDevices = isInputDevice ? midiInputs
  306. : midiOutputs;
  307. for (auto& d : midiDevices)
  308. if (d->deviceInfo == device)
  309. return d;
  310. return nullptr;
  311. }
  312. void closeUnpluggedDevices (const Array<MidiDeviceInfo>& currentlyPluggedInDevices, bool isInputDevice)
  313. {
  314. ReferenceCountedArray<MidiDeviceListEntry>& midiDevices = isInputDevice ? midiInputs
  315. : midiOutputs;
  316. for (auto i = midiDevices.size(); --i >= 0;)
  317. {
  318. auto& d = *midiDevices[i];
  319. if (! currentlyPluggedInDevices.contains (d.deviceInfo))
  320. {
  321. if (isInputDevice ? d.inDevice .get() != nullptr
  322. : d.outDevice.get() != nullptr)
  323. closeDevice (isInputDevice, i);
  324. midiDevices.remove (i);
  325. }
  326. }
  327. }
  328. void updateDeviceList (bool isInputDeviceList)
  329. {
  330. auto availableDevices = isInputDeviceList ? MidiInput::getAvailableDevices()
  331. : MidiOutput::getAvailableDevices();
  332. if (hasDeviceListChanged (availableDevices, isInputDeviceList))
  333. {
  334. ReferenceCountedArray<MidiDeviceListEntry>& midiDevices
  335. = isInputDeviceList ? midiInputs : midiOutputs;
  336. closeUnpluggedDevices (availableDevices, isInputDeviceList);
  337. ReferenceCountedArray<MidiDeviceListEntry> newDeviceList;
  338. // add all currently plugged-in devices to the device list
  339. for (auto& newDevice : availableDevices)
  340. {
  341. MidiDeviceListEntry::Ptr entry = findDevice (newDevice, isInputDeviceList);
  342. if (entry == nullptr)
  343. entry = new MidiDeviceListEntry (newDevice);
  344. newDeviceList.add (entry);
  345. }
  346. // actually update the device list
  347. midiDevices = newDeviceList;
  348. // update the selection status of the combo-box
  349. if (auto* midiSelector = isInputDeviceList ? midiInputSelector.get() : midiOutputSelector.get())
  350. midiSelector->syncSelectedItemsWithDeviceList (midiDevices);
  351. }
  352. }
  353. //==============================================================================
  354. void addLabelAndSetStyle (Label& label)
  355. {
  356. label.setFont (Font (15.00f, Font::plain));
  357. label.setJustificationType (Justification::centredLeft);
  358. label.setEditable (false, false, false);
  359. label.setColour (TextEditor::textColourId, Colours::black);
  360. label.setColour (TextEditor::backgroundColourId, Colour (0x00000000));
  361. addAndMakeVisible (label);
  362. }
  363. void updateDeviceLists()
  364. {
  365. for (const auto isInput : { true, false })
  366. updateDeviceList (isInput);
  367. }
  368. //==============================================================================
  369. Label midiInputLabel { "Midi Input Label", "MIDI Input:" };
  370. Label midiOutputLabel { "Midi Output Label", "MIDI Output:" };
  371. Label incomingMidiLabel { "Incoming Midi Label", "Received MIDI messages:" };
  372. Label outgoingMidiLabel { "Outgoing Midi Label", "Play the keyboard to send MIDI messages..." };
  373. MidiKeyboardState keyboardState;
  374. MidiKeyboardComponent midiKeyboard;
  375. TextEditor midiMonitor { "MIDI Monitor" };
  376. TextButton pairButton { "MIDI Bluetooth devices..." };
  377. ReferenceCountedArray<MidiDeviceListEntry> midiInputs, midiOutputs;
  378. std::unique_ptr<MidiDeviceListBox> midiInputSelector, midiOutputSelector;
  379. CriticalSection midiMonitorLock;
  380. Array<MidiMessage> incomingMessages;
  381. MidiDeviceListConnection connection = MidiDeviceListConnection::make ([this]
  382. {
  383. updateDeviceLists();
  384. });
  385. //==============================================================================
  386. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiDemo)
  387. };