/* ============================================================================== This file is part of the JUCE framework. Copyright (c) Raw Material Software Limited JUCE is an open source framework subject to commercial or open source licensing. By downloading, installing, or using the JUCE framework, or combining the JUCE framework with any other source code, object code, content or any other copyrightable work, you agree to the terms of the JUCE End User Licence Agreement, and all incorporated terms including the JUCE Privacy Policy and the JUCE Website Terms of Service, as applicable, which will bind you. If you do not agree to the terms of these agreements, we will not license the JUCE framework to you, and you must discontinue the installation or download process and cease use of the JUCE framework. JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ JUCE Privacy Policy: https://juce.com/juce-privacy-policy JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ Or: You may also use this code under the terms of the AGPLv3: https://www.gnu.org/licenses/agpl-3.0.en.html THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce { struct SimpleDeviceManagerInputLevelMeter final : public Component, public Timer { SimpleDeviceManagerInputLevelMeter (AudioDeviceManager& m) : manager (m) { startTimerHz (20); inputLevelGetter = manager.getInputLevelGetter(); } void timerCallback() override { if (isShowing()) { auto newLevel = (float) inputLevelGetter->getCurrentLevel(); if (std::abs (level - newLevel) > 0.005f) { level = newLevel; repaint(); } } else { level = 0; } } void paint (Graphics& g) override { // (add a bit of a skew to make the level more obvious) getLookAndFeel().drawLevelMeter (g, getWidth(), getHeight(), (float) std::exp (std::log (level) / 3.0)); } AudioDeviceManager& manager; AudioDeviceManager::LevelMeter::Ptr inputLevelGetter; float level = 0; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SimpleDeviceManagerInputLevelMeter) }; static void drawTextLayout (Graphics& g, Component& owner, StringRef text, const Rectangle& textBounds, bool enabled) { const auto textColour = owner.findColour (ListBox::textColourId, true).withMultipliedAlpha (enabled ? 1.0f : 0.6f); AttributedString attributedString { text }; attributedString.setColour (textColour); attributedString.setFont (owner.withDefaultMetrics (FontOptions { (float) textBounds.getHeight() * 0.6f })); attributedString.setJustification (Justification::centredLeft); attributedString.setWordWrap (AttributedString::WordWrap::none); TextLayout textLayout; textLayout.createLayout (attributedString, (float) textBounds.getWidth(), (float) textBounds.getHeight()); textLayout.draw (g, textBounds.toFloat()); } //============================================================================== class AudioDeviceSelectorComponent::MidiInputSelectorComponentListBox final : public ListBox, private ListBoxModel { public: MidiInputSelectorComponentListBox (AudioDeviceManager& dm, const String& noItems) : ListBox ({}, nullptr), deviceManager (dm), noItemsMessage (noItems) { updateDevices(); setModel (this); setOutlineThickness (1); } void updateDevices() { items = MidiInput::getAvailableDevices(); } int getNumRows() override { return items.size(); } void paintListBoxItem (int row, Graphics& g, int width, int height, bool rowIsSelected) override { if (isPositiveAndBelow (row, items.size())) { if (rowIsSelected) g.fillAll (findColour (TextEditor::highlightColourId) .withMultipliedAlpha (0.3f)); auto item = items[row]; bool enabled = deviceManager.isMidiInputDeviceEnabled (item.identifier); auto x = getTickX(); auto tickW = (float) height * 0.75f; getLookAndFeel().drawTickBox (g, *this, (float) x - tickW, ((float) height - tickW) * 0.5f, tickW, tickW, enabled, true, true, false); drawTextLayout (g, *this, item.name, { x + 5, 0, width - x - 5, height }, enabled); } } void listBoxItemClicked (int row, const MouseEvent& e) override { selectRow (row); if (e.x < getTickX()) flipEnablement (row); } void listBoxItemDoubleClicked (int row, const MouseEvent&) override { flipEnablement (row); } void returnKeyPressed (int row) override { flipEnablement (row); } void paint (Graphics& g) override { ListBox::paint (g); if (items.isEmpty()) { g.setColour (Colours::grey); g.setFont (0.5f * (float) getRowHeight()); g.drawText (noItemsMessage, 0, 0, getWidth(), getHeight() / 2, Justification::centred, true); } } int getBestHeight (int preferredHeight) { auto extra = getOutlineThickness() * 2; return jmax (getRowHeight() * 2 + extra, jmin (getRowHeight() * getNumRows() + extra, preferredHeight)); } private: //============================================================================== AudioDeviceManager& deviceManager; const String noItemsMessage; Array items; void flipEnablement (const int row) { if (isPositiveAndBelow (row, items.size())) { auto identifier = items[row].identifier; deviceManager.setMidiInputDeviceEnabled (identifier, ! deviceManager.isMidiInputDeviceEnabled (identifier)); } } int getTickX() const { return getRowHeight(); } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiInputSelectorComponentListBox) }; //============================================================================== struct AudioDeviceSetupDetails { AudioDeviceManager* manager; int minNumInputChannels, maxNumInputChannels; int minNumOutputChannels, maxNumOutputChannels; bool useStereoPairs; }; static String getNoDeviceString() { return "<< " + TRANS ("none") + " >>"; } //============================================================================== class AudioDeviceSelectorComponent::MidiOutputSelector final : public Component, private ChangeListener { public: explicit MidiOutputSelector (AudioDeviceManager& dm) : deviceManager (dm) { deviceManager.addChangeListener (this); selector.onChange = [&] { const auto selectedId = selector.getSelectedId(); jassert (selectedId != 0); const auto deviceId = selectedId == -1 ? String{} : MidiOutput::getAvailableDevices()[selectedId - 1].identifier; deviceManager.setDefaultMidiOutputDevice (deviceId); }; updateListOfDevices(); addAndMakeVisible (selector); } ~MidiOutputSelector() final { deviceManager.removeChangeListener (this); } void resized() final { selector.setBounds (getLocalBounds()); } private: void updateListOfDevices() { selector.clear(); const auto midiOutputs = MidiOutput::getAvailableDevices(); selector.addItem (getNoDeviceString(), -1); selector.setSelectedId (-1, dontSendNotification); selector.addSeparator(); for (auto [id, midiOutput] : enumerate (midiOutputs, 1)) { selector.addItem (midiOutput.name, id); if (midiOutput.identifier == deviceManager.getDefaultMidiOutputIdentifier()) selector.setSelectedId (id, dontSendNotification); } } void changeListenerCallback (ChangeBroadcaster*) final { updateListOfDevices(); } ComboBox selector; AudioDeviceManager& deviceManager; }; //============================================================================== class AudioDeviceSettingsPanel : public Component, private ChangeListener { public: AudioDeviceSettingsPanel (AudioIODeviceType& t, AudioDeviceSetupDetails& setupDetails, const bool hideAdvancedOptionsWithButton, AudioDeviceSelectorComponent& p) : type (t), setup (setupDetails), parent (p) { if (hideAdvancedOptionsWithButton) { showAdvancedSettingsButton = std::make_unique (TRANS ("Show advanced settings...")); addAndMakeVisible (showAdvancedSettingsButton.get()); showAdvancedSettingsButton->setClickingTogglesState (true); showAdvancedSettingsButton->onClick = [this] { toggleAdvancedSettings(); }; } type.scanForDevices(); setup.manager->addChangeListener (this); updateAllControls(); } ~AudioDeviceSettingsPanel() override { setup.manager->removeChangeListener (this); } void resized() override { Rectangle r (proportionOfWidth (0.35f), 0, proportionOfWidth (0.6f), 3000); const int maxListBoxHeight = 100; const int h = parent.getItemHeight(); const int space = h / 4; if (outputDeviceDropDown != nullptr) { auto row = r.removeFromTop (h); if (testButton != nullptr) { testButton->changeWidthToFitText (h); testButton->setBounds (row.removeFromRight (testButton->getWidth())); row.removeFromRight (space); } outputDeviceDropDown->setBounds (row); r.removeFromTop (space); } if (inputDeviceDropDown != nullptr) { auto row = r.removeFromTop (h); inputLevelMeter->setBounds (row.removeFromRight (testButton != nullptr ? testButton->getWidth() : row.getWidth() / 6)); row.removeFromRight (space); inputDeviceDropDown->setBounds (row); r.removeFromTop (space); } if (outputChanList != nullptr) { outputChanList->setRowHeight (jmin (22, h)); outputChanList->setBounds (r.removeFromTop (outputChanList->getBestHeight (maxListBoxHeight))); outputChanLabel->setBounds (0, outputChanList->getBounds().getCentreY() - h / 2, r.getX(), h); r.removeFromTop (space); } if (inputChanList != nullptr) { inputChanList->setRowHeight (jmin (22, h)); inputChanList->setBounds (r.removeFromTop (inputChanList->getBestHeight (maxListBoxHeight))); inputChanLabel->setBounds (0, inputChanList->getBounds().getCentreY() - h / 2, r.getX(), h); r.removeFromTop (space); } r.removeFromTop (space * 2); if (showAdvancedSettingsButton != nullptr && sampleRateDropDown != nullptr && bufferSizeDropDown != nullptr) { showAdvancedSettingsButton->setBounds (r.removeFromTop (h)); r.removeFromTop (space); showAdvancedSettingsButton->changeWidthToFitText(); } auto advancedSettingsVisible = showAdvancedSettingsButton == nullptr || showAdvancedSettingsButton->getToggleState(); if (sampleRateDropDown != nullptr) { sampleRateDropDown->setVisible (advancedSettingsVisible); if (advancedSettingsVisible) { sampleRateDropDown->setBounds (r.removeFromTop (h)); r.removeFromTop (space); } } if (bufferSizeDropDown != nullptr) { bufferSizeDropDown->setVisible (advancedSettingsVisible); if (advancedSettingsVisible) { bufferSizeDropDown->setBounds (r.removeFromTop (h)); r.removeFromTop (space); } } r.removeFromTop (space); if (showUIButton != nullptr || resetDeviceButton != nullptr) { auto buttons = r.removeFromTop (h); if (showUIButton != nullptr) { showUIButton->setVisible (advancedSettingsVisible); showUIButton->changeWidthToFitText (h); showUIButton->setBounds (buttons.removeFromLeft (showUIButton->getWidth())); buttons.removeFromLeft (space); } if (resetDeviceButton != nullptr) { resetDeviceButton->setVisible (advancedSettingsVisible); resetDeviceButton->changeWidthToFitText (h); resetDeviceButton->setBounds (buttons.removeFromLeft (resetDeviceButton->getWidth())); } r.removeFromTop (space); } setSize (getWidth(), r.getY()); } void updateConfig (bool updateOutputDevice, bool updateInputDevice, bool updateSampleRate, bool updateBufferSize) { auto config = setup.manager->getAudioDeviceSetup(); String error; if (updateOutputDevice || updateInputDevice) { if (outputDeviceDropDown != nullptr) config.outputDeviceName = outputDeviceDropDown->getSelectedId() < 0 ? String() : outputDeviceDropDown->getText(); if (inputDeviceDropDown != nullptr) config.inputDeviceName = inputDeviceDropDown->getSelectedId() < 0 ? String() : inputDeviceDropDown->getText(); if (! type.hasSeparateInputsAndOutputs()) config.inputDeviceName = config.outputDeviceName; if (updateInputDevice) config.useDefaultInputChannels = true; else config.useDefaultOutputChannels = true; error = setup.manager->setAudioDeviceSetup (config, true); updateSelectedInput(); updateSelectedOutput(); updateControlPanelButton(); resized(); } else if (updateSampleRate) { if (sampleRateDropDown->getSelectedId() > 0) { config.sampleRate = sampleRateDropDown->getSelectedId(); error = setup.manager->setAudioDeviceSetup (config, true); } } else if (updateBufferSize) { if (bufferSizeDropDown->getSelectedId() > 0) { config.bufferSize = bufferSizeDropDown->getSelectedId(); error = setup.manager->setAudioDeviceSetup (config, true); } } if (error.isNotEmpty()) messageBox = AlertWindow::showScopedAsync (MessageBoxOptions().withIconType (MessageBoxIconType::WarningIcon) .withTitle (TRANS ("Error when trying to open audio device!")) .withMessage (error) .withButton (TRANS ("OK")), nullptr); } bool showDeviceControlPanel() { if (auto* device = setup.manager->getCurrentAudioDevice()) { Component modalWindow; modalWindow.setOpaque (true); modalWindow.addToDesktop (0); modalWindow.enterModalState(); return device->showControlPanel(); } return false; } void toggleAdvancedSettings() { showAdvancedSettingsButton->setButtonText ((showAdvancedSettingsButton->getToggleState() ? "Hide " : "Show ") + String ("advanced settings...")); resized(); } void showDeviceUIPanel() { if (showDeviceControlPanel()) { setup.manager->closeAudioDevice(); setup.manager->restartLastAudioDevice(); getTopLevelComponent()->toFront (true); } } void playTestSound() { setup.manager->playTestSound(); } void updateAllControls() { updateOutputsComboBox(); updateInputsComboBox(); updateControlPanelButton(); updateResetButton(); if (auto* currentDevice = setup.manager->getCurrentAudioDevice()) { if (setup.maxNumOutputChannels > 0 && setup.minNumOutputChannels < setup.manager->getCurrentAudioDevice()->getOutputChannelNames().size()) { if (outputChanList == nullptr) { outputChanList = std::make_unique (setup, ChannelSelectorListBox::audioOutputType, TRANS ("(no audio output channels found)")); addAndMakeVisible (outputChanList.get()); outputChanLabel = std::make_unique