abcjs-rails 2.1 → 2.3

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.
@@ -0,0 +1,426 @@
1
+ // abc_midi_renderer.js: Create the actual format for the midi.
2
+ // Copyright (C) 2010,2015 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
+ if (!window.ABCJS)
18
+ window.ABCJS = {};
19
+
20
+ if (!window.ABCJS.midi)
21
+ window.ABCJS.midi = {};
22
+
23
+ (function() {
24
+ "use strict";
25
+ function setAttributes(elm, attrs) {
26
+ for (var attr in attrs)
27
+ if (attrs.hasOwnProperty(attr))
28
+ elm.setAttribute(attr, attrs[attr]);
29
+ return elm;
30
+ }
31
+
32
+ var MIDIPlugin;
33
+ //TODO-PER: put this back in when the MIDIPlugin works again.
34
+ //window.oldunload = window.onbeforeunload;
35
+ //window.onbeforeunload = function() {
36
+ // if (window.oldunload)
37
+ // window.oldunload();
38
+ // if (typeof(MIDIPlugin) !== "undefined" && MIDIPlugin) { // PER: take care of crash in IE 8
39
+ // MIDIPlugin.closePlugin();
40
+ // }
41
+ //};
42
+
43
+
44
+ function MidiProxy(javamidi, qtmidi) {
45
+ this.javamidi = javamidi;
46
+ this.qtmidi = qtmidi;
47
+ }
48
+
49
+ MidiProxy.prototype.setTempo = function(qpm) {
50
+ this.javamidi.setTempo(qpm);
51
+ this.qtmidi.setTempo(qpm);
52
+ };
53
+
54
+ MidiProxy.prototype.startTrack = function() {
55
+ this.javamidi.startTrack();
56
+ this.qtmidi.startTrack();
57
+ };
58
+
59
+ MidiProxy.prototype.endTrack = function() {
60
+ this.javamidi.endTrack();
61
+ this.qtmidi.endTrack();
62
+ };
63
+
64
+ MidiProxy.prototype.setInstrument = function(number) {
65
+ this.javamidi.setInstrument(number);
66
+ this.qtmidi.setInstrument(number);
67
+ };
68
+
69
+ MidiProxy.prototype.startNote = function(pitch, loudness, abcelem) {
70
+ this.javamidi.startNote(pitch, loudness, abcelem);
71
+ this.qtmidi.startNote(pitch, loudness, abcelem);
72
+ };
73
+
74
+ MidiProxy.prototype.endNote = function(pitch, length) {
75
+ this.javamidi.endNote(pitch, length);
76
+ this.qtmidi.endNote(pitch, length);
77
+ };
78
+
79
+ MidiProxy.prototype.addRest = function(length) {
80
+ this.javamidi.addRest(length);
81
+ this.qtmidi.addRest(length);
82
+ };
83
+
84
+ MidiProxy.prototype.embed = function(parent) {
85
+ this.javamidi.embed(parent);
86
+ this.qtmidi.embed(parent, true);
87
+ };
88
+
89
+ function JavaMidi(midiwriter) {
90
+ this.playlist = []; // contains {time:t,funct:f} pairs
91
+ this.trackcount = 0;
92
+ this.timecount = 0;
93
+ this.tempo = 60;
94
+ this.midiapi = MIDIPlugin;
95
+ this.midiwriter = midiwriter;
96
+ this.noteOnAndChannel = "%90";
97
+ }
98
+
99
+ JavaMidi.prototype.setTempo = function(qpm) {
100
+ this.tempo = qpm;
101
+ };
102
+
103
+ JavaMidi.prototype.startTrack = function() {
104
+ this.silencelength = 0;
105
+ this.trackcount++;
106
+ this.timecount = 0;
107
+ this.playlistpos = 0;
108
+ this.first = true;
109
+ if (this.instrument) {
110
+ this.setInstrument(this.instrument);
111
+ }
112
+ if (this.channel) {
113
+ this.setChannel(this.channel);
114
+ }
115
+ };
116
+
117
+ JavaMidi.prototype.endTrack = function() {
118
+ // need to do anything?
119
+ };
120
+
121
+ JavaMidi.prototype.setInstrument = function(number) {
122
+ this.instrument = number;
123
+ this.midiapi.setInstrument(number);
124
+ //TODO push this into the playlist?
125
+ };
126
+
127
+ JavaMidi.prototype.setChannel = function(number) {
128
+ this.channel = number;
129
+ this.midiapi.setChannel(number);
130
+ };
131
+
132
+ JavaMidi.prototype.updatePos = function() {
133
+ while (this.playlist[this.playlistpos] &&
134
+ this.playlist[this.playlistpos].time < this.timecount) {
135
+ this.playlistpos++;
136
+ }
137
+ };
138
+
139
+ JavaMidi.prototype.startNote = function(pitch, loudness, abcelem) {
140
+ this.timecount += this.silencelength;
141
+ this.silencelength = 0;
142
+ if (this.first) {
143
+ //nothing special if first?
144
+ }
145
+ this.updatePos();
146
+ var self = this;
147
+ this.playlist.splice(this.playlistpos, 0, {
148
+ time: this.timecount,
149
+ funct: function() {
150
+ self.midiapi.playNote(pitch);
151
+ self.midiwriter.notifySelect(abcelem);
152
+ }
153
+ });
154
+ };
155
+
156
+ JavaMidi.prototype.endNote = function(pitch, length) {
157
+ this.timecount += length;
158
+ this.updatePos();
159
+ var self = this;
160
+ this.playlist.splice(this.playlistpos, 0, {
161
+ time: this.timecount,
162
+ funct: function() {
163
+ self.midiapi.stopNote(pitch);
164
+ }
165
+ });
166
+ };
167
+
168
+ JavaMidi.prototype.addRest = function(length) {
169
+ this.silencelength += length;
170
+ };
171
+
172
+ JavaMidi.prototype.embed = function(parent) {
173
+
174
+
175
+ this.playlink = setAttributes(document.createElement('a'), {
176
+ style: "border:1px solid black; margin:3px;"
177
+ });
178
+ this.playlink.innerHTML = "play";
179
+ var self = this;
180
+ this.playlink.onmousedown = function() {
181
+ if (self.playing) {
182
+ this.innerHTML = "play";
183
+ self.pausePlay();
184
+ } else {
185
+ this.innerHTML = "pause";
186
+ self.startPlay();
187
+ }
188
+ };
189
+ parent.appendChild(this.playlink);
190
+
191
+ var stoplink = setAttributes(document.createElement('a'), {
192
+ style: "border:1px solid black; margin:3px;"
193
+ });
194
+ stoplink.innerHTML = "stop";
195
+ //var self = this;
196
+ stoplink.onmousedown = function() {
197
+ self.stopPlay();
198
+ };
199
+ parent.appendChild(stoplink);
200
+ this.i = 0;
201
+ this.currenttime = 0;
202
+ this.playing = false;
203
+ };
204
+
205
+ JavaMidi.prototype.stopPlay = function() {
206
+ this.i = 0;
207
+ this.currenttime = 0;
208
+ this.pausePlay();
209
+ this.playlink.innerHTML = "play";
210
+ };
211
+
212
+ JavaMidi.prototype.startPlay = function() {
213
+ this.playing = true;
214
+ var self = this;
215
+ // repeat every 16th note TODO see the min in the piece
216
+ this.ticksperinterval = 480 / 4;
217
+ this.doPlay();
218
+ this.playinterval = window.setInterval(function() {self.doPlay(); },
219
+ (60000 / (this.tempo * 4)));
220
+ };
221
+
222
+ JavaMidi.prototype.pausePlay = function() {
223
+ this.playing = false;
224
+ window.clearInterval(this.playinterval);
225
+ this.midiapi.stopAllNotes();
226
+ };
227
+
228
+ JavaMidi.prototype.doPlay = function() {
229
+ while (this.playlist[this.i] &&
230
+ this.playlist[this.i].time <= this.currenttime) {
231
+ this.playlist[this.i].funct();
232
+ this.i++;
233
+ }
234
+ if (this.playlist[this.i]) {
235
+ this.currenttime += this.ticksperinterval;
236
+ } else {
237
+ this.stopPlay();
238
+ }
239
+ };
240
+
241
+ function Midi() {
242
+ this.trackstrings = "";
243
+ this.trackcount = 0;
244
+ this.noteOnAndChannel = "%90";
245
+ }
246
+
247
+ Midi.prototype.setTempo = function(qpm) {
248
+ //console.log("setTempo",qpm);
249
+ if (this.trackcount === 0) {
250
+ this.startTrack();
251
+ this.track += "%00%FF%51%03" + toHex(Math.round(60000000 / qpm), 6);
252
+ this.endTrack();
253
+ }
254
+ };
255
+
256
+ Midi.prototype.setGlobalInfo = function(qpm, name) {
257
+ //console.log("setGlobalInfo",qpm, key, time, name);
258
+ if (this.trackcount === 0) {
259
+ this.startTrack();
260
+ this.track += "%00%FF%51%03" + toHex(Math.round(60000000 / qpm), 6);
261
+ // TODO-PER: we could also store the key and time signatures, something like:
262
+ //00 FF 5902 03 00 - key signature
263
+ //00 FF 5804 04 02 30 08 - time signature
264
+ if (name) {
265
+ this.track += "%00%FF%03" + toHex(name.length, 2);
266
+ for (var i = 0; i < name.length; i++)
267
+ this.track += toHex(name.charCodeAt(i), 2);
268
+ }
269
+ this.endTrack();
270
+ }
271
+ };
272
+
273
+ Midi.prototype.startTrack = function() {
274
+ //console.log("startTrack");
275
+ this.track = "";
276
+ this.silencelength = 0;
277
+ this.trackcount++;
278
+ this.first = true;
279
+ if (this.instrument) {
280
+ this.setInstrument(this.instrument);
281
+ }
282
+ };
283
+
284
+ Midi.prototype.endTrack = function() {
285
+ //console.log("endTrack");
286
+ var tracklength = toHex(this.track.length / 3 + 4, 8);
287
+ this.track = "MTrk" + tracklength + // track header
288
+ this.track +
289
+ '%00%FF%2F%00'; // track end
290
+ this.trackstrings += this.track;
291
+ };
292
+
293
+ Midi.prototype.setInstrument = function(number) {
294
+ //console.log("setInstrument", number);
295
+ if (this.track)
296
+ this.track = "%00%C0" + toHex(number, 2) + this.track;
297
+ else
298
+ this.track = "%00%C0" + toHex(number, 2);
299
+ this.instrument = number;
300
+ };
301
+
302
+ Midi.prototype.setChannel = function(number) {
303
+ //console.log("setChannel", number);
304
+ this.channel = number - 1;
305
+ this.noteOnAndChannel = "%9" + this.channel.toString(16);
306
+ };
307
+
308
+ Midi.prototype.startNote = function(pitch, loudness) {
309
+ //console.log("startNote", pitch, loudness);
310
+ this.track += toDurationHex(this.silencelength); // only need to shift by amount of silence (if there is any)
311
+ this.silencelength = 0;
312
+ if (this.first) {
313
+ this.first = false;
314
+ this.track += this.noteOnAndChannel;
315
+ }
316
+ this.track += "%" + pitch.toString(16) + toHex(loudness, 2); //note
317
+ };
318
+
319
+ Midi.prototype.endNote = function(pitch, length) {
320
+ //console.log("endNote", pitch, length);
321
+ this.track += toDurationHex(this.silencelength+length); // only need to shift by amount of silence (if there is any)
322
+ this.silencelength = 0;
323
+ // this.track += toDurationHex(length); //duration
324
+ this.track += "%" + pitch.toString(16) + "%00";//end note
325
+ };
326
+
327
+ Midi.prototype.addRest = function(length) {
328
+ //console.log("addRest", length);
329
+ this.silencelength += length;
330
+ };
331
+
332
+ Midi.prototype.getData = function() {
333
+ return "data:audio/midi," +
334
+ "MThd%00%00%00%06%00%01" + toHex(this.trackcount, 4) + "%01%e0" + // header
335
+ this.trackstrings;
336
+ };
337
+
338
+ Midi.prototype.embed = function(parent, noplayer) {
339
+
340
+ var data = this.getData();
341
+
342
+ var link = setAttributes(document.createElement('a'), {
343
+ href: data
344
+ });
345
+ link.innerHTML = "download midi";
346
+ parent.insertBefore(link, parent.firstChild);
347
+
348
+ if (noplayer) return;
349
+
350
+ var embed = setAttributes(document.createElement('embed'), {
351
+ src: data,
352
+ type: 'video/quicktime',
353
+ controller: 'true',
354
+ autoplay: 'false',
355
+ loop: 'false',
356
+ enablejavascript: 'true',
357
+ style: 'display:block; height: 20px;'
358
+ });
359
+ parent.insertBefore(embed, parent.firstChild);
360
+ };
361
+
362
+ // s is assumed to be of even length
363
+ function encodeHex(s) {
364
+ var ret = "";
365
+ for (var i = 0; i < s.length; i += 2) {
366
+ ret += "%";
367
+ ret += s.substr(i, 2);
368
+ }
369
+ return ret;
370
+ }
371
+
372
+ function toHex(n, padding) {
373
+ var s = n.toString(16);
374
+ while (s.length < padding) {
375
+ s = "0" + s;
376
+ }
377
+ return encodeHex(s);
378
+ }
379
+
380
+ function toDurationHex(n) {
381
+ var res = 0;
382
+ var a = [];
383
+
384
+ // cut up into 7 bit chunks;
385
+ while (n !== 0) {
386
+ a.push(n & 0x7F);
387
+ n = n >> 7;
388
+ }
389
+
390
+ // join the 7 bit chunks together, all but last chunk get leading 1
391
+ for (var i = a.length - 1; i >= 0; i--) {
392
+ res = res << 8;
393
+ var bits = a[i];
394
+ if (i !== 0) {
395
+ bits = bits | 0x80;
396
+ }
397
+ res = res | bits;
398
+ }
399
+
400
+ var padding = res.toString(16).length;
401
+ padding += padding % 2;
402
+
403
+ return toHex(res, padding);
404
+ }
405
+
406
+ window.ABCJS.midi.rendererFactory = function(isJava) {
407
+ return (isJava) ? new MidiProxy(new JavaMidi(this), new Midi()) : new Midi();
408
+ };
409
+
410
+ window.ABCJS.midi.initializeJava = function() {
411
+ MIDIPlugin = document.MIDIPlugin;
412
+ setTimeout(function() { // run on next event loop (once MIDIPlugin is loaded)
413
+ try { // activate MIDIPlugin
414
+ MIDIPlugin.openPlugin();
415
+
416
+ } catch(e) { // plugin not supported (download externals)
417
+ var a = document.createElement("a");
418
+ a.href = "http://java.sun.com/products/java-media/sound/soundbanks.html";
419
+ a.target = "_blank";
420
+ a.appendChild(document.createTextNode("Download Soundbank"));
421
+ parent.appendChild(a);
422
+ }
423
+ }, 0);
424
+ };
425
+
426
+ })();
@@ -0,0 +1,207 @@
1
+ // abc_midi_sequencer.js: Turn parsed abc into a linear series of events.
2
+ // Copyright (C) 2010,2015 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
+ if (!window.ABCJS)
18
+ window.ABCJS = {};
19
+
20
+ if (!window.ABCJS.midi)
21
+ window.ABCJS.midi = {};
22
+
23
+ (function() {
24
+ "use strict";
25
+
26
+ // The abc is provided to us line by line. It might have repeats in it. We want to re arrange the elements to
27
+ // be an array of voices with all the repeats embedded, and no lines. Then it is trivial to go through the events
28
+ // one at a time and turn it into midi.
29
+
30
+ window.ABCJS.midi.sequence = function(abctune, options) {
31
+ // Global options
32
+ options = options || {};
33
+ var qpm = options.qpm || 180; // The tempo if there isn't a tempo specified.
34
+ var program = options.program || 2; // The program if there isn't a program specified.
35
+ var transpose = options.transpose || 0;
36
+ var channel = options.channel || 0;
37
+
38
+ var bagpipes = abctune.formatting.bagpipes; // If it is bagpipes, then the gracenotes are played on top of the main note.
39
+ if (bagpipes)
40
+ program = 71;
41
+
42
+ // %%MIDI formatting options
43
+ if (abctune.formatting.midi) {
44
+ if (abctune.formatting.midi.transpose)
45
+ transpose = abctune.formatting.midi.transpose;
46
+
47
+ // PER: changed format of the global midi commands from the parser. Using the new definition here.
48
+ if (abctune.formatting.midi.program && abctune.formatting.midi.program.program)
49
+ program = abctune.formatting.midi.program.program;
50
+ if (abctune.formatting.midi.channel)
51
+ channel = abctune.formatting.midi.channel;
52
+ }
53
+
54
+ // Specified options in abc string.
55
+
56
+ if (abctune.metaText.tempo)
57
+ qpm = interpretTempo(abctune.metaText.tempo);
58
+
59
+ var startVoice = [];
60
+ if (bagpipes)
61
+ startVoice.push({ el_type: 'bagpipes' });
62
+ startVoice.push({ el_type: 'instrument', program: program });
63
+ if (channel)
64
+ startVoice.push({ el_type: 'channel', channel: channel });
65
+ if (transpose)
66
+ startVoice.push({ el_type: 'transpose', transpose: transpose });
67
+ startVoice.push({ el_type: 'tempo', qpm: qpm });
68
+
69
+ // the relevant part of the input structure is:
70
+ // abctune
71
+ // array lines
72
+ // array staff
73
+ // object key
74
+ // object meter
75
+ // array voices
76
+ // array abcelem
77
+
78
+ // visit each voice completely in turn
79
+ var voices = [];
80
+ var startRepeatPlaceholder = []; // There is a place holder for each voice.
81
+ var skipEndingPlaceholder = []; // This is the place where the first ending starts.
82
+ for (var i = 0; i < abctune.lines.length; i++) {
83
+ // For each group of staff lines in the tune.
84
+ var line = abctune.lines[i];
85
+ if (line.staff) {
86
+ var staves = line.staff;
87
+ var voiceNumber = 0;
88
+ for (var j = 0; j < staves.length; j++) {
89
+ var staff = staves[j];
90
+ // For each staff line
91
+ for (var k = 0; k < staff.voices.length; k++) {
92
+ // For each voice in a staff line
93
+ var voice = staff.voices[k];
94
+ if (!voices[voiceNumber])
95
+ voices[voiceNumber] = [].concat(startVoice);
96
+ if (staff.key) {
97
+ if (staff.key.root === 'HP')
98
+ voices[voiceNumber].push({el_type: 'key', accidentals: [{acc: 'natural', note: 'g'}, {acc: 'sharp', note: 'f'}, {acc: 'sharp', note: 'c'}]});
99
+ else
100
+ voices[voiceNumber].push({el_type: 'key', accidentals: staff.key.accidentals });
101
+ }
102
+ if (staff.meter) {
103
+ voices[voiceNumber].push(interpretMeter(staff.meter));
104
+ }
105
+ if (staff.clef && staff.clef.transpose) {
106
+ staff.clef.el_type = 'clef';
107
+ voices[voiceNumber].push({ el_type: 'transpose', transpose: staff.clef.transpose });
108
+ }
109
+ var noteEventsInBar = 0;
110
+ for (var v = 0; v < voice.length; v++) {
111
+ // For each element in a voice
112
+ var elem = voice[v];
113
+ switch (elem.el_type) {
114
+ case "note":
115
+ // regular items are just pushed.
116
+ if (!elem.rest || elem.rest.type !== 'spacer') {
117
+ voices[voiceNumber].push(elem);
118
+ noteEventsInBar++;
119
+ }
120
+ break;
121
+ case "key":
122
+ if (elem.root === 'HP')
123
+ voices[voiceNumber].push({el_type: 'key', accidentals: [{acc: 'natural', note: 'g'}, {acc: 'sharp', note: 'f'}, {acc: 'sharp', note: 'c'}]});
124
+ else
125
+ voices[voiceNumber].push({el_type: 'key', accidentals: elem.accidentals });
126
+ break;
127
+ case "meter":
128
+ voices[voiceNumber].push(interpretMeter(elem));
129
+ break;
130
+ case "clef": // need to keep this to catch the "transpose" element.
131
+ if (elem.transpose)
132
+ voices[voiceNumber].push({ el_type: 'transpose', transpose: elem.transpose });
133
+ break;
134
+ case "tempo":
135
+ qpm = interpretTempo(elem);
136
+ voices[voiceNumber].push({ el_type: 'tempo', qpm: qpm });
137
+ break;
138
+ case "bar":
139
+ if (noteEventsInBar > 0) // don't add two bars in a row.
140
+ voices[voiceNumber].push({ el_type: 'bar' }); // We need the bar marking to reset the accidentals.
141
+ noteEventsInBar = 0;
142
+ // figure out repeats and endings --
143
+ // The important part is where there is a start repeat, and end repeat, or a first ending.
144
+ var endRepeat = (elem.type === "bar_right_repeat" || elem.type === "bar_dbl_repeat");
145
+ var startEnding = (elem.startEnding) ? true : false;
146
+ var startRepeat = (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");
147
+ if (endRepeat) {
148
+ var s = startRepeatPlaceholder[voiceNumber];
149
+ if (!s) s = 0; // If there wasn't a left repeat, then we repeat from the beginning.
150
+ var e = skipEndingPlaceholder[voiceNumber];
151
+ if (!e) e = voices[voiceNumber].length; // If there wasn't a first ending marker, then we copy everything.
152
+ voices[voiceNumber] = voices[voiceNumber].concat(voices[voiceNumber].slice(s, e));
153
+ }
154
+ if (startEnding)
155
+ skipEndingPlaceholder[voiceNumber] = voices[voiceNumber].length;
156
+ if (startRepeat)
157
+ startRepeatPlaceholder[voiceNumber] = voices[voiceNumber].length;
158
+ break;
159
+ case 'style':
160
+ // TODO-PER: If this is set to rhythm heads, then it should use the percussion channel.
161
+ break;
162
+ case 'part':
163
+ // TODO-PER: If there is a part section in the header, then this should probably affect the repeats.
164
+ break;
165
+ case 'stem':
166
+ case 'scale':
167
+ // These elements don't affect sound
168
+ break;
169
+ default:
170
+ console.log("MIDI: element type " + elem.el_type + " not handled.");
171
+ }
172
+ }
173
+ voiceNumber++;
174
+ }
175
+ }
176
+ }
177
+ }
178
+ return voices;
179
+ };
180
+
181
+ function interpretTempo(element) {
182
+ var duration = 1/4;
183
+ if (element.duration) {
184
+ duration = element.duration[0];
185
+ }
186
+ var bpm = 60;
187
+ if (element.bpm) {
188
+ bpm = element.bpm;
189
+ }
190
+ return bpm*duration*4;
191
+ }
192
+
193
+ function interpretMeter(element) {
194
+ switch (element.type) {
195
+ case "common_time":
196
+ return { el_type: 'meter', num: 4, den: 4 };
197
+ case "cut_time":
198
+ return { el_type: 'meter', num: 2, den: 2 };
199
+ case "specified":
200
+ // TODO-PER: only taking the first meter, so the complex meters are not handled.
201
+ return { el_type: 'meter', num: element.value[0].num, den: element.value[0].den };
202
+ default:
203
+ // This should never happen.
204
+ return { el_type: 'meter' };
205
+ }
206
+ }
207
+ })();