548 lines
14 KiB
C++
548 lines
14 KiB
C++
//=============================================================================
|
|
// MuseScore
|
|
// Music Composition & Notation
|
|
//
|
|
// Copyright (C) 2007-2011 Werner Schweer
|
|
//
|
|
// This program is free software; you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License version 2
|
|
// as published by the Free Software Foundation and appearing in
|
|
// the file LICENCE.GPL
|
|
//=============================================================================
|
|
|
|
#include <QFile>
|
|
#include "midifile.h"
|
|
|
|
//---------------------------------------------------------
|
|
// MidiFile
|
|
//---------------------------------------------------------
|
|
|
|
MidiFile::MidiFile()
|
|
{
|
|
fp = 0;
|
|
_format = 1;
|
|
_division = 480;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// readMidi
|
|
// return false on error
|
|
//---------------------------------------------------------
|
|
|
|
bool MidiFile::read(const QString& path)
|
|
{
|
|
QFile f(path);
|
|
if (!f.open(QIODevice::ReadOnly)) {
|
|
return false;
|
|
}
|
|
return read(&f);
|
|
}
|
|
|
|
bool MidiFile::read(QIODevice* f)
|
|
{
|
|
fp = f;
|
|
_tracks.clear();
|
|
curPos = 0;
|
|
|
|
char tmp[4];
|
|
|
|
read(tmp, 4);
|
|
int len = readLong();
|
|
if (memcmp(tmp, "MThd", 4) || len < 6) {
|
|
throw(QString("bad midifile: MThd expected"));
|
|
}
|
|
|
|
_format = readShort();
|
|
int ntracks = readShort();
|
|
_division = readShort();
|
|
|
|
if (_division < 0) {
|
|
_division = (-(_division / 256)) * (_division & 0xff);
|
|
}
|
|
if (len > 6) {
|
|
skip(len - 6); // skip the excess
|
|
}
|
|
switch (_format) {
|
|
case 0:
|
|
if (readTrack()) {
|
|
return false;
|
|
}
|
|
break;
|
|
case 1:
|
|
for (int i = 0; i < ntracks; i++) {
|
|
if (readTrack()) {
|
|
return false;
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
throw(QString("midi file format %1 not implemented").arg(_format));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// readTrack
|
|
// return true on error
|
|
//---------------------------------------------------------
|
|
|
|
bool MidiFile::readTrack()
|
|
{
|
|
char tmp[4];
|
|
read(tmp, 4);
|
|
if (memcmp(tmp, "MTrk", 4)) {
|
|
throw(QString("bad midifile: MTrk expected"));
|
|
}
|
|
int len = readLong(); // len
|
|
qint64 endPos = curPos + len;
|
|
status = -1;
|
|
sstatus = -1; // running status, will not be reset on meta or sysex
|
|
click = 0;
|
|
MidiTrack* track = new MidiTrack(this);
|
|
_tracks.push_back(track);
|
|
|
|
for (;;) {
|
|
MidiEvent event;
|
|
int rv = readEvent(&event);
|
|
if (rv == 0) {
|
|
track->events().insert(std::pair<int, MidiEvent>(click, event));
|
|
} else if (rv == 2) {
|
|
break;
|
|
}
|
|
}
|
|
if (curPos != endPos) {
|
|
qWarning("bad track len: %lld != %lld, %lld bytes too much\n", endPos, curPos, endPos - curPos);
|
|
if (curPos < endPos) {
|
|
qWarning(" skip %lld\n", endPos - curPos);
|
|
skip(endPos - curPos);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/*---------------------------------------------------------
|
|
* read
|
|
* return false on error
|
|
*---------------------------------------------------------*/
|
|
|
|
void MidiFile::read(void* p, qint64 len)
|
|
{
|
|
curPos += len;
|
|
qint64 rv = fp->read((char*)p, len);
|
|
if (rv != len) {
|
|
throw(QString("bad midifile: unexpected EOF"));
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// readShort
|
|
//---------------------------------------------------------
|
|
|
|
int MidiFile::readShort()
|
|
{
|
|
char c;
|
|
int val = 0;
|
|
for (int i = 0; i < 2; ++i) {
|
|
fp->getChar(&c);
|
|
val <<= 8;
|
|
val += (c & 0xff);
|
|
}
|
|
return val;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// readLong
|
|
// writeLong
|
|
//---------------------------------------------------------
|
|
|
|
int MidiFile::readLong()
|
|
{
|
|
char c;
|
|
int val = 0;
|
|
for (int i = 0; i < 4; ++i) {
|
|
fp->getChar(&c);
|
|
val <<= 8;
|
|
val += (c & 0xff);
|
|
}
|
|
return val;
|
|
}
|
|
|
|
/*---------------------------------------------------------
|
|
* skip
|
|
* This is meant for skipping a few bytes in a
|
|
* file or fifo.
|
|
*---------------------------------------------------------*/
|
|
|
|
void MidiFile::skip(qint64 len)
|
|
{
|
|
if (len <= 0) {
|
|
return;
|
|
}
|
|
char tmp[len];
|
|
read(tmp, len);
|
|
}
|
|
|
|
/*---------------------------------------------------------
|
|
* getvl
|
|
* Read variable-length number (7 bits per byte, MSB first)
|
|
*---------------------------------------------------------*/
|
|
|
|
int MidiFile::getvl()
|
|
{
|
|
int l = 0;
|
|
for (int i = 0; i < 16; i++) {
|
|
uchar c;
|
|
read(&c, 1);
|
|
l += (c & 0x7f);
|
|
if (!(c & 0x80)) {
|
|
return l;
|
|
}
|
|
l <<= 7;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// MidiTrack
|
|
//---------------------------------------------------------
|
|
|
|
MidiTrack::MidiTrack(MidiFile* f)
|
|
{
|
|
mf = f;
|
|
// _outChannel = -1;
|
|
// _outPort = -1;
|
|
// _drumTrack = false;
|
|
// _hasKey = false;
|
|
// _staffIdx = -1;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// readEvent
|
|
// return 0 - valid event
|
|
// 1 - skip event
|
|
// 2 - EOT
|
|
//---------------------------------------------------------
|
|
|
|
int MidiFile::readEvent(MidiEvent* event)
|
|
{
|
|
uchar me, a, b;
|
|
|
|
int nclick = getvl();
|
|
if (nclick == -1) {
|
|
throw(QString("readEvent: error 1(getvl)"));
|
|
}
|
|
click += nclick;
|
|
for (;;) {
|
|
read(&me, 1);
|
|
if (me >= 0xf1 && me <= 0xfe && me != 0xf7) {
|
|
qDebug("Midi: Unknown Message 0x%02x", me & 0xff);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
uchar* data;
|
|
int dataLen;
|
|
|
|
if (me == 0xf0 || me == 0xf7) {
|
|
status = -1; // no running status
|
|
int len = getvl();
|
|
if (len == -1) {
|
|
throw(QString("readEvent: error 3"));
|
|
}
|
|
data = new unsigned char[len + 1];
|
|
dataLen = len;
|
|
read(data, len);
|
|
data[dataLen] = 0; // always terminate with zero
|
|
if (data[len - 1] != 0xf7) {
|
|
qDebug("SYSEX does not end with 0xf7!");
|
|
// more to come?
|
|
} else {
|
|
dataLen--; // don't count 0xf7
|
|
}
|
|
delete[] data;
|
|
#if 0
|
|
event->setType(MidiEventType::SYSEX);
|
|
event->setData(data);
|
|
event->setLen(dataLen);
|
|
event->setOntime(click);
|
|
#endif
|
|
return 1;
|
|
}
|
|
|
|
if (me == 0xff) { // MidiEventType::META) {
|
|
status = -1; // no running status
|
|
uchar type;
|
|
read(&type, 1);
|
|
dataLen = getvl(); // read len
|
|
if (dataLen == -1) {
|
|
throw(QString("readEvent: error 6"));
|
|
}
|
|
data = new unsigned char[dataLen + 1];
|
|
if (dataLen) {
|
|
read(data, dataLen);
|
|
}
|
|
data[dataLen] = 0; // always terminate with zero so we get valid C++ strings
|
|
if (type == META_TEMPO) {
|
|
unsigned tempo = data[2] + (data[1] << 8) + (data[0] << 16);
|
|
double t = 1000000.0 / double(tempo);
|
|
_tempoMap.insert(std::pair<const int, qreal>(click, t));
|
|
}
|
|
//else
|
|
//printf("META %02x\n", type);
|
|
delete[] data;
|
|
if (type == META_EOT) {
|
|
return 2;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
if (me & 0x80) { // status byte
|
|
status = me;
|
|
sstatus = status;
|
|
read(&a, 1);
|
|
} else {
|
|
if (status == -1) {
|
|
qDebug("readEvent: no running status, read 0x%02x", me);
|
|
qDebug("sstatus ist 0x%02x", sstatus);
|
|
if (sstatus == -1) {
|
|
return 0;
|
|
}
|
|
status = sstatus;
|
|
}
|
|
a = me;
|
|
}
|
|
int channel = status & 0x0f;
|
|
b = 0;
|
|
MidiEventType t = MidiEventType(status & 0xf0);
|
|
switch (t) {
|
|
case MidiEventType::NOTEOFF:
|
|
case MidiEventType::NOTEON:
|
|
case MidiEventType::POLYAFTER:
|
|
case MidiEventType::CONTROLLER: // controller
|
|
case MidiEventType::PITCHBEND: // pitch bend
|
|
read(&b, 1);
|
|
default:
|
|
break;
|
|
}
|
|
switch (t) {
|
|
case MidiEventType::NOTEOFF:
|
|
case MidiEventType::NOTEON:
|
|
case MidiEventType::CONTROLLER:
|
|
case MidiEventType::PITCHBEND:
|
|
case MidiEventType::POLYAFTER:
|
|
event->set(t, channel, a, b);
|
|
break;
|
|
case MidiEventType::PROGRAM:
|
|
case MidiEventType::AFTERTOUCH:
|
|
event->set(t, channel, a, 0);
|
|
break;
|
|
default: // f1 f2 f3 f4 f5 f6 f7 f8 f9
|
|
throw(QString("BAD STATUS")); // 0x%02x, me 0x%02x", status, me);
|
|
}
|
|
|
|
if ((a & 0x80) || (b & 0x80)) {
|
|
qDebug("8't bit in data set(%02x %02x): tick %d read 0x%02x status:0x%02x",
|
|
a & 0xff, b & 0xff, click, me, status);
|
|
qDebug("readEvent: error 16");
|
|
if (b & 0x80) {
|
|
// Try to fix: interpret as channel byte
|
|
status = b;
|
|
sstatus = status;
|
|
return 0;
|
|
}
|
|
throw(QString("readEvent: error 16"));
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// write
|
|
//---------------------------------------------------------
|
|
|
|
bool MidiFile::write(const QString& path)
|
|
{
|
|
QFile f(path);
|
|
if (!f.open(QIODevice::WriteOnly)) {
|
|
return false;
|
|
}
|
|
return write(&f);
|
|
}
|
|
|
|
bool MidiFile::write(QIODevice* f)
|
|
{
|
|
fp = f;
|
|
write("MThd", 4);
|
|
writeLong(6); // header len
|
|
writeShort(_format); // format
|
|
writeShort(_tracks.size());
|
|
writeShort(_division);
|
|
foreach (const MidiTrack* t, _tracks) {
|
|
if (writeTrack(t)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// write
|
|
//---------------------------------------------------------
|
|
|
|
void MidiFile::writeEvent(const MidiEvent& event)
|
|
{
|
|
switch (event.type()) {
|
|
case MidiEventType::NOTEON:
|
|
writeStatus(event.type(), event.channel());
|
|
put(event.dataA() & 0x7f);
|
|
put(event.dataB() & 0x7f);
|
|
break;
|
|
|
|
case MidiEventType::NOTEOFF:
|
|
writeStatus(event.type(), event.channel());
|
|
put(event.dataA() & 0x7f);
|
|
put(event.dataB() & 0x7f);
|
|
break;
|
|
|
|
case MidiEventType::CONTROLLER:
|
|
writeStatus(event.type(), event.channel());
|
|
put(event.dataA() & 0x7f);
|
|
put(event.dataB() & 0x7f);
|
|
break;
|
|
|
|
case MidiEventType::PROGRAM:
|
|
writeStatus(event.type(), event.channel());
|
|
put(event.dataA() & 0x7f);
|
|
break;
|
|
#if 0
|
|
case MidieEventType::META:
|
|
put(MidiEventType::META);
|
|
put(event.metaType());
|
|
putvl(event.len());
|
|
write(event.edata(), event.len());
|
|
resetRunningStatus(); // really ?!
|
|
break;
|
|
|
|
case MidiEventType::SYSEX:
|
|
put(MidiEventType::SYSEX);
|
|
putvl(event.len() + 1); // including 0xf7
|
|
write(event.edata(), event.len());
|
|
put(MidiEventTppe::ENDSYSEX);
|
|
resetRunningStatus();
|
|
break;
|
|
#endif
|
|
default:
|
|
//fprintf(stderr, "unsupported\n");
|
|
break;
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// writeTrack
|
|
//---------------------------------------------------------
|
|
|
|
bool MidiFile::writeTrack(const MidiTrack* t)
|
|
{
|
|
write("MTrk", 4);
|
|
qint64 lenpos = fp->pos();
|
|
writeLong(0); // dummy len
|
|
|
|
status = -1;
|
|
int tick = 0;
|
|
for (auto i : t->events()) {
|
|
int ntick = i.first;
|
|
putvl(ntick - tick); // write tick delta
|
|
writeEvent(i.second);
|
|
tick = ntick;
|
|
}
|
|
|
|
//---------------------------------------------------
|
|
// write "End Of Track" Meta
|
|
// write Track Len
|
|
//
|
|
|
|
putvl(1);
|
|
put(0xff); // Meta
|
|
put(0x2f); // EOT
|
|
putvl(0); // len 0
|
|
qint64 endpos = fp->pos();
|
|
fp->seek(lenpos);
|
|
writeLong(endpos - lenpos - 4); // tracklen
|
|
fp->seek(endpos);
|
|
return false;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// writeStatus
|
|
//---------------------------------------------------------
|
|
|
|
void MidiFile::writeStatus(MidiEventType st, int c)
|
|
{
|
|
uchar nstat = uchar(st) | (c & 0xf);
|
|
//
|
|
// running status; except for Sysex- and Meta Events
|
|
//
|
|
if ((((nstat & 0xf0) != 0xf0) && (nstat != status))) {
|
|
status = nstat;
|
|
put(nstat);
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// write
|
|
//---------------------------------------------------------
|
|
|
|
bool MidiFile::write(const void* p, qint64 len)
|
|
{
|
|
qint64 rv = fp->write((char*)p, len);
|
|
if (rv == len) {
|
|
return false;
|
|
}
|
|
qDebug("write midifile failed: %s", fp->errorString().toLatin1().data());
|
|
return true;
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// writeShort
|
|
//---------------------------------------------------------
|
|
|
|
void MidiFile::writeShort(int i)
|
|
{
|
|
fp->putChar(i >> 8);
|
|
fp->putChar(i);
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// writeLong
|
|
//---------------------------------------------------------
|
|
|
|
void MidiFile::writeLong(int i)
|
|
{
|
|
fp->putChar(i >> 24);
|
|
fp->putChar(i >> 16);
|
|
fp->putChar(i >> 8);
|
|
fp->putChar(i);
|
|
}
|
|
|
|
/*---------------------------------------------------------
|
|
* putvl
|
|
* Write variable-length number (7 bits per byte, MSB first)
|
|
*---------------------------------------------------------*/
|
|
|
|
void MidiFile::putvl(unsigned val)
|
|
{
|
|
unsigned long buf = val & 0x7f;
|
|
while ((val >>= 7) > 0) {
|
|
buf <<= 8;
|
|
buf |= 0x80;
|
|
buf += (val & 0x7f);
|
|
}
|
|
for (;;) {
|
|
put(buf);
|
|
if (buf & 0x80) {
|
|
buf >>= 8;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|