//CLI Midi file player based on libfluidsynth
//Midi file reading module
//Written by Chris Xiong, 2015
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cstdint>
#include <cstdarg>
#include <algorithm>
#include <stdexcept>
#include "qmpmidiplay.hpp"
static const char *GM1SysX = "\xF0\x7E\x7F\x09\x01\xF7";
static const char *GM2SysX = "\xF0\x7E\x7F\x09\x03\xF7";
static const char *GSSysEx = "\xF0\x41\x10\x42\x12\x40\x00\x7F\x00\x41\xF7";
static const char *XGSysEx = "\xF0\x43\x10\x4C\x00\x00\x7E\x00\xF7";
#define assert(x) if(!(x))this->error(false,"assertion failure @ qmpmidiread.cpp:%d",__LINE__)
void CSMFReader::error(int fatal, const char *format, ...)
{
va_list ap;
char buf[1024], bufr[1024];
va_start(ap, format);
vsnprintf(buf, 1024, format, ap);
va_end(ap);
snprintf(bufr, 1024, "%s at %#lx", buf, ftell(f));
if (fatal)
throw std::runtime_error(bufr);
else fprintf(stderr, "CSMFReader W: %s.\n", bufr);
}
uint8_t CSMFReader::read_u8()
{
uint8_t ret = 0;
int t = fgetc(f);
if (!~t)
error(1, "Unexpected EOF");
ret = (uint8_t)t;
return ret;
}
uint16_t CSMFReader::read_u16()
{
uint16_t ret = 0;
size_t sz = fread(&ret, 2, 1, f);
if (sz < 1)
error(1, "Unexpected EOF");
#if defined(_MSC_VER)&&defined(_WIN32)
ret = _byteswap_ushort(ret);
#elif __BYTE_ORDER__==__ORDER_LITTLE_ENDIAN__
ret = __builtin_bswap16(ret);
#endif
return ret;
}
uint32_t CSMFReader::read_u32()
{
uint32_t ret = 0;
size_t sz = fread(&ret, 4, 1, f);
if (sz < 1)
error(1, "Unexpected EOF");
#if defined(_MSC_VER)&&defined(_WIN32)
ret = _byteswap_ulong(ret);
#elif __BYTE_ORDER__==__ORDER_LITTLE_ENDIAN__
ret = __builtin_bswap32(ret);
#endif
return ret;
}
uint32_t CSMFReader::read_varlen()
{
uint32_t ret = 0, c = 0;
int t;
do
{
t = fgetc(f);
if (!~t)
error(1, "Unexpected EOF");
if (++c > 4)
error(1, "Variable length type overflow");
ret <<= 7;
ret |= (t & 0x7F);
} while (t & 0x80);
return ret;
}
int CSMFReader::read_event()//returns 0 if End of Track encountered
{
uint32_t delta = read_varlen();
curt += delta;
uint8_t type = read_u8();
uint32_t p1, p2;
static uint8_t lasttype;
eventdiscarded = false;
if (!(type & 0x80))
{
fseek(f, -1, SEEK_CUR);
type = lasttype;
}
switch (type & 0xF0)
{
case 0x80://Note Off
case 0x90://Note On
case 0xA0://Note Aftertouch
case 0xB0://Controller Change
case 0xE0://Pitch wheel
p1 = read_u8();
p2 = read_u8();
curTrack->appendEvent(SEvent(curid, curt, type, p1, p2));
break;
case 0xC0://Patch Change
case 0xD0://Channel Aftertouch
p1 = read_u8();
curTrack->appendEvent(SEvent(curid, curt, type, p1, 0));
break;
case 0xF0:
if ((type & 0x0F) == 0x0F) //Meta Event
{
uint8_t metatype = read_u8();
uint32_t len = read_varlen();
char *str = nullptr;
if (len <= 1024 && len > 0)
str = new char[len + 8];
if (str)
fread(str, 1, len, f);
else
fseek(f, len, SEEK_CUR);
std::string sstr;
if (str)
{
str[len] = '\0';
sstr = std::string(str, len);
}
switch (metatype)
{
case 0x00://Sequence Number
assert(len == 2 || len == 0);
break;
case 0x20://Channel Prefix
assert(len == 1);
break;
case 0x2F://End of Track
assert(len == 0);
return 0;
case 0x51://Set Tempo
assert(len == 3);
curTrack->appendEvent(SEvent(curid, curt, type, metatype, 0, sstr));
break;
case 0x54://SMTPE offset, not handled.
assert(len == 5);
break;
case 0x58://Time signature
assert(len == 4);
curTrack->appendEvent(SEvent(curid, curt, type, metatype, 0, sstr));
break;
case 0x59://Key signature
assert(len == 2);
curTrack->appendEvent(SEvent(curid, curt, type, metatype, 0, sstr));
break;
case 0x01:
case 0x02:
case 0x03:
case 0x04:
case 0x05:
case 0x06:
case 0x07:
case 0x7F:
default://text-like meta
{
curTrack->appendEvent(SEvent(curid, curt, type, metatype, 0, sstr));
if (str && metatype == 0x03 && !ret->title)
{
ret->title = new char[len + 8];
strcpy(ret->title, str);
}
if (str && metatype == 0x02 && !ret->copyright)
{
ret->copyright = new char[len + 8];
strcpy(ret->copyright, str);
}
}
}
if (str)
delete[] str;
}
else if ((type & 0x0F) == 0x00 || (type & 0x0F) == 0x07) //SysEx
{
uint32_t len = read_varlen();
char *str = nullptr;
str = new char[len + 8];
if ((type & 0x0F) == 0x00)
{
str[0] = char(0xF0);
++len;
size_t sz = fread(str + 1, 1, len - 1, f);
if (sz < len - 1)
error(1, "Unexpected EOF");
}
else
{
size_t sz = fread(str, 1, len, f);
if (sz < len)
error(1, "Unexpected EOF");
}
curTrack->appendEvent(SEvent(curid, curt, type, 0, 0, std::string(str, len)));
if (!strcmp(str, GM1SysX))
ret->std = 1;
if (!strcmp(str, GM2SysX))
ret->std = 2;
if (!strcmp(str, GSSysEx))
ret->std = 3;
if (!strcmp(str, XGSysEx))
ret->std = 4;
delete[] str;
}
else
error(0, "Unknown event type %#x", type);
break;
default:
error(0, "Unknown event type %#x", type);
}
lasttype = type;
++curid;
if (curTrack->eventList.size())
{
SEvent &le = curTrack->eventList.back();
CMidiPlayer::getInstance()->callEventReaderCB(le);
}
return 1;
}
void CSMFReader::read_track()
{
ret->tracks.push_back(CMidiTrack());
curTrack = &ret->tracks.back();
uint32_t chnklen = read_u32();
byteread = ftell(f);
curt = 0;
curid = 0;
while (read_event());
byteread = ftell(f) - byteread;
if (byteread < chnklen)
{
error(0, "Extra bytes after EOT event");
for (; byteread < chnklen; ++byteread)
fgetc(f);
}
if (byteread > chnklen)
error(1, "Read past end of track");
}
void CSMFReader::read_header()
{
uint32_t chnklen = read_u32();
byteread = ftell(f);
if (chnklen < 6)
error(1, "Header chunk too short");
if (chnklen > 6)
error(0, "Header chunk length longer than expected. Ignoring extra bytes");
fmt = read_u16();
trk = read_u16();
ret->divs = read_u16();
if (ret->divs & 0x8000)
error(1, "SMTPE format is not supported");
for (byteread = ftell(f) - byteread; byteread < chnklen; ++byteread)
fgetc(f);
}
uint32_t CSMFReader::read_chunk(int is_header)
{
char hdr[6];
fread(hdr, 1, 4, f);
if (feof(f))
error(1, "Unexpected EOF");
if (is_header)
{
if (!strncmp(hdr, "RIFF", 4))
{
fseek(f, 4, SEEK_CUR);
fread(hdr, 1, 4, f);
if (strncmp(hdr, "RMID", 4))
error(1, "Wrong file type in RIFF container");
fseek(f, 8, SEEK_CUR);
fread(hdr, 1, 4, f);
}
if (strncmp(hdr, "MThd", 4))
error(1, "Wrong MIDI header.");
else return read_header(), 0;
}
else if (strncmp(hdr, "MTrk", 4))
{
error(0, "Wrong track chunk header. Ignoring the entire chunk.");
uint32_t chnklen = read_u32();
fseek(f, chnklen, SEEK_CUR);
return 0;
}
else
return read_track(), 1;
return 0;
}
CSMFReader::CSMFReader()
{
f = nullptr;
}
CMidiFile *CSMFReader::readFile(const char *fn)
{
ret = new CMidiFile;
ret->title = ret->copyright = nullptr;
ret->std = 0;
ret->valid = 1;
try
{
if (!(f = fopen(fn, "rb")))
throw std::runtime_error("Can't open file");
read_chunk(1);
for (uint32_t i = 0; i < trk; i += read_chunk(0));
fclose(f);
f = nullptr;
}
catch (std::runtime_error &e)
{
fprintf(stderr, "CSMFReader E: %s is not a supported file. Cause: %s.\n", fn, e.what());
ret->valid = 0;
if (f)
fclose(f);
f = nullptr;
}
return ret;
}
CSMFReader::~CSMFReader()
{
}
void CSMFReader::discardCurrentEvent()
{
if (eventdiscarded)
return;
eventdiscarded = true;
curTrack->eventList.pop_back();
}
void CSMFReader::commitEventChange(SEvent d)
{
curTrack->eventList.back().time = d.time;
curTrack->eventList.back().type = d.type;
curTrack->eventList.back().p1 = d.p1;
curTrack->eventList.back().p2 = d.p2;
}
CMidiFileReaderCollection::CMidiFileReaderCollection()
{
readers.clear();
currentReader = nullptr;
registerReader(new CSMFReader(), "Default SMF Reader");
}
CMidiFileReaderCollection::~CMidiFileReaderCollection()
{
delete readers[0].first;
}
void CMidiFileReaderCollection::registerReader(qmpFileReader *reader, std::string name)
{
for (unsigned i = 0; i < readers.size(); ++i)
if (readers[i].second == name)
return;
readers.push_back(std::make_pair(reader, name));
}
void CMidiFileReaderCollection::unregisterReader(std::string name)
{
for (auto i = readers.begin(); i != readers.end(); ++i)
if (i->second == name)
{
readers.erase(i);
return;
}
}
CMidiFile *CMidiFileReaderCollection::readFile(const char *fn)
{
CMidiFile *file = nullptr;
for (unsigned i = 0; i < readers.size(); ++i)
{
currentReader = readers[i].first;
CMidiPlayer::getInstance()->notes = 0;
CMidiFile *t = readers[i].first->readFile(fn);
if (t->valid)
{
file = t;
break;
}
else
delete t;
}
currentReader = nullptr;
return file;
}
qmpFileReader *CMidiFileReaderCollection::getCurrentReader()
{
return currentReader;
}