abcjs-rails 1.1.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,414 @@
1
+ // abc_editor.js
2
+ // window.ABCJS.Editor is the interface class for the area that contains the ABC text. It is responsible for
3
+ // holding the text of the tune and calling the parser and the rendering engines.
4
+ //
5
+ // EditArea is an example of using a textarea as the control that is shown to the user. As long as
6
+ // the same interface is used, window.ABCJS.Editor can use a different type of object.
7
+ //
8
+ // EditArea:
9
+ // - constructor(textareaid)
10
+ // This contains the id of a textarea control that will be used.
11
+ // - addSelectionListener(listener)
12
+ // A callback class that contains the entry point fireSelectionChanged()
13
+ // - addChangeListener(listener)
14
+ // A callback class that contains the entry point fireChanged()
15
+ // - getSelection()
16
+ // returns the object { start: , end: } with the current selection in characters
17
+ // - setSelection(start, end)
18
+ // start and end are the character positions that should be selected.
19
+ // - getString()
20
+ // returns the ABC text that is currently displayed.
21
+ // - setString(str)
22
+ // sets the ABC text that is currently displayed, and resets the initialText variable
23
+ // - getElem()
24
+ // returns the textarea element
25
+ // - string initialText
26
+ // Contains the starting text. This can be compared against the current text to see if anything changed.
27
+ //
28
+
29
+ /*global document, window, clearTimeout, setTimeout */
30
+ /*global Raphael */
31
+
32
+ if (!window.ABCJS)
33
+ window.ABCJS = {};
34
+
35
+ if (!window.ABCJS.edit)
36
+ window.ABCJS.edit = {};
37
+
38
+ window.ABCJS.edit.EditArea = function(textareaid) {
39
+ this.textarea = document.getElementById(textareaid);
40
+ this.initialText = this.textarea.value;
41
+ this.isDragging = false;
42
+ }
43
+
44
+ window.ABCJS.edit.EditArea.prototype.addSelectionListener = function(listener) {
45
+ this.textarea.onmousemove = function(ev) {
46
+ if (this.isDragging)
47
+ listener.fireSelectionChanged();
48
+ };
49
+ };
50
+
51
+ window.ABCJS.edit.EditArea.prototype.addChangeListener = function(listener) {
52
+ this.changelistener = listener;
53
+ this.textarea.onkeyup = function() {
54
+ listener.fireChanged();
55
+ };
56
+ this.textarea.onmousedown = function() {
57
+ this.isDragging = true;
58
+ listener.fireSelectionChanged();
59
+ };
60
+ this.textarea.onmouseup = function() {
61
+ this.isDragging = false;
62
+ listener.fireChanged();
63
+ };
64
+ this.textarea.onchange = function() {
65
+ listener.fireChanged();
66
+ };
67
+ };
68
+
69
+ //TODO won't work under IE?
70
+ window.ABCJS.edit.EditArea.prototype.getSelection = function() {
71
+ return {start: this.textarea.selectionStart, end: this.textarea.selectionEnd};
72
+ };
73
+
74
+ window.ABCJS.edit.EditArea.prototype.setSelection = function(start, end) {
75
+ if(this.textarea.setSelectionRange)
76
+ this.textarea.setSelectionRange(start, end);
77
+ else if(this.textarea.createTextRange) {
78
+ // For IE8
79
+ var e = this.textarea.createTextRange();
80
+ e.collapse(true);
81
+ e.moveEnd('character', end);
82
+ e.moveStart('character', start);
83
+ e.select();
84
+ }
85
+ this.textarea.focus();
86
+ };
87
+
88
+ window.ABCJS.edit.EditArea.prototype.getString = function() {
89
+ return this.textarea.value;
90
+ };
91
+
92
+ window.ABCJS.edit.EditArea.prototype.setString = function(str) {
93
+ this.textarea.value = str;
94
+ this.initialText = this.getString();
95
+ if (this.changelistener) {
96
+ this.changelistener.fireChanged();
97
+ }
98
+ };
99
+
100
+ window.ABCJS.edit.EditArea.prototype.getElem = function() {
101
+ return this.textarea;
102
+ };
103
+
104
+ //
105
+ // window.ABCJS.Editor:
106
+ //
107
+ // constructor(editarea, params)
108
+ // if editarea is a string, then it is an HTML id of a textarea control.
109
+ // Otherwise, it should be an instantiation of an object that expresses the EditArea interface.
110
+ //
111
+ // params is a hash of:
112
+ // canvas_id: or paper_id: HTML id to draw in. If not present, then the drawing happens just below the editor.
113
+ // generate_midi: if present, then midi is generated.
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
+ // generate_warnings: if present, then parser warnings are displayed on the page.
116
+ // warnings_id: if present, the HTML id to place the warnings. Otherwise they are placed in the same div as the paper.
117
+ // onchange: if present, the callback function to call whenever there has been a change.
118
+ // gui: if present, the paper can send changes back to the editor (presumably because the user changed something directly.)
119
+ // parser_options: options to send to the parser engine.
120
+ // midi_options: options to send to the midi engine.
121
+ // render_options: options to send to the render engine.
122
+ // indicate_changed: the dirty flag is set if this is true.
123
+ //
124
+ // - setReadOnly(bool)
125
+ // adds or removes the class abc_textarea_readonly, and adds or removes the attribute readonly=yes
126
+ // - setDirtyStyle(bool)
127
+ // adds or removes the class abc_textarea_dirty
128
+ // - renderTune(abc, parserparams, div)
129
+ // Immediately renders the tune. (Useful for creating the SVG output behind the scenes, if div is hidden)
130
+ // string abc: the ABC text
131
+ // parserparams: params to send to the parser
132
+ // div: the HTML id to render to.
133
+ // - modelChanged()
134
+ // Called when the model has been changed to trigger re-rendering
135
+ // - parseABC()
136
+ // Called internally by fireChanged()
137
+ // returns true if there has been a change since last call.
138
+ // - updateSelection()
139
+ // Called when the user has changed the selection. This calls the printer to show the selection.
140
+ // - fireSelectionChanged()
141
+ // Called by the textarea object when the user has changed the selection.
142
+ // - paramChanged(printerparams)
143
+ // Called to signal that the printer params have changed, so re-rendering should occur.
144
+ // - fireChanged()
145
+ // Called by the textarea object when the user has changed something.
146
+ // - setNotDirty()
147
+ // Called by the client app to reset the dirty flag
148
+ // - isDirty()
149
+ // Returns true or false, whether the textarea contains the same text that it started with.
150
+ // - highlight(abcelem)
151
+ // Called by the printer to highlight an area.
152
+ // - pause(bool)
153
+ // Stops the automatic rendering when the user is typing.
154
+ //
155
+
156
+ window.ABCJS.Editor = function(editarea, params) {
157
+ if (params.indicate_changed)
158
+ this.indicate_changed = true;
159
+ if (typeof editarea === "string") {
160
+ this.editarea = new window.ABCJS.edit.EditArea(editarea);
161
+ } else {
162
+ this.editarea = editarea;
163
+ }
164
+ this.editarea.addSelectionListener(this);
165
+ this.editarea.addChangeListener(this);
166
+
167
+ if (params.canvas_id) {
168
+ this.div = document.getElementById(params.canvas_id);
169
+ } else if (params.paper_id) {
170
+ this.div = document.getElementById(params.paper_id);
171
+ } else {
172
+ this.div = document.createElement("DIV");
173
+ this.editarea.getElem().parentNode.insertBefore(this.div, this.editarea.getElem());
174
+ }
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
+
184
+ if (params.generate_warnings || params.warnings_id) {
185
+ if (params.warnings_id) {
186
+ this.warningsdiv = document.getElementById(params.warnings_id);
187
+ } else {
188
+ this.warningsdiv = this.div;
189
+ }
190
+ }
191
+
192
+ this.parserparams = params.parser_options || {};
193
+ this.midiparams = params.midi_options || {};
194
+ this.onchangeCallback = params.onchange;
195
+
196
+ this.printerparams = params.render_options || {};
197
+
198
+ if (params.gui) {
199
+ this.target = document.getElementById(editarea);
200
+ this.printerparams.editable = true;
201
+ }
202
+ this.oldt = "";
203
+ this.bReentry = false;
204
+ this.parseABC();
205
+ this.modelChanged();
206
+
207
+ this.addClassName = function(element, className) {
208
+ var hasClassName = function(element, className) {
209
+ var elementClassName = element.className;
210
+ return (elementClassName.length > 0 && (elementClassName === className ||
211
+ new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
212
+ };
213
+
214
+ if (!hasClassName(element, className))
215
+ element.className += (element.className ? ' ' : '') + className;
216
+ return element;
217
+ };
218
+
219
+ this.removeClassName = function(element, className) {
220
+ element.className = window.ABCJS.parse.strip(element.className.replace(
221
+ new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' '));
222
+ return element;
223
+ };
224
+
225
+ this.setReadOnly = function(readOnly) {
226
+ var readonlyClass = 'abc_textarea_readonly';
227
+ var el = this.editarea.getElem();
228
+ if (readOnly) {
229
+ el.setAttribute('readonly', 'yes');
230
+ this.addClassName(el, readonlyClass);
231
+ } else {
232
+ el.removeAttribute('readonly');
233
+ this.removeClassName(el, readonlyClass);
234
+ }
235
+ };
236
+ }
237
+
238
+ window.ABCJS.Editor.prototype.renderTune = function(abc, params, div) {
239
+ var tunebook = new ABCJS.TuneBook(abc);
240
+ var abcParser = window.ABCJS.parse.Parse();
241
+ abcParser.parse(tunebook.tunes[0].abc, params); //TODO handle multiple tunes
242
+ var tune = abcParser.getTune();
243
+ var paper = Raphael(div, 800, 400);
244
+ var printer = new ABCJS.write.Printer(paper, {}); // TODO: handle printer params
245
+ printer.printABC(tune);
246
+ };
247
+
248
+ window.ABCJS.Editor.prototype.modelChanged = function() {
249
+ if (this.tunes === undefined) {
250
+ if (this.mididiv !== undefined && this.mididiv !== this.div)
251
+ this.mididiv.innerHTML = "";
252
+ this.div.innerHTML = "";
253
+ return;
254
+ }
255
+
256
+ if (this.bReentry)
257
+ return; // TODO is this likely? maybe, if we rewrite abc immediately w/ abc2abc
258
+ this.bReentry = true;
259
+ this.timerId = null;
260
+ this.div.innerHTML = "";
261
+ var paper = Raphael(this.div, 800, 400);
262
+ this.printer = new ABCJS.write.Printer(paper, this.printerparams);
263
+ this.printer.printABC(this.tunes);
264
+ if (ABCJS.midi.MidiWriter && this.mididiv) {
265
+ if (this.mididiv !== this.div)
266
+ this.mididiv.innerHTML = "";
267
+ var midiwriter = new ABCJS.midi.MidiWriter(this.mididiv,this.midiparams);
268
+ midiwriter.addListener(this.printer);
269
+ midiwriter.writeABC(this.tunes[0]); //TODO handle multiple tunes
270
+ }
271
+ if (this.warningsdiv) {
272
+ this.warningsdiv.innerHTML = (this.warnings) ? this.warnings.join("<br />") : "No errors";
273
+ }
274
+ if (this.target) {
275
+ var textprinter = new window.ABCJS.transform.TextPrinter(this.target, true);
276
+ textprinter.printABC(this.tunes[0]); //TODO handle multiple tunes
277
+ }
278
+ this.printer.addSelectListener(this);
279
+ this.updateSelection();
280
+ this.bReentry = false;
281
+ };
282
+
283
+ // Call this to reparse in response to the printing parameters changing
284
+ window.ABCJS.Editor.prototype.paramChanged = function(printerparams) {
285
+ this.printerparams = printerparams;
286
+ this.oldt = "";
287
+ this.fireChanged();
288
+ };
289
+
290
+ // return true if the model has changed
291
+ window.ABCJS.Editor.prototype.parseABC = function() {
292
+ var t = this.editarea.getString();
293
+ if (t===this.oldt) {
294
+ this.updateSelection();
295
+ return false;
296
+ }
297
+
298
+ this.oldt = t;
299
+ if (t === "") {
300
+ this.tunes = undefined;
301
+ this.warnings = "";
302
+ return true;
303
+ }
304
+ var tunebook = new ABCJS.TuneBook(t);
305
+
306
+ this.tunes = [];
307
+ this.warnings = [];
308
+ for (var i=0; i<tunebook.tunes.length; i++) {
309
+ var abcParser = new window.ABCJS.parse.Parse();
310
+ abcParser.parse(tunebook.tunes[i].abc, this.parserparams); //TODO handle multiple tunes
311
+ this.tunes[i] = abcParser.getTune();
312
+ var warnings = abcParser.getWarnings() || [];
313
+ for (var j=0; j<warnings.length; j++) {
314
+ this.warnings.push(warnings[j]);
315
+ }
316
+ }
317
+ return true;
318
+ };
319
+
320
+ window.ABCJS.Editor.prototype.updateSelection = function() {
321
+ var selection = this.editarea.getSelection();
322
+ try {
323
+ this.printer.rangeHighlight(selection.start, selection.end);
324
+ } catch (e) {} // maybe printer isn't defined yet?
325
+ };
326
+
327
+ window.ABCJS.Editor.prototype.fireSelectionChanged = function() {
328
+ this.updateSelection();
329
+ };
330
+
331
+ window.ABCJS.Editor.prototype.setDirtyStyle = function(isDirty) {
332
+ if (this.indicate_changed === undefined)
333
+ return;
334
+ var addClassName = function(element, className) {
335
+ var hasClassName = function(element, className) {
336
+ var elementClassName = element.className;
337
+ return (elementClassName.length > 0 && (elementClassName === className ||
338
+ new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
339
+ };
340
+
341
+ if (!hasClassName(element, className))
342
+ element.className += (element.className ? ' ' : '') + className;
343
+ return element;
344
+ };
345
+
346
+ var removeClassName = function(element, className) {
347
+ element.className = window.ABCJS.parse.strip(element.className.replace(
348
+ new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' '));
349
+ return element;
350
+ };
351
+
352
+ var readonlyClass = 'abc_textarea_dirty';
353
+ var el = this.editarea.getElem();
354
+ if (isDirty) {
355
+ addClassName(el, readonlyClass);
356
+ } else {
357
+ removeClassName(el, readonlyClass);
358
+ }
359
+ };
360
+
361
+ // call when abc text is changed and needs re-parsing
362
+ window.ABCJS.Editor.prototype.fireChanged = function() {
363
+ if (this.bIsPaused)
364
+ return;
365
+ if (this.parseABC()) {
366
+ var self = this;
367
+ if (this.timerId) // If the user is still typing, cancel the update
368
+ clearTimeout(this.timerId);
369
+ this.timerId = setTimeout(function () {
370
+ self.modelChanged();
371
+ }, 300); // Is this a good comprimise between responsiveness and not redrawing too much?
372
+ var isDirty = this.isDirty();
373
+ if (this.wasDirty !== isDirty) {
374
+ this.wasDirty = isDirty;
375
+ this.setDirtyStyle(isDirty);
376
+ }
377
+ if (this.onchangeCallback)
378
+ this.onchangeCallback(this);
379
+ }
380
+ };
381
+
382
+ window.ABCJS.Editor.prototype.setNotDirty = function() {
383
+ this.editarea.initialText = this.editarea.getString();
384
+ this.wasDirty = false;
385
+ this.setDirtyStyle(false);
386
+ };
387
+
388
+ window.ABCJS.Editor.prototype.isDirty = function() {
389
+ if (this.indicate_changed === undefined)
390
+ return false;
391
+ return this.editarea.initialText !== this.editarea.getString();
392
+ };
393
+
394
+ window.ABCJS.Editor.prototype.highlight = function(abcelem) {
395
+ this.editarea.setSelection(abcelem.startChar, abcelem.endChar);
396
+ };
397
+
398
+ window.ABCJS.Editor.prototype.pause = function(shouldPause) {
399
+ this.bIsPaused = shouldPause;
400
+ if (!shouldPause)
401
+ this.updateRendering();
402
+ };
403
+
404
+ window.ABCJS.Editor.prototype.pauseMidi = function(shouldPause) {
405
+ if (shouldPause && this.mididiv) {
406
+ this.mididivSave = this.mididiv;
407
+ this.addClassName(this.mididiv, 'hidden');
408
+ this.mididiv = null;
409
+ } else if (!shouldPause && this.mididivSave) {
410
+ this.mididiv = this.mididivSave;
411
+ this.removeClassName(this.mididiv, 'hidden');
412
+ this.mididivSave = null;
413
+ }
414
+ };
@@ -0,0 +1,698 @@
1
+ /*global window, document, setTimeout */
2
+
3
+ if (!window.ABCJS)
4
+ window.ABCJS = {};
5
+
6
+ if (!window.ABCJS.midi)
7
+ window.ABCJS.midi = {};
8
+
9
+ (function() {
10
+ function setAttributes(elm, attrs){
11
+ for(var attr in attrs)
12
+ if (attrs.hasOwnProperty(attr))
13
+ elm.setAttribute(attr, attrs[attr]);
14
+ return elm;
15
+ }
16
+
17
+ //TODO-PER: put this back in when the MIDIPlugin works again.
18
+ //window.oldunload = window.onbeforeunload;
19
+ //window.onbeforeunload = function() {
20
+ // if (window.oldunload)
21
+ // window.oldunload();
22
+ // if (typeof(MIDIPlugin) !== "undefined" && MIDIPlugin) { // PER: take care of crash in IE 8
23
+ // MIDIPlugin.closePlugin();
24
+ // }
25
+ //};
26
+
27
+
28
+ function MidiProxy(javamidi,qtmidi) {
29
+ this.javamidi = javamidi;
30
+ this.qtmidi = qtmidi;
31
+ }
32
+
33
+ MidiProxy.prototype.setTempo = function (qpm) {
34
+ this.javamidi.setTempo(qpm);
35
+ this.qtmidi.setTempo(qpm);
36
+ };
37
+
38
+ MidiProxy.prototype.startTrack = function () {
39
+ this.javamidi.startTrack();
40
+ this.qtmidi.startTrack();
41
+ };
42
+
43
+ MidiProxy.prototype.endTrack = function () {
44
+ this.javamidi.endTrack();
45
+ this.qtmidi.endTrack();
46
+ };
47
+
48
+ MidiProxy.prototype.setInstrument = function (number) {
49
+ this.javamidi.setInstrument(number);
50
+ this.qtmidi.setInstrument(number);
51
+ };
52
+
53
+ MidiProxy.prototype.startNote = function (pitch, loudness, abcelem) {
54
+ this.javamidi.startNote(pitch, loudness, abcelem);
55
+ this.qtmidi.startNote(pitch, loudness, abcelem);
56
+ };
57
+
58
+ MidiProxy.prototype.endNote = function (pitch, length) {
59
+ this.javamidi.endNote(pitch, length);
60
+ this.qtmidi.endNote(pitch, length);
61
+ };
62
+
63
+ MidiProxy.prototype.addRest = function (length) {
64
+ this.javamidi.addRest(length);
65
+ this.qtmidi.addRest(length);
66
+ };
67
+
68
+ MidiProxy.prototype.embed = function(parent) {
69
+ this.javamidi.embed(parent);
70
+ this.qtmidi.embed(parent,true);
71
+ };
72
+
73
+ function JavaMidi(midiwriter) {
74
+ this.playlist = []; // contains {time:t,funct:f} pairs
75
+ this.trackcount = 0;
76
+ this.timecount = 0;
77
+ this.tempo = 60;
78
+ this.midiapi = MIDIPlugin;
79
+ this.midiwriter = midiwriter;
80
+ this.noteOnAndChannel = "%90";
81
+ }
82
+
83
+ JavaMidi.prototype.setTempo = function (qpm) {
84
+ this.tempo = qpm;
85
+ };
86
+
87
+ JavaMidi.prototype.startTrack = function () {
88
+ this.silencelength = 0;
89
+ this.trackcount++;
90
+ this.timecount=0;
91
+ this.playlistpos=0;
92
+ this.first=true;
93
+ if (this.instrument) {
94
+ this.setInstrument(this.instrument);
95
+ }
96
+ if (this.channel) {
97
+ this.setChannel(this.channel);
98
+ }
99
+ };
100
+
101
+ JavaMidi.prototype.endTrack = function () {
102
+ // need to do anything?
103
+ };
104
+
105
+ JavaMidi.prototype.setInstrument = function (number) {
106
+ this.instrument=number;
107
+ this.midiapi.setInstrument(number);
108
+ //TODO push this into the playlist?
109
+ };
110
+
111
+ JavaMidi.prototype.setChannel = function (number) {
112
+ this.channel=number;
113
+ this.midiapi.setChannel(number);
114
+ };
115
+
116
+ JavaMidi.prototype.updatePos = function() {
117
+ while(this.playlist[this.playlistpos] &&
118
+ this.playlist[this.playlistpos].time<this.timecount) {
119
+ this.playlistpos++;
120
+ }
121
+ };
122
+
123
+ JavaMidi.prototype.startNote = function (pitch, loudness, abcelem) {
124
+ this.timecount+=this.silencelength;
125
+ this.silencelength = 0;
126
+ if (this.first) {
127
+ //nothing special if first?
128
+ }
129
+ this.updatePos();
130
+ var self=this;
131
+ this.playlist.splice(this.playlistpos,0, {
132
+ time:this.timecount,
133
+ funct:function() {
134
+ self.midiapi.playNote(pitch);
135
+ self.midiwriter.notifySelect(abcelem);
136
+ }
137
+ });
138
+ };
139
+
140
+ JavaMidi.prototype.endNote = function (pitch, length) {
141
+ this.timecount+=length;
142
+ this.updatePos();
143
+ var self=this;
144
+ this.playlist.splice(this.playlistpos, 0, {
145
+ time:this.timecount,
146
+ funct: function() {
147
+ self.midiapi.stopNote(pitch);
148
+ }
149
+ });
150
+ };
151
+
152
+ JavaMidi.prototype.addRest = function (length) {
153
+ this.silencelength += length;
154
+ };
155
+
156
+ JavaMidi.prototype.embed = function(parent) {
157
+
158
+
159
+ this.playlink = setAttributes(document.createElement('a'), {
160
+ style: "border:1px solid black; margin:3px;"
161
+ });
162
+ this.playlink.innerHTML = "play";
163
+ var self = this;
164
+ this.playlink.onmousedown = function() {
165
+ if (self.playing) {
166
+ this.innerHTML = "play";
167
+ self.pausePlay();
168
+ } else {
169
+ this.innerHTML = "pause";
170
+ self.startPlay();
171
+ }
172
+ };
173
+ parent.appendChild(this.playlink);
174
+
175
+ var stoplink = setAttributes(document.createElement('a'), {
176
+ style: "border:1px solid black; margin:3px;"
177
+ });
178
+ stoplink.innerHTML = "stop";
179
+ //var self = this;
180
+ stoplink.onmousedown = function() {
181
+ self.stopPlay();
182
+ };
183
+ parent.appendChild(stoplink);
184
+ this.i=0;
185
+ this.currenttime=0;
186
+ this.playing = false;
187
+ };
188
+
189
+ JavaMidi.prototype.stopPlay = function() {
190
+ this.i=0;
191
+ this.currenttime=0;
192
+ this.pausePlay();
193
+ this.playlink.innerHTML = "play";
194
+ };
195
+
196
+ JavaMidi.prototype.startPlay = function() {
197
+ this.playing = true;
198
+ var self = this;
199
+ // repeat every 16th note TODO see the min in the piece
200
+ this.ticksperinterval = 480/4;
201
+ this.doPlay();
202
+ this.playinterval = window.setInterval(function() {self.doPlay(); },
203
+ (60000/(this.tempo*4)));
204
+ };
205
+
206
+ JavaMidi.prototype.pausePlay = function() {
207
+ this.playing = false;
208
+ window.clearInterval(this.playinterval);
209
+ this.midiapi.stopAllNotes();
210
+ };
211
+
212
+ JavaMidi.prototype.doPlay = function() {
213
+ while(this.playlist[this.i] &&
214
+ this.playlist[this.i].time <= this.currenttime) {
215
+ this.playlist[this.i].funct();
216
+ this.i++;
217
+ }
218
+ if (this.playlist[this.i]) {
219
+ this.currenttime+=this.ticksperinterval;
220
+ } else {
221
+ this.stopPlay();
222
+ }
223
+ };
224
+
225
+ function Midi() {
226
+ this.trackstrings="";
227
+ this.trackcount = 0;
228
+ this.noteOnAndChannel = "%90";
229
+ }
230
+
231
+ Midi.prototype.setTempo = function (qpm) {
232
+ if (this.trackcount===0) {
233
+ this.startTrack();
234
+ this.track+="%00%FF%51%03"+toHex(Math.round(60000000/qpm),6);
235
+ this.endTrack();
236
+ }
237
+ };
238
+
239
+ Midi.prototype.startTrack = function () {
240
+ this.track = "";
241
+ this.silencelength = 0;
242
+ this.trackcount++;
243
+ this.first=true;
244
+ if (this.instrument) {
245
+ this.setInstrument(this.instrument);
246
+ }
247
+ };
248
+
249
+ Midi.prototype.endTrack = function () {
250
+ var tracklength = toHex(this.track.length/3+4,8);
251
+ this.track = "MTrk"+tracklength+ // track header
252
+ this.track +
253
+ '%00%FF%2F%00'; // track end
254
+ this.trackstrings += this.track;
255
+ };
256
+
257
+ Midi.prototype.setInstrument = function (number) {
258
+ if (this.track)
259
+ this.track = "%00%C0"+toHex(number,2)+this.track;
260
+ else
261
+ this.track = "%00%C0"+toHex(number,2);
262
+ this.instrument=number;
263
+ };
264
+
265
+ Midi.prototype.setChannel = function (number) {
266
+ this.channel=number - 1;
267
+ this.noteOnAndChannel = "%9" + this.channel.toString(16);
268
+ };
269
+
270
+ Midi.prototype.startNote = function (pitch, loudness) {
271
+ this.track+=toDurationHex(this.silencelength); // only need to shift by amount of silence (if there is any)
272
+ this.silencelength = 0;
273
+ if (this.first) {
274
+ this.first = false;
275
+ this.track+=this.noteOnAndChannel;
276
+ }
277
+ this.track += "%"+pitch.toString(16)+"%"+loudness; //note
278
+ };
279
+
280
+ Midi.prototype.endNote = function (pitch, length) {
281
+ this.track += toDurationHex(length); //duration
282
+ this.track += "%"+pitch.toString(16)+"%00";//end note
283
+ };
284
+
285
+ Midi.prototype.addRest = function (length) {
286
+ this.silencelength += length;
287
+ };
288
+
289
+ Midi.prototype.embed = function(parent, noplayer) {
290
+
291
+ var data="data:audio/midi," +
292
+ "MThd%00%00%00%06%00%01"+toHex(this.trackcount,4)+"%01%e0"+ // header
293
+ this.trackstrings;
294
+
295
+ // var embedContainer = document.createElement("div");
296
+ // embedContainer.className = "embedContainer";
297
+ // document.body.appendChild(embedContainer);
298
+ // embedContainer.innerHTML = '<object id="embed1" classid="clsid:02BF25D5-8C17-4B23-BC80-D3488ABDDC6B" codebase="http://www.apple.com/qtactivex/qtplugin.cab"><param name="src" value="' + data + '"></param><param name="Autoplay" value="false"></param><embed name="embed1" src="' + data + '" autostart="false" enablejavascript="true" /></object>';
299
+ // embed = document["embed1"];
300
+
301
+
302
+ var link = setAttributes(document.createElement('a'), {
303
+ href: data
304
+ });
305
+ link.innerHTML = "download midi";
306
+ parent.insertBefore(link,parent.firstChild);
307
+
308
+ if (noplayer) return;
309
+
310
+ var embed = setAttributes(document.createElement('embed'), {
311
+ src : data,
312
+ type : 'video/quicktime',
313
+ controller : 'true',
314
+ autoplay : 'false',
315
+ loop : 'false',
316
+ enablejavascript: 'true',
317
+ style:'display:block; height: 20px;'
318
+ });
319
+ parent.insertBefore(embed,parent.firstChild);
320
+ };
321
+
322
+ // s is assumed to be of even length
323
+ function encodeHex(s) {
324
+ var ret = "";
325
+ for (var i=0; i<s.length; i+=2) {
326
+ ret += "%";
327
+ ret += s.substr(i,2);
328
+ }
329
+ return ret;
330
+ }
331
+
332
+ function toHex(n, padding) {
333
+ var s = n.toString(16);
334
+ while (s.length<padding) {
335
+ s="0"+s;
336
+ }
337
+ return encodeHex(s);
338
+ }
339
+
340
+ function toDurationHex(n) {
341
+ var res = 0;
342
+ var a = [];
343
+
344
+ // cut up into 7 bit chunks;
345
+ while (n!==0) {
346
+ a.push(n & 0x7F);
347
+ n = n>>7;
348
+ }
349
+
350
+ // join the 7 bit chunks together, all but last chunk get leading 1
351
+ for (var i=a.length-1;i>=0;i--) {
352
+ res = res << 8;
353
+ var bits = a[i];
354
+ if (i!==0) {
355
+ bits = bits | 0x80;
356
+ }
357
+ res = res | bits;
358
+ }
359
+
360
+ var padding = res.toString(16).length;
361
+ padding += padding%2;
362
+
363
+ return toHex(res, padding);
364
+ }
365
+
366
+ ABCJS.midi.MidiWriter = function(parent, options) {
367
+ options = options || {};
368
+ this.parent = parent;
369
+ this.scale = [0,2,4,5,7,9,11];
370
+ this.restart = {line:0, staff:0, voice:0, pos:0};
371
+ this.visited = {};
372
+ this.multiplier =1;
373
+ this.next = null;
374
+ this.qpm = options.qpm || 180;
375
+ this.program = options.program || 2;
376
+ this.noteOnAndChannel = "%90";
377
+ this.javamidi = options.type ==="java" || false;
378
+ this.listeners = [];
379
+ this.transpose = 0; // PER
380
+ if (this.javamidi) {
381
+ MIDIPlugin = document.MIDIPlugin;
382
+ setTimeout(function() { // run on next event loop (once MIDIPlugin is loaded)
383
+ try { // activate MIDIPlugin
384
+ MIDIPlugin.openPlugin();
385
+
386
+ } catch(e) { // plugin not supported (download externals)
387
+ var a = document.createElement("a");
388
+ a.href = "http://java.sun.com/products/java-media/sound/soundbanks.html";
389
+ a.target = "_blank";
390
+ a.appendChild(document.createTextNode("Download Soundbank"));
391
+ parent.appendChild(a);
392
+ }
393
+ }, 0);
394
+ }
395
+
396
+ };
397
+
398
+ ABCJS.midi.MidiWriter.prototype.addListener = function(listener) {
399
+ this.listeners.push(listener);
400
+ };
401
+
402
+ ABCJS.midi.MidiWriter.prototype.notifySelect = function (abcelem) {
403
+ for (var i=0; i<this.listeners.length;i++) {
404
+ this.listeners[i].notifySelect(abcelem.abselem);
405
+ }
406
+ };
407
+
408
+ ABCJS.midi.MidiWriter.prototype.getMark = function() {
409
+ return {line:this.line, staff:this.staff,
410
+ voice:this.voice, pos:this.pos};
411
+ };
412
+
413
+ ABCJS.midi.MidiWriter.prototype.getMarkString = function(mark) {
414
+ mark = mark || this;
415
+ return "line"+mark.line+"staff"+mark.staff+
416
+ "voice"+mark.voice+"pos"+mark.pos;
417
+ };
418
+
419
+ ABCJS.midi.MidiWriter.prototype.goToMark = function(mark) {
420
+ this.line=mark.line;
421
+ this.staff=mark.staff;
422
+ this.voice=mark.voice;
423
+ this.pos=mark.pos;
424
+ };
425
+
426
+ ABCJS.midi.MidiWriter.prototype.markVisited = function() {
427
+ this.lastmark = this.getMarkString();
428
+ this.visited[this.lastmark] = true;
429
+ };
430
+
431
+ ABCJS.midi.MidiWriter.prototype.isVisited = function() {
432
+ if (this.visited[this.getMarkString()]) return true;
433
+ return false;
434
+ };
435
+
436
+ ABCJS.midi.MidiWriter.prototype.setJumpMark = function(mark) {
437
+ this.visited[this.lastmark] = mark;
438
+ };
439
+
440
+ ABCJS.midi.MidiWriter.prototype.getJumpMark = function() {
441
+ return this.visited[this.getMarkString()];
442
+ };
443
+
444
+ ABCJS.midi.MidiWriter.prototype.getLine = function() {
445
+ return this.abctune.lines[this.line];
446
+ };
447
+
448
+ ABCJS.midi.MidiWriter.prototype.getStaff = function() {
449
+ try {
450
+ return this.getLine().staff[this.staff];
451
+ } catch (e) {
452
+
453
+ }
454
+ };
455
+
456
+ ABCJS.midi.MidiWriter.prototype.getVoice = function() {
457
+ return this.getStaff().voices[this.voice];
458
+ };
459
+
460
+ ABCJS.midi.MidiWriter.prototype.getElem = function() {
461
+ return this.getVoice()[this.pos];
462
+ };
463
+
464
+ ABCJS.midi.MidiWriter.prototype.writeABC = function(abctune) {
465
+ try {
466
+ this.midi = (this.javamidi) ? new MidiProxy(new JavaMidi(this), new Midi()) : new Midi();
467
+ this.baraccidentals = [];
468
+ this.abctune = abctune;
469
+ this.baseduration = 480*4; // nice and divisible, equals 1 whole note
470
+
471
+ // PER: add global transposition.
472
+ if (abctune.formatting.midi && abctune.formatting.midi.transpose)
473
+ this.transpose = abctune.formatting.midi.transpose;
474
+
475
+ // PER: changed format of the global midi commands from the parser. Using the new definition here.
476
+ if (abctune.formatting.midi && abctune.formatting.midi.program && abctune.formatting.midi.program.program) {
477
+ this.midi.setInstrument(abctune.formatting.midi.program.program);
478
+ } else {
479
+ this.midi.setInstrument(this.program);
480
+ }
481
+ if (abctune.formatting.midi && abctune.formatting.midi.channel) {
482
+ this.midi.setChannel(abctune.formatting.midi.channel);
483
+ }
484
+
485
+ if (abctune.metaText.tempo) {
486
+ var duration = 1/4;
487
+ if (abctune.metaText.tempo.duration) {
488
+ duration = abctune.metaText.tempo.duration[0];
489
+ }
490
+ var bpm = 60;
491
+ if (abctune.metaText.tempo.bpm) {
492
+ bpm = abctune.metaText.tempo.bpm;
493
+ }
494
+ this.qpm = bpm*duration*4;
495
+ }
496
+ this.midi.setTempo(this.qpm);
497
+
498
+ // visit each voice completely in turn
499
+ // "problematic" because it means visiting only one staff+voice for each line each time
500
+ this.staffcount=1; // we'll know the actual number once we enter the code
501
+ for(this.staff=0;this.staff<this.staffcount;this.staff++) {
502
+ this.voicecount=1;
503
+ for(this.voice=0;this.voice<this.voicecount;this.voice++) {
504
+ this.midi.startTrack();
505
+ this.restart = {line:0, staff:this.staff, voice:this.voice, pos:0};
506
+ this.next= null;
507
+ for(this.line=0; this.line<abctune.lines.length; this.line++) {
508
+ var abcline = abctune.lines[this.line];
509
+ if (this.getLine().staff) {
510
+ this.writeABCLine();
511
+ }
512
+ }
513
+ this.midi.endTrack();
514
+ }
515
+ }
516
+
517
+ this.midi.embed(this.parent);
518
+ } catch (e) {
519
+ this.parent.innerHTML="Couldn't write midi: "+e;
520
+ }
521
+ };
522
+
523
+ ABCJS.midi.MidiWriter.prototype.writeABCLine = function() {
524
+ this.staffcount = this.getLine().staff.length;
525
+ this.voicecount = this.getStaff().voices.length;
526
+ this.setKeySignature(this.getStaff().key);
527
+ this.writeABCVoiceLine();
528
+ };
529
+
530
+ ABCJS.midi.MidiWriter.prototype.writeABCVoiceLine = function () {
531
+ this.pos=0;
532
+ while (this.pos<this.getVoice().length) {
533
+ this.writeABCElement(this.getElem());
534
+ if (this.next) {
535
+ this.goToMark(this.next);
536
+ this.next = null;
537
+ if (!this.getLine().staff) return;
538
+ } else {
539
+ this.pos++;
540
+ }
541
+ }
542
+ };
543
+
544
+ ABCJS.midi.MidiWriter.prototype.writeABCElement = function(elem) {
545
+ var foo;
546
+ switch (elem.el_type) {
547
+ case "note":
548
+ this.writeNote(elem);
549
+ break;
550
+
551
+ case "key":
552
+ this.setKeySignature(elem);
553
+ break;
554
+ case "bar":
555
+ this.handleBar(elem);
556
+ break;
557
+ case "meter":
558
+ case "clef":
559
+ break;
560
+ default:
561
+
562
+ }
563
+
564
+ };
565
+
566
+
567
+ ABCJS.midi.MidiWriter.prototype.writeNote = function(elem) {
568
+
569
+ if (elem.startTriplet) {
570
+ this.multiplier=2/3;
571
+ }
572
+
573
+ var mididuration = elem.duration*this.baseduration*this.multiplier;
574
+ if (elem.pitches) {
575
+ var midipitches = [];
576
+ for (var i=0; i<elem.pitches.length; i++) {
577
+ var note = elem.pitches[i];
578
+ var pitch= note.pitch;
579
+ if (note.accidental) {
580
+ switch(note.accidental) { // change that pitch (not other octaves) for the rest of the bar
581
+ case "sharp":
582
+ this.baraccidentals[pitch]=1; break;
583
+ case "flat":
584
+ this.baraccidentals[pitch]=-1; break;
585
+ case "natural":
586
+ this.baraccidentals[pitch]=0; break;
587
+ case "dblsharp":
588
+ this.baraccidentals[pitch]=2; break;
589
+ case "dblflat":
590
+ this.baraccidentals[pitch]=-2; break;
591
+ }
592
+ }
593
+
594
+ midipitches[i] = 60 + 12*this.extractOctave(pitch)+this.scale[this.extractNote(pitch)];
595
+
596
+ if (this.baraccidentals[pitch]!==undefined) {
597
+ midipitches[i] += this.baraccidentals[pitch];
598
+ } else { // use normal accidentals
599
+ midipitches[i] += this.accidentals[this.extractNote(pitch)];
600
+ }
601
+ midipitches[i] += this.transpose; // PER
602
+
603
+ this.midi.startNote(midipitches[i], 64, elem);
604
+
605
+ if (note.startTie) {
606
+ this.tieduration=mididuration;
607
+ }
608
+ }
609
+
610
+ for (i=0; i<elem.pitches.length; i++) {
611
+ var note = elem.pitches[i];
612
+ var pitch= note.pitch+this.transpose; // PER
613
+ if (note.startTie) continue; // don't terminate it
614
+ if (note.endTie) {
615
+ this.midi.endNote(midipitches[i],mididuration+this.tieduration);
616
+ } else {
617
+ this.midi.endNote(midipitches[i],mididuration);
618
+ }
619
+ mididuration = 0; // put these to zero as we've moved forward in the midi
620
+ this.tieduration=0;
621
+ }
622
+ } else if (elem.rest && elem.rest.type !== 'spacer') {
623
+ this.midi.addRest(mididuration);
624
+ }
625
+
626
+ if (elem.endTriplet) {
627
+ this.multiplier=1;
628
+ }
629
+
630
+ };
631
+
632
+ ABCJS.midi.MidiWriter.prototype.handleBar = function (elem) {
633
+ this.baraccidentals = [];
634
+
635
+
636
+ var repeat = (elem.type==="bar_right_repeat" || elem.type==="bar_dbl_repeat");
637
+ var skip = (elem.startEnding)?true:false;
638
+ var setvisited = (repeat || skip);
639
+ var setrestart = (elem.type==="bar_left_repeat" || elem.type==="bar_dbl_repeat" || elem.type==="bar_thick_thin" || elem.type==="bar_thin_thick" || elem.type==="bar_thin_thin" || elem.type==="bar_right_repeat");
640
+
641
+ var next = null;
642
+
643
+ if (this.isVisited()) {
644
+ next = this.getJumpMark();
645
+ } else {
646
+
647
+ if (skip || repeat) {
648
+ if (this.visited[this.lastmark] === true) {
649
+ this.setJumpMark(this.getMark());
650
+ }
651
+ }
652
+
653
+ if (setvisited) {
654
+ this.markVisited();
655
+ }
656
+
657
+ if (repeat) {
658
+ next = this.restart;
659
+ this.setJumpMark(this.getMark());
660
+ }
661
+ }
662
+
663
+ if (setrestart) {
664
+ this.restart = this.getMark();
665
+ }
666
+
667
+ if (next && this.getMarkString(next)!==this.getMarkString()) {
668
+ this.next = next;
669
+ }
670
+
671
+ };
672
+
673
+ ABCJS.midi.MidiWriter.prototype.setKeySignature = function(elem) {
674
+ this.accidentals = [0,0,0,0,0,0,0];
675
+ if (this.abctune.formatting.bagpipes) {
676
+ elem.accidentals=[{acc: 'natural', note: 'g'}, {acc: 'sharp', note: 'f'}, {acc: 'sharp', note: 'c'}];
677
+ }
678
+ if (!elem.accidentals) return;
679
+ window.ABCJS.parse.each(elem.accidentals, function(acc) {
680
+ var d = (acc.acc === "sharp") ? 1 : (acc.acc === "natural") ?0 : -1;
681
+
682
+ var lowercase = acc.note.toLowerCase();
683
+ var note = this.extractNote(lowercase.charCodeAt(0)-'c'.charCodeAt(0));
684
+ this.accidentals[note]+=d;
685
+ }, this);
686
+
687
+ };
688
+
689
+ ABCJS.midi.MidiWriter.prototype.extractNote = function(pitch) {
690
+ pitch = pitch%7;
691
+ if (pitch<0) pitch+=7;
692
+ return pitch;
693
+ };
694
+
695
+ ABCJS.midi.MidiWriter.prototype.extractOctave = function(pitch) {
696
+ return Math.floor(pitch/7);
697
+ };
698
+ })();