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.

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