@logue/smfplayer
Version:
smfplayer.js is JavaScript based Standard Midi Player for WebMidiLink based synthesizer.
421 lines (392 loc) • 11.8 kB
JavaScript
import { ChannelEvent, SystemExclusiveEvent, MetaEvent } from './midi_event';
import Meta from './meta';
import Riff from './riff';
/**
* Standard Midi File Parser class
*/
export default class SMF {
/**
* @param {ByteArray} input input buffer.
* @param {Object=} optParams option parameters.
*/
constructor(input, optParams = {}) {
optParams.padding = false;
optParams.bigEndian = true;
/** @type {ByteArray} */
this.input = input;
/** @type {number} */
this.ip = optParams.index || 0;
/** @type {number} */
this.chunkIndex = 0;
/**
* @type {Riff}
* @private
*/
this.riffParser_ = new Riff(input, optParams);
// MIDI File Information
/** @type {number} */
this.formatType = 0;
/** @type {number} */
this.numberOfTracks = 0;
/** @type {number} */
this.timeDivision = 480;
/** @type {Array.<Array.<Midi.Event>>} */
this.tracks = [];
/** @type {Array.<Array.<ByteArray>>} */
this.plainTracks = [];
/** @type {number} */
this.version = Meta.version;
/** @type {string} */
this.build = Meta.build;
}
/**
*/
parse() {
/** @type {number} */
let i = 0;
/** @type {number} */
let il = 0;
// parse riff chunks
this.riffParser_.parse();
// parse header chunk
this.parseHeaderChunk();
// parse track chunks
for (i = 0, il = this.numberOfTracks; i < il; ++i) {
this.parseTrackChunk();
}
}
/**
*/
parseHeaderChunk() {
/** @type {?{type: string, size: number, offset: number}} */
const chunk = this.riffParser_.getChunk(this.chunkIndex++);
/** @type {ByteArray} */
const data = this.input;
/** @type {number} */
let ip = chunk.offset;
if (!chunk || chunk.type !== 'MThd') {
throw new Error('invalid header signature');
}
this.formatType = (data[ip++] << 8) | data[ip++];
this.numberOfTracks = (data[ip++] << 8) | data[ip++];
this.timeDivision = (data[ip++] << 8) | data[ip++];
}
/**
*/
parseTrackChunk() {
/** @type {?{type: string, size: number, offset: number}} */
const chunk = this.riffParser_.getChunk(this.chunkIndex++);
/** @type {ByteArray} */
const data = this.input;
/** @type {number} */
let ip = chunk.offset;
/** @type {number} */
let size = 0;
/** @type {number} */
let deltaTime = 0;
/** @type {number} */
let eventType = 0;
/** @type {number} */
let channel = 0;
/** @type {number} */
let prevEventType = -1;
/** @type {number} */
let prevChannel = -1;
/** @type {number} */
let tmp = 0;
/** @type {number} */
let totalTime = 0;
/** @type {number} */
let offset = 0;
/** @type {number} */
let length = 0;
/** @type {number} */
let status = 0;
/** @type {Event} */
let event;
/** @type {ByteArray} */
let plainBytes;
/** @return {number} */
const readNumber = () => {
/** @type {number} */
let result = 0;
tmp = 0;
do {
tmp = data[ip++];
result = (result << 7) | (tmp & 0x7f);
} while ((tmp & 0x80) !== 0);
return result;
};
if (!chunk || chunk.type !== 'MTrk') {
throw new Error('invalid header signature');
}
size = chunk.offset + chunk.size;
const eventQueue = [];
const plainQueue = [];
while (ip < size) {
// delta time
deltaTime = readNumber();
totalTime += deltaTime;
// offset
offset = ip;
// event type value, midi channel
status = data[ip++];
eventType = (status >> 4) & 0xf;
channel = status & 0xf;
// run status rule
if (eventType < 8) {
eventType = prevEventType;
channel = prevChannel;
status = (prevEventType << 4) | prevChannel;
ip--;
offset--;
} else {
prevEventType = eventType;
prevChannel = channel;
}
// TODO
const table = [
null,
null,
null,
null,
null,
null,
null,
null,
'NoteOff', // 0x8
'NoteOn',
'NoteAftertouch',
'ControlChange',
'ProgramChange',
'ChannelAftertouch',
'PitchBend',
];
switch (eventType) {
// channel events
case 0x8:
/* FALLTHROUGH */
case 0x9:
/* FALLTHROUGH */
case 0xa:
/* FALLTHROUGH */
case 0xb:
/* FALLTHROUGH */
case 0xd:
/* FALLTHROUGH */
case 0xe:
event = new ChannelEvent(
table[eventType],
deltaTime,
totalTime,
channel,
data[ip++],
data[ip++]
);
break;
case 0xc:
event = new ChannelEvent(
table[eventType],
deltaTime,
totalTime,
channel,
data[ip++]
);
break;
// meta events, system exclusive event
case 0xf:
switch (channel) {
// SysEx event
case 0x0:
tmp = readNumber();
if (data[ip + tmp - 1] !== 0xf7) {
throw new Error('invalid SysEx event');
}
event = new SystemExclusiveEvent(
'SystemExclusive',
deltaTime,
totalTime,
data.subarray(ip, (ip += tmp) - 1)
);
break;
case 0x7:
tmp = readNumber();
event = new SystemExclusiveEvent(
'SystemExclusive(F7)',
deltaTime,
totalTime,
data.subarray(ip, (ip += tmp))
);
break;
// meta event
case 0xf:
eventType = data[ip++];
tmp = readNumber();
switch (eventType) {
case 0x00: // sequence number
event = new MetaEvent(
'SequenceNumber',
deltaTime,
totalTime,
[data[ip++], data[ip++]]
);
break;
case 0x01: // text event
event = new MetaEvent('TextEvent', deltaTime, totalTime, [
String.fromCharCode.apply(
null,
data.subarray(ip, (ip += tmp))
),
]);
break;
case 0x02: // copyright notice
event = new MetaEvent(
'CopyrightNotice',
deltaTime,
totalTime,
[
String.fromCharCode.apply(
null,
data.subarray(ip, (ip += tmp))
),
]
);
break;
case 0x03: // sequence/track name
event = new MetaEvent(
'SequenceTrackName',
deltaTime,
totalTime,
[
String.fromCharCode.apply(
null,
data.subarray(ip, (ip += tmp))
),
]
);
break;
case 0x04: // instrument name
event = new MetaEvent(
'InstrumentName',
deltaTime,
totalTime,
[
String.fromCharCode.apply(
null,
data.subarray(ip, (ip += tmp))
),
]
);
break;
case 0x05: // lyrics
event = new MetaEvent('Lyrics', deltaTime, totalTime, [
String.fromCharCode.apply(
null,
data.subarray(ip, (ip += tmp))
),
]);
break;
case 0x06: // marker
event = new MetaEvent('Marker', deltaTime, totalTime, [
String.fromCharCode.apply(
null,
data.subarray(ip, (ip += tmp))
),
]);
break;
case 0x07: // cue point
event = new MetaEvent('CuePoint', deltaTime, totalTime, [
String.fromCharCode.apply(
null,
data.subarray(ip, (ip += tmp))
),
]);
break;
case 0x20: // midi channel prefix
event = new MetaEvent(
'MidiChannelPrefix',
deltaTime,
totalTime,
[data[ip++]]
);
break;
case 0x2f: // end of track
event = new MetaEvent('EndOfTrack', deltaTime, totalTime, []);
break;
case 0x51: // set tempo
event = new MetaEvent('SetTempo', deltaTime, totalTime, [
(data[ip++] << 16) | (data[ip++] << 8) | data[ip++],
]);
break;
case 0x54: // smpte offset
event = new MetaEvent('SmpteOffset', deltaTime, totalTime, [
data[ip++],
data[ip++],
data[ip++],
data[ip++],
data[ip++],
]);
break;
case 0x58: // time signature
event = new MetaEvent('TimeSignature', deltaTime, totalTime, [
data[ip++],
data[ip++],
data[ip++],
data[ip++],
]);
break;
case 0x59: // key signature
event = new MetaEvent('KeySignature', deltaTime, totalTime, [
data[ip++],
data[ip++],
]);
break;
case 0x7f: // sequencer specific
event = new MetaEvent(
'SequencerSpecific',
deltaTime,
totalTime,
[data.subarray(ip, (ip += tmp))]
);
break;
default:
// unknown
event = new MetaEvent('Unknown', deltaTime, totalTime, [
eventType,
data.subarray(ip, (ip += tmp)),
]);
}
break;
default:
console.warn('unknown message:', status.toString(16));
}
break;
// error
default:
throw new Error('invalid status');
}
// plain queue
length = ip - offset;
plainBytes = data.subarray(offset, offset + length);
plainBytes[0] = status;
if (
event instanceof ChannelEvent &&
event.subtype === 'NoteOn' &&
/** @type {ChannelEvent} */
(event).parameter2 === 0
) {
event.subtype = table[8];
plainBytes = new Uint8Array([
0x80 | event.channel,
event.parameter1,
event.parameter2,
]);
}
plainQueue.push(plainBytes);
// event queue
eventQueue.push(event);
}
this.tracks.push(eventQueue);
this.plainTracks.push(plainQueue);
}
}