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.

1202 lines
43KB

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