UNPKG

abcjs

Version:

Renderer for abc music notation

1,318 lines (1,248 loc) 50.1 kB
var parseCommon = require('./abc_common'); var parseKeyVoice = require('./abc_parse_key_voice'); var transpose = require('./abc_transpose'); var tokenizer; var warn; var multilineVars; var tune; var tuneBuilder; var header; var MusicParser = function(_tokenizer, _warn, _multilineVars, _tune, _tuneBuilder, _header) { tokenizer = _tokenizer; warn = _warn; multilineVars = _multilineVars; tune = _tune; tuneBuilder = _tuneBuilder; header = _header; this.lineContinuation = false; } // // Parse line of music // // This is a stream of <(bar-marking|header|note-group)...> in any order, with optional spaces between each element // core-note is <open-slur, accidental, pitch:required, octave, duration, close-slur&|tie> with no spaces within that // chord is <open-bracket:required, core-note:required... close-bracket:required duration> with no spaces within that // grace-notes is <open-brace:required, (open-slur|core-note:required|close-slur)..., close-brace:required> spaces are allowed // note-group is <grace-notes, chord symbols&|decorations..., grace-notes, slur&|triplet, chord|core-note, end-slur|tie> spaces are allowed between items // bar-marking is <ampersand> or <chord symbols&|decorations..., bar:required> spaces allowed // header is <open-bracket:required, K|M|L|V:required, colon:required, field:required, close-bracket:required> spaces can occur between the colon, in the field, and before the close bracket // header can also be the only thing on a line. This is true even if it is a continuation line. In this case the brackets are not required. // a space is a back-tick, a space, or a tab. If it is a back-tick, then there is no end-beam. // Line preprocessing: anything after a % is ignored (the double %% should have been taken care of before this) // Then, all leading and trailing spaces are ignored. // If there was a line continuation, the \n was replaced by a \r and the \ was replaced by a space. This allows the construct // of having a header mid-line conceptually, but actually be at the start of the line. This is equivolent to putting the header in [ ]. // TODO-PER: How to handle ! for line break? // TODO-PER: dots before bar, dots before slur // TODO-PER: U: redefinable symbols. // Ambiguous symbols: // "[" can be the start of a chord, the start of a header element or part of a bar line. // --- if it is immediately followed by "|", it is a bar line // --- if it is immediately followed by K: L: M: V: it is a header (note: there are other headers mentioned in the standard, but I'm not sure how they would be used.) // --- otherwise it is the beginning of a chord // "(" can be the start of a slur or a triplet // --- if it is followed by a number from 2-9, then it is a triplet // --- otherwise it is a slur // "]" // --- if there is a chord open, then this is the close // --- if it is after a [|, then it is an invisible bar line // --- otherwise, it is par of a bar // "." can be a bar modifier or a slur modifier, or a decoration // --- if it comes immediately before a bar, it is a bar modifier // --- if it comes immediately before a slur, it is a slur modifier // --- otherwise it is a decoration for the next note. // number: // --- if it is after a bar, with no space, it is an ending marker // --- if it is after a ( with no space, it is a triplet count // --- if it is after a pitch or octave or slash, then it is a duration // Unambiguous symbols (except inside quoted strings): // vertical-bar, colon: part of a bar // ABCDEFGabcdefg: pitch // xyzZ: rest // comma, prime: octave // close-paren: end-slur // hyphen: tie // tilde, v, u, bang, plus, THLMPSO: decoration // carat, underscore, equal: accidental // ampersand: time reset // open-curly, close-curly: grace notes // double-quote: chord symbol // less-than, greater-than, slash: duration // back-tick, space, tab: space var nonDecorations = "ABCDEFGabcdefgxyzZ[]|^_{"; // use this to prescreen so we don't have to look for a decoration at every note. var isInTie = function(multilineVars, overlayLevel, el) { if (multilineVars.inTie[overlayLevel] === undefined) return false; // If this is single voice music then the voice index isn't set, so we use the first voice. var voiceIndex = multilineVars.currentVoice ? multilineVars.currentVoice.staffNum * 100 + multilineVars.currentVoice.index : 0; if (multilineVars.inTie[overlayLevel][voiceIndex]) { if (el.pitches !== undefined || el.rest.type !== 'spacer') return true; } return false; }; var el = { }; MusicParser.prototype.parseMusic = function(line) { header.resolveTempo(); //multilineVars.havent_set_length = false; // To late to set this now. multilineVars.is_in_header = false; // We should have gotten a key header by now, but just in case, this is definitely out of the header. var i = 0; var startOfLine = multilineVars.iChar; // see if there is nothing but a comment on this line. If so, just ignore it. A full line comment is optional white space followed by % while (tokenizer.isWhiteSpace(line[i]) && i < line.length) i++; if (i === line.length || line[i] === '%') return; // Start with the standard staff, clef and key symbols on each line var delayStartNewLine = multilineVars.start_new_line; if (multilineVars.continueall === undefined) multilineVars.start_new_line = true; else multilineVars.start_new_line = false; var tripletNotesLeft = 0; // See if the line starts with a header field var retHeader = header.letter_to_body_header(line, i); if (retHeader[0] > 0) { i += retHeader[0]; // fixes bug on this: c[V:2]d if (retHeader[1] === 'V') this.startNewLine(); // delayStartNewLine = true; // TODO-PER: Handle inline headers } var overlayLevel = 0; while (i < line.length) { var startI = i; if (line[i] === '%') break; var retInlineHeader = header.letter_to_inline_header(line, i, delayStartNewLine); if (retInlineHeader[0] > 0) { i += retInlineHeader[0]; if (retInlineHeader[1] === 'V') delayStartNewLine = true; // fixes bug on this: c[V:2]d // TODO-PER: Handle inline headers //multilineVars.start_new_line = false; } else { // Wait until here to actually start the line because we know we're past the inline statements. if (!tuneBuilder.hasBeginMusic() || (delayStartNewLine && !this.lineContinuation)) { this.startNewLine(); delayStartNewLine = false; } // We need to decide if the following characters are a bar-marking or a note-group. // Unfortunately, that is ambiguous. Both can contain chord symbols and decorations. // If there is a grace note either before or after the chord symbols and decorations, then it is definitely a note-group. // If there is a bar marker, it is definitely a bar-marking. // If there is either a core-note or chord, it is definitely a note-group. // So, loop while we find grace-notes, chords-symbols, or decorations. [It is an error to have more than one grace-note group in a row; the others can be multiple] // Then, if there is a grace-note, we know where to go. // Else see if we have a chord, core-note, slur, triplet, or bar. var ret; while (1) { ret = tokenizer.eatWhiteSpace(line, i); if (ret > 0) { i += ret; } if (i > 0 && line[i-1] === '\x12') { // there is one case where a line continuation isn't the same as being on the same line, and that is if the next character after it is a header. ret = header.letter_to_body_header(line, i); if (ret[0] > 0) { if (ret[1] === 'V') this.startNewLine(); // fixes bug on this: c\\nV:2]\\nd // TODO: insert header here i = ret[0]; multilineVars.start_new_line = false; } } // gather all the grace notes, chord symbols and decorations ret = letter_to_spacer(line, i); if (ret[0] > 0) { i += ret[0]; } ret = letter_to_chord(line, i); if (ret[0] > 0) { // There could be more than one chord here if they have different positions. // If two chords have the same position, then connect them with newline. if (!el.chord) el.chord = []; var chordName = tokenizer.translateString(ret[1]); chordName = chordName.replace(/;/g, "\n"); var addedChord = false; for (var ci = 0; ci < el.chord.length; ci++) { if (el.chord[ci].position === ret[2]) { addedChord = true; el.chord[ci].name += "\n" + chordName; } } if (addedChord === false) { if (ret[2] === null && ret[3]) el.chord.push({name: chordName, rel_position: ret[3]}); else el.chord.push({name: chordName, position: ret[2]}); } i += ret[0]; var ii = tokenizer.skipWhiteSpace(line.substring(i)); if (ii > 0) el.force_end_beam_last = true; i += ii; } else { if (nonDecorations.indexOf(line[i]) === -1) ret = letter_to_accent(line, i); else ret = [ 0 ]; if (ret[0] > 0) { if (ret[1] === null) { if (i + 1 < line.length) this.startNewLine(); // There was a ! in the middle of the line. Start a new line if there is anything after it. } else if (ret[1].length > 0) { if (ret[1].indexOf("style=") === 0) { el.style = ret[1].substr(6); } else { if (el.decoration === undefined) el.decoration = []; if (ret[1] === 'beambr1') el.beambr = 1; else if (ret[1] === "beambr2") el.beambr = 2; else el.decoration.push(ret[1]); } } i += ret[0]; } else { ret = letter_to_grace(line, i); // TODO-PER: Be sure there aren't already grace notes defined. That is an error. if (ret[0] > 0) { el.gracenotes = ret[1]; i += ret[0]; } else break; } } } ret = letter_to_bar(line, i); if (ret[0] > 0) { // This is definitely a bar overlayLevel = 0; if (el.gracenotes !== undefined) { // Attach the grace note to an invisible note el.rest = { type: 'spacer' }; el.duration = 0.125; // TODO-PER: I don't think the duration of this matters much, but figure out if it does. multilineVars.addFormattingOptions(el, tune.formatting, 'note'); tuneBuilder.appendElement('note', startOfLine+i, startOfLine+i+ret[0], el); multilineVars.measureNotEmpty = true; el = {}; } var bar = {type: ret[1]}; if (bar.type.length === 0) warn("Unknown bar type", line, i); else { if (multilineVars.inEnding && bar.type !== 'bar_thin') { bar.endEnding = true; multilineVars.inEnding = false; } if (ret[2]) { bar.startEnding = ret[2]; if (multilineVars.inEnding) bar.endEnding = true; multilineVars.inEnding = true; if (ret[1] === "bar_right_repeat") { // restore the tie and slur state from the start repeat multilineVars.restoreStartEndingHoldOvers(); } else { // save inTie, inTieChord multilineVars.duplicateStartEndingHoldOvers(); } } if (el.decoration !== undefined) bar.decoration = el.decoration; if (el.chord !== undefined) bar.chord = el.chord; if (bar.startEnding && multilineVars.barFirstEndingNum === undefined) multilineVars.barFirstEndingNum = multilineVars.currBarNumber; else if (bar.startEnding && bar.endEnding && multilineVars.barFirstEndingNum) multilineVars.currBarNumber = multilineVars.barFirstEndingNum; else if (bar.endEnding) multilineVars.barFirstEndingNum = undefined; if (bar.type !== 'bar_invisible' && multilineVars.measureNotEmpty) { var isFirstVoice = multilineVars.currentVoice === undefined || (multilineVars.currentVoice.staffNum === 0 && multilineVars.currentVoice.index === 0); if (isFirstVoice) { multilineVars.currBarNumber++; if (multilineVars.barNumbers && multilineVars.currBarNumber % multilineVars.barNumbers === 0) bar.barNumber = multilineVars.currBarNumber; } } multilineVars.addFormattingOptions(el, tune.formatting, 'bar'); tuneBuilder.appendElement('bar', startOfLine+startI, startOfLine+i+ret[0], bar); multilineVars.measureNotEmpty = false; el = {}; } i += ret[0]; } else if (line[i] === '&') { // backtrack to beginning of measure ret = letter_to_overlay(line, i); if (ret[0] > 0) { tuneBuilder.appendElement('overlay', startOfLine, startOfLine+1, {}); i += 1; overlayLevel++; } } else { // This is definitely a note group // // Look for as many open slurs and triplets as there are. (Note: only the first triplet is valid.) ret = letter_to_open_slurs_and_triplets(line, i); if (ret.consumed > 0) { if (ret.startSlur !== undefined) el.startSlur = ret.startSlur; if (ret.dottedSlur) el.dottedSlur = true; if (ret.triplet !== undefined) { if (tripletNotesLeft > 0) warn("Can't nest triplets", line, i); else { el.startTriplet = ret.triplet; el.tripletMultiplier = ret.tripletQ / ret.triplet; el.tripletR = ret.num_notes; tripletNotesLeft = ret.num_notes === undefined ? ret.triplet : ret.num_notes; } } i += ret.consumed; } // handle chords. if (line[i] === '[') { var chordStartChar = i; i++; var chordDuration = null; var rememberEndBeam = false; var done = false; while (!done) { var accent = letter_to_accent(line, i); if (accent[0] > 0) { i += accent[0]; } var chordNote = getCoreNote(line, i, {}, false); if (chordNote !== null && chordNote.pitch !== undefined) { if (accent[0] > 0) { // If we found a decoration above, it modifies the entire chord. "style" is handled below. if (accent[1].indexOf("style=") !== 0) { if (el.decoration === undefined) el.decoration = []; el.decoration.push(accent[1]); } } if (chordNote.end_beam) { el.end_beam = true; delete chordNote.end_beam; } if (el.pitches === undefined) { el.duration = chordNote.duration; el.pitches = [ chordNote ]; } else // Just ignore the note lengths of all but the first note. The standard isn't clear here, but this seems less confusing. el.pitches.push(chordNote); delete chordNote.duration; if (accent[0] > 0) { // If we found a style above, it modifies the individual pitch, not the entire chord. if (accent[1].indexOf("style=") === 0) { el.pitches[el.pitches.length-1].style = accent[1].substr(6); } } if (multilineVars.inTieChord[el.pitches.length]) { chordNote.endTie = true; multilineVars.inTieChord[el.pitches.length] = undefined; } if (chordNote.startTie) multilineVars.inTieChord[el.pitches.length] = true; i = chordNote.endChar; delete chordNote.endChar; } else if (line[i] === ' ') { // Spaces are not allowed in chords, but we can recover from it by ignoring it. warn("Spaces are not allowed in chords", line, i); i++; } else { if (i < line.length && line[i] === ']') { // consume the close bracket i++; if (multilineVars.next_note_duration !== 0) { el.duration = el.duration * multilineVars.next_note_duration; multilineVars.next_note_duration = 0; } if (isInTie(multilineVars, overlayLevel, el)) { el.pitches.forEach(function(pitch) { pitch.endTie = true; }); setIsInTie(multilineVars, overlayLevel, false); } if (tripletNotesLeft > 0 && !(el.rest && el.rest.type === "spacer")) { tripletNotesLeft--; if (tripletNotesLeft === 0) { el.endTriplet = true; } } var postChordDone = false; while (i < line.length && !postChordDone) { switch (line[i]) { case ' ': case '\t': addEndBeam(el); break; case ')': if (el.endSlur === undefined) el.endSlur = 1; else el.endSlur++; break; case '-': el.pitches.forEach(function(pitch) { pitch.startTie = {}; }); setIsInTie(multilineVars, overlayLevel, true); break; case '>': case '<': var br2 = getBrokenRhythm(line, i); i += br2[0] - 1; // index gets incremented below, so we'll let that happen multilineVars.next_note_duration = br2[2]; if (chordDuration) chordDuration = chordDuration * br2[1]; else chordDuration = br2[1]; break; case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '/': var fraction = tokenizer.getFraction(line, i); chordDuration = fraction.value; i = fraction.index; var ch = line[i] if (ch === ' ') rememberEndBeam = true; if (ch === '-' || ch === ')' || ch === ' ' || ch === '<' || ch === '>') i--; // Subtracting one because one is automatically added below else postChordDone = true; break; default: postChordDone = true; break; } if (!postChordDone) { i++; } } } else warn("Expected ']' to end the chords", line, i); if (el.pitches !== undefined) { if (chordDuration !== null) { el.duration = el.duration * chordDuration; if (rememberEndBeam) addEndBeam(el); } multilineVars.addFormattingOptions(el, tune.formatting, 'note'); tuneBuilder.appendElement('note', startOfLine+startI, startOfLine+i, el); multilineVars.measureNotEmpty = true; el = {}; } done = true; } } } else { // Single pitch var el2 = {}; var core = getCoreNote(line, i, el2, true); if (el2.endTie !== undefined) setIsInTie(multilineVars, overlayLevel, true); if (core !== null) { if (core.pitch !== undefined) { el.pitches = [ { } ]; // TODO-PER: straighten this out so there is not so much copying: getCoreNote shouldn't change e' if (core.accidental !== undefined) el.pitches[0].accidental = core.accidental; el.pitches[0].pitch = core.pitch; el.pitches[0].name = core.name; if (core.midipitch || core.midipitch === 0) el.pitches[0].midipitch = core.midipitch; if (core.endSlur !== undefined) el.pitches[0].endSlur = core.endSlur; if (core.endTie !== undefined) el.pitches[0].endTie = core.endTie; if (core.startSlur !== undefined) el.pitches[0].startSlur = core.startSlur; if (el.startSlur !== undefined) el.pitches[0].startSlur = el.startSlur; if (el.dottedSlur !== undefined) el.pitches[0].dottedSlur = true; if (core.startTie !== undefined) el.pitches[0].startTie = core.startTie; if (el.startTie !== undefined) el.pitches[0].startTie = el.startTie; } else { el.rest = core.rest; if (core.endSlur !== undefined) el.endSlur = core.endSlur; if (core.endTie !== undefined) el.rest.endTie = core.endTie; if (core.startSlur !== undefined) el.startSlur = core.startSlur; if (core.startTie !== undefined) el.rest.startTie = core.startTie; if (el.startTie !== undefined) el.rest.startTie = el.startTie; } if (core.chord !== undefined) el.chord = core.chord; if (core.duration !== undefined) el.duration = core.duration; if (core.decoration !== undefined) el.decoration = core.decoration; if (core.graceNotes !== undefined) el.graceNotes = core.graceNotes; delete el.startSlur; delete el.dottedSlur; if (isInTie(multilineVars, overlayLevel, el)) { if (el.pitches !== undefined) { el.pitches[0].endTie = true; } else if (el.rest.type !== 'spacer') { el.rest.endTie = true; } setIsInTie(multilineVars, overlayLevel, false); } if (core.startTie || el.startTie) setIsInTie(multilineVars, overlayLevel, true); i = core.endChar; if (tripletNotesLeft > 0 && !(core.rest && core.rest.type === "spacer")) { tripletNotesLeft--; if (tripletNotesLeft === 0) { el.endTriplet = true; } } if (core.end_beam) addEndBeam(el); // If there is a whole rest, then it should be the duration of the measure, not it's own duration. We need to special case it. // If the time signature length is greater than 4/4, though, then a whole rest has no special treatment. if (el.rest && el.rest.type === 'rest' && el.duration === 1 && durationOfMeasure(multilineVars) <= 1) { el.rest.type = 'whole'; el.duration = durationOfMeasure(multilineVars); } // Create a warning if this is not a displayable duration. // The first item on a line is a regular note value, each item after that represents a dot placed after the previous note. // Only durations less than a whole note are tested because whole note durations have some tricky rules. var durations = [ 0.5, 0.75, 0.875, 0.9375, 0.96875, 0.984375, 0.25, 0.375, 0.4375, 0.46875, 0.484375, 0.4921875, 0.125, 0.1875, 0.21875, 0.234375, 0.2421875, 0.24609375, 0.0625, 0.09375, 0.109375, 0.1171875, 0.12109375, 0.123046875, 0.03125, 0.046875, 0.0546875, 0.05859375, 0.060546875, 0.0615234375, 0.015625, 0.0234375, 0.02734375, 0.029296875, 0.0302734375, 0.03076171875, ]; if (el.duration < 1 && durations.indexOf(el.duration) === -1 && el.duration !== 0) { if (!el.rest || el.rest.type !== 'spacer') warn("Duration not representable: " + line.substring(startI, i), line, i); } multilineVars.addFormattingOptions(el, tune.formatting, 'note'); tuneBuilder.appendElement('note', startOfLine+startI, startOfLine+i, el); multilineVars.measureNotEmpty = true; el = {}; } } if (i === startI) { // don't know what this is, so ignore it. if (line[i] !== ' ' && line[i] !== '`') warn("Unknown character ignored", line, i); i++; } } } } this.lineContinuation = line.indexOf('\x12') >= 0 || (retHeader[0] > 0) if (!this.lineContinuation) { el = { } } }; var setIsInTie =function(multilineVars, overlayLevel, value) { // If this is single voice music then the voice index isn't set, so we use the first voice. var voiceIndex = multilineVars.currentVoice ? multilineVars.currentVoice.staffNum * 100 + multilineVars.currentVoice.index : 0; if (multilineVars.inTie[overlayLevel] === undefined) multilineVars.inTie[overlayLevel] = []; multilineVars.inTie[overlayLevel][voiceIndex] = value; }; var letter_to_chord = function(line, i) { if (line[i] === '"') { var chord = tokenizer.getBrackettedSubstring(line, i, 5); if (!chord[2]) warn("Missing the closing quote while parsing the chord symbol", line , i); // If it starts with ^, then the chord appears above. // If it starts with _ then the chord appears below. // (note that the 2.0 draft standard defines them as not chords, but annotations and also defines @.) if (chord[0] > 0 && chord[1].length > 0 && chord[1][0] === '^') { chord[1] = chord[1].substring(1); chord[2] = 'above'; } else if (chord[0] > 0 && chord[1].length > 0 && chord[1][0] === '_') { chord[1] = chord[1].substring(1); chord[2] = 'below'; } else if (chord[0] > 0 && chord[1].length > 0 && chord[1][0] === '<') { chord[1] = chord[1].substring(1); chord[2] = 'left'; } else if (chord[0] > 0 && chord[1].length > 0 && chord[1][0] === '>') { chord[1] = chord[1].substring(1); chord[2] = 'right'; } else if (chord[0] > 0 && chord[1].length > 0 && chord[1][0] === '@') { // @-15,5.7 chord[1] = chord[1].substring(1); var x = tokenizer.getFloat(chord[1]); if (x.digits === 0) warn("Missing first position in absolutely positioned annotation.", line , i); chord[1] = chord[1].substring(x.digits); if (chord[1][0] !== ',') warn("Missing comma absolutely positioned annotation.", line , i); chord[1] = chord[1].substring(1); var y = tokenizer.getFloat(chord[1]); if (y.digits === 0) warn("Missing second position in absolutely positioned annotation.", line , i); chord[1] = chord[1].substring(y.digits); var ws = tokenizer.skipWhiteSpace(chord[1]); chord[1] = chord[1].substring(ws); chord[2] = null; chord[3] = { x: x.value, y: y.value }; } else { if (multilineVars.freegchord !== true) { chord[1] = chord[1].replace(/([ABCDEFG0-9])b/g, "$1♭"); chord[1] = chord[1].replace(/([ABCDEFG0-9])#/g, "$1♯"); chord[1] = chord[1].replace(/^([ABCDEFG])([♯♭]?)o([^A-Za-z])/g, "$1$2°$3"); chord[1] = chord[1].replace(/^([ABCDEFG])([♯♭]?)o$/g, "$1$2°"); chord[1] = chord[1].replace(/^([ABCDEFG])([♯♭]?)0([^A-Za-z])/g, "$1$2ø$3"); chord[1] = chord[1].replace(/^([ABCDEFG])([♯♭]?)\^([^A-Za-z])/g, "$1$2∆$3"); } chord[2] = 'default'; chord[1] = transpose.chordName(multilineVars, chord[1]); } return chord; } return [0, ""]; }; var letter_to_grace = function(line, i) { // Grace notes are an array of: startslur, note, endslur, space; where note is accidental, pitch, duration if (line[i] === '{') { // fetch the gracenotes string and consume that into the array var gra = tokenizer.getBrackettedSubstring(line, i, 1, '}'); if (!gra[2]) warn("Missing the closing '}' while parsing grace note", line, i); // If there is a slur after the grace construction, then move it to the last note inside the grace construction if (line[i+gra[0]] === ')') { gra[0]++; gra[1] += ')'; } var gracenotes = []; var ii = 0; var inTie = false; while (ii < gra[1].length) { var acciaccatura = false; if (gra[1][ii] === '/') { acciaccatura = true; ii++; } var note = getCoreNote(gra[1], ii, {}, false); if (note !== null) { // The grace note durations should not be affected by the default length: they should be based on 1/16, so if that isn't the default, then multiply here. note.duration = note.duration / (multilineVars.default_length * 8); if (acciaccatura) note.acciaccatura = true; gracenotes.push(note); if (inTie) { note.endTie = true; inTie = false; } if (note.startTie) inTie = true; ii = note.endChar; delete note.endChar; if (note.end_beam) { note.endBeam = true; delete note.end_beam; } } else { // We shouldn't get anything but notes or a space here, so report an error if (gra[1][ii] === ' ') { if (gracenotes.length > 0) gracenotes[gracenotes.length-1].endBeam = true; } else warn("Unknown character '" + gra[1][ii] + "' while parsing grace note", line, i); ii++; } } if (gracenotes.length) return [gra[0], gracenotes]; } return [ 0 ]; }; function letter_to_overlay(line, i) { if (line[i] === '&') { var start = i; while (line[i] && line[i] !== ':' && line[i] !== '|') i++; return [ i-start, line.substring(start+1, i) ]; } return [ 0 ]; } function durationOfMeasure(multilineVars) { // TODO-PER: This could be more complicated if one of the unusual measures is used. var meter = multilineVars.origMeter; if (!meter || meter.type !== 'specified') return 1; if (!meter.value || meter.value.length === 0) return 1; return parseInt(meter.value[0].num, 10) / parseInt(meter.value[0].den, 10); } var legalAccents = [ "trill", "lowermordent", "uppermordent", "mordent", "pralltriller", "accent", "fermata", "invertedfermata", "tenuto", "0", "1", "2", "3", "4", "5", "+", "wedge", "open", "thumb", "snap", "turn", "roll", "breath", "shortphrase", "mediumphrase", "longphrase", "segno", "coda", "D.S.", "D.C.", "fine", "beambr1", "beambr2", "slide", "marcato", "upbow", "downbow", "/", "//", "///", "////", "trem1", "trem2", "trem3", "trem4", "turnx", "invertedturn", "invertedturnx", "trill(", "trill)", "arpeggio", "xstem", "mark", "umarcato", "style=normal", "style=harmonic", "style=rhythm", "style=x", "style=triangle", "D.C.alcoda", "D.C.alfine", "D.S.alcoda", "D.S.alfine", "editorial", "courtesy" ]; var volumeDecorations = [ "p", "pp", "f", "ff", "mf", "mp", "ppp", "pppp", "fff", "ffff", "sfz" ]; var dynamicDecorations = [ "crescendo(", "crescendo)", "diminuendo(", "diminuendo)", "glissando(", "glissando)" ]; var accentPseudonyms = [ ["<", "accent"], [">", "accent"], ["tr", "trill"], ["plus", "+"], [ "emphasis", "accent"], [ "^", "umarcato" ], [ "marcato", "umarcato" ] ]; var accentDynamicPseudonyms = [ ["<(", "crescendo("], ["<)", "crescendo)"], [">(", "diminuendo("], [">)", "diminuendo)"] ]; var letter_to_accent = function(line, i) { var macro = multilineVars.macros[line[i]]; if (macro !== undefined) { if (macro[0] === '!' || macro[0] === '+') macro = macro.substring(1); if (macro[macro.length-1] === '!' || macro[macro.length-1] === '+') macro = macro.substring(0, macro.length-1); if (legalAccents.includes(macro)) return [ 1, macro ]; else if (volumeDecorations.includes(macro)) { if (multilineVars.volumePosition === 'hidden') macro = ""; return [1, macro]; } else if (dynamicDecorations.includes(macro)) { if (multilineVars.dynamicPosition === 'hidden') macro = ""; return [1, macro]; } else { if (!multilineVars.ignoredDecorations.includes(macro)) warn("Unknown macro: " + macro, line, i); return [1, '' ]; } } switch (line[i]) { case '.': if (line[i+1] === '(' || line[i+1] === '-') // a dot then open paren is a dotted slur; likewise dot dash is dotted tie. break; return [1, 'staccato']; case 'u':return [1, 'upbow']; case 'v':return [1, 'downbow']; case '~':return [1, 'irishroll']; case '!': case '+': var ret = tokenizer.getBrackettedSubstring(line, i, 5); // Be sure that the accent is recognizable. if (ret[1].length > 1 && (ret[1][0] === '^' || ret[1][0] ==='_')) ret[1] = ret[1].substring(1); // TODO-PER: The test files have indicators forcing the ornament to the top or bottom, but that isn't in the standard. We'll just ignore them. if (legalAccents.includes(ret[1])) return ret; if (volumeDecorations.includes(ret[1])) { if (multilineVars.volumePosition === 'hidden' ) ret[1] = ''; return ret; } if (dynamicDecorations.includes(ret[1])) { if (multilineVars.dynamicPosition === 'hidden' ) ret[1] = ''; return ret; } var ind = accentPseudonyms.findIndex(function (acc) { return ret[1] === acc[0]}) if (ind >= 0) { ret[1] = accentPseudonyms[ind][1]; return ret; } ind = accentDynamicPseudonyms.findIndex(function (acc) { return ret[1] === acc[0]}) if (ind >= 0) { ret[1] = accentDynamicPseudonyms[ind][1]; if (multilineVars.dynamicPosition === 'hidden' ) ret[1] = ''; return ret; } // We didn't find the accent in the list, so consume the space, but don't return an accent. // Although it is possible that ! was used as a line break, so accept that. if (line[i] === '!' && (ret[0] === 1 || line[i+ret[0]-1] !== '!')) return [1, null ]; warn("Unknown decoration: " + ret[1], line, i); ret[1] = ""; return ret; case 'H':return [1, 'fermata']; case 'J':return [1, 'slide']; case 'L':return [1, 'accent']; case 'M':return [1, 'mordent']; case 'O':return[1, 'coda']; case 'P':return[1, 'pralltriller']; case 'R':return [1, 'roll']; case 'S':return [1, 'segno']; case 'T':return [1, 'trill']; } return [0, 0]; }; var letter_to_spacer = function(line, i) { var start = i; while (tokenizer.isWhiteSpace(line[i])) i++; return [ i-start ]; }; // returns the class of the bar line // the number of the repeat // and the number of characters used up // if 0 is returned, then the next element was not a bar line var letter_to_bar = function(line, curr_pos) { var ret = tokenizer.getBarLine(line, curr_pos); if (ret.len === 0) return [0,""]; if (ret.warn) { warn(ret.warn, line, curr_pos); return [ret.len,""]; } // Now see if this is a repeated ending // A repeated ending is all of the characters 1,2,3,4,5,6,7,8,9,0,-, and comma // It can also optionally start with '[', which is ignored. // Also, it can have white space before the '['. for (var ws = 0; ws < line.length; ws++) if (line[curr_pos + ret.len + ws] !== ' ') break; var orig_bar_len = ret.len; if (line[curr_pos+ret.len+ws] === '[') { ret.len += ws + 1; } // It can also be a quoted string. It is unclear whether that construct requires '[', but it seems like it would. otherwise it would be confused with a regular chord. if (line[curr_pos+ret.len] === '"' && line[curr_pos+ret.len-1] === '[') { var ending = tokenizer.getBrackettedSubstring(line, curr_pos+ret.len, 5); return [ret.len+ending[0], ret.token, ending[1]]; } var retRep = tokenizer.getTokenOf(line.substring(curr_pos+ret.len), "1234567890-,"); if (retRep.len === 0 || retRep.token[0] === '-') return [orig_bar_len, ret.token]; return [ret.len+retRep.len, ret.token, retRep.token]; }; var tripletQ = { 2: 3, 3: 2, 4: 3, 5: 2, // TODO-PER: not handling 6/8 rhythm yet 6: 2, 7: 2, // TODO-PER: not handling 6/8 rhythm yet 8: 3, 9: 2 // TODO-PER: not handling 6/8 rhythm yet }; var letter_to_open_slurs_and_triplets = function(line, i) { // consume spaces, and look for all the open parens. If there is a number after the open paren, // that is a triplet. Otherwise that is a slur. Collect all the slurs and the first triplet. var ret = {}; var start = i; if (line[i] === '.' && line[i+1] === '(') { ret.dottedSlur = true; i++; } while (line[i] === '(' || tokenizer.isWhiteSpace(line[i])) { if (line[i] === '(') { if (i+1 < line.length && (line[i+1] >= '2' && line[i+1] <= '9')) { if (ret.triplet !== undefined) warn("Can't nest triplets", line, i); else { ret.triplet = line[i+1] - '0'; ret.tripletQ = tripletQ[ret.triplet]; ret.num_notes = ret.triplet; if (i+2 < line.length && line[i+2] === ':') { // We are expecting "(p:q:r" or "(p:q" or "(p::r" // That is: "put p notes into the time of q for the next r notes" // if r is missing, then it is equal to p. // if q is missing, it is determined from this table: // (2 notes in the time of 3 // (3 notes in the time of 2 // (4 notes in the time of 3 // (5 notes in the time of n | if time sig is (6/8, 9/8, 12/8), n=3, else n=2 // (6 notes in the time of 2 // (7 notes in the time of n // (8 notes in the time of 3 // (9 notes in the time of n if (i+3 < line.length && line[i+3] === ':') { // The second number, 'q', is not present. if (i+4 < line.length && (line[i+4] >= '1' && line[i+4] <= '9')) { ret.num_notes = line[i+4] - '0'; i += 3; } else warn("expected number after the two colons after the triplet to mark the duration", line, i); } else if (i+3 < line.length && (line[i+3] >= '1' && line[i+3] <= '9')) { ret.tripletQ = line[i+3] - '0'; if (i+4 < line.length && line[i+4] === ':') { if (i+5 < line.length && (line[i+5] >= '1' && line[i+5] <= '9')) { ret.num_notes = line[i+5] - '0'; i += 4; } } else { i += 2; } } else warn("expected number after the triplet to mark the duration", line, i); } } i++; } else { if (ret.startSlur === undefined) ret.startSlur = 1; else ret.startSlur++; } } i++; } ret.consumed = i-start; return ret; }; MusicParser.prototype.startNewLine = function() { var params = { startChar: -1, endChar: -1}; if (multilineVars.partForNextLine.title) params.part = multilineVars.partForNextLine; params.clef = multilineVars.currentVoice && multilineVars.staves[multilineVars.currentVoice.staffNum].clef !== undefined ? parseCommon.clone(multilineVars.staves[multilineVars.currentVoice.staffNum].clef) : parseCommon.clone(multilineVars.clef); var scoreTranspose = multilineVars.currentVoice ? multilineVars.currentVoice.scoreTranspose : 0; params.key = parseKeyVoice.standardKey(multilineVars.key.root+multilineVars.key.acc+multilineVars.key.mode, multilineVars.key.root, multilineVars.key.acc, scoreTranspose); params.key.mode = multilineVars.key.mode; if (multilineVars.key.impliedNaturals) params.key.impliedNaturals = multilineVars.key.impliedNaturals; if (multilineVars.key.explicitAccidentals) { for (var i = 0; i < multilineVars.key.explicitAccidentals.length; i++) { var found = false; for (var j = 0; j < params.key.accidentals.length; j++) { if (params.key.accidentals[j].note === multilineVars.key.explicitAccidentals[i].note) { // If the note is already in the list, override it with the new value params.key.accidentals[j].acc = multilineVars.key.explicitAccidentals[i].acc; found = true; } } if (!found) params.key.accidentals.push(multilineVars.key.explicitAccidentals[i]); } } multilineVars.targetKey = params.key; if (params.key.explicitAccidentals) delete params.key.explicitAccidentals; parseKeyVoice.addPosToKey(params.clef, params.key); if (multilineVars.meter !== null) { if (multilineVars.currentVoice) { multilineVars.staves.forEach(function(st) { st.meter = multilineVars.meter; }); params.meter = multilineVars.staves[multilineVars.currentVoice.staffNum].meter; multilineVars.staves[multilineVars.currentVoice.staffNum].meter = null; } else params.meter = multilineVars.meter; multilineVars.meter = null; } else if (multilineVars.currentVoice && multilineVars.staves[multilineVars.currentVoice.staffNum].meter) { // Make sure that each voice gets the meter marking. params.meter = multilineVars.staves[multilineVars.currentVoice.staffNum].meter; multilineVars.staves[multilineVars.currentVoice.staffNum].meter = null; } if (multilineVars.currentVoice && multilineVars.currentVoice.name) params.name = multilineVars.currentVoice.name; if (multilineVars.vocalfont) params.vocalfont = multilineVars.vocalfont; if (multilineVars.tripletfont) params.tripletfont = multilineVars.tripletfont; if (multilineVars.gchordfont) params.gchordfont = multilineVars.gchordfont; if (multilineVars.style) params.style = multilineVars.style; if (multilineVars.currentVoice) { var staff = multilineVars.staves[multilineVars.currentVoice.staffNum]; if (staff.brace) params.brace = staff.brace; if (staff.bracket) params.bracket = staff.bracket; if (staff.connectBarLines) params.connectBarLines = staff.connectBarLines; if (staff.name) params.name = staff.name[multilineVars.currentVoice.index]; if (staff.subname) params.subname = staff.subname[multilineVars.currentVoice.index]; if (multilineVars.currentVoice.stem) params.stem = multilineVars.currentVoice.stem; if (multilineVars.currentVoice.stafflines) params.stafflines = multilineVars.currentVoice.stafflines; if (multilineVars.currentVoice.staffscale) params.staffscale = multilineVars.currentVoice.staffscale; if (multilineVars.currentVoice.scale) params.scale = multilineVars.currentVoice.scale; if (multilineVars.currentVoice.color) params.color = multilineVars.currentVoice.color; if (multilineVars.currentVoice.style) params.style = multilineVars.currentVoice.style; if (multilineVars.currentVoice.transpose) params.clef.transpose = multilineVars.currentVoice.transpose; } var isFirstVoice = multilineVars.currentVoice === undefined || (multilineVars.currentVoice.staffNum === 0 && multilineVars.currentVoice.index === 0); if (multilineVars.barNumbers === 0 && isFirstVoice && multilineVars.currBarNumber !== 1) params.barNumber = multilineVars.currBarNumber; tuneBuilder.startNewLine(params); if (multilineVars.key.impliedNaturals) delete multilineVars.key.impliedNaturals; multilineVars.partForNextLine = {}; if (multilineVars.tempoForNextLine.length === 4) tuneBuilder.appendElement(multilineVars.tempoForNextLine[0],multilineVars.tempoForNextLine[1],multilineVars.tempoForNextLine[2],multilineVars.tempoForNextLine[3]); multilineVars.tempoForNextLine = []; } // TODO-PER: make this a method in el. var addEndBeam = function(el) { if (el.duration !== undefined && el.duration < 0.25) el.end_beam = true; return el; }; var pitches = {A: 5, B: 6, C: 0, D: 1, E: 2, F: 3, G: 4, a: 12, b: 13, c: 7, d: 8, e: 9, f: 10, g: 11}; var rests = {x: 'invisible', X: 'invisible-multimeasure', y: 'spacer', z: 'rest', Z: 'multimeasure' }; var accMap = { 'dblflat': '__', 'flat': '_', 'natural': '=', 'sharp': '^', 'dblsharp': '^^', 'quarterflat': '_/', 'quartersharp': '^/'}; var getCoreNote = function(line, index, el, canHaveBrokenRhythm) { //var el = { startChar: index }; var isComplete = function(state) { return (state === 'octave' || state === 'duration' || state === 'Zduration' || state === 'broken_rhythm' || state === 'end_slur'); }; var dottedTie; if (line[index] === '.' && line[index+1] === '-') { dottedTie = true; index++; } var state = 'startSlur'; var durationSetByPreviousNote = false; while (1) { switch(line[index]) { case '(': if (state === 'startSlur') { if (el.startSlur === undefined) el.startSlur = 1; else el.startSlur++; } else if (isComplete(state)) {el.endChar = index;return el;} else return null; break; case ')': if (isComplete(state)) { if (el.endSlur === undefined) el.endSlur = 1; else el.endSlur++; } else return null; break; case '^': if (state === 'startSlur') {el.accidental = 'sharp';state = 'sharp2';} else if (state === 'sharp2') {el.accidental = 'dblsharp';state = 'pitch';} else if (isComplete(state)) {el.endChar = index;return el;} else return null; break; case '_': if (state === 'startSlur') {el.accidental = 'flat';state = 'flat2';} else if (state === 'flat2') {el.accidental = 'dblflat';state = 'pitch';} else if (isComplete(state)) {el.endChar = index;return el;} else return null; break; case '=': if (state === 'startSlur') {el.accidental = 'natural';state = 'pitch';} else if (isComplete(state)) {el.endChar = index;return el;} else return null; break; case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'G': case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': if (state === 'startSlur' || state === 'sharp2' || state === 'flat2' || state === 'pitch') { el.pitch = pitches[line[index]]; el.pitch += 7 * (multilineVars.currentVoice && multilineVars.currentVoice.octave !== undefined ? multilineVars.currentVoice.octave : multilineVars.octave); el.name = line[index]; if (el.accidental) el.name = accMap[el.accidental] + el.name; transpose.note(multilineVars, el); state = 'octave'; // At this point we have a valid note. The rest is optional. Set the duration in case we don't get one below if (canHaveBrokenRhythm && multilineVars.next_note_duration !== 0) { el.duration = multilineVars.default_length * multilineVars.next_note_duration; multilineVars.next_note_duration = 0; durationSetByPreviousNote = true; } else el.duration = multilineVars.default_length; // If the clef is percussion, there is probably some translation of the pitch to a particular drum kit item. if ((multilineVars.clef && multilineVars.clef.type === "perc") || (multilineVars.currentVoice && multilineVars.currentVoice.clef === "perc")) { var key = line[index]; if (el.accidental) { key = accMap[el.accidental] + key; } if (tune.formatting && tune.formatting.midi && tune.formatting.midi.drummap) el.midipitch = tune.formatting.midi.drummap[key]; } } else if (isComplete(state)) {el.endChar = index;return el;} else return null; break; case ',': if (state === 'octave') {el.pitch -= 7; el.name += ','; } else if (isComplete(state)) {el.endChar = index;return el;} else return null; break; case '\'': if (state === 'octave') {el.pitch += 7; el.name += "'"; } else if (isComplete(state)) {el.endChar = index;return el;} else return null; break; case 'x': case 'X': case 'y': case 'z': case 'Z': if (state === 'startSlur') { el.rest = { type: rests[line[index]] }; // There shouldn't be some of the properties that notes have. If some sneak in due to bad syntax in the abc file, // just nix them here. delete el.accidental; delete el.startSlur; delete el.startTie; delete el.endSlur; delete el.endTie; delete el.end_beam; delete el.grace_notes; // At this point we have a valid note. The rest is optional. Set the duration in case we don't get one below if (el.rest.type.indexOf('multimeasure') >= 0) { el.duration = tune.getBarLength(); el.rest.text = 1; state = 'Zduration'; } else { if (canHaveBrokenRhythm && multilineVars.next_note_duration !== 0) { el.duration = multilineVars.default_length * multilineVars.next_note_duration; multilineVars.next_note_duration = 0; durationSetByPreviousNote = true; } else el.duration = multilineVars.default_length; state = 'duration'; } } else if (isComplete(state)) {el.endChar = index;return el;} else return null; break; case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '0': case '/': if (state === 'octave' || state === 'duration') { var fraction = tokenizer.getFraction(line, index); //if (!durationSetByPreviousNote) el.duration = el.duration * fraction.value; // TODO-PER: We can test the returned duration here and give a warning if it isn't the one expected. el.endChar = fraction.index; while (fraction.index < line.length && (tokenizer.isWhiteSpace(line[fraction.index]) || line[fraction.index] === '-')) { if (line[fraction.index] === '-') el.startTie = {}; else el = addEndBeam(el); fraction.index++; } index = fraction.index-1; state = 'broken_rhythm'; } else if (state === 'sharp2') { el.accidental = 'quartersharp';state = 'pitch'; } else if (state === 'flat2') { el.accidental = 'quarterflat';state = 'pitch'; } else if (state === 'Zduration') { var num = tokenizer.getNumber(line, index); el.duration = num.num * tune.getBarLength(); el.rest.text = num.num; el.endChar = num.index; return el; } else return null; break; case '-': if (state === 'startSlur') { // This is the first character, so it must have been meant for the previous note. Correct that here. tuneBuilder.addTieToLastNote(dottedTie); el.endTie = true; } else if (state === 'octave' || state === 'duration' || state === 'end_slur') { el.startTie = {}; if (!durationSetByPreviousNote && canHaveBrokenRhythm) state = 'broken_rhythm'; else { // Peek ahead to the next character. If it is a space, then we have an end beam. if (tokenizer.isWhiteSpace(line[index + 1])) addEndBeam(el); el.endChar = index+1; return el; } } else if (state === 'broken_rhythm') {el.endChar = index;return el;} else return null; break; case ' ': case '\t': if (isComplete(state)) { el.end_beam = true; // look ahead to see if there is a tie dottedTie = false; do { if (line[index] === '.' && line[index+1] === '-') { dottedTie = true; index++; } if (line[index] === '-') { el.startTie = {}; if (dottedTie) el.startTie.style = "dotted"; } index++; } while (index < line.length && (tokenizer.isWhiteSpace(line[index]) || line[index] === '-') || (line[index] === '.' && line[index+1] === '-')); el.endChar = index; if (!durationSetByPreviousNote && canHaveBrokenRhythm && (line[index] === '<' || line[index] === '>')) { // TODO-PER: Don't need the test for < and >, but that makes the endChar work out for the regression test. index--; state = 'broken_rhythm'; } else return el; } else return null; break; case '>': case '<': if (isComplete(state)) { if (canHaveBrokenRhythm) { var br2 = getBrokenRhythm(line, index); index += br2[0] - 1; // index gets incremented below, so we'll let that happen multilineVars.next_note_duration = br2[2]; el.duration = br2[1]*el.duration; state = 'end_slur'; } else { el.endChar = index; return el; } } else return null; break; default: if (isComplete(state)) { el.endChar = index; return el; } return null; } index++; if (index === line.length) { if (isComplete(state)) {el.endChar = index;return el;} else return null; } } return null; }; var getBrokenRhythm = function(line, index) { switch (line[index]) { case '>': if (index < line.length - 2 && line[index + 1] === '>' && line[index + 2] === '>') // triple >>> return [3, 1.875, 0.125]; else if (index < line.length - 1 && line[index + 1] === '>') // double >> return [2, 1.75, 0.25]; else return [1, 1.5, 0.5]; case '<': if (index < line.length - 2 && line[index + 1] === '<' && line[index + 2] === '<') // triple <<< return [3, 0.125, 1.875]; else if (index < line.length - 1 && line[index + 1] === '<') // double << return [2, 0.25, 1.75]; else return [1, 0.5, 1.5]; } ret