Skip to content

Commit 3ace4be

Browse files
committed
Add a control surface MCU basic interface
Adds compatibility with: - Play button - Stop button - Loop button - Record button - Jog wheel to select instrument
1 parent 0b0833b commit 3ace4be

File tree

10 files changed

+462
-7
lines changed

10 files changed

+462
-7
lines changed

include/ControlSurface.h

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* ControlSurface.h - Common control surface actions to lmms
3+
*
4+
* Copyright (c) 2025 - altrouge
5+
*
6+
* This file is part of LMMS - https://lmms.io
7+
*
8+
* This program is free software; you can redistribute it and/or
9+
* modify it under the terms of the GNU General Public
10+
* License as published by the Free Software Foundation; either
11+
* version 2 of the License, or (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16+
* General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU General Public
19+
* License along with this program (see COPYING); if not, write to the
20+
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21+
* Boston, MA 02110-1301 USA.
22+
*
23+
*/
24+
25+
#ifndef LMMS_CONTROL_SURFACE_H
26+
#define LMMS_CONTROL_SURFACE_H
27+
28+
#include <QObject>
29+
30+
#include "lmms_export.h"
31+
32+
namespace lmms {
33+
//! Implements functions linking controller to LMMS.
34+
class LMMS_EXPORT ControlSurface : public QObject
35+
{
36+
Q_OBJECT
37+
public:
38+
ControlSurface();
39+
40+
public:
41+
signals:
42+
void requestPlay();
43+
void requestStop();
44+
void requestLoop();
45+
void requestRecord();
46+
void requestPreviousInstrumentTrack();
47+
void requestNextInstrumentTrack();
48+
49+
// Use slots to call from correct thread.
50+
private slots:
51+
///! Starts playing.
52+
void play();
53+
///! Stops playing.
54+
void stop();
55+
///! Activate/deactivate the loop.
56+
void loop();
57+
///! Starts recording.
58+
void record();
59+
///! Selects previous instrument track.
60+
void previousInstrumentTrack();
61+
///! Selects next instrument track.
62+
void nextInstrumentTrack();
63+
};
64+
} // namespace lmms
65+
66+
#endif

include/ControlSurfaceMCU.h

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* ControlSurfaceMCU.h - A controller to receive MIDI MCU control
3+
*
4+
* Copyright (c) 2025 - altrouge
5+
*
6+
* This file is part of LMMS - https://lmms.io
7+
*
8+
* This program is free software; you can redistribute it and/or
9+
* modify it under the terms of the GNU General Public
10+
* License as published by the Free Software Foundation; either
11+
* version 2 of the License, or (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16+
* General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU General Public
19+
* License along with this program (see COPYING); if not, write to the
20+
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21+
* Boston, MA 02110-1301 USA.
22+
*
23+
*/
24+
25+
#ifndef LMMS_CONTROL_SURFACE_MCU_H
26+
#define LMMS_CONTROL_SURFACE_MCU_H
27+
28+
#include "ControlSurface.h"
29+
#include "MidiEventProcessor.h"
30+
#include "MidiPort.h"
31+
#include "Note.h"
32+
33+
namespace lmms {
34+
//! Implements the Mackie control protocol to control the DAW.
35+
class LMMS_EXPORT ControlSurfaceMCU : public MidiEventProcessor
36+
{
37+
public:
38+
ControlSurfaceMCU(const QString& device);
39+
40+
void processInEvent(const MidiEvent& event, const TimePos& time = TimePos(), f_cnt_t offset = 0) override;
41+
void processOutEvent(const MidiEvent& event, const TimePos& time = TimePos(), f_cnt_t offset = 0) override;
42+
43+
private:
44+
MidiPort m_midiPort;
45+
ControlSurface m_controlSurface;
46+
};
47+
} // namespace lmms
48+
49+
#endif

include/InstrumentTrack.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,9 @@ class LMMS_EXPORT InstrumentTrack : public Track, public MidiEventProcessor
240240

241241
void autoAssignMidiDevice( bool );
242242

243+
/// Gets the auto assigned track (more or less the selected track).
244+
static InstrumentTrack* getAutoAssignedTrack() { return InstrumentTrack::s_autoAssignedTrack; }
245+
243246
signals:
244247
void instrumentChanged();
245248
void midiNoteOn( const lmms::Note& );

include/SetupDialog.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ private slots:
189189
MswMap m_midiIfaceSetupWidgets;
190190
trMap m_midiIfaceNames;
191191
QComboBox * m_assignableMidiDevices;
192+
QComboBox* m_MCUDawMidiDevices;
192193
bool m_midiAutoQuantize;
193194

194195
// Paths settings widgets.

include/Song.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ namespace lmms
4444
{
4545

4646
class AutomationTrack;
47+
class ControlSurfaceMCU;
4748
class Keymap;
4849
class MidiClip;
4950
class Scale;
@@ -378,6 +379,9 @@ class LMMS_EXPORT Song : public TrackContainer
378379

379380
Metronome& metronome() { return m_metronome; }
380381

382+
/// Set a DAW MCU surface control.
383+
void setControlSurfaceMCU();
384+
381385
public slots:
382386
void playSong();
383387
void record();
@@ -512,6 +516,7 @@ private slots:
512516
TimePos m_exportSongEnd;
513517
TimePos m_exportEffectiveLength;
514518

519+
std::shared_ptr<ControlSurfaceMCU> m_mcu_controller = nullptr;
515520
std::shared_ptr<Scale> m_scales[MaxScaleCount];
516521
std::shared_ptr<Keymap> m_keymaps[MaxKeymapCount];
517522

src/core/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ set(LMMS_SRCS
1414
core/Clipboard.cpp
1515
core/ComboBoxModel.cpp
1616
core/ConfigManager.cpp
17+
core/ControlSurface.cpp
1718
core/Controller.cpp
1819
core/ControllerConnection.cpp
1920
core/DataFile.cpp
@@ -120,6 +121,7 @@ set(LMMS_SRCS
120121
core/lv2/Lv2UridMap.cpp
121122
core/lv2/Lv2Worker.cpp
122123

124+
core/midi/ControlSurfaceMCU.cpp
123125
core/midi/MidiAlsaRaw.cpp
124126
core/midi/MidiAlsaSeq.cpp
125127
core/midi/MidiClient.cpp

src/core/ControlSurface.cpp

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* ControlSurface.cpp - Common control surface actions to lmms
3+
*
4+
* Copyright (c) 2025 - altrouge
5+
*
6+
* This file is part of LMMS - https://lmms.io
7+
*
8+
* This program is free software; you can redistribute it and/or
9+
* modify it under the terms of the GNU General Public
10+
* License as published by the Free Software Foundation; either
11+
* version 2 of the License, or (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16+
* General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU General Public
19+
* License along with this program (see COPYING); if not, write to the
20+
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21+
* Boston, MA 02110-1301 USA.
22+
*
23+
*/
24+
25+
#include "ControlSurface.h"
26+
27+
#include "Engine.h"
28+
#include "GuiApplication.h"
29+
#include "InstrumentTrack.h"
30+
#include "MidiClip.h"
31+
#include "PianoRoll.h"
32+
#include "Song.h"
33+
#include "SongEditor.h"
34+
35+
namespace lmms {
36+
37+
ControlSurface::ControlSurface()
38+
{
39+
QObject::connect(this, &ControlSurface::requestPlay, this, &ControlSurface::play);
40+
QObject::connect(this, &ControlSurface::requestStop, this, &ControlSurface::stop);
41+
QObject::connect(this, &ControlSurface::requestLoop, this, &ControlSurface::loop);
42+
QObject::connect(this, &ControlSurface::requestRecord, this, &ControlSurface::record);
43+
QObject::connect(
44+
this, &ControlSurface::requestPreviousInstrumentTrack, this, &ControlSurface::previousInstrumentTrack);
45+
QObject::connect(this, &ControlSurface::requestNextInstrumentTrack, this, &ControlSurface::nextInstrumentTrack);
46+
}
47+
48+
void ControlSurface::play()
49+
{
50+
Engine::getSong()->playSong();
51+
}
52+
53+
void ControlSurface::stop()
54+
{
55+
auto piano_roll = gui::getGUI()->pianoRoll();
56+
if (piano_roll != nullptr) { piano_roll->stop(); }
57+
Engine::getSong()->stop();
58+
}
59+
60+
void ControlSurface::loop()
61+
{
62+
// Activate on MidiClip for piano roll and Song for whole song.
63+
auto& timeline_midi = Engine::getSong()->getTimeline(Song::PlayMode::MidiClip);
64+
timeline_midi.setLoopEnabled(!timeline_midi.loopEnabled());
65+
auto& timeline = Engine::getSong()->getTimeline(Song::PlayMode::Song);
66+
timeline.setLoopEnabled(!timeline.loopEnabled());
67+
}
68+
69+
void ControlSurface::record()
70+
{
71+
// Get the clip.
72+
auto assigned_instrument_track = InstrumentTrack::getAutoAssignedTrack();
73+
if (assigned_instrument_track != nullptr)
74+
{
75+
std::vector<Clip*> clips;
76+
auto current_time = Engine::getSong()->getPlayPos(Song::PlayMode::Song);
77+
assigned_instrument_track->getClipsInRange(clips, current_time, current_time);
78+
MidiClip* current_clip = nullptr;
79+
80+
// If there are no available clips, create a clip.
81+
if (!clips.empty()) { current_clip = dynamic_cast<MidiClip*>(clips.front()); }
82+
else { current_clip = dynamic_cast<MidiClip*>(assigned_instrument_track->createClip(current_time)); }
83+
84+
auto piano_roll = gui::getGUI()->pianoRoll();
85+
piano_roll->setCurrentMidiClip(current_clip);
86+
piano_roll->recordAccompany();
87+
}
88+
}
89+
90+
void followingInstrumentTrack(bool reverse)
91+
{
92+
// Get the clip.
93+
auto assigned_instrument_track = InstrumentTrack::getAutoAssignedTrack();
94+
const auto track_list = Engine::getSong()->tracks();
95+
int next = reverse ? -1 : 1;
96+
if (track_list.empty()) { return; }
97+
98+
int start_ind = reverse ? track_list.size() - 1 : 0;
99+
if (assigned_instrument_track != nullptr)
100+
{
101+
for (size_t ind = 0; ind < track_list.size(); ++ind)
102+
{
103+
if (track_list[ind] == assigned_instrument_track)
104+
{
105+
start_ind = (ind + track_list.size() + next) % track_list.size();
106+
break;
107+
}
108+
}
109+
}
110+
for (size_t iteration = 0; iteration < track_list.size(); ++iteration)
111+
{
112+
size_t current_ind = (start_ind + track_list.size() + next * iteration) % track_list.size();
113+
if (track_list[current_ind]->type() == Track::Type::Instrument)
114+
{
115+
assigned_instrument_track = dynamic_cast<InstrumentTrack*>(track_list[current_ind]);
116+
assigned_instrument_track->autoAssignMidiDevice(true);
117+
break;
118+
}
119+
}
120+
}
121+
122+
void ControlSurface::previousInstrumentTrack()
123+
{
124+
followingInstrumentTrack(true);
125+
}
126+
127+
void ControlSurface::nextInstrumentTrack()
128+
{
129+
followingInstrumentTrack(false);
130+
}
131+
} // namespace lmms

src/core/Song.cpp

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
#include "ConfigManager.h"
3838
#include "ControllerRackView.h"
3939
#include "ControllerConnection.h"
40+
#include "ControlSurfaceMCU.h"
4041
#include "EnvelopeAndLfoParameters.h"
4142
#include "Mixer.h"
4243
#include "MixerView.h"
@@ -45,6 +46,7 @@
4546
#include "InstrumentTrack.h"
4647
#include "Keymap.h"
4748
#include "NotePlayHandle.h"
49+
#include "MidiClient.h"
4850
#include "MidiClip.h"
4951
#include "PatternEditor.h"
5052
#include "PatternStore.h"
@@ -167,8 +169,16 @@ void Song::setTempo()
167169
emit tempoChanged( tempo );
168170
}
169171

170-
171-
172+
void Song::setControlSurfaceMCU()
173+
{
174+
m_mcu_controller.reset();
175+
const QString& device = ConfigManager::inst()->value("midi", "midimcudaw");
176+
// Check if the device exists
177+
if (Engine::audioEngine()->midiClient()->readablePorts().indexOf(device) >= 0)
178+
{
179+
m_mcu_controller = std::make_shared<ControlSurfaceMCU>(device);
180+
}
181+
}
172182

173183
void Song::setTimeSignature()
174184
{
@@ -1169,6 +1179,14 @@ void Song::loadProject( const QString & fileName )
11691179
node = node.nextSibling();
11701180
}
11711181

1182+
// Set the midi MCU controller and update it when the config is updated.
1183+
setControlSurfaceMCU();
1184+
connect(ConfigManager::inst(), &ConfigManager::valueChanged,
1185+
[this](QString const& cls, QString const& attribute, QString const& value) {
1186+
if (!(cls == "midi" && attribute == "midimcudaw")) { return; }
1187+
setControlSurfaceMCU();
1188+
});
1189+
11721190
// quirk for fixing projects with broken positions of Clips inside pattern tracks
11731191
Engine::patternStore()->fixIncorrectPositions();
11741192

0 commit comments

Comments
 (0)