diff --git a/data/themes/classic/midi_cc_rack.png b/data/themes/classic/midi_cc_rack.png new file mode 100644 index 00000000000..1fe8bb9cb24 Binary files /dev/null and b/data/themes/classic/midi_cc_rack.png differ diff --git a/data/themes/default/midi_cc_rack.png b/data/themes/default/midi_cc_rack.png new file mode 100644 index 00000000000..1fe8bb9cb24 Binary files /dev/null and b/data/themes/default/midi_cc_rack.png differ diff --git a/include/AutomatableModelView.h b/include/AutomatableModelView.h index 029ea16fba7..a5996118595 100644 --- a/include/AutomatableModelView.h +++ b/include/AutomatableModelView.h @@ -50,6 +50,7 @@ class LMMS_EXPORT AutomatableModelView : public ModelView } void setModel( Model* model, bool isOldModelValid = true ) override; + void unsetModel() override; template inline T value() const diff --git a/include/InstrumentTrack.h b/include/InstrumentTrack.h index b4bac9136a7..aea007fec65 100644 --- a/include/InstrumentTrack.h +++ b/include/InstrumentTrack.h @@ -30,6 +30,8 @@ #include "GroupBox.h" #include "InstrumentFunctions.h" #include "InstrumentSoundShaping.h" +#include "Midi.h" +#include "MidiCCRackView.h" #include "MidiEventProcessor.h" #include "MidiPort.h" #include "NotePlayHandle.h" @@ -229,7 +231,6 @@ class LMMS_EXPORT InstrumentTrack : public Track, public MidiEventProcessor void newNote(); void endNote(); - protected: QString nodeName() const override { @@ -248,6 +249,8 @@ protected slots: private: + void processCCEvent(int controller); + MidiPort m_midiPort; NotePlayHandle* m_notes[NumKeys]; @@ -287,11 +290,14 @@ protected slots: Piano m_piano; + std::unique_ptr m_midiCCEnable; + std::unique_ptr m_midiCCModel[MidiControllerCount]; friend class InstrumentTrackView; friend class InstrumentTrackWindow; friend class NotePlayHandle; friend class InstrumentMiscView; + friend class MidiCCRackView; } ; @@ -339,6 +345,7 @@ class InstrumentTrackView : public TrackView private slots: void toggleInstrumentWindow( bool _on ); + void toggleMidiCCRack(); void activityIndicatorPressed(); void activityIndicatorReleased(); @@ -366,6 +373,8 @@ private slots: QAction * m_midiInputAction; QAction * m_midiOutputAction; + std::unique_ptr m_midiCCRackView; + QPoint m_lastPos; FadeButton * getActivityIndicator() override diff --git a/include/MidiCCRackView.h b/include/MidiCCRackView.h new file mode 100644 index 00000000000..9d015e4e8fb --- /dev/null +++ b/include/MidiCCRackView.h @@ -0,0 +1,40 @@ +#ifndef MIDI_CC_RACK_VIEW_H +#define MIDI_CC_RACK_VIEW_H + +#include + +#include "GroupBox.h" +#include "Knob.h" +#include "Midi.h" +#include "SerializingObject.h" + +class InstrumentTrack; + +class MidiCCRackView : public QWidget, public SerializingObject +{ + Q_OBJECT +public: + MidiCCRackView(InstrumentTrack * track); + ~MidiCCRackView() override; + + void saveSettings(QDomDocument & doc, QDomElement & parent) override; + void loadSettings(const QDomElement &) override; + + inline QString nodeName() const override + { + return "MidiCCRackView"; + } + +private slots: + void renameWindow(); + +private: + InstrumentTrack *m_track; + + GroupBox *m_midiCCGroupBox; // MIDI CC GroupBox (used to enable disable MIDI CC) + + Knob *m_controllerKnob[MidiControllerCount]; // Holds the knob widgets for each controller + +}; + +#endif diff --git a/include/MidiEvent.h b/include/MidiEvent.h index 907314b4866..177ef75ea94 100644 --- a/include/MidiEvent.h +++ b/include/MidiEvent.h @@ -33,27 +33,30 @@ class MidiEvent { public: - MidiEvent( MidiEventTypes type = MidiActiveSensing, + MidiEvent(MidiEventTypes type = MidiActiveSensing, int8_t channel = 0, int16_t param1 = 0, int16_t param2 = 0, - const void* sourcePort = NULL ) : + const void* sourcePort = nullptr, + bool ignoreOnExport = true) : m_type( type ), m_metaEvent( MidiMetaInvalid ), m_channel( channel ), m_sysExData( NULL ), - m_sourcePort( sourcePort ) + m_sourcePort(sourcePort), + m_ignoreOnExport(ignoreOnExport) { m_data.m_param[0] = param1; m_data.m_param[1] = param2; } - MidiEvent( MidiEventTypes type, const char* sysExData, int dataLen ) : + MidiEvent(MidiEventTypes type, const char* sysExData, int dataLen, bool ignoreOnExport = true) : m_type( type ), m_metaEvent( MidiMetaInvalid ), m_channel( 0 ), m_sysExData( sysExData ), - m_sourcePort( NULL ) + m_sourcePort(nullptr), + m_ignoreOnExport(ignoreOnExport) { m_data.m_sysExDataLen = dataLen; } @@ -64,7 +67,8 @@ class MidiEvent m_channel( other.m_channel ), m_data( other.m_data ), m_sysExData( other.m_sysExData ), - m_sourcePort( other.m_sourcePort ) + m_sourcePort(other.m_sourcePort), + m_ignoreOnExport(other.m_ignoreOnExport) { } @@ -190,6 +194,16 @@ class MidiEvent setParam( 0, pitchBend ); } + bool ignoreOnExport() const + { + return m_ignoreOnExport; + } + + void setIgnoreOnExport(bool value) + { + m_ignoreOnExport = value; + } + private: MidiEventTypes m_type; // MIDI event type @@ -205,6 +219,9 @@ class MidiEvent const char* m_sysExData; const void* m_sourcePort; + // This helps us ignore MIDI events that shouldn't be processed + // during a project export, like physical controller events. + bool m_ignoreOnExport; } ; #endif diff --git a/include/ModelView.h b/include/ModelView.h index 907cd14efdf..04229ed0d2f 100644 --- a/include/ModelView.h +++ b/include/ModelView.h @@ -36,6 +36,7 @@ class LMMS_EXPORT ModelView virtual ~ModelView(); virtual void setModel( Model* model, bool isOldModelValid = true ); + virtual void unsetModel(); Model* model() { diff --git a/src/gui/AutomatableModelView.cpp b/src/gui/AutomatableModelView.cpp index 6a74ada90fa..efd6967cf18 100644 --- a/src/gui/AutomatableModelView.cpp +++ b/src/gui/AutomatableModelView.cpp @@ -142,6 +142,31 @@ void AutomatableModelView::setModel( Model* model, bool isOldModelValid ) +// Unsets the current model by setting a dummy empty model. The dummy model is marked as +// "defaultConstructed", so the next call to setModel will delete it. +void AutomatableModelView::unsetModel() +{ + if (dynamic_cast(this)) + { + setModel(new FloatModel(0, 0, 0, 1, nullptr, QString(), true)); + } + else if (dynamic_cast(this)) + { + setModel(new IntModel(0, 0, 0, nullptr, QString(), true)); + } + else if (dynamic_cast(this)) + { + setModel(new BoolModel(false, nullptr, QString(), true)); + } + else + { + ModelView::unsetModel(); + } +} + + + + void AutomatableModelView::mousePressEvent( QMouseEvent* event ) { if( event->button() == Qt::LeftButton && event->modifiers() & Qt::ControlModifier ) diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index e1be929aab1..193c0b9012c 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -22,6 +22,7 @@ SET(LMMS_SRCS gui/Lv2ViewBase.cpp gui/MainApplication.cpp gui/MainWindow.cpp + gui/MidiCCRackView.cpp gui/MidiSetupWidget.cpp gui/ModelView.cpp gui/PeakControllerDialog.cpp diff --git a/src/gui/MidiCCRackView.cpp b/src/gui/MidiCCRackView.cpp new file mode 100644 index 00000000000..4d932fd1c5d --- /dev/null +++ b/src/gui/MidiCCRackView.cpp @@ -0,0 +1,133 @@ +/* + * MidiCCRackView.cpp - implementation of the MIDI CC rack widget + * + * Copyright (c) 2020 Ian Caio + * + * 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 "MidiCCRackView.h" + +#include +#include +#include +#include +#include + +#include "embed.h" +#include "GroupBox.h" +#include "GuiApplication.h" +#include "InstrumentTrack.h" +#include "Knob.h" +#include "MainWindow.h" +#include "Track.h" + + +MidiCCRackView::MidiCCRackView(InstrumentTrack * track) : + QWidget(), + m_track(track) +{ + setWindowIcon(embed::getIconPixmap("midi_cc_rack")); + setWindowTitle(tr("MIDI CC Rack - %1").arg(m_track->name())); + + QMdiSubWindow * subWin = gui->mainWindow()->addWindowedWidget(this); + + // Remove maximize button + Qt::WindowFlags flags = subWin->windowFlags(); + flags &= ~Qt::WindowMaximizeButtonHint; + subWin->setWindowFlags(flags); + + // Adjust window attributes, sizing and position + subWin->setAttribute(Qt::WA_DeleteOnClose, false); + subWin->resize(350, 300); + subWin->setFixedWidth(350); + subWin->setMinimumHeight(300); + subWin->hide(); + + // Main window layout + QVBoxLayout *mainLayout = new QVBoxLayout(this); + + // Knobs GroupBox - Here we have the MIDI CC controller knobs for the selected track + m_midiCCGroupBox = new GroupBox(tr("MIDI CC Knobs:")); + + // Layout to keep scrollable area under the GroupBox header + QVBoxLayout *knobsGroupBoxLayout = new QVBoxLayout(); + knobsGroupBoxLayout->setContentsMargins(5, 16, 5, 5); + + m_midiCCGroupBox->setLayout(knobsGroupBoxLayout); + + // Scrollable area + widget + its layout that will have all the knobs + QScrollArea *knobsScrollArea = new QScrollArea(); + QWidget *knobsArea = new QWidget(); + QGridLayout *knobsAreaLayout = new QGridLayout(); + + knobsArea->setLayout(knobsAreaLayout); + knobsScrollArea->setWidget(knobsArea); + knobsScrollArea->setWidgetResizable(true); + + knobsGroupBoxLayout->addWidget(knobsScrollArea); + + // Adds the controller knobs + for (int i = 0; i < MidiControllerCount; ++i) + { + m_controllerKnob[i] = new Knob(knobBright_26); + m_controllerKnob[i]->setLabel(tr("CC %1").arg(i)); + knobsAreaLayout->addWidget(m_controllerKnob[i], i/4, i%4); + } + + // Set all the models + // Set the LED button to enable/disable the track midi cc + m_midiCCGroupBox->setModel(m_track->m_midiCCEnable.get()); + + // Set the model for each Knob + for (int i = 0; i < MidiControllerCount; ++i) + { + m_controllerKnob[i]->setModel(m_track->m_midiCCModel[i].get()); + } + + // Connection to update the name of the track on the label + connect(m_track, SIGNAL(nameChanged()), + this, SLOT(renameWindow())); + + // Adding everything to the main layout + mainLayout->addWidget(m_midiCCGroupBox); +} + +MidiCCRackView::~MidiCCRackView() +{ + if(parentWidget()) + { + parentWidget()->hide(); + parentWidget()->deleteLater(); + } +} + +void MidiCCRackView::renameWindow() +{ + setWindowTitle(tr("MIDI CC Rack - %1").arg(m_track->name())); +} + +void MidiCCRackView::saveSettings(QDomDocument & doc, QDomElement & parent) +{ +} + +void MidiCCRackView::loadSettings(const QDomElement &) +{ +} diff --git a/src/gui/ModelView.cpp b/src/gui/ModelView.cpp index 3b250fbcd3f..f9031ea9484 100644 --- a/src/gui/ModelView.cpp +++ b/src/gui/ModelView.cpp @@ -74,6 +74,16 @@ void ModelView::setModel( Model* model, bool isOldModelValid ) +// Unsets the current model by setting a dummy empty model. The dummy model is marked as +// "defaultConstructed", so the next call to setModel will delete it. +void ModelView::unsetModel() +{ + setModel(new Model(nullptr, QString(), true)); +} + + + + void ModelView::doConnections() { if( m_model != NULL ) diff --git a/src/tracks/InstrumentTrack.cpp b/src/tracks/InstrumentTrack.cpp index a8c1343aae9..9057c9e1214 100644 --- a/src/tracks/InstrumentTrack.cpp +++ b/src/tracks/InstrumentTrack.cpp @@ -74,6 +74,7 @@ #include "StringPairDrag.h" #include "TrackContainerView.h" #include "TrackLabelButton.h" +#include "MidiCCRackView.h" const int INSTRUMENT_WIDTH = 254; @@ -121,6 +122,21 @@ InstrumentTrack::InstrumentTrack( TrackContainer* tc ) : } + // Initialize the m_midiCCEnabled variable, but it's actually going to be connected + // to a LedButton + m_midiCCEnable = std::make_unique(false, nullptr, tr("Enable/Disable MIDI CC")); + + // Initialize the MIDI CC controller models and connect them to the method that processes + // the midi cc events + for (int i = 0; i < MidiControllerCount; ++i) + { + m_midiCCModel[i] = std::make_unique(0.0f, 0.0f, 127.0f, 1.0f, + nullptr, tr("CC Controller %1").arg(i)); + + connect(m_midiCCModel[i].get(), &FloatModel::dataChanged, + this, [this, i]{ processCCEvent(i); }, Qt::DirectConnection); + } + setName( tr( "Default preset" ) ); connect( &m_baseNoteModel, SIGNAL( dataChanged() ), @@ -244,9 +260,27 @@ MidiEvent InstrumentTrack::applyMasterKey( const MidiEvent& event ) +void InstrumentTrack::processCCEvent(int controller) +{ + // Does nothing if the LED is disabled + if (!m_midiCCEnable->value()) { return; } + + uint8_t channel = static_cast(midiPort()->realOutputChannel()); + uint16_t cc = static_cast(controller); + uint16_t value = static_cast(m_midiCCModel[controller]->value()); + + // Process the MIDI CC event as an input event but with ignoreOnExport set to false + // so we can know LMMS generated the event, not a controller, and can process it during + // the project export + processInEvent(MidiEvent(MidiControlChange, channel, cc, value, NULL, false)); +} + + + + void InstrumentTrack::processInEvent( const MidiEvent& event, const TimePos& time, f_cnt_t offset ) { - if( Engine::getSong()->isExporting() ) + if (Engine::getSong()->isExporting() && event.ignoreOnExport()) { return; } @@ -375,9 +409,11 @@ void InstrumentTrack::processInEvent( const MidiEvent& event, const TimePos& tim break; } - if( eventHandled == false && instrument()->handleMidiEvent( event, time, offset ) == false ) + // If the event wasn't handled, check if there's a loaded instrument and if so send the + // event to it. If it returns false means the instrument didn't handle the event, so we trigger a warning. + if (eventHandled == false && !(instrument() && instrument()->handleMidiEvent(event, time, offset))) { - qWarning( "InstrumentTrack: unhandled MIDI event %d", event.type() ); + qWarning("InstrumentTrack: unhandled MIDI event %d", event.type()); } } @@ -742,6 +778,15 @@ void InstrumentTrack::saveTrackSpecificSettings( QDomDocument& doc, QDomElement m_baseNoteModel.saveSettings( doc, thisElement, "basenote" ); m_useMasterPitchModel.saveSettings( doc, thisElement, "usemasterpitch"); + // Save MIDI CC stuff + m_midiCCEnable->saveSettings(doc, thisElement, "enablecc"); + QDomElement midiCC = doc.createElement("midicontrollers"); + thisElement.appendChild(midiCC); + for (int i = 0; i < MidiControllerCount; ++i) + { + m_midiCCModel[i]->saveSettings(doc, midiCC, "cc" + QString::number(i)); + } + if( m_instrument != NULL ) { QDomElement i = doc.createElement( "instrument" ); @@ -800,6 +845,10 @@ void InstrumentTrack::loadTrackSpecificSettings( const QDomElement & thisElement // clear effect-chain just in case we load an old preset without FX-data m_audioPort.effects()->clear(); + // We set MIDI CC enable to false so the knobs don't trigger MIDI CC events while + // they are being loaded. After all knobs are loaded we load the right value of m_midiCCEnable. + m_midiCCEnable->setValue(false); + QDomNode node = thisElement.firstChild(); while( !node.isNull() ) { @@ -844,6 +893,13 @@ void InstrumentTrack::loadTrackSpecificSettings( const QDomElement & thisElement emit instrumentChanged(); } } + else if (node.nodeName() == "midicontrollers") + { + for (int i = 0; i < MidiControllerCount; ++i) + { + m_midiCCModel[i]->loadSettings(node.toElement(), "cc" + QString::number(i)); + } + } // compat code - if node-name doesn't match any known // one, we assume that it is an instrument-plugin // which we'll try to load @@ -864,6 +920,10 @@ void InstrumentTrack::loadTrackSpecificSettings( const QDomElement & thisElement } node = node.nextSibling(); } + + // Load the right value of m_midiCCEnable + m_midiCCEnable->loadSettings(thisElement, "enablecc"); + updatePitchRange(); unlock(); } @@ -999,7 +1059,7 @@ InstrumentTrackView::InstrumentTrackView( InstrumentTrack * _it, TrackContainerV m_panningKnob = new Knob( knobSmall_17, getTrackSettingsWidget(), tr( "Panning" ) ); m_panningKnob->setModel( &_it->m_panningModel ); - m_panningKnob->setHintText( tr( "Panning:" ), "%" ); + m_panningKnob->setHintText(tr("Panning:"), "%"); m_panningKnob->move( widgetWidth-24, 2 ); m_panningKnob->setLabel( tr( "PAN" ) ); m_panningKnob->show(); @@ -1039,6 +1099,11 @@ InstrumentTrackView::InstrumentTrackView( InstrumentTrack * _it, TrackContainerV m_midiInputAction->setText( tr( "Input" ) ); m_midiOutputAction->setText( tr( "Output" ) ); + QAction *midiRackAction = m_midiMenu->addAction(tr("Open/Close MIDI CC Rack")); + midiRackAction->setIcon(embed::getIconPixmap("midi_cc_rack")); + connect(midiRackAction, SIGNAL(triggered()), + this, SLOT(toggleMidiCCRack())); + m_activityIndicator = new FadeButton( QApplication::palette().color( QPalette::Active, QPalette::Background), QApplication::palette().color( QPalette::Active, @@ -1075,6 +1140,29 @@ InstrumentTrackView::~InstrumentTrackView() +void InstrumentTrackView::toggleMidiCCRack() +{ + // Lazy creation: midiCCRackView is only created when accessed the first time. + // this->model() returns pointer to the InstrumentTrack who owns this InstrumentTrackView. + if (!m_midiCCRackView) + { + m_midiCCRackView = std::unique_ptr(new MidiCCRackView(this->model())); + } + + if (m_midiCCRackView->parentWidget()->isVisible()) + { + m_midiCCRackView->parentWidget()->hide(); + } + else + { + m_midiCCRackView->parentWidget()->show(); + m_midiCCRackView->show(); + } +} + + + + InstrumentTrackWindow * InstrumentTrackView::topLevelInstrumentTrackWindow() { InstrumentTrackWindow * w = NULL;