music21j-port
Version:
A toolkit for computer-aided musicology, Javascript version
398 lines (360 loc) • 13 kB
JavaScript
/*
----------------------------------------------------------
MIDI.Player : 0.3.1 : 2015-03-26
----------------------------------------------------------
https://github.com/mudcube/MIDI.js
----------------------------------------------------------
*/
import { channels, GM } from './gm.js';
import { MidiFile } from '../inc/jasmid/midifile.js';
import { Replayer } from '../inc/jasmid/replayer.js';
export class PlayInstance {
constructor(plugin) {
this.plugin = plugin;
this.currentTime = 0;
this.endTime = 0;
this.restart = 0;
this.playing = false;
this.timeWarp = 1;
this.startDelay = 0;
this.BPM = 120;
this.eventQueue = []; // hold events to be triggered
this.queuedTime = 0.0; //
this.startTime = 0; // to measure time elapse
this.noteRegistrar = {}; // get event for requested note
this.onMidiEvent = undefined; // listener
this.frame = undefined;
this.__now = undefined;
}
start(onsuccess) {
if (this.currentTime < -1) {
this.currentTime = -1;
}
this.startAudio(this.currentTime, null, onsuccess);
}
resume(onsuccess) {
return this.start(onsuccess);
}
pause() {
const tmp = this.restart;
this.stopAudio();
this.restart = tmp;
};
stop() {
this.stopAudio();
this.restart = 0;
this.currentTime = 0;
}
addListener(onsuccess) {
this.onMidiEvent = onsuccess;
}
removeListener() {
this.onMidiEvent = undefined;
}
clearAnimation() {
if (this.animationFrameId) {
cancelAnimationFrame(mthis.animationFrameId);
}
}
setAnimation(callback) {
let currentTime = 0;
let tOurTime = 0;
let tTheirTime = 0;
this.clearAnimation();
this.frame = () => {
this.animationFrameId = requestAnimationFrame(this.frame);
//
if (this.endTime === 0) {
return;
}
if (this.playing) {
currentTime = (tTheirTime === this.currentTime) ? tOurTime - Date.now() : 0;
if (this.currentTime === 0) {
currentTime = 0;
} else {
currentTime = this.currentTime - currentTime;
}
if (tTheirTime !== this.currentTime) {
tOurTime = Date.now();
tTheirTime = this.currentTime;
}
} else { // paused
currentTime = this.currentTime;
}
const endTime = this.endTime;
// const percent = currentTime / endTime;
const total = currentTime / 1000;
const minutes = total / 60;
const seconds = total - (minutes * 60);
const t1 = minutes * 60 + seconds;
const t2 = (endTime / 1000);
if (t2 - t1 < -1.0) {
// noinspection UnnecessaryReturnStatementJS
return;
} else {
callback({
now: t1,
end: t2,
events: this.noteRegistrar
});
}
};
requestAnimationFrame(this.frame);
}
loadMidiFile(onsuccess, onprogress, onerror) {
try {
this.replayer = new Replayer(MidiFile(this.currentData), this.timeWarp, null, this.BPM);
this.data = this.replayer.getData();
this.endTime = this.getLength();
this.loadPlugin({
// instruments: this.getFileInstruments(),
onsuccess: onsuccess,
onprogress: onprogress,
onerror: onerror
});
} catch(event) {
onerror && onerror(event);
}
}
loadPlugin(...options) { // override in subclasses
}
loadFile(file, onsuccess, onprogress, onerror) {
this.stop();
if (file.indexOf('base64,') !== -1) {
this.currentData = atob(file.split(',')[1]);
this.loadMidiFile(onsuccess, onprogress, onerror);
} else {
const fetch = new XMLHttpRequest();
fetch.open('GET', file);
fetch.overrideMimeType('text/plain; charset=x-user-defined');
fetch.onreadystatechange = () => {
if (fetch.readyState === 4) {
if (fetch.status === 200) {
const t = fetch.responseText || '';
const ff = [];
const mx = t.length;
const scc = String.fromCharCode;
for (let z = 0; z < mx; z++) {
ff[z] = scc(t.charCodeAt(z) & 255);
}
this.currentData = ff.join('');
this.loadMidiFile(onsuccess, onprogress, onerror);
} else {
onerror && onerror('Unable to load MIDI file');
}
}
};
fetch.send();
}
}
getFileInstruments() {
const instruments = {};
const programs = {};
for (let n = 0; n < midi.data.length; n ++) {
const event = midi.data[n][0].event;
if (event.type !== 'channel') {
continue;
}
const channel = event.channel;
switch(event.subtype) {
case 'controller':
// console.log(event.channel, MIDI.defineControl[event.controllerType], event.value);
break;
case 'programChange':
programs[channel] = event.programNumber;
break;
case 'noteOn':
const program = programs[channel];
const gm = GM.byId[isFinite(program) ? program : channel];
instruments[gm.id] = true;
break;
}
}
const ret = [];
for (const key of Object.keys(instruments)) {
ret.push(key);
}
return ret;
}
// Playing the audio
scheduleTracking(channel, note, currentTime, offset, message, velocity, time) {
return setTimeout(() => {
const data = {
channel: channel,
note: note,
now: currentTime,
end: this.endTime,
message: message,
velocity: velocity
};
if (message === 128) {
delete this.noteRegistrar[note];
} else {
this.noteRegistrar[note] = data;
}
if (this.onMidiEvent) {
this.onMidiEvent(data);
}
this.currentTime = currentTime;
this.eventQueue.shift();
if (this.eventQueue.length < 1000) {
this.startAudio(this.queuedTime, true);
} else if (this.currentTime === this.queuedTime && this.queuedTime < this.endTime) { // grab next sequence
this.startAudio(this.queuedTime, true);
}
}, currentTime - offset);
}
getContext() {
if (MIDI.api === 'webaudio') {
return MIDI.WebAudio.getContext();
} else {
this.ctx = {currentTime: 0};
}
return this.ctx;
}
getLength() {
const data = this.data;
const length = data.length;
let totalTime = 0.5;
for (let n = 0; n < length; n++) {
totalTime += data[n][1];
}
return totalTime;
}
getNow() {
if (window.performance && window.performance.now) {
return window.performance.now();
} else {
return Date.now();
}
}
startAudio(currentTime, fromCache, onsuccess) {
if (!this.replayer) {
return;
}
if (!fromCache) {
if (typeof currentTime === 'undefined') {
currentTime = this.restart;
}
this.playing && this.stopAudio();
this.playing = true;
this.data = this.replayer.getData();
this.endTime = this.getLength();
}
let note;
let offset = 0;
let messages = 0;
const data = this.data;
const ctx = this.getContext();
const length = data.length;
this.queuedTime = 0.5;
const interval = this.eventQueue[0] && this.eventQueue[0].interval || 0;
const foffset = currentTime - this.currentTime;
if (MIDI.api !== 'webaudio') { // set currentTime on ctx
const now = this.getNow();
this.__now = this.__now || now;
ctx.currentTime = (now - this.__now) / 1000;
}
this.startTime = ctx.currentTime;
for (let n = 0; n < length && messages < 100; n++) {
const obj = data[n];
if ((this.queuedTime += obj[1]) <= currentTime) {
offset = this.queuedTime;
continue;
}
currentTime = this.queuedTime - offset;
const event = obj[0].event;
if (event.type !== 'channel') {
continue;
}
const channelId = event.channel;
const channel = channels[channelId];
const delay = ctx.currentTime + ((currentTime + foffset + this.startDelay) / 1000);
const queueTime = this.queuedTime - offset + this.startDelay;
switch (event.subtype) {
case 'controller':
MIDI.setController(channelId, event.controllerType, event.value, delay);
break;
case 'programChange':
MIDI.programChange(channelId, event.programNumber, delay);
break;
case 'pitchBend':
MIDI.pitchBend(channelId, event.value, delay);
break;
case 'noteOn':
if (channel.mute) break;
note = event.noteNumber - (this.MIDIOffset || 0);
this.eventQueue.push({
event: event,
time: queueTime,
source: MIDI.noteOn(channelId, event.noteNumber, event.velocity, delay),
interval: this.scheduleTracking(
channelId,
note,
this.queuedTime + this.startDelay,
offset - foffset,
144,
event.velocity)
});
messages++;
break;
case 'noteOff':
if (channel.mute) {
break;
}
note = event.noteNumber - (this.MIDIOffset || 0);
this.eventQueue.push({
event: event,
time: queueTime,
source: MIDI.noteOff(channelId, event.noteNumber, delay),
interval: this.scheduleTracking(
channelId,
note,
this.queuedTime,
offset - foffset,
128,
0)
});
break;
default:
break;
}
}
//
onsuccess && onsuccess(this.eventQueue);
}
stopAudio() {
const ctx = this.getContext();
this.playing = false;
this.restart += (ctx.currentTime - this.startTime) * 1000;
// stop the audio, and intervals
while (this.eventQueue.length) {
const o = this.eventQueue.pop();
window.clearInterval(o.interval);
if (!o.source) {
continue; // is not webaudio
}
if (typeof(o.source) === 'number') {
window.clearTimeout(o.source);
} else { // webaudio
o.source.disconnect(0);
}
}
// run callback to cancel any notes still playing
for (let key in this.noteRegistrar) {
const o = this.noteRegistrar[key]
if (this.noteRegistrar[key].message === 144 && this.onMidiEvent) {
this.onMidiEvent({
channel: o.channel,
note: o.note,
now: o.now,
end: o.end,
message: 128,
velocity: o.velocity
});
}
}
// reset noteRegistrar
this.noteRegistrar = {};
}
}