Skip to content

Commit

Permalink
Add a control surface MCU basic interface
Browse files Browse the repository at this point in the history
Adds compatibility with:
- Play button
- Stop button
- Loop button
- Record button
- Jog wheel to select instrument
  • Loading branch information
altrouge committed Jan 23, 2025
1 parent 0b0833b commit 3ace4be
Show file tree
Hide file tree
Showing 10 changed files with 462 additions and 7 deletions.
66 changes: 66 additions & 0 deletions include/ControlSurface.h
Original file line number Diff line number Diff line change
@@ -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 <QObject>

#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
49 changes: 49 additions & 0 deletions include/ControlSurfaceMCU.h
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions include/InstrumentTrack.h
Original file line number Diff line number Diff line change
Expand Up @@ -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& );
Expand Down
1 change: 1 addition & 0 deletions include/SetupDialog.h
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ private slots:
MswMap m_midiIfaceSetupWidgets;
trMap m_midiIfaceNames;
QComboBox * m_assignableMidiDevices;
QComboBox* m_MCUDawMidiDevices;
bool m_midiAutoQuantize;

// Paths settings widgets.
Expand Down
5 changes: 5 additions & 0 deletions include/Song.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ namespace lmms
{

class AutomationTrack;
class ControlSurfaceMCU;
class Keymap;
class MidiClip;
class Scale;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -512,6 +516,7 @@ private slots:
TimePos m_exportSongEnd;
TimePos m_exportEffectiveLength;

std::shared_ptr<ControlSurfaceMCU> m_mcu_controller = nullptr;
std::shared_ptr<Scale> m_scales[MaxScaleCount];
std::shared_ptr<Keymap> m_keymaps[MaxKeymapCount];

Expand Down
2 changes: 2 additions & 0 deletions src/core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
131 changes: 131 additions & 0 deletions src/core/ControlSurface.cpp
Original file line number Diff line number Diff line change
@@ -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<Clip*> 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<MidiClip*>(clips.front()); }
else { current_clip = dynamic_cast<MidiClip*>(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<InstrumentTrack*>(track_list[current_ind]);
assigned_instrument_track->autoAssignMidiDevice(true);
break;
}
}
}

void ControlSurface::previousInstrumentTrack()
{
followingInstrumentTrack(true);
}

void ControlSurface::nextInstrumentTrack()
{
followingInstrumentTrack(false);
}
} // namespace lmms
22 changes: 20 additions & 2 deletions src/core/Song.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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<ControlSurfaceMCU>(device);
}
}

void Song::setTimeSignature()
{
Expand Down Expand Up @@ -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();

Expand Down
Loading

0 comments on commit 3ace4be

Please sign in to comment.