From b25ed327e07a1e368aea218ef58fb9f388539587 Mon Sep 17 00:00:00 2001 From: mohamed Date: Wed, 4 Feb 2015 20:23:43 +0100 Subject: [PATCH 1/6] export filter for export plugins --- include/ExportFilter.h | 64 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 include/ExportFilter.h diff --git a/include/ExportFilter.h b/include/ExportFilter.h new file mode 100644 index 00000000000..857ed1b6c73 --- /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 + * + * 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 + +#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 From af0f997af8c86515c302f3b0dc28ed8b83411db8 Mon Sep 17 00:00:00 2001 From: mohamed Date: Wed, 4 Feb 2015 20:23:53 +0100 Subject: [PATCH 2/6] midi export plugin --- plugins/MidiExport/CMakeLists.txt | 4 + plugins/MidiExport/MidiExport.cpp | 183 +++++++++++++++++ plugins/MidiExport/MidiExport.h | 58 ++++++ plugins/MidiExport/MidiFile.hpp | 320 ++++++++++++++++++++++++++++++ 4 files changed, 565 insertions(+) create mode 100644 plugins/MidiExport/CMakeLists.txt create mode 100644 plugins/MidiExport/MidiExport.cpp create mode 100644 plugins/MidiExport/MidiExport.h create mode 100644 plugins/MidiExport/MidiFile.hpp diff --git a/plugins/MidiExport/CMakeLists.txt b/plugins/MidiExport/CMakeLists.txt new file mode 100644 index 00000000000..1d19f081e6a --- /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 00000000000..03ef3a49683 --- /dev/null +++ b/plugins/MidiExport/MidiExport.cpp @@ -0,0 +1,183 @@ +/* + * MidiExport.cpp - support for importing MIDI files + * + * Author: Mohamed Abdel Maksoud + * + * 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 +#include +#include +#include +#include + +#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 ", + 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 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( 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 00000000000..d829a8b8ff3 --- /dev/null +++ b/plugins/MidiExport/MidiExport.h @@ -0,0 +1,58 @@ +/* + * MidiExport.h - support for Exporting MIDI-files + * + * Author: Mohamed Abdel Maksoud + * + * 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 + +#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 00000000000..33955e10a13 --- /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 + *----------------------------------------------------------------------------- + * Name: MidiFile.py + * Purpose: MIDI file manipulation utilities + * + * Author: Mark Conway Wirt + * + * 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 +#include +#include +#include +#include +#include +#include + +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> 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 +class MIDITrack +{ + // A class that encapsulates a MIDI track + // Nested class definitions. + vector 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 _events = events; + std::sort(_events.begin(), _events.end()); + vector::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 From 826591817a3ef601bfd8e59d3e0cad664b08ea3a Mon Sep 17 00:00:00 2001 From: mohamed Date: Wed, 4 Feb 2015 22:36:03 +0100 Subject: [PATCH 3/6] adding midiexport plugin to the list --- plugins/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index d7b4cf11ee7..56212ef3eed 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) From 9b7ac3b3db8ac075be9010d533875d0588c33785 Mon Sep 17 00:00:00 2001 From: mohamed Date: Wed, 4 Feb 2015 22:36:34 +0100 Subject: [PATCH 4/6] cleaning up event ordering function --- plugins/MidiExport/MidiFile.hpp | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/plugins/MidiExport/MidiFile.hpp b/plugins/MidiExport/MidiFile.hpp index 33955e10a13..0e2bfbe5bd7 100644 --- a/plugins/MidiExport/MidiFile.hpp +++ b/plugins/MidiExport/MidiFile.hpp @@ -186,19 +186,7 @@ struct Event // 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; + return this->time < b.time; } }; From 8d5077af96497db2071f0680ad60b0b7467fdb46 Mon Sep 17 00:00:00 2001 From: mohamed Date: Wed, 4 Feb 2015 22:37:00 +0100 Subject: [PATCH 5/6] adding menu entry file->export midi --- src/gui/MainWindow.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index b8ddd60714e..b37c3a79b9e 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() ), From 2260907285284b8c4b5b7cfee9a217ee3ff57785 Mon Sep 17 00:00:00 2001 From: mohamed Date: Wed, 4 Feb 2015 22:37:43 +0100 Subject: [PATCH 6/6] adding exportProjectMidi method, internally uses midiexport plugin --- include/Song.h | 1 + src/core/Song.cpp | 59 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/include/Song.h b/include/Song.h index 5da09818a22..487e856a6c9 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/src/core/Song.cpp b/src/core/Song.cpp index 7198416cc74..7942b6452aa 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,64 @@ 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"; + + QString export_filename = efd.selectedFiles()[0]; + if (!export_filename.endsWith(suffix)) export_filename += suffix; + + // NOTE start midi export + + // instantiate midi export plugin + TrackContainer::TrackList tracks; + tracks += Engine::getSong()->tracks(); + tracks += Engine::getBBTrackContainer()->tracks(); + ExportFilter *exf = dynamic_cast (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()