diff --git a/include/AudioJack.h b/include/AudioJack.h index 84d85a649..dec67eba1 100644 --- a/include/AudioJack.h +++ b/include/AudioJack.h @@ -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 getAudioPortNames(JackPortFlags portFlags) const; + std::vector getAudioInputNames() const; + std::vector getAudioOutputNames() const; + static QMenu* buildMenu(QToolButton* toolButton, const std::vector& 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 m_outputDevices; + std::vector 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; diff --git a/src/core/audio/AudioJack.cpp b/src/core/audio/AudioJack.cpp index 9abbcc347..b5b13d47b 100644 --- a/src/core/audio/AudioJack.cpp +++ b/src/core/audio/AudioJack.cpp @@ -27,8 +27,12 @@ #ifdef LMMS_HAVE_JACK #include +#include #include +#include #include +#include +#include #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(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(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(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& 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 AudioJack::setupWidget::getAudioPortNames(JackPortFlags portFlags) const +{ + std::vector 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 AudioJack::setupWidget::getAudioOutputNames() const +{ + return getAudioPortNames(JackPortIsInput); +} + +std::vector AudioJack::setupWidget::getAudioInputNames() const +{ + return getAudioPortNames(JackPortIsOutput); +} + +QMenu* AudioJack::setupWidget::buildMenu(QToolButton* toolButton, const std::vector& names, const QString& filteredLMMSClientName) +{ + auto menu = new QMenu(toolButton); + QMap clientNameToSubMenuMap; + QList 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; }