Inputs and Outputs Selection for Jack Driver (#7919)
# GUI ## Present inputs/outputs in hierarchical menu Present the available inputs and outputs in a hierarchical sorted menu which shows clients with their ports. The heavy lifting of creating the menu for the tool button is done in the new method `buildMenu`. It takes the input/output names in Jack's "Client name:Port name" format. If an input/output name can be successfully split into the client name and port name then a sub menu with the client name is created (if it was not already created before) and the port name is added as an entry. If the name cannot be split into exactly two components then it is simply added to the top level menu as is. Ports of the LMMS client are filtered out to prevent loops. The menu starts with the client's sub menus in alphabetical order. Then the top level entries are added in alphabetical order as well. The callbacks for the `QAction` instances are implemented with lambdas because MOC does not support nested classes like the setup widget is. For now the used lambda only sets the text of the `QToolButton` as these are used for persisting the configuration anyway. ## Disconnected state Add the option to keep inputs/outputs disconnected. The disconnected state is represented by the string "-" which is also what is saved into the configuration in this case. For now the representation in the GUI and of the save state is the same as it has the advantage that no translation is necessary and thus not mapping between display text and save state is necessary. ## Show technical output/input names Show the technical output and input port names used by LMMS in the setup dialog. Note: these are the names that are shown in tools like `qjackctl` or `qpwgraph`. This was proposed in a review. Personally I like the non-technical names better but let's see what's accepted. ## Let the tool buttons use available space Let the tool buttons stretch so that they look uniform and use all the available space. # Driver ## Reconnect inputs and outputs Attempt to reconnect the inputs and outputs from the configuration during startup of the Jack driver. Nothing will be done for inputs and outputs that are not available at startup. Example: the users might have saved some inputs when a device was available. The device is then disconnected and LMMS restarted. The stored inputs cannot be used anymore. To give the users the least surprise nothing is done. `AudioJack::attemptToConnect` does the actual reconnection and also prints some information for now. `attemptToReconnectOutput` and `attemptToReconnectInput` delegate to `attemptToConnect` with the right parameters. # Technical details ## Generalized number of inputs/outputs Generalize the number of inputs and outputs by using for loops. This affects the number of widgets that are created and the amount of configuration that is stored. This change is a result of a code review discussion. In my opinion it adds unnecessary complexity to something that should later be implemented completely different anyway. It is for example now necessary to compute the key names that are used during the saving of the configuration based on the channel number. The commit exists so that its changes can be discussed further. It might be reverted in one of the next commits. ## Collecting input and output names Add `AudioJack::setupWidget::getAudioPortNames` which takes the type of port and then collects all port names which match. Make `getAudioOutputNames` and `getAudioInputNames` delegate to that method with the appropriate type. This also hides the different terminologies a bit. ## Separate Jack client in the GUI The separate Jack client is necessary because the setup dialog does not have any access to the actual driver. So a new client is created when the dialog is opened and deleted when it is closed, i.e. when the dialog is deleted itself. ## Repeated strings Repeatedly used hard-coded strings are defined as static constant variables in the anonymous namespace. This should prevent subtle mistakes when working with the configuration values of the Jack driver. ## Saving the settings `AudioJack::setupWidget::saveSettings` saves the selections that have been made from the widgets right into the configuration.
This commit is contained in:
committed by
GitHub
parent
344fdd5b1f
commit
997764a0dc
@@ -47,6 +47,8 @@
|
||||
#endif
|
||||
|
||||
class QLineEdit;
|
||||
class QMenu;
|
||||
class QToolButton;
|
||||
|
||||
namespace lmms
|
||||
{
|
||||
@@ -79,8 +81,19 @@ public:
|
||||
setupWidget(QWidget* parent);
|
||||
void saveSettings() override;
|
||||
|
||||
private:
|
||||
std::vector<std::string> getAudioPortNames(JackPortFlags portFlags) const;
|
||||
std::vector<std::string> getAudioInputNames() const;
|
||||
std::vector<std::string> getAudioOutputNames() const;
|
||||
static QMenu* buildMenu(QToolButton* toolButton, const std::vector<std::string>& names, const QString& filteredLMMSClientName);
|
||||
|
||||
private:
|
||||
QLineEdit* m_clientName;
|
||||
// Because we do not have access to a JackAudio driver instance we have to be our own client to display inputs and outputs...
|
||||
jack_client_t* m_client;
|
||||
|
||||
std::vector<QToolButton*> m_outputDevices;
|
||||
std::vector<QToolButton*> m_inputDevices;
|
||||
};
|
||||
|
||||
private slots:
|
||||
@@ -90,6 +103,10 @@ private:
|
||||
bool initJackClient();
|
||||
void resizeInputBuffer(jack_nframes_t nframes);
|
||||
|
||||
void attemptToConnect(size_t index, const char *lmms_port_type, const char *source_port, const char *destination_port);
|
||||
void attemptToReconnectOutput(size_t outputIndex, const QString& targetPort);
|
||||
void attemptToReconnectInput(size_t inputIndex, const QString& sourcePort);
|
||||
|
||||
void startProcessing() override;
|
||||
void stopProcessing() override;
|
||||
|
||||
|
||||
@@ -27,8 +27,12 @@
|
||||
#ifdef LMMS_HAVE_JACK
|
||||
|
||||
#include <QFormLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QToolButton>
|
||||
#include <QStringList>
|
||||
|
||||
#include "AudioEngine.h"
|
||||
#include "ConfigManager.h"
|
||||
@@ -37,9 +41,42 @@
|
||||
#include "MainWindow.h"
|
||||
#include "MidiJack.h"
|
||||
|
||||
|
||||
namespace
|
||||
{
|
||||
static const QString audioJackClass("audiojack");
|
||||
static const QString clientNameKey("clientname");
|
||||
static const QString disconnectedRepresentation("-");
|
||||
|
||||
QString getOutputKeyByChannel(size_t channel)
|
||||
{
|
||||
return "output" + QString::number(channel + 1);
|
||||
}
|
||||
|
||||
QString getInputKeyByChannel(size_t channel)
|
||||
{
|
||||
return "input" + QString::number(channel + 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace lmms
|
||||
{
|
||||
|
||||
static QString buildChannelSuffix(ch_cnt_t ch)
|
||||
{
|
||||
return (ch % 2 ? "R" : "L") + QString::number(ch / 2 + 1);
|
||||
}
|
||||
|
||||
static QString buildOutputName(ch_cnt_t ch)
|
||||
{
|
||||
return QString("master out ") + buildChannelSuffix(ch);
|
||||
}
|
||||
|
||||
static QString buildInputName(ch_cnt_t ch)
|
||||
{
|
||||
return QString("master in ") + buildChannelSuffix(ch);
|
||||
}
|
||||
|
||||
AudioJack::AudioJack(bool& successful, AudioEngine* audioEngineParam)
|
||||
: AudioDevice(
|
||||
@@ -126,11 +163,9 @@ AudioJack* AudioJack::addMidiClient(MidiJack* midiClient)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
bool AudioJack::initJackClient()
|
||||
{
|
||||
QString clientName = ConfigManager::inst()->value("audiojack", "clientname");
|
||||
QString clientName = ConfigManager::inst()->value(audioJackClass, clientNameKey);
|
||||
if (clientName.isEmpty()) { clientName = "lmms"; }
|
||||
|
||||
const char* serverName = nullptr;
|
||||
@@ -169,11 +204,11 @@ bool AudioJack::initJackClient()
|
||||
|
||||
for (ch_cnt_t ch = 0; ch < channels(); ++ch)
|
||||
{
|
||||
QString name = QString("master out ") + ((ch % 2) ? "R" : "L") + QString::number(ch / 2 + 1);
|
||||
const QString name = buildOutputName(ch);
|
||||
m_outputPorts.push_back(
|
||||
jack_port_register(m_client, name.toLatin1().constData(), JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0));
|
||||
|
||||
QString input_name = QString("master in ") + ((ch % 2) ? "R" : "L") + QString::number(ch / 2 + 1);
|
||||
const QString input_name = buildInputName(ch);
|
||||
m_inputPorts.push_back(jack_port_register(m_client, input_name.toLatin1().constData(), JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0));
|
||||
|
||||
if (m_outputPorts.back() == nullptr)
|
||||
@@ -194,7 +229,50 @@ void AudioJack::resizeInputBuffer(jack_nframes_t nframes)
|
||||
m_inputFrameBuffer.resize(nframes);
|
||||
}
|
||||
|
||||
void AudioJack::attemptToConnect(size_t index, const char *lmms_port_type, const char *source_port, const char *destination_port)
|
||||
{
|
||||
printf("Attempting to reconnect %s port %u: %s -> %s", lmms_port_type, static_cast<unsigned int>(index), source_port, destination_port);
|
||||
if (!jack_connect(m_client, source_port, destination_port))
|
||||
{
|
||||
printf(" - Success!\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
printf(" - Failure\n");
|
||||
}
|
||||
}
|
||||
|
||||
void AudioJack::attemptToReconnectOutput(size_t outputIndex, const QString& targetPort)
|
||||
{
|
||||
if (outputIndex > m_outputPorts.size()) return;
|
||||
|
||||
if (targetPort == disconnectedRepresentation)
|
||||
{
|
||||
printf("Output port %u is not connected.\n", static_cast<unsigned int>(outputIndex));
|
||||
return;
|
||||
}
|
||||
|
||||
auto outputName = jack_port_name(m_outputPorts[outputIndex]);
|
||||
auto targetName = targetPort.toLatin1().constData();
|
||||
|
||||
attemptToConnect(outputIndex, "output", outputName, targetName);
|
||||
}
|
||||
|
||||
void AudioJack::attemptToReconnectInput(size_t inputIndex, const QString& sourcePort)
|
||||
{
|
||||
if (inputIndex > m_inputPorts.size()) return;
|
||||
|
||||
if (sourcePort == disconnectedRepresentation)
|
||||
{
|
||||
printf("Input port %u is not connected.\n", static_cast<unsigned int>(inputIndex));
|
||||
return;
|
||||
}
|
||||
|
||||
auto inputName = jack_port_name(m_inputPorts[inputIndex]);
|
||||
auto sourceName = sourcePort.toLatin1().constData();
|
||||
|
||||
attemptToConnect(inputIndex, "input", sourceName, inputName);
|
||||
}
|
||||
|
||||
|
||||
void AudioJack::startProcessing()
|
||||
@@ -216,26 +294,20 @@ void AudioJack::startProcessing()
|
||||
// try to sync JACK's and LMMS's buffer-size
|
||||
// jack_set_buffer_size( m_client, audioEngine()->framesPerPeriod() );
|
||||
|
||||
const char** ports = jack_get_ports(m_client, nullptr, nullptr, JackPortIsPhysical | JackPortIsInput);
|
||||
if (ports == nullptr)
|
||||
const auto cm = ConfigManager::inst();
|
||||
|
||||
const auto numberOfChannels = channels();
|
||||
for (size_t i = 0; i < numberOfChannels; ++i)
|
||||
{
|
||||
printf("no physical playback ports. you'll have to do "
|
||||
"connections at your own!\n");
|
||||
attemptToReconnectOutput(i, cm->value(audioJackClass, getOutputKeyByChannel(i)));
|
||||
}
|
||||
else
|
||||
|
||||
for (size_t i = 0; i < numberOfChannels; ++i)
|
||||
{
|
||||
for (ch_cnt_t ch = 0; ch < channels(); ++ch)
|
||||
{
|
||||
if (jack_connect(m_client, jack_port_name(m_outputPorts[ch]), ports[ch]))
|
||||
{
|
||||
printf("cannot connect output ports. you'll "
|
||||
"have to do connections at your own!\n");
|
||||
}
|
||||
}
|
||||
attemptToReconnectInput(i, cm->value(audioJackClass, getInputKeyByChannel(i)));
|
||||
}
|
||||
|
||||
m_stopped = false;
|
||||
jack_free(ports);
|
||||
}
|
||||
|
||||
|
||||
@@ -409,21 +481,184 @@ void AudioJack::shutdownCallback(void* udata)
|
||||
AudioJack::setupWidget::setupWidget(QWidget* parent)
|
||||
: AudioDeviceSetupWidget(AudioJack::name(), parent)
|
||||
{
|
||||
const char* serverName = nullptr;
|
||||
jack_status_t status;
|
||||
m_client = jack_client_open("LMMS-Setup Dialog", JackNullOption, &status, serverName);
|
||||
|
||||
QFormLayout * form = new QFormLayout(this);
|
||||
|
||||
QString cn = ConfigManager::inst()->value("audiojack", "clientname");
|
||||
// Set the field growth policy to allow fields to expand horizontally
|
||||
form->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
|
||||
|
||||
const auto cm = ConfigManager::inst();
|
||||
QString cn = cm->value(audioJackClass, clientNameKey);
|
||||
if (cn.isEmpty()) { cn = "lmms"; }
|
||||
m_clientName = new QLineEdit(cn, this);
|
||||
|
||||
form->addRow(tr("Client name"), m_clientName);
|
||||
|
||||
auto buildToolButton = [this](QWidget* parent, const QString& currentSelection, const std::vector<std::string>& names, const QString& filteredLMMSClientName)
|
||||
{
|
||||
auto toolButton = new QToolButton(parent);
|
||||
// Make sure that the tool button will fill out the available space in the form layout
|
||||
toolButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
|
||||
toolButton->setPopupMode(QToolButton::InstantPopup);
|
||||
toolButton->setText(currentSelection);
|
||||
auto menu = AudioJack::setupWidget::buildMenu(toolButton, names, filteredLMMSClientName);
|
||||
toolButton->setMenu(menu);
|
||||
|
||||
return toolButton;
|
||||
};
|
||||
|
||||
// Outputs
|
||||
const auto audioOutputNames = getAudioOutputNames();
|
||||
|
||||
constexpr size_t numberOfOutputChannels = 2;
|
||||
for (size_t i = 0; i < numberOfOutputChannels; ++i)
|
||||
{
|
||||
const auto outputKey = getOutputKeyByChannel(i);
|
||||
const auto outputValue = cm->value(audioJackClass, outputKey);
|
||||
auto outputDevice = buildToolButton(this, outputValue, audioOutputNames, cn);
|
||||
form->addRow(buildOutputName(i) + ":", outputDevice);
|
||||
m_outputDevices.push_back(outputDevice);
|
||||
}
|
||||
|
||||
// Inputs
|
||||
const auto audioInputNames = getAudioInputNames();
|
||||
|
||||
constexpr size_t numberOfInputChannels = 2;
|
||||
for (size_t i = 0; i < numberOfInputChannels; ++i)
|
||||
{
|
||||
const auto inputKey = getInputKeyByChannel(i);
|
||||
const auto inputValue = cm->value(audioJackClass, inputKey);
|
||||
auto inputDevice = buildToolButton(this, inputValue, audioInputNames, cn);
|
||||
form->addRow(buildInputName(i) + ":", inputDevice);
|
||||
m_inputDevices.push_back(inputDevice);
|
||||
}
|
||||
|
||||
if (m_client != nullptr)
|
||||
{
|
||||
jack_deactivate(m_client);
|
||||
jack_client_close(m_client);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
void AudioJack::setupWidget::saveSettings()
|
||||
{
|
||||
ConfigManager::inst()->setValue("audiojack", "clientname", m_clientName->text());
|
||||
ConfigManager::inst()->setValue(audioJackClass, clientNameKey, m_clientName->text());
|
||||
|
||||
for (size_t i = 0; i < m_outputDevices.size(); ++i)
|
||||
{
|
||||
ConfigManager::inst()->setValue(audioJackClass, getOutputKeyByChannel(i), m_outputDevices[i]->text());
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < m_inputDevices.size(); ++i)
|
||||
{
|
||||
ConfigManager::inst()->setValue(audioJackClass, getInputKeyByChannel(i), m_inputDevices[i]->text());
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> AudioJack::setupWidget::getAudioPortNames(JackPortFlags portFlags) const
|
||||
{
|
||||
std::vector<std::string> audioPorts;
|
||||
|
||||
const char **inputAudioPorts = jack_get_ports(m_client, nullptr, JACK_DEFAULT_AUDIO_TYPE, portFlags);
|
||||
if (inputAudioPorts)
|
||||
{
|
||||
for (int i = 0; inputAudioPorts[i] != nullptr; ++i)
|
||||
{
|
||||
auto currentPortName = inputAudioPorts[i];
|
||||
|
||||
audioPorts.push_back(currentPortName);
|
||||
}
|
||||
jack_free(inputAudioPorts); // Remember to free after use
|
||||
}
|
||||
|
||||
return audioPorts;
|
||||
}
|
||||
|
||||
std::vector<std::string> AudioJack::setupWidget::getAudioOutputNames() const
|
||||
{
|
||||
return getAudioPortNames(JackPortIsInput);
|
||||
}
|
||||
|
||||
std::vector<std::string> AudioJack::setupWidget::getAudioInputNames() const
|
||||
{
|
||||
return getAudioPortNames(JackPortIsOutput);
|
||||
}
|
||||
|
||||
QMenu* AudioJack::setupWidget::buildMenu(QToolButton* toolButton, const std::vector<std::string>& names, const QString& filteredLMMSClientName)
|
||||
{
|
||||
auto menu = new QMenu(toolButton);
|
||||
QMap<QString, QMenu*> clientNameToSubMenuMap;
|
||||
QList<QAction*> topLevelActions;
|
||||
for (const auto& currentName : names)
|
||||
{
|
||||
const auto clientNameWithPortName = QString::fromStdString(currentName);
|
||||
|
||||
auto actionLambda = [toolButton, clientNameWithPortName](bool checked)
|
||||
{
|
||||
toolButton->setText(clientNameWithPortName);
|
||||
};
|
||||
|
||||
// Split into individual client name and port name
|
||||
const auto list = clientNameWithPortName.split(":");
|
||||
if (list.size() == 2)
|
||||
{
|
||||
const auto& clientName = list[0];
|
||||
const auto& portName = list[1];
|
||||
|
||||
if (clientName == filteredLMMSClientName)
|
||||
{
|
||||
// Prevent loops by not adding port of the LMMS client to the menu
|
||||
continue;
|
||||
}
|
||||
|
||||
QMenu* clientSubMenu = nullptr;
|
||||
|
||||
auto it = clientNameToSubMenuMap.find(clientName);
|
||||
if (it == clientNameToSubMenuMap.end())
|
||||
{
|
||||
clientSubMenu = new QMenu(menu);
|
||||
clientSubMenu->setTitle(clientName);
|
||||
clientNameToSubMenuMap.insert(clientName, clientSubMenu);
|
||||
}
|
||||
else
|
||||
{
|
||||
clientSubMenu = *it;
|
||||
}
|
||||
|
||||
auto action = new QAction(portName, clientSubMenu);
|
||||
connect(action, &QAction::triggered, actionLambda);
|
||||
clientSubMenu->addAction(action);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We cannot split into client and port name. Add the whole thing to the top level menu
|
||||
auto action = new QAction(QString::fromStdString(currentName), menu);
|
||||
connect(action, &QAction::triggered, actionLambda);
|
||||
topLevelActions.append(action);
|
||||
}
|
||||
}
|
||||
|
||||
// First add the sub menus. By iterating the map they will be sorted automatically
|
||||
for (auto it = clientNameToSubMenuMap.begin(); it != clientNameToSubMenuMap.end(); ++it)
|
||||
{
|
||||
menu->addMenu(it.value());
|
||||
}
|
||||
|
||||
// Now add potential top level actions, i.e. the entries which cannot be split at exactly one ":"
|
||||
// They must be sorted explicitly
|
||||
std::sort(topLevelActions.begin(), topLevelActions.end(), [](QAction* a, QAction* b) { return a->text() < b->text(); });
|
||||
menu->addActions(topLevelActions);
|
||||
|
||||
// Add the menu entry which represents the disconnected state at the very end
|
||||
auto disconnectedAction = new QAction(disconnectedRepresentation, menu);
|
||||
connect(disconnectedAction, &QAction::triggered, [toolButton](bool checked) { toolButton->setText(disconnectedRepresentation); });
|
||||
menu->addAction(disconnectedAction);
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user