TealMIDIPlayer (JMP only)

MIDI Player bot for MPP. (Based off of Teal's MIDI player)

// ==UserScript==
// @name         TealMIDIPlayer (JMP only)
// @name:pt-BR   Tocador de MIDIs do Teal
// @homepage     https://ccjt.sad.ovh/
// @version      1.2.1
// @description  MIDI Player bot for MPP. (Based off of Teal's MIDI player)
// @description:pt-BR  Bot tocador de MIDIs para MPP. (Baseado no tocador de MIDIs do Teal)
// @author       ccjt
// @match        *://multiplayerpiano.net/*
// @match        *://multiplayerpiano.org/*
// @match        *://piano.mpp.community/*
// @match        *:///*
// @match        *://www.multiplayerpiano.dev/*
// @match        *://mpp.smp-meow.net/*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant        GM_info
// @license      MIT
// @namespace    https://greasyfork.org/users/1459137
// ==/UserScript==

// change this however you like - mude isso para o que quiser
const prefix = "p."
// you'll have to use commands by starting with the characters in the text above. - você vai precisar usar comandos com os caracteres no texto acima.

const sep = '-'

const SCRIPT = GM_info.script
const name = SCRIPT.name
const version = SCRIPT.version
const author = SCRIPT.author
const link = SCRIPT.homepage

// JMIDIPlayer
// "THE BEER-WARE LICENSE" (Revision 42):
// <[email protected]> wrote this file.
// As long as you retain this notice you can do whatever you want with this stuff.
// If we meet some day, and you think this stuff is worth it, you can buy me a beer in return.
// - James

const HEADER_LENGTH = 14;
const DEFAULT_TEMPO = 500000; // 120 bpm / 500ms/qn
const EVENT_SIZE = 8;
const EVENT_CODE = {
  NOTE_ON: 0x09,
  NOTE_OFF: 0x08,
  CONTROL_CHANGE: 0x0B,
  SET_TEMPO: 0x51,
  END_OF_TRACK: 0x2F
};

class JMIDIPlayer {
  // playback state
  #isPlaying = false;
  #currentTick = 0;
  #currentTempo = DEFAULT_TEMPO;
  #playbackWorker = null;

  // loading state & file data
  #isLoading = false;
  #totalEvents = 0;
  #totalTicks = 0;
  #songTime = 0;
  #ppqn = 0;
  #numTracks = 0;
  #timeMap = [];

  // configurable properties
  #playbackSpeed = 1; // multiplier

  // event listeners
  #eventListeners = {};

  constructor() {
    this.#eventListeners = {};
    this.#createWorker();
  }

  on(event, callback) {
    if (!this.#eventListeners[event]) {
      this.#eventListeners[event] = [];
    }
    this.#eventListeners[event].push(callback);
  }

  off(event, callback) {
    if (!this.#eventListeners[event]) return;
    const index = this.#eventListeners[event].indexOf(callback);
    if (index > -1) {
      this.#eventListeners[event].splice(index, 1);
    }
  }

  emit(event, data) {
    if (!this.#eventListeners[event]) return;
    for (const callback of this.#eventListeners[event]) {
      callback(data);
    }
  }

  async loadArrayBuffer(arrbuf) {
    const start = performance.now();
    this.#isLoading = true;

    return new Promise((resolve, reject) => {
      const handleMessage = (e) => {
        const msg = e.data;

        if (msg.type === 'parseComplete') {
          this.#playbackWorker.removeEventListener('message', handleMessage);
          this.#isLoading = false;
          this.#totalEvents = msg.totalEvents;
          this.#totalTicks = msg.totalTicks;
          this.#songTime = msg.songTime;
          this.#ppqn = msg.ppqn;
          this.#numTracks = msg.numTracks;
          this.#timeMap = msg.timeMap;

          const parseTime = performance.now() - start;
          this.emit("fileLoaded", { parseTime });
          resolve([0, parseTime]); // [readTime, parseTime]
        } else if (msg.type === 'parseError') {
          this.#playbackWorker.removeEventListener('message', handleMessage);
          this.unload();
          reject(new Error(msg.error));
        }
      };

      this.#playbackWorker.addEventListener('message', handleMessage);

      // transfer the buffer to the worker
      this.#playbackWorker.postMessage({
        type: 'load',
        buffer: arrbuf
      }, [arrbuf]);
    });
  }

  async loadFile(file) {
    const arrbuf = await file.arrayBuffer();
    return this.loadArrayBuffer(arrbuf);
  }

  unload() {
    this.stop();

    if (this.#isLoading) {
      this.#isLoading = false;
    }

    this.#numTracks = 0;
    this.#ppqn = 0;
    this.#totalEvents = 0;
    this.#totalTicks = 0;
    this.#songTime = 0;
    this.#timeMap = [];
    this.#currentTick = 0;
    this.#currentTempo = DEFAULT_TEMPO / this.#playbackSpeed;

    if (this.#playbackWorker) {
      this.#playbackWorker.postMessage({
        type: 'unload'
      });
    }

    this.emit("unloaded");
  }

  play() {
    if (this.#isPlaying) return;
    if (this.#isLoading) return;
    if (this.#totalTicks === 0) throw new Error("No MIDI data loaded.");

    this.#isPlaying = true;
    this.#playbackWorker.postMessage({
      type: 'play'
    });
    this.emit("play");
  }

  pause() {
    if (!this.#isPlaying) return;

    this.#isPlaying = false;
    this.#playbackWorker.postMessage({
      type: 'pause'
    });
    this.emit("pause");
  }

  stop() {
    if (!this.#isPlaying && this.#currentTick === 0) return;

    const needsEmit = this.#currentTick > 0;

    this.#isPlaying = false;
    this.#currentTick = 0;
    this.#currentTempo = DEFAULT_TEMPO / this.#playbackSpeed;

    this.#playbackWorker.postMessage({
      type: 'stop'
    });

    if (needsEmit) this.emit("stop");
  }

  seek(tick) {
    if (this.#isLoading || this.#totalTicks === 0) return;

    tick = Math.min(Math.max(0, tick), this.#totalTicks);
    if (Number.isNaN(tick)) return;

    const wasPlaying = this.#isPlaying;
    if (wasPlaying) this.pause();

    this.#currentTick = tick;
    this.#playbackWorker.postMessage({
      type: 'seek',
      tick
    });

    this.emit("seek", {
      tick
    });

    if (wasPlaying) this.play();
  }

  #createWorker() {
    const workerCode = `
      const EVENT_SIZE = 8;
      const DEFAULT_TEMPO = 500000;
      const EVENT_CODE = { NOTE_ON: 0x09, NOTE_OFF: 0x08, CONTROL_CHANGE: 0x0B, SET_TEMPO: 0x51, END_OF_TRACK: 0x2F };
      const HEADER_LENGTH = 14;

      // parsed MIDI data
      let tracks = [];
      let ppqn = 0;
      let tempoEvents = [];
      let totalTicks = 0;
      let numTracks = 0;
      let format = 0;

      // playback state
      let playbackSpeed = 1;
      let isPlaying = false;
      let currentTick = 0;
      let currentTempo = DEFAULT_TEMPO;
      let trackEventPointers = [];
      let startTick = 0;
      let startTime = 0;
      let playLoopInterval = null;
      const sampleRate = 5; // ms

      function parseVarlen(view, offset) {
        let value = 0;
        let startOffset = offset;
        let checkNextByte = true;
        while (checkNextByte) {
          const currentByte = view.getUint8(offset);
          value = (value << 7) | (currentByte & 0x7F);
          ++offset;
          checkNextByte = !!(currentByte & 0x80);
        }
        return [value, offset - startOffset];
      }

      function parseTrack(view, trackOffset) {
        let eventIndex = 0;
        let capacity = 2048;
        let packedBuffer = new ArrayBuffer(capacity * EVENT_SIZE);
        let packedView = new DataView(packedBuffer);

        const trackTempoEvents = [];
        let totalTicks = 0;
        let currentTick = 0;
        let runningStatus = 0;

        const trackLength = view.getUint32(trackOffset + 4);
        let offset = trackOffset + 8;
        const endOffset = offset + trackLength;

        while (offset < endOffset) {
          const deltaTimeVarlen = parseVarlen(view, offset);
          offset += deltaTimeVarlen[1];
          currentTick += deltaTimeVarlen[0];

          let statusByte = view.getUint8(offset);
          if (statusByte < 0x80) {
            statusByte = runningStatus;
          } else {
            runningStatus = statusByte;
            ++offset;
          }

          const eventType = statusByte >> 4;
          let ignore = false;

          let eventCode, p1, p2, p3;

          switch (eventType) {
            case 0x8: // note off
            case 0x9: // note on
              eventCode = eventType;
              const note = view.getUint8(offset++);
              const velocity = view.getUint8(offset++);

              p1 = statusByte & 0x0F; // channel
              p2 = note;
              p3 = velocity;
              break;

            case 0xB: // control change
              eventCode = eventType;
              const ccNum = view.getUint8(offset++);
              const ccValue = view.getUint8(offset++);
              if (ccNum !== 64) ignore = true;

              p1 = statusByte & 0x0F; // channel
              p2 = ccNum;
              p3 = ccValue;
              break;

            case 0xA:   // polyphonic key pressure
            case 0xE:   // pitch wheel change
              ++offset; // fallthrough
            case 0xC:   // program change
            case 0xD:   // channel pressure
              ++offset;
              ignore = true;
              break;

            case 0xF: // system common / meta event
              if (statusByte === 0xFF) {
                const metaType = view.getUint8(offset++);
                const lengthVarlen = parseVarlen(view, offset);
                offset += lengthVarlen[1];

                switch (metaType) {
                  case 0x51: // set tempo
                    if (lengthVarlen[0] !== 3) {
                      ignore = true;
                    } else {
                      p1 = view.getUint8(offset);
                      p2 = view.getUint8(offset + 1);
                      p3 = view.getUint8(offset + 2);
                      const uspq = (p1 << 16) | (p2 << 8) | p3;
                      trackTempoEvents.push({ tick: currentTick, uspq: uspq });
                      eventCode = EVENT_CODE.SET_TEMPO;
                    }
                    break;
                  case 0x2F: // end of track
                    eventCode = EVENT_CODE.END_OF_TRACK;
                    offset = endOffset;
                    break;
                  default:
                    ignore = true;
                    break;
                }

                offset += lengthVarlen[0];
              } else if (statusByte === 0xF0 || statusByte === 0xF7) {
                ignore = true;
                const lengthVarlen = parseVarlen(view, offset);
                offset += lengthVarlen[0] + lengthVarlen[1];
              } else {
                ignore = true;
              }
              break;

            default:
              ignore = true;
              break;
          }

          if (!ignore) {
            if (eventIndex >= capacity) {
              capacity *= 2;
              const newBuffer = new ArrayBuffer(capacity * EVENT_SIZE);
              new Uint8Array(newBuffer).set(new Uint8Array(packedBuffer));
              packedBuffer = newBuffer;
              packedView = new DataView(packedBuffer);
            }

            const byteOffset = eventIndex * EVENT_SIZE;

            if (currentTick > 0xFFFFFFFF) {
              throw new Error(\`MIDI file too long! Track tick count exceeds maximum.\`);
            }

            packedView.setUint32(byteOffset, currentTick);
            packedView.setUint8(byteOffset + 4, eventCode);
            packedView.setUint8(byteOffset + 5, p1 || 0);
            packedView.setUint8(byteOffset + 6, p2 || 0);
            packedView.setUint8(byteOffset + 7, p3 || 0);

            ++eventIndex;
          }
        }

        packedBuffer = packedBuffer.slice(0, eventIndex * EVENT_SIZE);
        totalTicks = currentTick;

        return { packedBuffer, tempoEvents: trackTempoEvents, totalTicks };
      }

      function parseMIDI(buffer) {
        const view = new DataView(buffer);

        // HEADER
        const magic = view.getUint32(0);
        if (magic !== 0x4d546864) {
          throw new Error(\`Invalid MIDI magic! Expected 4d546864, got \${magic.toString(16).padStart(8, "0")}.\`);
        }

        const length = view.getUint32(4);
        if (length !== 6) {
          throw new Error(\`Invalid header length! Expected 6, got \${length}.\`);
        }

        format = view.getUint16(8);
        numTracks = view.getUint16(10);

        if (format === 0 && numTracks > 1) {
          throw new Error(\`Invalid track count! Format 0 MIDIs should only have 1 track, got \${numTracks}.\`);
        }

        if (format >= 2) {
          throw new Error(\`Unsupported MIDI format: \${format}.\`);
        }

        ppqn = view.getUint16(12);

        if (ppqn === 0) {
          throw new Error(\`Invalid PPQN/division value!\`);
        }

        if ((ppqn & 0x8000) !== 0) {
          throw new Error(\`SMPTE timecode format is not supported!\`);
        }

        // TRACK OFFSETS
        const trackOffsets = new Array(numTracks);
        let currentOffset = HEADER_LENGTH;

        for (let i = 0; i < numTracks; ++i) {
          if (currentOffset >= buffer.byteLength) {
            throw new Error(\`Reached EOF while looking for track \${i}. Tracks reported: \${numTracks}.\`);
          }

          const trackMagic = view.getUint32(currentOffset);
          if (trackMagic !== 0x4d54726b) {
            throw new Error(\`Invalid track \${i} magic! Expected 4d54726b, got \${trackMagic.toString(16).padStart(8, "0")}.\`);
          }

          const trackLength = view.getUint32(currentOffset + 4);
          trackOffsets[i] = currentOffset;
          currentOffset += trackLength + 8;
        }

        // PARSE TRACKS
        tracks = new Array(numTracks);
        totalTicks = 0;
        tempoEvents = [];

        for (let i = 0; i < numTracks; ++i) {
          const result = parseTrack(view, trackOffsets[i]);
          tracks[i] = {
            packedBuffer: result.packedBuffer,
            eventCount: result.packedBuffer.byteLength / EVENT_SIZE,
            view: new DataView(result.packedBuffer)
          };
          totalTicks = Math.max(totalTicks, result.totalTicks);
          result.tempoEvents.forEach(event => tempoEvents.push(event));
        }

        tempoEvents.sort((a, b) => a.tick - b.tick);

        const tempoMap = [{ tick: 0, uspq: DEFAULT_TEMPO }];

        for (const event of tempoEvents) {
          const lastTempo = tempoMap[tempoMap.length - 1];
          if (event.tick === lastTempo.tick) {
            lastTempo.uspq = event.uspq;
          } else {
            tempoMap.push(event);
          }
        }

        let totalMs = 0;
        const timeMap = [{ tick: 0, time: 0, uspq: DEFAULT_TEMPO }];

        for (let i = 0; i < tempoMap.length; ++i) {
          const lastTimeData = timeMap[timeMap.length-1];
          const lastUspq = lastTimeData.uspq;
          const currentTempoEvent = tempoMap[i];

          const ticksSinceLast = currentTempoEvent.tick - lastTimeData.tick;
          const msSinceLast = (ticksSinceLast * (lastUspq / 1000)) / ppqn;
          const cumulativeTime = lastTimeData.time + msSinceLast;

          timeMap.push({
            tick: currentTempoEvent.tick,
            time: cumulativeTime,
            uspq: currentTempoEvent.uspq
          });
        }

        const lastTimeData = timeMap[timeMap.length - 1];
        const ticksInFinalSegment = totalTicks - lastTimeData.tick;
        const msInFinalSegment = (ticksInFinalSegment * (lastTimeData.uspq / 1000)) / ppqn;
        totalMs = lastTimeData.time + msInFinalSegment;

        const songTime = totalMs / 1000;
        const totalEvents = tracks.map(t => t?.eventCount || 0).reduce((a, b) => a + b, 0);

        return { totalEvents, totalTicks, songTime, ppqn, numTracks, timeMap };
      }

      function findNextEventIndex(trackIndex, tick) {
        const track = tracks[trackIndex];
        if (track.eventCount === 0) return 0;

        let low = 0;
        let high = track.eventCount;

        while (low < high) {
          const mid = Math.floor(low + (high - low) / 2);
          const eventTick = track.view.getUint32(mid * EVENT_SIZE);

          if (eventTick < tick) {
            low = mid + 1;
          } else {
            high = mid;
          }
        }
        return low;
      }

      function getCurrentTick() {
        if (!startTime) return startTick;

        const tpms = ppqn / (currentTempo / 1000);
        const ms = performance.now() - startTime;

        return Math.round(tpms * ms) + startTick;
      }

      function playLoop() {
        if (!isPlaying) {
          clearInterval(playLoopInterval);
          playLoopInterval = null;
          return;
        }

        currentTick = getCurrentTick();

        if (tracks.every((track, i) => trackEventPointers[i] >= track.eventCount) || currentTick > totalTicks) {
          isPlaying = false;
          clearInterval(playLoopInterval);
          playLoopInterval = null;
          currentTick = 0;
          startTick = 0;
          startTime = 0;
          postMessage({ type: 'endOfFile' });
          return;
        }

        const eventPointers = [];
        let totalEventsToPlay = 0;

        for (let i = 0; i < tracks.length; ++i) {
          const track = tracks[i];
          if (!track) continue;

          let ptr = trackEventPointers[i];
          const startPtr = ptr;

          while (ptr < track.eventCount && track.view.getUint32(ptr * EVENT_SIZE) <= currentTick) {
            const eventData = track.view.getUint32((ptr * EVENT_SIZE) + 4);
            const eventTypeCode = eventData >> 24;

            // handle tempo changes immediately
            if (eventTypeCode === EVENT_CODE.SET_TEMPO) {
              const eventTick = track.view.getUint32(ptr * EVENT_SIZE);
              const uspq = eventData & 0xFFFFFF;
              const oldTempo = currentTempo * playbackSpeed;
              const msAfterTempoEvent = ((currentTick - eventTick) * (oldTempo / 1000)) / ppqn;

              startTick = eventTick;
              startTime = performance.now() - msAfterTempoEvent;
              currentTempo = uspq / playbackSpeed;
            }
            ++ptr;
          }

          const numEventsInTrack = ptr - startPtr;
          if (numEventsInTrack > 0) {
            eventPointers.push({ trackIndex: i, start: startPtr, count: numEventsInTrack });
            totalEventsToPlay += numEventsInTrack;
          }
        }

        if (totalEventsToPlay > 0) {
          const buffer = new ArrayBuffer(totalEventsToPlay * EVENT_SIZE);
          const destView = new Uint8Array(buffer);
          let destOffset = 0;

          for (const pointer of eventPointers) {
            const track = tracks[pointer.trackIndex];
            const sourceByteOffset = pointer.start * EVENT_SIZE;
            const sourceByteLength = pointer.count * EVENT_SIZE;

            const sourceView = new Uint8Array(track.packedBuffer, sourceByteOffset, sourceByteLength);

            destView.set(sourceView, destOffset);
            destOffset += sourceByteLength;

            trackEventPointers[pointer.trackIndex] += pointer.count;
          }
          postMessage({ type: 'events', buffer: buffer, currentTick }, [buffer]);
        }
      }

      self.onmessage = function(e) {
        const msg = e.data;

        try {
          switch (msg.type) {
            case 'load':
              const result = parseMIDI(msg.buffer);
              trackEventPointers = new Array(tracks.length).fill(0);
              currentTick = 0;
              currentTempo = DEFAULT_TEMPO / playbackSpeed;
              postMessage({
                type: 'parseComplete',
                totalEvents: result.totalEvents,
                totalTicks: result.totalTicks,
                songTime: result.songTime,
                ppqn: result.ppqn,
                numTracks: result.numTracks,
                timeMap: result.timeMap
              });
              break;

            case 'unload':
              tracks = [];
              ppqn = 0;
              tempoEvents = [];
              totalTicks = 0;
              numTracks = 0;
              trackEventPointers = [];
              currentTick = 0;
              currentTempo = DEFAULT_TEMPO / playbackSpeed;
              isPlaying = false;
              if (playLoopInterval) {
                clearInterval(playLoopInterval);
                playLoopInterval = null;
              }
              break;

            case 'play':
              if (isPlaying) return;
              if (tracks.length === 0) return;
              isPlaying = true;
              startTime = performance.now();
              playLoopInterval = setInterval(playLoop, sampleRate);
              break;

            case 'pause':
              if (!isPlaying) return;
              isPlaying = false;
              clearInterval(playLoopInterval);
              playLoopInterval = null;
              startTick = getCurrentTick();
              currentTick = startTick;
              startTime = 0;
              postMessage({ type: 'tickUpdate', tick: currentTick });
              break;

            case 'stop':
              isPlaying = false;
              clearInterval(playLoopInterval);
              playLoopInterval = null;
              currentTick = 0;
              startTick = 0;
              startTime = 0;
              currentTempo = DEFAULT_TEMPO / playbackSpeed;
              break;

            case 'seek':
              const tick = msg.tick;

              // binary search for tempo
              if (tempoEvents.length > 0) {
                let low = 0;
                let high = tempoEvents.length - 1;
                let bestMatch = -1;

                while (low <= high) {
                  const mid = Math.floor(low + (high - low) / 2);
                  if (tempoEvents[mid].tick <= tick) {
                    bestMatch = mid;
                    low = mid + 1;
                  } else {
                    high = mid - 1;
                  }
                }

                currentTempo = ((bestMatch !== -1) ? tempoEvents[bestMatch].uspq : DEFAULT_TEMPO) / playbackSpeed;
              }

              for (let i = 0; i < tracks.length; ++i) {
                trackEventPointers[i] = findNextEventIndex(i, tick);
              }

              currentTick = tick;
              startTick = tick;
              postMessage({ type: 'tickUpdate', tick });
              break;

            case 'setPlaybackSpeed':
              const oldSpeed = playbackSpeed;
              playbackSpeed = msg.speed;

              if (isPlaying) {
                const tick = getCurrentTick();
                currentTempo = (currentTempo * oldSpeed) / playbackSpeed;
                startTick = tick;
                startTime = performance.now();
              }
              break;
          }
        } catch (error) {
          console.error(error);
          postMessage({ type: 'parseError', error: error.message });
        }
      };
    `;

    const blob = new Blob([workerCode], {
      type: 'application/javascript'
    });
    const workerUrl = URL.createObjectURL(blob);
    this.#playbackWorker = new Worker(workerUrl);

    this.#playbackWorker.onmessage = (e) => {
      const msg = e.data;

      switch (msg.type) {
        case 'events':
          this.#currentTick = msg.currentTick;

          const view = new DataView(msg.buffer);
          const numEvents = msg.buffer.byteLength / EVENT_SIZE;

          for (let i = 0; i < numEvents; i++) {
            const byteOffset = i * EVENT_SIZE;
            const eventTick = view.getUint32(byteOffset);
            const eventData = view.getUint32(byteOffset + 4);

            const eventTypeCode = eventData >> 24;
            const event = {
              tick: eventTick
            };

            switch (eventTypeCode) {
              case EVENT_CODE.NOTE_ON:
              case EVENT_CODE.NOTE_OFF:
                event.type = eventTypeCode;
                event.channel = (eventData >> 16) & 0xFF;
                event.note = (eventData >> 8) & 0xFF;
                event.velocity = eventData & 0xFF;
                break;
              case EVENT_CODE.CONTROL_CHANGE:
                event.type = 0x0B;
                event.channel = (eventData >> 16) & 0xFF;
                event.ccNum = (eventData >> 8) & 0xFF;
                event.ccValue = eventData & 0xFF;
                break;
              case EVENT_CODE.SET_TEMPO:
                event.type = 0xFF;
                event.metaType = 0x51;
                event.uspq = eventData & 0xFFFFFF;
                this.#currentTempo = event.uspq / this.#playbackSpeed;
                this.emit("tempoChange");
                break;
              case EVENT_CODE.END_OF_TRACK:
                event.type = 0xFF;
                event.metaType = 0x2F;
                break;
            }

            this.emit("midiEvent", event);
          }
          break;

        case 'endOfFile':
          this.#isPlaying = false;
          this.#currentTick = 0;
          this.emit("endOfFile");
          this.emit("stop");
          break;

        case 'tickUpdate':
          this.#currentTick = msg.tick;
          break;
      }
    };

    this.#playbackWorker.onerror = (error) => {
      console.error('Worker error:', error);
    };
  }

  getTimeAtTick(tick) {
    if (!this.#timeMap || this.#timeMap.length === 0 || this.#ppqn === 0) {
      return 0;
    }

    let low = 0;
    let high = this.#timeMap.length - 1;
    let bestMatchIndex = 0;

    while (low <= high) {
      const mid = Math.floor(low + (high - low) / 2);
      const midTick = this.#timeMap[mid].tick;

      if (midTick <= tick) {
        bestMatchIndex = mid;
        low = mid + 1;
      } else {
        high = mid - 1;
      }
    }

    const segment = this.#timeMap[bestMatchIndex];

    const ticksSinceSegmentStart = tick - segment.tick;
    const msSinceSegmentStart = (ticksSinceSegmentStart * (segment.uspq / 1000)) / this.#ppqn;

    const totalMs = segment.time + msSinceSegmentStart;
    return totalMs / 1000;
  }


  get isLoading() {
    return this.#isLoading;
  }

  get isPlaying() {
    return this.#isPlaying;
  }

  get trackCount() {
    return this.#numTracks;
  }

  get songTime() {
    return this.#songTime;
  }

  get ppqn() {
    return this.#ppqn;
  }

  get currentTempo() {
    return 60_000_000 / this.#currentTempo;
  }

  get totalEvents() {
    return this.#totalEvents;
  }

  get totalTicks() {
    return this.#totalTicks;
  }

  get currentTick() {
    return this.#currentTick;
  }

  get playbackSpeed() {
    return this.#playbackSpeed;
  }

  set playbackSpeed(speed) {
    speed = +speed;
    if (Number.isNaN(speed)) throw new Error("Playback speed must be a valid number!");
    if (speed <= 0) throw new Error("Playback speed must be a positive number!");

    const oldSpeed = this.#playbackSpeed;
    if (speed === oldSpeed) return;

    this.#playbackSpeed = speed;

    if (this.#playbackWorker) {
      this.#playbackWorker.postMessage({
        type: 'setPlaybackSpeed',
        speed
      });
    }
  }
}

