abcjs-rails 2.3 → 3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/abcjs/api/abc_animation.js +166 -4
  3. data/app/assets/javascripts/abcjs/api/abc_tunebook.js +170 -15
  4. data/app/assets/javascripts/abcjs/data/abc_tune.js +69 -47
  5. data/app/assets/javascripts/abcjs/edit/abc_editor.js +78 -37
  6. data/app/assets/javascripts/abcjs/midi/abc_midi_controls.js +513 -0
  7. data/app/assets/javascripts/abcjs/midi/abc_midi_create.js +29 -7
  8. data/app/assets/javascripts/abcjs/midi/abc_midi_flattener.js +21 -18
  9. data/app/assets/javascripts/abcjs/midi/abc_midi_js_preparer.js +233 -0
  10. data/app/assets/javascripts/abcjs/midi/abc_midi_renderer.js +3 -229
  11. data/app/assets/javascripts/abcjs/midi/abc_midi_sequencer.js +11 -3
  12. data/app/assets/javascripts/abcjs/parse/abc_common.js +24 -0
  13. data/app/assets/javascripts/abcjs/parse/abc_parse.js +58 -16
  14. data/app/assets/javascripts/abcjs/parse/abc_parse_header.js +4 -1
  15. data/app/assets/javascripts/abcjs/parse/abc_parse_key_voice.js +5 -0
  16. data/app/assets/javascripts/abcjs/write/abc_absolute_element.js +9 -2
  17. data/app/assets/javascripts/abcjs/write/abc_abstract_engraver.js +71 -19
  18. data/app/assets/javascripts/abcjs/write/abc_beam_element.js +11 -3
  19. data/app/assets/javascripts/abcjs/write/abc_brace_element.js +51 -0
  20. data/app/assets/javascripts/abcjs/write/abc_create_clef.js +3 -3
  21. data/app/assets/javascripts/abcjs/write/abc_create_key_signature.js +3 -3
  22. data/app/assets/javascripts/abcjs/write/abc_create_time_signature.js +3 -3
  23. data/app/assets/javascripts/abcjs/write/abc_crescendo_element.js +4 -1
  24. data/app/assets/javascripts/abcjs/write/abc_decoration.js +1 -1
  25. data/app/assets/javascripts/abcjs/write/abc_engraver_controller.js +10 -9
  26. data/app/assets/javascripts/abcjs/write/abc_glyphs.js +1 -1
  27. data/app/assets/javascripts/abcjs/write/abc_relative_element.js +1 -1
  28. data/app/assets/javascripts/abcjs/write/abc_renderer.js +85 -13
  29. data/app/assets/javascripts/abcjs/write/abc_staff_group_element.js +16 -0
  30. data/app/assets/javascripts/abcjs/write/abc_tempo_element.js +31 -11
  31. data/app/assets/javascripts/abcjs/write/abc_tie_element.js +8 -1
  32. data/lib/abcjs-rails/version.rb +1 -1
  33. metadata +6 -3
@@ -371,17 +371,36 @@ window.ABCJS.data.Tune = function() {
371
371
  //}
372
372
  }
373
373
 
