UNPKG

chordsong

Version:

ChordSong is a simple text format for the notation of lyrics with guitar chords, and an application that renders them to portable HTML pages.

219 lines (196 loc) 7.05 kB
export interface FretStrings { [fret: string]: number[] // is an array of pressed strings per fret } export interface FingerOrBarre { finger?: number // The finger that presses the fret string/s fret: number stringFrom: number stringTo: number } export const stringsFretsRegEx = /^([oOxX\d]{6}|(?:(?:[xXoO]|\d{1,2}),){5}(?:[xXoO]|\d{1,2}))$/ export const fretStringsRegEx = /^(([12]?\d):([1-6](-[1-6])?)(,[1-6](-[1-6])?){0,5})(\/([12]?\d):([1-6](-[1-6])?)(,[1-6](-[1-6])?){0,5}){1,3}$/ export class Diagram { chordName?: string chordVariant?: string chordRenderName?: string private _stringFrets: number[] = [] // array of pressed frets by string, starting by the thickest string. readonly fretStrings: FretStrings = {} // list of strings pressed at a given fret. private _fingersAndBarres: FingerOrBarre[] = [] // array with pressed strings in every fret by each finger constructor (parsedDiagram: string, chordName?: string, chordVariant?: string, chordRenderName?: string) { this.chordName = chordName this.chordVariant = chordVariant this.chordRenderName = chordRenderName ?? chordName try { this.fromStringFrets(parsedDiagram) } catch (error) { this.fromFingersAndBarres(parsedDiagram) } } get stringFrets (): number[] { return this._stringFrets } set stringFrets (arr: number[]) { this._stringFrets = arr this._computeFretStrings() this._computeFingersAndBarrels() } get fingersAndBarrels (): FingerOrBarre[] { return this._fingersAndBarres } set fingersAndBarrels (arr: FingerOrBarre[]) { this._fingersAndBarres = arr this._computeStringFrets() this._computeFretStrings() } get minFret (): number { return Math.min(...this.stringFrets.filter(fret => fret > 0)) } get maxFret (): number { return Math.max(...this.stringFrets) } private fromStringFrets (parsedDiagram: string): void { if (!stringsFretsRegEx.test(parsedDiagram)) { throw new Error(`invalid stringFrets format for ${parsedDiagram}`) } parsedDiagram = parsedDiagram .replace(/\|/g, '') .replace(/[Oo]/g, '0') .replace(/[Xx]/g, 'x') // Let's take what fret is assigned to every string. // When stringFrets is set, fretString and fingersAndBarrels are updated this.stringFrets = (parsedDiagram.match(/[x0-9]{6}/) != null ? parsedDiagram.split('') : parsedDiagram.split(',') ).map((str: string) => str === 'x' ? -1 : Number(str)) } private fromFingersAndBarres (parsedDiagram: string): void { // Expected input is a semicolon-sepparated string of fret/[stringRange,...] // Use fret 0 for open strings. Strings that are neither pressed or open are assumed to be muted // stringRange can be a single number from 1 (thinest) to 6 (thickest) or a range, eg.: 2 or 1-6 // Examples: // - barre F (132211): 1:6-1/2:3/3:4,5 // - E (022100): 0:1,2,6/1:3/2:4,5 // - Cm (x35543): 3:1-5/4:2/5:3,4 // - A (x02220): 0:1,5/2:3-4,2 if (!fretStringsRegEx.test(parsedDiagram)) { throw new Error(`invalid format for ${parsedDiagram}`) } const fingersAndBarres: FingerOrBarre[] = [] parsedDiagram.split('/').forEach(item => { const parsed: FingerOrBarre[] = [] let fret: number item.split(':').forEach((value, index) => { if (index === 0) { fret = Number(value) } else if (index === 1) { value.split(',').forEach(range => { let a = Number(range[0]) a = 6 - a // the 6th string is our 0, the 5th our one, and so on let b = Number(range[range.length - 1]) b = 6 - b // the 6th string is our 0, the 5th our one, and so on parsed.push({ fret, stringFrom: (a < b) ? a : b, stringTo: (a >= b) ? a : b }) }) } else { throw new Error(`Invalid diagram ${parsedDiagram}`) } }) parsed.forEach(item => { fingersAndBarres.push(item) }) }) this.fingersAndBarrels = fingersAndBarres // After fingersAndBarrels is set, stringFrets and fretString are automatically updated, so no need to do it manually } private _computeBarrel (): FingerOrBarre | null { // barrel not needed if: // 1. We need 4 or less fingers // 2. We need 5 fingers but there is a fret in the 6th string (it could be pressed with the thumb). const fingeredFrets = this.stringFrets.filter((item) => item > 0) if (fingeredFrets.length < 5 || (fingeredFrets.length === 5 && this.stringFrets[0] > 0)) { return null } const barrels: { [fretPos: string]: { from: number to: number } } = {} for (const [fretPos, strings] of Object.entries(this.fretStrings)) { if (Number(fretPos) <= 0) continue const from = Math.min(...strings) let to = from for (let i = from + 1; i < 6; i++) { if (!strings.includes(i) && this.stringFrets[i] >= 0 && this.stringFrets[i] < Number(fretPos)) { break } else { to++ } } barrels[fretPos] = { from, to } } let fret = 0 let stringFrom = 0 let stringTo = 0 for (const [fretPos, fromTo] of Object.entries(barrels)) { if (fromTo.to - fromTo.from + 1 > stringTo - stringFrom) { fret = Number(fretPos) stringFrom = fromTo.from stringTo = fromTo.to } } return { fret, stringFrom, stringTo } } private _computeStringFrets (): void { this._stringFrets = [-1, -1, -1, -1, -1, -1] this._fingersAndBarres.forEach(item => { const fret = item.fret const strings: number[] = range(item.stringTo, item.stringFrom) strings.forEach(item => { if (fret > this._stringFrets[item]) { this._stringFrets[item] = fret } }) }) } private _computeFretStrings (): void { this._stringFrets.forEach((fret, string) => { if (String(fret) in this.fretStrings) { this.fretStrings[String(fret)].push(string) } else { this.fretStrings[String(fret)] = [string] } }) } private _computeFingersAndBarrels (): void { const barrel = this._computeBarrel() if (barrel != null) { this._fingersAndBarres.push(barrel) } for (const [fretPos, strings] of Object.entries(this.fretStrings)) { if (Number(fretPos) <= 0) continue if (barrel === null || Number(fretPos) !== barrel.fret) { strings.forEach((fretStr) => this._fingersAndBarres.push({ fret: Number(fretPos), stringFrom: fretStr, stringTo: fretStr }) ) } } } toString (): string { return this.stringFrets.join(',').replace(/-1/g, 'x').replace(/0/g, 'o') } } function range (max: number, min: number = 0, step: number = 1): number[] { const arr: number[] = [] for (let i = min; i <= max; i += step) { arr.push(i) } return arr }