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
text/typescript
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
}