From c0682c94a437e63fba52be6b03e50623b3b669b9 Mon Sep 17 00:00:00 2001 From: Hyunjin Song Date: Thu, 14 Sep 2017 08:09:54 +0900 Subject: [PATCH] Fix MIDI export (#3733) * Re-enable MIDI export * Fix logic for processing BB tracks and BB notes * Consider master pitch and base note in MIDI export. * Cut BB notes at the end of BB pattern. --- README.md | 2 +- include/ExportFilter.h | 4 +- plugins/CMakeLists.txt | 2 +- plugins/MidiExport/MidiExport.cpp | 280 ++++++++++++++++++++++++------ plugins/MidiExport/MidiExport.h | 42 ++++- plugins/MidiExport/MidiFile.hpp | 5 +- src/core/Song.cpp | 7 +- src/gui/MainWindow.cpp | 5 +- 8 files changed, 277 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index 9c65c4be743..1061ecff600 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Features * Many powerful instrument and effect-plugins out of the box * Full user-defined track-based automation and computer-controlled automation sources * Compatible with many standards such as SoundFont2, VST(i), LADSPA, GUS Patches, and full MIDI support -* MIDI file importing +* MIDI file importing and exporting Building --------- diff --git a/include/ExportFilter.h b/include/ExportFilter.h index f27bc0c8295..35416f49280 100644 --- a/include/ExportFilter.h +++ b/include/ExportFilter.h @@ -39,7 +39,9 @@ class EXPORT ExportFilter : public Plugin virtual ~ExportFilter() {} - virtual bool tryExport( const TrackContainer::TrackList &tracks, int tempo, const QString &filename ) = 0; + virtual bool tryExport(const TrackContainer::TrackList &tracks, + const TrackContainer::TrackList &tracksBB, + int tempo, int masterPitch, const QString &filename ) = 0; protected: virtual void saveSettings( QDomDocument &, QDomElement & ) diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 8a464e3886d..24c15e391bd 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -56,7 +56,7 @@ IF("${PLUGIN_LIST}" STREQUAL "") LadspaEffect lb302 MidiImport - # MidiExport - temporarily disabled, MIDI export is broken + MidiExport MultitapEcho monstro nes diff --git a/plugins/MidiExport/MidiExport.cpp b/plugins/MidiExport/MidiExport.cpp index b838353d2c2..1e20e9d40f4 100644 --- a/plugins/MidiExport/MidiExport.cpp +++ b/plugins/MidiExport/MidiExport.cpp @@ -1,7 +1,8 @@ /* - * MidiExport.cpp - support for importing MIDI files + * MidiExport.cpp - support for Exporting MIDI files * - * Author: Mohamed Abdel Maksoud + * Copyright (c) 2015 Mohamed Abdel Maksoud + * Copyright (c) 2017 Hyunjin Song * * This file is part of LMMS - https://lmms.io * @@ -30,8 +31,10 @@ #include #include "MidiExport.h" -#include "Engine.h" + +#include "lmms_math.h" #include "TrackContainer.h" +#include "BBTrack.h" #include "InstrumentTrack.h" @@ -44,7 +47,8 @@ Plugin::Descriptor PLUGIN_EXPORT midiexport_plugin_descriptor = "MIDI Export", QT_TRANSLATE_NOOP( "pluginBrowser", "Filter for exporting MIDI-files from LMMS" ), - "Mohamed Abdel Maksoud ", + "Mohamed Abdel Maksoud and " + "Hyunjin Song ", 0x0100, Plugin::ExportFilter, NULL, @@ -68,99 +72,269 @@ MidiExport::~MidiExport() -bool MidiExport::tryExport( const TrackContainer::TrackList &tracks, int tempo, const QString &filename ) +bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, + const TrackContainer::TrackList &tracks_BB, + int tempo, int masterPitch, const QString &filename) { QFile f(filename); f.open(QIODevice::WriteOnly); QDataStream midiout(&f); InstrumentTrack* instTrack; + BBTrack* bbTrack; QDomElement element; int nTracks = 0; - const int BUFFER_SIZE = 50*1024; uint8_t buffer[BUFFER_SIZE]; uint32_t size; - for( const Track* track : tracks ) if( track->type() == Track::InstrumentTrack ) nTracks++; + for (const Track* track : tracks) if (track->type() == Track::InstrumentTrack) nTracks++; + for (const Track* track : tracks_BB) if (track->type() == Track::InstrumentTrack) nTracks++; // midi header MidiFile::MIDIHeader header(nTracks); size = header.writeToBuffer(buffer); midiout.writeRawData((char *)buffer, size); + std::vector>> plists; + // midi tracks - for( Track* track : tracks ) + for (Track* track : tracks) { - DataFile dataFile( DataFile::SongProject ); - MidiFile::MIDITrack mtrack; - - if( track->type() != Track::InstrumentTrack ) continue; - - //qDebug() << "exporting " << track->name(); - - + DataFile dataFile(DataFile::SongProject); + MTrack mtrack; + + if (track->type() == Track::InstrumentTrack) + { + + mtrack.addName(track->name().toStdString(), 0); + //mtrack.addProgramChange(0, 0); + mtrack.addTempo(tempo, 0); + + instTrack = dynamic_cast(track); + element = instTrack->saveState(dataFile, dataFile.content()); + + int base_pitch = 0; + double base_volume = 1.0; + int base_time = 0; + + MidiNoteVector pat; + + for (QDomNode n = element.firstChild(); !n.isNull(); n = n.nextSibling()) + { + + if (n.nodeName() == "instrumenttrack") + { + QDomElement it = n.toElement(); + // transpose +12 semitones, workaround for #1857 + base_pitch = (69 - it.attribute("basenote", "57").toInt()); + if (it.attribute("usemasterpitch", "1").toInt()) + { + base_pitch += masterPitch; + } + base_volume = it.attribute("volume", "100").toDouble()/100.0; + } + + if (n.nodeName() == "pattern") + { + base_time = n.toElement().attribute("pos", "0").toInt(); + writePattern(pat, n, base_pitch, base_volume, base_time); + } + + } + ProcessBBNotes(pat, INT_MAX); + writePatternToTrack(mtrack, pat); + size = mtrack.writeToBuffer(buffer); + midiout.writeRawData((char *)buffer, size); + } + + if (track->type() == Track::BBTrack) + { + bbTrack = dynamic_cast(track); + element = bbTrack->saveState(dataFile, dataFile.content()); + + std::vector> plist; + for (QDomNode n = element.firstChild(); !n.isNull(); n = n.nextSibling()) + { + + if (n.nodeName() == "bbtco") + { + QDomElement it = n.toElement(); + int pos = it.attribute("pos", "0").toInt(); + int len = it.attribute("len", "0").toInt(); + plist.push_back(std::pair(pos, pos+len)); + } + } + std::sort(plist.begin(), plist.end()); + plists.push_back(plist); + + } + } // for each track + + // midi tracks in BB tracks + for (Track* track : tracks_BB) + { + DataFile dataFile(DataFile::SongProject); + MTrack mtrack; + + auto itr = plists.begin(); + std::vector> st; + + if (track->type() != Track::InstrumentTrack) continue; + 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; + + instTrack = dynamic_cast(track); + element = instTrack->saveState(dataFile, dataFile.content()); + + int base_pitch = 0; double base_volume = 1.0; - int base_time = 0; - - - for(QDomNode n = element.firstChild(); !n.isNull(); n = n.nextSibling()) + + 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; + // transpose +12 semitones, workaround for #1857 + base_pitch = (69 - it.attribute("basenote", "57").toInt()); + if (it.attribute("usemasterpitch", "1").toInt()) + { + base_pitch += masterPitch; + } + 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()) + std::vector> &plist = *itr; + + MidiNoteVector nv, pat; + writePattern(pat, n, base_pitch, base_volume, 0); + + // workaround for nested BBTCOs + int pos = 0; + int len = n.toElement().attribute("steps", "1").toInt() * 12; + for (auto it = plist.begin(); it != plist.end(); ++it) + { + while (!st.empty() && st.back().second <= it->first) + { + writeBBPattern(pat, nv, len, st.back().first, pos, st.back().second); + pos = st.back().second; + st.pop_back(); + } + + if (!st.empty() && st.back().second <= it->second) + { + writeBBPattern(pat, nv, len, st.back().first, pos, it->first); + pos = it->first; + while (!st.empty() && st.back().second <= it->second) + { + st.pop_back(); + } + } + + st.push_back(*it); + pos = it->first; + } + + while (!st.empty()) { - 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); + writeBBPattern(pat, nv, len, st.back().first, pos, st.back().second); + pos = st.back().second; + st.pop_back(); } + + ProcessBBNotes(nv, pos); + writePatternToTrack(mtrack, nv); + ++itr; } - } size = mtrack.writeToBuffer(buffer); midiout.writeRawData((char *)buffer, size); - } // for each track - + } + return true; } +void MidiExport::writePattern(MidiNoteVector &pat, QDomNode n, + int base_pitch, double base_volume, int base_time) +{ + // 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") continue; + // TODO interpret pan="0" fxch="0" pitchrange="1" + MidiNote mnote; + mnote.pitch = qMax(0, qMin(127, note.attribute("key", "0").toInt() + base_pitch)); + mnote.volume = qMin(qRound(base_volume * note.attribute("vol", "100").toDouble()), 127); + mnote.time = base_time + note.attribute("pos", "0").toInt(); + mnote.duration = note.attribute("len", "0").toInt(); + pat.push_back(mnote); + } +} + + + +void MidiExport::writePatternToTrack(MTrack &mtrack, MidiNoteVector &nv) +{ + for (auto it = nv.begin(); it != nv.end(); ++it) + { + mtrack.addNote(it->pitch, it->volume, it->time / 48.0, it->duration / 48.0); + } +} + + + +void MidiExport::writeBBPattern(MidiNoteVector &src, MidiNoteVector &dst, + int len, int base, int start, int end) +{ + if (start >= end) { return; } + start -= base; + end -= base; + std::sort(src.begin(), src.end()); + for (auto it = src.begin(); it != src.end(); ++it) + { + for (int time = it->time + ceil((start - it->time) / len) + * len; time < end; time += len) + { + MidiNote note; + note.duration = it->duration; + note.pitch = it->pitch; + note.time = base + time; + note.volume = it->volume; + dst.push_back(note); + } + } +} + + + +void MidiExport::ProcessBBNotes(MidiNoteVector &nv, int cutPos) +{ + std::sort(nv.begin(), nv.end()); + int cur = INT_MAX, next = INT_MAX; + for (auto it = nv.rbegin(); it != nv.rend(); ++it) + { + if (it->time < cur) + { + next = cur; + cur = it->time; + } + if (it->duration < 0) + { + it->duration = qMin(qMin(-it->duration, next - cur), cutPos - it->time); + } + } +} + + void MidiExport::error() { diff --git a/plugins/MidiExport/MidiExport.h b/plugins/MidiExport/MidiExport.h index 279f369f6b3..3c36eeb8f55 100644 --- a/plugins/MidiExport/MidiExport.h +++ b/plugins/MidiExport/MidiExport.h @@ -1,7 +1,8 @@ /* * MidiExport.h - support for Exporting MIDI-files * - * Author: Mohamed Abdel Maksoud + * Copyright (c) 2015 Mohamed Abdel Maksoud + * Copyright (c) 2017 Hyunjin Song * * This file is part of LMMS - https://lmms.io * @@ -31,25 +32,52 @@ #include "MidiFile.hpp" +const int BUFFER_SIZE = 50*1024; +typedef MidiFile::MIDITrack MTrack; + +struct MidiNote +{ + int time; + uint8_t pitch; + int duration; + uint8_t volume; + + inline bool operator<(const MidiNote &b) const + { + return this->time < b.time; + } +} ; + +typedef std::vector MidiNoteVector; +typedef std::vector::iterator MidiNoteIterator; + + class MidiExport: public ExportFilter { // Q_OBJECT public: - MidiExport( ); + MidiExport(); ~MidiExport(); - virtual PluginView * instantiateView( QWidget * ) + virtual PluginView *instantiateView(QWidget *) { - return( NULL ); + return nullptr; } - virtual bool tryExport( const TrackContainer::TrackList &tracks, int tempo, const QString &filename ); + virtual bool tryExport(const TrackContainer::TrackList &tracks, + const TrackContainer::TrackList &tracks_BB, + int tempo, int masterPitch, const QString &filename); private: - + void writePattern(MidiNoteVector &pat, QDomNode n, + int base_pitch, double base_volume, int base_time); + void writePatternToTrack(MTrack &mtrack, MidiNoteVector &nv); + void writeBBPattern(MidiNoteVector &src, MidiNoteVector &dst, + int len, int base, int start, int end); + void ProcessBBNotes(MidiNoteVector &nv, int cutPos); - void error( void ); + void error(); } ; diff --git a/plugins/MidiExport/MidiFile.hpp b/plugins/MidiExport/MidiFile.hpp index 0e2bfbe5bd7..a1f91de2fea 100644 --- a/plugins/MidiExport/MidiFile.hpp +++ b/plugins/MidiExport/MidiFile.hpp @@ -156,8 +156,10 @@ struct Event 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]; @@ -186,7 +188,8 @@ struct Event // events are sorted by their time inline bool operator < (const Event& b) const { - return this->time < b.time; + return this->time < b.time || + (this->time == b.time && this->type > b.type); } }; diff --git a/src/core/Song.cpp b/src/core/Song.cpp index a576bbcbd75..01071668c7a 100644 --- a/src/core/Song.cpp +++ b/src/core/Song.cpp @@ -1444,14 +1444,15 @@ void Song::exportProjectMidi() // instantiate midi export plugin TrackContainer::TrackList tracks; - tracks += Engine::getSong()->tracks(); - tracks += Engine::getBBTrackContainer()->tracks(); + TrackContainer::TrackList tracks_BB; + tracks = Engine::getSong()->tracks(); + tracks_BB = 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); + exf->tryExport(tracks, tracks_BB, getTempo(), m_masterPitchModel.value(), export_filename); } } diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index f65c9606612..9bb4bfea667 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -300,12 +300,11 @@ void MainWindow::finalize() SLOT( exportProjectTracks() ), Qt::CTRL + Qt::SHIFT + Qt::Key_E ); - // temporarily disabled broken MIDI export - /*project_menu->addAction( embed::getIconPixmap( "midi_file" ), + project_menu->addAction( embed::getIconPixmap( "midi_file" ), tr( "Export &MIDI..." ), Engine::getSong(), SLOT( exportProjectMidi() ), - Qt::CTRL + Qt::Key_M );*/ + Qt::CTRL + Qt::Key_M ); // Prevent dangling separator at end of menu per https://bugreports.qt.io/browse/QTBUG-40071 #if !(defined(LMMS_BUILD_APPLE) && (QT_VERSION >= 0x050000) && (QT_VERSION < 0x050600))