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.

1200 lines
42KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE 6 technical preview.
  4. Copyright (c) 2020 - Raw Material Software Limited
  5. You may use this code under the terms of the GPL v3
  6. (see www.gnu.org/licenses).
  7. For this technical preview, this file is not subject to commercial licensing.
  8. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  9. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  10. DISCLAIMED.
  11. ==============================================================================
  12. */
  13. namespace juce
  14. {
  15. struct SimpleDeviceManagerInputLevelMeter : public Component,
  16. public Timer
  17. {
  18. SimpleDeviceManagerInputLevelMeter (AudioDeviceManager& m) : manager (m)
  19. {
  20. startTimerHz (20);
  21. inputLevelGetter = manager.getInputLevelGetter();
  22. }
  23. ~SimpleDeviceManagerInputLevelMeter() override
  24. {
  25. }
  26. void timerCallback() override
  27. {
  28. if (isShowing())
  29. {
  30. auto newLevel = (float) inputLevelGetter->getCurrentLevel();
  31. if (std::abs (level - newLevel) > 0.005f)
  32. {
  33. level = newLevel;
  34. repaint();
  35. }
  36. }
  37. else
  38. {
  39. level = 0;
  40. }
  41. }
  42. void paint (Graphics& g) override
  43. {
  44. // (add a bit of a skew to make the level more obvious)
  45. getLookAndFeel().drawLevelMeter (g, getWidth(), getHeight(),
  46. (float) std::exp (std::log (level) / 3.0));
  47. }
  48. AudioDeviceManager& manager;
  49. AudioDeviceManager::LevelMeter::Ptr inputLevelGetter;
  50. float level = 0;
  51. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SimpleDeviceManagerInputLevelMeter)
  52. };
  53. //==============================================================================
  54. class AudioDeviceSelectorComponent::MidiInputSelectorComponentListBox : public ListBox,
  55. private ListBoxModel
  56. {
  57. public:
  58. MidiInputSelectorComponentListBox (AudioDeviceManager& dm, const String& noItems)
  59. : ListBox ({}, nullptr),
  60. deviceManager (dm),
  61. noItemsMessage (noItems)
  62. {
  63. updateDevices();
  64. setModel (this);
  65. setOutlineThickness (1);
  66. }
  67. void updateDevices()
  68. {
  69. items = MidiInput::getAvailableDevices();
  70. }
  71. int getNumRows() override
  72. {
  73. return items.size();
  74. }
  75. void paintListBoxItem (int row, Graphics& g, int width, int height, bool rowIsSelected) override
  76. {
  77. if (isPositiveAndBelow (row, items.size()))
  78. {
  79. if (rowIsSelected)
  80. g.fillAll (findColour (TextEditor::highlightColourId)
  81. .withMultipliedAlpha (0.3f));
  82. auto item = items[row];
  83. bool enabled = deviceManager.isMidiInputDeviceEnabled (item.identifier);
  84. auto x = getTickX();
  85. auto tickW = height * 0.75f;
  86. getLookAndFeel().drawTickBox (g, *this, x - tickW, (height - tickW) / 2, tickW, tickW,
  87. enabled, true, true, false);
  88. g.setFont (height * 0.6f);
  89. g.setColour (findColour (ListBox::textColourId, true).withMultipliedAlpha (enabled ? 1.0f : 0.6f));
  90. g.drawText (item.name, x + 5, 0, width - x - 5, height, Justification::centredLeft, true);
  91. }
  92. }
  93. void listBoxItemClicked (int row, const MouseEvent& e) override
  94. {
  95. selectRow (row);
  96. if (e.x < getTickX())
  97. flipEnablement (row);
  98. }
  99. void listBoxItemDoubleClicked (int row, const MouseEvent&) override
  100. {
  101. flipEnablement (row);
  102. }
  103. void returnKeyPressed (int row) override
  104. {
  105. flipEnablement (row);
  106. }
  107. void paint (Graphics& g) override
  108. {
  109. ListBox::paint (g);
  110. if (items.isEmpty())
  111. {
  112. g.setColour (Colours::grey);
  113. g.setFont (0.5f * getRowHeight());
  114. g.drawText (noItemsMessage,
  115. 0, 0, getWidth(), getHeight() / 2,
  116. Justification::centred, true);
  117. }
  118. }
  119. int getBestHeight (int preferredHeight)
  120. {
  121. auto extra = getOutlineThickness() * 2;
  122. return jmax (getRowHeight() * 2 + extra,
  123. jmin (getRowHeight() * getNumRows() + extra,
  124. preferredHeight));
  125. }
  126. private:
  127. //==============================================================================
  128. AudioDeviceManager& deviceManager;
  129. const String noItemsMessage;
  130. Array<MidiDeviceInfo> items;
  131. void flipEnablement (const int row)
  132. {
  133. if (isPositiveAndBelow (row, items.size()))
  134. {
  135. auto identifier = items[row].identifier;
  136. deviceManager.setMidiInputDeviceEnabled (identifier, ! deviceManager.isMidiInputDeviceEnabled (identifier));
  137. }
  138. }
  139. int getTickX() const
  140. {
  141. return getRowHeight();
  142. }
  143. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiInputSelectorComponentListBox)
  144. };
  145. //==============================================================================
  146. struct AudioDeviceSetupDetails
  147. {
  148. AudioDeviceManager* manager;
  149. int minNumInputChannels, maxNumInputChannels;
  150. int minNumOutputChannels, maxNumOutputChannels;
  151. bool useStereoPairs;
  152. };
  153. static String getNoDeviceString() { return "<< " + TRANS("none") + " >>"; }
  154. //==============================================================================
  155. class AudioDeviceSettingsPanel : public Component,
  156. private ChangeListener
  157. {
  158. public:
  159. AudioDeviceSettingsPanel (AudioIODeviceType& t, AudioDeviceSetupDetails& setupDetails,
  160. const bool hideAdvancedOptionsWithButton)
  161. : type (t), setup (setupDetails)
  162. {
  163. if (hideAdvancedOptionsWithButton)
  164. {
  165. showAdvancedSettingsButton.reset (new TextButton (TRANS("Show advanced settings...")));
  166. addAndMakeVisible (showAdvancedSettingsButton.get());
  167. showAdvancedSettingsButton->setClickingTogglesState (true);
  168. showAdvancedSettingsButton->onClick = [this] { toggleAdvancedSettings(); };
  169. }
  170. type.scanForDevices();
  171. setup.manager->addChangeListener (this);
  172. }
  173. ~AudioDeviceSettingsPanel() override
  174. {
  175. setup.manager->removeChangeListener (this);
  176. }
  177. void resized() override
  178. {
  179. if (auto* parent = findParentComponentOfClass<AudioDeviceSelectorComponent>())
  180. {
  181. Rectangle<int> r (proportionOfWidth (0.35f), 0, proportionOfWidth (0.6f), 3000);
  182. const int maxListBoxHeight = 100;
  183. const int h = parent->getItemHeight();
  184. const int space = h / 4;
  185. if (outputDeviceDropDown != nullptr)
  186. {
  187. auto row = r.removeFromTop (h);
  188. if (testButton != nullptr)
  189. {
  190. testButton->changeWidthToFitText (h);
  191. testButton->setBounds (row.removeFromRight (testButton->getWidth()));
  192. row.removeFromRight (space);
  193. }
  194. outputDeviceDropDown->setBounds (row);
  195. r.removeFromTop (space);
  196. }
  197. if (inputDeviceDropDown != nullptr)
  198. {
  199. auto row = r.removeFromTop (h);
  200. inputLevelMeter->setBounds (row.removeFromRight (testButton != nullptr ? testButton->getWidth() : row.getWidth() / 6));
  201. row.removeFromRight (space);
  202. inputDeviceDropDown->setBounds (row);
  203. r.removeFromTop (space);
  204. }
  205. if (outputChanList != nullptr)
  206. {
  207. outputChanList->setRowHeight (jmin (22, h));
  208. outputChanList->setBounds (r.removeFromTop (outputChanList->getBestHeight (maxListBoxHeight)));
  209. outputChanLabel->setBounds (0, outputChanList->getBounds().getCentreY() - h / 2, r.getX(), h);
  210. r.removeFromTop (space);
  211. }
  212. if (inputChanList != nullptr)
  213. {
  214. inputChanList->setRowHeight (jmin (22, h));
  215. inputChanList->setBounds (r.removeFromTop (inputChanList->getBestHeight (maxListBoxHeight)));
  216. inputChanLabel->setBounds (0, inputChanList->getBounds().getCentreY() - h / 2, r.getX(), h);
  217. r.removeFromTop (space);
  218. }
  219. r.removeFromTop (space * 2);
  220. if (showAdvancedSettingsButton != nullptr
  221. && sampleRateDropDown != nullptr && bufferSizeDropDown != nullptr)
  222. {
  223. showAdvancedSettingsButton->setBounds (r.removeFromTop (h));
  224. r.removeFromTop (space);
  225. showAdvancedSettingsButton->changeWidthToFitText();
  226. }
  227. auto advancedSettingsVisible = showAdvancedSettingsButton == nullptr
  228. || showAdvancedSettingsButton->getToggleState();
  229. if (sampleRateDropDown != nullptr)
  230. {
  231. sampleRateDropDown->setVisible (advancedSettingsVisible);
  232. if (advancedSettingsVisible)
  233. {
  234. sampleRateDropDown->setBounds (r.removeFromTop (h));
  235. r.removeFromTop (space);
  236. }
  237. }
  238. if (bufferSizeDropDown != nullptr)
  239. {
  240. bufferSizeDropDown->setVisible (advancedSettingsVisible);
  241. if (advancedSettingsVisible)
  242. {
  243. bufferSizeDropDown->setBounds (r.removeFromTop (h));
  244. r.removeFromTop (space);
  245. }
  246. }
  247. r.removeFromTop (space);
  248. if (showUIButton != nullptr || resetDeviceButton != nullptr)
  249. {
  250. auto buttons = r.removeFromTop (h);
  251. if (showUIButton != nullptr)
  252. {
  253. showUIButton->setVisible (advancedSettingsVisible);
  254. showUIButton->changeWidthToFitText (h);
  255. showUIButton->setBounds (buttons.removeFromLeft (showUIButton->getWidth()));
  256. buttons.removeFromLeft (space);
  257. }
  258. if (resetDeviceButton != nullptr)
  259. {
  260. resetDeviceButton->setVisible (advancedSettingsVisible);
  261. resetDeviceButton->changeWidthToFitText (h);
  262. resetDeviceButton->setBounds (buttons.removeFromLeft (resetDeviceButton->getWidth()));
  263. }
  264. r.removeFromTop (space);
  265. }
  266. setSize (getWidth(), r.getY());
  267. }
  268. else
  269. {
  270. jassertfalse;
  271. }
  272. }
  273. void updateConfig (bool updateOutputDevice, bool updateInputDevice, bool updateSampleRate, bool updateBufferSize)
  274. {
  275. auto config = setup.manager->getAudioDeviceSetup();
  276. String error;
  277. if (updateOutputDevice || updateInputDevice)
  278. {
  279. if (outputDeviceDropDown != nullptr)
  280. config.outputDeviceName = outputDeviceDropDown->getSelectedId() < 0 ? String()
  281. : outputDeviceDropDown->getText();
  282. if (inputDeviceDropDown != nullptr)
  283. config.inputDeviceName = inputDeviceDropDown->getSelectedId() < 0 ? String()
  284. : inputDeviceDropDown->getText();
  285. if (! type.hasSeparateInputsAndOutputs())
  286. config.inputDeviceName = config.outputDeviceName;
  287. if (updateInputDevice)
  288. config.useDefaultInputChannels = true;
  289. else
  290. config.useDefaultOutputChannels = true;
  291. error = setup.manager->setAudioDeviceSetup (config, true);
  292. showCorrectDeviceName (inputDeviceDropDown.get(), true);
  293. showCorrectDeviceName (outputDeviceDropDown.get(), false);
  294. updateControlPanelButton();
  295. resized();
  296. }
  297. else if (updateSampleRate)
  298. {
  299. if (sampleRateDropDown->getSelectedId() > 0)
  300. {
  301. config.sampleRate = sampleRateDropDown->getSelectedId();
  302. error = setup.manager->setAudioDeviceSetup (config, true);
  303. }
  304. }
  305. else if (updateBufferSize)
  306. {
  307. if (bufferSizeDropDown->getSelectedId() > 0)
  308. {
  309. config.bufferSize = bufferSizeDropDown->getSelectedId();
  310. error = setup.manager->setAudioDeviceSetup (config, true);
  311. }
  312. }
  313. if (error.isNotEmpty())
  314. AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon,
  315. TRANS("Error when trying to open audio device!"),
  316. error);
  317. }
  318. bool showDeviceControlPanel()
  319. {
  320. if (auto* device = setup.manager->getCurrentAudioDevice())
  321. {
  322. Component modalWindow;
  323. modalWindow.setOpaque (true);
  324. modalWindow.addToDesktop (0);
  325. modalWindow.enterModalState();
  326. return device->showControlPanel();
  327. }
  328. return false;
  329. }
  330. void toggleAdvancedSettings()
  331. {
  332. showAdvancedSettingsButton->setButtonText ((showAdvancedSettingsButton->getToggleState() ? "Hide " : "Show ")
  333. + String ("advanced settings..."));
  334. resized();
  335. }
  336. void showDeviceUIPanel()
  337. {
  338. if (showDeviceControlPanel())
  339. {
  340. setup.manager->closeAudioDevice();
  341. setup.manager->restartLastAudioDevice();
  342. getTopLevelComponent()->toFront (true);
  343. }
  344. }
  345. void playTestSound()
  346. {
  347. setup.manager->playTestSound();
  348. }
  349. void updateAllControls()
  350. {
  351. updateOutputsComboBox();
  352. updateInputsComboBox();
  353. updateControlPanelButton();
  354. updateResetButton();
  355. if (auto* currentDevice = setup.manager->getCurrentAudioDevice())
  356. {
  357. if (setup.maxNumOutputChannels > 0
  358. && setup.minNumOutputChannels < setup.manager->getCurrentAudioDevice()->getOutputChannelNames().size())
  359. {
  360. if (outputChanList == nullptr)
  361. {
  362. outputChanList.reset (new ChannelSelectorListBox (setup, ChannelSelectorListBox::audioOutputType,
  363. TRANS ("(no audio output channels found)")));
  364. addAndMakeVisible (outputChanList.get());
  365. outputChanLabel.reset (new Label ({}, TRANS("Active output channels:")));
  366. outputChanLabel->setJustificationType (Justification::centredRight);
  367. outputChanLabel->attachToComponent (outputChanList.get(), true);
  368. }
  369. outputChanList->refresh();
  370. }
  371. else
  372. {
  373. outputChanLabel.reset();
  374. outputChanList.reset();
  375. }
  376. if (setup.maxNumInputChannels > 0
  377. && setup.minNumInputChannels < setup.manager->getCurrentAudioDevice()->getInputChannelNames().size())
  378. {
  379. if (inputChanList == nullptr)
  380. {
  381. inputChanList.reset (new ChannelSelectorListBox (setup, ChannelSelectorListBox::audioInputType,
  382. TRANS("(no audio input channels found)")));
  383. addAndMakeVisible (inputChanList.get());
  384. inputChanLabel.reset (new Label ({}, TRANS("Active input channels:")));
  385. inputChanLabel->setJustificationType (Justification::centredRight);
  386. inputChanLabel->attachToComponent (inputChanList.get(), true);
  387. }
  388. inputChanList->refresh();
  389. }
  390. else
  391. {
  392. inputChanLabel.reset();
  393. inputChanList.reset();
  394. }
  395. updateSampleRateComboBox (currentDevice);
  396. updateBufferSizeComboBox (currentDevice);
  397. }
  398. else
  399. {
  400. jassert (setup.manager->getCurrentAudioDevice() == nullptr); // not the correct device type!
  401. inputChanLabel.reset();
  402. outputChanLabel.reset();
  403. sampleRateLabel.reset();
  404. bufferSizeLabel.reset();
  405. inputChanList.reset();
  406. outputChanList.reset();
  407. sampleRateDropDown.reset();
  408. bufferSizeDropDown.reset();
  409. if (outputDeviceDropDown != nullptr)
  410. outputDeviceDropDown->setSelectedId (-1, dontSendNotification);
  411. if (inputDeviceDropDown != nullptr)
  412. inputDeviceDropDown->setSelectedId (-1, dontSendNotification);
  413. }
  414. sendLookAndFeelChange();
  415. resized();
  416. setSize (getWidth(), getLowestY() + 4);
  417. }
  418. void changeListenerCallback (ChangeBroadcaster*) override
  419. {
  420. updateAllControls();
  421. }
  422. void resetDevice()
  423. {
  424. setup.manager->closeAudioDevice();
  425. setup.manager->restartLastAudioDevice();
  426. }
  427. private:
  428. AudioIODeviceType& type;
  429. const AudioDeviceSetupDetails setup;
  430. std::unique_ptr<ComboBox> outputDeviceDropDown, inputDeviceDropDown, sampleRateDropDown, bufferSizeDropDown;
  431. std::unique_ptr<Label> outputDeviceLabel, inputDeviceLabel, sampleRateLabel, bufferSizeLabel, inputChanLabel, outputChanLabel;
  432. std::unique_ptr<TextButton> testButton;
  433. std::unique_ptr<Component> inputLevelMeter;
  434. std::unique_ptr<TextButton> showUIButton, showAdvancedSettingsButton, resetDeviceButton;
  435. void showCorrectDeviceName (ComboBox* box, bool isInput)
  436. {
  437. if (box != nullptr)
  438. {
  439. auto* currentDevice = setup.manager->getCurrentAudioDevice();
  440. auto index = type.getIndexOfDevice (currentDevice, isInput);
  441. box->setSelectedId (index < 0 ? index : index + 1, dontSendNotification);
  442. if (testButton != nullptr && ! isInput)
  443. testButton->setEnabled (index >= 0);
  444. }
  445. }
  446. void addNamesToDeviceBox (ComboBox& combo, bool isInputs)
  447. {
  448. const StringArray devs (type.getDeviceNames (isInputs));
  449. combo.clear (dontSendNotification);
  450. for (int i = 0; i < devs.size(); ++i)
  451. combo.addItem (devs[i], i + 1);
  452. combo.addItem (getNoDeviceString(), -1);
  453. combo.setSelectedId (-1, dontSendNotification);
  454. }
  455. int getLowestY() const
  456. {
  457. int y = 0;
  458. for (auto* c : getChildren())
  459. y = jmax (y, c->getBottom());
  460. return y;
  461. }
  462. void updateControlPanelButton()
  463. {
  464. auto* currentDevice = setup.manager->getCurrentAudioDevice();
  465. showUIButton.reset();
  466. if (currentDevice != nullptr && currentDevice->hasControlPanel())
  467. {
  468. showUIButton.reset (new TextButton (TRANS ("Control Panel"),
  469. TRANS ("Opens the device's own control panel")));
  470. addAndMakeVisible (showUIButton.get());
  471. showUIButton->onClick = [this] { showDeviceUIPanel(); };
  472. }
  473. resized();
  474. }
  475. void updateResetButton()
  476. {
  477. if (auto* currentDevice = setup.manager->getCurrentAudioDevice())
  478. {
  479. if (currentDevice->hasControlPanel())
  480. {
  481. if (resetDeviceButton == nullptr)
  482. {
  483. resetDeviceButton.reset (new TextButton (TRANS ("Reset Device"),
  484. TRANS ("Resets the audio interface - sometimes needed after changing a device's properties in its custom control panel")));
  485. addAndMakeVisible (resetDeviceButton.get());
  486. resetDeviceButton->onClick = [this] { resetDevice(); };
  487. resized();
  488. }
  489. return;
  490. }
  491. }
  492. resetDeviceButton.reset();
  493. }
  494. void updateOutputsComboBox()
  495. {
  496. if (setup.maxNumOutputChannels > 0 || ! type.hasSeparateInputsAndOutputs())
  497. {
  498. if (outputDeviceDropDown == nullptr)
  499. {
  500. outputDeviceDropDown.reset (new ComboBox());
  501. outputDeviceDropDown->onChange = [this] { updateConfig (true, false, false, false); };
  502. addAndMakeVisible (outputDeviceDropDown.get());
  503. outputDeviceLabel.reset (new Label ({}, type.hasSeparateInputsAndOutputs() ? TRANS("Output:")
  504. : TRANS("Device:")));
  505. outputDeviceLabel->attachToComponent (outputDeviceDropDown.get(), true);
  506. if (setup.maxNumOutputChannels > 0)
  507. {
  508. testButton.reset (new TextButton (TRANS("Test"), TRANS("Plays a test tone")));
  509. addAndMakeVisible (testButton.get());
  510. testButton->onClick = [this] { playTestSound(); };
  511. }
  512. }
  513. addNamesToDeviceBox (*outputDeviceDropDown, false);
  514. }
  515. showCorrectDeviceName (outputDeviceDropDown.get(), false);
  516. }
  517. void updateInputsComboBox()
  518. {
  519. if (setup.maxNumInputChannels > 0 && type.hasSeparateInputsAndOutputs())
  520. {
  521. if (inputDeviceDropDown == nullptr)
  522. {
  523. inputDeviceDropDown.reset (new ComboBox());
  524. inputDeviceDropDown->onChange = [this] { updateConfig (false, true, false, false); };
  525. addAndMakeVisible (inputDeviceDropDown.get());
  526. inputDeviceLabel.reset (new Label ({}, TRANS("Input:")));
  527. inputDeviceLabel->attachToComponent (inputDeviceDropDown.get(), true);
  528. inputLevelMeter.reset (new SimpleDeviceManagerInputLevelMeter (*setup.manager));
  529. addAndMakeVisible (inputLevelMeter.get());
  530. }
  531. addNamesToDeviceBox (*inputDeviceDropDown, true);
  532. }
  533. showCorrectDeviceName (inputDeviceDropDown.get(), true);
  534. }
  535. void updateSampleRateComboBox (AudioIODevice* currentDevice)
  536. {
  537. if (sampleRateDropDown == nullptr)
  538. {
  539. sampleRateDropDown.reset (new ComboBox());
  540. addAndMakeVisible (sampleRateDropDown.get());
  541. sampleRateLabel.reset (new Label ({}, TRANS("Sample rate:")));
  542. sampleRateLabel->attachToComponent (sampleRateDropDown.get(), true);
  543. }
  544. else
  545. {
  546. sampleRateDropDown->clear();
  547. sampleRateDropDown->onChange = nullptr;
  548. }
  549. for (auto rate : currentDevice->getAvailableSampleRates())
  550. {
  551. auto intRate = roundToInt (rate);
  552. sampleRateDropDown->addItem (String (intRate) + " Hz", intRate);
  553. }
  554. sampleRateDropDown->setSelectedId (roundToInt (currentDevice->getCurrentSampleRate()), dontSendNotification);
  555. sampleRateDropDown->onChange = [this] { updateConfig (false, false, true, false); };
  556. }
  557. void updateBufferSizeComboBox (AudioIODevice* currentDevice)
  558. {
  559. if (bufferSizeDropDown == nullptr)
  560. {
  561. bufferSizeDropDown.reset (new ComboBox());
  562. addAndMakeVisible (bufferSizeDropDown.get());
  563. bufferSizeLabel.reset (new Label ({}, TRANS("Audio buffer size:")));
  564. bufferSizeLabel->attachToComponent (bufferSizeDropDown.get(), true);
  565. }
  566. else
  567. {
  568. bufferSizeDropDown->clear();
  569. bufferSizeDropDown->onChange = nullptr;
  570. }
  571. auto currentRate = currentDevice->getCurrentSampleRate();
  572. if (currentRate == 0)
  573. currentRate = 48000.0;
  574. for (auto bs : currentDevice->getAvailableBufferSizes())
  575. bufferSizeDropDown->addItem (String (bs) + " samples (" + String (bs * 1000.0 / currentRate, 1) + " ms)", bs);
  576. bufferSizeDropDown->setSelectedId (currentDevice->getCurrentBufferSizeSamples(), dontSendNotification);
  577. bufferSizeDropDown->onChange = [this] { updateConfig (false, false, false, true); };
  578. }
  579. public:
  580. //==============================================================================
  581. class ChannelSelectorListBox : public ListBox,
  582. private ListBoxModel
  583. {
  584. public:
  585. enum BoxType
  586. {
  587. audioInputType,
  588. audioOutputType
  589. };
  590. //==============================================================================
  591. ChannelSelectorListBox (const AudioDeviceSetupDetails& setupDetails, BoxType boxType, const String& noItemsText)
  592. : ListBox ({}, nullptr), setup (setupDetails), type (boxType), noItemsMessage (noItemsText)
  593. {
  594. refresh();
  595. setModel (this);
  596. setOutlineThickness (1);
  597. }
  598. void refresh()
  599. {
  600. items.clear();
  601. if (auto* currentDevice = setup.manager->getCurrentAudioDevice())
  602. {
  603. if (type == audioInputType)
  604. items = currentDevice->getInputChannelNames();
  605. else if (type == audioOutputType)
  606. items = currentDevice->getOutputChannelNames();
  607. if (setup.useStereoPairs)
  608. {
  609. StringArray pairs;
  610. for (int i = 0; i < items.size(); i += 2)
  611. {
  612. auto& name = items[i];
  613. if (i + 1 >= items.size())
  614. pairs.add (name.trim());
  615. else
  616. pairs.add (getNameForChannelPair (name, items[i + 1]));
  617. }
  618. items = pairs;
  619. }
  620. }
  621. updateContent();
  622. repaint();
  623. }
  624. int getNumRows() override
  625. {
  626. return items.size();
  627. }
  628. void paintListBoxItem (int row, Graphics& g, int width, int height, bool) override
  629. {
  630. if (isPositiveAndBelow (row, items.size()))
  631. {
  632. g.fillAll (findColour (ListBox::backgroundColourId));
  633. auto item = items[row];
  634. bool enabled = false;
  635. auto config = setup.manager->getAudioDeviceSetup();
  636. if (setup.useStereoPairs)
  637. {
  638. if (type == audioInputType)
  639. enabled = config.inputChannels[row * 2] || config.inputChannels[row * 2 + 1];
  640. else if (type == audioOutputType)
  641. enabled = config.outputChannels[row * 2] || config.outputChannels[row * 2 + 1];
  642. }
  643. else
  644. {
  645. if (type == audioInputType)
  646. enabled = config.inputChannels[row];
  647. else if (type == audioOutputType)
  648. enabled = config.outputChannels[row];
  649. }
  650. auto x = getTickX();
  651. auto tickW = height * 0.75f;
  652. getLookAndFeel().drawTickBox (g, *this, x - tickW, (height - tickW) / 2, tickW, tickW,
  653. enabled, true, true, false);
  654. g.setFont (height * 0.6f);
  655. g.setColour (findColour (ListBox::textColourId, true).withMultipliedAlpha (enabled ? 1.0f : 0.6f));
  656. g.drawText (item, x + 5, 0, width - x - 5, height, Justification::centredLeft, true);
  657. }
  658. }
  659. void listBoxItemClicked (int row, const MouseEvent& e) override
  660. {
  661. selectRow (row);
  662. if (e.x < getTickX())
  663. flipEnablement (row);
  664. }
  665. void listBoxItemDoubleClicked (int row, const MouseEvent&) override
  666. {
  667. flipEnablement (row);
  668. }
  669. void returnKeyPressed (int row) override
  670. {
  671. flipEnablement (row);
  672. }
  673. void paint (Graphics& g) override
  674. {
  675. ListBox::paint (g);
  676. if (items.isEmpty())
  677. {
  678. g.setColour (Colours::grey);
  679. g.setFont (0.5f * getRowHeight());
  680. g.drawText (noItemsMessage,
  681. 0, 0, getWidth(), getHeight() / 2,
  682. Justification::centred, true);
  683. }
  684. }
  685. int getBestHeight (int maxHeight)
  686. {
  687. return getRowHeight() * jlimit (2, jmax (2, maxHeight / getRowHeight()),
  688. getNumRows())
  689. + getOutlineThickness() * 2;
  690. }
  691. private:
  692. //==============================================================================
  693. const AudioDeviceSetupDetails setup;
  694. const BoxType type;
  695. const String noItemsMessage;
  696. StringArray items;
  697. static String getNameForChannelPair (const String& name1, const String& name2)
  698. {
  699. String commonBit;
  700. for (int j = 0; j < name1.length(); ++j)
  701. if (name1.substring (0, j).equalsIgnoreCase (name2.substring (0, j)))
  702. commonBit = name1.substring (0, j);
  703. // Make sure we only split the name at a space, because otherwise, things
  704. // like "input 11" + "input 12" would become "input 11 + 2"
  705. while (commonBit.isNotEmpty() && ! CharacterFunctions::isWhitespace (commonBit.getLastCharacter()))
  706. commonBit = commonBit.dropLastCharacters (1);
  707. return name1.trim() + " + " + name2.substring (commonBit.length()).trim();
  708. }
  709. void flipEnablement (int row)
  710. {
  711. jassert (type == audioInputType || type == audioOutputType);
  712. if (isPositiveAndBelow (row, items.size()))
  713. {
  714. auto config = setup.manager->getAudioDeviceSetup();
  715. if (setup.useStereoPairs)
  716. {
  717. BigInteger bits;
  718. auto& original = (type == audioInputType ? config.inputChannels
  719. : config.outputChannels);
  720. for (int i = 0; i < 256; i += 2)
  721. bits.setBit (i / 2, original[i] || original[i + 1]);
  722. if (type == audioInputType)
  723. {
  724. config.useDefaultInputChannels = false;
  725. flipBit (bits, row, setup.minNumInputChannels / 2, setup.maxNumInputChannels / 2);
  726. }
  727. else
  728. {
  729. config.useDefaultOutputChannels = false;
  730. flipBit (bits, row, setup.minNumOutputChannels / 2, setup.maxNumOutputChannels / 2);
  731. }
  732. for (int i = 0; i < 256; ++i)
  733. original.setBit (i, bits[i / 2]);
  734. }
  735. else
  736. {
  737. if (type == audioInputType)
  738. {
  739. config.useDefaultInputChannels = false;
  740. flipBit (config.inputChannels, row, setup.minNumInputChannels, setup.maxNumInputChannels);
  741. }
  742. else
  743. {
  744. config.useDefaultOutputChannels = false;
  745. flipBit (config.outputChannels, row, setup.minNumOutputChannels, setup.maxNumOutputChannels);
  746. }
  747. }
  748. setup.manager->setAudioDeviceSetup (config, true);
  749. }
  750. }
  751. static void flipBit (BigInteger& chans, int index, int minNumber, int maxNumber)
  752. {
  753. auto numActive = chans.countNumberOfSetBits();
  754. if (chans[index])
  755. {
  756. if (numActive > minNumber)
  757. chans.setBit (index, false);
  758. }
  759. else
  760. {
  761. if (numActive >= maxNumber)
  762. {
  763. auto firstActiveChan = chans.findNextSetBit (0);
  764. chans.clearBit (index > firstActiveChan ? firstActiveChan : chans.getHighestBit());
  765. }
  766. chans.setBit (index, true);
  767. }
  768. }
  769. int getTickX() const
  770. {
  771. return getRowHeight();
  772. }
  773. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ChannelSelectorListBox)
  774. };
  775. private:
  776. std::unique_ptr<ChannelSelectorListBox> inputChanList, outputChanList;
  777. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioDeviceSettingsPanel)
  778. };
  779. //==============================================================================
  780. AudioDeviceSelectorComponent::AudioDeviceSelectorComponent (AudioDeviceManager& dm,
  781. int minInputChannelsToUse,
  782. int maxInputChannelsToUse,
  783. int minOutputChannelsToUse,
  784. int maxOutputChannelsToUse,
  785. bool showMidiInputOptions,
  786. bool showMidiOutputSelector,
  787. bool showChannelsAsStereoPairsToUse,
  788. bool hideAdvancedOptionsWithButtonToUse)
  789. : deviceManager (dm),
  790. itemHeight (24),
  791. minOutputChannels (minOutputChannelsToUse),
  792. maxOutputChannels (maxOutputChannelsToUse),
  793. minInputChannels (minInputChannelsToUse),
  794. maxInputChannels (maxInputChannelsToUse),
  795. showChannelsAsStereoPairs (showChannelsAsStereoPairsToUse),
  796. hideAdvancedOptionsWithButton (hideAdvancedOptionsWithButtonToUse)
  797. {
  798. jassert (minOutputChannels >= 0 && minOutputChannels <= maxOutputChannels);
  799. jassert (minInputChannels >= 0 && minInputChannels <= maxInputChannels);
  800. const OwnedArray<AudioIODeviceType>& types = deviceManager.getAvailableDeviceTypes();
  801. if (types.size() > 1)
  802. {
  803. deviceTypeDropDown.reset (new ComboBox());
  804. for (int i = 0; i < types.size(); ++i)
  805. deviceTypeDropDown->addItem (types.getUnchecked(i)->getTypeName(), i + 1);
  806. addAndMakeVisible (deviceTypeDropDown.get());
  807. deviceTypeDropDown->onChange = [this] { updateDeviceType(); };
  808. deviceTypeDropDownLabel.reset (new Label ({}, TRANS("Audio device type:")));
  809. deviceTypeDropDownLabel->setJustificationType (Justification::centredRight);
  810. deviceTypeDropDownLabel->attachToComponent (deviceTypeDropDown.get(), true);
  811. }
  812. if (showMidiInputOptions)
  813. {
  814. midiInputsList.reset (new MidiInputSelectorComponentListBox (deviceManager,
  815. "(" + TRANS("No MIDI inputs available") + ")"));
  816. addAndMakeVisible (midiInputsList.get());
  817. midiInputsLabel.reset (new Label ({}, TRANS ("Active MIDI inputs:")));
  818. midiInputsLabel->setJustificationType (Justification::topRight);
  819. midiInputsLabel->attachToComponent (midiInputsList.get(), true);
  820. if (BluetoothMidiDevicePairingDialogue::isAvailable())
  821. {
  822. bluetoothButton.reset (new TextButton (TRANS("Bluetooth MIDI"), TRANS("Scan for bluetooth MIDI devices")));
  823. addAndMakeVisible (bluetoothButton.get());
  824. bluetoothButton->onClick = [this] { handleBluetoothButton(); };
  825. }
  826. }
  827. else
  828. {
  829. midiInputsList.reset();
  830. midiInputsLabel.reset();
  831. bluetoothButton.reset();
  832. }
  833. if (showMidiOutputSelector)
  834. {
  835. midiOutputSelector.reset (new ComboBox());
  836. addAndMakeVisible (midiOutputSelector.get());
  837. midiOutputSelector->onChange = [this] { updateMidiOutput(); };
  838. midiOutputLabel.reset (new Label ("lm", TRANS("MIDI Output:")));
  839. midiOutputLabel->attachToComponent (midiOutputSelector.get(), true);
  840. }
  841. else
  842. {
  843. midiOutputSelector.reset();
  844. midiOutputLabel.reset();
  845. }
  846. deviceManager.addChangeListener (this);
  847. updateAllControls();
  848. startTimer (1000);
  849. }
  850. AudioDeviceSelectorComponent::~AudioDeviceSelectorComponent()
  851. {
  852. deviceManager.removeChangeListener (this);
  853. }
  854. void AudioDeviceSelectorComponent::setItemHeight (int newItemHeight)
  855. {
  856. itemHeight = newItemHeight;
  857. resized();
  858. }
  859. void AudioDeviceSelectorComponent::resized()
  860. {
  861. Rectangle<int> r (proportionOfWidth (0.35f), 15, proportionOfWidth (0.6f), 3000);
  862. auto space = itemHeight / 4;
  863. if (deviceTypeDropDown != nullptr)
  864. {
  865. deviceTypeDropDown->setBounds (r.removeFromTop (itemHeight));
  866. r.removeFromTop (space * 3);
  867. }
  868. if (audioDeviceSettingsComp != nullptr)
  869. {
  870. audioDeviceSettingsComp->resized();
  871. audioDeviceSettingsComp->setBounds (r.removeFromTop (audioDeviceSettingsComp->getHeight())
  872. .withX (0).withWidth (getWidth()));
  873. r.removeFromTop (space);
  874. }
  875. if (midiInputsList != nullptr)
  876. {
  877. midiInputsList->setRowHeight (jmin (22, itemHeight));
  878. midiInputsList->setBounds (r.removeFromTop (midiInputsList->getBestHeight (jmin (itemHeight * 8,
  879. getHeight() - r.getY() - space - itemHeight))));
  880. r.removeFromTop (space);
  881. }
  882. if (bluetoothButton != nullptr)
  883. {
  884. bluetoothButton->setBounds (r.removeFromTop (24));
  885. r.removeFromTop (space);
  886. }
  887. if (midiOutputSelector != nullptr)
  888. midiOutputSelector->setBounds (r.removeFromTop (itemHeight));
  889. r.removeFromTop (itemHeight);
  890. setSize (getWidth(), r.getY());
  891. }
  892. void AudioDeviceSelectorComponent::timerCallback()
  893. {
  894. // TODO
  895. // unfortunately, the AudioDeviceManager only gives us changeListenerCallbacks
  896. // if an audio device has changed, but not if a MIDI device has changed.
  897. // This needs to be implemented properly. Until then, we use a workaround
  898. // where we update the whole component once per second on a timer callback.
  899. updateAllControls();
  900. }
  901. void AudioDeviceSelectorComponent::updateDeviceType()
  902. {
  903. if (auto* type = deviceManager.getAvailableDeviceTypes() [deviceTypeDropDown->getSelectedId() - 1])
  904. {
  905. audioDeviceSettingsComp.reset();
  906. deviceManager.setCurrentAudioDeviceType (type->getTypeName(), true);
  907. updateAllControls(); // needed in case the type hasn't actually changed
  908. }
  909. }
  910. void AudioDeviceSelectorComponent::updateMidiOutput()
  911. {
  912. auto selectedId = midiOutputSelector->getSelectedId();
  913. if (selectedId == -1)
  914. deviceManager.setDefaultMidiOutputDevice ({});
  915. else
  916. deviceManager.setDefaultMidiOutputDevice (currentMidiOutputs[selectedId - 1].identifier);
  917. }
  918. void AudioDeviceSelectorComponent::changeListenerCallback (ChangeBroadcaster*)
  919. {
  920. updateAllControls();
  921. }
  922. void AudioDeviceSelectorComponent::updateAllControls()
  923. {
  924. if (deviceTypeDropDown != nullptr)
  925. deviceTypeDropDown->setText (deviceManager.getCurrentAudioDeviceType(), dontSendNotification);
  926. if (audioDeviceSettingsComp == nullptr
  927. || audioDeviceSettingsCompType != deviceManager.getCurrentAudioDeviceType())
  928. {
  929. audioDeviceSettingsCompType = deviceManager.getCurrentAudioDeviceType();
  930. audioDeviceSettingsComp.reset();
  931. if (auto* type = deviceManager.getAvailableDeviceTypes() [deviceTypeDropDown == nullptr
  932. ? 0 : deviceTypeDropDown->getSelectedId() - 1])
  933. {
  934. AudioDeviceSetupDetails details;
  935. details.manager = &deviceManager;
  936. details.minNumInputChannels = minInputChannels;
  937. details.maxNumInputChannels = maxInputChannels;
  938. details.minNumOutputChannels = minOutputChannels;
  939. details.maxNumOutputChannels = maxOutputChannels;
  940. details.useStereoPairs = showChannelsAsStereoPairs;
  941. auto* sp = new AudioDeviceSettingsPanel (*type, details, hideAdvancedOptionsWithButton);
  942. audioDeviceSettingsComp.reset (sp);
  943. addAndMakeVisible (sp);
  944. sp->updateAllControls();
  945. }
  946. }
  947. if (midiInputsList != nullptr)
  948. {
  949. midiInputsList->updateDevices();
  950. midiInputsList->updateContent();
  951. midiInputsList->repaint();
  952. }
  953. if (midiOutputSelector != nullptr)
  954. {
  955. midiOutputSelector->clear();
  956. currentMidiOutputs = MidiOutput::getAvailableDevices();
  957. midiOutputSelector->addItem (getNoDeviceString(), -1);
  958. midiOutputSelector->addSeparator();
  959. auto defaultOutputIdentifier = deviceManager.getDefaultMidiOutputIdentifier();
  960. int i = 0;
  961. for (auto& out : currentMidiOutputs)
  962. {
  963. midiOutputSelector->addItem (out.name, i + 1);
  964. if (defaultOutputIdentifier.isNotEmpty() && out.identifier == defaultOutputIdentifier)
  965. midiOutputSelector->setSelectedId (i + 1);
  966. ++i;
  967. }
  968. }
  969. resized();
  970. }
  971. void AudioDeviceSelectorComponent::handleBluetoothButton()
  972. {
  973. if (! RuntimePermissions::isGranted (RuntimePermissions::bluetoothMidi))
  974. RuntimePermissions::request (RuntimePermissions::bluetoothMidi, nullptr);
  975. if (RuntimePermissions::isGranted (RuntimePermissions::bluetoothMidi))
  976. BluetoothMidiDevicePairingDialogue::open();
  977. }
  978. ListBox* AudioDeviceSelectorComponent::getMidiInputSelectorListBox() const noexcept
  979. {
  980. return midiInputsList.get();
  981. }
  982. } // namespace juce