UNPKG

croonjs

Version:

Toolkit for parsing and playing numbered musical notation

188 lines (187 loc) 5.88 kB
function transformNoteNotation(note) { switch (note.toLowerCase()) { case '1': case 'do': return 1; case '2': case 're': return 2; case '3': case 'mi': return 3; case '4': case 'fa': return 4; case '5': case 'so': case 'sol': return 5; case '6': case 'la': return 6; case '7': case 'ti': case 'si': return 7; default: return 0; } } function getPosition(source, index) { var _a; const prefix = source.slice(0, index); const breakIndexes = Array.from(prefix) .map((item, itemIndex) => (item === '\n' ? itemIndex : -1)) .filter(itemIndex => itemIndex !== -1); const lastBreakIndex = (_a = breakIndexes[breakIndexes.length - 1]) !== null && _a !== void 0 ? _a : -1; return { line: breakIndexes.length + 1, column: (index - lastBreakIndex) + 1, }; } function parseToken(token, index, source) { const range = [index, index + token.length]; const loc = { start: getPosition(source, index), end: getPosition(source, index + token.length), }; const tempoMatches = token.match(/^!(\d+)$/); if (tempoMatches) { return { type: 'TempoNode', range, loc, raw: token, beat: Number(tempoMatches[1]), }; } const keySignatureMatches = token.match(/^([1-7])=([#b])?([A-G])$/); if (keySignatureMatches) { return { type: 'KeySignatureNode', range, loc, raw: token, tonic: Number(keySignatureMatches[1]), accidental: keySignatureMatches[2] === '#' ? 1 : (keySignatureMatches[2] === 'b' ? -1 : 0), pitch: keySignatureMatches[3], }; } const timeSignatureMatches = token.match(/^(\d+)\/(\d+)$/); if (timeSignatureMatches) { return { type: 'TimeSignatureNode', range, loc, raw: token, beat: Number(timeSignatureMatches[1]), unit: Number(timeSignatureMatches[2]), }; } const noteMatches = token.match(/^(\^)?([#b])?([A-Za-z]+|[0-7])(\+*|-*)(_*\.*)(&)?$/); if (noteMatches) { const dotIndex = noteMatches[5].indexOf('.'); const dotCount = dotIndex === -1 ? 0 : noteMatches[5].length - dotIndex; const underlineCount = noteMatches[5].length - dotCount; return { type: 'NoteNode', range, loc, raw: token, continuation: Boolean(noteMatches[1]), accidental: noteMatches[2] === '#' ? 1 : (noteMatches[2] === 'b' ? -1 : 0), notation: transformNoteNotation(noteMatches[3]), octave: noteMatches[4].startsWith('+') ? noteMatches[4].length : (noteMatches[4].startsWith('-') ? -noteMatches[4].length : 0), dot: dotCount, half: underlineCount, leaning: Boolean(noteMatches[6]), }; } if (token === '-') { return { type: 'DashNode', range, loc, raw: token, }; } const barLineMatches = token.match(/^(?:(\|{1,2})|(\|{2}:)|(:\|{2}))$/); if (barLineMatches) { return { type: 'BarLineNode', range, loc, raw: token, end: Boolean(barLineMatches[1]) && token.length > 1, repeat: barLineMatches[2] ? -1 : (barLineMatches[3] ? 1 : 0), }; } const fineMatches = token.match(/^\[(\d+)\.$/); if (fineMatches) { return { type: 'FineNode', range, loc, raw: token, except: Number(fineMatches[1]), }; } return { type: 'UnknownNode', range, loc, raw: token, }; } export function parse(notation) { const nodes = []; let matches; const matcher = /\S+/g; // eslint-disable-next-line no-cond-assign while (matches = matcher.exec(notation)) { nodes.push(parseToken(matches[0], matches.index, notation)); } return { type: 'ParsedNotation', nodes, }; } function stringifyNode(node) { switch (node.type) { case 'TempoNode': return `!${node.beat}`; case 'KeySignatureNode': return `${node.tonic}=${node.accidental === -1 ? 'b' : (node.accidental === 1 ? '#' : '')}${node.pitch}`; case 'TimeSignatureNode': return `${node.beat}/${node.unit}`; case 'NoteNode': return `${node.continuation ? '^' : ''}${node.notation}${node.octave > 0 ? '+'.repeat(node.octave) : (node.octave < 0 ? '-'.repeat(-node.octave) : '')}${'_'.repeat(node.half)}${'.'.repeat(node.dot)}${node.accidental === -1 ? 'b' : (node.accidental === 1 ? '#' : '')}${node.leaning ? '&' : ''}`; case 'DashNode': return '-'; case 'BarLineNode': return `${node.repeat === 1 ? ':' : ''}|${node.end || node.repeat ? '|' : ''}${node.repeat === -1 ? ':' : ''}`; case 'FineNode': return `[${node.except}.`; case 'UnknownNode': return node.raw; default: break; } } export function stringify(notation) { let result = ''; let lastPosition; for (const node of notation.nodes) { if (lastPosition) { if (node.loc && node.loc.start.line > lastPosition.line) { result += '\n'; } else { result += ' '; } } lastPosition = node.loc ? node.loc.end : { line: 1, column: 1 }; result += stringifyNode(node); } return result; }