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.
- checksums.yaml +4 -4
- data/app/assets/javascripts/abcjs/api/abc_animation.js +166 -4
- data/app/assets/javascripts/abcjs/api/abc_tunebook.js +170 -15
- data/app/assets/javascripts/abcjs/data/abc_tune.js +69 -47
- data/app/assets/javascripts/abcjs/edit/abc_editor.js +78 -37
- data/app/assets/javascripts/abcjs/midi/abc_midi_controls.js +513 -0
- data/app/assets/javascripts/abcjs/midi/abc_midi_create.js +29 -7
- data/app/assets/javascripts/abcjs/midi/abc_midi_flattener.js +21 -18
- data/app/assets/javascripts/abcjs/midi/abc_midi_js_preparer.js +233 -0
- data/app/assets/javascripts/abcjs/midi/abc_midi_renderer.js +3 -229
- data/app/assets/javascripts/abcjs/midi/abc_midi_sequencer.js +11 -3
- data/app/assets/javascripts/abcjs/parse/abc_common.js +24 -0
- data/app/assets/javascripts/abcjs/parse/abc_parse.js +58 -16
- data/app/assets/javascripts/abcjs/parse/abc_parse_header.js +4 -1
- data/app/assets/javascripts/abcjs/parse/abc_parse_key_voice.js +5 -0
- data/app/assets/javascripts/abcjs/write/abc_absolute_element.js +9 -2
- data/app/assets/javascripts/abcjs/write/abc_abstract_engraver.js +71 -19
- data/app/assets/javascripts/abcjs/write/abc_beam_element.js +11 -3
- data/app/assets/javascripts/abcjs/write/abc_brace_element.js +51 -0
- data/app/assets/javascripts/abcjs/write/abc_create_clef.js +3 -3
- data/app/assets/javascripts/abcjs/write/abc_create_key_signature.js +3 -3
- data/app/assets/javascripts/abcjs/write/abc_create_time_signature.js +3 -3
- data/app/assets/javascripts/abcjs/write/abc_crescendo_element.js +4 -1
- data/app/assets/javascripts/abcjs/write/abc_decoration.js +1 -1
- data/app/assets/javascripts/abcjs/write/abc_engraver_controller.js +10 -9
- data/app/assets/javascripts/abcjs/write/abc_glyphs.js +1 -1
- data/app/assets/javascripts/abcjs/write/abc_relative_element.js +1 -1
- data/app/assets/javascripts/abcjs/write/abc_renderer.js +85 -13
- data/app/assets/javascripts/abcjs/write/abc_staff_group_element.js +16 -0
- data/app/assets/javascripts/abcjs/write/abc_tempo_element.js +31 -11
- data/app/assets/javascripts/abcjs/write/abc_tie_element.js +8 -1
- data/lib/abcjs-rails/version.rb +1 -1
- 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
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
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
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
this.lines[this.lineNum].staff
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
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
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
if (
|
570
|
-
hashParams.
|
571
|
-
|
572
|
-
|
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
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
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.
|
251
|
-
this.
|
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
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
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);
|
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
|
-
|
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
|
-
|
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
|
+
})();
|