/**
* The CSVFile class reads data from CSV files and returns the data as JSON.
*/
class CSVFile {
/**
* Construct a new CSVFile.
*/
constructor() {
this.hasHeader = true;
this.timeColumn = 0;
this.valueColumn = 1;
this.name;
this.readyPromise;
this.data = [];
}
/**
* Read a CSV file and convert it to JSON-formatted data.
* @param {File} file - A CSV file.
*/
readFile(file) {
this.name = name;
this.readyPromise = file.text().then((textString) => {
let csv;
let startingRow = 0;
if (this.hasHeader) {
csv = d3.csvParse(textString);
startingRow = 1; // Start to convert after the header
}
else {
csv = d3.csvParseRows(textString);
}
for (let i = startingRow; i < csv.length; i++) {
let t = +csv[i][csv.columns[this.timeColumn]];
let v = +csv[i][csv.columns[this.valueColumn]];
if (!Number.isNaN(t) && !Number.isNaN(v)) {
this.data.push({time: t, value: v});
}
}
});
}
/**
* Is this CSV file ready?
* @return {Promise} A promise that resolves when the CSV file has been read.
*/
async isReady() {
return await this.readyPromise ?? Promise.resolve(false);
}
/**
* Returns JSON-formatted data derived from a CSV file.
* @return {Array} An array of JSON data.
*/
getData() {
return this.data;
}
}
/** A MIDIFile both reads and write MIDI files. */
class MIDIFile {
/**
* Create a MIDI file reader/writer.
* @param {string} name - A name for the MIDIFile.
* @author Lawrence Fyfe
*/
constructor(name) {
this.name = name;
this.readByteArray = [];
this.readByteArrayIndex = 0;
this.headerChunkLength = 6;
this.format = 0;
this.ntracks = 1;
this.timingFlag = false;
this.tickdiv = 384;
this.trackArray = [];
this.lastEvent;
this.sysExPacket = false;
this.tempo = 500000;
this.eventSeconds = 0;
this.framesPerSecond = 30;
this.ticksPerFrame = 8;
this.secondsPerTick = 1/(this.framesPerSecond * this.ticksPerFrame);
this.totalTime = 0;
this.writeByteArray = [];
this.trackWriteByteArray = [];
this.trackWriteByteCounter = 0;
this.previousTicks = 0;
this.mergeNoteEvents = true;
}
/**
* Convert a byte array to a number.
* @param {Array} bytes - The byte array to convert.
*/
toNumber(bytes) {
switch (bytes.length) {
case 1:
return bytes[0];
case 2:
return (bytes[0] << 8) + bytes[1];
case 3:
return (bytes[0] << 16) + (bytes[1] << 8) + bytes[2];
case 4:
return (bytes[0] << 24) + (bytes[1] << 16) + (bytes[2] << 8) + bytes[3];
}
}
/**
* Convert MIDI ticks to seconds.
* @param {number} ticks - The ticks to convert.
* @return {number} Seconds.
*/
toSeconds(ticks) {
if (this.timingFlag) {
return ticks*this.secondsPerTick; // Time code seconds
}
else {
return ticks*this.tempo/(1e6*this.tickdiv); // Metrical seconds
}
}
/**
* Convert seconds to MIDI ticks.
* @param {number} seconds - The seconds to convert.
* @return {number} Ticks.
*/
toTicks(seconds) {
if (this.timingFlag) {
return Math.round(seconds/this.secondsPerTick); // Time code ticks
}
else {
return Math.round(seconds*(1e6*this.tickdiv)/this.tempo); // Metrical ticks
}
}
/**
* Convert a byte value to frames per second.
* @param {number} byte - The byte to convert.
* @return {number} Frames per second.
*/
toFramesPerSecond(byte) {
switch (byte.toString(16)) {
case "e8":
return 24;
case "e7":
return 25;
case "e3":
return 29.97;
case "e2":
return 30;
}
}
/**
* Convert frames per second to a byte value.
* @param {number} fps - Frames per second to convert.
* @return {number} A byte representing frames per second.
*/
toFramesPerSecondByte(fps) {
switch (fps) {
case 24:
return 0xe8;
case 25:
return 0xe7;
case 29.97:
return 0xe3;
case 30:
return 0xe2;
}
}
/**
* Read a MIDI file as an array of bytes.
* @param {string} name - A name for the MIDI file.
* @param {Uint8Array} byteArray - An array of bytes from the MIDI file.
*/
readFile(name, byteArray) {
this.name = name;
if (byteArray !== undefined && byteArray.length > 0) {
this.readByteArray = byteArray;
this.readByteArrayIndex = 0;
this.readHeader();
// Tracks
this.currentTrack;
for (let i = 0; i < this.ntracks; i++) {
this.currentTrack = i;
let track = new MIDITrack(i);
track.identifier = this.readIdentifier();
track.chunkLength = this.toNumber(this.readByteArray.slice(this.readByteArrayIndex, this.readByteArrayIndex += 4));
let totalByteLength = track.chunkLength + this.readByteArrayIndex;
while (this.readByteArrayIndex < totalByteLength) {
let trackEvent = this.readTrackEvent();
if (this.mergeNoteEvents) {
if (trackEvent.type === "Note Off") {
track.setNoteEndTime(trackEvent.note, trackEvent.seconds);
}
else if (trackEvent.type === "Note On") {
if (trackEvent.velocity === 0) {
// Note ends
track.setNoteEndTime(trackEvent.note, trackEvent.seconds);
}
else {
// Note starts
let noteEvent = {};
noteEvent.type = "Note";
noteEvent.channel = trackEvent.channel;
noteEvent.note = trackEvent.note;
noteEvent.velocity = trackEvent.velocity;
noteEvent.startTime = trackEvent.seconds;
noteEvent.endTime = null;
track.eventArray.push(noteEvent);
}
}
else {
track.eventArray.push(trackEvent);
}
}
else {
track.eventArray.push(trackEvent);
}
if (trackEvent.type === "End of Track") {
track.endOfTrackTime = trackEvent.seconds;
}
}
this.trackArray.push(track);
this.eventSeconds = 0;
}
}
}
/**
* Read the MIDI file identifier.
* @return {string} The file identifier.
*/
readIdentifier() {
return this.readString(this.readByteArrayIndex, this.readByteArrayIndex += 4);
}
/**
* Read the MIDI file header.
*/
readHeader() {
this.headerIdentifier = this.readIdentifier();
this.headerChunkLength = this.toNumber(this.readByteArray.slice(this.readByteArrayIndex, this.readByteArrayIndex += 4));
// Header values
this.headerValuesArray = this.readByteArray.slice(this.readByteArrayIndex, this.readByteArrayIndex += this.headerChunkLength);
this.format = this.toNumber(this.headerValuesArray.slice(0, 2));
this.ntracks = this.toNumber(this.headerValuesArray.slice(2, 4));
this.tickdivArray = this.headerValuesArray.slice(4, 6);
this.timingFlag = this.tickdivArray[0] & (1 << 7);
this.timingScheme = this.timingFlag ? "timecode" : "metrical";
if (this.timingFlag) {
this.framesPerSecond = this.toFramesPerSecond(this.tickdivArray[0]);
this.ticksPerFrame = this.tickdivArray[1];
this.secondsPerTick = 1/(this.framesPerSecond * this.ticksPerFrame);
}
else {
this.tickdiv = this.toNumber(this.tickdivArray);
}
}
/**
* Read a single byte.
* @return {number} A byte.
*/
readByte() {
return this.readByteArray.slice(this.readByteArrayIndex, this.readByteArrayIndex += 1)[0];
}
/**
* Read a number represented by 1-4 bytes.
* @return {number} A number.
*/
readVariableByteNumber() {
var byte = this.readByte();
var byteArray = [];
var byteNumber = 0;
if (byte & 0x80) {
byteArray.push(byte & 0x7f);
byte = this.readByte();
if (byte & 0x80) {
byteArray.push(byte & 0x7f);
byte = this.readByte();
if (byte & 0x80) {
byteArray.push(byte & 0x7f);
byte = this.readByte();
byteArray.push(byte);
byteNumber = (byteArray[0] << 21) + (byteArray[1] << 14) + (byteArray[2] << 7) + byteArray[3];
}
else {
byteArray.push(byte);
byteNumber = (byteArray[0] << 14) + (byteArray[1] << 7) + byteArray[2];
}
}
else {
byteArray.push(byte);
byteNumber = (byteArray[0] << 7) + byteArray[1];
}
}
else {
byteArray.push(byte);
byteNumber = byteArray[0];
}
return byteNumber;
}
/**
* Read a string from bytes at the specified Array range.
* @param {number} start - The starting byte.
* @param {number} end - The ending byte.
* @return {string} A string.
*/
readString(start, end) {
return String.fromCharCode(...this.readByteArray.slice(start, end));
}
/**
* Read event bytes and return a MIDI, SysEx, or Meta event object.
* @return {Object} A MIDI, SysEx, or Meta event.
*/
readTrackEvent() {
var trackEvent = {};
trackEvent.deltaTime = this.readVariableByteNumber();
this.eventSeconds += this.toSeconds(trackEvent.deltaTime);
trackEvent.seconds = this.eventSeconds;
var statusByte = this.readByte();
if (statusByte === undefined) {
return;
}
trackEvent.statusByteHex = statusByte.toString(16);
if (statusByte < 0x80) {
// Running status
if (this.lastEvent !== undefined && this.lastEvent.status === "MIDI") {
trackEvent.status = "MIDI";
trackEvent.channel = this.lastEvent.channel;
trackEvent.type = this.lastEvent.type;
trackEvent.running = true;
this.readByteArrayIndex -= 1;
this.readMIDIEvent(trackEvent, this.lastEvent.typeNibble);
}
}
else if (statusByte >= 0x80 && statusByte <= 0xef) {
trackEvent.status = "MIDI";
trackEvent.channel = statusByte & 0x0f;
this.readMIDIEvent(trackEvent, statusByte >> 4);
}
else if (statusByte === 0xf0) {
trackEvent.status = "SysEx";
this.readSysExEvent(trackEvent);
}
else if (statusByte === 0xf7) {
trackEvent.status = "SysEx";
this.readSysExEvent(trackEvent, statusByte);
}
else if (statusByte === 0xff) {
trackEvent.status = "Meta";
this.readMetaEvent(trackEvent);
}
else {
trackEvent.status = "Unknown";
}
this.lastEvent = trackEvent;
return trackEvent;
}
/**
* Read a MIDI event.
* @param {Object} midiEvent - The MIDI event.
* @param {nibble} typeNibble - Half of a byte indicating the MIDI event type.
*/
readMIDIEvent(midiEvent, typeNibble) {
midiEvent.typeNibble = typeNibble;
switch (typeNibble) {
case 0x08:
midiEvent.type = "Note Off";
midiEvent.note = this.readByte();
midiEvent.velocity = this.readByte();
break;
case 0x09:
midiEvent.type = "Note On";
midiEvent.note = this.readByte();
midiEvent.velocity = this.readByte();
break;
case 0x0a:
midiEvent.type = "Polyphonic Pressure";
midiEvent.note = this.readByte();
midiEvent.pressure = this.readByte();
break;
case 0x0b:
midiEvent.type = "Controller";
midiEvent.controller = this.readByte();
midiEvent.value = this.readByte();
break;
case 0x0c:
midiEvent.type = "Program Change";
midiEvent.program = this.readByte();
break;
case 0x0d:
midiEvent.type = "Channel Pressure";
midiEvent.pressure = this.readByte();
break;
case 0x0e:
midiEvent.type = "Pitch Bend";
let leastByte = this.readByte();
let mostByte = this.readByte();
midiEvent.pitchBend = this.toNumber([mostByte, leastByte]) >> 1;
break;
}
}
/**
* Read a SysEx event.
* @param {Object} sysExEvent - The SysEx event.
* @param {byte} status - The status (type) of the SysEx event.
*/
readSysExEvent(sysExEvent, status) {
sysExEvent.deltaTime = this.readVariableByteNumber();
sysExEvent.length = this.readVariableByteNumber();
sysExEvent.message = this.readByteArray.slice(this.readByteArrayIndex, this.readByteArrayIndex += sysExEvent.length);
if (status === 0xf0) {
if (sysExEvent.message[sysExEvent.message.length-1] !== 0xf7) {
this.sysExPacket = true;
}
else {
sysExEvent.type = "Single";
}
}
else if (status === 0xf7) {
// Status 0xf7 could be a SysEx packet or an escape sequence
if (this.sysExPacket) {
sysExEvent.type = "Continuation";
if (sysExEvent.message[sysExEvent.message.length-1] === 0xf7) {
this.sysExPacket = false;
}
else {
this.sysExPacket = true;
}
}
else {
sysExEvent.type = "Escape Sequence";
}
}
}
/**
* Read a Meta event.
* @param {Object} metaEvent - The Meta event.
*/
readMetaEvent(metaEvent) {
var type = this.readByte();
metaEvent.length = this.readVariableByteNumber();
switch (type) {
case 0x00:
metaEvent.type = "Sequence Number";
metaEvent.number = this.toNumber(this.readByteArray.slice(this.readByteArrayIndex, this.readByteArrayIndex += metaEvent.length));
break;
case 0x01:
metaEvent.type = "Text";
metaEvent.text = this.readString(this.readByteArrayIndex, this.readByteArrayIndex += metaEvent.length);
break;
case 0x02:
metaEvent.type = "Copyright";
metaEvent.text = this.readString(this.readByteArrayIndex, this.readByteArrayIndex += metaEvent.length);
break;
case 0x03:
if ((this.format === 0 || this.format === 1) && this.currentTrack === 0) {
metaEvent.type = "Sequence Name";
}
else {
metaEvent.type = "Track Name";
}
metaEvent.text = this.readString(this.readByteArrayIndex, this.readByteArrayIndex += metaEvent.length);
break;
case 0x04:
metaEvent.type = "Instrument Name";
metaEvent.text = this.readString(this.readByteArrayIndex, this.readByteArrayIndex += metaEvent.length);
break;
case 0x05:
metaEvent.type = "Lyric";
metaEvent.text = this.readString(this.readByteArrayIndex, this.readByteArrayIndex += metaEvent.length);
break;
case 0x06:
metaEvent.type = "Marker";
metaEvent.text = this.readString(this.readByteArrayIndex, this.readByteArrayIndex += metaEvent.length);
break;
case 0x07:
metaEvent.type = "Cue Point";
metaEvent.text = this.readString(this.readByteArrayIndex, this.readByteArrayIndex += metaEvent.length);
break;
case 0x08:
metaEvent.type = "Program Name";
metaEvent.text = this.readString(this.readByteArrayIndex, this.readByteArrayIndex += metaEvent.length);
break;
case 0x09:
metaEvent.type = "Device Name";
metaEvent.text = this.readString(this.readByteArrayIndex, this.readByteArrayIndex += metaEvent.length);
break;
case 0x20:
metaEvent.type = "MIDI Channel Prefix";
metaEvent.channel = this.readByteArray.slice(this.readByteArrayIndex, this.readByteArrayIndex += metaEvent.length);
break;
case 0x21:
metaEvent.type = "MIDI Port";
metaEvent.port = this.readByteArray.slice(this.readByteArrayIndex, this.readByteArrayIndex += metaEvent.length);
break;
case 0x2f:
metaEvent.type = "End of Track";
if (metaEvent.seconds > this.totalTime) {
this.totalTime = metaEvent.seconds;
}
break;
case 0x51:
metaEvent.type = "Tempo";
metaEvent.tempo = this.toNumber(this.readByteArray.slice(this.readByteArrayIndex, this.readByteArrayIndex += metaEvent.length));
this.tempo = metaEvent.tempo;
break;
case 0x54:
metaEvent.type = "SMPTE Offset";
let smpteOffsetBytes = this.readByteArray.slice(this.readByteArrayIndex, this.readByteArrayIndex += metaEvent.length);
let frameRateBits = smpteOffsetBytes[0] >> 5;
switch (frameRateBits) {
case 0:
metaEvent.framesPerSecond = 24;
break;
case 1:
metaEvent.framesPerSecond = 25;
break;
case 2:
metaEvent.framesPerSecond = 29.97;
break;
case 3:
metaEvent.framesPerSecond = 30;
break;
}
metaEvent.smpteHour = smpteOffsetBytes[0] & (0xff >> 3);
metaEvent.smpteMinutes = smpteOffsetBytes[1];
metaEvent.smpteSeconds = smpteOffsetBytes[2];
metaEvent.frames = smpteOffsetBytes[3];
metaEvent.fractionalFrames = smpteOffsetBytes[4];
break;
case 0x58:
metaEvent.type = "Time Signature";
let timeSignatureBytes = this.readByteArray.slice(this.readByteArrayIndex, this.readByteArrayIndex += metaEvent.length);
metaEvent.numerator = timeSignatureBytes[0];
metaEvent.denomiator = 2 ** timeSignatureBytes[1];
metaEvent.metronome = timeSignatureBytes[2];
metaEvent.quarterNote = timeSignatureBytes[3];
break;
case 0x59:
metaEvent.type = "Key Signature";
let keySignatureBytes = this.readByteArray.slice(this.readByteArrayIndex, this.readByteArrayIndex += metaEvent.length);
metaEvent.sharpsOrFlats = keySignatureBytes[0];
if (metaEvent.sharpsOrFlats < 0) {
metaEvent.flats = metaEvent.sharpsOrFlats;
}
else if (metaEvent.sharpsOrFlats > 0) {
metaEvent.sharps = metaEvent.sharpsOrFlats;
}
else {
metaEvent.key = "C";
}
metaEvent.mode = keySignatureBytes[1] ? "minor" : "major";
break;
case 0x7f:
metaEvent.type = "Sequencer Specific Event";
metaEvent.data = this.readByteArray.slice(this.readByteArrayIndex, this.readByteArrayIndex += metaEvent.length);
break;
}
}
/**
* Write a MIDI file.
*/
writeFile() {
this.writeHeader();
for (let i = 0; i < this.trackArray.length; i++) {
this.writeTrack(this.trackArray[i]);
}
}
/**
* Write a MIDI file header.
*/
writeHeader() {
this.writeIdentifierBytes("MThd");
this.writeNumberBytes(this.headerChunkLength, 4, this.writeByteArray);
this.writeNumberBytes(this.format, 2, this.writeByteArray);
this.writeNumberBytes(this.ntracks, 2, this.writeByteArray);
if (this.timingFlag) {
// timecode
this.writeNumberBytes(this.toFramesPerSecondByte(this.framesPerSecond), 1, this.writeByteArray);
this.writeNumberBytes(this.ticksPerFrame, 1, this.writeByteArray);
}
else {
// metrical
this.writeNumberBytes(this.tickdiv, 2, this.writeByteArray);
}
}
/**
* Write the specified track to a MIDI file.
* @param {MIDITrack} track - The MIDITrack to write.
*/
writeTrack(track) {
this.writeIdentifierBytes("MTrk");
this.trackWriteByteArray = [];
this.trackWriteByteCounter = 0;
this.previousTicks = 0;
var writeEventArray = [];
for (let i = 0; i < track.eventArray.length; i++) {
if (track.eventArray[i].type === "Note") {
let noteOnEvent = {};
noteOnEvent.status = "MIDI";
noteOnEvent.seconds = track.eventArray[i].startTime;
noteOnEvent.type = "Note On";
noteOnEvent.channel = track.eventArray[i].channel;
noteOnEvent.note = track.eventArray[i].note;
noteOnEvent.velocity = track.eventArray[i].velocity;
writeEventArray.push(noteOnEvent);
let noteOffEvent = {};
noteOffEvent.status = "MIDI";
noteOffEvent.seconds = track.eventArray[i].endTime;
noteOffEvent.type = "Note Off";
noteOffEvent.channel = track.eventArray[i].channel;
noteOffEvent.note = track.eventArray[i].note;
noteOffEvent.velocity = track.eventArray[i].velocity;
writeEventArray.push(noteOffEvent);
}
else {
if (track.eventArray[i].type === "End of Track" && track.eventArray[i].seconds < track.endOfTrackTime) {
track.eventArray[i].seconds = track.endOfTrackTime;
}
writeEventArray.push(track.eventArray[i]);
}
}
writeEventArray.sort((a, b) => a.seconds - b.seconds);
if (writeEventArray[writeEventArray.length-1].seconds > track.endOfTrackTime) {
track.endOfTrackTime = writeEventArray[writeEventArray.length-1].seconds;
}
if (writeEventArray.findIndex(trackEvent => trackEvent.type === "End of Track") === -1) {
writeEventArray.push({status: "Meta", type: "End of Track", seconds: track.endOfTrackTime, length: 0});
}
for (let i = 0; i < writeEventArray.length; i++) {
this.writeEvent(writeEventArray[i]);
}
this.writeNumberBytes(this.trackWriteByteCounter, 4, this.writeByteArray);
this.writeByteArray = this.writeByteArray.concat(this.trackWriteByteArray);
}
/**
* Write the specified event to a MIDI track.
* @param {Object} event - The MIDI, SysEx, or Meta event to write.
*/
writeEvent(event) {
var absoluteTicks = this.toTicks(event.seconds);
this.trackWriteByteCounter += this.writeVariableNumberBytes(absoluteTicks-this.previousTicks);
this.previousTicks = absoluteTicks;
switch (event.status) {
case "MIDI":
this.trackWriteByteCounter += this.writeMIDIBytes(event);
break;
case "SysEx":
this.writeSysExEvent(event);
break;
case "Meta":
this.writeMetaEvent(event);
break;
}
}
/**
* Write the specified string to bytes.
* @param {string} string - The string to convert to bytes for writing.
*/
writeIdentifierBytes(string) {
var encoder = new TextEncoder();
var uint8array = encoder.encode(string);
this.writeByteArray.push(uint8array);
}
/**
* Write the specified number to bytes.
* @param {number} number - The number to convert to bytes for writing.
* @param {number} number - The number of bytes for representing the number.
* @param {Array} writeToByteArray - An Array for writing.
* @return {number} The number of bytes written.
*/
writeNumberBytes(number, byteCount, writeToByteArray) {
var byteArray = new Uint8Array(byteCount);
switch (byteCount) {
case 1:
byteArray[0] = number;
break;
case 2:
byteArray[0] = number >> 8;
byteArray[1] = number;
break;
case 3:
byteArray[0] = number >> 16;
byteArray[1] = number >> 8;
byteArray[2] = number;
break;
case 4:
byteArray[0] = number >> 24;
byteArray[1] = number >> 16;
byteArray[2] = number >> 8;
byteArray[3] = number;
break;
}
writeToByteArray.push(byteArray);
return byteCount;
}
/**
* Write the specified number to a variable number of bytes.
* @param {number} number - The number to convert to 1-4 bytes for writing.
* @return {number} The number of bytes written.
*/
writeVariableNumberBytes(number) {
var byteArray;
if (number < 0x80) {
byteArray = new Uint8Array(1);
byteArray[0] = number;
}
else if (number >= 0x80 && number < 0x4000) {
byteArray = new Uint8Array(2);
byteArray[0] = (number >> 7) & 0x7f | 0x80;
byteArray[1] = number & 0x7f;
}
else if (number >= 0x4000 && number < 0x200000) {
byteArray = new Uint8Array(3);
byteArray[0] = (number >> 14) & 0x7f | 0x80;
byteArray[1] = (number >> 7) & 0x7f | 0x80;
byteArray[2] = number & 0x7f;
}
else if (number >= 0x200000 && number < 0x10000000) {
byteArray = new Uint8Array(4);
byteArray[0] = (number >> 21) & 0x7f | 0x80;
byteArray[1] = (number >> 14) & 0x7f | 0x80;
byteArray[2] = (number >> 7) & 0x7f | 0x80;
byteArray[3] = number & 0x7f;
}
this.trackWriteByteArray.push(byteArray);
return byteArray.length;
}
/**
* Write the specified MIDI event to bytes.
* @param {Object} event - The MIDI event to convert to bytes for writing.
* @return {number} The number of bytes written.
*/
writeMIDIBytes(event) {
var byteArray;
var byteCount = 0;
switch (event.type) {
case "Note Off":
byteCount = 3;
byteArray = new Uint8Array(byteCount);
byteArray[0] = 0x80 | event.channel;
byteArray[1] = event.note;
byteArray[2] = event.velocity;
break;
case "Note On":
byteCount = 3;
byteArray = new Uint8Array(byteCount);
byteArray[0] = 0x90 | event.channel;
byteArray[1] = event.note;
byteArray[2] = event.velocity;
break;
case "Polyphonic Pressure":
byteCount = 3;
byteArray = new Uint8Array(byteCount);
byteArray[0] = 0xa0 | event.channel;
byteArray[1] = event.note;
byteArray[2] = event.pressure;
break;
case "Controller":
byteCount = 3;
byteArray = new Uint8Array(byteCount);
byteArray[0] = 0xb0 | event.channel;
byteArray[1] = event.controller;
byteArray[2] = event.value;
break;
case "Program Change":
byteCount = 2;
byteArray = new Uint8Array(byteCount);
byteArray[0] = 0xc0 | event.channel;
byteArray[1] = event.program;
break;
case "Channel Pressure":
byteCount = 2;
byteArray = new Uint8Array(byteCount);
byteArray[0] = 0xd0 | event.channel;
byteArray[1] = event.pressure;
break;
case "Pitch Bend":
byteCount = 3;
byteArray = new Uint8Array(byteCount);
byteArray[0] = 0xe0 | event.channel;
byteArray[1] = event.pitchBend >> 9;
byteArray[2] = event.pitchBend & (1 >> 9);
break;
}
this.trackWriteByteArray.push(byteArray);
return byteCount;
}
/**
* Write the specified SysEx event to bytes.
* @param {Object} event - The SysEx event to convert to bytes for writing.
*/
writeSysExEvent(event) {
switch (event.type) {
case "Single":
this.trackWriteByteCounter += this.writeByte(0xf0);
this.trackWriteByteCounter += this.writeLengthAndData(event.message);
break;
case "Continuation":
this.trackWriteByteCounter += this.writeByte(0xf7);
this.trackWriteByteCounter += this.writeLengthAndData(event.message);
break;
case "Escape Sequence":
this.trackWriteByteCounter += this.writeByte(0xf7);
this.trackWriteByteCounter += this.writeLengthAndData(event.message);
break;
}
}
/**
* Write the specified Meta event to bytes.
* @param {Object} event - The Meta event to convert to bytes for writing.
*/
writeMetaEvent(event) {
this.trackWriteByteCounter += this.writeByte(0xff);
switch (event.type) {
case "Sequence Number":
this.trackWriteByteCounter += this.writeByte(0x00);
this.trackWriteByteCounter += this.writeByte(0x02);
this.trackWriteByteCounter += this.writeNumberBytes(event.number, 2, this.trackWriteByteArray);
break;
case "Text":
this.trackWriteByteCounter += this.writeByte(0x01);
this.trackWriteByteCounter += this.writeLengthAndString(event.text);
break;
case "Copyright":
this.trackWriteByteCounter += this.writeByte(0x02);
this.trackWriteByteCounter += this.writeLengthAndString(event.text);
break;
case "Sequence Name":
this.trackWriteByteCounter += this.writeByte(0x03);
this.trackWriteByteCounter += this.writeLengthAndString(event.text);
break;
case "Track Name":
this.trackWriteByteCounter += this.writeByte(0x03);
this.trackWriteByteCounter += this.writeLengthAndString(event.text);
break;
case "Instrument Name":
this.trackWriteByteCounter += this.writeByte(0x04);
this.trackWriteByteCounter += this.writeLengthAndString(event.text);
break;
case "Lyric":
this.trackWriteByteCounter += this.writeByte(0x05);
this.trackWriteByteCounter += this.writeLengthAndString(event.text);
break;
case "Marker":
this.trackWriteByteCounter += this.writeByte(0x06);
this.trackWriteByteCounter += this.writeLengthAndString(event.text);
break;
case "Cue Point":
this.trackWriteByteCounter += this.writeByte(0x07);
this.writeLengthAndString(event.text);
break;
case "Program Name":
this.trackWriteByteCounter += this.writeByte(0x08);
this.writeLengthAndString(event.text);
break;
case "Device Name":
this.trackWriteByteCounter += this.writeByte(0x09);
this.writeLengthAndString(event.text);
break;
case "MIDI Channel Prefix":
this.trackWriteByteCounter += this.writeByte(0x20);
this.trackWriteByteCounter += this.writeByte(0x01);
this.trackWriteByteCounter += this.writeByte(event.channel);
break;
case "MIDI Port":
this.trackWriteByteCounter += this.writeByte(0x21);
this.trackWriteByteCounter += this.writeByte(0x01);
this.trackWriteByteCounter += this.writeByte(event.port);
break;
case "End of Track":
this.trackWriteByteCounter += this.writeByte(0x2f);
this.trackWriteByteCounter += this.writeByte(0x00);
break;
case "Tempo":
this.trackWriteByteCounter += this.writeByte(0x51);
this.trackWriteByteCounter += this.writeByte(0x03);
this.trackWriteByteCounter += this.writeNumberBytes(event.tempo, 3, this.trackWriteByteArray);
break;
case "SMPTE Offset":
this.trackWriteByteCounter += this.writeByte(0x54);
this.trackWriteByteCounter += this.writeByte(5);
this.trackWriteByteCounter += this.writeByte(event.smpteHour);
this.trackWriteByteCounter += this.writeByte(event.smpteMinutes);
this.trackWriteByteCounter += this.writeByte(event.smpteSeconds);
this.trackWriteByteCounter += this.writeByte(event.frames);
this.trackWriteByteCounter += this.writeByte(event.fractionalFrames);
break;
case "Time Signature":
this.trackWriteByteCounter += this.writeByte(0x58);
this.trackWriteByteCounter += this.writeByte(0x04);
this.trackWriteByteCounter += this.writeByte(event.numerator);
this.trackWriteByteCounter += this.writeByte(Math.pow(event.denomiator, 0.5));
this.trackWriteByteCounter += this.writeByte(event.metronome);
this.trackWriteByteCounter += this.writeByte(event.quarterNote);
break;
case "Key Signature":
this.trackWriteByteCounter += this.writeByte(0x59);
this.trackWriteByteCounter += this.writeByte(0x02);
this.trackWriteByteCounter += this.writeByte(event.sharpsOrFlats);
if (event.mode === "major") {
this.trackWriteByteCounter += this.writeByte(0x00);
}
else {
this.trackWriteByteCounter += this.writeByte(0x01);
}
break;
case "Sequencer Specific Event":
this.trackWriteByteCounter += this.writeByte(0x7f);
break;
}
}
/**
* Write the specified byte.
* @param {number} byte - The byte to write.
* @return {number} The number of bytes written: one.
*/
writeByte(byte) {
var byteArray = new Uint8Array(1);
byteArray[0] = byte;
this.trackWriteByteArray.push(byteArray);
return 1;
}
/**
* Write variable length data to bytes. The length is determined from the size
* of the data and written as a variable number of bytes.
* @param {Array} data - The data to convert to bytes for writing.
* @return {number} The number of bytes written.
*/
writeLengthAndData(data) {
var dataLengthByteCount = this.writeVariableNumberBytes(data.length);
this.trackWriteByteArray.push(data);
return dataLengthByteCount + data.length;
}
/**
* Write an arbitrary string to bytes. The length is determined from the size
* of the data and written as a variable number of bytes.
* @param {string} string - The string to convert to bytes for writing.
* @return {number} The number of bytes written.
*/
writeLengthAndString(string) {
var encoder = new TextEncoder();
var stringByteArray = encoder.encode(string);
var stringLengthByteCount = this.writeVariableNumberBytes(stringByteArray.length);
this.trackWriteByteArray.push(stringByteArray);
return stringLengthByteCount + stringByteArray.length;
}
/**
* Add the specified track to this MIDIFile.
* @param {MIDITrack} track - The track to add.
*/
addTrack(track) {
this.trackArray.push(track);
}
/**
* Get the track specified by the track number.
* @param {number} trackNumber - The track number to get.
* @return {MIDITrack} The specified track if it exists.
*/
getTrack(trackNumber) {
var track;
if (trackNumber >= 0 && trackNumber < this.trackArray.length) {
track = this.trackArray[trackNumber];
}
return track;
}
/**
* Get all tracks in this MIDIFile.
* @return {Array} An array of tracks.
*/
getAllTracks() {
return this.trackArray;
}
/**
* Get events from the specified track.
* @param {number} trackNumber - The track number to get events from.
* @return {Array} An array of events.
*/
getTrackEvents(trackNumber) {
var events;
if (trackNumber >= 0 && trackNumber < this.trackArray.length) {
let track = this.trackArray[trackNumber];
events = track.getAllEvents();
}
return events;
}
/**
* Set events for the specified track. This will replace any existing events
* in the track!
* @param {number} trackNumber - The track number to set events for.
* @param {Array} events - An array of events.
*/
setTrackEvents(trackNumber, events) {
if (trackNumber >= 0 && trackNumber < this.trackArray.length) {
this.trackArray[trackNumber].setEvents(events);
}
}
/**
* Get the specified type of events from the specified track.
* @param {string} type - The event type.
* @param {number} trackNumber - The track number to get events from.
* @return {Array} An array of events.
*/
getTrackEventType(type, trackNumber) {
var events;
if (trackNumber >= 0 && trackNumber < this.trackArray.length) {
let track = this.trackArray[trackNumber];
events = track.getEventType(type);
}
return events;
}
/**
* Get the byte array for the MIDIFile.
* @return {Array} An array of bytes.
*/
getByteArray() {
return this.writeByteArray;
}
/**
* Dump header data to the console.
*/
dumpHeader() {
console.log("header identifier: " + this.headerIdentifier);
console.log("header chunk length: " + this.headerChunkLength);
console.log("format: " + this.format);
console.log("ntracks: " + this.ntracks);
console.log("timing scheme: " + this.timingScheme);
if (this.timingScheme === "metrical") {
console.log(" ppqn: " + this.tickdiv);
}
else {
console.log(" framesPerSecond: " + this.framesPerSecond);
console.log(" ticksPerFrame: " + this.ticksPerFrame);
console.log(" secondsPerTick: " + this.secondsPerTick);
}
console.log("\n");
}
/**
* Dump event data to the console for all tracks.
*/
dumpTrackEvents() {
for (let i = 0; i < this.trackArray.length; i++) {
console.log("track number: " + this.trackArray[i].number);
console.log("track identifier: " + this.trackArray[i].identifier);
console.log("track chunk length: " + this.trackArray[i].chunkLength);
for (let j = 0; j < this.trackArray[i].eventArray.length; j++) {
console.log(this.trackArray[i].eventArray[j]);
}
console.log("\n");
}
}
}
/**
* Create a MIDI track.
* @param {number} number - The track number.
* @author Lawrence Fyfe
*/
class MIDITrack {
constructor(number) {
this.number = number;
this.identifier;
this.chunkLength;
this.eventArray = [];
this.endOfTrackTime = 0;
}
/**
* Find a note and set its end time.
* @param {number} note - The note value.
* @param {number} endTime - The end time to set.
*/
setNoteEndTime(note, endTime) {
for (let i = this.eventArray.length-1; i >= 0; i--) {
if (this.eventArray[i].type === "Note" &&
this.eventArray[i].note === note &&
this.eventArray[i].endTime === null) {
this.eventArray[i].endTime = endTime;
}
}
}
/**
* Get all events from this track.
* @return {Array} An array of events.
*/
getAllEvents() {
return this.eventArray;
}
/**
* Get all events of the specified type from this track.
* @param {string} type - The event type.
* @return {Array} An array of events.
*/
getEventType(type) {
return this.eventArray.filter(event => event.type === type);;
}
/**
* Set all events in this track to the specified events. This will replace
* any existing events!
* @param {Array} events - The event type.
*/
setEvents(events) {
this.eventArray = [].concat(events);
}
/**
* Clear all events from this track.
*/
clearEvents() {
this.eventArray.length = 0;
}
}
class EventOrganizer {
constructor() {
//
}
}