abcjs-rails 2.1 → 2.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ })();