midi export plugin
This commit is contained in:
4
plugins/MidiExport/CMakeLists.txt
Normal file
4
plugins/MidiExport/CMakeLists.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
INCLUDE(BuildPlugin)
|
||||
|
||||
BUILD_PLUGIN(midiexport MidiExport.cpp MidiExport.h MidiFile.hpp
|
||||
MOCFILES MidiExport.h)
|
||||
183
plugins/MidiExport/MidiExport.cpp
Normal file
183
plugins/MidiExport/MidiExport.cpp
Normal file
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* MidiExport.cpp - support for importing MIDI files
|
||||
*
|
||||
* Author: Mohamed Abdel Maksoud <mohamed at amaksoud.com>
|
||||
*
|
||||
* This file is part of LMMS - http://lmms.io
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public
|
||||
* License along with this program (see COPYING); if not, write to the
|
||||
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
||||
* Boston, MA 02110-1301 USA.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
#include <QDomDocument>
|
||||
#include <QDir>
|
||||
#include <QApplication>
|
||||
#include <QMessageBox>
|
||||
#include <QProgressDialog>
|
||||
|
||||
#include "MidiExport.h"
|
||||
#include "Engine.h"
|
||||
#include "TrackContainer.h"
|
||||
#include "InstrumentTrack.h"
|
||||
|
||||
|
||||
extern "C"
|
||||
{
|
||||
|
||||
Plugin::Descriptor PLUGIN_EXPORT midiexport_plugin_descriptor =
|
||||
{
|
||||
STRINGIFY( PLUGIN_NAME ),
|
||||
"MIDI Export",
|
||||
QT_TRANSLATE_NOOP( "pluginBrowser",
|
||||
"Filter for exporting MIDI-files from LMMS" ),
|
||||
"Mohamed Abdel Maksoud <mohamed at amaksoud.com>",
|
||||
0x0100,
|
||||
Plugin::ExportFilter,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL
|
||||
} ;
|
||||
|
||||
}
|
||||
|
||||
|
||||
MidiExport::MidiExport() : ExportFilter( &midiexport_plugin_descriptor)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
MidiExport::~MidiExport()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
bool MidiExport::tryExport( const TrackContainer::TrackList &tracks, int tempo, const QString &filename )
|
||||
{
|
||||
QFile f(filename);
|
||||
f.open(QIODevice::WriteOnly);
|
||||
QDataStream midiout(&f);
|
||||
|
||||
InstrumentTrack* instTrack;
|
||||
QDomElement element;
|
||||
|
||||
|
||||
int nTracks = 0;
|
||||
const int BUFFER_SIZE = 50*1024;
|
||||
uint8_t buffer[BUFFER_SIZE];
|
||||
uint32_t size;
|
||||
|
||||
foreach( Track* track, tracks ) if( track->type() == Track::InstrumentTrack ) nTracks++;
|
||||
|
||||
// midi header
|
||||
MidiFile::MIDIHeader header(nTracks);
|
||||
size = header.writeToBuffer(buffer);
|
||||
midiout.writeRawData((char *)buffer, size);
|
||||
|
||||
// midi tracks
|
||||
foreach( Track* track, tracks )
|
||||
{
|
||||
DataFile dataFile( DataFile::SongProject );
|
||||
MidiFile::MIDITrack<BUFFER_SIZE> mtrack;
|
||||
|
||||
if( track->type() != Track::InstrumentTrack ) continue;
|
||||
|
||||
//qDebug() << "exporting " << track->name();
|
||||
|
||||
|
||||
mtrack.addName(track->name().toStdString(), 0);
|
||||
//mtrack.addProgramChange(0, 0);
|
||||
mtrack.addTempo(tempo, 0);
|
||||
|
||||
instTrack = dynamic_cast<InstrumentTrack *>( track );
|
||||
element = instTrack->saveState( dataFile, dataFile.content() );
|
||||
|
||||
// instrumentTrack
|
||||
// - instrumentTrack
|
||||
// - pattern
|
||||
int base_pitch = 0;
|
||||
double base_volume = 1.0;
|
||||
int base_time = 0;
|
||||
|
||||
|
||||
for(QDomNode n = element.firstChild(); !n.isNull(); n = n.nextSibling())
|
||||
{
|
||||
//QDomText txt = n.toText();
|
||||
//qDebug() << ">> child node " << n.nodeName();
|
||||
|
||||
if (n.nodeName() == "instrumenttrack")
|
||||
{
|
||||
// TODO interpret pan="0" fxch="0" usemasterpitch="1" pitchrange="1" pitch="0" basenote="57"
|
||||
QDomElement it = n.toElement();
|
||||
base_pitch = it.attribute("pitch", "0").toInt();
|
||||
base_volume = it.attribute("volume", "100").toDouble()/100.0;
|
||||
}
|
||||
|
||||
if (n.nodeName() == "pattern")
|
||||
{
|
||||
base_time = n.toElement().attribute("pos", "0").toInt();
|
||||
// TODO interpret steps="12" muted="0" type="1" name="Piano1" len="2592"
|
||||
for(QDomNode nn = n.firstChild(); !nn.isNull(); nn = nn.nextSibling())
|
||||
{
|
||||
QDomElement note = nn.toElement();
|
||||
if (note.attribute("len", "0") == "0" || note.attribute("vol", "0") == "0") continue;
|
||||
#if 0
|
||||
qDebug() << ">>>> key " << note.attribute( "key", "0" )
|
||||
<< " " << note.attribute("len", "0") << " @"
|
||||
<< note.attribute("pos", "0");
|
||||
#endif
|
||||
mtrack.addNote(
|
||||
note.attribute("key", "0").toInt()+base_pitch
|
||||
, 100 * base_volume * (note.attribute("vol", "100").toDouble()/100)
|
||||
, (base_time+note.attribute("pos", "0").toDouble())/48
|
||||
, (note.attribute("len", "0")).toDouble()/48);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
size = mtrack.writeToBuffer(buffer);
|
||||
midiout.writeRawData((char *)buffer, size);
|
||||
} // for each track
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
void MidiExport::error()
|
||||
{
|
||||
//qDebug() << "MidiExport error: " << m_error ;
|
||||
}
|
||||
|
||||
|
||||
|
||||
extern "C"
|
||||
{
|
||||
|
||||
// necessary for getting instance out of shared lib
|
||||
Plugin * PLUGIN_EXPORT lmms_plugin_main( Model *, void * _data )
|
||||
{
|
||||
return new MidiExport();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
58
plugins/MidiExport/MidiExport.h
Normal file
58
plugins/MidiExport/MidiExport.h
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* MidiExport.h - support for Exporting MIDI-files
|
||||
*
|
||||
* Author: Mohamed Abdel Maksoud <mohamed at amaksoud.com>
|
||||
*
|
||||
* This file is part of LMMS - http://lmms.io
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public
|
||||
* License along with this program (see COPYING); if not, write to the
|
||||
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
||||
* Boston, MA 02110-1301 USA.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef _MIDI_EXPORT_H
|
||||
#define _MIDI_EXPORT_H
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include "ExportFilter.h"
|
||||
#include "MidiFile.hpp"
|
||||
|
||||
|
||||
|
||||
class MidiExport: public ExportFilter
|
||||
{
|
||||
// Q_OBJECT
|
||||
public:
|
||||
MidiExport( );
|
||||
~MidiExport();
|
||||
|
||||
virtual PluginView * instantiateView( QWidget * )
|
||||
{
|
||||
return( NULL );
|
||||
}
|
||||
|
||||
virtual bool tryExport( const TrackContainer::TrackList &tracks, int tempo, const QString &filename );
|
||||
|
||||
private:
|
||||
|
||||
|
||||
void error( void );
|
||||
|
||||
|
||||
} ;
|
||||
|
||||
|
||||
#endif
|
||||
320
plugins/MidiExport/MidiFile.hpp
Normal file
320
plugins/MidiExport/MidiFile.hpp
Normal file
@@ -0,0 +1,320 @@
|
||||
#ifndef MIDIFILE_HPP
|
||||
#define MIDIFILE_HPP
|
||||
|
||||
/**
|
||||
* Name: MidiFile.hpp
|
||||
* Purpose: C++ re-write of the python module MidiFile.py
|
||||
* Author: Mohamed Abdel Maksoud <mohamed at amaksoud.com>
|
||||
*-----------------------------------------------------------------------------
|
||||
* Name: MidiFile.py
|
||||
* Purpose: MIDI file manipulation utilities
|
||||
*
|
||||
* Author: Mark Conway Wirt <emergentmusics) at (gmail . com>
|
||||
*
|
||||
* Created: 2008/04/17
|
||||
* Copyright: (c) 2009 Mark Conway Wirt
|
||||
* License: Please see License.txt for the terms under which this
|
||||
* software is distributed.
|
||||
*-----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <set>
|
||||
#include <algorithm>
|
||||
#include <assert.h>
|
||||
|
||||
using std::string;
|
||||
using std::vector;
|
||||
using std::set;
|
||||
|
||||
namespace MidiFile
|
||||
{
|
||||
|
||||
const int TICKSPERBEAT = 128;
|
||||
|
||||
|
||||
int writeVarLength(uint32_t val, uint8_t *buffer)
|
||||
{
|
||||
/*
|
||||
Accept an input, and write a MIDI-compatible variable length stream
|
||||
|
||||
The MIDI format is a little strange, and makes use of so-called variable
|
||||
length quantities. These quantities are a stream of bytes. If the most
|
||||
significant bit is 1, then more bytes follow. If it is zero, then the
|
||||
byte in question is the last in the stream
|
||||
*/
|
||||
int size = 0;
|
||||
uint8_t result, little_endian[4];
|
||||
result = val & 0x7F;
|
||||
little_endian[size++] = result;
|
||||
val = val >> 7;
|
||||
while (val > 0)
|
||||
{
|
||||
result = val & 0x7F;
|
||||
result = result | 0x80;
|
||||
little_endian[size++] = result;
|
||||
val = val >> 7;
|
||||
}
|
||||
for (int i=0; i<size; i++)
|
||||
{
|
||||
buffer[i] = little_endian[size-i-1];
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
int writeBigEndian4(uint32_t val, uint8_t *buf)
|
||||
{
|
||||
buf[0] = val >> 24;
|
||||
buf[1] = val >> 16 & 0xff;
|
||||
buf[2] = val >> 8 & 0xff;
|
||||
buf[3] = val & 0xff;
|
||||
return 4;
|
||||
}
|
||||
|
||||
int writeBigEndian2(uint16_t val, uint8_t *buf)
|
||||
{
|
||||
buf[0] = val >> 8 & 0xff;
|
||||
buf[1] = val & 0xff;
|
||||
return 2;
|
||||
}
|
||||
|
||||
|
||||
class MIDIHeader
|
||||
{
|
||||
// Class to encapsulate the MIDI header structure.
|
||||
uint16_t numTracks;
|
||||
uint16_t ticksPerBeat;
|
||||
|
||||
public:
|
||||
|
||||
MIDIHeader(uint16_t nTracks, uint16_t ticksPB=TICKSPERBEAT): numTracks(nTracks), ticksPerBeat(ticksPB) {}
|
||||
|
||||
inline int writeToBuffer(uint8_t *buffer, int start=0) const
|
||||
{
|
||||
// chunk ID
|
||||
buffer[start++] = 'M'; buffer[start++] = 'T'; buffer[start++] = 'h'; buffer[start++] = 'd';
|
||||
// chunk size (6 bytes always)
|
||||
buffer[start++] = 0; buffer[start++] = 0; buffer[start++] = 0; buffer[start++] = 0x06;
|
||||
// format: 1 (multitrack)
|
||||
buffer[start++] = 0; buffer[start++] = 0x01;
|
||||
|
||||
start += writeBigEndian2(numTracks, buffer+start);
|
||||
|
||||
start += writeBigEndian2(ticksPerBeat, buffer+start);
|
||||
|
||||
return start;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
struct Event
|
||||
{
|
||||
uint32_t time;
|
||||
uint32_t tempo;
|
||||
string trackName;
|
||||
enum {NOTE_ON, NOTE_OFF, TEMPO, PROG_CHANGE, TRACK_NAME} type;
|
||||
// TODO make a union to save up space
|
||||
uint8_t pitch;
|
||||
uint8_t programNumber;
|
||||
uint8_t duration;
|
||||
uint8_t volume;
|
||||
uint8_t channel;
|
||||
|
||||
Event() {time=tempo=pitch=programNumber=duration=volume=channel=0; trackName="";}
|
||||
|
||||
inline int writeToBuffer(uint8_t *buffer) const
|
||||
{
|
||||
uint8_t code, fourbytes[4];
|
||||
int size=0;
|
||||
switch (type)
|
||||
{
|
||||
case NOTE_ON:
|
||||
code = 0x9 << 4 | channel;
|
||||
size += writeVarLength(time, buffer+size);
|
||||
buffer[size++] = code;
|
||||
buffer[size++] = pitch;
|
||||
buffer[size++] = volume;
|
||||
break;
|
||||
case NOTE_OFF:
|
||||
code = 0x8 << 4 | channel;
|
||||
size += writeVarLength(time, buffer+size);
|
||||
buffer[size++] = code;
|
||||
buffer[size++] = pitch;
|
||||
buffer[size++] = volume;
|
||||
break;
|
||||
case TEMPO:
|
||||
code = 0xFF;
|
||||
size += writeVarLength(time, buffer+size);
|
||||
buffer[size++] = code;
|
||||
buffer[size++] = 0x51;
|
||||
buffer[size++] = 0x03;
|
||||
writeBigEndian4(int(60000000.0 / tempo), fourbytes);
|
||||
|
||||
//printf("tempo of %x translates to ", tempo);
|
||||
for (int i=0; i<3; i++) printf("%02x ", fourbytes[i+1]);
|
||||
printf("\n");
|
||||
buffer[size++] = fourbytes[1];
|
||||
buffer[size++] = fourbytes[2];
|
||||
buffer[size++] = fourbytes[3];
|
||||
break;
|
||||
case PROG_CHANGE:
|
||||
code = 0xC << 4 | channel;
|
||||
size += writeVarLength(time, buffer+size);
|
||||
buffer[size++] = code;
|
||||
buffer[size++] = programNumber;
|
||||
break;
|
||||
case TRACK_NAME:
|
||||
size += writeVarLength(time, buffer+size);
|
||||
buffer[size++] = 0xFF;
|
||||
buffer[size++] = 0x03;
|
||||
size += writeVarLength(trackName.size(), buffer+size);
|
||||
trackName.copy((char *)(&buffer[size]), trackName.size());
|
||||
size += trackName.size();
|
||||
// buffer[size++] = '\0';
|
||||
// buffer[size++] = '\0';
|
||||
|
||||
break;
|
||||
}
|
||||
return size;
|
||||
} // writeEventsToBuffer
|
||||
|
||||
|
||||
// events are sorted by their time
|
||||
inline bool operator < (const Event& b) const {
|
||||
if (this->time < b.time) return true;
|
||||
#if 0
|
||||
if (this->type < b.type) return true;
|
||||
if (this->pitch < b.pitch) return true;
|
||||
if (this->duration < b.duration) return true;
|
||||
if (this->volume < b.volume) return true;
|
||||
#if 1
|
||||
if (this->programNumber < b.programNumber) return true;
|
||||
if (this->channel < b.channel) return true;
|
||||
if (this->trackName < b.trackName) return true;
|
||||
#endif
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
template<const int MAX_TRACK_SIZE>
|
||||
class MIDITrack
|
||||
{
|
||||
// A class that encapsulates a MIDI track
|
||||
// Nested class definitions.
|
||||
vector<Event> events;
|
||||
|
||||
public:
|
||||
uint8_t channel;
|
||||
|
||||
MIDITrack(): channel(0) {}
|
||||
|
||||
inline void addEvent(const Event &e)
|
||||
{
|
||||
Event E = e;
|
||||
events.push_back(E);
|
||||
}
|
||||
|
||||
inline void addNote(uint8_t pitch, uint8_t volume, double time, double duration)
|
||||
{
|
||||
Event event; event.channel = channel;
|
||||
event.volume = volume;
|
||||
|
||||
event.type = Event::NOTE_ON; event.pitch = pitch; event.time= (uint32_t) (time * TICKSPERBEAT);
|
||||
addEvent(event);
|
||||
|
||||
event.type = Event::NOTE_OFF; event.pitch = pitch; event.time=(uint32_t) ((time+duration) * TICKSPERBEAT);
|
||||
addEvent(event);
|
||||
|
||||
//printf("note: %d-%d\n", (uint32_t) time * TICKSPERBEAT, (uint32_t)((time+duration) * TICKSPERBEAT));
|
||||
}
|
||||
|
||||
inline void addName(const string &name, uint32_t time)
|
||||
{
|
||||
Event event; event.channel = channel;
|
||||
event.type = Event::TRACK_NAME; event.time=time; event.trackName = name;
|
||||
addEvent(event);
|
||||
}
|
||||
|
||||
inline void addProgramChange(uint8_t prog, uint32_t time)
|
||||
{
|
||||
Event event; event.channel = channel;
|
||||
event.type = Event::PROG_CHANGE; event.time=time; event.programNumber = prog;
|
||||
addEvent(event);
|
||||
}
|
||||
|
||||
inline void addTempo(uint8_t tempo, uint32_t time)
|
||||
{
|
||||
Event event; event.channel = channel;
|
||||
event.type = Event::TEMPO; event.time=time; event.tempo = tempo;
|
||||
addEvent(event);
|
||||
}
|
||||
|
||||
inline int writeMIDIToBuffer(uint8_t *buffer, int start=0) const
|
||||
{
|
||||
// Write the meta data and note data to the packed MIDI stream.
|
||||
// Process the events in the eventList
|
||||
|
||||
start += writeEventsToBuffer(buffer, start);
|
||||
|
||||
// Write MIDI close event.
|
||||
buffer[start++] = 0x00;
|
||||
buffer[start++] = 0xFF;
|
||||
buffer[start++] = 0x2F;
|
||||
buffer[start++] = 0x00;
|
||||
|
||||
// return the entire length of the data and write to the header
|
||||
|
||||
return start;
|
||||
}
|
||||
|
||||
inline int writeEventsToBuffer(uint8_t *buffer, int start=0) const
|
||||
{
|
||||
// Write the events in MIDIEvents to the MIDI stream.
|
||||
vector<Event> _events = events;
|
||||
std::sort(_events.begin(), _events.end());
|
||||
vector<Event>::const_iterator it;
|
||||
uint32_t time_last = 0, tmp;
|
||||
for (it = _events.begin(); it!=_events.end(); ++it)
|
||||
{
|
||||
Event e = *it;
|
||||
if (e.time < time_last){
|
||||
printf("error: e.time=%d time_last=%d\n", e.time, time_last);
|
||||
assert(false);
|
||||
}
|
||||
tmp = e.time;
|
||||
e.time -= time_last;
|
||||
time_last = tmp;
|
||||
start += e.writeToBuffer(buffer+start);
|
||||
if (start >= MAX_TRACK_SIZE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return start;
|
||||
}
|
||||
|
||||
inline int writeToBuffer(uint8_t *buffer, int start=0) const
|
||||
{
|
||||
uint8_t eventsBuffer[MAX_TRACK_SIZE];
|
||||
uint32_t events_size = writeMIDIToBuffer(eventsBuffer);
|
||||
//printf(">> track %lu events took 0x%x bytes\n", events.size(), events_size);
|
||||
|
||||
// chunk ID
|
||||
buffer[start++] = 'M'; buffer[start++] = 'T'; buffer[start++] = 'r'; buffer[start++] = 'k';
|
||||
// chunk size
|
||||
start += writeBigEndian4(events_size, buffer+start);
|
||||
// copy events data
|
||||
memmove(buffer+start, eventsBuffer, events_size);
|
||||
start += events_size;
|
||||
return start;
|
||||
}
|
||||
};
|
||||
|
||||
}; // namespace
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user