Skip to content

Instantly share code, notes, and snippets.

@badosu
Created February 4, 2015 00:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save badosu/120eaf72dc3847049ce0 to your computer and use it in GitHub Desktop.
Save badosu/120eaf72dc3847049ce0 to your computer and use it in GitHub Desktop.
diff --git a/include/ExportFilter.h b/include/ExportFilter.h
new file mode 100644
index 0000000..857ed1b
--- /dev/null
+++ b/include/ExportFilter.h
@@ -0,0 +1,64 @@
+/*
+ * ExportFilter.h - declaration of class ExportFilter, the base-class for all
+ * file export filters
+ *
+ * Copyright (c) 2006-2014 Tobias Doerffel <tobydox/at/users.sourceforge.net>
+ *
+ * This file is part of LMMS - http://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 EXPORT_FILTER_H
+#define EXPORT_FILTER_H
+
+#include <QtCore/QFile>
+
+#include "TrackContainer.h"
+#include "Plugin.h"
+
+
+class EXPORT ExportFilter : public Plugin
+{
+public:
+ ExportFilter( const Descriptor * _descriptor ) : Plugin( _descriptor, NULL ) {}
+ virtual ~ExportFilter() {}
+
+
+ virtual bool tryExport( const TrackContainer::TrackList &tracks, int tempo, const QString &filename ) = 0;
+protected:
+
+ virtual void saveSettings( QDomDocument &, QDomElement & )
+ {
+ }
+
+ virtual void loadSettings( const QDomElement & )
+ {
+ }
+
+ virtual QString nodeName() const
+ {
+ return "import_filter";
+ }
+
+
+private:
+
+} ;
+
+
+#endif
diff --git a/include/Song.h b/include/Song.h
index 5da0981..487e856 100644
--- a/include/Song.h
+++ b/include/Song.h
@@ -268,6 +268,7 @@ public slots:
void importProject();
void exportProject(bool multiExport=false);
void exportProjectTracks();
+ void exportProjectMidi();
void startExport();
void stopExport();
diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt
index d7b4cf1..56212ef 100644
--- a/plugins/CMakeLists.txt
+++ b/plugins/CMakeLists.txt
@@ -20,6 +20,7 @@ ADD_SUBDIRECTORY(LadspaEffect)
ADD_SUBDIRECTORY(lb302)
#ADD_SUBDIRECTORY(lb303)
ADD_SUBDIRECTORY(MidiImport)
+ADD_SUBDIRECTORY(MidiExport)
ADD_SUBDIRECTORY(MultitapEcho)
ADD_SUBDIRECTORY(monstro)
ADD_SUBDIRECTORY(nes)
diff --git a/plugins/MidiExport/CMakeLists.txt b/plugins/MidiExport/CMakeLists.txt
new file mode 100644
index 0000000..1d19f08
--- /dev/null
+++ b/plugins/MidiExport/CMakeLists.txt
@@ -0,0 +1,4 @@
+INCLUDE(BuildPlugin)
+
+BUILD_PLUGIN(midiexport MidiExport.cpp MidiExport.h MidiFile.hpp
+ MOCFILES MidiExport.h)
diff --git a/plugins/MidiExport/MidiExport.cpp b/plugins/MidiExport/MidiExport.cpp
new file mode 100644
index 0000000..03ef3a4
--- /dev/null
+++ b/plugins/MidiExport/MidiExport.cpp
@@ -0,0 +1,183 @@
+/*
+ * MidiExport.cpp - support for importing MIDI files
+ *
+ * Author: Mohamed Abdel Maksoud <mohamed at amaksoud.com>
+ *
+ * This file is part of LMMS - http://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 <QDomDocument>
+#include <QDir>
+#include <QApplication>
+#include <QMessageBox>
+#include <QProgressDialog>
+
+#include "MidiExport.h"
+#include "Engine.h"
+#include "TrackContainer.h"
+#include "InstrumentTrack.h"
+
+
+extern "C"
+{
+
+Plugin::Descriptor PLUGIN_EXPORT midiexport_plugin_descriptor =
+{
+ STRINGIFY( PLUGIN_NAME ),
+ "MIDI Export",
+ QT_TRANSLATE_NOOP( "pluginBrowser",
+ "Filter for exporting MIDI-files from LMMS" ),
+ "Mohamed Abdel Maksoud <mohamed at amaksoud.com>",
+ 0x0100,
+ Plugin::ExportFilter,
+ NULL,
+ NULL,
+ NULL
+} ;
+
+}
+
+
+MidiExport::MidiExport() : ExportFilter( &midiexport_plugin_descriptor)
+{
+}
+
+
+
+
+MidiExport::~MidiExport()
+{
+}
+
+
+
+bool MidiExport::tryExport( const TrackContainer::TrackList &tracks, int tempo, const QString &filename )
+{
+ QFile f(filename);
+ f.open(QIODevice::WriteOnly);
+ QDataStream midiout(&f);
+
+ InstrumentTrack* instTrack;
+ QDomElement element;
+
+
+ int nTracks = 0;
+ const int BUFFER_SIZE = 50*1024;
+ uint8_t buffer[BUFFER_SIZE];
+ uint32_t size;
+
+ foreach( Track* track, tracks ) if( track->type() == Track::InstrumentTrack ) nTracks++;
+
+ // midi header
+ MidiFile::MIDIHeader header(nTracks);
+ size = header.writeToBuffer(buffer);
+ midiout.writeRawData((char *)buffer, size);
+
+ // midi tracks
+ foreach( Track* track, tracks )
+ {
+ DataFile dataFile( DataFile::SongProject );
+ MidiFile::MIDITrack<BUFFER_SIZE> mtrack;
+
+ if( track->type() != Track::InstrumentTrack ) continue;
+
+ //qDebug() << "exporting " << track->name();
+
+
+ mtrack.addName(track->name().toStdString(), 0);
+ //mtrack.addProgramChange(0, 0);
+ mtrack.addTempo(tempo, 0);
+
+ instTrack = dynamic_cast<InstrumentTrack *>( track );
+ element = instTrack->saveState( dataFile, dataFile.content() );
+
+ // instrumentTrack
+ // - instrumentTrack
+ // - pattern
+ int base_pitch = 0;
+ double base_volume = 1.0;
+ int base_time = 0;
+
+
+ for(QDomNode n = element.firstChild(); !n.isNull(); n = n.nextSibling())
+ {
+ //QDomText txt = n.toText();
+ //qDebug() << ">> child node " << n.nodeName();
+
+ if (n.nodeName() == "instrumenttrack")
+ {
+ // TODO interpret pan="0" fxch="0" usemasterpitch="1" pitchrange="1" pitch="0" basenote="57"
+ QDomElement it = n.toElement();
+ base_pitch = it.attribute("pitch", "0").toInt();
+ base_volume = it.attribute("volume", "100").toDouble()/100.0;
+ }
+
+ if (n.nodeName() == "pattern")
+ {
+ base_time = n.toElement().attribute("pos", "0").toInt();
+ // TODO interpret steps="12" muted="0" type="1" name="Piano1" len="2592"
+ for(QDomNode nn = n.firstChild(); !nn.isNull(); nn = nn.nextSibling())
+ {
+ QDomElement note = nn.toElement();
+ if (note.attribute("len", "0") == "0" || note.attribute("vol", "0") == "0") continue;
+ #if 0
+ qDebug() << ">>>> key " << note.attribute( "key", "0" )
+ << " " << note.attribute("len", "0") << " @"
+ << note.attribute("pos", "0");
+ #endif
+ mtrack.addNote(
+ note.attribute("key", "0").toInt()+base_pitch
+ , 100 * base_volume * (note.attribute("vol", "100").toDouble()/100)
+ , (base_time+note.attribute("pos", "0").toDouble())/48
+ , (note.attribute("len", "0")).toDouble()/48);
+ }
+ }
+
+ }
+ size = mtrack.writeToBuffer(buffer);
+ midiout.writeRawData((char *)buffer, size);
+ } // for each track
+
+ return true;
+
+}
+
+
+
+
+void MidiExport::error()
+{
+ //qDebug() << "MidiExport error: " << m_error ;
+}
+
+
+
+extern "C"
+{
+
+// necessary for getting instance out of shared lib
+Plugin * PLUGIN_EXPORT lmms_plugin_main( Model *, void * _data )
+{
+ return new MidiExport();
+}
+
+
+}
+
diff --git a/plugins/MidiExport/MidiExport.h b/plugins/MidiExport/MidiExport.h
new file mode 100644
index 0000000..d829a8b
--- /dev/null
+++ b/plugins/MidiExport/MidiExport.h
@@ -0,0 +1,58 @@
+/*
+ * MidiExport.h - support for Exporting MIDI-files
+ *
+ * Author: Mohamed Abdel Maksoud <mohamed at amaksoud.com>
+ *
+ * This file is part of LMMS - http://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 _MIDI_EXPORT_H
+#define _MIDI_EXPORT_H
+
+#include <QString>
+
+#include "ExportFilter.h"
+#include "MidiFile.hpp"
+
+
+
+class MidiExport: public ExportFilter
+{
+// Q_OBJECT
+public:
+ MidiExport( );
+ ~MidiExport();
+
+ virtual PluginView * instantiateView( QWidget * )
+ {
+ return( NULL );
+ }
+
+ virtual bool tryExport( const TrackContainer::TrackList &tracks, int tempo, const QString &filename );
+
+private:
+
+
+ void error( void );
+
+
+} ;
+
+
+#endif
diff --git a/plugins/MidiExport/MidiFile.hpp b/plugins/MidiExport/MidiFile.hpp
new file mode 100644
index 0000000..33955e1
--- /dev/null
+++ b/plugins/MidiExport/MidiFile.hpp
@@ -0,0 +1,320 @@
+#ifndef MIDIFILE_HPP
+#define MIDIFILE_HPP
+
+/**
+ * Name: MidiFile.hpp
+ * Purpose: C++ re-write of the python module MidiFile.py
+ * Author: Mohamed Abdel Maksoud <mohamed at amaksoud.com>
+ *-----------------------------------------------------------------------------
+ * Name: MidiFile.py
+ * Purpose: MIDI file manipulation utilities
+ *
+ * Author: Mark Conway Wirt <emergentmusics) at (gmail . com>
+ *
+ * Created: 2008/04/17
+ * Copyright: (c) 2009 Mark Conway Wirt
+ * License: Please see License.txt for the terms under which this
+ * software is distributed.
+ *-----------------------------------------------------------------------------
+ */
+
+#include <string.h>
+#include <stdint.h>
+#include <string>
+#include <vector>
+#include <set>
+#include <algorithm>
+#include <assert.h>
+
+using std::string;
+using std::vector;
+using std::set;
+
+namespace MidiFile
+{
+
+const int TICKSPERBEAT = 128;
+
+
+int writeVarLength(uint32_t val, uint8_t *buffer)
+{
+ /*
+ Accept an input, and write a MIDI-compatible variable length stream
+
+ The MIDI format is a little strange, and makes use of so-called variable
+ length quantities. These quantities are a stream of bytes. If the most
+ significant bit is 1, then more bytes follow. If it is zero, then the
+ byte in question is the last in the stream
+ */
+ int size = 0;
+ uint8_t result, little_endian[4];
+ result = val & 0x7F;
+ little_endian[size++] = result;
+ val = val >> 7;
+ while (val > 0)
+ {
+ result = val & 0x7F;
+ result = result | 0x80;
+ little_endian[size++] = result;
+ val = val >> 7;
+ }
+ for (int i=0; i<size; i++)
+ {
+ buffer[i] = little_endian[size-i-1];
+ }
+
+ return size;
+}
+
+int writeBigEndian4(uint32_t val, uint8_t *buf)
+{
+ buf[0] = val >> 24;
+ buf[1] = val >> 16 & 0xff;
+ buf[2] = val >> 8 & 0xff;
+ buf[3] = val & 0xff;
+ return 4;
+}
+
+int writeBigEndian2(uint16_t val, uint8_t *buf)
+{
+ buf[0] = val >> 8 & 0xff;
+ buf[1] = val & 0xff;
+ return 2;
+}
+
+
+class MIDIHeader
+{
+ // Class to encapsulate the MIDI header structure.
+ uint16_t numTracks;
+ uint16_t ticksPerBeat;
+
+ public:
+
+ MIDIHeader(uint16_t nTracks, uint16_t ticksPB=TICKSPERBEAT): numTracks(nTracks), ticksPerBeat(ticksPB) {}
+
+ inline int writeToBuffer(uint8_t *buffer, int start=0) const
+ {
+ // chunk ID
+ buffer[start++] = 'M'; buffer[start++] = 'T'; buffer[start++] = 'h'; buffer[start++] = 'd';
+ // chunk size (6 bytes always)
+ buffer[start++] = 0; buffer[start++] = 0; buffer[start++] = 0; buffer[start++] = 0x06;
+ // format: 1 (multitrack)
+ buffer[start++] = 0; buffer[start++] = 0x01;
+
+ start += writeBigEndian2(numTracks, buffer+start);
+
+ start += writeBigEndian2(ticksPerBeat, buffer+start);
+
+ return start;
+ }
+
+};
+
+
+struct Event
+{
+ uint32_t time;
+ uint32_t tempo;
+ string trackName;
+ enum {NOTE_ON, NOTE_OFF, TEMPO, PROG_CHANGE, TRACK_NAME} type;
+ // TODO make a union to save up space
+ uint8_t pitch;
+ uint8_t programNumber;
+ uint8_t duration;
+ uint8_t volume;
+ uint8_t channel;
+
+ Event() {time=tempo=pitch=programNumber=duration=volume=channel=0; trackName="";}
+
+ inline int writeToBuffer(uint8_t *buffer) const
+ {
+ uint8_t code, fourbytes[4];
+ int size=0;
+ switch (type)
+ {
+ case NOTE_ON:
+ code = 0x9 << 4 | channel;
+ size += writeVarLength(time, buffer+size);
+ buffer[size++] = code;
+ buffer[size++] = pitch;
+ buffer[size++] = volume;
+ break;
+ case NOTE_OFF:
+ code = 0x8 << 4 | channel;
+ size += writeVarLength(time, buffer+size);
+ buffer[size++] = code;
+ buffer[size++] = pitch;
+ buffer[size++] = volume;
+ break;
+ case TEMPO:
+ code = 0xFF;
+ size += writeVarLength(time, buffer+size);
+ buffer[size++] = code;
+ buffer[size++] = 0x51;
+ buffer[size++] = 0x03;
+ writeBigEndian4(int(60000000.0 / tempo), fourbytes);
+
+ //printf("tempo of %x translates to ", tempo);
+ for (int i=0; i<3; i++) printf("%02x ", fourbytes[i+1]);
+ printf("\n");
+ buffer[size++] = fourbytes[1];
+ buffer[size++] = fourbytes[2];
+ buffer[size++] = fourbytes[3];
+ break;
+ case PROG_CHANGE:
+ code = 0xC << 4 | channel;
+ size += writeVarLength(time, buffer+size);
+ buffer[size++] = code;
+ buffer[size++] = programNumber;
+ break;
+ case TRACK_NAME:
+ size += writeVarLength(time, buffer+size);
+ buffer[size++] = 0xFF;
+ buffer[size++] = 0x03;
+ size += writeVarLength(trackName.size(), buffer+size);
+ trackName.copy((char *)(&buffer[size]), trackName.size());
+ size += trackName.size();
+// buffer[size++] = '\0';
+// buffer[size++] = '\0';
+
+ break;
+ }
+ return size;
+ } // writeEventsToBuffer
+
+
+ // events are sorted by their time
+ inline bool operator < (const Event& b) const {
+ if (this->time < b.time) return true;
+#if 0
+ if (this->type < b.type) return true;
+ if (this->pitch < b.pitch) return true;
+ if (this->duration < b.duration) return true;
+ if (this->volume < b.volume) return true;
+ #if 1
+ if (this->programNumber < b.programNumber) return true;
+ if (this->channel < b.channel) return true;
+ if (this->trackName < b.trackName) return true;
+ #endif
+#endif
+ return false;
+ }
+};
+
+template<const int MAX_TRACK_SIZE>
+class MIDITrack
+{
+ // A class that encapsulates a MIDI track
+ // Nested class definitions.
+ vector<Event> events;
+
+ public:
+ uint8_t channel;
+
+ MIDITrack(): channel(0) {}
+
+ inline void addEvent(const Event &e)
+ {
+ Event E = e;
+ events.push_back(E);
+ }
+
+ inline void addNote(uint8_t pitch, uint8_t volume, double time, double duration)
+ {
+ Event event; event.channel = channel;
+ event.volume = volume;
+
+ event.type = Event::NOTE_ON; event.pitch = pitch; event.time= (uint32_t) (time * TICKSPERBEAT);
+ addEvent(event);
+
+ event.type = Event::NOTE_OFF; event.pitch = pitch; event.time=(uint32_t) ((time+duration) * TICKSPERBEAT);
+ addEvent(event);
+
+ //printf("note: %d-%d\n", (uint32_t) time * TICKSPERBEAT, (uint32_t)((time+duration) * TICKSPERBEAT));
+ }
+
+ inline void addName(const string &name, uint32_t time)
+ {
+ Event event; event.channel = channel;
+ event.type = Event::TRACK_NAME; event.time=time; event.trackName = name;
+ addEvent(event);
+ }
+
+ inline void addProgramChange(uint8_t prog, uint32_t time)
+ {
+ Event event; event.channel = channel;
+ event.type = Event::PROG_CHANGE; event.time=time; event.programNumber = prog;
+ addEvent(event);
+ }
+
+ inline void addTempo(uint8_t tempo, uint32_t time)
+ {
+ Event event; event.channel = channel;
+ event.type = Event::TEMPO; event.time=time; event.tempo = tempo;
+ addEvent(event);
+ }
+
+ inline int writeMIDIToBuffer(uint8_t *buffer, int start=0) const
+ {
+ // Write the meta data and note data to the packed MIDI stream.
+ // Process the events in the eventList
+
+ start += writeEventsToBuffer(buffer, start);
+
+ // Write MIDI close event.
+ buffer[start++] = 0x00;
+ buffer[start++] = 0xFF;
+ buffer[start++] = 0x2F;
+ buffer[start++] = 0x00;
+
+ // return the entire length of the data and write to the header
+
+ return start;
+ }
+
+ inline int writeEventsToBuffer(uint8_t *buffer, int start=0) const
+ {
+ // Write the events in MIDIEvents to the MIDI stream.
+ vector<Event> _events = events;
+ std::sort(_events.begin(), _events.end());
+ vector<Event>::const_iterator it;
+ uint32_t time_last = 0, tmp;
+ for (it = _events.begin(); it!=_events.end(); ++it)
+ {
+ Event e = *it;
+ if (e.time < time_last){
+ printf("error: e.time=%d time_last=%d\n", e.time, time_last);
+ assert(false);
+ }
+ tmp = e.time;
+ e.time -= time_last;
+ time_last = tmp;
+ start += e.writeToBuffer(buffer+start);
+ if (start >= MAX_TRACK_SIZE) {
+ break;
+ }
+ }
+ return start;
+ }
+
+ inline int writeToBuffer(uint8_t *buffer, int start=0) const
+ {
+ uint8_t eventsBuffer[MAX_TRACK_SIZE];
+ uint32_t events_size = writeMIDIToBuffer(eventsBuffer);
+ //printf(">> track %lu events took 0x%x bytes\n", events.size(), events_size);
+
+ // chunk ID
+ buffer[start++] = 'M'; buffer[start++] = 'T'; buffer[start++] = 'r'; buffer[start++] = 'k';
+ // chunk size
+ start += writeBigEndian4(events_size, buffer+start);
+ // copy events data
+ memmove(buffer+start, eventsBuffer, events_size);
+ start += events_size;
+ return start;
+ }
+};
+
+}; // namespace
+
+#endif
diff --git a/src/core/Song.cpp b/src/core/Song.cpp
index 7198416..5a3b762 100644
--- a/src/core/Song.cpp
+++ b/src/core/Song.cpp
@@ -47,6 +47,7 @@
#include "FxMixerView.h"
#include "GuiApplication.h"
#include "ImportFilter.h"
+#include "ExportFilter.h"
#include "InstrumentTrack.h"
#include "MainWindow.h"
#include "FileDialog.h"
@@ -1275,6 +1276,63 @@ void Song::exportProject(bool multiExport)
}
+void Song::exportProjectMidi()
+{
+ if( isEmpty() )
+ {
+ QMessageBox::information( gui->mainWindow(),
+ tr( "Empty project" ),
+ tr( "This project is empty so exporting makes "
+ "no sense. Please put some items into "
+ "Song Editor first!" ) );
+ return;
+ }
+
+ FileDialog efd( gui->mainWindow() );
+
+ efd.setFileMode( FileDialog::AnyFile );
+
+ QStringList types;
+ types << tr("MIDI File (*.mid)");
+ efd.setNameFilters( types );
+ QString base_filename;
+ if( !m_fileName.isEmpty() )
+ {
+ efd.setDirectory( QFileInfo( m_fileName ).absolutePath() );
+ base_filename = QFileInfo( m_fileName ).completeBaseName();
+ }
+ else
+ {
+ efd.setDirectory( ConfigManager::inst()->userProjectsDir() );
+ base_filename = tr( "untitled" );
+ }
+ efd.selectFile( base_filename + ".mid" );
+ efd.setWindowTitle( tr( "Select file for project-export..." ) );
+
+ efd.setAcceptMode( FileDialog::AcceptSave );
+
+
+ if( efd.exec() == QDialog::Accepted && !efd.selectedFiles().isEmpty() && !efd.selectedFiles()[0].isEmpty() )
+ {
+ const QString suffix = ".mid";
+
+ const QString export_filename = efd.selectedFiles()[0] + suffix;
+
+ // NOTE start midi export
+
+ // instantiate midi export plugin
+ TrackContainer::TrackList tracks;
+ tracks += Engine::getSong()->tracks();
+ tracks += Engine::getBBTrackContainer()->tracks();
+ ExportFilter *exf = dynamic_cast<ExportFilter *> (Plugin::instantiate("midiexport", NULL, NULL));
+ if (exf==NULL) {
+ qDebug() << "failed to load midi export filter!";
+ return;
+ }
+ exf->tryExport(tracks, Engine::getSong()->getTempo(), export_filename);
+ }
+}
+
void Song::updateFramesPerTick()
diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp
index b8ddd60..b37c3a7 100644
--- a/src/gui/MainWindow.cpp
+++ b/src/gui/MainWindow.cpp
@@ -273,6 +273,12 @@ void MainWindow::finalize()
SLOT( exportProjectTracks() ),
Qt::CTRL + Qt::SHIFT + Qt::Key_E );
+ project_menu->addAction( embed::getIconPixmap( "midi_file" ),
+ tr( "E&xport MIDI..." ),
+ Engine::getSong(),
+ SLOT( exportProjectMidi() ),
+ Qt::CTRL + Qt::Key_M );
+
project_menu->addSeparator();
project_menu->addAction( embed::getIconPixmap( "exit" ), tr( "&Quit" ),
qApp, SLOT( closeAllWindows() ),
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment