// ==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)
}
}
}
}
}
})