Bug 22581: Show and play musical inscripts
[koha.git] / koha-tmpl / opac-tmpl / lib / verovio / midiplayer.js
blobd54e3a2bed2020e2106218eed50f7c40529a1ee1
1 /************************************************************************
2  * Circular Web Audio Buffer Queue
3  */
4 function CircularAudioBuffer(slots) {
5     slots = slots || 24;
6     // number of buffers
7     this.slots = slots;
8     this.buffers = new Array(slots);
10     this.reset();
12     for (var i = 0; i < this.slots; i++) {
13         var buffer = audioCtx.createBuffer(channels, BUFFER, SAMPLE_RATE);
14         this.buffers[i] = buffer;
15     }
18 // pseudo empty all buffers
19 CircularAudioBuffer.prototype.reset = function () {
20     this.used = 0;
21     this.filled = 0;
24 // returns number of buffers that are filled
25 CircularAudioBuffer.prototype.filledBuffers = function () {
26     var fills = this.filled - this.used;
27     if (fills < 0) fills += this.slots;
28     return fills;
31 // returns whether buffers are all filled
32 CircularAudioBuffer.prototype.full = function () {
33     //console.debug(this.filledBuffers());
34     return this.filledBuffers() >= this.slots - 1;
37 // returns a reference to next available buffer to be filled
38 CircularAudioBuffer.prototype.prepare = function () {
39     if (this.full()) {
40         //console.log('buffers full!!')
41         return
42     }
43     var buffer = this.buffers[ this.filled++];
44     this.filled %= this.slots;
45     return buffer;
48 // returns the next buffer in the queue
49 CircularAudioBuffer.prototype.use = function () {
50     if (! this.filledBuffers()) return;
51     var buffer = this.buffers[ this.used++];
52     this.used %= this.slots;
53     return buffer;
56 /************************************************************************
57  * Web Audio Stuff
58  */
60 var SAMPLE_RATE = 44100;
61 var BUFFER = 4096;
62 var channels = 2;
64 var audioCtx;
65 var source;
66 var scriptNode;
67 var circularBuffer;
68 var emptyBuffer;
70 function initAudio() {
71     audioCtx = new (window.AudioContext || window.webkitAudioContext)();
72     scriptNode = audioCtx.createScriptProcessor(BUFFER, 0, channels);
73     scriptNode.onaudioprocess = onAudioProcess;
75     source = audioCtx.createBufferSource();
76     circularBuffer = new CircularAudioBuffer(8);
77     emptyBuffer = audioCtx.createBuffer(channels, BUFFER, SAMPLE_RATE);
79     source.connect(scriptNode);
80     source.start(0);
81     console.debug("initAudio");
84 function startAudio() {
85     scriptNode.connect(audioCtx.destination);
86     console.debug("startAudio");
89 function pauseAudio() {
90     circularBuffer.reset();
91     scriptNode.disconnect();
92         console.debug("pauseAudio");
96 /************************************************************************
97  * Emscripten variables and callback - cannot be renamed
98  */
100 var ULONG_MAX = 4294967295;
101 var _EM_signalStop = 0;
102 var _EM_seekSamples = ULONG_MAX;
104 function processAudio(buffer_loc, size) {
105     var buffer = circularBuffer.prepare();
106     var left_buffer_f32 = buffer.getChannelData(0);
107     var right_buffer_f32 = buffer.getChannelData(1);
109     // Copy emscripten memory (OpenAL stereo16 format) to JS
110     for (var i = 0; i < size; i++) {
111         left_buffer_f32[i] = MidiPlayer.HEAP16[(buffer_loc >> 1) + 2 * i + 0] / 32768;
112         right_buffer_f32[i] = MidiPlayer.HEAP16[(buffer_loc >> 1) + 2 * i + 1] / 32768;
113     }
116 function updateProgress(current, total) {
117     midiPlayer_currentSamples = current;
118     midiPlayer_totalSamples = total;
119     midiPlayer_progress.style.width = (current / total * 100) + '%';
120     midiPlayer_playingTime.innerHTML = samplesToTime(current);
121     midiPlayer_totalTime.innerHTML = samplesToTime(total);
123     var millisec = Math.floor(current * 1000 / SAMPLE_RATE / midiPlayer_updateRate);
124     if (midiPlayer_lastMillisec > millisec) {
125         midiPlayer_lastMillisec = 0;
126     }
127     if (millisec > midiPlayer_lastMillisec) {
128         if (midiPlayer_onUpdate != null) midiPlayer_onUpdate(millisec * midiPlayer_updateRate);
129         //console.log(millisec * UPDATE_RATE);
130     }
131     midiPlayer_lastMillisec = millisec;
134 function completeConversion(status) {
135     midiPlayer_drainBuffer = true;
136     console.debug('completeConversion');
137     midiPlayer_convertionJob = null;
138     // Not a pause
139     if (_EM_signalStop != 2) {
140         setTimeout(stop, 1000);
141     }
144 /************************************************************************
145  * Global player variables and functions
146  */
148 // html elements
149 var midiPlayer_width;
150 var midiPlayer_bar;
151 var midiPlayer_progress;
152 var midiPlayer_playingTime;
153 var midiPlayer_play;
154 var midiPlayer_pause;
155 var midiPlayer_stop;
156 var midiPlayer_totalTime;
158 // variables
159 var midiPlayer_isLoaded = false;
160 var midiPlayer_isAudioInit = false;
161 var midiPlayer_input = null;
162 var midiPlayer_lastMillisec = 0;
163 var midiPlayer_midiName = ''
164 var midiPlayer_convertionJob = null;
165 var midiPlayer_currentSamples = ULONG_MAX;
166 var midiPlayer_totalSamples = 0;
167 var midiPlayer_updateRate = 50;
168 var midiPlayer_drainBuffer = false;
169 var BASE64_MARKER = ';base64,';
171 // callbacks
172 var midiPlayer_onStop = null;
173 var midiPlayer_onUpdate = null;
175 var MidiPlayer = {
176     noInitialRun: true,
177     totalDependencies: 1,
178     monitorRunDependencies: function(left) {
179         //console.log(this.totalDependencies);
180         //console.log(left);
181         if ((left == 0) && !midiPlayer_isLoaded) {
182           console.log("MidiPlayer is loaded");
183           midiPlayer_isLoaded = true;
184         }
185     }
188 function onAudioProcess(audioProcessingEvent) {
189     var generated = circularBuffer.use();
191     if (!generated && midiPlayer_drainBuffer) {
192         // wait for remaining buffer to drain before disconnect audio
193         pauseAudio();
194         midiPlayer_drainBuffer = false;
195         return;
196     }
197     if (!generated) {
198         //console.log('buffer under run!!')
199         generated = emptyBuffer;
200     }
202     var outputBuffer = audioProcessingEvent.outputBuffer;
203     var offset = 0;
204     if (outputBuffer.copyToChannel !== undefined) {
205         // Firefox -> about 50% faster than decoding
206         outputBuffer.copyToChannel(generated.getChannelData(0), 0, offset);
207         outputBuffer.copyToChannel(generated.getChannelData(1), 1, offset);
208     } else {
209         // Other browsers -> about 20 - 70% slower than decoding
210         var leftChannel = outputBuffer.getChannelData(0);
211         var rightChannel = outputBuffer.getChannelData(1);
212         var generatedLeftChannel = generated.getChannelData(0);
213         var generatedRightChannel = generated.getChannelData(1);
214         var i;
215         for (i = 0; i < BUFFER; i++) {
216             leftChannel[i] = generatedLeftChannel[i];
217             rightChannel[i] = generatedRightChannel[i];
218         }
219     }
222 function samplesToTime(at) {
223     var in_s = Math.floor(at / SAMPLE_RATE);
224     var s = in_s % 60;
225     var min = in_s / 60 | 0;
226     return min + ':' + (s === 0 ? '00': s < 10 ? '0' + s: s);
229 function convertDataURIToBinary(dataURI) {
230     var base64Index = dataURI.indexOf(BASE64_MARKER) + BASE64_MARKER.length;
231     var base64 = dataURI.substring(base64Index);
232     var raw = window.atob(base64);
233     var rawLength = raw.length;
234     var array = new Uint8Array(new ArrayBuffer(rawLength));
236     for (var i = 0; i < rawLength; i++) {
237         array[i] = raw.charCodeAt(i);
238     }
239     return array;
242 function convertFile(file, data) {
243     midiPlayer_midiName = file;
244     midiPlayer_input = null;
245     console.log('open ', midiPlayer_midiName);
246     MidiPlayer['FS'].writeFile(midiPlayer_midiName, data, {
247         encoding: 'binary'
248     });
249     play();
252 function pause() {
253     _EM_signalStop = 2;
254     circularBuffer.reset();
255     midiPlayer_play.style.display = 'inline-block';
256     midiPlayer_pause.style.display = 'none';
259 function play() {
260     if (!midiPlayer_isLoaded) {
261         console.error("MidiPlayer is not loaded yet");
262         return;
263     }
264     if (!midiPlayer_isAudioInit) {
265           initAudio();
266           midiPlayer_isAudioInit = true;
267     }
269     _EM_seekSamples = midiPlayer_currentSamples;
270     if (midiPlayer_convertionJob) {
271         return;
272     }
274     _EM_signalStop = 0;
275     midiPlayer_play.style.display = 'none';
276     midiPlayer_pause.style.display = 'inline-block';
277     midiPlayer_stop.style.display = 'inline-block';
278     // add small delay so UI can update.
279     setTimeout(runConversion, 100);
282 function stop() {
283     _EM_signalStop = 1;
284     _EM_seekSamples = 0;
285     circularBuffer.reset();
287     midiPlayer_totalSamples = 0;
288     midiPlayer_currentSamples = ULONG_MAX;
289     midiPlayer_progress.style.width = '0%';
290     midiPlayer_playingTime.innerHTML = "00.00";
291     midiPlayer_totalTime.innerHTML = "00.00";
293     midiPlayer_play.style.display = 'none';
294     midiPlayer_pause.style.display = 'none';
295     midiPlayer_stop.style.display = 'none';
297     if (midiPlayer_onStop != null) midiPlayer_onStop();
300 function runConversion() {
301     midiPlayer_convertionJob = {
302         sourceMidi: midiPlayer_midiName,
303         targetWav: midiPlayer_midiName.replace(/\.midi?$/i, '.wav'),
304         targetPath: '',
305         conversion_start: Date.now()
306     };
308     var sleep = 10;
309     circularBuffer.reset();
310     startAudio();
312     console.log(midiPlayer_convertionJob);
314     MidiPlayer.ccall('wildwebmidi',
315         null,[ 'string', 'string', 'number'],[midiPlayer_convertionJob.sourceMidi, midiPlayer_convertionJob.targetPath, sleep], {
316             async: true
317         });
320 /************************************************************************
321  * jQuery player plugin
322  */
324 (function ($) {
326     $.fn.midiPlayer = function (options) {
328         var options = $.extend({
329             // These are the defaults.
330             color: "#556b2f",
331             backgroundColor: "white",
332             width: 500,
333             onStop: null,
334             onUpdate: null,
335             updateRate: 50,
336         },
337         options);
338         // width should not be less than 150
339         options.width = Math.max(options.width, 150);
340         // update rate should not be less than 10 milliseconds
341         options.updateRate = Math.max(options.updateRate, 10);
343         if(options.locateFile) MidiPlayer.locateFile = options.locateFile;
345         MidiModule(MidiPlayer);
347         $.fn.midiPlayer.play = function (song) {
348             if (midiPlayer_isLoaded == false) {
349                 midiPlayer_input = song;
350             }
351             else {
352                 var byteArray = convertDataURIToBinary(song);
353                 if (midiPlayer_totalSamples > 0) {
354                     stop();
355                     // a timeout is necessary because otherwise writing to the disk is not done
356                     setTimeout(function() {convertFile("player.midi", byteArray);}, 200);
357                 }
358                 else {
359                     convertFile("player.midi", byteArray);
360                 }
361             }
362         };
364         $.fn.midiPlayer.pause = function () {
365             pause();
366         };
368         $.fn.midiPlayer.seek = function (millisec) {
369             if (midiPlayer_totalSamples == 0) return;
370             var samples = millisec * SAMPLE_RATE / 1000;
371             midiPlayer_currentSamples = Math.min(midiPlayer_totalSamples, samples);
372             play();
373         };
375         $.fn.midiPlayer.stop = function () {
376             stop();
377         };
379         // Create the player
380         this.append("<div id=\"midiPlayer_div\"></div>");
381         $("#midiPlayer_div").append("<div id=\"midiPlayer_playingTime\">0:00</div>")
382             .append("<div id=\"midiPlayer_bar\"><div id=\"midiPlayer_progress\"></div></div>")
383             .append("<div id=\"midiPlayer_totalTime\">0:00</div>")
384             .append("<a class=\"icon play\" id=\"midiPlayer_play\" onclick=\"play()\"></a>")
385             .append("<a class=\"icon pause\" id=\"midiPlayer_pause\" onclick=\"pause()\"></a>")
386             .append("<a class=\"icon stop\" id=\"midiPlayer_stop\" onclick=\"stop()\"></a>");
388         $("#midiPlayer_div").css("width", options.width + 200);
389         $("#midiPlayer_bar").css("width", options.width);
390         $("#midiPlayer_progress").css("background", options.color);
392         // Assign the global variables
393         midiPlayer_onStop = options.onStop;
394         midiPlayer_onUpdate = options.onUpdate;
395         midiPlayer_updateRate = options.updateRate;
396         midiPlayer_bar = document.getElementById('midiPlayer_bar');
397         midiPlayer_progress = document.getElementById('midiPlayer_progress');
398         midiPlayer_playingTime = document.getElementById('midiPlayer_playingTime');
399         midiPlayer_play = document.getElementById('midiPlayer_play');
400         midiPlayer_pause = document.getElementById('midiPlayer_pause');
401         midiPlayer_stop = document.getElementById('midiPlayer_stop');
402         midiPlayer_totalTime = document.getElementById('midiPlayer_totalTime');
405         var pageDragStart = 0;
406         var barDragStart = 0;
407         midiPlayer_bar.addEventListener('mousedown', function (e) {
408             if (midiPlayer_totalSamples == 0) return;
409             pageDragStart = e.pageX;
410             barDragStart = e.offsetX;
411             updateDragging(e.pageX);
412         });
413         window.addEventListener('mousemove', function (e) {
414             if (pageDragStart != 0) {
415                 pause();
416                 updateDragging(e.pageX);
417             }
419         });
420         window.addEventListener('mouseup', function (e) {
421             if (pageDragStart == 0) return;
422             if (midiPlayer_totalSamples == 0) return;
423             pageDragStart = 0;
424             play();
425         });
427         function updateDragging(pageX) {
428             var posX =  barDragStart + (pageX - pageDragStart);
429             if (posX >= 0 && posX <= options.width) {
430                 var percent = posX / options.width;
431                 midiPlayer_currentSamples = percent * midiPlayer_totalSamples | 0;
432                 updateProgress(midiPlayer_currentSamples, midiPlayer_totalSamples);
433             }
434         }
436         return;
437     };
439 (jQuery));