justasample/Source/Components/InputDeviceSelector.cpp

352 lines
13 KiB
C++
Raw Normal View History

2026-07-23 23:40:48 +02:00
/*
==============================================================================
InputDeviceSelector.cpp
Created: 18 Aug 2024 8:20:19pm
Author: binya
==============================================================================
*/
#include "InputDeviceSelector.h"
ChannelSelectorListBox::ChannelSelectorListBox(juce::AudioDeviceManager& audioDeviceManager, int minNumInputChannels, int maxNumInputChannels, bool useStereoPairs)
: manager(audioDeviceManager), minInputChannels(minNumInputChannels), maxInputChannels(maxNumInputChannels), useStereoPairs(useStereoPairs)
{
setModel(this);
audioDeviceManager.addChangeListener(this);
changeListenerCallback(nullptr);
}
ChannelSelectorListBox::~ChannelSelectorListBox()
{
manager.removeChangeListener(this);
}
void ChannelSelectorListBox::changeListenerCallback(juce::ChangeBroadcaster*)
{
repaint();
auto* currentDevice = manager.getCurrentAudioDevice();
if (device == currentDevice)
return;
channels.clear();
if (!currentDevice)
return;
channels = currentDevice->getInputChannelNames();
device = currentDevice;
if (useStereoPairs)
{
juce::StringArray pairs;
int numPairs = channels.size() / 2;
for (int i = 0; i < numPairs; i++)
pairs.add(getNameForChannelPair(channels[i * 2], channels[i * 2 + 1]));
// Interestingly, StringArray->end() caused an issue on Mac
if (channels.size() % 2)
pairs.add(channels[channels.size() - 1].trim());
channels = pairs;
}
updateContent();
}
int ChannelSelectorListBox::getNumRows()
{
return channels.size();
}
void ChannelSelectorListBox::listBoxItemClicked(int row, const juce::MouseEvent& /*e*/)
{
if (row < channels.size())
flipEnablement(row);
}
void ChannelSelectorListBox::paintListBoxItem(int row, juce::Graphics& g, int width, int height, bool)
{
using namespace juce;
if (row >= channels.size())
return;
auto item = channels[row];
auto inputChannels = manager.getAudioDeviceSetup().inputChannels;
// We need to actually check the device for enablement
bool enabled = useStereoPairs ? inputChannels[row * 2] || inputChannels[row * 2 + 1] : inputChannels[row];
Rectangle<float> bounds{ float(width), float(height) };
bounds = bounds.reduced(bounds.getHeight() * 0.04f);
// To draw the tick box, we create a temporary ToggleButton (due to how I implemented the lnf function)
ToggleButton tempComp;
tempComp.setBounds(bounds.removeFromLeft(bounds.getHeight()).toNearestInt());
getLookAndFeel().drawTickBox(g, tempComp, 0, 0, 0, 0, enabled, true, true, false);
bounds.removeFromLeft(bounds.getHeight() * 0.3f);
g.setColour(findColour(textColourId));
g.setFont(getInter().withHeight(bounds.getHeight() * 0.85f));
g.drawText(channels[row], bounds, Justification::centredLeft);
}
void ChannelSelectorListBox::flipEnablement(int row) const
{
auto config = manager.getAudioDeviceSetup();
config.useDefaultInputChannels = false;
if (useStereoPairs)
{
bool enabled = config.inputChannels[row * 2] || config.inputChannels[row * 2 + 1];
setBit(config.inputChannels, row * 2 + 1, !enabled);
setBit(config.inputChannels, row * 2, !enabled);
}
else
{
setBit(config.inputChannels, row, !config.inputChannels[row]);
}
manager.setAudioDeviceSetup(config, true);
}
void ChannelSelectorListBox::setBit(juce::BigInteger& ch, int index, bool set) const
{
auto numActive = ch.countNumberOfSetBits();
if (ch[index] == set || (!set && numActive <= minInputChannels))
return;
// Clear a bit if necessary
if (set && numActive >= maxInputChannels)
{
auto firstActiveChan = ch.findNextSetBit(0);
ch.clearBit(index > firstActiveChan ? firstActiveChan : ch.getHighestBit());
}
ch.setBit(index, set);
}
juce::String ChannelSelectorListBox::getNameForChannelPair(const juce::String& name1, const juce::String& name2) const
{
juce::String commonBit;
for (int j = 0; j < name1.length(); ++j)
if (name1.substring(0, j).equalsIgnoreCase(name2.substring(0, j)))
commonBit = name1.substring(0, j);
// Make sure we only split the name at a space, because otherwise, things like "input 11" + "input 12" would become "input 11 + 2"
while (commonBit.isNotEmpty() && !juce::CharacterFunctions::isWhitespace(commonBit.getLastCharacter()))
commonBit = commonBit.dropLastCharacters(1);
return name1.trim() + " + " + name2.substring(commonBit.length()).trim();
}
//==============================================================================
InputDeviceSelector::InputDeviceSelector(juce::AudioDeviceManager& deviceManager, int minInputChannelsToUse, int maxInputChannelsToUse, bool showChannelsAsStereoPairs) :
manager(deviceManager),
titleLabel("", "Input Device Settings"),
warningLabel("", "No device opened, check DAW audio settings if issues persist"),
deviceTypeLabel("", "Type: "),
deviceLabel("", "Device: "),
channelsLabel("", "Channels: "),
sampleRateLabel("", "Sample rate: "),
bufferSizeLabel("", "Buffer size: "),
levelMeter(deviceManager),
channelSelector(deviceManager, minInputChannelsToUse, maxInputChannelsToUse, showChannelsAsStereoPairs)
{
jassert(minInputChannelsToUse >= 0 && minInputChannelsToUse <= maxInputChannelsToUse);
titleLabel.setJustificationType(juce::Justification::centred);
juce::Array labels = { &titleLabel, &deviceTypeLabel, &deviceLabel, &channelsLabel, &sampleRateLabel, &bufferSizeLabel };
for (auto* label : labels)
addAndMakeVisible(label);
warningLabel.setJustificationType(juce::Justification::centred);
addChildComponent(warningLabel);
const auto& types= manager.getAvailableDeviceTypes();
for (int i = 0; i < types.size(); ++i)
deviceTypeChooser.addItem(types.getUnchecked(i)->getTypeName(), i + 1);
deviceTypeChooser.setTextWhenNoChoicesAvailable("No devices available");
deviceTypeChooser.setSelectedId(1);
juce::Array comboBoxes = { &deviceTypeChooser, &deviceChooser, &sampleRateChooser, &bufferSizeChooser };
for (auto* box : comboBoxes)
{
box->onChange = [this] { settingsChanged(); };
addAndMakeVisible(box);
}
addAndMakeVisible(levelMeter);
addAndMakeVisible(channelSelector);
manager.addChangeListener(this);
settingsChanged();
}
InputDeviceSelector::~InputDeviceSelector()
{
manager.removeChangeListener(this);
}
void InputDeviceSelector::paint(juce::Graphics& g)
{
auto colors = getTheme();
g.fillAll(colors.prompt);
g.setColour(colors.foreground);
g.fillRoundedRectangle(getLocalBounds().toFloat(), 5.f);
}
void InputDeviceSelector::resized()
{
auto bounds = getLocalBounds().toFloat();
auto w = bounds.getWidth();
const auto row = bounds.getHeight() * 0.07f;
const auto padding = bounds.getHeight() * 0.035f;
constexpr auto labelProp = 0.25f;
bounds.reduce(w * 0.12f, w * 0.05f);
auto titleBounds = bounds.removeFromTop(bounds.proportionOfHeight(0.1f));
titleLabel.setFont(getInter().withHeight(titleBounds.getHeight()));
titleLabel.setBounds(titleBounds.toNearestInt());
bounds.removeFromTop(padding / 4);
auto warningLabelHeight = std::round(row * 0.665f);
warningLabel.setFont(getInter().withHeight(warningLabelHeight));
warningLabel.setBounds(bounds.removeFromTop(warningLabelHeight).toNearestInt());
bounds.removeFromTop(padding);
auto deviceTypeBounds = bounds.removeFromTop(row);
deviceTypeLabel.setFont(getInter().withHeight(std::round(row * 0.95f)));
deviceTypeLabel.setBounds(deviceTypeBounds.removeFromLeft(deviceTypeBounds.proportionOfWidth(labelProp)).toNearestInt());
deviceTypeChooser.setBounds(deviceTypeBounds.withHeight(row * 1.2f).toNearestInt());
bounds.removeFromTop(padding);
auto deviceBounds = bounds.removeFromTop(row);
deviceLabel.setFont(getInter().withHeight(std::round(row * 0.95f)));
deviceLabel.setBounds(deviceBounds.removeFromLeft(deviceBounds.proportionOfWidth(labelProp)).toNearestInt());
deviceChooser.setBounds(deviceBounds.removeFromLeft(bounds.proportionOfWidth(0.56f)).withHeight(row * 1.2f).toNearestInt());
levelMeter.setBounds(deviceBounds.removeFromRight(bounds.proportionOfWidth(0.15f)).toNearestInt());
bounds.removeFromTop(padding);
auto channelsBounds = bounds.removeFromTop(5 * row);
channelsLabel.setFont(getInter().withHeight(std::round(row * 0.95f)));
channelsLabel.setBounds(channelsBounds.removeFromLeft(channelsBounds.proportionOfWidth(labelProp)).removeFromTop(row).toNearestInt());
channelSelector.setBounds(channelsBounds.toNearestInt());
channelSelector.setRowHeight(int(std::round(row)));
bounds.removeFromTop(padding);
auto lastRow = row * 0.7f;
auto sampleRateBounds = bounds.removeFromTop(lastRow);
auto bufferSizeBounds = sampleRateBounds.removeFromRight(sampleRateBounds.proportionOfWidth(0.55f));
auto sampleRateLabelBounds = sampleRateBounds.removeFromLeft(sampleRateBounds.proportionOfWidth(0.5f));
sampleRateLabel.setFont(getInter().withHeight(std::round(lastRow * 0.95f)));
sampleRateLabel.setBounds(sampleRateLabelBounds.toNearestInt());
sampleRateChooser.setBounds(sampleRateBounds.withHeight(lastRow * 1.2f).toNearestInt());
bufferSizeBounds.removeFromLeft(bounds.getWidth() * 0.05f);
bufferSizeLabel.setFont(getInter().withHeight(std::round(lastRow * 0.95f)));
bufferSizeLabel.setBounds(bufferSizeBounds.removeFromLeft(bufferSizeBounds.proportionOfWidth(0.5f)).toNearestInt());
bufferSizeChooser.setBounds(bufferSizeBounds.withHeight(lastRow * 1.2f).toNearestInt());
}
void InputDeviceSelector::lookAndFeelChanged()
{
auto colors = getTheme();
warningLabel.setColour(juce::Label::textColourId, colors.highlight);
juce::Array comboBoxes = { &deviceTypeChooser, &deviceChooser, &sampleRateChooser, &bufferSizeChooser };
for (auto* box : comboBoxes)
box->setColour(Colors::backgroundColorId, colors.foreground);
}
void InputDeviceSelector::changeListenerCallback(juce::ChangeBroadcaster*)
{
auto* device = manager.getCurrentAudioDevice();
warningLabel.setVisible(!device);
if (!device)
return;
inputDevice = device;
deviceChooser.setText(device->getName());
sampleRateChooser.clear();
for (auto rate : device->getAvailableSampleRates())
sampleRateChooser.addItem(juce::String(rate) + " hz", int(rate));
sampleRateChooser.setText(juce::String(int(device->getCurrentSampleRate())) + " hz");
bufferSizeChooser.clear();
for (auto size : device->getAvailableBufferSizes())
bufferSizeChooser.addItem(juce::String(size), int(size));
bufferSizeChooser.setText(juce::String(device->getCurrentBufferSizeSamples()));
updateEnablement();
}
void InputDeviceSelector::settingsChanged()
{
juce::AudioDeviceManager::AudioDeviceSetup settings = manager.getAudioDeviceSetup();
auto deviceType = manager.getCurrentAudioDeviceType();
auto* selectedDeviceType = manager.getAvailableDeviceTypes()[deviceTypeChooser.getSelectedId() - 1];
bool newDeviceSelected = false;
if (deviceType != selectedDeviceType->getTypeName() || !bool(deviceChooser.getNumItems()))
{
manager.setCurrentAudioDeviceType(selectedDeviceType->getTypeName(), true);
deviceChooser.clear();
auto devices = selectedDeviceType->getDeviceNames(true);
for (int i = 0; i < devices.size(); i++)
deviceChooser.addItem(devices[i], i + 1);
deviceChooser.setSelectedId(1);
newDeviceSelected = true;
}
auto device = settings.inputDeviceName;
auto selectedDevice = deviceChooser.getItemText(deviceChooser.getSelectedItemIndex());
if (device != selectedDevice || newDeviceSelected)
{
settings.inputDeviceName = selectedDevice;
settings.useDefaultInputChannels = true;
}
auto sampleRate = int(settings.sampleRate);
auto selectedSampleRate = sampleRateChooser.getSelectedId();
if (sampleRate != selectedSampleRate && !newDeviceSelected && selectedSampleRate)
settings.sampleRate = selectedSampleRate;
auto bufferSize = settings.bufferSize;
auto selectedBufferSize = bufferSizeChooser.getSelectedId();
if (bufferSize != selectedBufferSize && !newDeviceSelected && selectedBufferSize)
settings.bufferSize = selectedBufferSize;
manager.setAudioDeviceSetup(settings, true);
updateEnablement();
}
void InputDeviceSelector::updateEnablement()
{
deviceChooser.setEnabled(bool(deviceChooser.getNumItems()));
sampleRateChooser.setEnabled(bool(sampleRateChooser.getNumItems()) && inputDevice && manager.getAudioDeviceSetup().inputChannels.countNumberOfSetBits());
bufferSizeChooser.setEnabled(bool(bufferSizeChooser.getNumItems()) && inputDevice && manager.getAudioDeviceSetup().inputChannels.countNumberOfSetBits());
levelMeter.setEnabled(bool(inputDevice));
channelSelector.setVisible(bool(inputDevice));
}