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.

459 lines
17KB

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