From 6cc7d9fea6d5fed9927f0769442519ef1cbbd216 Mon Sep 17 00:00:00 2001 From: Gustavo Chicato Date: Sat, 16 May 2020 04:32:19 -0300 Subject: [PATCH 01/10] Started working on MIDI exporting (MidiFile.hpp) --- plugins/MidiExport/MidiExport.cpp | 3 +- plugins/MidiExport/MidiExport.h | 2 +- plugins/MidiExport/MidiFile.hpp | 438 ++++++++++++++++++------------ 3 files changed, 268 insertions(+), 175 deletions(-) diff --git a/plugins/MidiExport/MidiExport.cpp b/plugins/MidiExport/MidiExport.cpp index 1860527c10e..777706f41ff 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 @@ -357,4 +357,3 @@ PLUGIN_EXPORT Plugin * lmms_plugin_main( Model *, void * _data ) } - diff --git a/plugins/MidiExport/MidiExport.h b/plugins/MidiExport/MidiExport.h index 3c36eeb8f55..5593512c308 100644 --- a/plugins/MidiExport/MidiExport.h +++ b/plugins/MidiExport/MidiExport.h @@ -68,7 +68,7 @@ class MidiExport: public ExportFilter 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); diff --git a/plugins/MidiExport/MidiFile.hpp b/plugins/MidiExport/MidiFile.hpp index a1f91de2fea..5f067063fa1 100644 --- a/plugins/MidiExport/MidiFile.hpp +++ b/plugins/MidiExport/MidiFile.hpp @@ -18,294 +18,388 @@ *----------------------------------------------------------------------------- */ -#include -#include #include #include -#include #include -#include +#include +#include +#include using std::string; using std::vector; -using std::set; +using std::move; -namespace MidiFile -{ +/*---------------------------------------------------------------------------*/ -const int TICKSPERBEAT = 128; +namespace MidiFile +{ +//! Default number of ticks per single beat +static constexpr u_int16_t TICKS_PER_BEAT = 128; -int writeVarLength(uint32_t val, uint8_t *buffer) +/*! \brief 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. + */ +size_t 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; + size_t size = 0; uint8_t result, little_endian[4]; + + // Build little endian array from 7-bit packs result = val & 0x7F; little_endian[size++] = result; - val = val >> 7; + val >>= 7; while (val > 0) { result = val & 0x7F; - result = result | 0x80; + result |= 0x80; little_endian[size++] = result; - val = val >> 7; + val >>= 7; } - for (int i=0; i> 24; - buf[1] = val >> 16 & 0xff; - buf[2] = val >> 8 & 0xff; - buf[3] = val & 0xff; + buffer[0] = val >> 24; + buffer[1] = (val >> 16) & 0xff; + buffer[2] = (val >> 8) & 0xff; + buffer[3] = val & 0xff; return 4; } -int writeBigEndian2(uint16_t val, uint8_t *buf) +//! Buffer gets two 8-bit values from left to right +const size_t writeBigEndian2(uint16_t val, uint8_t *buffer) { - buf[0] = val >> 8 & 0xff; - buf[1] = val & 0xff; + buffer[0] = (val >> 8) & 0xff; + buffer[1] = val & 0xff; return 2; } +/*---------------------------------------------------------------------------*/ +//! Class to encapsulate the MIDI header structure class MIDIHeader { - // Class to encapsulate the MIDI header structure. +private: + //! Number of tracks in MIDI file uint16_t numTracks; + + //! How many ticks each beat has 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 + + /*-----------------------------------------------------------------------*/ + +public: + MIDIHeader(uint16_t numTracks, uint16_t ticksPerBeat=TICKS_PER_BEAT): + numTracks(numTracks), + ticksPerBeat(ticksPerBeat) {} + + //! Write header info to buffer + inline size_t writeToBuffer(uint8_t *buffer, size_t 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); - + // 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; + + // Write chunks to buffer + start += writeBigEndian2(numTracks, buffer + start); + start += writeBigEndian2(ticksPerBeat, buffer + start); return start; } - }; +/*---------------------------------------------------------------------------*/ +//! Represents a single MIDI event struct Event { + //! Possible event types + enum { NOTE_ON, NOTE_OFF, TEMPO, PROG_CHANGE, TRACK_NAME } type; + + //! Time count when event happens 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; + + //! Channel number where event is uint8_t channel; - - Event() {time=tempo=pitch=programNumber=duration=volume=channel=0; trackName="";} - - inline int writeToBuffer(uint8_t *buffer) const + + // Union for saving space + union { - uint8_t code, fourbytes[4]; - int size=0; - switch (type) + struct + { + //! Note pitch + uint8_t pitch; + + //! Note volume + uint8_t volume; + } + note; + + //! Tempo of event (in BPM) + uint32_t tempo; + + //! Program number where event is + uint8_t programNumber; + }; + + //! Name of track where event is + // (too much trouble putting it inside union...) + string trackName; + + /*-----------------------------------------------------------------------*/ + + //! Write MIDI event info to buffer + inline size_t writeToBuffer(uint8_t *buffer) const + { + uint8_t code, fourBytes[4]; + size_t size = 0; + + switch (type) { case NOTE_ON: - code = 0x9 << 4 | channel; - size += writeVarLength(time, buffer+size); + // A note starts playing + code = (0x9 << 4) | channel; + size = writeVarLength(time, buffer); buffer[size++] = code; - buffer[size++] = pitch; - buffer[size++] = volume; + buffer[size++] = note.pitch; + buffer[size++] = note.volume; break; + case NOTE_OFF: - code = 0x8 << 4 | channel; - size += writeVarLength(time, buffer+size); + // A note finishes playing + code = (0x8 << 4) | channel; + size = writeVarLength(time, buffer); buffer[size++] = code; - buffer[size++] = pitch; - buffer[size++] = volume; + buffer[size++] = note.pitch; + buffer[size++] = note.volume; break; + case TEMPO: + // A tempo measure code = 0xFF; - size += writeVarLength(time, buffer+size); + size = writeVarLength(time, buffer); 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]; + + // Convert to microseconds before writting + writeBigEndian4(6e7 / tempo, fourBytes); + 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); + // Change to another numbered program + code = (0xC << 4) | channel; + size = writeVarLength(time, buffer); buffer[size++] = code; buffer[size++] = programNumber; break; + case TRACK_NAME: - size += writeVarLength(time, buffer+size); + // Name of current track + size = writeVarLength(time, buffer); buffer[size++] = 0xFF; buffer[size++] = 0x03; - size += writeVarLength(trackName.size(), buffer+size); - trackName.copy((char *)(&buffer[size]), trackName.size()); + + // Write name string size and then copy it's content + // to the following size bytes of buffer + 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); + } + + //! \brief Comparison operator + // Events are sorted by their time + inline bool operator<(const Event& b) const + { + if (time < b.time) + { + return true; + } + // In case of same time events, sort by event priority + return (time == b.time and type > b.type); } }; -template +/*---------------------------------------------------------------------------*/ + +//! Class that encapsulates a MIDI track. +// Maximum available track size defined in template +template class MIDITrack { - // A class that encapsulates a MIDI track - // Nested class definitions. +private: + //! Variable-length vector of events vector events; - - public: - uint8_t channel; - - MIDITrack(): channel(0) {} - - inline void addEvent(const Event &e) + + //! Append a single event to vector + inline void addEvent(const Event &e, uint32_t time) { - Event E = e; - events.push_back(E); + Event event = e; + event.time = time; + event.channel = channel; + events.push_back(event); } - - inline void addNote(uint8_t pitch, uint8_t volume, double time, double duration) + +public: + //! Channel number corresponding to self + uint8_t channel = 0; + + // TODO: Constructor? + + /*-----------------------------------------------------------------------*/ + + //! Add both NOTE_ON and NOTE_OFF effects, starting at + // continuous time and separated by a delay + inline void addNote(uint8_t pitch, uint8_t volume, + double realTime, 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)); + Event event; + event.note.volume = volume; + + // Add start of note + event.type = Event::NOTE_ON; + event.note.pitch = pitch; + uint32_t time = realTime * TICKS_PER_BEAT; + addEvent(event, time); + + // Add end of note + event.type = Event::NOTE_OFF; + event.note.pitch = pitch; + time = (realTime + duration) * TICKS_PER_BEAT; + addEvent(event, time); } - - inline void addName(const string &name, uint32_t time) + + //! Add a tempo mark + inline void addTempo(uint8_t tempo, uint32_t time) { - Event event; event.channel = channel; - event.type = Event::TRACK_NAME; event.time=time; event.trackName = name; - addEvent(event); + Event event; + event.type = Event::TEMPO; + event.tempo = tempo; + addEvent(event, time); } - + + //! Add a program change 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); + Event event; + event.type = Event::PROG_CHANGE; + event.programNumber = prog; + addEvent(event, time); } - - inline void addTempo(uint8_t tempo, uint32_t time) + + //! Add a track name event + inline void addName(const string &name, uint32_t time) { - Event event; event.channel = channel; - event.type = Event::TEMPO; event.time=time; event.tempo = tempo; - addEvent(event); + Event event; + event.type = Event::TRACK_NAME; + event.trackName = name; + addEvent(event, time); } - - 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 + /*-----------------------------------------------------------------------*/ + + //! Write the meta data and note data to the packed MIDI stream + inline size_t writeMIDIToBuffer(uint8_t *buffer, size_t start=0) const + { + // Process events in the eventList start += writeEventsToBuffer(buffer, start); - // Write MIDI close event. + // 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 the entire length of the data return start; } - inline int writeEventsToBuffer(uint8_t *buffer, int start=0) const + //! Write the events in MIDIEvents to the MIDI stream + inline size_t writeEventsToBuffer(uint8_t *buffer, size_t 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) + // Created sorted vector of events + vector eventsSorted = events; + std::sort(eventsSorted.begin(), eventsSorted.end()); + + uint32_t timeLast = 0; + for (Event &e : eventsSorted) { - Event e = *it; - if (e.time < time_last){ - printf("error: e.time=%d time_last=%d\n", e.time, time_last); + // If something went wrong on sorting, maybe? + if (e.time < timeLast) + { + fprintf(stderr, "error: e.time=%d timeLast=%d\n", + e.time, timeLast); assert(false); } - tmp = e.time; - e.time -= time_last; - time_last = tmp; - start += e.writeToBuffer(buffer+start); + uint32_t tmp = e.time; + e.time -= timeLast; + timeLast = tmp; + + // Write event to buffer + start += e.writeToBuffer(buffer + start); + + // In case of exceding maximum size, go away if (start >= MAX_TRACK_SIZE) { break; } } return start; } - - inline int writeToBuffer(uint8_t *buffer, int start=0) const + + //! Write MIDI track to buffer + inline size_t writeToBuffer(uint8_t *buffer, size_t start=0) const { + // Write all events to buffer 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; + uint32_t eventsSize = writeMIDIToBuffer(eventsBuffer); + + // Chunk ID + buffer[start++] = 'M'; + buffer[start++] = 'T'; + buffer[start++] = 'r'; + buffer[start++] = 'k'; + + // Chunk size + start += writeBigEndian4(eventsSize, buffer + start); + + // Copy events data + memmove(buffer + start, eventsBuffer, eventsSize); + start += eventsSize; return start; } }; +/*---------------------------------------------------------------------------*/ + }; // namespace #endif From c5a19b50fd8bf81ab67bbd5829633719d583f174 Mon Sep 17 00:00:00 2001 From: Gustavo Chicato Date: Sat, 16 May 2020 21:10:40 -0300 Subject: [PATCH 02/10] Working on MidiExport.cpp (new struct Pattern) --- plugins/MidiExport/MidiExport.cpp | 293 +++++++++++++++--------------- plugins/MidiExport/MidiExport.h | 58 +++--- plugins/MidiExport/MidiFile.hpp | 7 +- 3 files changed, 184 insertions(+), 174 deletions(-) diff --git a/plugins/MidiExport/MidiExport.cpp b/plugins/MidiExport/MidiExport.cpp index 777706f41ff..8fb274f7ee8 100644 --- a/plugins/MidiExport/MidiExport.cpp +++ b/plugins/MidiExport/MidiExport.cpp @@ -23,7 +23,6 @@ * */ - #include #include #include @@ -37,47 +36,105 @@ #include "BBTrack.h" #include "InstrumentTrack.h" #include "LocaleHelper.h" - #include "plugin_export.h" +using std::sort; + extern "C" { +//! Standardized plugin descriptor for MIDI exporter Plugin::Descriptor PLUGIN_EXPORT midiexport_plugin_descriptor = { 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) -{ -} +MidiExport::MidiExport() : ExportFilter( &midiexport_plugin_descriptor) {} +MidiExport::~MidiExport() {} +/*---------------------------------------------------------------------------*/ +struct MidiExport::Pattern +{ + MidiNoteVector notes; + void write(QDomNode element, + int base_pitch, double base_volume, int base_time) + { + // TODO interpret steps="12" muted="0" type="1" name="Piano1" len="2592" + for (QDomNode node = element.firstChild(); !node.isNull(); + node = node.nextSibling()) + { + QDomElement note = node.toElement(); + if (note.attribute("len", "0") == "0") continue; + // TODO interpret pan="0" fxch="0" pitchrange="1" + + MidiNote mnote; + int pitch = note.attribute("key", "0").toInt() + base_pitch; + mnote.pitch = qBound(0, pitch, 127); + // Map from LMMS volume to MIDI velocity + double volume = LocaleHelper::toDouble(note.attribute("vol", "100")); + volume *= base_volume * (127.0 / 200.0); + mnote.volume = qMin(qRound(volume), 127); + mnote.time = base_time + note.attribute("pos", "0").toInt(); + mnote.duration = note.attribute("len", "0").toInt(); + + notes.push_back(mnote); + } + } -MidiExport::~MidiExport() -{ -} + void writeToTrack(MTrack &mtrack) + { + for (MidiNote &n : notes) { + mtrack.addNote(n.pitch, n.volume, + n.time / 48.0, n.duration / 48.0); + } + } + + //! Adjust negative duration BB notes by resizing them positively + void processBBNotes(int cutPos) + { + // Sort in reverse order + sort(notes.rbegin(), notes.rend()); + int cur = INT_MAX, next = INT_MAX; + for (MidiNote &n : notes) + { + if (n.time < cur) + { + next = cur; + cur = n.time; + } + if (n.duration < 0) + { + n.duration = qMin(-n.duration, next - cur); + n.duration = qMin(n.duration, cutPos - n.time); + } + } + } +}; +/*---------------------------------------------------------------------------*/ bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, - const TrackContainer::TrackList &tracks_BB, + const TrackContainer::TrackList &tracksBB, int tempo, int masterPitch, const QString &filename) { + // Open .mid file, from where to be exported, in write mode QFile f(filename); f.open(QIODevice::WriteOnly); QDataStream midiout(&f); @@ -86,30 +143,35 @@ bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, BBTrack* bbTrack; QDomElement element; - - int nTracks = 0; uint8_t buffer[BUFFER_SIZE]; - uint32_t size; - for (const Track* track : tracks) if (track->type() == Track::InstrumentTrack) nTracks++; - for (const Track* track : tracks_BB) if (track->type() == Track::InstrumentTrack) nTracks++; + // Count total number of tracks + uint8_t nTracks = 0; + for (const Track *track : tracks) + { + if (track->type() == Track::InstrumentTrack) nTracks++; + } + for (const Track *track : tracksBB) + { + if (track->type() == Track::InstrumentTrack) nTracks++; + } - // midi header + // Write MIDI header data to file MidiFile::MIDIHeader header(nTracks); - size = header.writeToBuffer(buffer); + size_t size = header.writeToBuffer(buffer); midiout.writeRawData((char *)buffer, size); std::vector>> plists; - // midi tracks - for (Track* track : tracks) + // Iterate through "normal" tracks + for (Track *track : tracks) { DataFile dataFile(DataFile::SongProject); MTrack mtrack; if (track->type() == Track::InstrumentTrack) { - + // Firstly, add info about tempo and track name (at time 0) mtrack.addName(track->name().toStdString(), 0); //mtrack.addProgramChange(0, 0); mtrack.addTempo(tempo, 0); @@ -121,48 +183,48 @@ bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, double base_volume = 1.0; int base_time = 0; - MidiNoteVector pat; + Pattern pat; - for (QDomNode n = element.firstChild(); !n.isNull(); n = n.nextSibling()) + for (QDomNode node = element.firstChild(); not node.isNull(); + node = node.nextSibling()) { - - if (n.nodeName() == "instrumenttrack") + if (node.nodeName() == "instrumenttrack") { - QDomElement it = n.toElement(); + QDomElement it = node.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; + base_volume = LocaleHelper::toDouble( + it.attribute("volume", "100"))/100.0; } - - if (n.nodeName() == "pattern") + else if (node.nodeName() == "pattern") { - base_time = n.toElement().attribute("pos", "0").toInt(); - writePattern(pat, n, base_pitch, base_volume, base_time); + base_time = node.toElement().attribute("pos", "0").toInt(); + pat.write(node, base_pitch, base_volume, base_time); } } - ProcessBBNotes(pat, INT_MAX); - writePatternToTrack(mtrack, pat); + pat.processBBNotes(INT_MAX); + pat.writeToTrack(mtrack); size = mtrack.writeToBuffer(buffer); - midiout.writeRawData((char *)buffer, size); + midiout.writeRawData((char *) buffer, size); } - if (track->type() == Track::BBTrack) + else 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()) + for (QDomNode node = element.firstChild(); not node.isNull(); + node = node.nextSibling()) { - - if (n.nodeName() == "bbtco") + if (node.nodeName() == "bbtco") { - QDomElement it = n.toElement(); + QDomElement it = node.toElement(); int pos = it.attribute("pos", "0").toInt(); int len = it.attribute("len", "0").toInt(); plist.push_back(std::pair(pos, pos+len)); @@ -170,12 +232,11 @@ bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, } std::sort(plist.begin(), plist.end()); plists.push_back(plist); - } - } // for each track + } - // midi tracks in BB tracks - for (Track* track : tracks_BB) + // Iterate through BB tracks + for (Track* track : tracksBB) { DataFile dataFile(DataFile::SongProject); MTrack mtrack; @@ -195,165 +256,103 @@ bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, int base_pitch = 0; double base_volume = 1.0; - for (QDomNode n = element.firstChild(); !n.isNull(); n = n.nextSibling()) + for (QDomNode node = element.firstChild(); not node.isNull(); + node = node.nextSibling()) { - if (n.nodeName() == "instrumenttrack") + if (node.nodeName() == "instrumenttrack") { - QDomElement it = n.toElement(); + QDomElement it = node.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; + base_volume = LocaleHelper::toDouble( + it.attribute("volume", "100"))/100.0; } - - if (n.nodeName() == "pattern") + else if (node.nodeName() == "pattern") { std::vector> &plist = *itr; - MidiNoteVector nv, pat; - writePattern(pat, n, base_pitch, base_volume, 0); + Pattern pat, bbPat; + pat.write(node, 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) + int len = node.toElement().attribute("steps", "1").toInt(); + len *= 12; + for (pair &p : plist) { - while (!st.empty() && st.back().second <= it->first) + while (not st.empty() and st.back().second <= p.first) { - writeBBPattern(pat, nv, len, st.back().first, pos, st.back().second); + writeBBPattern(pat, bbPat, + len, st.back().first, pos, st.back().second); pos = st.back().second; st.pop_back(); } - - if (!st.empty() && st.back().second <= it->second) + if (not st.empty() and st.back().second <= p.second) { - writeBBPattern(pat, nv, len, st.back().first, pos, it->first); - pos = it->first; - while (!st.empty() && st.back().second <= it->second) + writeBBPattern(pat, bbPat, + len, st.back().first, pos, p.first); + pos = p.first; + while (not st.empty() and st.back().second <= p.second) { st.pop_back(); } } - - st.push_back(*it); - pos = it->first; + st.push_back(p); + pos = p.first; } - - while (!st.empty()) + while (not st.empty()) { - writeBBPattern(pat, nv, len, st.back().first, pos, st.back().second); + writeBBPattern(pat, bbPat, + len, st.back().first, pos, st.back().second); pos = st.back().second; st.pop_back(); } - - ProcessBBNotes(nv, pos); - writePatternToTrack(mtrack, nv); + bbPat.processBBNotes(pos); + bbPat.writeToTrack(mtrack); ++itr; } } size = mtrack.writeToBuffer(buffer); - midiout.writeRawData((char *)buffer, size); + midiout.writeRawData((char *) buffer, size); } + // Always returns true 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)); - // 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); - } -} - - - -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, +void MidiExport::writeBBPattern(Pattern &src, Pattern &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) + std::sort(src.notes.begin(), src.notes.end()); + for (MidiNote note : src.notes) { - for (int time = it->time + ceil((start - it->time) / len) - * len; time < end; time += len) + for (int time = note.time + ceil((start - note.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); + note.time += base; + dst.notes.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() -{ - //qDebug() << "MidiExport error: " << m_error ; -} - - +/*---------------------------------------------------------------------------*/ extern "C" { -// necessary for getting instance out of shared lib +// Necessary for getting instance out of shared lib PLUGIN_EXPORT Plugin * lmms_plugin_main( Model *, void * _data ) { return new MidiExport(); } - } diff --git a/plugins/MidiExport/MidiExport.h b/plugins/MidiExport/MidiExport.h index 5593512c308..9838e5c1e57 100644 --- a/plugins/MidiExport/MidiExport.h +++ b/plugins/MidiExport/MidiExport.h @@ -31,31 +31,52 @@ #include "ExportFilter.h" #include "MidiFile.hpp" +using std::vector; -const int BUFFER_SIZE = 50*1024; -typedef MidiFile::MIDITrack MTrack; +/*---------------------------------------------------------------------------*/ +//! Size of the buffer used for exporting info +constexpr size_t BUFFER_SIZE = 50*1024; + +//! A single note struct MidiNote { - int time; + //! The pitch (tone), which can be lower or higher uint8_t pitch; - int duration; + + //! Volume (loudness) uint8_t volume; + //! Absolute time (from song start) when the note starts playing + int time; + + //! For how long the note plays + int duration; + + //! Sort notes by time inline bool operator<(const MidiNote &b) const { - return this->time < b.time; + return time < b.time; } -} ; - -typedef std::vector MidiNoteVector; -typedef std::vector::iterator MidiNoteIterator; +}; +// Helper vector typedefs +typedef vector MidiNoteVector; +typedef vector::iterator MidiNoteIterator; +/*---------------------------------------------------------------------------*/ +//! MIDI exporting base class class MidiExport: public ExportFilter { -// Q_OBJECT + typedef MidiFile::MIDITrack MTrack; + +private: + struct Pattern; + void writeBBPattern(Pattern &src, Pattern &dst, + int len, int base, int start, int end); + void error(); + public: MidiExport(); ~MidiExport(); @@ -65,22 +86,13 @@ class MidiExport: public ExportFilter return nullptr; } - virtual bool tryExport(const TrackContainer::TrackList &tracks, + //! Export (or try to) a list of tracks with tempo and MP + // to designated filename. Return if operation was successful + 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(); - - } ; +/*---------------------------------------------------------------------------*/ #endif diff --git a/plugins/MidiExport/MidiFile.hpp b/plugins/MidiExport/MidiFile.hpp index 5f067063fa1..7c0e7523efd 100644 --- a/plugins/MidiExport/MidiFile.hpp +++ b/plugins/MidiExport/MidiFile.hpp @@ -21,13 +21,12 @@ #include #include #include -#include #include #include using std::string; using std::vector; -using std::move; +using std::sort; /*---------------------------------------------------------------------------*/ @@ -88,7 +87,7 @@ const size_t writeBigEndian2(uint16_t val, uint8_t *buffer) /*---------------------------------------------------------------------------*/ -//! Class to encapsulate the MIDI header structure +//! Class to encapsulate MIDI header structure class MIDIHeader { private: @@ -348,7 +347,7 @@ class MIDITrack { // Created sorted vector of events vector eventsSorted = events; - std::sort(eventsSorted.begin(), eventsSorted.end()); + sort(eventsSorted.begin(), eventsSorted.end()); uint32_t timeLast = 0; for (Event &e : eventsSorted) From c242e64db60ab9b66c8f62d2d24288895f332158 Mon Sep 17 00:00:00 2001 From: Gustavo Chicato Date: Sun, 17 May 2020 01:07:11 -0300 Subject: [PATCH 03/10] More documentation to MidiExport.cpp --- plugins/MidiExport/MidiExport.cpp | 275 +++++++++++++++++------------- plugins/MidiExport/MidiExport.h | 6 +- 2 files changed, 156 insertions(+), 125 deletions(-) diff --git a/plugins/MidiExport/MidiExport.cpp b/plugins/MidiExport/MidiExport.cpp index 8fb274f7ee8..d954881d844 100644 --- a/plugins/MidiExport/MidiExport.cpp +++ b/plugins/MidiExport/MidiExport.cpp @@ -23,21 +23,18 @@ * */ -#include -#include -#include -#include -#include +#include #include "MidiExport.h" -#include "lmms_math.h" -#include "TrackContainer.h" #include "BBTrack.h" #include "InstrumentTrack.h" #include "LocaleHelper.h" #include "plugin_export.h" +using std::pair; +using std::vector; +using std::stack; using std::sort; extern "C" @@ -46,7 +43,7 @@ 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"), @@ -63,49 +60,58 @@ Plugin::Descriptor PLUGIN_EXPORT midiexport_plugin_descriptor = /*---------------------------------------------------------------------------*/ -MidiExport::MidiExport() : ExportFilter( &midiexport_plugin_descriptor) {} +// Constructor and destructor +MidiExport::MidiExport() : + ExportFilter(&midiexport_plugin_descriptor) {} MidiExport::~MidiExport() {} /*---------------------------------------------------------------------------*/ +//! Represents a pattern of notes struct MidiExport::Pattern { + //! Vector of actual notes MidiNoteVector notes; - void write(QDomNode element, - int base_pitch, double base_volume, int base_time) + //! Append notes from root node to pattern + void write(const QDomNode root, + int basePitch, double baseVolume, int baseTime) { - // TODO interpret steps="12" muted="0" type="1" name="Piano1" len="2592" - for (QDomNode node = element.firstChild(); !node.isNull(); - node = node.nextSibling()) + // TODO interpret steps="12" muted="0" type="1" name="Piano1" len="259" + for (QDomNode node = root.firstChild(); not node.isNull(); + node = root.nextSibling()) { QDomElement note = node.toElement(); + + // Ignore zero-length notes if (note.attribute("len", "0") == "0") continue; - // TODO interpret pan="0" fxch="0" pitchrange="1" + // Adjust note attributes based on base measures MidiNote mnote; - int pitch = note.attribute("key", "0").toInt() + base_pitch; + int pitch = note.attribute("key", "0").toInt() + basePitch; mnote.pitch = qBound(0, pitch, 127); - // Map from LMMS volume to MIDI velocity - double volume = LocaleHelper::toDouble(note.attribute("vol", "100")); - volume *= base_volume * (127.0 / 200.0); + double volume = + LocaleHelper::toDouble(note.attribute("vol", "100")); + volume *= baseVolume * (127.0 / 200.0); mnote.volume = qMin(qRound(volume), 127); - mnote.time = base_time + note.attribute("pos", "0").toInt(); + mnote.time = baseTime + note.attribute("pos", "0").toInt(); mnote.duration = note.attribute("len", "0").toInt(); + // Append note to vector notes.push_back(mnote); } } - void writeToTrack(MTrack &mtrack) + //! Add pattern notes to MIDI file track + void writeToTrack(MTrack &mtrack) const { - for (MidiNote &n : notes) { + for (const MidiNote &n : notes) { mtrack.addNote(n.pitch, n.volume, n.time / 48.0, n.duration / 48.0); } } - //! Adjust negative duration BB notes by resizing them positively + //! Adjust special duration BB notes by resizing them accordingly void processBBNotes(int cutPos) { // Sort in reverse order @@ -116,37 +122,59 @@ struct MidiExport::Pattern { if (n.time < cur) { + // Set last two notes positions next = cur; cur = n.time; } if (n.duration < 0) { + // Note should have positive duration that neither + // overlaps next one nor exceeds cutPos n.duration = qMin(-n.duration, next - cur); n.duration = qMin(n.duration, cutPos - n.time); } } } + + //! Write sorted notes to a BB explictly repeating pattern + void writeToBB(Pattern &bbPat, int len, int base, int start, int end) + { + // Avoid misplaced start and end positions + if (start >= end) return; + + // Adjust positions relatively to base pos + start -= base; + end -= base; + + sort(notes.begin(), notes.end()); + for (MidiNote note : notes) + { + // TODO + int t0 = note.time + ceil((start - note.time) / len) * len; + for (int time = t0; time < end; time += len) + { + note.time = base + time; + bbPat.notes.push_back(note); + } + } + } }; /*---------------------------------------------------------------------------*/ +//! Export a list of normal tracks and a list of BB ones, with +// global tempo and master pitch, to a MIDI file indicated by bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, const TrackContainer::TrackList &tracksBB, int tempo, int masterPitch, const QString &filename) { - // Open .mid file, from where to be exported, in write mode + // Open designated blank MIDI file (and data stream) for writing QFile f(filename); f.open(QIODevice::WriteOnly); QDataStream midiout(&f); - InstrumentTrack* instTrack; - BBTrack* bbTrack; - QDomElement element; - - uint8_t buffer[BUFFER_SIZE]; - // Count total number of tracks - uint8_t nTracks = 0; + int nTracks = 0; for (const Track *track : tracks) { if (track->type() == Track::InstrumentTrack) nTracks++; @@ -156,57 +184,68 @@ bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, if (track->type() == Track::InstrumentTrack) nTracks++; } - // Write MIDI header data to file + // Write MIDI header data to stream MidiFile::MIDIHeader header(nTracks); + uint8_t buffer[BUFFER_SIZE]; size_t size = header.writeToBuffer(buffer); - midiout.writeRawData((char *)buffer, size); + midiout.writeRawData((char *) buffer, size); - std::vector>> plists; + // Matrix containing (start, end) pairs for BB objects + vector>> plists; // Iterate through "normal" tracks + QDomElement element; for (Track *track : tracks) { - DataFile dataFile(DataFile::SongProject); MTrack mtrack; + DataFile dataFile(DataFile::SongProject); if (track->type() == Track::InstrumentTrack) { // Firstly, add info about tempo and track name (at time 0) mtrack.addName(track->name().toStdString(), 0); - //mtrack.addProgramChange(0, 0); + // mtrack.addProgramChange(0, 0); mtrack.addTempo(tempo, 0); - instTrack = dynamic_cast(track); + // Cast track as a instrument one and save info from it to element + InstrumentTrack *instTrack = + dynamic_cast(track); element = instTrack->saveState(dataFile, dataFile.content()); - int base_pitch = 0; - double base_volume = 1.0; - int base_time = 0; - + // Get track info and then update pattern + int basePitch; + double baseVolume; Pattern pat; - for (QDomNode node = element.firstChild(); not node.isNull(); node = node.nextSibling()) { + QDomElement e = node.toElement(); + if (node.nodeName() == "instrumenttrack") { - QDomElement it = node.toElement(); - // transpose +12 semitones, workaround for #1857 - base_pitch = (69 - it.attribute("basenote", "57").toInt()); - if (it.attribute("usemasterpitch", "1").toInt()) + // Transpose +12 semitones (workaround for #1857). + // Adjust to masterPitch if enabled + basePitch = e.attribute("basenote", "57").toInt(); + basePitch = 69 - basePitch; + if (e.attribute("usemasterpitch", "1").toInt()) { - base_pitch += masterPitch; + basePitch += masterPitch; } - base_volume = LocaleHelper::toDouble( - it.attribute("volume", "100"))/100.0; + // Volume ranges in [0, 2] + baseVolume = LocaleHelper::toDouble( + e.attribute("volume", "100"))/100.0; } else if (node.nodeName() == "pattern") { - base_time = node.toElement().attribute("pos", "0").toInt(); - pat.write(node, base_pitch, base_volume, base_time); + // Base time == initial position + int baseTime = e.attribute("pos", "0").toInt(); + + // Write track notes to pattern + pat.write(node, basePitch, baseVolume, baseTime); } } + // Write pattern info to MIDI file track, and then to stream pat.processBBNotes(INT_MAX); pat.writeToTrack(mtrack); size = mtrack.writeToBuffer(buffer); @@ -215,22 +254,25 @@ bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, else if (track->type() == Track::BBTrack) { - bbTrack = dynamic_cast(track); + // Cast track as a BB one and save info from it to element + BBTrack *bbTrack = dynamic_cast(track); element = bbTrack->saveState(dataFile, dataFile.content()); - std::vector> plist; + // Build lists of (start, end) pairs from BB note objects + vector> plist; for (QDomNode node = element.firstChild(); not node.isNull(); node = node.nextSibling()) { if (node.nodeName() == "bbtco") { - QDomElement it = node.toElement(); - int pos = it.attribute("pos", "0").toInt(); - int len = it.attribute("len", "0").toInt(); - plist.push_back(std::pair(pos, pos+len)); + QDomElement e = node.toElement(); + int start = e.attribute("pos", "0").toInt(); + int end = start + e.attribute("len", "0").toInt(); + plist.push_back(pair(start, end)); } } - std::sort(plist.begin(), plist.end()); + // Sort list in ascending order and append it to matrix + sort(plist.begin(), plist.end()); plists.push_back(plist); } } @@ -238,121 +280,112 @@ bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, // Iterate through BB tracks for (Track* track : tracksBB) { - DataFile dataFile(DataFile::SongProject); MTrack mtrack; + DataFile dataFile(DataFile::SongProject); - auto itr = plists.begin(); - std::vector> st; - + // Cast track as a instrument one and save info from it to element if (track->type() != Track::InstrumentTrack) continue; + InstrumentTrack *instTrack = dynamic_cast(track); + element = instTrack->saveState(dataFile, dataFile.content()); + // Add usual info to start of track mtrack.addName(track->name().toStdString(), 0); - //mtrack.addProgramChange(0, 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; - + // Get track info and then update pattern(s) + int basePitch; + double baseVolume; + int i = 0; for (QDomNode node = element.firstChild(); not node.isNull(); node = node.nextSibling()) { + QDomElement e = node.toElement(); + if (node.nodeName() == "instrumenttrack") { - QDomElement it = node.toElement(); - // transpose +12 semitones, workaround for #1857 - base_pitch = (69 - it.attribute("basenote", "57").toInt()); - if (it.attribute("usemasterpitch", "1").toInt()) + // Transpose +12 semitones (workaround for #1857). + // Adjust to masterPitch if enabled + basePitch = e.attribute("basenote", "57").toInt(); + basePitch = 69 - basePitch; + if (e.attribute("usemasterpitch", "1").toInt()) { - base_pitch += masterPitch; + basePitch += masterPitch; } - base_volume = LocaleHelper::toDouble( - it.attribute("volume", "100"))/100.0; + // Volume ranges in [0, 2] + baseVolume = LocaleHelper::toDouble( + e.attribute("volume", "100"))/100.0; } else if (node.nodeName() == "pattern") { - std::vector> &plist = *itr; - + // Write to-be repeated BB notes to pattern + // (notice base time of 0) Pattern pat, bbPat; - pat.write(node, base_pitch, base_volume, 0); + pat.write(node, basePitch, baseVolume, 0); - // workaround for nested BBTCOs + // Workaround for nested BBTCOs int pos = 0; - int len = node.toElement().attribute("steps", "1").toInt(); - len *= 12; + int len = 12 * e.attribute("steps", "1").toInt(); + + // Iterate through BBTCO pairs of current list + // TODO: Rewrite or completely refactor this + vector> &plist = plists[i++]; + stack> st; for (pair &p : plist) { - while (not st.empty() and st.back().second <= p.first) + while (not st.empty() and st.top().second <= p.first) { - writeBBPattern(pat, bbPat, - len, st.back().first, pos, st.back().second); - pos = st.back().second; - st.pop_back(); + pat.writeToBB(bbPat, + len, st.top().first, pos, st.top().second); + pos = st.top().second; + st.pop(); } - if (not st.empty() and st.back().second <= p.second) + if (not st.empty() and st.top().second <= p.second) { - writeBBPattern(pat, bbPat, - len, st.back().first, pos, p.first); + pat.writeToBB(bbPat, + len, st.top().first, pos, p.first); pos = p.first; - while (not st.empty() and st.back().second <= p.second) + while (not st.empty() and st.top().second <= p.second) { - st.pop_back(); + st.pop(); } } - st.push_back(p); + st.push(p); pos = p.first; } while (not st.empty()) { - writeBBPattern(pat, bbPat, - len, st.back().first, pos, st.back().second); - pos = st.back().second; - st.pop_back(); + 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(mtrack); - ++itr; + + // Increment matrix line index + i++; } } + // Write track data to stream size = mtrack.writeToBuffer(buffer); midiout.writeRawData((char *) buffer, size); } - // Always returns true + // Always returns success... for now? return true; } /*---------------------------------------------------------------------------*/ -void MidiExport::writeBBPattern(Pattern &src, Pattern &dst, - int len, int base, int start, int end) -{ - if (start >= end) { return; } - start -= base; - end -= base; - std::sort(src.notes.begin(), src.notes.end()); - for (MidiNote note : src.notes) - { - for (int time = note.time + ceil((start - note.time) / len) * len; - time < end; time += len) - { - note.time += base; - dst.notes.push_back(note); - } - } -} - -/*---------------------------------------------------------------------------*/ - extern "C" { // Necessary for getting instance out of shared lib -PLUGIN_EXPORT Plugin * lmms_plugin_main( Model *, void * _data ) +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 9838e5c1e57..4fbe0448262 100644 --- a/plugins/MidiExport/MidiExport.h +++ b/plugins/MidiExport/MidiExport.h @@ -73,8 +73,6 @@ class MidiExport: public ExportFilter private: struct Pattern; - void writeBBPattern(Pattern &src, Pattern &dst, - int len, int base, int start, int end); void error(); public: @@ -89,8 +87,8 @@ class MidiExport: public ExportFilter //! Export (or try to) a list of tracks with tempo and MP // to designated filename. Return if operation was successful bool tryExport(const TrackContainer::TrackList &tracks, - const TrackContainer::TrackList &tracks_BB, - int tempo, int masterPitch, const QString &filename); + const TrackContainer::TrackList &tracksBB, + int tempo, int masterPitch, const QString &filename); } ; /*---------------------------------------------------------------------------*/ From d8659eda38b31bd9a4b3b60183cfd1b860ce0548 Mon Sep 17 00:00:00 2001 From: Gustavo Chicato Date: Sun, 17 May 2020 01:54:31 -0300 Subject: [PATCH 04/10] MIDI exporting preserves individual tracks, instead of merging --- plugins/MidiExport/MidiExport.cpp | 22 ++++++++++------------ plugins/MidiExport/MidiFile.hpp | 10 ++++++---- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/plugins/MidiExport/MidiExport.cpp b/plugins/MidiExport/MidiExport.cpp index d954881d844..ef751ff1b10 100644 --- a/plugins/MidiExport/MidiExport.cpp +++ b/plugins/MidiExport/MidiExport.cpp @@ -79,7 +79,7 @@ struct MidiExport::Pattern { // TODO interpret steps="12" muted="0" type="1" name="Piano1" len="259" for (QDomNode node = root.firstChild(); not node.isNull(); - node = root.nextSibling()) + node = node.nextSibling()) { QDomElement note = node.toElement(); @@ -195,16 +195,17 @@ bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, // Iterate through "normal" tracks QDomElement element; + uint8_t channel_id = 0; for (Track *track : tracks) { - MTrack mtrack; + MTrack mtrack(channel_id++); DataFile dataFile(DataFile::SongProject); if (track->type() == Track::InstrumentTrack) { // Firstly, add info about tempo and track name (at time 0) mtrack.addName(track->name().toStdString(), 0); - // mtrack.addProgramChange(0, 0); + mtrack.addProgramChange(rand() % 8, 0); mtrack.addTempo(tempo, 0); // Cast track as a instrument one and save info from it to element @@ -213,8 +214,8 @@ bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, element = instTrack->saveState(dataFile, dataFile.content()); // Get track info and then update pattern - int basePitch; - double baseVolume; + int basePitch = 57; + double baseVolume = 1.0; Pattern pat; for (QDomNode node = element.firstChild(); not node.isNull(); node = node.nextSibling()) @@ -233,7 +234,7 @@ bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, } // Volume ranges in [0, 2] baseVolume = LocaleHelper::toDouble( - e.attribute("volume", "100"))/100.0; + e.attribute("volume", "100")) / 100.0; } else if (node.nodeName() == "pattern") { @@ -280,7 +281,7 @@ bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, // Iterate through BB tracks for (Track* track : tracksBB) { - MTrack mtrack; + MTrack mtrack(5); DataFile dataFile(DataFile::SongProject); // Cast track as a instrument one and save info from it to element @@ -294,8 +295,8 @@ bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, mtrack.addTempo(tempo, 0); // Get track info and then update pattern(s) - int basePitch; - double baseVolume; + int basePitch = 57; + double baseVolume = 1.0; int i = 0; for (QDomNode node = element.firstChild(); not node.isNull(); node = node.nextSibling()) @@ -363,9 +364,6 @@ bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, // Write pattern info to MIDI file track bbPat.processBBNotes(pos); bbPat.writeToTrack(mtrack); - - // Increment matrix line index - i++; } } // Write track data to stream diff --git a/plugins/MidiExport/MidiFile.hpp b/plugins/MidiExport/MidiFile.hpp index 7c0e7523efd..bde0e8c27cb 100644 --- a/plugins/MidiExport/MidiFile.hpp +++ b/plugins/MidiExport/MidiFile.hpp @@ -212,7 +212,7 @@ struct Event break; case PROG_CHANGE: - // Change to another numbered program + // Change patch number code = (0xC << 4) | channel; size = writeVarLength(time, buffer); buffer[size++] = code; @@ -269,10 +269,12 @@ class MIDITrack } public: - //! Channel number corresponding to self - uint8_t channel = 0; + //! Track channel number + uint8_t channel; - // TODO: Constructor? + //! Constructor + MIDITrack(uint8_t channel): + channel(channel) {} /*-----------------------------------------------------------------------*/ From 3e914fff8353cbab6fd5ccdd24c30739d2866f9d Mon Sep 17 00:00:00 2001 From: Gustavo Chicato Date: Sun, 17 May 2020 06:48:51 -0300 Subject: [PATCH 05/10] SF2 instrument patch info is MIDI exported successfully to tracks --- plugins/MidiExport/MidiExport.cpp | 39 ++++++++++++++++++++----------- plugins/MidiExport/MidiExport.h | 2 +- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/plugins/MidiExport/MidiExport.cpp b/plugins/MidiExport/MidiExport.cpp index ef751ff1b10..542d6b6966b 100644 --- a/plugins/MidiExport/MidiExport.cpp +++ b/plugins/MidiExport/MidiExport.cpp @@ -23,14 +23,15 @@ * */ -#include - #include "MidiExport.h" +#include + #include "BBTrack.h" #include "InstrumentTrack.h" #include "LocaleHelper.h" #include "plugin_export.h" +#include "../sf2_player/sf2_player.h" using std::pair; using std::vector; @@ -162,8 +163,6 @@ struct MidiExport::Pattern /*---------------------------------------------------------------------------*/ -//! Export a list of normal tracks and a list of BB ones, with -// global tempo and master pitch, to a MIDI file indicated by bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, const TrackContainer::TrackList &tracksBB, int tempo, int masterPitch, const QString &filename) @@ -203,16 +202,28 @@ bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, if (track->type() == Track::InstrumentTrack) { - // Firstly, add info about tempo and track name (at time 0) - mtrack.addName(track->name().toStdString(), 0); - mtrack.addProgramChange(rand() % 8, 0); - mtrack.addTempo(tempo, 0); - // Cast track as a instrument one and save info from it to element InstrumentTrack *instTrack = dynamic_cast(track); element = instTrack->saveState(dataFile, dataFile.content()); + // Add info about tempo and track number + mtrack.addTempo(tempo, 0); + mtrack.addName(track->name().toStdString(), 0); + + // If the current track is a Sf2 Player one, set the current + // patch for 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). + uint8_t patch = 0; + QString instName = instTrack->instrumentName(); + if (instName == "Sf2 Player") + { + class Instrument *inst = instTrack->instrument(); + patch = inst->childModel("patch")->value(); + } + mtrack.addProgramChange(patch, 0); + // Get track info and then update pattern int basePitch = 57; double baseVolume = 1.0; @@ -284,16 +295,16 @@ bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, MTrack mtrack(5); DataFile dataFile(DataFile::SongProject); - // Cast track as a instrument one and save info from it to element - if (track->type() != Track::InstrumentTrack) continue; - InstrumentTrack *instTrack = dynamic_cast(track); - element = instTrack->saveState(dataFile, dataFile.content()); - // Add usual info to start of track mtrack.addName(track->name().toStdString(), 0); // mtrack.addProgramChange(0, 0); mtrack.addTempo(tempo, 0); + // Cast track as a instrument one and save info from it to element + if (track->type() != Track::InstrumentTrack) continue; + InstrumentTrack *instTrack = dynamic_cast(track); + element = instTrack->saveState(dataFile, dataFile.content()); + // Get track info and then update pattern(s) int basePitch = 57; double baseVolume = 1.0; diff --git a/plugins/MidiExport/MidiExport.h b/plugins/MidiExport/MidiExport.h index 4fbe0448262..db723aa54db 100644 --- a/plugins/MidiExport/MidiExport.h +++ b/plugins/MidiExport/MidiExport.h @@ -84,7 +84,7 @@ class MidiExport: public ExportFilter return nullptr; } - //! Export (or try to) a list of tracks with tempo and MP + //! Export a list of tracks with tempo and master pitch // to designated filename. Return if operation was successful bool tryExport(const TrackContainer::TrackList &tracks, const TrackContainer::TrackList &tracksBB, From 692f2b5ed2cd512c3ddd854bea8c2d9eb18a294a Mon Sep 17 00:00:00 2001 From: Gustavo Chicato Date: Thu, 21 May 2020 23:55:39 -0300 Subject: [PATCH 06/10] MidiExport: refactoring and more documentation (in progress...) --- plugins/MidiExport/MidiExport.cpp | 411 ++++++++--------- plugins/MidiExport/MidiExport.h | 69 ++- plugins/MidiExport/MidiFile.hpp | 722 +++++++++++++++++------------- 3 files changed, 644 insertions(+), 558 deletions(-) diff --git a/plugins/MidiExport/MidiExport.cpp b/plugins/MidiExport/MidiExport.cpp index 542d6b6966b..56510ca3486 100644 --- a/plugins/MidiExport/MidiExport.cpp +++ b/plugins/MidiExport/MidiExport.cpp @@ -27,14 +27,12 @@ #include +#include "Instrument.h" #include "BBTrack.h" #include "InstrumentTrack.h" #include "LocaleHelper.h" #include "plugin_export.h" -#include "../sf2_player/sf2_player.h" -using std::pair; -using std::vector; using std::stack; using std::sort; @@ -42,7 +40,7 @@ extern "C" { //! Standardized plugin descriptor for MIDI exporter -Plugin::Descriptor PLUGIN_EXPORT midiexport_plugin_descriptor = +static Plugin::Descriptor PLUGIN_EXPORT midiexport_plugin_descriptor = { STRINGIFY(PLUGIN_NAME), "MIDI Export", @@ -61,274 +59,213 @@ Plugin::Descriptor PLUGIN_EXPORT midiexport_plugin_descriptor = /*---------------------------------------------------------------------------*/ -// Constructor and destructor -MidiExport::MidiExport() : - ExportFilter(&midiexport_plugin_descriptor) {} -MidiExport::~MidiExport() {} - -/*---------------------------------------------------------------------------*/ - -//! Represents a pattern of notes -struct MidiExport::Pattern -{ - //! Vector of actual notes - MidiNoteVector notes; - - //! Append notes from root node to pattern - void write(const QDomNode root, - int basePitch, double baseVolume, int baseTime) +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(); not node.isNull(); node = node.nextSibling()) { - QDomElement note = node.toElement(); + QDomElement element = node.toElement(); // Ignore zero-length notes - if (note.attribute("len", "0") == "0") continue; + if (element.attribute("len", "0") == "0") continue; // Adjust note attributes based on base measures - MidiNote mnote; - int pitch = note.attribute("key", "0").toInt() + basePitch; - mnote.pitch = qBound(0, pitch, 127); + MidiNote note; + int pitch = element.attribute("key", "0").toInt() + basePitch; + note.m_pitch = qBound(0, pitch, 127); double volume = - LocaleHelper::toDouble(note.attribute("vol", "100")); + LocaleHelper::toDouble(element.attribute("vol", "100")); volume *= baseVolume * (127.0 / 200.0); - mnote.volume = qMin(qRound(volume), 127); - mnote.time = baseTime + note.attribute("pos", "0").toInt(); - mnote.duration = note.attribute("len", "0").toInt(); + 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 - notes.push_back(mnote); + m_notes.push_back(note); } } - //! Add pattern notes to MIDI file track - void writeToTrack(MTrack &mtrack) const - { - for (const MidiNote &n : notes) { - mtrack.addNote(n.pitch, n.volume, - n.time / 48.0, n.duration / 48.0); - } +void MidiExport::Pattern::writeToTrack(MidiFile::Track &mTrack) const +{ + for (const MidiNote &n : m_notes) { + mTrack.addNote(n.m_pitch, n.m_volume, + n.m_time / 48.0, n.m_duration / 48.0); } +} - //! Adjust special duration BB notes by resizing them accordingly - void processBBNotes(int cutPos) - { - // Sort in reverse order - sort(notes.rbegin(), notes.rend()); +void MidiExport::Pattern::processBBNotes(int cutPos) +{ + // Sort in reverse order + sort(m_notes.rbegin(), m_notes.rend()); - int cur = INT_MAX, next = INT_MAX; - for (MidiNote &n : notes) + int cur = INT_MAX, next = INT_MAX; + for (MidiNote &n : m_notes) + { + if (n.m_time < cur) { - if (n.time < cur) - { - // Set last two notes positions - next = cur; - cur = n.time; - } - if (n.duration < 0) - { - // Note should have positive duration that neither - // overlaps next one nor exceeds cutPos - n.duration = qMin(-n.duration, next - cur); - n.duration = qMin(n.duration, cutPos - n.time); - } + // Set last two notes positions + next = cur; + cur = n.m_time; + } + if (n.m_duration < 0) + { + // Note should have positive duration that neither + // overlaps next one nor exceeds cutPos + n.m_duration = qMin(-n.m_duration, next - cur); + n.m_duration = qMin(n.m_duration, cutPos - n.m_time); } } +} - //! Write sorted notes to a BB explictly repeating pattern - void writeToBB(Pattern &bbPat, int len, int base, int start, int end) - { - // Avoid misplaced start and end positions - if (start >= end) return; +void MidiExport::Pattern::writeToBB(Pattern &bbPat, + int len, int base, int start, int end) +{ + // Avoid misplaced start and end positions + if (start >= end) { return; } - // Adjust positions relatively to base pos - start -= base; - end -= base; + // Adjust positions relatively to base pos + start -= base; + end -= base; - sort(notes.begin(), notes.end()); - for (MidiNote note : notes) + sort(m_notes.begin(), m_notes.end()); + for (MidiNote 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) { - // TODO - int t0 = note.time + ceil((start - note.time) / len) * len; - for (int time = t0; time < end; time += len) - { - note.time = base + time; - bbPat.notes.push_back(note); - } + note.m_time = base + time; + bbPat.m_notes.push_back(note); } } -}; +} + +/*---------------------------------------------------------------------------*/ + +// Constructor and destructor +MidiExport::MidiExport() : + ExportFilter(&midiexport_plugin_descriptor) {} /*---------------------------------------------------------------------------*/ bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, - const TrackContainer::TrackList &tracksBB, - int tempo, int masterPitch, const QString &filename) + const TrackContainer::TrackList &tracksBB, + int tempo, int masterPitch, const QString &filename) { - // Open designated blank MIDI file (and data stream) for writing - QFile f(filename); - f.open(QIODevice::WriteOnly); - QDataStream midiout(&f); - // Count total number of tracks - int nTracks = 0; - for (const Track *track : tracks) + uint16_t nTracks = 0; + for (auto trackList : {tracks, tracksBB}) { - if (track->type() == Track::InstrumentTrack) nTracks++; - } - for (const Track *track : tracksBB) - { - if (track->type() == Track::InstrumentTrack) nTracks++; + for (const Track *track : trackList) + { + if (track->type() == Track::InstrumentTrack) { nTracks++; } + } } + // Create MIDI file object + MidiFile file(filename, nTracks); + m_file = &file; - // Write MIDI header data to stream - MidiFile::MIDIHeader header(nTracks); - uint8_t buffer[BUFFER_SIZE]; - size_t size = header.writeToBuffer(buffer); - midiout.writeRawData((char *) buffer, size); + // Write header info + m_file->m_header.writeToBuffer(); - // Matrix containing (start, end) pairs for BB objects - vector>> plists; + // Some useful class members + m_tempo = tempo; + m_masterPitch = masterPitch; // Iterate through "normal" tracks - QDomElement element; uint8_t channel_id = 0; for (Track *track : tracks) { - MTrack mtrack(channel_id++); - DataFile dataFile(DataFile::SongProject); - if (track->type() == Track::InstrumentTrack) { - // Cast track as a instrument one and save info from it to element - InstrumentTrack *instTrack = - dynamic_cast(track); - element = instTrack->saveState(dataFile, dataFile.content()); - - // Add info about tempo and track number - mtrack.addTempo(tempo, 0); - mtrack.addName(track->name().toStdString(), 0); - - // If the current track is a Sf2 Player one, set the current - // patch for 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). - uint8_t patch = 0; - QString instName = instTrack->instrumentName(); - if (instName == "Sf2 Player") - { - class Instrument *inst = instTrack->instrument(); - patch = inst->childModel("patch")->value(); - } - mtrack.addProgramChange(patch, 0); - - // Get track info and then update pattern - int basePitch = 57; - double baseVolume = 1.0; - Pattern pat; - for (QDomNode node = element.firstChild(); not node.isNull(); - node = node.nextSibling()) - { - QDomElement e = node.toElement(); - - if (node.nodeName() == "instrumenttrack") - { - // Transpose +12 semitones (workaround for #1857). - // Adjust to masterPitch if enabled - basePitch = e.attribute("basenote", "57").toInt(); - basePitch = 69 - basePitch; - if (e.attribute("usemasterpitch", "1").toInt()) - { - basePitch += masterPitch; - } - // Volume ranges in [0, 2] - baseVolume = LocaleHelper::toDouble( - e.attribute("volume", "100")) / 100.0; - } - else if (node.nodeName() == "pattern") - { - // Base time == initial position - int baseTime = e.attribute("pos", "0").toInt(); - - // Write track notes to pattern - pat.write(node, basePitch, baseVolume, baseTime); - } - - } - // Write pattern info to MIDI file track, and then to stream - pat.processBBNotes(INT_MAX); - pat.writeToTrack(mtrack); - size = mtrack.writeToBuffer(buffer); - midiout.writeRawData((char *) buffer, size); + foo(track, channel_id); } - else if (track->type() == Track::BBTrack) { - // Cast track as a BB one and save info from it to element - BBTrack *bbTrack = dynamic_cast(track); - element = bbTrack->saveState(dataFile, dataFile.content()); - - // Build lists of (start, end) pairs from BB note objects - vector> plist; - for (QDomNode node = element.firstChild(); not node.isNull(); - node = node.nextSibling()) - { - if (node.nodeName() == "bbtco") - { - QDomElement e = node.toElement(); - int start = e.attribute("pos", "0").toInt(); - int end = start + e.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()); - plists.push_back(plist); + goo(track); } } - // Iterate through BB tracks for (Track* track : tracksBB) { - MTrack mtrack(5); - DataFile dataFile(DataFile::SongProject); - - // Add usual info to start of track - mtrack.addName(track->name().toStdString(), 0); - // mtrack.addProgramChange(0, 0); - mtrack.addTempo(tempo, 0); - - // Cast track as a instrument one and save info from it to element - if (track->type() != Track::InstrumentTrack) continue; - InstrumentTrack *instTrack = dynamic_cast(track); - element = instTrack->saveState(dataFile, dataFile.content()); - - // Get track info and then update pattern(s) - int basePitch = 57; - double baseVolume = 1.0; - int i = 0; - for (QDomNode node = element.firstChild(); not node.isNull(); - node = node.nextSibling()) - { - QDomElement e = node.toElement(); + foo(track, channel_id, true); + } + // Write buffered data to stream + m_file->writeAllToStream(); + + // Always returns success... for now? + return true; +} + +void MidiExport::foo(Track *track, uint8_t &channelID, bool isBB) +{ + // Cast track as a instrument one and save info from it to element + InstrumentTrack *instTrack = dynamic_cast(track); + QDomElement element = + instTrack->saveState(m_dataFile, m_dataFile.content()); + + // Create track with incremental id + MidiFile::Track &midiTrack = m_file->m_tracks[channelID]; + + // 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 for 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). + uint8_t patch = 0; + QString instName = instTrack->instrumentName(); + if (instName == "Sf2 Player") + { + class Instrument *inst = instTrack->instrument(); + patch = inst->childModel("patch")->value(); + } + midiTrack.addProgramChange(patch, 0); + + // Get track info and then update pattern + int basePitch = 57; + double baseVolume = 1.0; + Pattern pat; + int i = 0; + for (QDomNode node = element.firstChild(); not node.isNull(); + node = node.nextSibling()) + { + QDomElement e = node.toElement(); - if (node.nodeName() == "instrumenttrack") + if (node.nodeName() == "instrumenttrack") + { + // Transpose +12 semitones (workaround for #1857). + // Adjust to masterPitch if enabled + basePitch = e.attribute("basenote", "57").toInt(); + basePitch = 69 - basePitch; + if (e.attribute("usemasterpitch", "1").toInt()) { - // Transpose +12 semitones (workaround for #1857). - // Adjust to masterPitch if enabled - basePitch = e.attribute("basenote", "57").toInt(); - basePitch = 69 - basePitch; - if (e.attribute("usemasterpitch", "1").toInt()) - { - basePitch += masterPitch; - } - // Volume ranges in [0, 2] - baseVolume = LocaleHelper::toDouble( - e.attribute("volume", "100"))/100.0; + basePitch += m_masterPitch; } - else if (node.nodeName() == "pattern") + // Volume ranges in [0.0, 2.0] + baseVolume = LocaleHelper::toDouble( + e.attribute("volume", "100")) / 100.0; + } + else if (node.nodeName() == "pattern") + { + if (not isBB) + { + // Base time == initial position + int baseTime = e.attribute("pos", "0").toInt(); + + // Write track notes to pattern + pat.write(node, basePitch, baseVolume, baseTime); + + // Write pattern info to MIDI file track + pat.processBBNotes(INT_MAX); + pat.writeToTrack(midiTrack); + } + else { // Write to-be repeated BB notes to pattern // (notice base time of 0) @@ -340,10 +277,10 @@ bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, int len = 12 * e.attribute("steps", "1").toInt(); // Iterate through BBTCO pairs of current list - // TODO: Rewrite or completely refactor this - vector> &plist = plists[i++]; + // TODO: This *may* need some corrections? + const vector> &plist = m_plists[i++]; stack> st; - for (pair &p : plist) + for (const pair &p : plist) { while (not st.empty() and st.top().second <= p.first) { @@ -374,16 +311,38 @@ bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, } // Write pattern info to MIDI file track bbPat.processBBNotes(pos); - bbPat.writeToTrack(mtrack); + bbPat.writeToTrack(midiTrack); + + // Write track data to buffer + midiTrack.writeToBuffer(); } } - // Write track data to stream - size = mtrack.writeToBuffer(buffer); - midiout.writeRawData((char *) buffer, size); } +} - // Always returns success... for now? - return true; +void MidiExport::goo(Track *track) +{ + // Cast track as a BB one and save info from it to element + BBTrack *bbTrack = dynamic_cast(track); + QDomElement element = + bbTrack->saveState(m_dataFile, m_dataFile.content()); + + // Build lists of (start, end) pairs from BB note objects + vector> plist; + for (QDomNode node = element.firstChild(); not node.isNull(); + node = node.nextSibling()) + { + if (node.nodeName() == "bbtco") + { + QDomElement e = node.toElement(); + int start = e.attribute("pos", "0").toInt(); + int end = start + e.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); } /*---------------------------------------------------------------------------*/ @@ -391,7 +350,7 @@ bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, extern "C" { -// Necessary for getting instance out of shared lib +//! Necessary for getting instance out of shared lib PLUGIN_EXPORT Plugin * lmms_plugin_main(Model *, void * _data) { return new MidiExport(); diff --git a/plugins/MidiExport/MidiExport.h b/plugins/MidiExport/MidiExport.h index db723aa54db..da07950bf6b 100644 --- a/plugins/MidiExport/MidiExport.h +++ b/plugins/MidiExport/MidiExport.h @@ -28,52 +28,80 @@ #include -#include "ExportFilter.h" #include "MidiFile.hpp" +#include "DataFile.h" +#include "ExportFilter.h" +using std::pair; using std::vector; /*---------------------------------------------------------------------------*/ -//! Size of the buffer used for exporting info -constexpr size_t BUFFER_SIZE = 50*1024; - //! A single note struct MidiNote { //! The pitch (tone), which can be lower or higher - uint8_t pitch; + uint8_t m_pitch; //! Volume (loudness) - uint8_t volume; + uint8_t m_volume; //! Absolute time (from song start) when the note starts playing - int time; + int m_time; //! For how long the note plays - int duration; + int m_duration; //! Sort notes by time inline bool operator<(const MidiNote &b) const { - return time < b.time; + return m_time < b.m_time; } }; -// Helper vector typedefs -typedef vector MidiNoteVector; -typedef vector::iterator MidiNoteIterator; - /*---------------------------------------------------------------------------*/ //! MIDI exporting base class -class MidiExport: public ExportFilter +class MidiExport : public ExportFilter { - typedef MidiFile::MIDITrack MTrack; - private: - struct Pattern; - void error(); + //! Represents a pattern of notes + struct Pattern + { + //! Vector of actual notes + vector m_notes; + + //! Append notes from root node to pattern + void write(const QDomNode &root, + int basePitch, double baseVolume, int baseTime); + + //! Add pattern notes to MIDI file track + void writeToTrack(MidiFile::Track &mTrack) const; + + //! Adjust special duration BB notes by resizing them accordingly + void processBBNotes(int cutPos); + + //! Write sorted notes to a explicitly repeating BB pattern + void writeToBB(Pattern &bbPat, + int len, int base, int start, int end); + }; + + //! DataFile to be used by Qt elements + DataFile m_dataFile = DataFile(DataFile::SongProject); + + MidiFile *m_file; + + //! Song tempo + int m_tempo; + + //! Song master pitch + int m_masterPitch; + + //! Matrix containing (start, end) pairs for BB objects + vector>> m_plists; + + void foo(Track *track, uint8_t &channelID, bool isBB=false); + void goo(Track *track); public: MidiExport(); @@ -84,8 +112,9 @@ class MidiExport: public ExportFilter return nullptr; } - //! Export a list of tracks with tempo and master pitch - // to designated filename. Return if operation was successful + //! Export normal and BB tracks from a project with designated + // tempo and master pitch to a file indicated by \param filename + // \return If operation was successful bool tryExport(const TrackContainer::TrackList &tracks, const TrackContainer::TrackList &tracksBB, int tempo, int masterPitch, const QString &filename); diff --git a/plugins/MidiExport/MidiFile.hpp b/plugins/MidiExport/MidiFile.hpp index bde0e8c27cb..e3aeea1fc27 100644 --- a/plugins/MidiExport/MidiFile.hpp +++ b/plugins/MidiExport/MidiFile.hpp @@ -19,388 +19,486 @@ */ #include +#include +#include #include +#include #include -#include #include +#include +#include +#include + using std::string; +using std::array; +using std::stack; using std::vector; +using std::initializer_list; using std::sort; /*---------------------------------------------------------------------------*/ -namespace MidiFile +//! MIDI file class used in exporting +class MidiFile { +public: + //! Default number of ticks per single beat + static constexpr u_int16_t TICKS_PER_BEAT = 128; -//! Default number of ticks per single beat -static constexpr u_int16_t TICKS_PER_BEAT = 128; + //! Maximum size of buffers used for each section + static constexpr size_t BUFFER_SIZE = 50 * 1024; -/*! \brief 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. - */ -size_t writeVarLength(uint32_t val, uint8_t *buffer) -{ - size_t size = 0; - uint8_t result, little_endian[4]; +private: + //! Base class for sections of MIDI file + class Section + { + public: + //! Constant-capacity vector to serve as buffer before writting + // to stream (should be better than std::array as it provides + // size() and push_back() funcionalities) + vector m_buffer; + + //! Reserve constant space for BUFFER_SIZE capacity vector + Section(); + + protected: + //! Write bytes from initializer list to vector (or buffer by default) + 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(); + }; - // Build little endian array from 7-bit packs - result = val & 0x7F; - little_endian[size++] = result; - val >>= 7; - while (val > 0) + /*-----------------------------------------------------------------------*/ + + //! Represents MIDI header info + class Header : public Section { - result = val & 0x7F; - result |= 0x80; - little_endian[size++] = result; - val >>= 7; - } - // Reverse it for the actual buffer - for (int i = 0; i < size; i++) + private: + //! Number of tracks in MIDI file + uint16_t m_numTracks; + + //! How many ticks each beat has + uint16_t m_ticksPerBeat; + + public: + //! Constructor + Header(uint16_t numTracks, uint16_t ticksPerBeat=TICKS_PER_BEAT); + + //! Write header info to buffer + void writeToBuffer(); + }; + + /*-----------------------------------------------------------------------*/ + +public: + //! Represents a MIDI track + class Track : public Section { - buffer[i] = little_endian[size-i-1]; + private: + struct Event + { + //! Possible event types + enum { NOTE_ON, NOTE_OFF, TEMPO, PROG_CHANGE, TRACK_NAME } type; + + //! Time count when event happens + uint32_t time; + + //! Channel number where event is + uint8_t channel; + + // Union for saving space + union + { + struct + { + //! Note pitch + uint8_t pitch; + + //! Note volume + uint8_t volume; + } + note; + + //! Tempo of event (in BPM) + uint32_t tempo; + + //! Program (patch) number of instrument + uint8_t programNumber; + }; + + //! Name of track where event is + // (too much trouble putting it inside union...) + string trackName; + + //! Write MIDI event info to track buffer + inline void writeToBuffer(Track &parent) const; + + //! \brief Comparison operator + // Events are sorted by their time + inline bool operator<(const Event& b) const; + }; + + /*-------------------------------------------------------------------*/ + + //! Variable-length vector of events + vector events; + + //! Append a single event to vector + inline void addEvent(Event event, uint32_t time); + + public: + //! Track channel number + uint8_t channel; + + //! Constructor + Track(uint8_t channel); + + //! Add both NOTE_ON and NOTE_OFF effects + // \param realTime Time of note start + // \param duration How long the note lasts + inline void addNote(uint8_t pitch, uint8_t volume, + double realTime, double duration); + + //! Add a tempo mark + inline void addTempo(uint8_t tempo, uint32_t time); + + //! Add a program (patch) change event + inline void addProgramChange(uint8_t prog, uint32_t time); + + //! Add a track name event + inline void addName(const string &name, uint32_t time); + + //! Write MIDI track to buffer + inline void writeToBuffer(); + + //! Write the meta data and note data to the packed MIDI stream + inline void writeMIDIToBuffer(); + + //! Write the events in MIDIEvents to the MIDI stream + inline void writeEventsToBuffer(); + }; + + /*-----------------------------------------------------------------------*/ + +private: + //! Qt file to be opened + QFile m_file; + + //! Qt data stream for writing + QDataStream *m_stream; + +public: + //! The sole file header + Header m_header; + + //! List of tracks + vector m_tracks; + + MidiFile(const QString &filename, uint16_t nTracks); + + ~MidiFile(); + + void writeAllToStream(); +}; + +/*---------------------------------------------------------------------------*/ + +MidiFile::MidiFile(const QString &filename, uint16_t nTracks): + m_file(filename), m_header(nTracks) +{ + // Open designated blank MIDI file (and data stream) for writing + m_file.open(QIODevice::WriteOnly); + m_stream = new QDataStream(&m_file); + + // Reserve space for track list + m_tracks.reserve(nTracks); + for (size_t i = 0; i < nTracks; i++) + { + m_tracks.push_back(Track(i)); } - return size; } -//! Buffer gets four 8-bit values from left to right -const size_t writeBigEndian4(uint32_t val, uint8_t *buffer) +MidiFile::~MidiFile() { - buffer[0] = val >> 24; - buffer[1] = (val >> 16) & 0xff; - buffer[2] = (val >> 8) & 0xff; - buffer[3] = val & 0xff; - return 4; + delete m_stream; } -//! Buffer gets two 8-bit values from left to right -const size_t writeBigEndian2(uint16_t val, uint8_t *buffer) +void MidiFile::writeAllToStream() { - buffer[0] = (val >> 8) & 0xff; - buffer[1] = val & 0xff; - return 2; + 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()); + } } /*---------------------------------------------------------------------------*/ -//! Class to encapsulate MIDI header structure -class MIDIHeader +MidiFile::Section::Section() { -private: - //! Number of tracks in MIDI file - uint16_t numTracks; - - //! How many ticks each beat has - uint16_t ticksPerBeat; - - /*-----------------------------------------------------------------------*/ + m_buffer.reserve(BUFFER_SIZE); +} -public: - MIDIHeader(uint16_t numTracks, uint16_t ticksPerBeat=TICKS_PER_BEAT): - numTracks(numTracks), - ticksPerBeat(ticksPerBeat) {} +void MidiFile::Section::writeBytes(vector bytes, vector *v) +{ + if (not v) v = &m_buffer; + v->insert(v->end(), bytes.begin(), bytes.end()); +} - //! Write header info to buffer - inline size_t writeToBuffer(uint8_t *buffer, size_t start=0) const +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 (not little_endian.empty()) { - // 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; - - // Write chunks to buffer - start += writeBigEndian2(numTracks, buffer + start); - start += writeBigEndian2(ticksPerBeat, buffer + start); - return start; + 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); +} -//! Represents a single MIDI event -struct Event +void MidiFile::Section::writeBigEndian2(uint16_t val, vector *v) { - //! Possible event types - enum { NOTE_ON, NOTE_OFF, TEMPO, PROG_CHANGE, TRACK_NAME } type; + vector bytes; + bytes.push_back(val >> 8); + bytes.push_back(val & 0xff); + writeBytes(bytes, v); +} - //! Time count when event happens - uint32_t time; +/*---------------------------------------------------------------------------*/ - //! Channel number where event is - uint8_t channel; +MidiFile::Header::Header(uint16_t numTracks, uint16_t ticksPerBeat): + m_numTracks(numTracks), + m_ticksPerBeat(ticksPerBeat) {} - // Union for saving space - union - { - struct - { - //! Note pitch - uint8_t pitch; +void MidiFile::Header::writeToBuffer() +{ + // Chunk ID + writeBytes({'M', 'T', 'h', 'd'}); - //! Note volume - uint8_t volume; - } - note; + // Chunk size (6 bytes always) + writeBytes({0, 0, 0, 0x06}); - //! Tempo of event (in BPM) - uint32_t tempo; + // Format: 1 (multitrack) + writeBytes({0, 0x01}); - //! Program number where event is - uint8_t programNumber; - }; + // Extra info + writeBigEndian2(m_numTracks); + writeBigEndian2(m_ticksPerBeat); +} - //! Name of track where event is - // (too much trouble putting it inside union...) - string trackName; +/*---------------------------------------------------------------------------*/ - /*-----------------------------------------------------------------------*/ +inline void MidiFile::Track::Event::writeToBuffer(Track &parent) const +{ + // First of all, write event time + parent.writeVarLength(time); - //! Write MIDI event info to buffer - inline size_t writeToBuffer(uint8_t *buffer) const + uint8_t code; + vector fourBytes; + switch (type) { - uint8_t code, fourBytes[4]; - size_t size = 0; - - switch (type) - { - case NOTE_ON: - // A note starts playing - code = (0x9 << 4) | channel; - size = writeVarLength(time, buffer); - buffer[size++] = code; - buffer[size++] = note.pitch; - buffer[size++] = note.volume; - break; - - case NOTE_OFF: - // A note finishes playing - code = (0x8 << 4) | channel; - size = writeVarLength(time, buffer); - buffer[size++] = code; - buffer[size++] = note.pitch; - buffer[size++] = note.volume; - break; - - case TEMPO: - // A tempo measure - code = 0xFF; - size = writeVarLength(time, buffer); - buffer[size++] = code; - buffer[size++] = 0x51; - buffer[size++] = 0x03; - - // Convert to microseconds before writting - writeBigEndian4(6e7 / tempo, fourBytes); - buffer[size++] = fourBytes[1]; - buffer[size++] = fourBytes[2]; - buffer[size++] = fourBytes[3]; - break; - - case PROG_CHANGE: - // Change patch number - code = (0xC << 4) | channel; - size = writeVarLength(time, buffer); - buffer[size++] = code; - buffer[size++] = programNumber; - break; - - case TRACK_NAME: - // Name of current track - size = writeVarLength(time, buffer); - buffer[size++] = 0xFF; - buffer[size++] = 0x03; - - // Write name string size and then copy it's content - // to the following size bytes of buffer - size += writeVarLength(trackName.size(), buffer + size); - trackName.copy((char *) &buffer[size], trackName.size()); - size += trackName.size(); - break; - } - return size; + case NOTE_ON: + // A note starts playing + code = (0x9 << 4) | channel; + parent.writeBytes({code, note.pitch, note.volume}); + break; + + case NOTE_OFF: + // A note finishes playing + code = (0x8 << 4) | channel; + parent.writeBytes({code, note.pitch, note.volume}); + break; + + case TEMPO: + // A tempo measure + code = 0xFF; + parent.writeBytes({code, 0x51, 0x03}); + + // Convert to microseconds before writting + parent.writeBigEndian4(6e7 / tempo, &fourBytes); + parent.writeBytes({fourBytes[1], fourBytes[2], fourBytes[3]}); + break; + + case PROG_CHANGE: + // Change patch number + code = (0xC << 4) | channel; + parent.writeBytes({code, programNumber}); + break; + + case TRACK_NAME: + // Name of current track + parent.writeBytes({0xFF, 0x03}); + + // Write name string size and then copy it's content + // to the following size bytes of buffer + vector bytes(trackName.begin(), trackName.end()); + parent.writeVarLength(trackName.size()); + parent.writeBytes(bytes); + break; } +} - //! \brief Comparison operator - // Events are sorted by their time - inline bool operator<(const Event& b) const - { - if (time < b.time) - { - return true; - } - // In case of same time events, sort by event priority - return (time == b.time and type > b.type); - } -}; +inline bool MidiFile::Track::Event::operator<(const Event& b) const +{ + if (time < b.time) { return true; } + return (time == b.time and type > b.type); +} /*---------------------------------------------------------------------------*/ -//! Class that encapsulates a MIDI track. -// Maximum available track size defined in template -template -class MIDITrack +inline void MidiFile::Track::addEvent(Event event, uint32_t time) { -private: - //! Variable-length vector of events - vector events; + event.time = time; + event.channel = channel; + events.push_back(event); +} - //! Append a single event to vector - inline void addEvent(const Event &e, uint32_t time) - { - Event event = e; - event.time = time; - event.channel = channel; - events.push_back(event); - } +MidiFile::Track::Track(uint8_t channel): + channel(channel) {} -public: - //! Track channel number - uint8_t channel; +inline void MidiFile::Track::addNote(uint8_t pitch, uint8_t volume, + double realTime, double duration) +{ + Event event; + event.note.volume = volume; + + // Add start of note + event.type = Event::NOTE_ON; + event.note.pitch = pitch; + uint32_t time = realTime * TICKS_PER_BEAT; + addEvent(event, time); + + // Add end of note + event.type = Event::NOTE_OFF; + event.note.pitch = pitch; + time = (realTime + duration) * TICKS_PER_BEAT; + addEvent(event, time); +} - //! Constructor - MIDITrack(uint8_t channel): - channel(channel) {} +inline void MidiFile::Track::addTempo(uint8_t tempo, uint32_t time) +{ + Event event; + event.type = Event::TEMPO; + event.tempo = tempo; + addEvent(event, time); +} - /*-----------------------------------------------------------------------*/ +inline void MidiFile::Track::addProgramChange(uint8_t prog, uint32_t time) +{ + Event event; + event.type = Event::PROG_CHANGE; + event.programNumber = prog; + addEvent(event, time); +} - //! Add both NOTE_ON and NOTE_OFF effects, starting at - // continuous time and separated by a delay - inline void addNote(uint8_t pitch, uint8_t volume, - double realTime, double duration) - { - Event event; - event.note.volume = volume; - - // Add start of note - event.type = Event::NOTE_ON; - event.note.pitch = pitch; - uint32_t time = realTime * TICKS_PER_BEAT; - addEvent(event, time); - - // Add end of note - event.type = Event::NOTE_OFF; - event.note.pitch = pitch; - time = (realTime + duration) * TICKS_PER_BEAT; - addEvent(event, time); - } +inline void MidiFile::Track::addName(const string &name, uint32_t time) +{ + Event event; + event.type = Event::TRACK_NAME; + event.trackName = name; + addEvent(event, time); +} - //! Add a tempo mark - inline void addTempo(uint8_t tempo, uint32_t time) - { - Event event; - event.type = Event::TEMPO; - event.tempo = tempo; - addEvent(event, time); - } +inline void MidiFile::Track::writeToBuffer() +{ + // Chunk ID + writeBytes({'M', 'T', 'r', 'k'}); - //! Add a program change event - inline void addProgramChange(uint8_t prog, uint32_t time) - { - Event event; - event.type = Event::PROG_CHANGE; - event.programNumber = prog; - addEvent(event, time); - } + // Chunk size placeholder + size_t idx = m_buffer.size(); + writeBigEndian4(9); - //! Add a track name event - inline void addName(const string &name, uint32_t time) + // Write all events to buffer + writeMIDIToBuffer(); + + // Write correct size + size_t size = m_buffer.size() - idx; + vector v; + writeBigEndian4(size, &v); + for (size_t i = idx; i < idx + 4; idx++) { - Event event; - event.type = Event::TRACK_NAME; - event.trackName = name; - addEvent(event, time); + m_buffer[i] = v[i]; } +} - /*-----------------------------------------------------------------------*/ - - //! Write the meta data and note data to the packed MIDI stream - inline size_t writeMIDIToBuffer(uint8_t *buffer, size_t start=0) const - { - // Process events in the eventList - start += writeEventsToBuffer(buffer, start); +inline void MidiFile::Track::writeMIDIToBuffer() +{ + // Process events in the eventList + writeEventsToBuffer(); - // Write MIDI close event - buffer[start++] = 0x00; - buffer[start++] = 0xFF; - buffer[start++] = 0x2F; - buffer[start++] = 0x00; + // Write MIDI close event + writeBytes({0x00, 0xFF, 0x2F, 0x00}); +} - // Return the entire length of the data - return start; - } +inline void MidiFile::Track::writeEventsToBuffer() +{ + // Create sorted vector of events + vector eventsSorted = events; + sort(eventsSorted.begin(), eventsSorted.end()); - //! Write the events in MIDIEvents to the MIDI stream - inline size_t writeEventsToBuffer(uint8_t *buffer, size_t start=0) const + uint32_t timeLast = 0; + for (Event &e : eventsSorted) { - // Created sorted vector of events - vector eventsSorted = events; - sort(eventsSorted.begin(), eventsSorted.end()); - - uint32_t timeLast = 0; - for (Event &e : eventsSorted) + // If something went wrong on sorting, maybe? + if (e.time < timeLast) { - // If something went wrong on sorting, maybe? - if (e.time < timeLast) - { - fprintf(stderr, "error: e.time=%d timeLast=%d\n", - e.time, timeLast); - assert(false); - } - uint32_t tmp = e.time; - e.time -= timeLast; - timeLast = tmp; - - // Write event to buffer - start += e.writeToBuffer(buffer + start); - - // In case of exceding maximum size, go away - if (start >= MAX_TRACK_SIZE) { - break; - } + fprintf(stderr, "error: e.time=%d timeLast=%d\n", + e.time, timeLast); + assert(false); } - return start; - } + uint32_t tmp = e.time; + e.time -= timeLast; + timeLast = tmp; - //! Write MIDI track to buffer - inline size_t writeToBuffer(uint8_t *buffer, size_t start=0) const - { - // Write all events to buffer - uint8_t eventsBuffer[MAX_TRACK_SIZE]; - uint32_t eventsSize = writeMIDIToBuffer(eventsBuffer); - - // Chunk ID - buffer[start++] = 'M'; - buffer[start++] = 'T'; - buffer[start++] = 'r'; - buffer[start++] = 'k'; - - // Chunk size - start += writeBigEndian4(eventsSize, buffer + start); - - // Copy events data - memmove(buffer + start, eventsBuffer, eventsSize); - start += eventsSize; - return start; - } -}; + // Write event to buffer + e.writeToBuffer(*this); + // In case of exceding maximum size, go away + if (m_buffer.size() >= BUFFER_SIZE) { break; } + } +} /*---------------------------------------------------------------------------*/ -}; // namespace - #endif From 100c7a502ecd9801690f63d6a5672bdcdd59749a Mon Sep 17 00:00:00 2001 From: Gustavo Chicato Date: Sat, 23 May 2020 07:50:57 -0300 Subject: [PATCH 07/10] MidiExport: more refactoring, documentation... and seems to be OK --- plugins/MidiExport/CMakeLists.txt | 2 +- plugins/MidiExport/MidiExport.cpp | 267 ++++++++-------- plugins/MidiExport/MidiExport.h | 99 +++--- plugins/MidiExport/MidiFile.cpp | 317 +++++++++++++++++++ plugins/MidiExport/MidiFile.h | 239 ++++++++++++++ plugins/MidiExport/MidiFile.hpp | 504 ------------------------------ 6 files changed, 741 insertions(+), 687 deletions(-) create mode 100644 plugins/MidiExport/MidiFile.cpp create mode 100644 plugins/MidiExport/MidiFile.h delete mode 100644 plugins/MidiExport/MidiFile.hpp 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 56510ca3486..a5802e9c8c0 100644 --- a/plugins/MidiExport/MidiExport.cpp +++ b/plugins/MidiExport/MidiExport.cpp @@ -40,7 +40,7 @@ extern "C" { //! Standardized plugin descriptor for MIDI exporter -static Plugin::Descriptor PLUGIN_EXPORT midiexport_plugin_descriptor = +Plugin::Descriptor PLUGIN_EXPORT midiexport_plugin_descriptor = { STRINGIFY(PLUGIN_NAME), "MIDI Export", @@ -61,65 +61,65 @@ static Plugin::Descriptor PLUGIN_EXPORT 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(); not node.isNull(); + node = node.nextSibling()) { - // TODO interpret steps="12" muted="0" type="1" name="Piano1" len="259" - for (QDomNode node = root.firstChild(); not 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 - MidiNote 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); - } + 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); } +} void MidiExport::Pattern::writeToTrack(MidiFile::Track &mTrack) const { - for (const MidiNote &n : m_notes) { - mTrack.addNote(n.m_pitch, n.m_volume, - n.m_time / 48.0, n.m_duration / 48.0); + for (const Note ¬e : m_notes) { + mTrack.addNote(note.m_pitch, note.m_volume, + note.m_time / 48.0, note.m_duration / 48.0); } } -void MidiExport::Pattern::processBBNotes(int cutPos) +void MidiExport::Pattern::processBbNotes(int cutPos) { // Sort in reverse order sort(m_notes.rbegin(), m_notes.rend()); int cur = INT_MAX, next = INT_MAX; - for (MidiNote &n : m_notes) + for (Note ¬e : m_notes) { - if (n.m_time < cur) + if (note.m_time < cur) { // Set last two notes positions next = cur; - cur = n.m_time; + cur = note.m_time; } - if (n.m_duration < 0) + if (note.m_duration < 0) { // Note should have positive duration that neither // overlaps next one nor exceeds cutPos - n.m_duration = qMin(-n.m_duration, next - cur); - n.m_duration = qMin(n.m_duration, cutPos - n.m_time); + note.m_duration = qMin(-note.m_duration, next - cur); + note.m_duration = qMin(note.m_duration, cutPos - note.m_time); } } } -void MidiExport::Pattern::writeToBB(Pattern &bbPat, +void MidiExport::Pattern::writeToBb(Pattern &bbPat, int len, int base, int start, int end) { // Avoid misplaced start and end positions @@ -130,7 +130,7 @@ void MidiExport::Pattern::writeToBB(Pattern &bbPat, end -= base; sort(m_notes.begin(), m_notes.end()); - for (MidiNote note : m_notes) + for (Note note : m_notes) { // Insert periodically repeating notes from and spaced // by to mimic BB pattern behavior @@ -145,7 +145,6 @@ void MidiExport::Pattern::writeToBB(Pattern &bbPat, /*---------------------------------------------------------------------------*/ -// Constructor and destructor MidiExport::MidiExport() : ExportFilter(&midiexport_plugin_descriptor) {} @@ -171,7 +170,7 @@ bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, // Write header info m_file->m_header.writeToBuffer(); - // Some useful class members + // Set tempo and pitch properties m_tempo = tempo; m_masterPitch = masterPitch; @@ -181,34 +180,33 @@ bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, { if (track->type() == Track::InstrumentTrack) { - foo(track, channel_id); + processTrack(track, channel_id); } else if (track->type() == Track::BBTrack) { - goo(track); + processBbTrack(track); } } // Iterate through BB tracks - for (Track* track : tracksBB) + for (Track *track : tracksBB) { - foo(track, channel_id, true); + processTrack(track, channel_id, true); } - // Write buffered data to stream + // Write all buffered data to stream m_file->writeAllToStream(); // Always returns success... for now? return true; } -void MidiExport::foo(Track *track, uint8_t &channelID, bool isBB) +void MidiExport::processTrack(Track *track, uint8_t &channelID, bool isBB) { // Cast track as a instrument one and save info from it to element InstrumentTrack *instTrack = dynamic_cast(track); - QDomElement element = - instTrack->saveState(m_dataFile, m_dataFile.content()); + QDomElement root = instTrack->saveState(m_dataFile, m_dataFile.content()); // Create track with incremental id - MidiFile::Track &midiTrack = m_file->m_tracks[channelID]; + MidiFile::Track &midiTrack = m_file->m_tracks[channelID++]; // Add info about tempo and track name midiTrack.addTempo(m_tempo, 0); @@ -227,118 +225,107 @@ void MidiExport::foo(Track *track, uint8_t &channelID, bool isBB) } midiTrack.addProgramChange(patch, 0); - // Get track info and then update pattern - int basePitch = 57; - double baseVolume = 1.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()) + { + basePitch += m_masterPitch; + } + // Volume ranges in [0.0, 2.0] + double baseVolume = LocaleHelper::toDouble( + trackElem.attribute("volume", "100")) / 100.0; + + // ---- Pattern ---- // + QDomNode patternNode = root.firstChildElement("pattern"); + QDomElement patElem = patternNode.toElement(); Pattern pat; - int i = 0; - for (QDomNode node = element.firstChild(); not node.isNull(); - node = node.nextSibling()) + if (not isBB) { - QDomElement e = node.toElement(); + // Base time == initial position + int baseTime = patElem.attribute("pos", "0").toInt(); + + // Write track notes to pattern + pat.write(patternNode, basePitch, baseVolume, baseTime); - if (node.nodeName() == "instrumenttrack") + // Write pattern info to MIDI file track + pat.processBbNotes(INT_MAX); + pat.writeToTrack(midiTrack); + } + else + { + // Write to-be repeated BB notes to pattern + // (notice base time of 0) + pat.write(patternNode, basePitch, baseVolume, 0); + } + + // Write track data to buffer + midiTrack.writeToBuffer(); +} + +void MidiExport::writeBbPattern(Pattern &pat, const QDomElement &patElem, + uint8_t channelID, MidiFile::Track &midiTrack) +{ + // 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[channelID - 1]; + stack> st; + Pattern bbPat; + for (const pair &p : plist) + { + while (not st.empty() and st.top().second <= p.first) { - // Transpose +12 semitones (workaround for #1857). - // Adjust to masterPitch if enabled - basePitch = e.attribute("basenote", "57").toInt(); - basePitch = 69 - basePitch; - if (e.attribute("usemasterpitch", "1").toInt()) - { - basePitch += m_masterPitch; - } - // Volume ranges in [0.0, 2.0] - baseVolume = LocaleHelper::toDouble( - e.attribute("volume", "100")) / 100.0; + pat.writeToBb(bbPat, len, st.top().first, pos, st.top().second); + pos = st.top().second; + st.pop(); } - else if (node.nodeName() == "pattern") + if (not st.empty() and st.top().second <= p.second) { - if (not isBB) - { - // Base time == initial position - int baseTime = e.attribute("pos", "0").toInt(); - - // Write track notes to pattern - pat.write(node, basePitch, baseVolume, baseTime); - - // Write pattern info to MIDI file track - pat.processBBNotes(INT_MAX); - pat.writeToTrack(midiTrack); - } - else + pat.writeToBb(bbPat, len, st.top().first, pos, p.first); + pos = p.first; + while (not st.empty() and st.top().second <= p.second) { - // Write to-be repeated BB notes to pattern - // (notice base time of 0) - Pattern pat, bbPat; - pat.write(node, basePitch, baseVolume, 0); - - // Workaround for nested BBTCOs - int pos = 0; - int len = 12 * e.attribute("steps", "1").toInt(); - - // Iterate through BBTCO pairs of current list - // TODO: This *may* need some corrections? - const vector> &plist = m_plists[i++]; - stack> st; - for (const pair &p : plist) - { - while (not st.empty() and st.top().second <= p.first) - { - pat.writeToBB(bbPat, - len, st.top().first, pos, st.top().second); - pos = st.top().second; - st.pop(); - } - if (not st.empty() and st.top().second <= p.second) - { - pat.writeToBB(bbPat, - len, st.top().first, pos, p.first); - pos = p.first; - while (not st.empty() and st.top().second <= p.second) - { - st.pop(); - } - } - st.push(p); - pos = p.first; - } - while (not 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); - - // Write track data to buffer - midiTrack.writeToBuffer(); + st.pop(); } } + st.push(p); + pos = p.first; + } + while (not 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::goo(Track *track) +void MidiExport::processBbTrack(Track *track) { // Cast track as a BB one and save info from it to element BBTrack *bbTrack = dynamic_cast(track); - QDomElement element = - bbTrack->saveState(m_dataFile, m_dataFile.content()); + QDomElement root = bbTrack->saveState(m_dataFile, m_dataFile.content()); // Build lists of (start, end) pairs from BB note objects vector> plist; - for (QDomNode node = element.firstChild(); not node.isNull(); - node = node.nextSibling()) + for (QDomNode bbtcoNode = root.firstChildElement("bbtco"); + not bbtcoNode.isNull(); + bbtcoNode = bbtcoNode.nextSiblingElement("bbtco")) { - if (node.nodeName() == "bbtco") - { - QDomElement e = node.toElement(); - int start = e.attribute("pos", "0").toInt(); - int end = start + e.attribute("len", "0").toInt(); - plist.push_back(pair(start, end)); - } + 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()); diff --git a/plugins/MidiExport/MidiExport.h b/plugins/MidiExport/MidiExport.h index da07950bf6b..a0512fdc282 100644 --- a/plugins/MidiExport/MidiExport.h +++ b/plugins/MidiExport/MidiExport.h @@ -28,7 +28,7 @@ #include -#include "MidiFile.hpp" +#include "MidiFile.h" #include "DataFile.h" #include "ExportFilter.h" @@ -37,61 +37,66 @@ using std::vector; /*---------------------------------------------------------------------------*/ -//! A single note -struct MidiNote +//! MIDI exporting base class +class MidiExport : public ExportFilter { - //! The pitch (tone), which can be lower or higher - uint8_t m_pitch; +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; + //! Volume (loudness) + uint8_t m_volume; - //! Absolute time (from song start) when the note starts playing - int m_time; + //! Absolute time (from song start) when the note starts playing + int m_time; - //! For how long the note plays - int m_duration; + //! For how long the note plays + int m_duration; - //! Sort notes by time - inline bool operator<(const MidiNote &b) const - { - return m_time < b.m_time; - } -}; + //! Sort notes by time + inline bool operator<(const Note &b) const + { + return m_time < b.m_time; + } + }; -/*---------------------------------------------------------------------------*/ + /*-----------------------------------------------------------------------*/ -//! MIDI exporting base class -class MidiExport : public ExportFilter -{ -private: - //! Represents a pattern of notes - struct Pattern + //! A pattern of MIDI notes + class Pattern { - //! Vector of actual notes - vector m_notes; + private: + //! List of actual notes + vector m_notes; + 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; - //! Adjust special duration BB notes by resizing them accordingly - void processBBNotes(int cutPos); - //! Write sorted notes to a explicitly repeating BB pattern - void writeToBB(Pattern &bbPat, + void writeToBb(Pattern &bbPat, int len, int base, int start, int end); }; + /*-----------------------------------------------------------------------*/ + //! DataFile to be used by Qt elements DataFile m_dataFile = DataFile(DataFile::SongProject); + //! MIDI file object to work with MidiFile *m_file; - //! Song tempo + //! Song global tempo int m_tempo; //! Song master pitch @@ -100,24 +105,34 @@ class MidiExport : public ExportFilter //! Matrix containing (start, end) pairs for BB objects vector>> m_plists; - void foo(Track *track, uint8_t &channelID, bool isBB=false); - void goo(Track *track); + //! Necessary for lmms_plugin_main() + PluginView *instantiateView(QWidget *) { return nullptr; } public: + //! Explicit constructor for setting plugin descriptor MidiExport(); - ~MidiExport(); - virtual PluginView *instantiateView(QWidget *) - { - return nullptr; - } - - //! Export normal and BB tracks from a project with designated - // tempo and master pitch to a file indicated by \param filename - // \return If operation was successful + //! \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); + +private: + //! Process a given instrument track + void processTrack(Track *track, uint8_t &channelID, bool isBB=false); + + //! 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); } ; /*---------------------------------------------------------------------------*/ diff --git a/plugins/MidiExport/MidiFile.cpp b/plugins/MidiExport/MidiFile.cpp new file mode 100644 index 00000000000..a2c7d42b75b --- /dev/null +++ b/plugins/MidiExport/MidiFile.cpp @@ -0,0 +1,317 @@ +/** + * 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, uint16_t 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)); + + // Reserve space for track list + m_tracks.reserve(nTracks); + for (size_t i = 0; i < nTracks; i++) + { + m_tracks.push_back(Track(i)); + } +} + +void MidiFile::writeAllToStream() +{ + // reinterpret_cast should be used to convert raw 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 (not 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 (not 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(uint16_t numTracks, uint16_t 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); +} + +/*---------------------------------------------------------------------------*/ + +MidiFile::Track::Track(uint8_t channel): + m_channel(channel) {} + +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 + 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..3d372648603 --- /dev/null +++ b/plugins/MidiExport/MidiFile.h @@ -0,0 +1,239 @@ +#ifndef _MIDI_FILE_H +#define _MIDI_FILE_H + +/** + * 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 +#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 + uint16_t m_numTracks; + + //! How many ticks each beat has + uint16_t m_ticksPerBeat; + + public: + //! Constructor + Header(uint16_t numTracks, uint16_t 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 and m_type > b.m_type); + } + }; + + /*-----------------------------------------------------------------------*/ + +public: + //! Represents a MIDI track + class Track : public Section + { + private: + //! Variable-length vector of events + vector m_events; + + //! Append a single event to vector + void addEvent(Event event, uint32_t time); + + //! Track channel number + uint8_t m_channel; + + public: + //! Constructor + Track(uint8_t channel); + + //! \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 reserve track space + //! \param filename Name of file to be opened + //! \param nTracks Total number of MIDI tracks + MidiFile(const QString &filename, uint16_t 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 e3aeea1fc27..00000000000 --- a/plugins/MidiExport/MidiFile.hpp +++ /dev/null @@ -1,504 +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 - -#include -#include -#include - -using std::string; -using std::array; -using std::stack; -using std::vector; -using std::initializer_list; -using std::sort; - -/*---------------------------------------------------------------------------*/ - -//! MIDI file class used in exporting -class MidiFile -{ -public: - //! Default number of ticks per single beat - static constexpr u_int16_t 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: - //! Constant-capacity vector to serve as buffer before writting - // to stream (should be better than std::array as it provides - // size() and push_back() funcionalities) - vector m_buffer; - - //! Reserve constant space for BUFFER_SIZE capacity vector - Section(); - - protected: - //! Write bytes from initializer list to vector (or buffer by default) - 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(); - }; - - /*-----------------------------------------------------------------------*/ - - //! Represents MIDI header info - class Header : public Section - { - private: - //! Number of tracks in MIDI file - uint16_t m_numTracks; - - //! How many ticks each beat has - uint16_t m_ticksPerBeat; - - public: - //! Constructor - Header(uint16_t numTracks, uint16_t ticksPerBeat=TICKS_PER_BEAT); - - //! Write header info to buffer - void writeToBuffer(); - }; - - /*-----------------------------------------------------------------------*/ - -public: - //! Represents a MIDI track - class Track : public Section - { - private: - struct Event - { - //! Possible event types - enum { NOTE_ON, NOTE_OFF, TEMPO, PROG_CHANGE, TRACK_NAME } type; - - //! Time count when event happens - uint32_t time; - - //! Channel number where event is - uint8_t channel; - - // Union for saving space - union - { - struct - { - //! Note pitch - uint8_t pitch; - - //! Note volume - uint8_t volume; - } - note; - - //! Tempo of event (in BPM) - uint32_t tempo; - - //! Program (patch) number of instrument - uint8_t programNumber; - }; - - //! Name of track where event is - // (too much trouble putting it inside union...) - string trackName; - - //! Write MIDI event info to track buffer - inline void writeToBuffer(Track &parent) const; - - //! \brief Comparison operator - // Events are sorted by their time - inline bool operator<(const Event& b) const; - }; - - /*-------------------------------------------------------------------*/ - - //! Variable-length vector of events - vector events; - - //! Append a single event to vector - inline void addEvent(Event event, uint32_t time); - - public: - //! Track channel number - uint8_t channel; - - //! Constructor - Track(uint8_t channel); - - //! Add both NOTE_ON and NOTE_OFF effects - // \param realTime Time of note start - // \param duration How long the note lasts - inline void addNote(uint8_t pitch, uint8_t volume, - double realTime, double duration); - - //! Add a tempo mark - inline void addTempo(uint8_t tempo, uint32_t time); - - //! Add a program (patch) change event - inline void addProgramChange(uint8_t prog, uint32_t time); - - //! Add a track name event - inline void addName(const string &name, uint32_t time); - - //! Write MIDI track to buffer - inline void writeToBuffer(); - - //! Write the meta data and note data to the packed MIDI stream - inline void writeMIDIToBuffer(); - - //! Write the events in MIDIEvents to the MIDI stream - inline void writeEventsToBuffer(); - }; - - /*-----------------------------------------------------------------------*/ - -private: - //! Qt file to be opened - QFile m_file; - - //! Qt data stream for writing - QDataStream *m_stream; - -public: - //! The sole file header - Header m_header; - - //! List of tracks - vector m_tracks; - - MidiFile(const QString &filename, uint16_t nTracks); - - ~MidiFile(); - - void writeAllToStream(); -}; - -/*---------------------------------------------------------------------------*/ - -MidiFile::MidiFile(const QString &filename, uint16_t nTracks): - m_file(filename), m_header(nTracks) -{ - // Open designated blank MIDI file (and data stream) for writing - m_file.open(QIODevice::WriteOnly); - m_stream = new QDataStream(&m_file); - - // Reserve space for track list - m_tracks.reserve(nTracks); - for (size_t i = 0; i < nTracks; i++) - { - m_tracks.push_back(Track(i)); - } -} - -MidiFile::~MidiFile() -{ - delete m_stream; -} - -void MidiFile::writeAllToStream() -{ - 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() -{ - m_buffer.reserve(BUFFER_SIZE); -} - -void MidiFile::Section::writeBytes(vector bytes, vector *v) -{ - if (not 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 (not 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(uint16_t numTracks, uint16_t 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}); - - // Extra info - writeBigEndian2(m_numTracks); - writeBigEndian2(m_ticksPerBeat); -} - -/*---------------------------------------------------------------------------*/ - -inline void MidiFile::Track::Event::writeToBuffer(Track &parent) const -{ - // First of all, write event time - parent.writeVarLength(time); - - uint8_t code; - vector fourBytes; - switch (type) - { - case NOTE_ON: - // A note starts playing - code = (0x9 << 4) | channel; - parent.writeBytes({code, note.pitch, note.volume}); - break; - - case NOTE_OFF: - // A note finishes playing - code = (0x8 << 4) | channel; - parent.writeBytes({code, note.pitch, note.volume}); - break; - - case TEMPO: - // A tempo measure - code = 0xFF; - parent.writeBytes({code, 0x51, 0x03}); - - // Convert to microseconds before writting - parent.writeBigEndian4(6e7 / tempo, &fourBytes); - parent.writeBytes({fourBytes[1], fourBytes[2], fourBytes[3]}); - break; - - case PROG_CHANGE: - // Change patch number - code = (0xC << 4) | channel; - parent.writeBytes({code, programNumber}); - break; - - case TRACK_NAME: - // Name of current track - parent.writeBytes({0xFF, 0x03}); - - // Write name string size and then copy it's content - // to the following size bytes of buffer - vector bytes(trackName.begin(), trackName.end()); - parent.writeVarLength(trackName.size()); - parent.writeBytes(bytes); - break; - } -} - -inline bool MidiFile::Track::Event::operator<(const Event& b) const -{ - if (time < b.time) { return true; } - return (time == b.time and type > b.type); -} - -/*---------------------------------------------------------------------------*/ - -inline void MidiFile::Track::addEvent(Event event, uint32_t time) -{ - event.time = time; - event.channel = channel; - events.push_back(event); -} - -MidiFile::Track::Track(uint8_t channel): - channel(channel) {} - -inline void MidiFile::Track::addNote(uint8_t pitch, uint8_t volume, - double realTime, double duration) -{ - Event event; - event.note.volume = volume; - - // Add start of note - event.type = Event::NOTE_ON; - event.note.pitch = pitch; - uint32_t time = realTime * TICKS_PER_BEAT; - addEvent(event, time); - - // Add end of note - event.type = Event::NOTE_OFF; - event.note.pitch = pitch; - time = (realTime + duration) * TICKS_PER_BEAT; - addEvent(event, time); -} - -inline void MidiFile::Track::addTempo(uint8_t tempo, uint32_t time) -{ - Event event; - event.type = Event::TEMPO; - event.tempo = tempo; - addEvent(event, time); -} - -inline void MidiFile::Track::addProgramChange(uint8_t prog, uint32_t time) -{ - Event event; - event.type = Event::PROG_CHANGE; - event.programNumber = prog; - addEvent(event, time); -} - -inline void MidiFile::Track::addName(const string &name, uint32_t time) -{ - Event event; - event.type = Event::TRACK_NAME; - event.trackName = name; - addEvent(event, time); -} - -inline void MidiFile::Track::writeToBuffer() -{ - // Chunk ID - writeBytes({'M', 'T', 'r', 'k'}); - - // Chunk size placeholder - size_t idx = m_buffer.size(); - writeBigEndian4(9); - - // Write all events to buffer - writeMIDIToBuffer(); - - // Write correct size - size_t size = m_buffer.size() - idx; - vector v; - writeBigEndian4(size, &v); - for (size_t i = idx; i < idx + 4; idx++) - { - m_buffer[i] = v[i]; - } -} - -inline void MidiFile::Track::writeMIDIToBuffer() -{ - // Process events in the eventList - writeEventsToBuffer(); - - // Write MIDI close event - writeBytes({0x00, 0xFF, 0x2F, 0x00}); -} - -inline void MidiFile::Track::writeEventsToBuffer() -{ - // Create sorted vector of events - vector eventsSorted = events; - sort(eventsSorted.begin(), eventsSorted.end()); - - uint32_t timeLast = 0; - for (Event &e : eventsSorted) - { - // If something went wrong on sorting, maybe? - if (e.time < timeLast) - { - fprintf(stderr, "error: e.time=%d timeLast=%d\n", - e.time, timeLast); - assert(false); - } - uint32_t tmp = e.time; - e.time -= timeLast; - timeLast = tmp; - - // Write event to buffer - e.writeToBuffer(*this); - - // In case of exceding maximum size, go away - if (m_buffer.size() >= BUFFER_SIZE) { break; } - } -} -/*---------------------------------------------------------------------------*/ - -#endif From dc7b67d82d7c669dbbc421f333299a308af6517a Mon Sep 17 00:00:00 2001 From: Gustavo Chicato Date: Sat, 23 May 2020 13:33:31 -0300 Subject: [PATCH 08/10] Added BB export support to channel 10 (plus some bug corrections) --- plugins/MidiExport/MidiExport.cpp | 93 ++++++++++++++++--------------- plugins/MidiExport/MidiExport.h | 8 +-- plugins/MidiExport/MidiFile.cpp | 23 +++++--- plugins/MidiExport/MidiFile.h | 26 +++++---- 4 files changed, 81 insertions(+), 69 deletions(-) diff --git a/plugins/MidiExport/MidiExport.cpp b/plugins/MidiExport/MidiExport.cpp index a5802e9c8c0..af6429a0dde 100644 --- a/plugins/MidiExport/MidiExport.cpp +++ b/plugins/MidiExport/MidiExport.cpp @@ -151,46 +151,43 @@ MidiExport::MidiExport() : /*---------------------------------------------------------------------------*/ bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, - const TrackContainer::TrackList &tracksBB, + const TrackContainer::TrackList &tracksBb, int tempo, int masterPitch, const QString &filename) { - // Count total number of tracks - uint16_t nTracks = 0; - for (auto trackList : {tracks, tracksBB}) + // Count number of instrument (and instrument BB) tracks + int nInstTracks = 0; + for (const Track *track : tracks) { - for (const Track *track : trackList) - { - if (track->type() == Track::InstrumentTrack) { nTracks++; } - } + if (track->type() == Track::InstrumentTrack) { nInstTracks++; } } + int nInstBbTracks = tracksBb.size(); + // Create MIDI file object - MidiFile file(filename, nTracks); + MidiFile file(filename, nInstTracks, nInstBbTracks); m_file = &file; + m_tempo = tempo; + m_masterPitch = masterPitch; // Write header info m_file->m_header.writeToBuffer(); - // Set tempo and pitch properties - m_tempo = tempo; - m_masterPitch = masterPitch; - // Iterate through "normal" tracks - uint8_t channel_id = 0; + size_t trackIdx = 0; for (Track *track : tracks) { if (track->type() == Track::InstrumentTrack) { - processTrack(track, channel_id); + processTrack(track, trackIdx++); } else if (track->type() == Track::BBTrack) { processBbTrack(track); } } - // Iterate through BB tracks - for (Track *track : tracksBB) + // Iterate through instrument BB tracks + for (Track *track : tracksBb) { - processTrack(track, channel_id, true); + processTrack(track, trackIdx++, true); } // Write all buffered data to stream m_file->writeAllToStream(); @@ -199,26 +196,27 @@ bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, return true; } -void MidiExport::processTrack(Track *track, uint8_t &channelID, bool isBB) +void MidiExport::processTrack(Track *track, size_t trackIdx, bool isBb) { // 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()); - // Create track with incremental id - MidiFile::Track &midiTrack = m_file->m_tracks[channelID++]; + // Get next MIDI file track object of the list + MidiFile::Track &midiTrack = m_file->m_tracks[trackIdx]; // 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 for to the exporting track. Note that this only works + // 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") + if (instName == "Sf2 Player" and not isBb) { class Instrument *inst = instTrack->instrument(); patch = inst->childModel("patch")->value(); @@ -240,35 +238,42 @@ void MidiExport::processTrack(Track *track, uint8_t &channelID, bool isBB) double baseVolume = LocaleHelper::toDouble( trackElem.attribute("volume", "100")) / 100.0; - // ---- Pattern ---- // - QDomNode patternNode = root.firstChildElement("pattern"); - QDomElement patElem = patternNode.toElement(); - Pattern pat; - if (not isBB) + // ---- Patterns ---- // + uint8_t bbId = 0; + for (QDomNode patNode = root.firstChildElement("pattern"); + not patNode.isNull(); + patNode = patNode.nextSiblingElement("pattern")) { - // Base time == initial position - int baseTime = patElem.attribute("pos", "0").toInt(); + QDomElement patElem = patNode.toElement(); + Pattern pat; + if (not isBb) + { + // Base time == initial position + int baseTime = patElem.attribute("pos", "0").toInt(); - // Write track notes to pattern - pat.write(patternNode, basePitch, baseVolume, baseTime); + // Write track notes to pattern + pat.write(patNode, basePitch, baseVolume, baseTime); - // Write pattern info to MIDI file track - pat.processBbNotes(INT_MAX); - pat.writeToTrack(midiTrack); - } - else - { - // Write to-be repeated BB notes to pattern - // (notice base time of 0) - pat.write(patternNode, basePitch, baseVolume, 0); - } + // Write pattern info to MIDI file track + pat.processBbNotes(INT_MAX); + pat.writeToTrack(midiTrack); + } + else + { + // 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::writeBbPattern(Pattern &pat, const QDomElement &patElem, - uint8_t channelID, MidiFile::Track &midiTrack) + uint8_t bbId, MidiFile::Track &midiTrack) { // Workaround for nested BBTCOs int pos = 0; @@ -276,7 +281,7 @@ void MidiExport::writeBbPattern(Pattern &pat, const QDomElement &patElem, // Iterate through BBTCO pairs of current list // TODO: This *may* need some corrections? - const vector> &plist = m_plists[channelID - 1]; + const vector> &plist = m_plists[bbId]; stack> st; Pattern bbPat; for (const pair &p : plist) diff --git a/plugins/MidiExport/MidiExport.h b/plugins/MidiExport/MidiExport.h index a0512fdc282..9371fb2a4a8 100644 --- a/plugins/MidiExport/MidiExport.h +++ b/plugins/MidiExport/MidiExport.h @@ -90,9 +90,6 @@ class MidiExport : public ExportFilter /*-----------------------------------------------------------------------*/ - //! DataFile to be used by Qt elements - DataFile m_dataFile = DataFile(DataFile::SongProject); - //! MIDI file object to work with MidiFile *m_file; @@ -102,6 +99,9 @@ class MidiExport : public ExportFilter //! Song master pitch int m_masterPitch; + //! DataFile to be used by Qt elements + DataFile m_dataFile = DataFile(DataFile::SongProject); + //! Matrix containing (start, end) pairs for BB objects vector>> m_plists; @@ -125,7 +125,7 @@ class MidiExport : public ExportFilter private: //! Process a given instrument track - void processTrack(Track *track, uint8_t &channelID, bool isBB=false); + void processTrack(Track *track, size_t channelID, bool isBB=false); //! Build a repeating pattern from a normal one and write to MIDI track void writeBbPattern(Pattern &pat, const QDomElement &patElem, diff --git a/plugins/MidiExport/MidiFile.cpp b/plugins/MidiExport/MidiFile.cpp index a2c7d42b75b..ca0cb713743 100644 --- a/plugins/MidiExport/MidiFile.cpp +++ b/plugins/MidiExport/MidiFile.cpp @@ -35,24 +35,29 @@ using std::sort; /*---------------------------------------------------------------------------*/ -MidiFile::MidiFile(const QString &filename, uint16_t nTracks): - m_file(filename), m_header(nTracks) +MidiFile::MidiFile(const QString &filename, + int nInstTracks, int nInstBbTracks): + m_file(filename), + m_header(nInstTracks + nInstBbTracks) { // Open designated blank MIDI file (and data stream) for writing m_file.open(QIODevice::WriteOnly); m_stream = QSharedPointer(new QDataStream(&m_file)); - // Reserve space for track list - m_tracks.reserve(nTracks); - for (size_t i = 0; i < nTracks; i++) + // Add tracks with ascending channel numbers (skipping 10) + size_t id = 0; + for (size_t i = 0; i < nInstTracks; ++i) { - m_tracks.push_back(Track(i)); + if (++id == 9) ++id; + m_tracks.push_back(Track(id)); } + // Add channel 10 tracks for all instrument BB ones + m_tracks.insert(m_tracks.end(), nInstBbTracks, Track(9)); } void MidiFile::writeAllToStream() { - // reinterpret_cast should be used to convert raw data to (char *) + // 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()); @@ -123,7 +128,7 @@ void MidiFile::Section::writeBigEndian2(uint16_t val, /*---------------------------------------------------------------------------*/ -MidiFile::Header::Header(uint16_t numTracks, uint16_t ticksPerBeat): +MidiFile::Header::Header(int numTracks, int ticksPerBeat): m_numTracks(numTracks), m_ticksPerBeat(ticksPerBeat) {} @@ -216,7 +221,7 @@ void MidiFile::Track::writeToBuffer() writeBigEndian4(size, &v); for (size_t i = 0; i < 4; ++i) { - m_buffer[idx + i] = v[i]; + m_buffer[idx - 4 + i] = v[i]; } } diff --git a/plugins/MidiExport/MidiFile.h b/plugins/MidiExport/MidiFile.h index 3d372648603..89f4b6498b9 100644 --- a/plugins/MidiExport/MidiFile.h +++ b/plugins/MidiExport/MidiFile.h @@ -2,7 +2,7 @@ #define _MIDI_FILE_H /** - * Name: MidiFile.cpp + * Name: MidiFile.h * Purpose: C++ re-write of the python module MidiFile.py * Author: Mohamed Abdel Maksoud *----------------------------------------------------------------------------- @@ -60,7 +60,7 @@ class MidiFile //! \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 ) + //! \param v Pointer to vector (if none, use \ref m_buffer) void writeBytes(vector bytes, vector *v=nullptr); @@ -80,7 +80,7 @@ class MidiFile void writeBigEndian2(uint16_t val, vector *v=nullptr); //! Write section info to buffer - virtual void writeToBuffer(); + virtual void writeToBuffer() {} }; /*-----------------------------------------------------------------------*/ @@ -91,14 +91,14 @@ class MidiFile { private: //! Number of tracks in MIDI file - uint16_t m_numTracks; + const int m_numTracks; //! How many ticks each beat has - uint16_t m_ticksPerBeat; + const int m_ticksPerBeat; public: //! Constructor - Header(uint16_t numTracks, uint16_t ticksPerBeat=TICKS_PER_BEAT); + Header(int numTracks, int ticksPerBeat=TICKS_PER_BEAT); //! Write header info to buffer void writeToBuffer(); @@ -167,12 +167,12 @@ class MidiFile //! Variable-length vector of events vector m_events; - //! Append a single event to vector - void addEvent(Event event, uint32_t time); - //! Track channel number uint8_t m_channel; + //! Append a single event to vector + void addEvent(Event event, uint32_t time); + public: //! Constructor Track(uint8_t channel); @@ -225,10 +225,12 @@ class MidiFile //! List of tracks vector m_tracks; - //! \brief Open data stream for writing to file and reserve track space + //! \brief Open data stream for writing to file and create list of tracks //! \param filename Name of file to be opened - //! \param nTracks Total number of MIDI tracks - MidiFile(const QString &filename, uint16_t nTracks); + //! \param nInstTracks Number of instrument tracks + //! \param nInstBbTracks Number of instrument BB tracks + MidiFile(const QString &filename, + int nInstTracks, int nInstBbTracks); //! Write all data (both header and tracks) to stream void writeAllToStream(); From cb9cc8628f7a4065c8a094d3891e884e419d617a Mon Sep 17 00:00:00 2001 From: Gustavo Chicato Date: Sun, 24 May 2020 11:42:28 -0300 Subject: [PATCH 09/10] MIDI export only Sf2 drum tracks to channel 10, be them BB or not - Remaining tracks in BB editor are exported normally to other channels --- plugins/MidiExport/MidiExport.cpp | 24 +++++++++++++++++------- plugins/MidiExport/MidiExport.h | 9 ++++++--- plugins/MidiExport/MidiFile.cpp | 19 ++++--------------- plugins/MidiExport/MidiFile.h | 11 ++++------- 4 files changed, 31 insertions(+), 32 deletions(-) diff --git a/plugins/MidiExport/MidiExport.cpp b/plugins/MidiExport/MidiExport.cpp index af6429a0dde..4ade129f881 100644 --- a/plugins/MidiExport/MidiExport.cpp +++ b/plugins/MidiExport/MidiExport.cpp @@ -155,15 +155,15 @@ bool MidiExport::tryExport(const TrackContainer::TrackList &tracks, int tempo, int masterPitch, const QString &filename) { // Count number of instrument (and instrument BB) tracks - int nInstTracks = 0; + int nTracks = 0; for (const Track *track : tracks) { - if (track->type() == Track::InstrumentTrack) { nInstTracks++; } + if (track->type() == Track::InstrumentTrack) { nTracks++; } } - int nInstBbTracks = tracksBb.size(); + nTracks += tracksBb.size(); // Create MIDI file object - MidiFile file(filename, nInstTracks, nInstBbTracks); + MidiFile file(filename, nTracks); m_file = &file; m_tempo = tempo; m_masterPitch = masterPitch; @@ -202,8 +202,10 @@ void MidiExport::processTrack(Track *track, size_t trackIdx, bool isBb) InstrumentTrack *instTrack = dynamic_cast(track); QDomElement root = instTrack->saveState(m_dataFile, m_dataFile.content()); - // Get next MIDI file track object of the list + // 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); @@ -216,10 +218,18 @@ void MidiExport::processTrack(Track *track, size_t trackIdx, bool isBb) // BB tracks are always bank 128 (see MidiImport), patch 0. uint8_t patch = 0; QString instName = instTrack->instrumentName(); - if (instName == "Sf2 Player" and not isBb) + if (instName == "Sf2 Player") { class Instrument *inst = instTrack->instrument(); - patch = inst->childModel("patch")->value(); + 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(); } } midiTrack.addProgramChange(patch, 0); diff --git a/plugins/MidiExport/MidiExport.h b/plugins/MidiExport/MidiExport.h index 9371fb2a4a8..d63de4887b7 100644 --- a/plugins/MidiExport/MidiExport.h +++ b/plugins/MidiExport/MidiExport.h @@ -99,15 +99,15 @@ class MidiExport : public ExportFilter //! 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; - //! Necessary for lmms_plugin_main() - PluginView *instantiateView(QWidget *) { return nullptr; } - public: //! Explicit constructor for setting plugin descriptor MidiExport(); @@ -133,6 +133,9 @@ class MidiExport : public ExportFilter //! Process a given BB track void processBbTrack(Track *track); + + //! Necessary for lmms_plugin_main() + PluginView *instantiateView(QWidget *) { return nullptr; } } ; /*---------------------------------------------------------------------------*/ diff --git a/plugins/MidiExport/MidiFile.cpp b/plugins/MidiExport/MidiFile.cpp index ca0cb713743..2d93ba26b30 100644 --- a/plugins/MidiExport/MidiFile.cpp +++ b/plugins/MidiExport/MidiFile.cpp @@ -35,24 +35,16 @@ using std::sort; /*---------------------------------------------------------------------------*/ -MidiFile::MidiFile(const QString &filename, - int nInstTracks, int nInstBbTracks): +MidiFile::MidiFile(const QString &filename, int nTracks): m_file(filename), - m_header(nInstTracks + nInstBbTracks) + 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)); - // Add tracks with ascending channel numbers (skipping 10) - size_t id = 0; - for (size_t i = 0; i < nInstTracks; ++i) - { - if (++id == 9) ++id; - m_tracks.push_back(Track(id)); - } - // Add channel 10 tracks for all instrument BB ones - m_tracks.insert(m_tracks.end(), nInstBbTracks, Track(9)); + // Resize track list + m_tracks.resize(nTracks); } void MidiFile::writeAllToStream() @@ -150,9 +142,6 @@ void MidiFile::Header::writeToBuffer() /*---------------------------------------------------------------------------*/ -MidiFile::Track::Track(uint8_t channel): - m_channel(channel) {} - void MidiFile::Track::addEvent(Event event, uint32_t time) { event.m_time = time; diff --git a/plugins/MidiExport/MidiFile.h b/plugins/MidiExport/MidiFile.h index 89f4b6498b9..d91b31e7bb5 100644 --- a/plugins/MidiExport/MidiFile.h +++ b/plugins/MidiExport/MidiFile.h @@ -167,16 +167,15 @@ class MidiFile //! 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: - //! Constructor - Track(uint8_t channel); - //! \brief Add both NOTE_ON and NOTE_OFF effects //! \param pitch Note pitch //! \param volume Note volume @@ -227,10 +226,8 @@ class MidiFile //! \brief Open data stream for writing to file and create list of tracks //! \param filename Name of file to be opened - //! \param nInstTracks Number of instrument tracks - //! \param nInstBbTracks Number of instrument BB tracks - MidiFile(const QString &filename, - int nInstTracks, int nInstBbTracks); + //! \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(); From 086bc2484bb5a7a50c0b1ca7c717e85f66ffc8f4 Mon Sep 17 00:00:00 2001 From: Gustavo Chicato Date: Sun, 24 May 2020 13:46:37 -0300 Subject: [PATCH 10/10] Change back comparison operators for compatibility with VS --- plugins/MidiExport/MidiExport.cpp | 17 ++++++++--------- plugins/MidiExport/MidiFile.cpp | 4 ++-- plugins/MidiExport/MidiFile.h | 2 +- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/plugins/MidiExport/MidiExport.cpp b/plugins/MidiExport/MidiExport.cpp index 4ade129f881..7cb24e038a9 100644 --- a/plugins/MidiExport/MidiExport.cpp +++ b/plugins/MidiExport/MidiExport.cpp @@ -63,7 +63,7 @@ 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(); not node.isNull(); + for (QDomNode node = root.firstChild(); !node.isNull(); node = node.nextSibling()) { QDomElement element = node.toElement(); @@ -251,12 +251,11 @@ void MidiExport::processTrack(Track *track, size_t trackIdx, bool isBb) // ---- Patterns ---- // uint8_t bbId = 0; for (QDomNode patNode = root.firstChildElement("pattern"); - not patNode.isNull(); - patNode = patNode.nextSiblingElement("pattern")) + !patNode.isNull(); patNode = patNode.nextSiblingElement("pattern")) { QDomElement patElem = patNode.toElement(); Pattern pat; - if (not isBb) + if (!isBb) { // Base time == initial position int baseTime = patElem.attribute("pos", "0").toInt(); @@ -296,17 +295,17 @@ void MidiExport::writeBbPattern(Pattern &pat, const QDomElement &patElem, Pattern bbPat; for (const pair &p : plist) { - while (not st.empty() and st.top().second <= p.first) + while (!st.empty() && st.top().second <= p.first) { pat.writeToBb(bbPat, len, st.top().first, pos, st.top().second); pos = st.top().second; st.pop(); } - if (not st.empty() and st.top().second <= p.second) + if (!st.empty() && st.top().second <= p.second) { pat.writeToBb(bbPat, len, st.top().first, pos, p.first); pos = p.first; - while (not st.empty() and st.top().second <= p.second) + while (!st.empty() && st.top().second <= p.second) { st.pop(); } @@ -314,7 +313,7 @@ void MidiExport::writeBbPattern(Pattern &pat, const QDomElement &patElem, st.push(p); pos = p.first; } - while (not st.empty()) + while (!st.empty()) { pat.writeToBb(bbPat, len, st.top().first, pos, st.top().second); pos = st.top().second; @@ -334,7 +333,7 @@ void MidiExport::processBbTrack(Track *track) // Build lists of (start, end) pairs from BB note objects vector> plist; for (QDomNode bbtcoNode = root.firstChildElement("bbtco"); - not bbtcoNode.isNull(); + !bbtcoNode.isNull(); bbtcoNode = bbtcoNode.nextSiblingElement("bbtco")) { QDomElement bbtcoElem = bbtcoNode.toElement(); diff --git a/plugins/MidiExport/MidiFile.cpp b/plugins/MidiExport/MidiFile.cpp index 2d93ba26b30..9b6b95d9a29 100644 --- a/plugins/MidiExport/MidiFile.cpp +++ b/plugins/MidiExport/MidiFile.cpp @@ -73,7 +73,7 @@ void MidiFile::Section::writeBytes(vector bytes, vector *v) { // Insert content to the end of *v - if (not v) v = &m_buffer; + if (!v) v = &m_buffer; v->insert(v->end(), bytes.begin(), bytes.end()); } @@ -91,7 +91,7 @@ void MidiFile::Section::writeVarLength(uint32_t val) val >>= 7; } // Add packs in reverse order to actual buffer - while (not little_endian.empty()) + while (!little_endian.empty()) { m_buffer.push_back(little_endian.top()); little_endian.pop(); diff --git a/plugins/MidiExport/MidiFile.h b/plugins/MidiExport/MidiFile.h index d91b31e7bb5..a93fc5a70c1 100644 --- a/plugins/MidiExport/MidiFile.h +++ b/plugins/MidiExport/MidiFile.h @@ -153,7 +153,7 @@ class MidiFile inline bool operator<(const Event& b) const { if (m_time < b.m_time) { return true; } - return (m_time == b.m_time and m_type > b.m_type); + return (m_time == b.m_time && m_type > b.m_type); } };