let player = new JMIDIPlayer()

const charLimit = 512
function send(msg, reply, replyTo) {
    if (typeof msg == "string") {
        msg = msg
    } else {
        msg = JSON.stringify(msg)
    }
    msg = "⇀ " + msg
    if (msg.length > charLimit) {
        for (let i = 0; i < Math.floor(msg.length / charLimit) + 1; i++) {
            MPP.chat.send(msg.slice(charLimit * i, charLimit * (i + 1)));
        }
    } else {
        MPP.chat.send(msg)
    }
}
function midiLoading() {
    if (!player.isPlaying) MPP.press('as3', 1);
    setTimeout(() => {
        MPP.release('as3');
        if (!player.isPlaying) MPP.press('cs4', 1)
    }, 250);
    setTimeout(() => {
        MPP.release('cs4');
        if (!player.isPlaying) MPP.press('fs4', 1)
    }, 500);
    setTimeout(() => {
        MPP.release('fs4');
    }, 750);
    setTimeout(() => {
        MPP.release('c4')
    }, 1000)
}
let loadnotes
function loadNotes(start) {
    if (start) {
        midiLoading()
        loadnotes = setInterval(midiLoading, 1e3)
    }
    else clearInterval(loadnotes)
}
function validUrl(url) {
    let result
    try {
        new URL(url)
        result = true
    }
    catch {
        result = false
    }
    return result
}
const signal = new AbortController().signal
async function loadStuff(a, url, id) {
    function validMidi(arrbuf) {
        const decoder = new TextDecoder('utf8');
        let textdata = decoder.decode(arrbuf);
        if (textdata.startsWith('MThd')) return true
        else return false
    }
    let out = await a.arrayBuffer();
    if (validMidi(out)) return out
    else throw new Error('The file provided is invalid, as it doesn\'t start with the header `MThd`.')
}
let playing = {}
async function playMidiFromUrl(url, id) {
    let fetchtime
    let fetchstart
    let parsetime
    loadNotes(true)
    setTimeout(()=>{}, 50)
    let result
    fetchtime = 0
    fetchstart = Date.now()
    console.log('trying to play', url)
    if (validUrl(url)) {
        if (player.isPlaying) {
            player.stop();
            player.unload()
        }
        try {
            fetch(url, {
                method: 'get',
                signal: signal
            }).then(async (a) => {
                try {
                    let out = await loadStuff(a, url, id)
                    return out
                } catch (err) {
                    send("There was an error when playing the file. " + sep + " Error: " + err.message)
                    console.log(err)
                    loadNotes(false)
                    return false
                }
            }).then(async a => {
                fetchtime = Date.now() - fetchstart
                if (a) {
                    let parsestart = Date.now()
                    try {
                        await player.loadArrayBuffer(a)
                        player.play()
                        parsetime = Date.now() - parsestart
                    }
                    catch (err) {
                        parsetime = Date.now() - parsestart
                        result = false
                        send("There was an error when playing the file. ||" + err + "||")
                        console.log(err)
                        loadNotes(false)
                        return
                    }
                    console.log("Fetch time: " + fetchtime + "ms\nParse time: " + parsetime + "ms")
                    send("Fetched MIDI in " + fetchtime + "ms. " + sep + " Parsed MIDI in " + parsetime + "ms. " + sep + " Now playing `" + decodeURIComponent(url.split("/")[url.split("/").length - 1]) + "`.")
                    playing.url = url
                    loadNotes(false)
                } else {
                    loadNotes(false)
                    return
                }
            })
        } catch (err) {
            result = false
            send('Error')
            loadNotes(false)
        }
    } else {
        result = false
        send("There was an error when playing the file. ||Invalid URL||")
    }
    loadNotes(false)
    return result
}
let looping = false
let sustain = false
let transpose = 0
let volume = 1
let jevents = {
    noteon: 9,
    noteoff: 8,
    ctrlChange: 0x0B,
    setTempo: 0x51,
    end: 0x2F,
    meta: 0xFF
};
let eventsplayed = 0
let keys = Object.keys(MPP.piano.keys)
let currenttick
player.on('midiEvent', (event) => {
    eventsplayed++
    currenttick = event.tick
    if (event.type == jevents.noteon && event.velocity !== 0 && event.channel !== 9) {
        MPP.press(keys[event.note - 21 + transpose], (event.velocity / 127) * volume)
    } else if (event.type == jevents.noteoff || event.velocity == 0) {
        if (!sustain) MPP.release(keys[event.note - 21 + transpose])
    }
})
player.on('endOfFile', async () => {
    if (looping) {
        setTimeout(()=>{
            eventsplayed = 0;
            player.seek(0);
            player.play();
        }, 15)
    } else {
        loadNotes(false)
        send("Finished playing track. Duration: `" + player.songTime.toFixed(2) + "s` " + sep + " Played `" + eventsplayed + "` out of `" + player.totalEvents + "` (" + (eventsplayed / player.totalEvents * 100).toFixed(2) + "%) events.")
        MPP.client.sendArray([{
            m: "n",
            n: stopnotes
        }])
        eventsplayed = 0
        sustain = false
    }
})
let stopnotes = []
for (let i = 0; i < keys.length; i++) {
    stopnotes.push({
        'n': keys[i],
        's': 1
    })
}

