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.

1190 lines
42KB

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