1 | /*
|
---|
2 | ----------------------------------------------------------
|
---|
3 | MIDI.Player : 0.3.1 : 2015-03-26
|
---|
4 | ----------------------------------------------------------
|
---|
5 | https://github.com/mudcube/MIDI.js
|
---|
6 | ----------------------------------------------------------
|
---|
7 | */
|
---|
8 |
|
---|
9 | if (typeof MIDI === 'undefined') MIDI = {};
|
---|
10 | if (typeof MIDI.Player === 'undefined') MIDI.Player = {};
|
---|
11 |
|
---|
12 | (function() { 'use strict';
|
---|
13 |
|
---|
14 | var midi = MIDI.Player;
|
---|
15 | midi.currentTime = 0;
|
---|
16 | midi.endTime = 0;
|
---|
17 | midi.restart = 0;
|
---|
18 | midi.playing = false;
|
---|
19 | midi.timeWarp = 1;
|
---|
20 | midi.startDelay = 0;
|
---|
21 | midi.BPM = 120;
|
---|
22 |
|
---|
23 | midi.start =
|
---|
24 | midi.resume = function(onsuccess) {
|
---|
25 | if (midi.currentTime < -1) {
|
---|
26 | midi.currentTime = -1;
|
---|
27 | }
|
---|
28 | startAudio(midi.currentTime, null, onsuccess);
|
---|
29 | };
|
---|
30 |
|
---|
31 | midi.pause = function() {
|
---|
32 | var tmp = midi.restart;
|
---|
33 | stopAudio();
|
---|
34 | midi.restart = tmp;
|
---|
35 | };
|
---|
36 |
|
---|
37 | midi.stop = function() {
|
---|
38 | stopAudio();
|
---|
39 | midi.restart = 0;
|
---|
40 | midi.currentTime = 0;
|
---|
41 | };
|
---|
42 |
|
---|
43 | midi.addListener = function(onsuccess) {
|
---|
44 | onMidiEvent = onsuccess;
|
---|
45 | };
|
---|
46 |
|
---|
47 | midi.removeListener = function() {
|
---|
48 | onMidiEvent = undefined;
|
---|
49 | };
|
---|
50 |
|
---|
51 | midi.clearAnimation = function() {
|
---|
52 | if (midi.animationFrameId) {
|
---|
53 | cancelAnimationFrame(midi.animationFrameId);
|
---|
54 | }
|
---|
55 | };
|
---|
56 |
|
---|
57 | midi.setAnimation = function(callback) {
|
---|
58 | var currentTime = 0;
|
---|
59 | var tOurTime = 0;
|
---|
60 | var tTheirTime = 0;
|
---|
61 | //
|
---|
62 | midi.clearAnimation();
|
---|
63 | ///
|
---|
64 | var frame = function() {
|
---|
65 | midi.animationFrameId = requestAnimationFrame(frame);
|
---|
66 | ///
|
---|
67 | if (midi.endTime === 0) {
|
---|
68 | return;
|
---|
69 | }
|
---|
70 | if (midi.playing) {
|
---|
71 | currentTime = (tTheirTime === midi.currentTime) ? tOurTime - Date.now() : 0;
|
---|
72 | if (midi.currentTime === 0) {
|
---|
73 | currentTime = 0;
|
---|
74 | } else {
|
---|
75 | currentTime = midi.currentTime - currentTime;
|
---|
76 | }
|
---|
77 | if (tTheirTime !== midi.currentTime) {
|
---|
78 | tOurTime = Date.now();
|
---|
79 | tTheirTime = midi.currentTime;
|
---|
80 | }
|
---|
81 | } else { // paused
|
---|
82 | currentTime = midi.currentTime;
|
---|
83 | }
|
---|
84 | ///
|
---|
85 | var endTime = midi.endTime;
|
---|
86 | var percent = currentTime / endTime;
|
---|
87 | var total = currentTime / 1000;
|
---|
88 | var minutes = total / 60;
|
---|
89 | var seconds = total - (minutes * 60);
|
---|
90 | var t1 = minutes * 60 + seconds;
|
---|
91 | var t2 = (endTime / 1000);
|
---|
92 | ///
|
---|
93 | if (t2 - t1 < -1.0) {
|
---|
94 | return;
|
---|
95 | } else {
|
---|
96 | callback({
|
---|
97 | now: t1,
|
---|
98 | end: t2,
|
---|
99 | events: noteRegistrar
|
---|
100 | });
|
---|
101 | }
|
---|
102 | };
|
---|
103 | ///
|
---|
104 | requestAnimationFrame(frame);
|
---|
105 | };
|
---|
106 |
|
---|
107 | // helpers
|
---|
108 |
|
---|
109 | midi.loadMidiFile = function(onsuccess, onprogress, onerror) {
|
---|
110 | try {
|
---|
111 | midi.replayer = new Replayer(MidiFile(midi.currentData), midi.timeWarp, null, midi.BPM);
|
---|
112 | midi.data = midi.replayer.getData();
|
---|
113 | midi.endTime = getLength();
|
---|
114 | ///
|
---|
115 | MIDI.loadPlugin({
|
---|
116 | // instruments: midi.getFileInstruments(),
|
---|
117 | onsuccess: onsuccess,
|
---|
118 | onprogress: onprogress,
|
---|
119 | onerror: onerror
|
---|
120 | });
|
---|
121 | } catch(event) {
|
---|
122 | onerror && onerror(event);
|
---|
123 | }
|
---|
124 | };
|
---|
125 |
|
---|
126 | midi.loadFile = function(file, onsuccess, onprogress, onerror) {
|
---|
127 | midi.stop();
|
---|
128 | if (file.indexOf('base64,') !== -1) {
|
---|
129 | var data = window.atob(file.split(',')[1]);
|
---|
130 | midi.currentData = data;
|
---|
131 | midi.loadMidiFile(onsuccess, onprogress, onerror);
|
---|
132 | } else {
|
---|
133 | var fetch = new XMLHttpRequest();
|
---|
134 | fetch.open('GET', file);
|
---|
135 | fetch.overrideMimeType('text/plain; charset=x-user-defined');
|
---|
136 | fetch.onreadystatechange = function() {
|
---|
137 | if (this.readyState === 4) {
|
---|
138 | if (this.status === 200) {
|
---|
139 | var t = this.responseText || '';
|
---|
140 | var ff = [];
|
---|
141 | var mx = t.length;
|
---|
142 | var scc = String.fromCharCode;
|
---|
143 | for (var z = 0; z < mx; z++) {
|
---|
144 | ff[z] = scc(t.charCodeAt(z) & 255);
|
---|
145 | }
|
---|
146 | ///
|
---|
147 | var data = ff.join('');
|
---|
148 | midi.currentData = data;
|
---|
149 | midi.loadMidiFile(onsuccess, onprogress, onerror);
|
---|
150 | } else {
|
---|
151 | onerror && onerror('Unable to load MIDI file');
|
---|
152 | }
|
---|
153 | }
|
---|
154 | };
|
---|
155 | fetch.send();
|
---|
156 | }
|
---|
157 | };
|
---|
158 |
|
---|
159 | midi.getFileInstruments = function() {
|
---|
160 | var instruments = {};
|
---|
161 | var programs = {};
|
---|
162 | for (var n = 0; n < midi.data.length; n ++) {
|
---|
163 | var event = midi.data[n][0].event;
|
---|
164 | if (event.type !== 'channel') {
|
---|
165 | continue;
|
---|
166 | }
|
---|
167 | var channel = event.channel;
|
---|
168 | switch(event.subtype) {
|
---|
169 | case 'controller':
|
---|
170 | // console.log(event.channel, MIDI.defineControl[event.controllerType], event.value);
|
---|
171 | break;
|
---|
172 | case 'programChange':
|
---|
173 | programs[channel] = event.programNumber;
|
---|
174 | break;
|
---|
175 | case 'noteOn':
|
---|
176 | var program = programs[channel];
|
---|
177 | var gm = MIDI.GM.byId[isFinite(program) ? program : channel];
|
---|
178 | instruments[gm.id] = true;
|
---|
179 | break;
|
---|
180 | }
|
---|
181 | }
|
---|
182 | var ret = [];
|
---|
183 | for (var key in instruments) {
|
---|
184 | ret.push(key);
|
---|
185 | }
|
---|
186 | return ret;
|
---|
187 | };
|
---|
188 |
|
---|
189 | // Playing the audio
|
---|
190 |
|
---|
191 | var eventQueue = []; // hold events to be triggered
|
---|
192 | var queuedTime; //
|
---|
193 | var startTime = 0; // to measure time elapse
|
---|
194 | var noteRegistrar = {}; // get event for requested note
|
---|
195 | var onMidiEvent = undefined; // listener
|
---|
196 | var scheduleTracking = function(channel, note, currentTime, offset, message, velocity, time) {
|
---|
197 | return setTimeout(function() {
|
---|
198 | var data = {
|
---|
199 | channel: channel,
|
---|
200 | note: note,
|
---|
201 | now: currentTime,
|
---|
202 | end: midi.endTime,
|
---|
203 | message: message,
|
---|
204 | velocity: velocity
|
---|
205 | };
|
---|
206 | //
|
---|
207 | if (message === 128) {
|
---|
208 | delete noteRegistrar[note];
|
---|
209 | } else {
|
---|
210 | noteRegistrar[note] = data;
|
---|
211 | }
|
---|
212 | if (onMidiEvent) {
|
---|
213 | onMidiEvent(data);
|
---|
214 | }
|
---|
215 | midi.currentTime = currentTime;
|
---|
216 | ///
|
---|
217 | eventQueue.shift();
|
---|
218 | ///
|
---|
219 | if (eventQueue.length < 1000) {
|
---|
220 | startAudio(queuedTime, true);
|
---|
221 | } else if (midi.currentTime === queuedTime && queuedTime < midi.endTime) { // grab next sequence
|
---|
222 | startAudio(queuedTime, true);
|
---|
223 | }
|
---|
224 | }, currentTime - offset);
|
---|
225 | };
|
---|
226 |
|
---|
227 | var getContext = function() {
|
---|
228 | if (MIDI.api === 'webaudio') {
|
---|
229 | return MIDI.WebAudio.getContext();
|
---|
230 | } else {
|
---|
231 | midi.ctx = {currentTime: 0};
|
---|
232 | }
|
---|
233 | return midi.ctx;
|
---|
234 | };
|
---|
235 |
|
---|
236 | var getLength = function() {
|
---|
237 | var data = midi.data;
|
---|
238 | var length = data.length;
|
---|
239 | var totalTime = 0.5;
|
---|
240 | for (var n = 0; n < length; n++) {
|
---|
241 | totalTime += data[n][1];
|
---|
242 | }
|
---|
243 | return totalTime;
|
---|
244 | };
|
---|
245 |
|
---|
246 | var __now;
|
---|
247 | var getNow = function() {
|
---|
248 | if (window.performance && window.performance.now) {
|
---|
249 | return window.performance.now();
|
---|
250 | } else {
|
---|
251 | return Date.now();
|
---|
252 | }
|
---|
253 | };
|
---|
254 |
|
---|
255 | var startAudio = function(currentTime, fromCache, onsuccess) {
|
---|
256 | if (!midi.replayer) {
|
---|
257 | return;
|
---|
258 | }
|
---|
259 | if (!fromCache) {
|
---|
260 | if (typeof currentTime === 'undefined') {
|
---|
261 | currentTime = midi.restart;
|
---|
262 | }
|
---|
263 | ///
|
---|
264 | midi.playing && stopAudio();
|
---|
265 | midi.playing = true;
|
---|
266 | midi.data = midi.replayer.getData();
|
---|
267 | midi.endTime = getLength();
|
---|
268 | }
|
---|
269 | ///
|
---|
270 | var note;
|
---|
271 | var offset = 0;
|
---|
272 | var messages = 0;
|
---|
273 | var data = midi.data;
|
---|
274 | var ctx = getContext();
|
---|
275 | var length = data.length;
|
---|
276 | //
|
---|
277 | queuedTime = 0.5;
|
---|
278 | ///
|
---|
279 | var interval = eventQueue[0] && eventQueue[0].interval || 0;
|
---|
280 | var foffset = currentTime - midi.currentTime;
|
---|
281 | ///
|
---|
282 | if (MIDI.api !== 'webaudio') { // set currentTime on ctx
|
---|
283 | var now = getNow();
|
---|
284 | __now = __now || now;
|
---|
285 | ctx.currentTime = (now - __now) / 1000;
|
---|
286 | }
|
---|
287 | ///
|
---|
288 | startTime = ctx.currentTime;
|
---|
289 | ///
|
---|
290 | for (var n = 0; n < length && messages < 100; n++) {
|
---|
291 | var obj = data[n];
|
---|
292 | if ((queuedTime += obj[1]) <= currentTime) {
|
---|
293 | offset = queuedTime;
|
---|
294 | continue;
|
---|
295 | }
|
---|
296 | ///
|
---|
297 | currentTime = queuedTime - offset;
|
---|
298 | ///
|
---|
299 | var event = obj[0].event;
|
---|
300 | if (event.type !== 'channel') {
|
---|
301 | continue;
|
---|
302 | }
|
---|
303 | ///
|
---|
304 | var channelId = event.channel;
|
---|
305 | var channel = MIDI.channels[channelId];
|
---|
306 | var delay = ctx.currentTime + ((currentTime + foffset + midi.startDelay) / 1000);
|
---|
307 | var queueTime = queuedTime - offset + midi.startDelay;
|
---|
308 | switch (event.subtype) {
|
---|
309 | case 'controller':
|
---|
310 | MIDI.setController(channelId, event.controllerType, event.value, delay);
|
---|
311 | break;
|
---|
312 | case 'programChange':
|
---|
313 | MIDI.programChange(channelId, event.programNumber, delay);
|
---|
314 | break;
|
---|
315 | case 'pitchBend':
|
---|
316 | MIDI.pitchBend(channelId, event.value, delay);
|
---|
317 | break;
|
---|
318 | case 'noteOn':
|
---|
319 | if (channel.mute) break;
|
---|
320 | note = event.noteNumber - (midi.MIDIOffset || 0);
|
---|
321 | eventQueue.push({
|
---|
322 | event: event,
|
---|
323 | time: queueTime,
|
---|
324 | source: MIDI.noteOn(channelId, event.noteNumber, event.velocity, delay),
|
---|
325 | interval: scheduleTracking(channelId, note, queuedTime + midi.startDelay, offset - foffset, 144, event.velocity)
|
---|
326 | });
|
---|
327 | messages++;
|
---|
328 | break;
|
---|
329 | case 'noteOff':
|
---|
330 | if (channel.mute) break;
|
---|
331 | note = event.noteNumber - (midi.MIDIOffset || 0);
|
---|
332 | eventQueue.push({
|
---|
333 | event: event,
|
---|
334 | time: queueTime,
|
---|
335 | source: MIDI.noteOff(channelId, event.noteNumber, delay),
|
---|
336 | interval: scheduleTracking(channelId, note, queuedTime, offset - foffset, 128, 0)
|
---|
337 | });
|
---|
338 | break;
|
---|
339 | default:
|
---|
340 | break;
|
---|
341 | }
|
---|
342 | }
|
---|
343 | ///
|
---|
344 | onsuccess && onsuccess(eventQueue);
|
---|
345 | };
|
---|
346 |
|
---|
347 | var stopAudio = function() {
|
---|
348 | var ctx = getContext();
|
---|
349 | midi.playing = false;
|
---|
350 | midi.restart += (ctx.currentTime - startTime) * 1000;
|
---|
351 | // stop the audio, and intervals
|
---|
352 | while (eventQueue.length) {
|
---|
353 | var o = eventQueue.pop();
|
---|
354 | window.clearInterval(o.interval);
|
---|
355 | if (!o.source) continue; // is not webaudio
|
---|
356 | if (typeof(o.source) === 'number') {
|
---|
357 | window.clearTimeout(o.source);
|
---|
358 | } else { // webaudio
|
---|
359 | o.source.disconnect(0);
|
---|
360 | }
|
---|
361 | }
|
---|
362 | // run callback to cancel any notes still playing
|
---|
363 | for (var key in noteRegistrar) {
|
---|
364 | var o = noteRegistrar[key]
|
---|
365 | if (noteRegistrar[key].message === 144 && onMidiEvent) {
|
---|
366 | onMidiEvent({
|
---|
367 | channel: o.channel,
|
---|
368 | note: o.note,
|
---|
369 | now: o.now,
|
---|
370 | end: o.end,
|
---|
371 | message: 128,
|
---|
372 | velocity: o.velocity
|
---|
373 | });
|
---|
374 | }
|
---|
375 | }
|
---|
376 | // reset noteRegistrar
|
---|
377 | noteRegistrar = {};
|
---|
378 | };
|
---|
379 |
|
---|
380 | })(); |
---|