import {Constants} from './constants';
import {Utils} from './utils';
/**
* Class representing a track. Contains methods for parsing events and keeping track of pointer.
*/
class Track {
constructor(index, data) {
this.enabled = true;
this.eventIndex = 0;
this.pointer = 0;
this.lastTick = 0;
this.lastStatus = null;
this.index = index;
this.data = data;
this.delta = 0;
this.runningDelta = 0;
this.events = [];
// Ensure last 3 bytes of track are End of Track event
const lastThreeBytes = this.data.subarray(this.data.length - 3, this.data.length);
if (!(lastThreeBytes[0] === 0xff && lastThreeBytes[1] === 0x2f && lastThreeBytes[2] === 0x00)) {
throw 'Invalid MIDI file; Last three bytes of track ' + this.index + 'must be FF 2F 00 to mark end of track';
}
}
/**
* Resets all stateful track informaion used during playback.
* @return {Track}
*/
reset() {
this.enabled = true;
this.eventIndex = 0;
this.pointer = 0;
this.lastTick = 0;
this.lastStatus = null;
this.delta = 0;
this.runningDelta = 0;
return this;
}
/**
* Sets this track to be enabled during playback.
* @return {Track}
*/
enable() {
this.enabled = true;
return this;
}
/**
* Sets this track to be disabled during playback.
* @return {Track}
*/
disable() {
this.enabled = false;
return this;
}
/**
* Sets the track event index to the nearest event to the given tick.
* @param {number} tick
* @return {Track}
*/
setEventIndexByTick(tick) {
tick = tick || 0;
for (var i in this.events) {
if (this.events[i].tick >= tick) {
this.eventIndex = i;
return this;
}
}
}
/**
* Gets byte located at pointer position.
* @return {number}
*/
getCurrentByte() {
return this.data[this.pointer];
}
/**
* Gets count of delta bytes and current pointer position.
* @return {number}
*/
getDeltaByteCount() {
return Utils.getVarIntLength(this.data.subarray(this.pointer));
}
/**
* Get delta value at current pointer position.
* @return {number}
*/
getDelta() {
return Utils.readVarInt(this.data.subarray(this.pointer, this.pointer + this.getDeltaByteCount()));
}
/**
* Handles event within a given track starting at specified index
* @param {number} currentTick
* @param {boolean} dryRun - If true events will be parsed and returned regardless of time.
*/
handleEvent(currentTick, dryRun) {
dryRun = dryRun || false;
if (dryRun) {
var elapsedTicks = currentTick - this.lastTick;
var delta = this.getDelta();
var eventReady = elapsedTicks >= delta;
if (this.pointer < this.data.length && (dryRun || eventReady)) {
let event = this.parseEvent();
if (this.enabled) return event;
// Recursively call this function for each event ahead that has 0 delta time?
}
} else {
// Let's actually play the MIDI from the generated JSON events created by the dry run.
if (this.events[this.eventIndex] && this.events[this.eventIndex].tick <= currentTick) {
this.eventIndex++;
if (this.enabled) return this.events[this.eventIndex - 1];
}
}
return null;
}
/**
* Get string data from event.
* @param {number} eventStartIndex
* @return {string}
*/
getStringData(eventStartIndex) {
const varIntLength = Utils.getVarIntLength(this.data.subarray(eventStartIndex + 2));
const varIntValue = Utils.readVarInt(this.data.subarray(eventStartIndex + 2, eventStartIndex + 2 + varIntLength));
const letters = Utils.bytesToLetters(this.data.subarray(eventStartIndex + 2 + varIntLength, eventStartIndex + 2 + varIntLength + varIntValue));
return letters;
}
/**
* Parses event into JSON and advances pointer for the track
* @return {object}
*/
parseEvent() {
var eventStartIndex = this.pointer + this.getDeltaByteCount();
var eventJson = {};
var deltaByteCount = this.getDeltaByteCount();
eventJson.track = this.index + 1;
eventJson.delta = this.getDelta();
this.lastTick = this.lastTick + eventJson.delta;
this.runningDelta += eventJson.delta;
eventJson.tick = this.runningDelta;
eventJson.byteIndex = this.pointer;
//eventJson.raw = event;
if (this.data[eventStartIndex] == 0xff) {
// Meta Event
// If this is a meta event we should emit the data and immediately move to the next event
// otherwise if we let it run through the next cycle a slight delay will accumulate if multiple tracks
// are being played simultaneously
switch(this.data[eventStartIndex + 1]) {
case 0x00: // Sequence Number
eventJson.name = 'Sequence Number';
break;
case 0x01: // Text Event
eventJson.name = 'Text Event';
eventJson.string = this.getStringData(eventStartIndex);
break;
case 0x02: // Copyright Notice
eventJson.name = 'Copyright Notice';
break;
case 0x03: // Sequence/Track Name
eventJson.name = 'Sequence/Track Name';
eventJson.string = this.getStringData(eventStartIndex);
break;
case 0x04: // Instrument Name
eventJson.name = 'Instrument Name';
eventJson.string = this.getStringData(eventStartIndex);
break;
case 0x05: // Lyric
eventJson.name = 'Lyric';
eventJson.string = this.getStringData(eventStartIndex);
break;
case 0x06: // Marker
eventJson.name = 'Marker';
break;
case 0x07: // Cue Point
eventJson.name = 'Cue Point';
eventJson.string = this.getStringData(eventStartIndex);
break;
case 0x09: // Device Name
eventJson.name = 'Device Name';
eventJson.string = this.getStringData(eventStartIndex);
break;
case 0x20: // MIDI Channel Prefix
eventJson.name = 'MIDI Channel Prefix';
break;
case 0x21: // MIDI Port
eventJson.name = 'MIDI Port';
eventJson.data = Utils.bytesToNumber([this.data[eventStartIndex + 3]]);
break;
case 0x2F: // End of Track
eventJson.name = 'End of Track';
break;
case 0x51: // Set Tempo
eventJson.name = 'Set Tempo';
eventJson.data = Math.round(60000000 / Utils.bytesToNumber(this.data.subarray(eventStartIndex + 3, eventStartIndex + 6)));
this.tempo = eventJson.data;
break;
case 0x54: // SMTPE Offset
eventJson.name = 'SMTPE Offset';
break;
case 0x58: // Time Signature
// FF 58 04 nn dd cc bb
eventJson.name = 'Time Signature';
eventJson.data = this.data.subarray(eventStartIndex + 3, eventStartIndex + 7);
eventJson.timeSignature = "" + eventJson.data[0] + "/" + Math.pow(2, eventJson.data[1]);
break;
case 0x59: // Key Signature
// FF 59 02 sf mi
eventJson.name = 'Key Signature';
eventJson.data = this.data.subarray(eventStartIndex + 3, eventStartIndex + 5);
if (eventJson.data[0] >= 0) {
eventJson.keySignature = Constants.CIRCLE_OF_FIFTHS[eventJson.data[0]];
} else if (eventJson.data[0] < 0) {
eventJson.keySignature = Constants.CIRCLE_OF_FOURTHS[Math.abs(eventJson.data[0])];
}
if (eventJson.data[1] == 0) {
eventJson.keySignature += " Major";
} else if (eventJson.data[1] == 1) {
eventJson.keySignature += " Minor";
}
break;
case 0x7F: // Sequencer-Specific Meta-event
eventJson.name = 'Sequencer-Specific Meta-event';
break;
default:
eventJson.name = 'Unknown: ' + this.data[eventStartIndex + 1].toString(16);
break;
}
const varIntLength = Utils.getVarIntLength(this.data.subarray(eventStartIndex + 2));
const length = Utils.readVarInt(this.data.subarray(eventStartIndex + 2, eventStartIndex + 2 + varIntLength));
//console.log(eventJson);
this.pointer += deltaByteCount + 3 + length;
//console.log(eventJson);
} else if (this.data[eventStartIndex] === 0xf0) {
// Sysex
eventJson.name = 'Sysex';
const varQuantityByteLength = Utils.getVarIntLength(this.data.subarray(eventStartIndex + 1));
const varQuantityByteValue = Utils.readVarInt(this.data.subarray(eventStartIndex + 1, eventStartIndex + 1 + varQuantityByteLength));
eventJson.data = this.data.subarray(
eventStartIndex + 1 + varQuantityByteLength,
eventStartIndex + 1 + varQuantityByteLength + varQuantityByteValue
);
this.pointer += deltaByteCount + 1 + varQuantityByteLength + varQuantityByteValue;
} else if (this.data[eventStartIndex] === 0xf7) {
// Sysex (escape)
// http://www.somascape.org/midi/tech/mfile.html#sysex
eventJson.name = 'Sysex (escape)';
const varQuantityByteLength = Utils.getVarIntLength(this.data.subarray(eventStartIndex + 1));
const varQuantityByteValue = Utils.readVarInt(this.data.subarray(eventStartIndex + 1, eventStartIndex + 1 + varQuantityByteLength));
eventJson.data = this.data.subarray(
eventStartIndex + 1 + varQuantityByteLength,
eventStartIndex + 1 + varQuantityByteLength + varQuantityByteValue
);
this.pointer += deltaByteCount + 1 + varQuantityByteLength + varQuantityByteValue;
} else {
// Voice event
if (this.data[eventStartIndex] < 0x80) {
// Running status
eventJson.running = true;
eventJson.noteNumber = this.data[eventStartIndex];
eventJson.noteName = Constants.NOTES[this.data[eventStartIndex]];
eventJson.velocity = this.data[eventStartIndex + 1];
if (this.lastStatus <= 0x8f) {
eventJson.name = 'Note off';
eventJson.channel = this.lastStatus - 0x80 + 1;
this.pointer += deltaByteCount + 2;
} else if (this.lastStatus <= 0x9f) {
eventJson.name = 'Note on';
eventJson.channel = this.lastStatus - 0x90 + 1;
this.pointer += deltaByteCount + 2;
} else if (this.lastStatus <= 0xaf) {
// Polyphonic Key Pressure
eventJson.name = 'Polyphonic Key Pressure';
eventJson.channel = this.lastStatus - 0xa0 + 1;
eventJson.note = Constants.NOTES[this.data[eventStartIndex + 1]];
eventJson.pressure = event[1];
this.pointer += deltaByteCount + 2;
} else if (this.lastStatus <= 0xbf) {
// Controller Change
eventJson.name = 'Controller Change';
eventJson.channel = this.lastStatus - 0xb0 + 1;
eventJson.number = this.data[eventStartIndex + 1];
eventJson.value = this.data[eventStartIndex + 2];
this.pointer += deltaByteCount + 2;
} else if (this.lastStatus <= 0xcf) {
// Program Change
eventJson.name = 'Program Change';
eventJson.channel = this.lastStatus - 0xc0 + 1;
eventJson.value = this.data[eventStartIndex + 1];
this.pointer += deltaByteCount + 1;
} else if (this.lastStatus <= 0xdf) {
// Channel Key Pressure
eventJson.name = 'Channel Key Pressure';
eventJson.channel = this.lastStatus - 0xd0 + 1;
this.pointer += deltaByteCount + 1;
} else if (this.lastStatus <= 0xef) {
// Pitch Bend
eventJson.name = 'Pitch Bend';
eventJson.channel = this.lastStatus - 0xe0 + 1;
eventJson.value = this.data[eventStartIndex + 2]
this.pointer += deltaByteCount + 2;
} else {
throw `Unknown event (running): ${this.lastStatus}`;
}
} else {
this.lastStatus = this.data[eventStartIndex];
if (this.data[eventStartIndex] <= 0x8f) {
// Note off
eventJson.name = 'Note off';
eventJson.channel = this.lastStatus - 0x80 + 1;
eventJson.noteNumber = this.data[eventStartIndex + 1];
eventJson.noteName = Constants.NOTES[this.data[eventStartIndex + 1]];
eventJson.velocity = Math.round(this.data[eventStartIndex + 2] / 127 * 100);
this.pointer += deltaByteCount + 3;
} else if (this.data[eventStartIndex] <= 0x9f) {
// Note on
eventJson.name = 'Note on';
eventJson.channel = this.lastStatus - 0x90 + 1;
eventJson.noteNumber = this.data[eventStartIndex + 1];
eventJson.noteName = Constants.NOTES[this.data[eventStartIndex + 1]];
eventJson.velocity = Math.round(this.data[eventStartIndex + 2] / 127 * 100);
this.pointer += deltaByteCount + 3;
} else if (this.data[eventStartIndex] <= 0xaf) {
// Polyphonic Key Pressure
eventJson.name = 'Polyphonic Key Pressure';
eventJson.channel = this.lastStatus - 0xa0 + 1;
eventJson.note = Constants.NOTES[this.data[eventStartIndex + 1]];
eventJson.pressure = event[2];
this.pointer += deltaByteCount + 3;
} else if (this.data[eventStartIndex] <= 0xbf) {
// Controller Change
eventJson.name = 'Controller Change';
eventJson.channel = this.lastStatus - 0xb0 + 1;
eventJson.number = this.data[eventStartIndex + 1];
eventJson.value = this.data[eventStartIndex + 2];
this.pointer += deltaByteCount + 3;
} else if (this.data[eventStartIndex] <= 0xcf) {
// Program Change
eventJson.name = 'Program Change';
eventJson.channel = this.lastStatus - 0xc0 + 1;
eventJson.value = this.data[eventStartIndex + 1];
this.pointer += deltaByteCount + 2;
} else if (this.data[eventStartIndex] <= 0xdf) {
// Channel Key Pressure
eventJson.name = 'Channel Key Pressure';
eventJson.channel = this.lastStatus - 0xd0 + 1;
this.pointer += deltaByteCount + 2;
} else if (this.data[eventStartIndex] <= 0xef) {
// Pitch Bend
eventJson.name = 'Pitch Bend';
eventJson.channel = this.lastStatus - 0xe0 + 1;
this.pointer += deltaByteCount + 3;
} else {
throw `Unknown event: ${this.data[eventStartIndex]}`;
//eventJson.name = `Unknown. Pointer: ${this.pointer.toString()}, ${eventStartIndex.toString()}, ${this.data[eventStartIndex]}, ${this.data.length}`;
}
}
}
this.delta += eventJson.delta;
this.events.push(eventJson);
return eventJson;
}
/**
* Returns true if pointer has reached the end of the track.
* @param {boolean}
*/
endOfTrack() {
if (this.data[this.pointer + 1] == 0xff && this.data[this.pointer + 2] == 0x2f && this.data[this.pointer + 3] == 0x00) {
return true;
}
return false;
}
}
export {Track};