diff --git a/plugins/MidiExport/CMakeLists.txt b/plugins/MidiExport/CMakeLists.txt index 1d19f081e6a..69fd87e3c4a 100644 --- a/plugins/MidiExport/CMakeLists.txt +++ b/plugins/MidiExport/CMakeLists.txt @@ -1,4 +1,4 @@ INCLUDE(BuildPlugin) -BUILD_PLUGIN(midiexport MidiExport.cpp MidiExport.h MidiFile.hpp +BUILD_PLUGIN(midiexport MidiExport.cpp MidiExport.h MidiFile.cpp MidiFile.h MOCFILES MidiExport.h) diff --git a/plugins/MidiExport/MidiExport.cpp b/plugins/MidiExport/MidiExport.cpp index 1860527c10e..7cb24e038a9 100644 --- a/plugins/MidiExport/MidiExport.cpp +++ b/plugins/MidiExport/MidiExport.cpp @@ -5,7 +5,7 @@ * Copyright (c) 2017 Hyunjin Song * * 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 @@ -23,338 +23,338 @@ * */ - -#include -#include -#include -#include -#include - #include "MidiExport.h" -#include "lmms_math.h" -#include "TrackContainer.h" +#include + +#include "Instrument.h" #include "BBTrack.h" #include "InstrumentTrack.h" #include "LocaleHelper.h" - #include "plugin_export.h" +using std::stack; +using std::sort; + extern "C" { +//! Standardized plugin descriptor for MIDI exporter Plugin::Descriptor PLUGIN_EXPORT midiexport_plugin_descriptor = { - STRINGIFY( PLUGIN_NAME ), + STRINGIFY(PLUGIN_NAME), "MIDI Export", - QT_TRANSLATE_NOOP( "pluginBrowser", - "Filter for exporting MIDI-files from LMMS" ), + QT_TRANSLATE_NOOP("pluginBrowser", + "Filter for exporting MIDI-files from LMMS"), "Mohamed Abdel Maksoud and " - "Hyunjin Song ", + "Hyunjin Song ", 0x0100, Plugin::ExportFilter, NULL, NULL, NULL -} ; +}; -} +} // extern "C" +/*---------------------------------------------------------------------------*/ -MidiExport::MidiExport() : ExportFilter( &midiexport_plugin_descriptor) +void MidiExport::Pattern::write(const QDomNode &root, + int basePitch, double baseVolume, int baseTime) { + // TODO interpret steps="12" muted="0" type="1" name="Piano1" len="259" + for (QDomNode node = root.firstChild(); !node.isNull(); + node = node.nextSibling()) + { + QDomElement element = node.toElement(); + + // Ignore zero-length notes + if (element.attribute("len", "0") == "0") continue; + + // Adjust note attributes based on base measures + Note note; + int pitch = element.attribute("key", "0").toInt() + basePitch; + note.m_pitch = qBound(0, pitch, 127); + double volume = + LocaleHelper::toDouble(element.attribute("vol", "100")); + volume *= baseVolume * (127.0 / 200.0); + note.m_volume = qMin(qRound(volume), 127); + note.m_time = baseTime + element.attribute("pos", "0").toInt(); + note.m_duration = element.attribute("len", "0").toInt(); + + // Append note to vector + m_notes.push_back(note); + } } - - - -MidiExport::~MidiExport() +void MidiExport::Pattern::writeToTrack(MidiFile::Track &mTrack) const { + for (const Note ¬e : m_notes) { + mTrack.addNote(note.m_pitch, note.m_volume, + note.m_time / 48.0, note.m_duration / 48.0); + } } - - -bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, - const TrackContainer::TrackList &tracks_BB, - int tempo, int masterPitch, const QString &filename) +void MidiExport::Pattern::processBbNotes(int cutPos) { - QFile f(filename); - f.open(QIODevice::WriteOnly); - QDataStream midiout(&f); - - InstrumentTrack* instTrack; - BBTrack* bbTrack; - QDomElement element; - - - int nTracks = 0; - uint8_t buffer[BUFFER_SIZE]; - uint32_t size; + // Sort in reverse order + sort(m_notes.rbegin(), m_notes.rend()); - 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) + int cur = INT_MAX, next = INT_MAX; + for (Note ¬e : m_notes) { - DataFile dataFile(DataFile::SongProject); - MTrack mtrack; - - if (track->type() == Track::InstrumentTrack) + if (note.m_time < cur) { - - 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 = LocaleHelper::toDouble(it.attribute("volume", "100"))/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); + // Set last two notes positions + next = cur; + cur = note.m_time; } - - if (track->type() == Track::BBTrack) + if (note.m_duration < 0) { - bbTrack = dynamic_cast(track); - element = bbTrack->saveState(dataFile, dataFile.content()); + // Note should have positive duration that neither + // overlaps next one nor exceeds cutPos + note.m_duration = qMin(-note.m_duration, next - cur); + note.m_duration = qMin(note.m_duration, cutPos - note.m_time); + } + } +} - std::vector> plist; - for (QDomNode n = element.firstChild(); !n.isNull(); n = n.nextSibling()) - { +void MidiExport::Pattern::writeToBb(Pattern &bbPat, + int len, int base, int start, int end) +{ + // Avoid misplaced start and end positions + if (start >= end) { return; } - 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); + // Adjust positions relatively to base pos + start -= base; + end -= base; + sort(m_notes.begin(), m_notes.end()); + for (Note note : m_notes) + { + // Insert periodically repeating notes from and spaced + // by to mimic BB pattern behavior + int t0 = note.m_time + ceil((start - note.m_time) / len) * len; + for (int time = t0; time < end; time += len) + { + note.m_time = base + time; + bbPat.m_notes.push_back(note); } - } // 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; +MidiExport::MidiExport() : + ExportFilter(&midiexport_plugin_descriptor) {} - if (track->type() != Track::InstrumentTrack) continue; +/*---------------------------------------------------------------------------*/ - mtrack.addName(track->name().toStdString(), 0); - //mtrack.addProgramChange(0, 0); - mtrack.addTempo(tempo, 0); +bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, + const TrackContainer::TrackList &tracksBb, + int tempo, int masterPitch, const QString &filename) +{ + // Count number of instrument (and instrument BB) tracks + int nTracks = 0; + for (const Track *track : tracks) + { + if (track->type() == Track::InstrumentTrack) { nTracks++; } + } + nTracks += tracksBb.size(); - instTrack = dynamic_cast(track); - element = instTrack->saveState(dataFile, dataFile.content()); + // Create MIDI file object + MidiFile file(filename, nTracks); + m_file = &file; + m_tempo = tempo; + m_masterPitch = masterPitch; - int base_pitch = 0; - double base_volume = 1.0; + // Write header info + m_file->m_header.writeToBuffer(); - for (QDomNode n = element.firstChild(); !n.isNull(); n = n.nextSibling()) + // Iterate through "normal" tracks + size_t trackIdx = 0; + for (Track *track : tracks) + { + if (track->type() == Track::InstrumentTrack) { - 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 = LocaleHelper::toDouble(it.attribute("volume", "100")) / 100.0; - } - - if (n.nodeName() == "pattern") - { - 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()) - { - 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; - } + processTrack(track, trackIdx++); + } + else if (track->type() == Track::BBTrack) + { + processBbTrack(track); } - size = mtrack.writeToBuffer(buffer); - midiout.writeRawData((char *)buffer, size); } + // Iterate through instrument BB tracks + for (Track *track : tracksBb) + { + processTrack(track, trackIdx++, true); + } + // Write all buffered data to stream + m_file->writeAllToStream(); + // Always returns success... for now? return true; - } - - -void MidiExport::writePattern(MidiNoteVector &pat, QDomNode n, - int base_pitch, double base_volume, int base_time) +void MidiExport::processTrack(Track *track, size_t trackIdx, bool isBb) { - // TODO interpret steps="12" muted="0" type="1" name="Piano1" len="2592" - for (QDomNode nn = n.firstChild(); !nn.isNull(); nn = nn.nextSibling()) + // Cast track as a instrument one and save info from it to element + InstrumentTrack *instTrack = dynamic_cast(track); + QDomElement root = instTrack->saveState(m_dataFile, m_dataFile.content()); + + // Get next MIDI file track object of the list and set its channel + MidiFile::Track &midiTrack = m_file->m_tracks[trackIdx]; + if (m_channel == 9) { ++m_channel; } + midiTrack.m_channel = m_channel++; + + // Add info about tempo and track name + midiTrack.addTempo(m_tempo, 0); + midiTrack.addName(track->name().toStdString(), 0); + + // If the current track is a Sf2 Player one, set the current + // patch to the exporting track. Note that this only works + // decently if the current bank is a GM 1~128 one (which would be + // needed as the default either way for successful import). + // BB tracks are always bank 128 (see MidiImport), patch 0. + uint8_t patch = 0; + QString instName = instTrack->instrumentName(); + if (instName == "Sf2 Player") { - 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)); - // Map from LMMS volume to MIDI velocity - mnote.volume = qMin(qRound(base_volume * LocaleHelper::toDouble(note.attribute("vol", "100")) * (127.0 / 200.0)), 127); - mnote.time = base_time + note.attribute("pos", "0").toInt(); - mnote.duration = note.attribute("len", "0").toInt(); - pat.push_back(mnote); + class Instrument *inst = instTrack->instrument(); + uint8_t bank = inst->childModel("bank")->value(); + if (bank == 128) + { + // Drum Sf2 track, so set its channel to 10 + // (and reverse counter increment) + midiTrack.m_channel = 9; + m_channel--; + } + else { patch = inst->childModel("patch")->value(); } } -} - - - -void MidiExport::writePatternToTrack(MTrack &mtrack, MidiNoteVector &nv) -{ - for (auto it = nv.begin(); it != nv.end(); ++it) + midiTrack.addProgramChange(patch, 0); + + // ---- Instrument track ---- // + QDomNode trackNode = root.firstChildElement("instrumenttrack"); + QDomElement trackElem = trackNode.toElement(); + // Transpose +12 semitones (workaround for #1857) + int basePitch = trackElem.attribute("basenote", "57").toInt(); + basePitch = 69 - basePitch; + // Adjust to masterPitch if enabled + if (trackElem.attribute("usemasterpitch", "1").toInt()) { - mtrack.addNote(it->pitch, it->volume, it->time / 48.0, it->duration / 48.0); + basePitch += m_masterPitch; } -} - + // Volume ranges in [0.0, 2.0] + double baseVolume = LocaleHelper::toDouble( + trackElem.attribute("volume", "100")) / 100.0; + + // ---- Patterns ---- // + uint8_t bbId = 0; + for (QDomNode patNode = root.firstChildElement("pattern"); + !patNode.isNull(); patNode = patNode.nextSiblingElement("pattern")) + { + QDomElement patElem = patNode.toElement(); + Pattern pat; + if (!isBb) + { + // Base time == initial position + int baseTime = patElem.attribute("pos", "0").toInt(); + // Write track notes to pattern + pat.write(patNode, basePitch, baseVolume, baseTime); -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) + // Write pattern info to MIDI file track + pat.processBbNotes(INT_MAX); + pat.writeToTrack(midiTrack); + } + else { - MidiNote note; - note.duration = it->duration; - note.pitch = it->pitch; - note.time = base + time; - note.volume = it->volume; - dst.push_back(note); + // Write to-be repeated BB notes to pattern + // (notice base time of 0) + pat.write(patNode, basePitch, baseVolume, 0); + + // Write pattern to track + writeBbPattern(pat, patElem, bbId++, midiTrack); } } + // Write track data to buffer + midiTrack.writeToBuffer(); } - - -void MidiExport::ProcessBBNotes(MidiNoteVector &nv, int cutPos) +void MidiExport::writeBbPattern(Pattern &pat, const QDomElement &patElem, + uint8_t bbId, MidiFile::Track &midiTrack) { - std::sort(nv.begin(), nv.end()); - int cur = INT_MAX, next = INT_MAX; - for (auto it = nv.rbegin(); it != nv.rend(); ++it) + // Workaround for nested BBTCOs + int pos = 0; + int len = 12 * patElem.attribute("steps", "1").toInt(); + + // Iterate through BBTCO pairs of current list + // TODO: This *may* need some corrections? + const vector> &plist = m_plists[bbId]; + stack> st; + Pattern bbPat; + for (const pair &p : plist) { - if (it->time < cur) + while (!st.empty() && st.top().second <= p.first) { - next = cur; - cur = it->time; + pat.writeToBb(bbPat, len, st.top().first, pos, st.top().second); + pos = st.top().second; + st.pop(); } - if (it->duration < 0) + if (!st.empty() && st.top().second <= p.second) { - it->duration = qMin(qMin(-it->duration, next - cur), cutPos - it->time); + pat.writeToBb(bbPat, len, st.top().first, pos, p.first); + pos = p.first; + while (!st.empty() && st.top().second <= p.second) + { + st.pop(); + } } + st.push(p); + pos = p.first; } + while (!st.empty()) + { + pat.writeToBb(bbPat, len, st.top().first, pos, st.top().second); + pos = st.top().second; + st.pop(); + } + // Write pattern info to MIDI file track + bbPat.processBbNotes(pos); + bbPat.writeToTrack(midiTrack); } - - -void MidiExport::error() +void MidiExport::processBbTrack(Track *track) { - //qDebug() << "MidiExport error: " << m_error ; + // Cast track as a BB one and save info from it to element + BBTrack *bbTrack = dynamic_cast(track); + QDomElement root = bbTrack->saveState(m_dataFile, m_dataFile.content()); + + // Build lists of (start, end) pairs from BB note objects + vector> plist; + for (QDomNode bbtcoNode = root.firstChildElement("bbtco"); + !bbtcoNode.isNull(); + bbtcoNode = bbtcoNode.nextSiblingElement("bbtco")) + { + QDomElement bbtcoElem = bbtcoNode.toElement(); + int start = bbtcoElem.attribute("pos", "0").toInt(); + int end = start + bbtcoElem.attribute("len", "0").toInt(); + plist.push_back(pair(start, end)); + } + // Sort list in ascending order and append it to matrix + sort(plist.begin(), plist.end()); + m_plists.push_back(plist); } - +/*---------------------------------------------------------------------------*/ extern "C" { -// necessary for getting instance out of shared lib -PLUGIN_EXPORT Plugin * lmms_plugin_main( Model *, void * _data ) +//! Necessary for getting instance out of shared lib +PLUGIN_EXPORT Plugin * lmms_plugin_main(Model *, void * _data) { return new MidiExport(); } - -} - +} // extern "C" diff --git a/plugins/MidiExport/MidiExport.h b/plugins/MidiExport/MidiExport.h index 3c36eeb8f55..d63de4887b7 100644 --- a/plugins/MidiExport/MidiExport.h +++ b/plugins/MidiExport/MidiExport.h @@ -28,59 +28,116 @@ #include +#include "MidiFile.h" +#include "DataFile.h" #include "ExportFilter.h" -#include "MidiFile.hpp" +using std::pair; +using std::vector; -const int BUFFER_SIZE = 50*1024; -typedef MidiFile::MIDITrack MTrack; +/*---------------------------------------------------------------------------*/ -struct MidiNote +//! MIDI exporting base class +class MidiExport : public ExportFilter { - int time; - uint8_t pitch; - int duration; - uint8_t volume; +private: + //! A single MIDI note + struct Note + { + //! The pitch (tone), which can be lower or higher + uint8_t m_pitch; + + //! Volume (loudness) + uint8_t m_volume; + + //! Absolute time (from song start) when the note starts playing + int m_time; + + //! For how long the note plays + int m_duration; - inline bool operator<(const MidiNote &b) const + //! Sort notes by time + inline bool operator<(const Note &b) const + { + return m_time < b.m_time; + } + }; + + /*-----------------------------------------------------------------------*/ + + //! A pattern of MIDI notes + class Pattern { - return this->time < b.time; - } -} ; + private: + //! List of actual notes + vector m_notes; -typedef std::vector MidiNoteVector; -typedef std::vector::iterator MidiNoteIterator; + public: + //! Append notes from root node to pattern + void write(const QDomNode &root, + int basePitch, double baseVolume, int baseTime); + //! Adjust special duration BB notes by resizing them accordingly + void processBbNotes(int cutPos); + //! Add pattern notes to MIDI file track + void writeToTrack(MidiFile::Track &mTrack) const; + + //! Write sorted notes to a explicitly repeating BB pattern + void writeToBb(Pattern &bbPat, + int len, int base, int start, int end); + }; + + /*-----------------------------------------------------------------------*/ + + //! MIDI file object to work with + MidiFile *m_file; + + //! Song global tempo + int m_tempo; + + //! Song master pitch + int m_masterPitch; + + //! Current (incremental) track channel number for non drum tracks + uint8_t m_channel = 0; + + //! DataFile to be used by Qt elements + DataFile m_dataFile = DataFile(DataFile::SongProject); + + //! Matrix containing (start, end) pairs for BB objects + vector>> m_plists; -class MidiExport: public ExportFilter -{ -// Q_OBJECT public: + //! Explicit constructor for setting plugin descriptor MidiExport(); - ~MidiExport(); - virtual PluginView *instantiateView(QWidget *) - { - return nullptr; - } + //! \brief Export tracks from a project to a .mid extension MIDI file + //! \param tracks Normal instrument tracks + //! \param tracksBB Beat + Bassline tracks + //! \param tempo Song global tempo + //! \param masterPitch Song master pitch + //! \param filename Name of file to be saved + //! \return If operation was successful + bool tryExport(const TrackContainer::TrackList &tracks, + const TrackContainer::TrackList &tracksBB, + int tempo, int masterPitch, 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); + //! Process a given instrument track + void processTrack(Track *track, size_t channelID, bool isBB=false); - void error(); + //! Build a repeating pattern from a normal one and write to MIDI track + void writeBbPattern(Pattern &pat, const QDomElement &patElem, + uint8_t channelID, MidiFile::Track &midiTrack); + //! Process a given BB track + void processBbTrack(Track *track); + //! Necessary for lmms_plugin_main() + PluginView *instantiateView(QWidget *) { return nullptr; } } ; +/*---------------------------------------------------------------------------*/ #endif diff --git a/plugins/MidiExport/MidiFile.cpp b/plugins/MidiExport/MidiFile.cpp new file mode 100644 index 00000000000..9b6b95d9a29 --- /dev/null +++ b/plugins/MidiExport/MidiFile.cpp @@ -0,0 +1,311 @@ +/** + * Name: MidiFile.cpp + * 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 "MidiFile.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using std::string; +using std::stack; +using std::vector; +using std::sort; + +/*---------------------------------------------------------------------------*/ + +MidiFile::MidiFile(const QString &filename, int nTracks): + m_file(filename), + m_header(nTracks) +{ + // Open designated blank MIDI file (and data stream) for writing + m_file.open(QIODevice::WriteOnly); + m_stream = QSharedPointer(new QDataStream(&m_file)); + + // Resize track list + m_tracks.resize(nTracks); +} + +void MidiFile::writeAllToStream() +{ + // reinterpret_cast is used to convert raw uint8_t data to char + m_stream->writeRawData( + reinterpret_cast(m_header.m_buffer.data()), + m_header.m_buffer.size()); + for (Track &track : m_tracks) + { + m_stream->writeRawData( + reinterpret_cast(track.m_buffer.data()), + track.m_buffer.size()); + } +} + +/*---------------------------------------------------------------------------*/ + +MidiFile::Section::Section() +{ + // Increases allocated capacity (not size) + m_buffer.reserve(BUFFER_SIZE); +} + +void MidiFile::Section::writeBytes(vector bytes, + vector *v) +{ + // Insert content to the end of *v + if (!v) v = &m_buffer; + v->insert(v->end(), bytes.begin(), bytes.end()); +} + +void MidiFile::Section::writeVarLength(uint32_t val) +{ + // Build little endian stack from 7-bit packs + uint8_t result = val & 0x7F; + stack little_endian({result}); + val >>= 7; + while (val > 0) + { + result = val & 0x7F; + result |= 0x80; + little_endian.push(result); + val >>= 7; + } + // Add packs in reverse order to actual buffer + while (!little_endian.empty()) + { + m_buffer.push_back(little_endian.top()); + little_endian.pop(); + } +} + +void MidiFile::Section::writeBigEndian4(uint32_t val, + vector *v) +{ + vector bytes; + bytes.push_back(val >> 24); + bytes.push_back((val >> 16) & 0xff), + bytes.push_back((val >> 8) & 0xff); + bytes.push_back(val & 0xff); + writeBytes(bytes, v); +} + +void MidiFile::Section::writeBigEndian2(uint16_t val, + vector *v) +{ + vector bytes; + bytes.push_back(val >> 8); + bytes.push_back(val & 0xff); + writeBytes(bytes, v); +} + +/*---------------------------------------------------------------------------*/ + +MidiFile::Header::Header(int numTracks, int ticksPerBeat): + m_numTracks(numTracks), + m_ticksPerBeat(ticksPerBeat) {} + +void MidiFile::Header::writeToBuffer() +{ + // Chunk ID + writeBytes({'M', 'T', 'h', 'd'}); + + // Chunk size (6 bytes always) + writeBytes({0, 0, 0, 0x06}); + + // Format: 1 (multitrack) + writeBytes({0, 0x01}); + + // Track and ticks info + writeBigEndian2(m_numTracks); + writeBigEndian2(m_ticksPerBeat); +} + +/*---------------------------------------------------------------------------*/ + +void MidiFile::Track::addEvent(Event event, uint32_t time) +{ + event.m_time = time; + event.m_channel = m_channel; + m_events.push_back(event); +} + +void MidiFile::Track::addNote(uint8_t pitch, uint8_t volume, + double realTime, double duration) +{ + Event event; + event.m_note.volume = volume; + + // Add start of note + event.m_type = Event::NOTE_ON; + event.m_note.pitch = pitch; + uint32_t time = realTime * TICKS_PER_BEAT; + addEvent(event, time); + + // Add end of note + event.m_type = Event::NOTE_OFF; + event.m_note.pitch = pitch; + time = (realTime + duration) * TICKS_PER_BEAT; + addEvent(event, time); +} + +void MidiFile::Track::addTempo(uint8_t tempo, uint32_t time) +{ + Event event; + event.m_type = Event::TEMPO; + event.m_tempo = tempo; + addEvent(event, time); +} + +void MidiFile::Track::addProgramChange(uint8_t prog, uint32_t time) +{ + Event event; + event.m_type = Event::PROG_CHANGE; + event.m_programNumber = prog; + addEvent(event, time); +} + +void MidiFile::Track::addName(const string &name, uint32_t time) +{ + Event event; + event.m_type = Event::TRACK_NAME; + event.m_trackName = name; + addEvent(event, time); +} + +void MidiFile::Track::writeToBuffer() +{ + // Chunk ID + writeBytes({'M', 'T', 'r', 'k'}); + + // Chunk size placeholder + writeBigEndian4(0); + size_t idx = m_buffer.size(); + + // Write all events to buffer + writeMIDIToBuffer(); + + // Write correct size in placeholder place + size_t size = m_buffer.size() - idx; + vector v; + writeBigEndian4(size, &v); + for (size_t i = 0; i < 4; ++i) + { + m_buffer[idx - 4 + i] = v[i]; + } +} + +void MidiFile::Track::writeMIDIToBuffer() +{ + // Process events in the eventList + writeEventsToBuffer(); + + // Write MIDI close event + writeBytes({0x00, 0xFF, 0x2F, 0x00}); +} + +void MidiFile::Track::writeEventsToBuffer() +{ + // Create sorted vector of events + vector eventsSorted = m_events; + sort(eventsSorted.begin(), eventsSorted.end()); + + int timeLast = 0; + for (Event &event : eventsSorted) + { + // If something went wrong on sorting, maybe? + if (event.m_time < timeLast) + { + fprintf(stderr, "error: event.m_time=%d timeLast=%d\n", + event.m_time, timeLast); + assert(false); + } + int tmp = event.m_time; + event.m_time -= timeLast; + timeLast = tmp; + + // Write event to buffer + writeSingleEventToBuffer(event); + + // In case of exceding maximum size, go away + if (m_buffer.size() >= BUFFER_SIZE) { break; } + } +} + +void MidiFile::Track::writeSingleEventToBuffer(Event &event) +{ + // First of all, write event time + writeVarLength(event.m_time); + + uint8_t code; + vector fourBytes; + switch (event.m_type) + { + case MidiFile::Event::NOTE_ON: + { + // A note starts playing + code = (0x9 << 4) | m_channel; + writeBytes({code, event.m_note.pitch, event.m_note.volume}); + break; + } + case MidiFile::Event::NOTE_OFF: + { + // A note finishes playing + code = (0x8 << 4) | m_channel; + writeBytes({code, event.m_note.pitch, event.m_note.volume}); + break; + } + case MidiFile::Event::TEMPO: + { + // A tempo measure + code = 0xFF; + writeBytes({code, 0x51, 0x03}); + + // Convert to microseconds before writting + writeBigEndian4(6e7 / event.m_tempo, &fourBytes); + writeBytes({fourBytes[1], fourBytes[2], fourBytes[3]}); + break; + } + case MidiFile::Event::PROG_CHANGE: + { + // Change patch number + code = (0xC << 4) | m_channel; + writeBytes({code, event.m_programNumber}); + break; + } + case MidiFile::Event::TRACK_NAME: + { + // Name of current track + writeBytes({0xFF, 0x03}); + + // Write name string size and then copy it's content + // to the following size bytes of buffer + vector bytes(event.m_trackName.begin(), + event.m_trackName.end()); + writeVarLength(event.m_trackName.size()); + writeBytes(bytes); + break; + } + } +} + +/*---------------------------------------------------------------------------*/ diff --git a/plugins/MidiExport/MidiFile.h b/plugins/MidiExport/MidiFile.h new file mode 100644 index 00000000000..a93fc5a70c1 --- /dev/null +++ b/plugins/MidiExport/MidiFile.h @@ -0,0 +1,238 @@ +#ifndef _MIDI_FILE_H +#define _MIDI_FILE_H + +/** + * Name: MidiFile.h + * 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 + +using std::string; +using std::vector; + +/*---------------------------------------------------------------------------*/ + +//! MIDI file class for exporting purposes +class MidiFile +{ +public: + //! Default number of ticks per single beat + static constexpr int TICKS_PER_BEAT = 128; + + //! Maximum size of buffers used for each section + static constexpr size_t BUFFER_SIZE = 50 * 1024; + + /*-----------------------------------------------------------------------*/ + +private: + //! Base class for sections of MIDI file + class Section + { + public: + //! \brief Constant-capacity buffer vector to be written to stream + //! + //! (should be better than `std::array` as it provides + //! `size()` and `push_back()` funcionalities) + vector m_buffer; + + protected: + //! Reserve constant space for \ref BUFFER_SIZE capacity vector + Section(); + + //! \brief Write bytes to vector (or buffer by default) + //! \param bytes List of bytes to be written + //! \param v Pointer to vector (if none, use \ref m_buffer) + void writeBytes(vector bytes, + vector *v=nullptr); + + /*! \brief Write a MIDI-compatible variable length stream + * \param val A four-byte value + * + * 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. */ + void writeVarLength(uint32_t val); + + //! Buffer gets four 8-bit values from left to right + void writeBigEndian4(uint32_t val, vector *v=nullptr); + + //! Buffer gets two 8-bit values from left to right + void writeBigEndian2(uint16_t val, vector *v=nullptr); + + //! Write section info to buffer + virtual void writeToBuffer() {} + }; + + /*-----------------------------------------------------------------------*/ + +public: + //! Represents MIDI header info + class Header : public Section + { + private: + //! Number of tracks in MIDI file + const int m_numTracks; + + //! How many ticks each beat has + const int m_ticksPerBeat; + + public: + //! Constructor + Header(int numTracks, int ticksPerBeat=TICKS_PER_BEAT); + + //! Write header info to buffer + void writeToBuffer(); + }; + + /*-----------------------------------------------------------------------*/ + +private: + //! Represents a track event. See \ref Event::Type for more info + struct Event + { + //! Possible event types + enum Type + { + NOTE_ON, NOTE_OFF, TEMPO, PROG_CHANGE, TRACK_NAME + } + m_type; + + //! Time count when event happens + int m_time; + + //! Channel number where event is + uint8_t m_channel; + + //! Union for saving space + union + { + //! Note properties + struct + { + //! Note pitch + uint8_t pitch; + + //! Note volume + uint8_t volume; + } + m_note; + + //! Tempo of event (in BPM) + int m_tempo; + + //! Program (patch) number of instrument + uint8_t m_programNumber; + }; + + //! Name of track where event is + // (too much trouble putting it inside union...) + string m_trackName; + + //! \brief Comparison operator + //! Events are sorted by their time (or, in case of tie, type priority) + inline bool operator<(const Event& b) const + { + if (m_time < b.m_time) { return true; } + return (m_time == b.m_time && m_type > b.m_type); + } + }; + + /*-----------------------------------------------------------------------*/ + +public: + //! Represents a MIDI track + class Track : public Section + { + private: + //! Variable-length vector of events + vector m_events; + + public: + //! Track channel number + uint8_t m_channel; + + private: + //! Append a single event to vector + void addEvent(Event event, uint32_t time); + + public: + //! \brief Add both NOTE_ON and NOTE_OFF effects + //! \param pitch Note pitch + //! \param volume Note volume + //! \param realTime Time of note start + //! \param duration How long the note lasts + void addNote(uint8_t pitch, uint8_t volume, + double realTime, double duration); + + //! Add a tempo mark + void addTempo(uint8_t tempo, uint32_t time); + + //! Add a program (patch) change event + void addProgramChange(uint8_t prog, uint32_t time); + + //! Add a track name event + void addName(const string &name, uint32_t time); + + //! Write MIDI track to buffer + void writeToBuffer(); + + private: + //! Write the meta data and note data to buffer + void writeMIDIToBuffer(); + + //! Write the sorted events in \ref m_events to buffer + void writeEventsToBuffer(); + + //! Write a single event to buffer + void writeSingleEventToBuffer(Event &event); + }; + + /*-----------------------------------------------------------------------*/ + +private: + //! Qt file to be opened + QFile m_file; + + //! \brief Smart pointer to write-only Qt data stream + //! (this should automatically be freed upon lifetime end) + QSharedPointer m_stream; + +public: + //! The sole file header + Header m_header; + + //! List of tracks + vector m_tracks; + + //! \brief Open data stream for writing to file and create list of tracks + //! \param filename Name of file to be opened + //! \param nTracks Number of instrument (BB and non BB) tracks + MidiFile(const QString &filename, int nTracks); + + //! Write all data (both header and tracks) to stream + void writeAllToStream(); +}; + +/*---------------------------------------------------------------------------*/ + +#endif diff --git a/plugins/MidiExport/MidiFile.hpp b/plugins/MidiExport/MidiFile.hpp deleted file mode 100644 index a1f91de2fea..00000000000 --- a/plugins/MidiExport/MidiFile.hpp +++ /dev/null @@ -1,311 +0,0 @@ -#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 { - return this->time < b.time || - (this->time == b.time && this->type > b.type); - } -}; - -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