let cmds = {
    help: {
        aliases: ['h'],
        about: "Shows commands and command info.",
        func: (...args) => {
            let ogcmd = args[0]
            if (args.length == 1)
                send(`Commands ${sep} ${createcmdstr()} ${sep} Use \`${ogcmd} <command name>\` to get info about a specific command.`)
            else
                if (Object.keys(cmds).includes(args[1])) {
                    console.log(cmds[args[1]])
                    let cmdinfo = cmds[args[1]]
                    let aliases = cmdinfo.aliases.length > 0 ? `${prefix}${cmdinfo.aliases.join(", " + prefix)}` : "*(none)*"
                    send(`${args[1]} ${sep} Description: ${cmdinfo.about} ${sep} Aliases: ${aliases}`)
                } else send("That command doesn't exist.")
        }
    },
    about: {
        aliases: ['ab'],
        about: "Tells info about the script.",
        func: (...args) => {
            send(`${name} v${version} by ${author} ${sep} ccjt's site: ${link} ${sep} JMIDIPlayer module originally made by seq.wtf ${sep} Get this userscript at https://greasyfork.org/en/scripts/554578-jmidiplayer`)
        }
    },
    play: {
        aliases: ['p'],
        about: "Plays a MIDI file from the web.",
        func: (...args) => {
            let ogcmd = args[0]
            if (args.length === 1)
                send(`Please specify a direct download URL to the desired MIDI file to play. ${sep} Usage: \`${ogcmd} <URL>\``)
            else {
                function getFileName(url) {
                    let filename = ""
                    filename = url.split('/')
                    filename = filename[filename.length - 1]
                    filename = decodeURIComponent(filename)
                    return filename
                }
                player.stop()
                send("Downloading...")
                playing.name = getFileName(args[1])
                playMidiFromUrl(args[1])
            }
        }
    },
    stop: {
        aliases: ['s'],
        about: "Stops the current track.",
        func: (...args) => {
            loadNotes(false)
            player.stop()
            send("Stopped playing.")
        }
    },
    volume: {
        aliases: ['vol', 'v'],
        about: "Adjusts track volume.",
        func: (...args) => {
            let ogcmd = args[0]
            if (args.length === 1) {
                send(`Please specify a volume to set to. ${sep} Range: \`0.0 to 1.0\` ${sep} Usage: \`${ogcmd} <volume from 0.0 to 1.0>\``)
            } else {
                args[1] = parseFloat(args[1])
                if (args[1] >= 0 && args[1] <= 1) {
                    volume = args[1]
                    send(`Volume set to \`${volume}\`.`)
                } else {
                    send("That value is out of range. Please specify a value between `0.0` and `1.0`.")
                }
            }
        }
    },
    pause: {
        aliases: ['pa'],
        about: "Pauses and resumes a track.",
        func: (...args) => {
            if (!player.isLoading && player.trackCount > 0) {
                if (player.isPlaying) {
                    player.pause()
                    send("Paused track.")
                } else {
                    player.play()
                    send("Resumed track.")
                }
            } else send("Nothing loaded yet.")
        }
    },
    resume: {
        aliases: ['re'],
        about: "Resumes a track.",
        func: (...args) => {
            if (!player.isLoading && player.trackCount > 0) {
                player.play()
                send("Resumed track.")
            } else send("Nothing loaded yet.")
        }
    },
    transpose: {
        aliases: ['tr'],
        about: "Changes the key of the current track.",
        func: (...args) => {
            let ogcmd = args[0]
            if (args.length === 1)
                send(`Please specify a value between \`-24\` and \`36\`. ${sep} Usage: \`${ogcmd} <value>\``)
            else {
                args[1] = parseInt(args[1])
                if (isNaN(args[1]))
                    send("Please specify a *number*.")
                else
                    if (args[1] > 36 || args[1] < -24)
                        send("Please specify a value under `36` and above `-24`.")
                    else {
                        transpose = args[1]
                        send(`Transposition set to \`${transpose}\`. ||Note that this will reset after the track has finished.||`)
                    }
            }
        }
    },
    loop: {
        aliases: ['l'],
        about: "Toggles looping.",
        func: (...args) => {
            looping = !looping
            if (looping) send("Now looping track.")
            else send("Stopped looping track.")
        }
    },
    track: {
        aliases: ["t"],
        about: "Shows info about the current playing track.",
        func: (...args) => {
            if (!player.isLoading && player.trackCount > 0) {
                let remaining = ((currenttick * (60 * 1000 / (player.currentTempo * player.ppqn)) / 1000) / player.songTime) * 100
                let progressbar = []
                for (let i = 0; i < remaining / 10; i++) {
                    progressbar.push('█')
                }
                let secondsstr = ""
                let played = currenttick * (60 * 1000 / (player.currentTempo * player.ppqn)) / 1000
                let playedsecondsstr = ""
                let songtime = player.songTime
                if (songtime % 60 > 0) secondsstr = " " + parseInt(songtime % 60) + "s"
                if (played % 60 > 0) playedsecondsstr = " " + parseInt(played % 60) + "s"
                let length = songtime > 60 ? parseInt(songtime / 60) + "m" + secondsstr : songtime.toFixed(3) + "s"
                let lengthplayed = played > 60 ? parseInt(played / 60) + "m" + playedsecondsstr : played.toFixed(3) + "s"
                let totalevents = player.totalEvents
                let bpm = player.currentTempo
                send("`Track name: " + playing.name + " - Track BPM: " + bpm + " - Track length: " + length + " - [" + progressbar.join('').padEnd(10, " ") + "] - " + remaining.toFixed(2) + "% (" + lengthplayed + ") played  - Events played: " + eventsplayed + " out of " + totalevents + " - Looping: " + looping + "`")
            } else {
                send("Nothing loaded yet.")
            }
        }
    }
}
function createcmdstr() {
    let result = []
    for (let i = -1; ++i < Object.keys(cmds).length;) {
        result.push(prefix + Object.keys(cmds)[i])
        if (cmds[Object.keys(cmds)[i]].aliases.length > 0) {
            result[i] += " (" + prefix + cmds[Object.keys(cmds)[i]].aliases.join(', ' + prefix) + ")"
        }
    }
    return result.join(', ')
}
MPP.client.on('a', (data) => {
    if (data.p._id === MPP.client.getOwnParticipant()._id) {
        let args = data.a.split(' ')
        let cmd = args[0].toLowerCase()
        if (cmd.startsWith(prefix)) {
            cmd = cmd.substring(prefix.length)
            if (Object.keys(cmds).includes(cmd)) {
                cmds[cmd].func(...args)
            } else {
                for (let i = -1; ++i < Object.keys(cmds).length;) {
                    if (Object.values(cmds)[i].aliases.includes(cmd)) {
                        cmds[Object.keys(cmds)[i]].func(...args)
                    }
                }
            }
        }
    }
})