Source: files.js

/**
* 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() {
    //
  }
}