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.

485 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. 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 : private ListBoxModel,
  194. public ListBox
  195. {
  196. MidiDeviceListBox (const String& name,
  197. MidiDemo& contentComponent,
  198. bool isInputDeviceList)
  199. : ListBox (name),
  200. parent (contentComponent),
  201. isInput (isInputDeviceList)
  202. {
  203. setModel (this);
  204. setOutlineThickness (1);
  205. setMultipleSelectionEnabled (true);
  206. setClickingTogglesRowSelection (true);
  207. }
  208. //==============================================================================
  209. int getNumRows() override
  210. {
  211. return isInput ? parent.getNumMidiInputs()
  212. : parent.getNumMidiOutputs();
  213. }
  214. void paintListBoxItem (int rowNumber, Graphics& g,
  215. int width, int height, bool rowIsSelected) override
  216. {
  217. auto textColour = getLookAndFeel().findColour (ListBox::textColourId);
  218. if (rowIsSelected)
  219. g.fillAll (textColour.interpolatedWith (getLookAndFeel().findColour (ListBox::backgroundColourId), 0.5));
  220. g.setColour (textColour);
  221. g.setFont ((float) height * 0.7f);
  222. if (isInput)
  223. {
  224. if (rowNumber < parent.getNumMidiInputs())
  225. g.drawText (parent.getMidiDevice (rowNumber, true)->deviceInfo.name,
  226. 5, 0, width, height,
  227. Justification::centredLeft, true);
  228. }
  229. else
  230. {
  231. if (rowNumber < parent.getNumMidiOutputs())
  232. g.drawText (parent.getMidiDevice (rowNumber, false)->deviceInfo.name,
  233. 5, 0, width, height,
  234. Justification::centredLeft, true);
  235. }
  236. }
  237. //==============================================================================
  238. void selectedRowsChanged (int) override
  239. {
  240. auto newSelectedItems = getSelectedRows();
  241. if (newSelectedItems != lastSelectedItems)
  242. {
  243. for (auto i = 0; i < lastSelectedItems.size(); ++i)
  244. {
  245. if (! newSelectedItems.contains (lastSelectedItems[i]))
  246. parent.closeDevice (isInput, lastSelectedItems[i]);
  247. }
  248. for (auto i = 0; i < newSelectedItems.size(); ++i)
  249. {
  250. if (! lastSelectedItems.contains (newSelectedItems[i]))
  251. parent.openDevice (isInput, newSelectedItems[i]);
  252. }
  253. lastSelectedItems = newSelectedItems;
  254. }
  255. }
  256. //==============================================================================
  257. void syncSelectedItemsWithDeviceList (const ReferenceCountedArray<MidiDeviceListEntry>& midiDevices)
  258. {
  259. SparseSet<int> selectedRows;
  260. for (auto i = 0; i < midiDevices.size(); ++i)
  261. if (midiDevices[i]->inDevice.get() != nullptr || midiDevices[i]->outDevice.get() != nullptr)
  262. selectedRows.addRange (Range<int> (i, i + 1));
  263. lastSelectedItems = selectedRows;
  264. updateContent();
  265. setSelectedRows (selectedRows, dontSendNotification);
  266. }
  267. private:
  268. //==============================================================================
  269. MidiDemo& parent;
  270. bool isInput;
  271. SparseSet<int> lastSelectedItems;
  272. };
  273. //==============================================================================
  274. void handleIncomingMidiMessage (MidiInput* /*source*/, const MidiMessage& message) override
  275. {
  276. // This is called on the MIDI thread
  277. const ScopedLock sl (midiMonitorLock);
  278. incomingMessages.add (message);
  279. triggerAsyncUpdate();
  280. }
  281. void handleAsyncUpdate() override
  282. {
  283. // This is called on the message loop
  284. Array<MidiMessage> messages;
  285. {
  286. const ScopedLock sl (midiMonitorLock);
  287. messages.swapWith (incomingMessages);
  288. }
  289. String messageText;
  290. for (auto& m : messages)
  291. messageText << m.getDescription() << "\n";
  292. midiMonitor.insertTextAtCaret (messageText);
  293. }
  294. void sendToOutputs (const MidiMessage& msg)
  295. {
  296. for (auto midiOutput : midiOutputs)
  297. if (midiOutput->outDevice.get() != nullptr)
  298. midiOutput->outDevice->sendMessageNow (msg);
  299. }
  300. //==============================================================================
  301. bool hasDeviceListChanged (const Array<MidiDeviceInfo>& availableDevices, bool isInputDevice)
  302. {
  303. ReferenceCountedArray<MidiDeviceListEntry>& midiDevices = isInputDevice ? midiInputs
  304. : midiOutputs;
  305. if (availableDevices.size() != midiDevices.size())
  306. return true;
  307. for (auto i = 0; i < availableDevices.size(); ++i)
  308. if (availableDevices[i] != midiDevices[i]->deviceInfo)
  309. return true;
  310. return false;
  311. }
  312. ReferenceCountedObjectPtr<MidiDeviceListEntry> findDevice (MidiDeviceInfo device, bool isInputDevice) const
  313. {
  314. const ReferenceCountedArray<MidiDeviceListEntry>& midiDevices = isInputDevice ? midiInputs
  315. : midiOutputs;
  316. for (auto& d : midiDevices)
  317. if (d->deviceInfo == device)
  318. return d;
  319. return nullptr;
  320. }
  321. void closeUnpluggedDevices (const Array<MidiDeviceInfo>& currentlyPluggedInDevices, bool isInputDevice)
  322. {
  323. ReferenceCountedArray<MidiDeviceListEntry>& midiDevices = isInputDevice ? midiInputs
  324. : midiOutputs;
  325. for (auto i = midiDevices.size(); --i >= 0;)
  326. {
  327. auto& d = *midiDevices[i];
  328. if (! currentlyPluggedInDevices.contains (d.deviceInfo))
  329. {
  330. if (isInputDevice ? d.inDevice .get() != nullptr
  331. : d.outDevice.get() != nullptr)
  332. closeDevice (isInputDevice, i);
  333. midiDevices.remove (i);
  334. }
  335. }
  336. }
  337. void updateDeviceList (bool isInputDeviceList)
  338. {
  339. auto availableDevices = isInputDeviceList ? MidiInput::getAvailableDevices()
  340. : MidiOutput::getAvailableDevices();
  341. if (hasDeviceListChanged (availableDevices, isInputDeviceList))
  342. {
  343. ReferenceCountedArray<MidiDeviceListEntry>& midiDevices
  344. = isInputDeviceList ? midiInputs : midiOutputs;
  345. closeUnpluggedDevices (availableDevices, isInputDeviceList);
  346. ReferenceCountedArray<MidiDeviceListEntry> newDeviceList;
  347. // add all currently plugged-in devices to the device list
  348. for (auto& newDevice : availableDevices)
  349. {
  350. MidiDeviceListEntry::Ptr entry = findDevice (newDevice, isInputDeviceList);
  351. if (entry == nullptr)
  352. entry = new MidiDeviceListEntry (newDevice);
  353. newDeviceList.add (entry);
  354. }
  355. // actually update the device list
  356. midiDevices = newDeviceList;
  357. // update the selection status of the combo-box
  358. if (auto* midiSelector = isInputDeviceList ? midiInputSelector.get() : midiOutputSelector.get())
  359. midiSelector->syncSelectedItemsWithDeviceList (midiDevices);
  360. }
  361. }
  362. //==============================================================================
  363. void addLabelAndSetStyle (Label& label)
  364. {
  365. label.setFont (Font (15.00f, Font::plain));
  366. label.setJustificationType (Justification::centredLeft);
  367. label.setEditable (false, false, false);
  368. label.setColour (TextEditor::textColourId, Colours::black);
  369. label.setColour (TextEditor::backgroundColourId, Colour (0x00000000));
  370. addAndMakeVisible (label);
  371. }
  372. //==============================================================================
  373. Label midiInputLabel { "Midi Input Label", "MIDI Input:" };
  374. Label midiOutputLabel { "Midi Output Label", "MIDI Output:" };
  375. Label incomingMidiLabel { "Incoming Midi Label", "Received MIDI messages:" };
  376. Label outgoingMidiLabel { "Outgoing Midi Label", "Play the keyboard to send MIDI messages..." };
  377. MidiKeyboardState keyboardState;
  378. MidiKeyboardComponent midiKeyboard;
  379. TextEditor midiMonitor { "MIDI Monitor" };
  380. TextButton pairButton { "MIDI Bluetooth devices..." };
  381. std::unique_ptr<MidiDeviceListBox> midiInputSelector, midiOutputSelector;
  382. ReferenceCountedArray<MidiDeviceListEntry> midiInputs, midiOutputs;
  383. CriticalSection midiMonitorLock;
  384. Array<MidiMessage> incomingMessages;
  385. //==============================================================================
  386. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiDemo)
  387. };