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 @@
1
+ //= require_tree ./abcjs
@@ -0,0 +1,158 @@
1
+ // abc_tunebook.js: splits a string representing ABC Music Notation into individual tunes.
2
+ // Copyright (C) 2010 Paul Rosen (paul at paulrosen dot net)
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
+ /*global document, Raphael */
18
+ /*global window */
19
+
20
+ if (!window.ABCJS)
21
+ window.ABCJS = {};
22
+
23
+ (function() {
24
+ ABCJS.numberOfTunes = function(abc) {
25
+ var tunes = abc.split("\nX:");
26
+ var num = tunes.length;
27
+ if (num === 0) num = 1;
28
+ return num;
29
+ };
30
+
31
+ ABCJS.TuneBook = function(book) {
32
+ var This = this;
33
+ var directives = "";
34
+ book = window.ABCJS.parse.strip(book);
35
+ var tunes = book.split("\nX:");
36
+ for (var i = 1; i < tunes.length; i++) // Put back the X: that we lost when splitting the tunes.
37
+ tunes[i] = "X:" + tunes[i];
38
+ // Keep track of the character position each tune starts with.
39
+ var pos = 0;
40
+ This.tunes = [];
41
+ window.ABCJS.parse.each(tunes, function(tune) {
42
+ This.tunes.push({ abc: tune, startPos: pos});
43
+ pos += tune.length;
44
+ });
45
+ if (This.tunes.length > 1 && !window.ABCJS.parse.startsWith(This.tunes[0].abc, 'X:')) { // If there is only one tune, the X: might be missing, otherwise assume the top of the file is "intertune"
46
+ // There could be file-wide directives in this, if so, we need to insert it into each tune. We can probably get away with
47
+ // just looking for file-wide directives here (before the first tune) and inserting them at the bottom of each tune, since
48
+ // the tune is parsed all at once. The directives will be seen before the printer begins processing.
49
+ var dir = This.tunes.shift();
50
+ var arrDir = dir.abc.split('\n');
51
+ window.ABCJS.parse.each(arrDir, function(line) {
52
+ if (window.ABCJS.parse.startsWith(line, '%%'))
53
+ directives += line + '\n';
54
+ });
55
+ }
56
+ // Now, the tune ends at a blank line, so truncate it if needed. There may be "intertune" stuff.
57
+ window.ABCJS.parse.each(This.tunes, function(tune) {
58
+ var end = tune.abc.indexOf('\n\n');
59
+ if (end > 0)
60
+ tune.abc = tune.abc.substring(0, end);
61
+ tune.abc = directives + tune.abc;
62
+ });
63
+ };
64
+
65
+ function renderEngine(callback, output, abc, parserParams, renderParams) {
66
+ var isArray = function(testObject) {
67
+ return testObject && !(testObject.propertyIsEnumerable('length')) && typeof testObject === 'object' && typeof testObject.length === 'number';
68
+ };
69
+
70
+ // check and normalize input parameters
71
+ if (output === undefined || abc === undefined)
72
+ return;
73
+ if (!isArray(output))
74
+ output = [ output ];
75
+ if (parserParams === undefined)
76
+ parserParams = {};
77
+ if (renderParams === undefined)
78
+ renderParams = {};
79
+ var currentTune = renderParams.startingTune ? renderParams.startingTune : 0;
80
+
81
+ // parse the abc string
82
+ var book = new ABCJS.TuneBook(abc);
83
+ var abcParser = new window.ABCJS.parse.Parse();
84
+
85
+ // output each tune, if it exists. Otherwise clear the div.
86
+ for (var i = 0; i < output.length; i++) {
87
+ var div = output[i];
88
+ if (typeof(div) === "string")
89
+ div = document.getElementById(div);
90
+ if (div) {
91
+ div.innerHTML = "";
92
+ if (currentTune < book.tunes.length) {
93
+ abcParser.parse(book.tunes[currentTune].abc, parserParams);
94
+ var tune = abcParser.getTune();
95
+ callback(div, tune);
96
+ }
97
+ }
98
+ currentTune++;
99
+ }
100
+ }
101
+
102
+ // A quick way to render a tune from javascript when interactivity is not required.
103
+ // This is used when a javascript routine has some abc text that it wants to render
104
+ // in a div or collection of divs. One tune or many can be rendered.
105
+ //
106
+ // parameters:
107
+ // output: an array of divs that the individual tunes are rendered to.
108
+ // If the number of tunes exceeds the number of divs in the array, then
109
+ // only the first tunes are rendered. If the number of divs exceeds the number
110
+ // of tunes, then the unused divs are cleared. The divs can be passed as either
111
+ // elements or strings of ids. If ids are passed, then the div MUST exist already.
112
+ // (if a single element is passed, then it is an implied array of length one.)
113
+ // (if a null is passed for an element, or the element doesn't exist, then that tune is skipped.)
114
+ // abc: text representing a tune or an entire tune book in ABC notation.
115
+ // renderParams: hash of:
116
+ // startingTune: an index, starting at zero, representing which tune to start rendering at.
117
+ // (If this element is not present, then rendering starts at zero.)
118
+ // width: 800 by default. The width in pixels of the output paper
119
+ ABCJS.renderAbc = function(output, abc, parserParams, printerParams, renderParams) {
120
+ function callback(div, tune) {
121
+ var width = renderParams ? renderParams.width ? renderParams.width : 800 : 800;
122
+ var paper = Raphael(div, width, 400);
123
+ if (printerParams === undefined)
124
+ printerParams = {};
125
+ var printer = new ABCJS.write.Printer(paper, printerParams);
126
+ printer.printABC(tune);
127
+ }
128
+
129
+ renderEngine(callback, output, abc, parserParams, renderParams);
130
+ };
131
+
132
+ // A quick way to render a tune from javascript when interactivity is not required.
133
+ // This is used when a javascript routine has some abc text that it wants to render
134
+ // in a div or collection of divs. One tune or many can be rendered.
135
+ //
136
+ // parameters:
137
+ // output: an array of divs that the individual tunes are rendered to.
138
+ // If the number of tunes exceeds the number of divs in the array, then
139
+ // only the first tunes are rendered. If the number of divs exceeds the number
140
+ // of tunes, then the unused divs are cleared. The divs can be passed as either
141
+ // elements or strings of ids. If ids are passed, then the div MUST exist already.
142
+ // (if a single element is passed, then it is an implied array of length one.)
143
+ // (if a null is passed for an element, or the element doesn't exist, then that tune is skipped.)
144
+ // abc: text representing a tune or an entire tune book in ABC notation.
145
+ // renderParams: hash of:
146
+ // startingTune: an index, starting at zero, representing which tune to start rendering at.
147
+ // (If this element is not present, then rendering starts at zero.)
148
+ ABCJS.renderMidi = function(output, abc, parserParams, midiParams, renderParams) {
149
+ function callback(div, tune) {
150
+ if (midiParams === undefined)
151
+ midiParams = {};
152
+ var midiwriter = new ABCJS.midi.MidiWriter(div, midiParams);
153
+ midiwriter.writeABC(tune);
154
+ }
155
+
156
+ renderEngine(callback, output, abc, parserParams, renderParams);
157
+ };
158
+ })();
@@ -0,0 +1,686 @@
1
+ // abc_tune.js: a computer usable internal structure representing one tune.
2
+ // Copyright (C) 2010 Paul Rosen (paul at paulrosen dot net)
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
+ /*global window */
18
+
19
+ if (!window.ABCJS)
20
+ window.ABCJS = {};
21
+
22
+ if (!window.ABCJS.data)
23
+ window.ABCJS.data = {};
24
+
25
+ // This is the data for a single ABC tune. It is created and populated by the window.ABCJS.parse.Parse class.
26
+ window.ABCJS.data.Tune = function() {
27
+ // The structure consists of a hash with the following two items:
28
+ // metaText: a hash of {key, value}, where key is one of: title, author, rhythm, source, transcription, unalignedWords, etc...
29
+ // tempo: { noteLength: number (e.g. .125), bpm: number }
30
+ // lines: an array of elements, or one of the following:
31
+ //
32
+ // STAFF: array of elements
33
+ // SUBTITLE: string
34
+ //
35
+ // TODO: actually, the start and end char should modify each part of the note type
36
+ // The elements all have a type field and a start and end char
37
+ // field. The rest of the fields depend on the type and are listed below:
38
+ // REST: duration=1,2,4,8; chord: string
39
+ // NOTE: accidental=none,dbl_flat,flat,natural,sharp,dbl_sharp
40
+ // pitch: "C" is 0. The numbers refer to the pitch letter.
41
+ // duration: .5 (sixteenth), .75 (dotted sixteenth), 1 (eighth), 1.5 (dotted eighth)
42
+ // 2 (quarter), 3 (dotted quarter), 4 (half), 6 (dotted half) 8 (whole)
43
+ // chord: { name:chord, position: one of 'default', 'above', 'below' }
44
+ // end_beam = true or undefined if this is the last note in a beam.
45
+ // lyric: array of { syllable: xxx, divider: one of " -_" }
46
+ // startTie = true|undefined
47
+ // endTie = true|undefined
48
+ // startTriplet = num <- that is the number to print
49
+ // endTriplet = true|undefined (the last note of the triplet)
50
+ // TODO: actually, decoration should be an array.
51
+ // decoration: upbow, downbow, accent
52
+ // BAR: type=bar_thin, bar_thin_thick, bar_thin_thin, bar_thick_thin, bar_right_repeat, bar_left_repeat, bar_double_repeat
53
+ // number: 1 or 2: if it is the start of a first or second ending
54
+ // CLEF: type=treble,bass
55
+ // KEY-SIG:
56
+ // accidentals[]: { acc:sharp|dblsharp|natural|flat|dblflat, note:a|b|c|d|e|f|g }
57
+ // METER: type: common_time,cut_time,specified
58
+ // if specified, { num: 99, den: 99 }
59
+ this.reset = function () {
60
+ this.version = "1.0.1";
61
+ this.media = "screen";
62
+ this.metaText = {};
63
+ this.formatting = {};
64
+ this.lines = [];
65
+ this.staffNum = 0;
66
+ this.voiceNum = 0;
67
+ this.lineNum = 0;
68
+ };
69
+
70
+ this.cleanUp = function(defWidth, defLength, barsperstaff, staffnonote) {
71
+ this.closeLine(); // Close the last line.
72
+
73
+ // Remove any blank lines
74
+ var anyDeleted = false;
75
+ var i, s, v;
76
+ for (i = 0; i < this.lines.length; i++) {
77
+ if (this.lines[i].staff !== undefined) {
78
+ var hasAny = false;
79
+ for (s = 0; s < this.lines[i].staff.length; s++) {
80
+ if (this.lines[i].staff[s] === undefined) {
81
+ anyDeleted = true;
82
+ this.lines[i].staff[s] = null;
83
+ //this.lines[i].staff[s] = { voices: []}; // TODO-PER: There was a part missing in the abc music. How should we recover?
84
+ } else {
85
+ for (v = 0; v < this.lines[i].staff[s].voices.length; v++) {
86
+ if (this.lines[i].staff[s].voices[v] === undefined)
87
+ this.lines[i].staff[s].voices[v] = []; // TODO-PER: There was a part missing in the abc music. How should we recover?
88
+ else
89
+ if (this.containsNotes(this.lines[i].staff[s].voices[v])) hasAny = true;
90
+ }
91
+ }
92
+ }
93
+ if (!hasAny) {
94
+ this.lines[i] = null;
95
+ anyDeleted = true;
96
+ }
97
+ }
98
+ }
99
+ if (anyDeleted) {
100
+ this.lines = window.ABCJS.parse.compact(this.lines);
101
+ window.ABCJS.parse.each(this.lines, function(line) {
102
+ if (line.staff)
103
+ line.staff = window.ABCJS.parse.compact(line.staff);
104
+ });
105
+ }
106
+
107
+ // if we exceeded the number of bars allowed on a line, then force a new line
108
+ if (barsperstaff) {
109
+ for (i = 0; i < this.lines.length; i++) {
110
+ if (this.lines[i].staff !== undefined) {
111
+ for (s = 0; s < this.lines[i].staff.length; s++) {
112
+ for (v = 0; v < this.lines[i].staff[s].voices.length; v++) {
113
+ var barNumThisLine = 0;
114
+ for (var n = 0; n < this.lines[i].staff[s].voices[v].length; n++) {
115
+ if (this.lines[i].staff[s].voices[v][n].el_type === 'bar') {
116
+ barNumThisLine++;
117
+ if (barNumThisLine >= barsperstaff) {
118
+ // push everything else to the next line, if there is anything else,
119
+ // and there is a next line. If there isn't a next line, create one.
120
+ if (n < this.lines[i].staff[s].voices[v].length - 1) {
121
+ if (i === this.lines.length - 1) {
122
+ var cp = JSON.parse(JSON.stringify(this.lines[i]));
123
+ this.lines.push(window.ABCJS.parse.clone(cp));
124
+ for (var ss = 0; ss < this.lines[i+1].staff.length; ss++) {
125
+ for (var vv = 0; vv < this.lines[i+1].staff[ss].voices.length; vv++)
126
+ this.lines[i+1].staff[ss].voices[vv] = [];
127
+ }
128
+ }
129
+ var startElement = n + 1;
130
+ var section = this.lines[i].staff[s].voices[v].slice(startElement);
131
+ this.lines[i].staff[s].voices[v] = this.lines[i].staff[s].voices[v].slice(0, startElement);
132
+ this.lines[i+1].staff[s].voices[v] = section.concat(this.lines[i+1].staff[s].voices[v]);
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ }
139
+ }
140
+ }
141
+ }
142
+ }
143
+
144
+ // If we were passed staffnonote, then we want to get rid of all staffs that contain only rests.
145
+ if (barsperstaff) {
146
+ anyDeleted = false;
147
+ for (i = 0; i < this.lines.length; i++) {
148
+ if (this.lines[i].staff !== undefined) {
149
+ for (s = 0; s < this.lines[i].staff.length; s++) {
150
+ var keepThis = false;
151
+ for (v = 0; v < this.lines[i].staff[s].voices.length; v++) {
152
+ if (this.containsNotesStrict(this.lines[i].staff[s].voices[v])) {
153
+ keepThis = true;
154
+ }
155
+ }
156
+ if (!keepThis) {
157
+ anyDeleted = true;
158
+ this.lines[i].staff[s] = null;
159
+ }
160
+ }
161
+ }
162
+ }
163
+ if (anyDeleted) {
164
+ window.ABCJS.parse.each(this.lines, function(line) {
165
+ if (line.staff)
166
+ line.staff = window.ABCJS.parse.compact(line.staff);
167
+ });
168
+ }
169
+ }
170
+
171
+ function cleanUpSlursInLine(line) {
172
+ var currSlur = [];
173
+ var x;
174
+ // var lyr = null; // TODO-PER: debugging.
175
+
176
+ var addEndSlur = function(obj, num, chordPos) {
177
+ if (currSlur[chordPos] === undefined) {
178
+ // There isn't an exact match for note position, but we'll take any other open slur.
179
+ for (x = 0; x < currSlur.length; x++) {
180
+ if (currSlur[x] !== undefined) {
181
+ chordPos = x;
182
+ break;
183
+ }
184
+ }
185
+ if (currSlur[chordPos] === undefined) {
186
+ var offNum = chordPos*100;
187
+ window.ABCJS.parse.each(obj.endSlur, function(x) { if (offNum === x) --offNum; });
188
+ currSlur[chordPos] = [offNum];
189
+ }
190
+ }
191
+ var slurNum;
192
+ for (var i = 0; i < num; i++) {
193
+ slurNum = currSlur[chordPos].pop();
194
+ obj.endSlur.push(slurNum);
195
+ // lyr.syllable += '<' + slurNum; // TODO-PER: debugging
196
+ }
197
+ if (currSlur[chordPos].length === 0)
198
+ delete currSlur[chordPos];
199
+ return slurNum;
200
+ };
201
+
202
+ var addStartSlur = function(obj, num, chordPos, usedNums) {
203
+ obj.startSlur = [];
204
+ if (currSlur[chordPos] === undefined) {
205
+ currSlur[chordPos] = [];
206
+ }
207
+ var nextNum = chordPos*100+1;
208
+ for (var i = 0; i < num; i++) {
209
+ if (usedNums) {
210
+ window.ABCJS.parse.each(usedNums, function(x) { if (nextNum === x) ++nextNum; });
211
+ window.ABCJS.parse.each(usedNums, function(x) { if (nextNum === x) ++nextNum; });
212
+ window.ABCJS.parse.each(usedNums, function(x) { if (nextNum === x) ++nextNum; });
213
+ }
214
+ window.ABCJS.parse.each(currSlur[chordPos], function(x) { if (nextNum === x) ++nextNum; });
215
+ window.ABCJS.parse.each(currSlur[chordPos], function(x) { if (nextNum === x) ++nextNum; });
216
+
217
+ currSlur[chordPos].push(nextNum);
218
+ obj.startSlur.push({ label: nextNum });
219
+ // lyr.syllable += ' ' + nextNum + '>'; // TODO-PER:debugging
220
+ nextNum++;
221
+ }
222
+ };
223
+
224
+ for (var i = 0; i < line.length; i++) {
225
+ var el = line[i];
226
+ // if (el.lyric === undefined) // TODO-PER: debugging
227
+ // el.lyric = [{ divider: '-' }]; // TODO-PER: debugging
228
+ // lyr = el.lyric[0]; // TODO-PER: debugging
229
+ // lyr.syllable = ''; // TODO-PER: debugging
230
+ if (el.el_type === 'note') {
231
+ if (el.gracenotes) {
232
+ for (var g = 0; g < el.gracenotes.length; g++) {
233
+ if (el.gracenotes[g].endSlur) {
234
+ var gg = el.gracenotes[g].endSlur;
235
+ el.gracenotes[g].endSlur = [];
236
+ for (var ggg = 0; ggg < gg; ggg++)
237
+ addEndSlur(el.gracenotes[g], 1, 20);
238
+ }
239
+ if (el.gracenotes[g].startSlur) {
240
+ x = el.gracenotes[g].startSlur;
241
+ addStartSlur(el.gracenotes[g], x, 20);
242
+ }
243
+ }
244
+ }
245
+ if (el.endSlur) {
246
+ x = el.endSlur;
247
+ el.endSlur = [];
248
+ addEndSlur(el, x, 0);
249
+ }
250
+ if (el.startSlur) {
251
+ x = el.startSlur;
252
+ addStartSlur(el, x, 0);
253
+ }
254
+ if (el.pitches) {
255
+ var usedNums = [];
256
+ for (var p = 0; p < el.pitches.length; p++) {
257
+ if (el.pitches[p].endSlur) {
258
+ var k = el.pitches[p].endSlur;
259
+ el.pitches[p].endSlur = [];
260
+ for (var j = 0; j < k; j++) {
261
+ var slurNum = addEndSlur(el.pitches[p], 1, p+1);
262
+ usedNums.push(slurNum);
263
+ }
264
+ }
265
+ }
266
+ for (p = 0; p < el.pitches.length; p++) {
267
+ if (el.pitches[p].startSlur) {
268
+ x = el.pitches[p].startSlur;
269
+ addStartSlur(el.pitches[p], x, p+1, usedNums);
270
+ }
271
+ }
272
+ // Correct for the weird gracenote case where ({g}a) should match.
273
+ // The end slur was already assigned to the note, and needs to be moved to the first note of the graces.
274
+ if (el.gracenotes && el.pitches[0].endSlur && el.pitches[0].endSlur[0] === 100 && el.pitches[0].startSlur) {
275
+ if (el.gracenotes[0].endSlur)
276
+ el.gracenotes[0].endSlur.push(el.pitches[0].startSlur[0].label);
277
+ else
278
+ el.gracenotes[0].endSlur = [el.pitches[0].startSlur[0].label];
279
+ if (el.pitches[0].endSlur.length === 1)
280
+ delete el.pitches[0].endSlur;
281
+ else if (el.pitches[0].endSlur[0] === 100)
282
+ el.pitches[0].endSlur.shift();
283
+ else if (el.pitches[0].endSlur[el.pitches[0].endSlur.length-1] === 100)
284
+ el.pitches[0].endSlur.pop();
285
+ if (currSlur[1].length === 1)
286
+ delete currSlur[1];
287
+ else
288
+ currSlur[1].pop();
289
+ }
290
+ }
291
+ }
292
+ }
293
+ }
294
+
295
+ // TODO-PER: This could be done faster as we go instead of as the last step.
296
+ function fixClefPlacement(el) {
297
+ //if (el.el_type === 'clef') {
298
+ var min = -2;
299
+ var max = 5;
300
+ switch(el.type) {
301
+ case 'treble+8':
302
+ case 'treble-8':
303
+ break;
304
+ case 'bass':
305
+ case 'bass+8':
306
+ case 'bass-8':
307
+ el.verticalPos = 20 + el.verticalPos; min += 6; max += 6;
308
+ break;
309
+ case 'tenor':
310
+ case 'tenor+8':
311
+ case 'tenor-8':
312
+ el.verticalPos = - el.verticalPos; min = -40; max = 40;
313
+ // el.verticalPos+=2; min += 6; max += 6;
314
+ break;
315
+ case 'alto':
316
+ case 'alto+8':
317
+ case 'alto-8':
318
+ el.verticalPos = - el.verticalPos; min = -40; max = 40;
319
+ // el.verticalPos-=2; min += 4; max += 4;
320
+ break;
321
+ }
322
+ if (el.verticalPos < min) {
323
+ while (el.verticalPos < min)
324
+ el.verticalPos += 7;
325
+ } else if (el.verticalPos > max) {
326
+ while (el.verticalPos > max)
327
+ el.verticalPos -= 7;
328
+ }
329
+ //}
330
+ }
331
+
332
+ for (this.lineNum = 0; this.lineNum < this.lines.length; this.lineNum++) {
333
+ if (this.lines[this.lineNum].staff) for (this.staffNum = 0; this.staffNum < this.lines[this.lineNum].staff.length; this.staffNum++) {
334
+ if (this.lines[this.lineNum].staff[this.staffNum].clef)
335
+ fixClefPlacement(this.lines[this.lineNum].staff[this.staffNum].clef);
336
+ for (this.voiceNum = 0; this.voiceNum < this.lines[this.lineNum].staff[this.staffNum].voices.length; this.voiceNum++) {
337
+ // var el = this.getLastNote();
338
+ // if (el) el.end_beam = true;
339
+ cleanUpSlursInLine(this.lines[this.lineNum].staff[this.staffNum].voices[this.voiceNum]);
340
+ for (var j = 0; j < this.lines[this.lineNum].staff[this.staffNum].voices[this.voiceNum].length; j++)
341
+ if (this.lines[this.lineNum].staff[this.staffNum].voices[this.voiceNum][j].el_type === 'clef')
342
+ fixClefPlacement(this.lines[this.lineNum].staff[this.staffNum].voices[this.voiceNum][j]);
343
+ }
344
+ }
345
+ }
346
+
347
+ if (!this.formatting.pagewidth)
348
+ this.formatting.pagewidth = defWidth;
349
+ if (!this.formatting.pageheight)
350
+ this.formatting.pageheight = defLength;
351
+
352
+ // Remove temporary variables that the outside doesn't need to know about
353
+ delete this.staffNum;
354
+ delete this.voiceNum;
355
+ delete this.lineNum;
356
+ delete this.potentialStartBeam;
357
+ delete this.potentialEndBeam;
358
+ delete this.vskipPending;
359
+ };
360
+
361
+ this.reset();
362
+
363
+ this.getLastNote = function() {
364
+ if (this.lines[this.lineNum] && this.lines[this.lineNum].staff && this.lines[this.lineNum].staff[this.staffNum] &&
365
+ this.lines[this.lineNum].staff[this.staffNum].voices[this.voiceNum]) {
366
+ for (var i = this.lines[this.lineNum].staff[this.staffNum].voices[this.voiceNum].length-1; i >= 0; i--) {
367
+ var el = this.lines[this.lineNum].staff[this.staffNum].voices[this.voiceNum][i];
368
+ if (el.el_type === 'note') {
369
+ return el;
370
+ }
371
+ }
372
+ }
373
+ return null;
374
+ };
375
+
376
+ this.addTieToLastNote = function() {
377
+ // TODO-PER: if this is a chord, which note?
378
+ var el = this.getLastNote();
379
+ if (el && el.pitches && el.pitches.length > 0) {
380
+ el.pitches[0].startTie = {};
381
+ return true;
382
+ }
383
+ return false;
384
+ };
385
+
386
+ this.getDuration = function(el) {
387
+ if (el.duration) return el.duration;
388
+ //if (el.pitches && el.pitches.length > 0) return el.pitches[0].duration;
389
+ return 0;
390
+ };
391
+
392
+ this.closeLine = function() {
393
+ if (this.potentialStartBeam && this.potentialEndBeam) {
394
+ this.potentialStartBeam.startBeam = true;
395
+ this.potentialEndBeam.endBeam = true;
396
+ }
397
+ delete this.potentialStartBeam;
398
+ delete this.potentialEndBeam;
399
+ };
400
+
401
+ this.appendElement = function(type, startChar, endChar, hashParams)
402
+ {
403
+ var This = this;
404
+ var pushNote = function(hp) {
405
+ if (hp.pitches !== undefined) {
406
+ var mid = This.lines[This.lineNum].staff[This.staffNum].clef.verticalPos;
407
+ window.ABCJS.parse.each(hp.pitches, function(p) { p.verticalPos = p.pitch - mid; });
408
+ }
409
+ if (hp.gracenotes !== undefined) {
410
+ var mid2 = This.lines[This.lineNum].staff[This.staffNum].clef.verticalPos;
411
+ window.ABCJS.parse.each(hp.gracenotes, function(p) { p.verticalPos = p.pitch - mid2; });
412
+ }
413
+ This.lines[This.lineNum].staff[This.staffNum].voices[This.voiceNum].push(hp);
414
+ };
415
+ hashParams.el_type = type;
416
+ if (startChar !== null)
417
+ hashParams.startChar = startChar;
418
+ if (endChar !== null)
419
+ hashParams.endChar = endChar;
420
+ var endBeamHere = function() {
421
+ This.potentialStartBeam.startBeam = true;
422
+ hashParams.endBeam = true;
423
+ delete This.potentialStartBeam;
424
+ delete This.potentialEndBeam;
425
+ };
426
+ var endBeamLast = function() {
427
+ if (This.potentialStartBeam !== undefined && This.potentialEndBeam !== undefined) { // Do we have a set of notes to beam?
428
+ This.potentialStartBeam.startBeam = true;
429
+ This.potentialEndBeam.endBeam = true;
430
+ }
431
+ delete This.potentialStartBeam;
432
+ delete This.potentialEndBeam;
433
+ };
434
+ if (type === 'note') { // && (hashParams.rest !== undefined || hashParams.end_beam === undefined)) {
435
+ // Now, add the startBeam and endBeam where it is needed.
436
+ // end_beam is already set on the places where there is a forced end_beam. We'll remove that here after using that info.
437
+ // this.potentialStartBeam either points to null or the start beam.
438
+ // this.potentialEndBeam either points to null or the start beam.
439
+ // If we have a beam break (note is longer than a quarter, or an end_beam is on this element), then set the beam if we have one.
440
+ // reset the variables for the next notes.
441
+ var dur = This.getDuration(hashParams);
442
+ if (dur >= 0.25) { // The beam ends on the note before this.
443
+ endBeamLast();
444
+ } else if (hashParams.force_end_beam_last && This.potentialStartBeam !== undefined) {
445
+ endBeamLast();
446
+ } else if (hashParams.end_beam && This.potentialStartBeam !== undefined) { // the beam is forced to end on this note, probably because of a space in the ABC
447
+ if (hashParams.rest === undefined)
448
+ endBeamHere();
449
+ else
450
+ endBeamLast();
451
+ } else if (hashParams.rest === undefined) { // this a short note and we aren't about to end the beam
452
+ if (This.potentialStartBeam === undefined) { // We aren't collecting notes for a beam, so start here.
453
+ if (!hashParams.end_beam) {
454
+ This.potentialStartBeam = hashParams;
455
+ delete This.potentialEndBeam;
456
+ }
457
+ } else {
458
+ This.potentialEndBeam = hashParams; // Continue the beaming, look for the end next note.
459
+ }
460
+ }
461
+
462
+ // end_beam goes on rests and notes which precede rests _except_ when a rest (or set of adjacent rests) has normal notes on both sides (no spaces)
463
+ // if (hashParams.rest !== undefined)
464
+ // {
465
+ // hashParams.end_beam = true;
466
+ // var el2 = this.getLastNote();
467
+ // if (el2) el2.end_beam = true;
468
+ // // TODO-PER: implement exception mentioned in the comment.
469
+ // }
470
+ } else { // It's not a note, so there definitely isn't beaming after it.
471
+ endBeamLast();
472
+ }
473
+ delete hashParams.end_beam; // We don't want this temporary variable hanging around.
474
+ delete hashParams.force_end_beam_last; // We don't want this temporary variable hanging around.
475
+ pushNote(hashParams);
476
+ };
477
+
478
+ this.appendStartingElement = function(type, startChar, endChar, hashParams2)
479
+ {
480
+ // We only ever want implied naturals the first time.
481
+ var impliedNaturals;
482
+ if (type === 'key') {
483
+ impliedNaturals = hashParams2.impliedNaturals;
484
+ delete hashParams2.impliedNaturals;
485
+ }
486
+
487
+ // Clone the object because it will be sticking around for the next line and we don't want the extra fields in it.
488
+ var hashParams = window.ABCJS.parse.clone(hashParams2);
489
+
490
+ // 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.
491
+ var voice = this.lines[this.lineNum].staff[this.staffNum].voices[this.voiceNum];
492
+ for (var i = 0; i < voice.length; i++) {
493
+ if (voice[i].el_type === 'note' || voice[i].el_type === 'bar') {
494
+ hashParams.el_type = type;
495
+ hashParams.startChar = startChar;
496
+ hashParams.endChar = endChar;
497
+ if (impliedNaturals)
498
+ hashParams.accidentals = impliedNaturals.concat(hashParams.accidentals);
499
+ voice.push(hashParams);
500
+ return;
501
+ }
502
+ if (voice[i].el_type === type) {
503
+ hashParams.el_type = type;
504
+ hashParams.startChar = startChar;
505
+ hashParams.endChar = endChar;
506
+ if (impliedNaturals)
507
+ hashParams.accidentals = impliedNaturals.concat(hashParams.accidentals);
508
+ voice[i] = hashParams;
509
+ return;
510
+ }
511
+ }
512
+ // We didn't see either that type or a note, so replace the element to the staff.
513
+ this.lines[this.lineNum].staff[this.staffNum][type] = hashParams2;
514
+ };
515
+
516
+ this.getNumLines = function() {
517
+ return this.lines.length;
518
+ };
519
+
520
+ this.pushLine = function(hash) {
521
+ if (this.vskipPending) {
522
+ hash.vskip = this.vskipPending;
523
+ delete this.vskipPending;
524
+ }
525
+ this.lines.push(hash);
526
+ };
527
+
528
+ this.addSubtitle = function(str) {
529
+ this.pushLine({subtitle: str});
530
+ };
531
+
532
+ this.addSpacing = function(num) {
533
+ this.vskipPending = num;
534
+ };
535
+
536
+ this.addNewPage = function(num) {
537
+ this.pushLine({newpage: num});
538
+ };
539
+
540
+ this.addSeparator = function(spaceAbove, spaceBelow, lineLength) {
541
+ this.pushLine({separator: {spaceAbove: spaceAbove, spaceBelow: spaceBelow, lineLength: lineLength}});
542
+ };
543
+
544
+ this.addText = function(str) {
545
+ this.pushLine({text: str});
546
+ };
547
+
548
+ this.addCentered = function(str) {
549
+ this.pushLine({text: [{text: str, center: true }]});
550
+ };
551
+
552
+ this.containsNotes = function(voice) {
553
+ for (var i = 0; i < voice.length; i++) {
554
+ if (voice[i].el_type === 'note' || voice[i].el_type === 'bar')
555
+ return true;
556
+ }
557
+ return false;
558
+ };
559
+
560
+ this.containsNotesStrict = function(voice) {
561
+ for (var i = 0; i < voice.length; i++) {
562
+ if (voice[i].el_type === 'note' && voice[i].rest === undefined)
563
+ return true;
564
+ }
565
+ return false;
566
+ };
567
+
568
+ // anyVoiceContainsNotes: function(line) {
569
+ // for (var i = 0; i < line.staff.voices.length; i++) {
570
+ // if (this.containsNotes(line.staff.voices[i]))
571
+ // return true;
572
+ // }
573
+ // return false;
574
+ // },
575
+
576
+ this.startNewLine = function(params) {
577
+ // If the pointed to line doesn't exist, just create that. If the line does exist, but doesn't have any music on it, just use it.
578
+ // If it does exist and has music, then increment the line number. If the new element doesn't exist, create it.
579
+ var This = this;
580
+ this.closeLine(); // Close the previous line.
581
+ var createVoice = function(params) {
582
+ This.lines[This.lineNum].staff[This.staffNum].voices[This.voiceNum] = [];
583
+ if (This.isFirstLine(This.lineNum)) {
584
+ if (params.name) {if (!This.lines[This.lineNum].staff[This.staffNum].title) This.lines[This.lineNum].staff[This.staffNum].title = [];This.lines[This.lineNum].staff[This.staffNum].title[This.voiceNum] = params.name;}
585
+ } else {
586
+ if (params.subname) {if (!This.lines[This.lineNum].staff[This.staffNum].title) This.lines[This.lineNum].staff[This.staffNum].title = [];This.lines[This.lineNum].staff[This.staffNum].title[This.voiceNum] = params.subname;}
587
+ }
588
+ if (params.style)
589
+ This.appendElement('style', null, null, {head: params.style});
590
+ if (params.stem)
591
+ This.appendElement('stem', null, null, {direction: params.stem});
592
+ else if (This.voiceNum > 0) {
593
+ if (This.lines[This.lineNum].staff[This.staffNum].voices[0]!== undefined) {
594
+ var found = false;
595
+ for (var i = 0; i < This.lines[This.lineNum].staff[This.staffNum].voices[0].length; i++) {
596
+ if (This.lines[This.lineNum].staff[This.staffNum].voices[0].el_type === 'stem')
597
+ found = true;
598
+ }
599
+ if (!found) {
600
+ var stem = { el_type: 'stem', direction: 'up' };
601
+ This.lines[This.lineNum].staff[This.staffNum].voices[0].splice(0,0,stem);
602
+ }
603
+ }
604
+ This.appendElement('stem', null, null, {direction: 'down'});
605
+ }
606
+ if (params.scale)
607
+ This.appendElement('scale', null, null, { size: params.scale} );
608
+ };
609
+ var createStaff = function(params) {
610
+ This.lines[This.lineNum].staff[This.staffNum] = {voices: [ ], clef: params.clef, key: params.key};
611
+ if (params.vocalfont) This.lines[This.lineNum].staff[This.staffNum].vocalfont = params.vocalfont;
612
+ if (params.bracket) This.lines[This.lineNum].staff[This.staffNum].bracket = params.bracket;
613
+ if (params.brace) This.lines[This.lineNum].staff[This.staffNum].brace = params.brace;
614
+ if (params.connectBarLines) This.lines[This.lineNum].staff[This.staffNum].connectBarLines = params.connectBarLines;
615
+ createVoice(params);
616
+ // Some stuff just happens for the first voice
617
+ if (params.part)
618
+ This.appendElement('part', params.startChar, params.endChar, {title: params.part});
619
+ if (params.meter !== undefined) This.lines[This.lineNum].staff[This.staffNum].meter = params.meter;
620
+ };
621
+ var createLine = function(params) {
622
+ This.lines[This.lineNum] = {staff: []};
623
+ createStaff(params);
624
+ };
625
+ if (this.lines[this.lineNum] === undefined) createLine(params);
626
+ else if (this.lines[this.lineNum].staff === undefined) {
627
+ this.lineNum++;
628
+ this.startNewLine(params);
629
+ } else if (this.lines[this.lineNum].staff[this.staffNum] === undefined) createStaff(params);
630
+ else if (this.lines[this.lineNum].staff[this.staffNum].voices[this.voiceNum] === undefined) createVoice(params);
631
+ else if (!this.containsNotes(this.lines[this.lineNum].staff[this.staffNum].voices[this.voiceNum])) return;
632
+ else {
633
+ this.lineNum++;
634
+ this.startNewLine(params);
635
+ }
636
+ };
637
+
638
+ this.hasBeginMusic = function() {
639
+ return this.lines.length > 0;
640
+ };
641
+
642
+ this.isFirstLine = function(index) {
643
+ for (var i = index-1; i >= 0; i--) {
644
+ if (this.lines[i].staff !== undefined) return false;
645
+ }
646
+ return true;
647
+ };
648
+
649
+ this.getCurrentVoice = function() {
650
+ if (this.lines[this.lineNum] !== undefined && this.lines[this.lineNum].staff[this.staffNum] !== undefined && this.lines[this.lineNum].staff[this.staffNum].voices[this.voiceNum] !== undefined)
651
+ return this.lines[this.lineNum].staff[this.staffNum].voices[this.voiceNum];
652
+ else return null;
653
+ };
654
+
655
+ this.setCurrentVoice = function(staffNum, voiceNum) {
656
+ this.staffNum = staffNum;
657
+ this.voiceNum = voiceNum;
658
+ for (var i = 0; i < this.lines.length; i++) {
659
+ if (this.lines[i].staff) {
660
+ if (this.lines[i].staff[staffNum] === undefined || this.lines[i].staff[staffNum].voices[voiceNum] === undefined ||
661
+ !this.containsNotes(this.lines[i].staff[staffNum].voices[voiceNum] )) {
662
+ this.lineNum = i;
663
+ return;
664
+ }
665
+ }
666
+ }
667
+ this.lineNum = i;
668
+ };
669
+
670
+ this.addMetaText = function(key, value) {
671
+ if (this.metaText[key] === undefined)
672
+ this.metaText[key] = value;
673
+ else
674
+ this.metaText[key] += "\n" + value;
675
+ };
676
+
677
+ this.addMetaTextArray = function(key, value) {
678
+ if (this.metaText[key] === undefined)
679
+ this.metaText[key] = [value];
680
+ else
681
+ this.metaText[key].push(value);
682
+ };
683
+ this.addMetaTextObj = function(key, value) {
684
+ this.metaText[key] = value;
685
+ };
686
+ }