diff --git a/include/ControlSurface.h b/include/ControlSurface.h new file mode 100644 index 00000000000..28e4ac466a5 --- /dev/null +++ b/include/ControlSurface.h @@ -0,0 +1,66 @@ +/* + * ControlSurface.h - Common control surface actions to lmms + * + * Copyright (c) 2025 - altrouge + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_CONTROL_SURFACE_H +#define LMMS_CONTROL_SURFACE_H + +#include + +#include "lmms_export.h" + +namespace lmms { +//! Implements functions linking controller to LMMS. +class LMMS_EXPORT ControlSurface : public QObject +{ + Q_OBJECT +public: + ControlSurface(); + +public: +signals: + void requestPlay(); + void requestStop(); + void requestLoop(); + void requestRecord(); + void requestPreviousInstrumentTrack(); + void requestNextInstrumentTrack(); + + // Use slots to call from correct thread. +private slots: + ///! Starts playing. + void play(); + ///! Stops playing. + void stop(); + ///! Activate/deactivate the loop. + void loop(); + ///! Starts recording. + void record(); + ///! Selects previous instrument track. + void previousInstrumentTrack(); + ///! Selects next instrument track. + void nextInstrumentTrack(); +}; +} // namespace lmms + +#endif diff --git a/include/ControlSurfaceMCU.h b/include/ControlSurfaceMCU.h new file mode 100644 index 00000000000..430d91982df --- /dev/null +++ b/include/ControlSurfaceMCU.h @@ -0,0 +1,49 @@ +/* + * ControlSurfaceMCU.h - A controller to receive MIDI MCU control + * + * Copyright (c) 2025 - altrouge + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_CONTROL_SURFACE_MCU_H +#define LMMS_CONTROL_SURFACE_MCU_H + +#include "ControlSurface.h" +#include "MidiEventProcessor.h" +#include "MidiPort.h" +#include "Note.h" + +namespace lmms { +//! Implements the Mackie control protocol to control the DAW. +class LMMS_EXPORT ControlSurfaceMCU : public MidiEventProcessor +{ +public: + ControlSurfaceMCU(const QString& device); + + void processInEvent(const MidiEvent& event, const TimePos& time = TimePos(), f_cnt_t offset = 0) override; + void processOutEvent(const MidiEvent& event, const TimePos& time = TimePos(), f_cnt_t offset = 0) override; + +private: + MidiPort m_midiPort; + ControlSurface m_controlSurface; +}; +} // namespace lmms + +#endif diff --git a/include/InstrumentTrack.h b/include/InstrumentTrack.h index 689c962cdab..1c9ff6ee5e1 100644 --- a/include/InstrumentTrack.h +++ b/include/InstrumentTrack.h @@ -240,6 +240,9 @@ class LMMS_EXPORT InstrumentTrack : public Track, public MidiEventProcessor void autoAssignMidiDevice( bool ); + /// Gets the auto assigned track (more or less the selected track). + static InstrumentTrack* getAutoAssignedTrack() { return InstrumentTrack::s_autoAssignedTrack; } + signals: void instrumentChanged(); void midiNoteOn( const lmms::Note& ); diff --git a/include/SetupDialog.h b/include/SetupDialog.h index 871a80bcd4b..bdc9cd4f26a 100644 --- a/include/SetupDialog.h +++ b/include/SetupDialog.h @@ -189,6 +189,7 @@ private slots: MswMap m_midiIfaceSetupWidgets; trMap m_midiIfaceNames; QComboBox * m_assignableMidiDevices; + QComboBox* m_MCUDawMidiDevices; bool m_midiAutoQuantize; // Paths settings widgets. diff --git a/include/Song.h b/include/Song.h index f08edfff602..764c9e0ac56 100644 --- a/include/Song.h +++ b/include/Song.h @@ -44,6 +44,7 @@ namespace lmms { class AutomationTrack; +class ControlSurfaceMCU; class Keymap; class MidiClip; class Scale; @@ -378,6 +379,9 @@ class LMMS_EXPORT Song : public TrackContainer Metronome& metronome() { return m_metronome; } + /// Set a DAW MCU surface control. + void setControlSurfaceMCU(); + public slots: void playSong(); void record(); @@ -512,6 +516,7 @@ private slots: TimePos m_exportSongEnd; TimePos m_exportEffectiveLength; + std::shared_ptr m_mcu_controller = nullptr; std::shared_ptr m_scales[MaxScaleCount]; std::shared_ptr m_keymaps[MaxKeymapCount]; diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 1e2c4f3cfdb..c097bf0a08d 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -14,6 +14,7 @@ set(LMMS_SRCS core/Clipboard.cpp core/ComboBoxModel.cpp core/ConfigManager.cpp + core/ControlSurface.cpp core/Controller.cpp core/ControllerConnection.cpp core/DataFile.cpp @@ -120,6 +121,7 @@ set(LMMS_SRCS core/lv2/Lv2UridMap.cpp core/lv2/Lv2Worker.cpp + core/midi/ControlSurfaceMCU.cpp core/midi/MidiAlsaRaw.cpp core/midi/MidiAlsaSeq.cpp core/midi/MidiClient.cpp diff --git a/src/core/ControlSurface.cpp b/src/core/ControlSurface.cpp new file mode 100644 index 00000000000..e3bf046faf3 --- /dev/null +++ b/src/core/ControlSurface.cpp @@ -0,0 +1,131 @@ +/* + * ControlSurface.cpp - Common control surface actions to lmms + * + * Copyright (c) 2025 - altrouge + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "ControlSurface.h" + +#include "Engine.h" +#include "GuiApplication.h" +#include "InstrumentTrack.h" +#include "MidiClip.h" +#include "PianoRoll.h" +#include "Song.h" +#include "SongEditor.h" + +namespace lmms { + +ControlSurface::ControlSurface() +{ + QObject::connect(this, &ControlSurface::requestPlay, this, &ControlSurface::play); + QObject::connect(this, &ControlSurface::requestStop, this, &ControlSurface::stop); + QObject::connect(this, &ControlSurface::requestLoop, this, &ControlSurface::loop); + QObject::connect(this, &ControlSurface::requestRecord, this, &ControlSurface::record); + QObject::connect( + this, &ControlSurface::requestPreviousInstrumentTrack, this, &ControlSurface::previousInstrumentTrack); + QObject::connect(this, &ControlSurface::requestNextInstrumentTrack, this, &ControlSurface::nextInstrumentTrack); +} + +void ControlSurface::play() +{ + Engine::getSong()->playSong(); +} + +void ControlSurface::stop() +{ + auto piano_roll = gui::getGUI()->pianoRoll(); + if (piano_roll != nullptr) { piano_roll->stop(); } + Engine::getSong()->stop(); +} + +void ControlSurface::loop() +{ + // Activate on MidiClip for piano roll and Song for whole song. + auto& timeline_midi = Engine::getSong()->getTimeline(Song::PlayMode::MidiClip); + timeline_midi.setLoopEnabled(!timeline_midi.loopEnabled()); + auto& timeline = Engine::getSong()->getTimeline(Song::PlayMode::Song); + timeline.setLoopEnabled(!timeline.loopEnabled()); +} + +void ControlSurface::record() +{ + // Get the clip. + auto assigned_instrument_track = InstrumentTrack::getAutoAssignedTrack(); + if (assigned_instrument_track != nullptr) + { + std::vector clips; + auto current_time = Engine::getSong()->getPlayPos(Song::PlayMode::Song); + assigned_instrument_track->getClipsInRange(clips, current_time, current_time); + MidiClip* current_clip = nullptr; + + // If there are no available clips, create a clip. + if (!clips.empty()) { current_clip = dynamic_cast(clips.front()); } + else { current_clip = dynamic_cast(assigned_instrument_track->createClip(current_time)); } + + auto piano_roll = gui::getGUI()->pianoRoll(); + piano_roll->setCurrentMidiClip(current_clip); + piano_roll->recordAccompany(); + } +} + +void followingInstrumentTrack(bool reverse) +{ + // Get the clip. + auto assigned_instrument_track = InstrumentTrack::getAutoAssignedTrack(); + const auto track_list = Engine::getSong()->tracks(); + int next = reverse ? -1 : 1; + if (track_list.empty()) { return; } + + int start_ind = reverse ? track_list.size() - 1 : 0; + if (assigned_instrument_track != nullptr) + { + for (size_t ind = 0; ind < track_list.size(); ++ind) + { + if (track_list[ind] == assigned_instrument_track) + { + start_ind = (ind + track_list.size() + next) % track_list.size(); + break; + } + } + } + for (size_t iteration = 0; iteration < track_list.size(); ++iteration) + { + size_t current_ind = (start_ind + track_list.size() + next * iteration) % track_list.size(); + if (track_list[current_ind]->type() == Track::Type::Instrument) + { + assigned_instrument_track = dynamic_cast(track_list[current_ind]); + assigned_instrument_track->autoAssignMidiDevice(true); + break; + } + } +} + +void ControlSurface::previousInstrumentTrack() +{ + followingInstrumentTrack(true); +} + +void ControlSurface::nextInstrumentTrack() +{ + followingInstrumentTrack(false); +} +} // namespace lmms diff --git a/src/core/Song.cpp b/src/core/Song.cpp index 4e6bf6f583d..3def4dba1d7 100644 --- a/src/core/Song.cpp +++ b/src/core/Song.cpp @@ -37,6 +37,7 @@ #include "ConfigManager.h" #include "ControllerRackView.h" #include "ControllerConnection.h" +#include "ControlSurfaceMCU.h" #include "EnvelopeAndLfoParameters.h" #include "Mixer.h" #include "MixerView.h" @@ -45,6 +46,7 @@ #include "InstrumentTrack.h" #include "Keymap.h" #include "NotePlayHandle.h" +#include "MidiClient.h" #include "MidiClip.h" #include "PatternEditor.h" #include "PatternStore.h" @@ -167,8 +169,16 @@ void Song::setTempo() emit tempoChanged( tempo ); } - - +void Song::setControlSurfaceMCU() +{ + m_mcu_controller.reset(); + const QString& device = ConfigManager::inst()->value("midi", "midimcudaw"); + // Check if the device exists + if (Engine::audioEngine()->midiClient()->readablePorts().indexOf(device) >= 0) + { + m_mcu_controller = std::make_shared(device); + } +} void Song::setTimeSignature() { @@ -1169,6 +1179,14 @@ void Song::loadProject( const QString & fileName ) node = node.nextSibling(); } + // Set the midi MCU controller and update it when the config is updated. + setControlSurfaceMCU(); + connect(ConfigManager::inst(), &ConfigManager::valueChanged, + [this](QString const& cls, QString const& attribute, QString const& value) { + if (!(cls == "midi" && attribute == "midimcudaw")) { return; } + setControlSurfaceMCU(); + }); + // quirk for fixing projects with broken positions of Clips inside pattern tracks Engine::patternStore()->fixIncorrectPositions(); diff --git a/src/core/midi/ControlSurfaceMCU.cpp b/src/core/midi/ControlSurfaceMCU.cpp new file mode 100644 index 00000000000..52c1ab292bf --- /dev/null +++ b/src/core/midi/ControlSurfaceMCU.cpp @@ -0,0 +1,164 @@ +/* + * ControlSurfaceMCU.cpp - A controller to receive MIDI MCU control + * + * Copyright (c) 2025 - altrouge + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "ControlSurfaceMCU.h" + +#include + +#include "AudioEngine.h" +#include "ControlSurface.h" +#include "Engine.h" +#include "MidiClient.h" +#include "Song.h" + +namespace lmms { + +namespace { +enum class MCUEvents : int16_t +{ + // SELECT_CHANNEL_1 = Octave::Octave_1 + Key::C, // C1 + // SELECT_CHANNEL_2, // C#1 + // SELECT_CHANNEL_3, // D1 + // SELECT_CHANNEL_4, // D#1 + // SELECT_CHANNEL_5, // E1 + // SELECT_CHANNEL_6, // F1 + // SELECT_CHANNEL_7, // F#1 + // SELECT_CHANNEL_8, // G1 + // REC_READY_CHANNEL_1, // C-1 + // REC_READY_CHANNEL_2, // C#-1 + // REC_READY_CHANNEL_3, // D-1 + // REC_READY_CHANNEL_4, // D#-1 + // REC_READY_CHANNEL_5, // E-1 + // REC_READY_CHANNEL_6, // F-1 + // REC_READY_CHANNEL_7, // F#-1 + // REC_READY_CHANNEL_8, // G-1 + // ... + SHIFT = Octave::Octave_4 + Key::Ais, // A#4 + OPTION = Octave::Octave_4 + Key::H, // B4 + CONTROL = Octave::Octave_5 + Key::C, // C5 + ALT = Octave::Octave_5 + Key::Cis, // C#5 + PREVIOUS_FRM = Octave::Octave_6 + Key::C, // C6 + NEXT_FRM = Octave::Octave_6 + Key::Cis, // C#6 + LOOP = Octave::Octave_6 + Key::D, // D6 + PI_ = Octave::Octave_6 + Key::Dis, // D#6 + PO = Octave::Octave_6 + Key::E, // E6 + HOME = Octave::Octave_6 + Key::F, // F6 + END = Octave::Octave_6 + Key::Fis, // F#6 + REWIND = Octave::Octave_6 + Key::G, // G6 + FFWD = Octave::Octave_6 + Key::Gis, // G#6 + STOP = Octave::Octave_6 + Key::A, // A6 + PLAY = Octave::Octave_6 + Key::Ais, // A#6 + RECORD = Octave::Octave_6 + Key::H, // B6 (German notations) +}; + +enum class MCUControlChangeEvents : int16_t +{ + VPOT_1_ROTATION = 16, + VPOT_2_ROTATION = 17, + VPOT_3_ROTATION = 18, + VPOT_4_ROTATION = 19, + VPOT_5_ROTATION = 20, + VPOT_6_ROTATION = 21, + VPOT_7_ROTATION = 22, + VPOT_8_ROTATION = 23, + JOG_WHEEL = 60, +}; + +constexpr int keyUpVelocity = 127; +constexpr int keyDownVelocity = 0; + +constexpr int CLOCKWISE_1 = 1; +constexpr int CLOCKWISE_2 = 2; // Faster motion. +constexpr int COUNTER_CLOCKWISE_1 = 65; +constexpr int COUNTER_CLOCKWISE_2 = 66; // Faster motion. +} // namespace + +void ControlSurfaceMCU::processInEvent(const MidiEvent& event, const TimePos& time, f_cnt_t offset) +{ + switch (event.type()) + { + case MidiNoteOn: { + // Only apply the actions on KeyUp. + if (event.velocity() == keyUpVelocity) + { + switch (event.key()) + { + case static_cast(MCUEvents::RECORD): + emit m_controlSurface.requestRecord(); + break; + case static_cast(MCUEvents::LOOP): + emit m_controlSurface.requestLoop(); + break; + case static_cast(MCUEvents::STOP): + emit m_controlSurface.requestStop(); + break; + case static_cast(MCUEvents::PLAY): + emit m_controlSurface.requestPlay(); + break; + default: + break; + } + } + break; + } + case MidiControlChange: { + switch (event.key()) + { + case static_cast(MCUControlChangeEvents::JOG_WHEEL): + switch (event.velocity()) + { + case CLOCKWISE_1: + emit m_controlSurface.requestNextInstrumentTrack(); + break; + case COUNTER_CLOCKWISE_1: + emit m_controlSurface.requestPreviousInstrumentTrack(); + break; + } + break; + } + break; + } + case MidiPitchBend: { + // Fader change. + break; + } + default: + break; + } +} + +void ControlSurfaceMCU::processOutEvent(const MidiEvent& event, const TimePos& time, f_cnt_t offset) +{ +} + +ControlSurfaceMCU::ControlSurfaceMCU(const QString& device) + : MidiEventProcessor() + , m_midiPort(QString::fromStdString("mcu_controller"), Engine::audioEngine()->midiClient(), this) +{ + if (Engine::audioEngine()->midiClient()->readablePorts().indexOf(device) >= 0) + { + m_midiPort.subscribeReadablePort(device, true); + } +} +} // namespace lmms diff --git a/src/gui/modals/SetupDialog.cpp b/src/gui/modals/SetupDialog.cpp index d71ede03f53..96e5977fc9d 100644 --- a/src/gui/modals/SetupDialog.cpp +++ b/src/gui/modals/SetupDialog.cpp @@ -700,14 +700,27 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : { m_assignableMidiDevices->addItems(Engine::audioEngine()->midiClient()->readablePorts()); } - else + else { m_assignableMidiDevices->addItem("all"); } { - m_assignableMidiDevices->addItem("all"); + int current = m_assignableMidiDevices->findText(ConfigManager::inst()->value("midi", "midiautoassign")); + if (current >= 0) { m_assignableMidiDevices->setCurrentIndex(current); } } - int current = m_assignableMidiDevices->findText(ConfigManager::inst()->value("midi", "midiautoassign")); - if (current >= 0) + + // MIDI daw mode + QGroupBox* midiMCUBox = new QGroupBox(tr("Use surface control (MCU protocol)"), midi_w); + QVBoxLayout* midiMCULayout = new QVBoxLayout(midiMCUBox); + + m_MCUDawMidiDevices = new QComboBox(midiMCUBox); + midiMCULayout->addWidget(m_MCUDawMidiDevices); + m_MCUDawMidiDevices->addItem("none"); + if (!Engine::audioEngine()->midiClient()->isRaw()) + { + m_MCUDawMidiDevices->addItems(Engine::audioEngine()->midiClient()->readablePorts()); + } + else {} { - m_assignableMidiDevices->setCurrentIndex(current); + int current = m_MCUDawMidiDevices->findText(ConfigManager::inst()->value("midi", "midimcudaw")); + if (current >= 0) { m_MCUDawMidiDevices->setCurrentIndex(current); } } // MIDI Recording tab @@ -725,6 +738,7 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : midi_layout->addWidget(midiInterfaceBox); midi_layout->addWidget(ms_w); midi_layout->addWidget(midiAutoAssignBox); + midi_layout->addWidget(midiMCUBox); midi_layout->addWidget(midiRecordingTab); midi_layout->addStretch(); @@ -969,6 +983,8 @@ void SetupDialog::accept() m_midiIfaceNames[m_midiInterfaces->currentText()]); ConfigManager::inst()->setValue("midi", "midiautoassign", m_assignableMidiDevices->currentText()); + ConfigManager::inst()->setValue("midi", "midimcudaw", + m_MCUDawMidiDevices->currentText()); ConfigManager::inst()->setValue("midi", "autoquantize", QString::number(m_midiAutoQuantize));