374
+ function getNextMusicLine(lines, currentLine) {
375
+ currentLine++;
376
+ while (lines.length > currentLine) {
377
+ if (lines[currentLine].staff)
378
+ return lines[currentLine];
379
+ }
380
+ return null;
381
+ }
382
+
374
383
  for (this.lineNum = 0; this.lineNum < this.lines.length; this.lineNum++) {
375
- if (this.lines[this.lineNum].staff) for (this.staffNum = 0; this.staffNum < this.lines[this.lineNum].staff.length; this.staffNum++) {
376
- if (this.lines[this.lineNum].staff[this.staffNum].clef)
377
- fixClefPlacement(this.lines[this.lineNum].staff[this.staffNum].clef);
378
- for (this.voiceNum = 0; this.voiceNum < this.lines[this.lineNum].staff[this.staffNum].voices.length; this.voiceNum++) {
379
- // var el = this.getLastNote();
380
- // if (el) el.end_beam = true;
381
- cleanUpSlursInLine(this.lines[this.lineNum].staff[this.staffNum].voices[this.voiceNum]);
382
- for (var j = 0; j < this.lines[this.lineNum].staff[this.staffNum].voices[this.voiceNum].length; j++)
383
- if (this.lines[this.lineNum].staff[this.staffNum].voices[this.voiceNum][j].el_type === 'clef')
384
- fixClefPlacement(this.lines[this.lineNum].staff[this.staffNum].voices[this.voiceNum][j]);
384
+ var staff = this.lines[this.lineNum].staff;
385
+ if (staff) {
386
+ for (this.staffNum = 0; this.staffNum < staff.length; this.staffNum++) {
387
+ if (staff[this.staffNum].clef)
388
+ fixClefPlacement(staff[this.staffNum].clef);
389
+ for (this.voiceNum = 0; this.voiceNum < staff[this.staffNum].voices.length; this.voiceNum++) {
390
+ var voice = staff[this.staffNum].voices[this.voiceNum];
391
+ cleanUpSlursInLine(voice);
392
+ for (var j = 0; j < voice.length; j++) {
393
+ if (voice[j].el_type === 'clef')
394
+ fixClefPlacement(voice[j]);
395
+ }
396
+ if (voice[voice.length-1].barNumber) {
397
+ // Don't hang a bar number on the last bar line: it should go on the next line.
398
+ var nextLine = getNextMusicLine(this.lines, this.lineNum);
399
+ if (nextLine)
400
+ nextLine.staff[0].barNumber = voice[voice.length-1].barNumber;
401
+ delete voice[voice.length-1].barNumber;
402
+ }
403
+ }
385
404
  }
386
405
  }
387
406
  }
@@ -534,46 +553,48 @@ window.ABCJS.data.Tune = function() {
534
553
  // Clone the object because it will be sticking around for the next line and we don't want the extra fields in it.
535
554
  var hashParams = window.ABCJS.parse.clone(hashParams2);
536
555
 
537
- // If this is a clef type, then we replace the working clef on the line. This is kept separate from
538
- // the clef in case there is an inline clef field. We need to know what the current position for
539
- // the note is.
540
- if (type === 'clef')
541
- this.lines[this.lineNum].staff[this.staffNum].workingClef = hashParams;
542
-
543
- // If this is the first item in this staff, then we might have to initialize the staff, first.
544
- if (this.lines[this.lineNum].staff.length <= this.staffNum) {
545
- this.lines[this.lineNum].staff[this.staffNum] = {};
546
- this.lines[this.lineNum].staff[this.staffNum].clef = window.ABCJS.parse.clone(this.lines[this.lineNum].staff[0].clef);
547
- this.lines[this.lineNum].staff[this.staffNum].key = window.ABCJS.parse.clone(this.lines[this.lineNum].staff[0].key);
548
- this.lines[this.lineNum].staff[this.staffNum].meter = window.ABCJS.parse.clone(this.lines[this.lineNum].staff[0].meter);
549
- this.lines[this.lineNum].staff[this.staffNum].workingClef = window.ABCJS.parse.clone(this.lines[this.lineNum].staff[0].workingClef);
550
- this.lines[this.lineNum].staff[this.staffNum].voices = [[]];
551
- }
552
-
553
- // These elements should not be added twice, so if the element exists on this line without a note or bar before it, just replace the staff version.
554
- var voice = this.lines[this.lineNum].staff[this.staffNum].voices[this.voiceNum];
555
- for (var i = 0; i < voice.length; i++) {
556
- if (voice[i].el_type === 'note' || voice[i].el_type === 'bar') {
557
- hashParams.el_type = type;
558
- hashParams.startChar = startChar;
559
- hashParams.endChar = endChar;
560
- if (impliedNaturals)
561
- hashParams.accidentals = impliedNaturals.concat(hashParams.accidentals);
562
- voice.push(hashParams);
563
- return;
556
+ if (this.lines[this.lineNum].staff) { // be sure that we are on a music type line before doing the following.
557
+ // If this is a clef type, then we replace the working clef on the line. This is kept separate from
558
+ // the clef in case there is an inline clef field. We need to know what the current position for
559
+ // the note is.
560
+ if (type === 'clef')
561
+ this.lines[this.lineNum].staff[this.staffNum].workingClef = hashParams;
562
+
563
+ // If this is the first item in this staff, then we might have to initialize the staff, first.
564
+ if (this.lines[this.lineNum].staff.length <= this.staffNum) {
565
+ this.lines[this.lineNum].staff[this.staffNum] = {};
566
+ this.lines[this.lineNum].staff[this.staffNum].clef = window.ABCJS.parse.clone(this.lines[this.lineNum].staff[0].clef);
567
+ this.lines[this.lineNum].staff[this.staffNum].key = window.ABCJS.parse.clone(this.lines[this.lineNum].staff[0].key);
568
+ this.lines[this.lineNum].staff[this.staffNum].meter = window.ABCJS.parse.clone(this.lines[this.lineNum].staff[0].meter);
569
+ this.lines[this.lineNum].staff[this.staffNum].workingClef = window.ABCJS.parse.clone(this.lines[this.lineNum].staff[0].workingClef);
570
+ this.lines[this.lineNum].staff[this.staffNum].voices = [[]];
564
571
  }
565
- if (voice[i].el_type === type) {
566
- hashParams.el_type = type;
567
- hashParams.startChar = startChar;
568
- hashParams.endChar = endChar;
569
- if (impliedNaturals)
570
- hashParams.accidentals = impliedNaturals.concat(hashParams.accidentals);
571
- voice[i] = hashParams;
572
- return;
572
+
573
+ // These elements should not be added twice, so if the element exists on this line without a note or bar before it, just replace the staff version.
574
+ var voice = this.lines[this.lineNum].staff[this.staffNum].voices[this.voiceNum];
575
+ for (var i = 0; i < voice.length; i++) {
576
+ if (voice[i].el_type === 'note' || voice[i].el_type === 'bar') {
577
+ hashParams.el_type = type;
578
+ hashParams.startChar = startChar;
579
+ hashParams.endChar = endChar;
580
+ if (impliedNaturals)
581
+ hashParams.accidentals = impliedNaturals.concat(hashParams.accidentals);
582
+ voice.push(hashParams);
583
+ return;
584
+ }
585
+ if (voice[i].el_type === type) {
586
+ hashParams.el_type = type;
587
+ hashParams.startChar = startChar;
588
+ hashParams.endChar = endChar;
589
+ if (impliedNaturals)
590
+ hashParams.accidentals = impliedNaturals.concat(hashParams.accidentals);
591
+ voice[i] = hashParams;
592
+ return;
593
+ }
573
594
  }
595
+ // We didn't see either that type or a note, so replace the element to the staff.
596
+ this.lines[this.lineNum].staff[this.staffNum][type] = hashParams2;
574
597
  }
575
- // We didn't see either that type or a note, so replace the element to the staff.
576
- this.lines[this.lineNum].staff[this.staffNum][type] = hashParams2;
577
598
  };
578
599
 
579
600
  this.getNumLines = function() {
@@ -675,6 +696,7 @@ window.ABCJS.data.Tune = function() {
675
696
  if (params.bracket) This.lines[This.lineNum].staff[This.staffNum].bracket = params.bracket;
676
697
  if (params.brace) This.lines[This.lineNum].staff[This.staffNum].brace = params.brace;
677
698
  if (params.connectBarLines) This.lines[This.lineNum].staff[This.staffNum].connectBarLines = params.connectBarLines;
699
+ if (params.barNumber) This.lines[This.lineNum].staff[This.staffNum].barNumber = params.barNumber;
678
700
  createVoice(params);
679
701
  // Some stuff just happens for the first voice
680
702
  if (params.part)
@@ -112,6 +112,7 @@ window.ABCJS.edit.EditArea.prototype.getElem = function() {
112
112
  // canvas_id: or paper_id: HTML id to draw in. If not present, then the drawing happens just below the editor.
113
113
  // generate_midi: if present, then midi is generated.
114
114
  // midi_id: if present, the HTML id to place the midi control. Otherwise it is placed in the same div as the paper.
115
+ // midi_download_id: if present, the HTML id to place the midi download link. Otherwise it is placed in the same div as the paper.
115
116
  // generate_warnings: if present, then parser warnings are displayed on the page.
116
117
  // warnings_id: if present, the HTML id to place the warnings. Otherwise they are placed in the same div as the paper.
117
118
  // onchange: if present, the callback function to call whenever there has been a change.
@@ -172,15 +173,25 @@ window.ABCJS.Editor = function(editarea, params) {
172
173
  this.div = document.createElement("DIV");
173
174
  this.editarea.getElem().parentNode.insertBefore(this.div, this.editarea.getElem());
174
175
  }
175
-
176
- if (params.generate_midi || params.midi_id) {
177
- if (params.midi_id) {
178
- this.mididiv = document.getElementById(params.midi_id);
179
- } else {
180
- this.mididiv = this.div;
181
- }
182
- }
183
-
176
+
177
+ // If the user wants midi, then store the elements that it will be written to. The element could either be passed in as an id,
178
+ // an element, or nothing. If nothing is passed in, then just put the midi on top of the generated music.
179
+ if (params.generate_midi) {
180
+ this.midiParams = params.midi_options || {};
181
+ if (this.midiParams.generateDownload) {
182
+ if (typeof params.midi_download_id === 'string')
183
+ this.downloadMidi = document.getElementById(params.midi_download_id);
184
+ else if (params.midi_download_id) // assume, if the var is not a string it is an element. If not, it will crash soon enough.
185
+ this.downloadMidi = params.midi_download_id;
186
+ }
187
+ if (this.midiParams.generateInline !== false) { // The default for this is true, so undefined is also true.
188
+ if (typeof params.midi_id === 'string')
189
+ this.inlineMidi = document.getElementById(params.midi_id);
190
+ else if (params.midi_id) // assume, if the var is not a string it is an element. If not, it will crash soon enough.
191
+ this.inlineMidi = params.midi_id;
192
+ }
193
+ }
194
+
184
195
  if (params.generate_warnings || params.warnings_id) {
185
196
  if (params.warnings_id) {
186
197
  this.warningsdiv = document.getElementById(params.warnings_id);
@@ -190,7 +201,6 @@ window.ABCJS.Editor = function(editarea, params) {
190
201
  }
191
202
 
192
203
  this.parserparams = params.parser_options || {};
193
- this.midiparams = params.midi_options || {};
194
204
  this.onchangeCallback = params.onchange;
195
205
 
196
206
  this.engraverparams = params.render_options || {};
@@ -247,8 +257,10 @@ window.ABCJS.Editor.prototype.renderTune = function(abc, params, div) {
247
257
 
248
258
  window.ABCJS.Editor.prototype.modelChanged = function() {
249
259
  if (this.tunes === undefined) {
250
- if (this.mididiv !== undefined && this.mididiv !== this.div)
251
- this.mididiv.innerHTML = "";
260
+ if (this.downloadMidi !== undefined)
261
+ this.downloadMidi.innerHTML = "";
262
+ if (this.inlineMidi !== undefined)
263
+ this.inlineMidi.innerHTML = "";
252
264
  this.div.innerHTML = "";
253
265
  return;
254
266
  }
@@ -262,19 +274,51 @@ window.ABCJS.Editor.prototype.modelChanged = function() {
262
274
  this.engraver_controller = new ABCJS.write.EngraverController(paper, this.engraverparams);
263
275
  this.engraver_controller.engraveABC(this.tunes);
264
276
  this.tunes[0].engraver = this.engraver_controller; // TODO-PER: We actually want an output object for each tune, not the entire controller. When refactoring, don't save data in the controller.
265
- if (window.ABCJS.midi && this.mididiv) {
266
- // if (this.mididiv !== this.div)
267
- // this.mididiv.innerHTML = "";
268
- var midi = window.ABCJS.midi.create(this.tunes[0], this.midiParams); //TODO handle multiple tunes
269
- var title = this.tunes[0].metaText && this.tunes[0].metaText.title ? this.tunes[0].metaText.title : 'Untitled';
270
- var linkTitle = "Download MIDI for \"" + title + "\"";
271
- if (title)
272
- title = title.toLowerCase().replace(/'/g, '').replace(/\W/g, '_').replace(/__/g, '_');
273
- this.mididiv.innerHTML = '<a download="' + title + '.midi" href="' + midi + '">' + linkTitle + '</a>';
274
- // var midiwriter = new ABCJS.midi.MidiWriter(this.mididiv,this.midiparams);
275
- // midiwriter.addListener(this.engraver_controller);
276
- // midiwriter.writeABC(this.tunes[0]); //TODO handle multiple tunes
277
- }
277
+ var downloadMidiHtml = "";
278
+ var inlineMidiHtml = "";
279
+ if (this.midiParams && !this.midiPause) {
280
+ for (var i = 0; i < this.tunes.length; i++) {
281
+ var midi = window.ABCJS.midi.create(this.tunes[i], this.midiParams);
282
+
283
+ if (this.midiParams.generateInline && this.midiParams.generateDownload) {
284
+ downloadMidiHtml += window.ABCJS.midi.generateMidiDownloadLink(this.tunes[i], this.midiParams, midi.download, i);
285
+ inlineMidiHtml += window.ABCJS.midi.generateMidiControls(this.tunes[i], this.midiParams, midi.inline, i);
286
+ } else if (this.midiParams.generateInline)
287
+ inlineMidiHtml += window.ABCJS.midi.generateMidiControls(this.tunes[i], this.midiParams, midi, i);
288
+ else
289
+ downloadMidiHtml += window.ABCJS.midi.generateMidiDownloadLink(this.tunes[i], this.midiParams, midi, i);
290
+ }
291
+ if (this.midiParams.generateDownload) {
292
+ if (this.downloadMidi)
293
+ this.downloadMidi.innerHTML = downloadMidiHtml;
294
+ else
295
+ this.div.innerHTML += downloadMidiHtml;
296
+ }
297
+ var find = function(element, cls) {
298
+ var els = element.getElementsByClassName(cls);
299
+ if (els.length === 0)
300
+ return null;
301
+ return els[0];
302
+ };
303
+ if (this.midiParams.generateInline) {
304
+ var inlineDiv;
305
+ if (this.inlineMidi) {
306
+ this.inlineMidi.innerHTML = inlineMidiHtml;
307
+ inlineDiv = this.inlineMidi;
308
+ } else {
309
+ this.div.innerHTML += inlineMidiHtml;
310
+ inlineDiv = this.div;
311
+ }
312
+ if (this.midiParams.animate || this.midiParams.listener) {
313
+ for (i = 0; i < this.tunes.length; i++) {
314
+ var parent = find(inlineDiv, "abcjs-midi-" + i);
315
+ parent.abcjsTune = this.tunes[i];
316
+ parent.abcjsListener = this.midiParams.listener;
317
+ parent.abcjsAnimate = this.midiParams.animate;
318
+ }
319
+ }
320
+ }
321
+ }
278
322
  if (this.warningsdiv) {
279
323
  this.warningsdiv.innerHTML = (this.warnings) ? this.warnings.join("<br />") : "No errors";
280
324
  }
@@ -311,11 +355,13 @@ window.ABCJS.Editor.prototype.parseABC = function() {
311
355
  var tunebook = new ABCJS.TuneBook(t);
312
356
 
313
357
  this.tunes = [];
358
+ this.startPos = [];
314
359
  this.warnings = [];
315
360
  for (var i=0; i<tunebook.tunes.length; i++) {
316
361
  var abcParser = new window.ABCJS.parse.Parse();
317
- abcParser.parse(tunebook.tunes[i].abc, this.parserparams); //TODO handle multiple tunes
362
+ abcParser.parse(tunebook.tunes[i].abc, this.parserparams);
318
363
  this.tunes[i] = abcParser.getTune();
364
+ this.startPos[i] = tunebook.tunes[i].startPos;
319
365
  var warnings = abcParser.getWarnings() || [];
320
366
  for (var j=0; j<warnings.length; j++) {
321
367
  this.warnings.push(warnings[j]);
@@ -398,8 +444,11 @@ window.ABCJS.Editor.prototype.isDirty = function() {
398
444
  return this.editarea.initialText !== this.editarea.getString();
399
445
  };
400
446
 
401
- window.ABCJS.Editor.prototype.highlight = function(abcelem) {
402
- this.editarea.setSelection(abcelem.startChar, abcelem.endChar);
447
+ window.ABCJS.Editor.prototype.highlight = function(abcelem, tuneNumber) {
448
+ // TODO-PER: The marker appears to get off by one for each tune parsed. I'm not sure why, but adding the tuneNumber in corrects it for the time being.
449
+ var offset = (tuneNumber !== undefined) ? this.startPos[tuneNumber] + tuneNumber : 0;
450
+
451
+ this.editarea.setSelection(offset + abcelem.startChar, offset + abcelem.endChar);
403
452
  };
404
453
 
405
454
  window.ABCJS.Editor.prototype.pause = function(shouldPause) {
@@ -409,13 +458,5 @@ window.ABCJS.Editor.prototype.pause = function(shouldPause) {
409
458
  };
410
459
 
411
460
  window.ABCJS.Editor.prototype.pauseMidi = function(shouldPause) {
412
- if (shouldPause && this.mididiv) {
413
- this.mididivSave = this.mididiv;
414
- this.addClassName(this.mididiv, 'hidden');
415
- this.mididiv = null;
416
- } else if (!shouldPause && this.mididivSave) {
417
- this.mididiv = this.mididivSave;
418
- this.removeClassName(this.mididiv, 'hidden');
419
- this.mididivSave = null;
420
- }
461
+ this.midiPause = shouldPause;
421
462
  };
@@ -0,0 +1,513 @@
1
+ // abc_midi_controls.js: Handle the visual part of playing MIDI
2
+ // Copyright (C) 2010,2016 Gregory Dyke (gregdyke at gmail dot com) and Paul Rosen
3
+ //
4
+ // This program is free software: you can redistribute it and/or modify
5
+ // it under the terms of the GNU General Public License as published by
6
+ // the Free Software Foundation, either version 3 of the License, or
7
+ // (at your option) any later version.
8
+ //
9
+ // This program is distributed in the hope that it will be useful,
10
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ // GNU General Public License for more details.
13
+ //
14
+ // You should have received a copy of the GNU General Public License
15
+ // along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ /*globals MIDI */
18
+ if (!window.ABCJS)
19
+ window.ABCJS = {};
20
+
21
+ if (!window.ABCJS.midi)
22
+ window.ABCJS.midi = {};
23
+
24
+ (function() {
25
+ "use strict";
26
+ function isFunction(functionToCheck) {
27
+ var getType = {};
28
+ return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
29
+ }
30
+
31
+ window.ABCJS.midi.generateMidiDownloadLink = function(tune, midiParams, midi, index) {
32
+ var html = '<div class="download-midi midi-' + index + '">';
33
+ if (midiParams.preTextDownload)
34
+ html += midiParams.preTextDownload;
35
+ var title = tune.metaText && tune.metaText.title ? tune.metaText.title : 'Untitled';
36
+ var label;
37
+ if (midiParams.downloadLabel && isFunction(midiParams.downloadLabel))
38
+ label = midiParams.downloadLabel(tune, index);
39
+ else if (midiParams.downloadLabel)
40
+ label = midiParams.downloadLabel.replace(/%T/, title);
41
+ else
42
+ label = "Download MIDI for \"" + title + "\"";
43
+ title = title.toLowerCase().replace(/'/g, '').replace(/\W/g, '_').replace(/__/g, '_');
44
+ html += '<a download="' + title + '.midi" href="' + midi + '">' + label + '</a>';
45
+ if (midiParams.postTextDownload)
46
+ html += midiParams.postTextDownload;
47
+ return html + "</div>";
48
+ };
49
+
50
+ function preprocessLabel(label, title) {
51
+ return label.replace(/%T/g, title);
52
+ }
53
+
54
+ window.ABCJS.midi.generateMidiControls = function(tune, midiParams, midi, index) {
55
+ if (window.ABCJS.midiInlineInitialized === 'failed')
56
+ return '<div class="abcjs-inline-midi abcjs-midi-' + index + '">ERROR</div>';
57
+ if (window.ABCJS.midiInlineInitialized === 'not loaded')
58
+ return '<div class="abcjs-inline-midi abcjs-midi-' + index + '">MIDI NOT PRESENT</div>';
59
+
60
+ var title = tune.metaText && tune.metaText.title ? tune.metaText.title : 'Untitled';
61
+ var options = midiParams.inlineControls || {};
62
+ if (options.standard === undefined) options.standard = true;
63
+
64
+ if (options.tooltipSelection === undefined) options.tooltipSelection = "Click to toggle play selection/play all.";
65
+ if (options.tooltipLoop === undefined) options.tooltipLoop = "Click to toggle play once/repeat.";
66
+ if (options.tooltipReset === undefined) options.tooltipReset = "Click to go to beginning.";
67
+ if (options.tooltipPlay === undefined) options.tooltipPlay = "Click to play/pause.";
68
+ if (options.tooltipProgress === undefined) options.tooltipProgress = "Click to change the playback position.";
69
+ if (options.tooltipTempo === undefined) options.tooltipTempo = "Change the playback speed.";
70
+
71
+ var style = "";
72
+ if (options.hide)
73
+ style = 'style="display:none;"';
74
+ var html = '<div class="abcjs-inline-midi abcjs-midi-' + index + '" ' + style + '>';
75
+ html += '<span class="abcjs-data" style="display:none;">' + JSON.stringify(midi) + '</span>';
76
+ if (midiParams.preTextInline)
77
+ html += '<span class="abcjs-midi-pre">' + preprocessLabel(midiParams.preTextInline, title) + '</span>';
78
+
79
+ if (options.selectionToggle)
80
+ html += '<button class="abcjs-midi-selection abcjs-btn" title="' + options.tooltipSelection + '"></button>';
81
+ if (options.loopToggle)
82
+ html += '<button class="abcjs-midi-loop abcjs-btn" title="' + options.tooltipLoop + '"></button>';
83
+ if (options.standard)
84
+ html += '<button class="abcjs-midi-reset abcjs-btn" title="' + options.tooltipReset + '"></button><button class="abcjs-midi-start abcjs-btn" title="' + options.tooltipPlay + '"></button><button class="abcjs-midi-progress-background" title="' + options.tooltipProgress + '"><span class="abcjs-midi-progress-indicator"></span></button><span class="abcjs-midi-clock"> 0:00</span>';
85
+ if (options.tempo) {
86
+ var startTempo = tune && tune.metaText && tune.metaText.tempo ? tune.metaText.tempo.bpm : 180;
87
+ html += '<span class="abcjs-tempo-wrapper"><input class="abcjs-midi-tempo" value="100" type="number" min="1" max="300" data-start-tempo="' + startTempo + '" title="' + options.tooltipTempo + '" />% (<span class="abcjs-midi-current-tempo">' + startTempo + '</span> BPM)</span>';
88
+ }
89
+
90
+ if (midiParams.postTextInline)
91
+ html += '<span class="abcjs-midi-post">' + preprocessLabel(midiParams.postTextInline, title) + '</span>';
92
+ return html + "</div>";
93
+ };
94
+
95
+ // The default location for the sound font files. Simply set this to a different value if the files are served in a different place.
96
+ window.ABCJS.midi.soundfontUrl = "/soundfont/";
97
+
98
+ function hasClass(element, cls) {
99
+ if (!element)
100
+ return false;
101
+ return (' ' + element.className + ' ').indexOf(' ' + cls + ' ') > -1;
102
+ }
103
+
104
+ function addClass(element, cls) {
105
+ if (!element)
106
+ return;
107
+ if (!hasClass(element, cls))
108
+ element.className = element.className + " " + cls;
109
+ }
110
+
111
+ function removeClass(element, cls) {
112
+ if (!element)
113
+ return;
114
+ element.className = element.className.replace(cls, "").trim().replace(" ", " ");
115
+ }
116
+
117
+ function toggleClass(element, cls) {
118
+ if (!element)
119
+ return;
120
+ if (hasClass(element, cls))
121
+ removeClass(element, cls);
122
+ else
123
+ addClass(element, cls);
124
+ }
125
+
126
+ function closest(element, cls) {
127
+ // This finds the closest parent that contains the class passed in.
128
+ if (!element)
129
+ return null;
130
+ while (element !== document.body) {
131
+ if (hasClass(element, cls))
132
+ return element;
133
+ element = element.parentNode;
134
+ }
135
+ return null;
136
+ }
137
+
138
+ function find(element, cls) {
139
+ if (!element)
140
+ return null;
141
+ var els = element.getElementsByClassName(cls);
142
+ if (els.length === 0)
143
+ return null;
144
+ return els[0];
145
+ }
146
+
147
+ function addLoadEvent(func) {
148
+ var oldOnLoad = window.onload;
149
+ if (typeof window.onload !== 'function') {
150
+ window.onload = func;
151
+ } else {
152
+ window.onload = function() {
153
+ if (oldOnLoad) {
154
+ oldOnLoad();
155
+ }
156
+ func();
157
+ };
158
+ }
159
+ }
160
+
161
+ var midiJsInitialized = false;
162
+
163
+ function afterSetup(timeWarp, data, onSuccess) {
164
+ MIDI.player.currentTime = 0;
165
+ MIDI.player.warp = timeWarp;
166
+
167
+ MIDI.player.load({ events: data });
168
+ onSuccess();
169
+ }
170
+
171
+ function setCurrentMidiTune(timeWarp, data, onSuccess) {
172
+ if (!midiJsInitialized) {
173
+ MIDI.setup({
174
+ debug: true,
175
+ soundfontUrl: window.ABCJS.midi.soundfontUrl
176
+ }).then(function() {
177
+ midiJsInitialized = true;
178
+ afterSetup(timeWarp, data, onSuccess);
179
+ });
180
+ } else {
181
+ afterSetup(timeWarp, data, onSuccess);
182
+ }
183
+ }
184
+
185
+ function startCurrentlySelectedTune() {
186
+ MIDI.player.start(MIDI.player.currentTime);
187
+ }
188
+
189
+ function stopCurrentlyPlayingTune() {
190
+ MIDI.player.stop();
191
+ }
192
+
193
+ function pauseCurrentlyPlayingTune() {
194
+ MIDI.player.pause();
195
+ }
196
+
197
+ function setMidiCallback(midiJsListener) {
198
+ MIDI.player.setAnimation(midiJsListener);
199
+ }
200
+
201
+ function jumpToMidiPosition(play, offset, width) {
202
+ var ratio = offset / width;
203
+ var endTime = MIDI.player.duration; // MIDI.Player.endTime;
204
+ if (play)
205
+ pauseCurrentlyPlayingTune();
206
+ MIDI.player.currentTime = endTime * ratio;
207
+ if (play)
208
+ startCurrentlySelectedTune();
209
+ }
210
+
211
+ function setTimeWarp(percent) {
212
+ // Time warp is a multiplier: the larger the number, the longer the time. Therefore,
213
+ // it is opposite of the percentage. That is, playing at 50% is actually multiplying the time by 2.
214
+ MIDI.player.warp = (percent > 0) ? 100 / percent : 1;
215
+ }
216
+
217
+ function loadMidi(target, onSuccess) {
218
+ var dataEl = find(target, "abcjs-data");
219
+ var data = JSON.parse(dataEl.innerHTML);
220
+
221
+ // See if the tempo changer is present, and use that tempo if so.
222
+ var timeWarp = 1;
223
+ var tempoEl = find(target, "abcjs-midi-tempo");
224
+ if (tempoEl) {
225
+ // Time warp is a multiplier: the larger the number, the longer the time. Therefore,
226
+ // it is opposite of the percentage. That is, playing at 50% is actually multiplying the time by 2.
227
+ var percent = parseInt(tempoEl.value, 10);
228
+ if (percent > 0)
229
+ timeWarp = 100 / percent;
230
+ }
231
+ setCurrentMidiTune(timeWarp, data, onSuccess);
232
+ }
233
+
234
+ function deselectMidiControl() {
235
+ var otherMidi = find(document, "abcjs-midi-current");
236
+ if (otherMidi) {
237
+ stopCurrentlyPlayingTune();
238
+ removeClass(otherMidi, "abcjs-midi-current");
239
+ var otherMidiStart = find(otherMidi, "abcjs-midi-start");
240
+ removeClass(otherMidiStart, "abcjs-pushed");
241
+ }
242
+ }
243
+
244
+ var lastNow;
245
+ //var tempSelection = 109;
246
+
247
+ function midiJsListener(position) {
248
+ // { currentTime: in seconds, duration: total length in seconds, progress: percent between 0 and 1 }
249
+ var midiControl;
250
+ if (position.duration > 0 && lastNow !== position.progress) {
251
+ lastNow = position.progress;
252
+ midiControl = find(document, "abcjs-midi-current");
253
+ if (midiControl) {
254
+ var progressBackground = find(midiControl, "abcjs-midi-progress-background");
255
+ var totalWidth = progressBackground.offsetWidth;
256
+ var progressIndicator = find(midiControl, "abcjs-midi-progress-indicator");
257
+ var scaled = totalWidth * lastNow; // The number of pixels
258
+ progressIndicator.style.left = scaled + "px";
259
+ var clock = find(midiControl, "abcjs-midi-clock");
260
+ if (clock) {
261
+ var seconds = Math.floor(position.currentTime);
262
+ var minutes = Math.floor(seconds / 60);
263
+ seconds = seconds % 60;
264
+ if (seconds < 10) seconds = "0" + seconds;
265
+ if (minutes < 10) minutes = " " + minutes;
266
+ clock.innerHTML = minutes + ":" + seconds;
267
+ }
268
+ if (midiControl.abcjsListener)
269
+ midiControl.abcjsListener(midiControl, position);
270
+ //if (midiControl.abcjsAnimate) {
271
+ // if (currentNote.velocity > 0) {
272
+ // midiControl.abcjsTune.engraver.rangeHighlight(tempSelection, tempSelection + 1);
273
+ // tempSelection++;
274
+ // if (tempSelection > 343)
275
+ // tempSelection = 109;
276
+ // }
277
+ //}
278
+ }
279
+ }
280
+ if (position.progress === 1) {
281
+ // The playback is stopping. We need to either indicate that
282
+ // it has stopped, or start over at the beginning.
283
+ midiControl = find(document, "abcjs-midi-current");
284
+ var loopControl = find(midiControl, "abcjs-midi-loop");
285
+
286
+ var finishedResetting = function() {
287
+ if (loopControl && hasClass(loopControl, "abcjs-pushed")) {
288
+ onStart(find(midiControl, "abcjs-midi-start"));
289
+ }
290
+ };
291
+
292
+ // midi.js is not quite finished: it still will process the last event, so we wait a minimum amount of time
293
+ // before doing another action.
294
+ setTimeout(function() {
295
+ doReset(midiControl, finishedResetting);
296
+ }, 1);
297
+ }
298
+ }
299
+
300
+ //function midiJsListener(currentNote, b, c) {
301
+ // console.log("listener ==>", currentNote, b, c);
302
+ // return;
303
+ // // { currentTime, duration, progress }
304
+ // // currentNote is a hash containing: { channel, end, message, note, now, velocity }
305
+ // var midiControl;
306
+ // if (currentNote.end > 0 && lastNow !== currentNote.now) {
307
+ // lastNow = currentNote.now;
308
+ // var currentPosition = currentNote.now / currentNote.end; // This returns a number between 0 and 1.
309
+ // midiControl = find(document, "abcjs-midi-current");
310
+ // if (midiControl) {
311
+ // var progressBackground = find(midiControl, "abcjs-midi-progress-background");
312
+ // var totalWidth = progressBackground.offsetWidth;
313
+ // var progressIndicator = find(midiControl, "abcjs-midi-progress-indicator");
314
+ // var scaled = totalWidth * currentPosition; // The number of pixels
315
+ // progressIndicator.style.left = scaled + "px";
316
+ // var clock = find(midiControl, "abcjs-midi-clock");
317
+ // if (clock) {
318
+ // var seconds = Math.floor(currentNote.now / 1000);
319
+ // var minutes = Math.floor(seconds / 60);
320
+ // seconds = seconds % 60;
321
+ // if (seconds < 10) seconds = "0" + seconds;
322
+ // if (minutes < 10) minutes = " " + minutes;
323
+ // clock.innerHTML = minutes + ":" + seconds;
324
+ // }
325
+ // if (midiControl.abcjsListener)
326
+ // midiControl.abcjsListener(midiControl, currentNote);
327
+ // if (midiControl.abcjsAnimate) {
328
+ // if (currentNote.velocity > 0) {
329
+ // midiControl.abcjsTune.engraver.rangeHighlight(tempSelection, tempSelection + 1);
330
+ // tempSelection++;
331
+ // if (tempSelection > 343)
332
+ // tempSelection = 109;
333
+ // }
334
+ // }
335
+ // }
336
+ // }
337
+ // if (currentNote.now === currentNote.end) {
338
+ // // The playback is stopping. We need to either indicate that
339
+ // // it has stopped, or start over at the beginning.
340
+ // midiControl = find(document, "abcjs-midi-current");
341
+ // var loopControl = find(midiControl, "abcjs-midi-loop");
342
+ //
343
+ // var finishedResetting = function() {
344
+ // if (loopControl && hasClass(loopControl, "abcjs-pushed")) {
345
+ // onStart(find(midiControl, "abcjs-midi-start"));
346
+ // }
347
+ // };
348
+ //
349
+ // // midi.js is not quite finished: it still will process the last event, so we wait a minimum amount of time
350
+ // // before doing another action.
351
+ // setTimeout(function() {
352
+ // doReset(midiControl, finishedResetting);
353
+ // }, 1);
354
+ // }
355
+ //}
356
+ //
357
+ function onStart(target) {
358
+ // If this midi is already playing,
359
+ if (hasClass(target, 'abcjs-pushed')) {
360
+ // Stop it.
361
+ pauseCurrentlyPlayingTune();
362
+ // Change the element so that the start icon is shown.
363
+ removeClass(target, "abcjs-pushed");
364
+ } else { // Else,
365
+ // If some other midi is running, turn it off.
366
+ var parent = closest(target, "abcjs-inline-midi");
367
+ // If this is the current midi, just continue.
368
+ if (hasClass(parent, "abcjs-midi-current"))
369
+ // Start this tune playing from wherever it had stopped.
370
+ startCurrentlySelectedTune();
371
+ else {
372
+ deselectMidiControl();
373
+
374
+ // else, load this midi from scratch.
375
+ var onSuccess = function() {
376
+ startCurrentlySelectedTune();
377
+ addClass(parent, 'abcjs-midi-current');
378
+ };
379
+ loadMidi(parent, onSuccess);
380
+ }
381
+ // Change the element so that the pause icon is shown.
382
+ addClass(target, "abcjs-pushed");
383
+ }
384
+ // This replaces the old callback. It really only needs to be called once, but it doesn't hurt to set it every time.
385
+ setMidiCallback(midiJsListener);
386
+ }
387
+
388
+ window.ABCJS.midi.startPlaying = function(target) {
389
+ onStart(target);
390
+ };
391
+
392
+ window.ABCJS.midi.stopPlaying = function() {
393
+ stopCurrentlyPlayingTune();
394
+ };
395
+
396
+ function onSelection(target) {
397
+ toggleClass(target, 'abcjs-pushed');
398
+ }
399
+
400
+ function onLoop(target) {
401
+ toggleClass(target, 'abcjs-pushed');
402
+ }
403
+
404
+ function doReset(target, callback) {
405
+ var parent = closest(target, "abcjs-inline-midi");
406
+
407
+ function onSuccess() {
408
+ addClass(parent, 'abcjs-midi-current');
409
+ var progressIndicator = find(parent, "abcjs-midi-progress-indicator");
410
+ progressIndicator.style.left = "0px";
411
+ var clock = find(parent, "abcjs-midi-clock");
412
+ clock.innerHTML = " 0:00";
413
+ if (callback)
414
+ callback();
415
+ }
416
+
417
+ // If the tune is playing, stop it.
418
+ deselectMidiControl();
419
+ loadMidi(parent, onSuccess);
420
+ }
421
+
422
+ function onReset(target) {
423
+ var parent = closest(target, "abcjs-inline-midi");
424
+ var playEl = find(parent, "abcjs-midi-start");
425
+ var play = hasClass(playEl, "abcjs-pushed");
426
+ function keepPlaying() {
427
+ if (play) {
428
+ startCurrentlySelectedTune();
429
+ addClass(playEl, 'abcjs-pushed');
430
+ }
431
+ }
432
+ if (hasClass(parent, "abcjs-midi-current")) // Only listen to the reset for the currently playing tune.
433
+ doReset(target, keepPlaying);
434
+ }
435
+
436
+ function relMouseX(target, event) {
437
+ var totalOffsetX = 0;
438
+
439
+ do {
440
+ totalOffsetX += target.offsetLeft - target.scrollLeft;
441
+ target = target.offsetParent;
442
+ }
443
+ while (target);
444
+
445
+ return event.pageX - totalOffsetX;
446
+ }
447
+
448
+ function onProgress(target, event) {
449
+ var parent = closest(target, "abcjs-inline-midi");
450
+ if (hasClass(parent, "abcjs-midi-current")) {
451
+ var play = find(parent, "abcjs-midi-start");
452
+ play = hasClass(play, "abcjs-pushed");
453
+ var width = target.offsetWidth;
454
+ var offset = relMouseX(target, event);
455
+ jumpToMidiPosition(play, offset, width);
456
+ }
457
+ }
458
+
459
+ function onTempo(el) {
460
+ var percent = parseInt(el.value, 10);
461
+ var startTempo = parseInt(el.getAttribute("data-start-tempo"), 10);
462
+
463
+ while (el && !hasClass(el, 'abcjs-midi-current-tempo')) {
464
+ el = el.nextSibling;
465
+ }
466
+ el.innerHTML = Math.floor(percent * startTempo / 100);
467
+ setTimeWarp(percent);
468
+ }
469
+
470
+ function addDelegates() {
471
+ document.body.addEventListener("click", function(event) {
472
+ event = event || window.event;
473
+ var target = event.target || event.srcElement;
474
+ while (target && target !== document.body) {
475
+ if (hasClass(target, 'abcjs-midi-start')) {
476
+ onStart(target, event);
477
+ return;
478
+ } else if (hasClass(target, 'abcjs-midi-selection')) {
479
+ onSelection(target, event);
480
+ return;
481
+ } else if (hasClass(target, 'abcjs-midi-loop')) {
482
+ onLoop(target, event);
483
+ return;
484
+ } else if (hasClass(target, 'abcjs-midi-reset')) {
485
+ onReset(target, event);
486
+ return;
487
+ } else if (hasClass(target, 'abcjs-midi-progress-background')) {
488
+ onProgress(target, event);
489
+ return;
490
+ }
491
+ target = target.parentNode;
492
+ }
493
+ });
494
+ document.body.addEventListener("change", function(event) {
495
+ event = event || window.event;
496
+ var target = event.target || event.srcElement;
497
+ while (target !== document.body) {
498
+ if (hasClass(target, 'abcjs-midi-tempo'))
499
+ onTempo(target, event);
500
+ target = target.parentNode;
501
+ }
502
+ });
503
+ if (window.MIDI === undefined) {
504
+ window.ABCJS.midi.midiInlineInitialized = 'not loaded';
505
+ var els = document.getElementsByClassName('abcjs-inline-midi');
506
+ for (var i = 0; i < els.length; i++)
507
+ els[i].innerHTML = "MIDI NOT PRESENT";
508
+ }
509
+ }
510
+
511
+ addLoadEvent(addDelegates);
512
+
513
+ })();