midifile
Version:
Read/write standard MIDI files.
597 lines (553 loc) • 20.9 kB
JavaScript
;
var fs = require('fs');
var assert = require('assert');
var path = require('path');
var MIDIFile = require('../src/MIDIFile.js');
var MIDIFileHeader = require('../src/MIDIFileHeader.js');
var MIDIEvents = require('midievents');
// Tests
describe('Reading well formed MIDI files', function() {
it('Format 0 MIDI file', () =>
readFileAsArrayBuffer(path.join('sounds', 'MIDIOkFormat0.mid')).then(
data => {
var events;
var mF = new MIDIFile(data);
assert.equal(mF.header.getFormat(), 0);
assert.equal(mF.header.getTracksCount(), 1);
assert.equal(
mF.header.getTimeDivision(),
MIDIFileHeader.TICKS_PER_BEAT
);
assert.equal(mF.header.getTicksPerBeat(), 96);
assert.equal(mF.tracks.length, 1);
assert.equal(mF.tracks[0].getTrackLength(), 59);
events = mF.tracks[0].getTrackContent();
assert.equal(events.buffer.byteLength, 81);
assert.equal(events.byteLength, 59);
assert.equal(events.byteOffset, 22);
// Check events retrieving
events = mF.getMidiEvents();
assert.equal(events.length, 11);
assert.equal(events[0].type, MIDIEvents.EVENT_MIDI);
assert.equal(events[0].subtype, MIDIEvents.EVENT_MIDI_PROGRAM_CHANGE);
assert.equal(events[0].index, 0x25);
assert.equal(events[0].delta, 0);
assert.equal(events[0].channel, 0);
assert.equal(events[0].param1, 0x5);
assert.equal(events[1].type, MIDIEvents.EVENT_MIDI);
assert.equal(events[1].subtype, MIDIEvents.EVENT_MIDI_PROGRAM_CHANGE);
assert.equal(events[1].index, 0x28);
assert.equal(events[1].delta, 0);
assert.equal(events[1].channel, 1);
assert.equal(events[1].param1, 0x2e);
assert.equal(events[2].type, MIDIEvents.EVENT_MIDI);
assert.equal(events[2].subtype, MIDIEvents.EVENT_MIDI_PROGRAM_CHANGE);
assert.equal(events[2].index, 0x2b);
assert.equal(events[2].delta, 0);
assert.equal(events[2].channel, 2);
assert.equal(events[2].param1, 0x46);
assert.equal(events[3].type, MIDIEvents.EVENT_MIDI);
assert.equal(events[3].subtype, MIDIEvents.EVENT_MIDI_NOTE_ON);
assert.equal(events[3].index, 0x2e);
assert.equal(events[3].delta, 0);
assert.equal(events[3].channel, 2);
assert.equal(events[3].param1, 0x30);
assert.equal(events[3].param2, 0x60);
assert.equal(events[4].type, MIDIEvents.EVENT_MIDI);
assert.equal(events[4].subtype, MIDIEvents.EVENT_MIDI_NOTE_ON);
assert.equal(events[4].index, 0x32);
assert.equal(events[4].delta, 0);
assert.equal(events[4].channel, 2);
assert.equal(events[4].param1, 0x3c);
assert.equal(events[4].param2, 0x60);
assert.equal(events[5].type, MIDIEvents.EVENT_MIDI);
assert.equal(events[5].subtype, MIDIEvents.EVENT_MIDI_NOTE_ON);
assert.equal(events[5].index, 0x35);
assert.equal(events[5].delta, 0x60);
assert.equal(events[5].channel, 1);
assert.equal(events[5].param1, 0x43);
assert.equal(events[5].param2, 0x40);
assert.equal(events[6].type, MIDIEvents.EVENT_MIDI);
assert.equal(events[6].subtype, MIDIEvents.EVENT_MIDI_NOTE_ON);
assert.equal(events[6].index, 0x39);
assert.equal(events[6].delta, 0x60);
assert.equal(events[6].channel, 0);
assert.equal(events[6].param1, 0x4c);
assert.equal(events[6].param2, 0x20);
assert.equal(events[7].type, MIDIEvents.EVENT_MIDI);
assert.equal(events[7].subtype, MIDIEvents.EVENT_MIDI_NOTE_OFF);
assert.equal(events[7].index, 0x3d);
assert.equal(events[7].delta, 192); // 2 bytes delta time
assert.equal(events[7].channel, 2);
assert.equal(events[7].param1, 0x30);
assert.equal(events[7].param2, 0x40);
assert.equal(events[8].type, MIDIEvents.EVENT_MIDI);
assert.equal(events[8].subtype, MIDIEvents.EVENT_MIDI_NOTE_OFF);
assert.equal(events[8].index, 0x42);
assert.equal(events[8].delta, 0x00);
assert.equal(events[8].channel, 2);
assert.equal(events[8].param1, 0x3c);
assert.equal(events[8].param2, 0x40);
assert.equal(events[9].type, MIDIEvents.EVENT_MIDI);
assert.equal(events[9].subtype, MIDIEvents.EVENT_MIDI_NOTE_OFF);
assert.equal(events[9].index, 0x45);
assert.equal(events[9].delta, 0x00);
assert.equal(events[9].channel, 1);
assert.equal(events[9].param1, 0x43);
assert.equal(events[9].param2, 0x40);
assert.equal(events[10].type, MIDIEvents.EVENT_MIDI);
assert.equal(events[10].subtype, MIDIEvents.EVENT_MIDI_NOTE_OFF);
assert.equal(events[10].index, 0x49);
assert.equal(events[10].delta, 0x00);
assert.equal(events[10].channel, 0);
assert.equal(events[10].param1, 0x4c);
assert.equal(events[10].param2, 0x40);
}
));
it('Format 1 MIDI file', () =>
readFileAsArrayBuffer(path.join('sounds', 'MIDIOkFormat1.mid')).then(
data => {
var events;
var mF = new MIDIFile(data);
assert.equal(mF.header.getFormat(), 1);
assert.equal(mF.header.getTracksCount(), 4);
assert.equal(
mF.header.getTimeDivision(),
MIDIFileHeader.TICKS_PER_BEAT
);
assert.equal(mF.header.getTicksPerBeat(), 96);
assert.equal(mF.tracks.length, 4);
// Track 1
assert.equal(mF.tracks[0].getTrackLength(), 19);
events = mF.tracks[0].getTrackContent();
assert.equal(events.buffer.byteLength, 117);
assert.equal(events.byteLength, 19);
assert.equal(events.byteOffset, 22);
// Track 2
assert.equal(mF.tracks[1].getTrackLength(), 16);
events = mF.tracks[1].getTrackContent();
assert.equal(events.buffer.byteLength, 117);
assert.equal(events.byteLength, 16);
assert.equal(events.byteOffset, 49);
// Track 3
assert.equal(mF.tracks[2].getTrackLength(), 15);
events = mF.tracks[2].getTrackContent();
assert.equal(events.buffer.byteLength, 117);
assert.equal(events.byteLength, 15);
assert.equal(events.byteOffset, 73);
// Track 4
assert.equal(mF.tracks[3].getTrackLength(), 21);
events = mF.tracks[3].getTrackContent();
assert.equal(events.buffer.byteLength, 117);
assert.equal(events.byteLength, 21);
assert.equal(events.byteOffset, 96);
}
));
it('Format 2 MIDI file', () =>
readFileAsArrayBuffer(path.join('sounds', 'MIDIOkFormat2.mid')).then(
data => {
var events;
var mF = new MIDIFile(data);
assert.equal(mF.header.getFormat(), 2);
assert.equal(mF.header.getTracksCount(), 9);
assert.equal(
mF.header.getTimeDivision(),
MIDIFileHeader.TICKS_PER_BEAT
);
assert.equal(mF.header.getTicksPerBeat(), 96);
assert.equal(mF.tracks.length, 9);
// Track 1
assert.equal(mF.tracks[0].getTrackLength(), 24);
events = mF.tracks[0].getTrackContent();
assert.equal(events.buffer.byteLength, 270);
assert.equal(events.byteLength, 24);
assert.equal(events.byteOffset, 22);
// Track 2
assert.equal(mF.tracks[1].getTrackLength(), 20);
events = mF.tracks[1].getTrackContent();
assert.equal(events.buffer.byteLength, 270);
assert.equal(events.byteLength, 20);
assert.equal(events.byteOffset, 54);
// Track 3
assert.equal(mF.tracks[2].getTrackLength(), 20);
events = mF.tracks[2].getTrackContent();
assert.equal(events.buffer.byteLength, 270);
assert.equal(events.byteLength, 20);
assert.equal(events.byteOffset, 82);
// Track 4
assert.equal(mF.tracks[3].getTrackLength(), 20);
events = mF.tracks[3].getTrackContent();
assert.equal(events.buffer.byteLength, 270);
assert.equal(events.byteLength, 20);
assert.equal(events.byteOffset, 110);
// Track 5
assert.equal(mF.tracks[4].getTrackLength(), 20);
events = mF.tracks[4].getTrackContent();
assert.equal(events.buffer.byteLength, 270);
assert.equal(events.byteLength, 20);
assert.equal(events.byteOffset, 138);
// Track 6
assert.equal(mF.tracks[5].getTrackLength(), 20);
events = mF.tracks[5].getTrackContent();
assert.equal(events.buffer.byteLength, 270);
assert.equal(events.byteLength, 20);
assert.equal(events.byteOffset, 166);
// Track 7
assert.equal(mF.tracks[6].getTrackLength(), 20);
events = mF.tracks[6].getTrackContent();
assert.equal(events.buffer.byteLength, 270);
assert.equal(events.byteLength, 20);
assert.equal(events.byteOffset, 194);
// Track 8
assert.equal(mF.tracks[7].getTrackLength(), 20);
events = mF.tracks[7].getTrackContent();
assert.equal(events.buffer.byteLength, 270);
assert.equal(events.byteLength, 20);
assert.equal(events.byteOffset, 222);
// Track 9
assert.equal(mF.tracks[8].getTrackLength(), 20);
events = mF.tracks[8].getTrackContent();
assert.equal(events.buffer.byteLength, 270);
assert.equal(events.byteLength, 20);
assert.equal(events.byteOffset, 250);
}
));
it('Karaoke MIDI file', () =>
readFileAsArrayBuffer(path.join('sounds', 'MIDIOkFormat1-lyrics.mid')).then(
data => {
var events;
var lyrics;
var mF = new MIDIFile(data);
assert.equal(mF.header.getFormat(), 1);
assert.equal(mF.header.getTracksCount(), 1);
assert.equal(
mF.header.getTimeDivision(),
MIDIFileHeader.TICKS_PER_BEAT
);
assert.equal(mF.header.getTicksPerBeat(), 96);
assert.equal(mF.tracks.length, 1);
assert.equal(mF.tracks[0].getTrackLength(), 109);
events = mF.tracks[0].getTrackContent();
assert.equal(events.buffer.byteLength, 131);
assert.equal(events.byteLength, 109);
assert.equal(events.byteOffset, 22);
// Reading lyrics
lyrics = mF.getLyrics();
assert.equal(lyrics[0].text, 'He');
assert.equal(lyrics[0].playTime, 0);
assert.equal(lyrics[1].text, 'llo');
assert.equal(Math.floor(lyrics[1].playTime), 666);
assert.equal(lyrics[2].text, '\\Ka');
assert.equal(Math.floor(lyrics[1].playTime), 666);
assert.equal(lyrics[3].text, 'ra');
assert.equal(Math.floor(lyrics[1].playTime), 666);
assert.equal(lyrics[4].text, 'o');
assert.equal(Math.floor(lyrics[1].playTime), 666);
assert.equal(lyrics[5].text, 'ke');
assert.equal(Math.floor(lyrics[1].playTime), 666);
}
));
it('Real world MIDI file : Mountain Man', () =>
readFileAsArrayBuffer(
path.join('sounds', 'Avgvst', 'MountainMan.mid')
).then(data => {
var events;
var mF = new MIDIFile(data);
assert.equal(mF.header.getFormat(), 0);
assert.equal(mF.header.getTracksCount(), 1);
assert.equal(mF.header.getTimeDivision(), MIDIFileHeader.TICKS_PER_BEAT);
assert.equal(mF.header.getTicksPerBeat(), 192);
assert.equal(mF.tracks.length, 1);
assert.equal(mF.tracks[0].getTrackLength(), 47411);
events = mF.tracks[0].getTrackContent();
assert.equal(events.buffer.byteLength, 47433);
assert.equal(events.byteLength, 47411);
assert.equal(events.byteOffset, 22);
}));
it('MIDI data from a Node.js buffer', function() {
var data = 'TVRoZAAAAAYAAQABAeBNVHJrAAAAFAD/BAAAwAAAkEVVkmCARVUA/y8A';
new MIDIFile(Buffer.from(data, 'base64'));
});
});
describe('Reading invalid MIDI files', function() {
it('Should fail when the header chunk is bad', () =>
readFileAsArrayBuffer(path.join('sounds', 'MIDIBadHeaderChunk.mid')).then(
data => {
assert.throws(function() {
new MIDIFile(data);
});
}
));
it('Should fail when the MIDI format is invalid', () =>
readFileAsArrayBuffer(path.join('sounds', 'MIDIBadHeaderFormat.mid')).then(
data => {
assert.throws(function() {
var mF = new MIDIFile(data);
mF.header.getFormat();
});
}
));
it('Should fail when the header chunk is bad', () =>
readFileAsArrayBuffer(path.join('sounds', 'MIDIBadHeaderChunk.mid')).then(
data => {
assert.throws(function() {
new MIDIFile(data);
});
}
));
it('Should fail when the header length is bad', () =>
readFileAsArrayBuffer(path.join('sounds', 'MIDIBadHeaderLength.mid')).then(
data => {
assert.throws(function() {
var mF = new MIDIFile(data);
mF.getFormat();
});
}
));
it('Should fail when the header chunk is bad', () =>
readFileAsArrayBuffer(path.join('sounds', 'MIDIBadHeaderSMTPE.mid')).then(
data => {
assert.throws(function() {
var mF = new MIDIFile(data);
mF.header.getSMPTEFrames();
});
}
));
it('Should fail when tracks count is not corresponding to the real track count', () =>
readFileAsArrayBuffer(
path.join('sounds', 'MIDIBadHeaderTracksNum.mid')
).then(data => {
assert.throws(function() {
new MIDIFile(data, true);
});
}));
it('Should fail when the track header is bad', () =>
readFileAsArrayBuffer(path.join('sounds', 'MIDIBadTrackHdr.mid')).then(
data => {
assert.throws(function() {
new MIDIFile(data);
});
}
));
it('Should fail when the track length is bad', () =>
readFileAsArrayBuffer(path.join('sounds', 'MIDIBadTrackLength.mid')).then(
data => {
assert.throws(function() {
new MIDIFile(data);
});
}
));
});
describe('Reading malformed MIDI files in strict mode', function() {
it('Should fail when tracks count is not corresponding to the real track count', () =>
readFileAsArrayBuffer(
path.join('sounds', 'MIDIBadHeaderTracksNum.mid')
).then(data => {
assert.throws(function() {
new MIDIFile(data, true);
});
}));
});
describe('Calculating required buffer length ', function() {
it('Should work with events of Format 0 MIDI file', () =>
readFileAsArrayBuffer(path.join('sounds', 'MIDIOkFormat0.mid')).then(
data => {
var events;
var mF = new MIDIFile(data);
events = mF.getTrackEvents(0);
// -2 bytes for running statuses
assert.equal(
MIDIEvents.getRequiredBufferLength(events) - 2,
mF.tracks[0].getTrackLength()
);
}
));
it('Should work with events of Format 1 MIDI file', () =>
readFileAsArrayBuffer(path.join('sounds', 'MIDIOkFormat1.mid')).then(
data => {
var events;
var mF = new MIDIFile(data);
events = mF.getTrackEvents(0);
assert.equal(
MIDIEvents.getRequiredBufferLength(events),
mF.tracks[0].getTrackLength()
);
events = mF.getTrackEvents(1);
// -1 byte for running status
assert.equal(
MIDIEvents.getRequiredBufferLength(events) - 1,
mF.tracks[1].getTrackLength()
);
events = mF.getTrackEvents(2);
// -1 byte for running statuses
assert.equal(
MIDIEvents.getRequiredBufferLength(events) - 1,
mF.tracks[2].getTrackLength()
);
events = mF.getTrackEvents(3);
// -3 bytes for running statuses
assert.equal(
MIDIEvents.getRequiredBufferLength(events) - 3,
mF.tracks[3].getTrackLength()
);
}
));
it('Should work with events of Format 1 MIDI file', () =>
readFileAsArrayBuffer(path.join('sounds', 'MIDIOkFormat1-lyrics.mid')).then(
data => {
var events;
var mF = new MIDIFile(data);
events = mF.getTrackEvents(0);
assert.equal(
MIDIEvents.getRequiredBufferLength(events),
mF.tracks[0].getTrackLength()
);
}
));
it('Should work with events of Format 2 MIDI file', () =>
readFileAsArrayBuffer(path.join('sounds', 'MIDIOkFormat2.mid')).then(
data => {
var events;
var mF = new MIDIFile(data);
events = mF.getTrackEvents(0);
assert.equal(
MIDIEvents.getRequiredBufferLength(events),
mF.tracks[0].getTrackLength()
);
}
));
});
describe('MIDI file', function() {
var mF;
var events;
it('creation should work', function() {
mF = new MIDIFile();
});
it('ticks per beat setting should work', function() {
mF.header.setTicksPerBeat(128);
});
it('Track addition should work', function() {
mF.addTrack(1); // at the end by specifying an index
mF.addTrack(); // at the end
mF.addTrack(1); // in the middle
assert.equal(mF.tracks.length, mF.header.getTracksCount());
assert.equal(mF.tracks.length, 4);
});
it('Track deletion should work', function() {
mF.deleteTrack(1);
assert.equal(mF.tracks.length, mF.header.getTracksCount());
assert.equal(mF.tracks.length, 3);
});
it('Track events basic reading should work', function() {
events = mF.getTrackEvents(1);
assert.equal(events.length, 1);
});
});
describe('MIDI file reencryption loop should work', function() {
it('with the format 0 MIDI file', () =>
encodingLoop(path.join('sounds', 'MIDIOkFormat0.mid')));
it('with the format 1 MIDI file', () =>
encodingLoop(path.join('sounds', 'MIDIOkFormat1.mid')));
it('with the format 2 MIDI file', () =>
encodingLoop(path.join('sounds', 'MIDIOkFormat2.mid')));
if ('undefined' === typeof __karma__) {
it('with the Avgvst MIDI files', function() {
this.timeout(10000);
return dirEncodingLoop('Avgvst');
});
it('with the Avgvst MIDI files', function() {
this.timeout(10000);
return dirEncodingLoop('GerardoMarset');
});
it('with the Avgvst MIDI files', function() {
this.timeout(10000);
return dirEncodingLoop('HorrorPen');
});
it('with the Avgvst MIDI files', function() {
this.timeout(10000);
return dirEncodingLoop('Yubatake');
});
}
});
function readFileAsArrayBuffer(file) {
var XMLHttpRequest =
// eslint-disable-next-line
'undefined' !== typeof global.XMLHttpRequest && global.XMLHttpRequest;
if (XMLHttpRequest) {
return new Promise((resolve, reject) => {
const req = new XMLHttpRequest();
req.open('GET', '/base/' + file, true);
req.responseType = 'arraybuffer';
req.onload = function() {
if (req.status !== 200) {
reject(new Error('E_NOT_FOUND'));
}
resolve(req.response);
};
req.onerror = function(err) {
reject(err);
};
req.send(null);
});
}
return new Promise((resolve, reject) => {
fs.readFile(path.join(__dirname, '..', file), function(err, data) {
if (err) {
reject(err);
return;
}
resolve(toArrayBuffer(data));
});
});
}
function encodingLoop(filePath) {
return readFileAsArrayBuffer(filePath).then(data => {
var mF;
var newMF;
var mFEvents;
var newMFEvents;
mF = new MIDIFile(data);
mF.tracks.forEach(function(track, index) {
var events = mF.getTrackEvents(index);
mF.setTrackEvents(index, events);
});
newMF = new MIDIFile(mF.getContent().slice());
mFEvents = mF.getEvents();
newMFEvents = newMF.getEvents();
assert.equal(mFEvents.length, newMFEvents.length);
// Testing each events
mFEvents.forEach(function(event, index) {
// Remove indexes since it is normal they differs
delete event.index;
delete newMFEvents[index].index;
assert.deepEqual(
event,
newMFEvents[index],
'Same event at index:' + index
);
});
});
}
function dirEncodingLoop(dirName) {
return Promise.all(
fs
.readdirSync(path.join(__dirname, '..', 'sounds', dirName))
.map(function(file) {
if ('README.md' !== file) {
return encodingLoop(path.join('sounds', dirName, file));
}
})
);
}
// Helper to get an ArrayBuffer from a NodeJS buffer
// Borrowed here : http://stackoverflow.com/questions/8609289/convert-a-binary-
// nodejs-buffer-to-javascript-arraybuffer
function toArrayBuffer(buffer) {
var ab = new ArrayBuffer(buffer.length);
var view = new Uint8Array(ab);
var i;
for (i = 0; i < buffer.length; ++i) {
view[i] = buffer[i];
}
return ab;
}