Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 | 1x 1x 84x 84x 84x 84x 3x 3x 3x 84x 84x 84x 84x 241x 241x 241x 241x 241x 174x 174x 174x 174x 174x 174x 174x 174x 67x 241x 9x 241x 18x 18x 18x 18x 18x 9x 84x 84x 1x 1x 217x 13x 13x 13x 112x 112x 112x 1008x 1008x 1008x 112x 112x 13x 13x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 12x 12x 12x 12x 12x 12x 12x 12x 1x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 105x 105x 105x 105x 105x 105x 105x 105x 27x 78x 105x 105x 105x 6x 6x 105x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 7x 7x 42x 42x 42x 7x 7x 7x 7x 7x 7x 42x 7x 7x 1x 1x 1x 1x 1x | /**
* Melodies written out as letters, the way players jot down a horn line or
* a riff: "(G G G FF F# G) x4", "B B D# B C# A. B B B B A C#". Turned into
* notation (ABC) for horns, keys and strings, or guitar tab for guitar and
* bass parts.
*
* Letters carry pitch only. Octaves follow the line's shape (each note goes
* to the octave nearest the one before), and rhythm is what the spacing
* suggests: a letter alone is a quarter, doubled letters ("FF") are two
* eighths, and a period or a wide gap ends a phrase (a bar line).
*/
export type WrittenNote = {
letter: string // A-G
acc: '' | '#' | 'b'
eighth: boolean
/** A phrase ends after this note */
phraseEnd: boolean
}
export type WrittenLine = {notes: WrittenNote[]; repeats: string | null}
const ONE = /^([A-Ga-g])([#b]?)$/
/** The notes in a written-out line, or null if it isn't one. */
export function readNoteLine(line: string): WrittenLine | null {
let text = line.trim()
let repeats: string | null = null
const rep = text.match(/\s+(x\s?\d+)\s*$/i)
if (rep) {
repeats = rep[1].replace(/\s/g, '')
text = text.slice(0, rep.index).trim()
}
text = text.replace(/^\(|\)$/g, '').trim()
if (!text) return null
const notes: WrittenNote[] = []
// Each token with the gap after it: a wide gap ends a phrase
for (const m of text.matchAll(/(\S+)(\s*)/g)) {
let tok = m[1]
const end = tok.endsWith('.') || m[2].length >= 3
tok = tok.replace(/[.,]$/, '')
const one = tok.match(ONE)
if (one) {
notes.push({
letter: one[1].toUpperCase(),
acc: one[2] as WrittenNote['acc'],
eighth: false,
phraseEnd: end,
})
continue
}
// "FF", "GG", "BbBb": the same note twice, quick
const twice = tok.match(/^(([A-Ga-g])([#b]?))\1$/)
if (!twice) return null
for (const k of [0, 1])
notes.push({
letter: twice[2].toUpperCase(),
acc: twice[3] as WrittenNote['acc'],
eighth: true,
phraseEnd: k === 1 && end,
})
}
return notes.length >= 4 ? {notes, repeats} : null
}
const BASE: Record<string, number> = {C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11}
const semitone = (n: WrittenNote) =>
BASE[n.letter] + (n.acc === '#' ? 1 : n.acc === 'b' ? -1 : 0)
/** MIDI numbers: each note in the octave nearest the previous one. */
function pitches(notes: WrittenNote[], start: number) {
let prev = start
return notes.map((n) => {
const s = semitone(n)
let best = 0
for (let o = 0; o < 9; o++) {
const midi = 12 * (o + 1) + s
if (!best || Math.abs(midi - prev) < Math.abs(best - prev)) best = midi
}
prev = best
return best
})
}
// Sharps (+) or flats (-) in each key's signature
const SIGNATURE: Record<string, number> = {
C: 0,
G: 1,
D: 2,
A: 3,
E: 4,
B: 5,
'F#': 6,
'C#': 7,
F: -1,
Bb: -2,
Eb: -3,
Ab: -4,
Db: -5,
Gb: -6,
Cb: -7,
Am: 0,
Em: 1,
Bm: 2,
'F#m': 3,
'C#m': 4,
'G#m': 5,
'D#m': 6,
Dm: -1,
Gm: -2,
Cm: -3,
Fm: -4,
Bbm: -5,
Ebm: -6,
}
/** What the key signature does to each letter: '#', 'b' or ''. */
function keyAccidentals(key: string | null) {
const k = key?.match(/^([A-G][#b]?m?)/)?.[1] ?? 'C'
const n = SIGNATURE[k] ?? 0
const out: Record<string, string> = {}
for (const l of 'FCGDAEB'.slice(0, Math.max(n, 0))) out[l] = '#'
for (const l of 'BEADGCF'.slice(0, Math.max(-n, 0))) out[l] = 'b'
return out
}
/**
* ABC for one or more written lines, in the song's key (so it transposes
* with the chart). Accidentals are spelled exactly as written.
*/
export function linesToAbc(
lines: WrittenLine[],
key: string | null,
/** A guitar or bass part: written where guitarists read it, and the
* tab view uses that instrument's strings */
instrument: 'guitar' | 'bass' | null = null,
) {
const sig = keyAccidentals(key)
const body = lines.map((line) => {
// Horns and keys sit around A4; a guitar riff around B3 and a bass
// riff around B2 (both written an octave above where they sound)
const midi = pitches(
line.notes,
instrument === 'bass' ? 47 : instrument ? 59 : 69,
)
let bar: Record<string, string> = {}
let out = line.repeats ? `"^${line.repeats}"` : ''
line.notes.forEach((n, i) => {
const octave = Math.floor((midi[i] - semitone(n) - 12) / 12)
const id = `${n.letter}${octave}`
// Accidental needed if the bar so far (or the key) says otherwise
const now = bar[id] ?? sig[n.letter] ?? ''
const mark =
n.acc === now ? '' : n.acc === '#' ? '^' : n.acc === 'b' ? '_' : '='
bar[id] = n.acc
const letter =
octave >= 5
? n.letter.toLowerCase() + "'".repeat(octave - 5)
: n.letter + ','.repeat(Math.max(4 - octave, 0))
out += `${mark}${letter}${n.eighth ? '' : '2'}`
const next = line.notes[i + 1]
if (n.phraseEnd && next) {
out += ' | '
bar = {}
} else if (next && !(n.eighth && next.eighth && !n.phraseEnd)) out += ' '
})
return out + ' |]'
})
return [
...(instrument ? [`% instrument: ${instrument}`] : []),
`M:none`,
`L:1/8`,
`K:${key ?? 'C'}${instrument === 'bass' ? ' clef=bass' : ''}`,
...body,
].join('\n')
}
// Standard tuning, low to high (MIDI)
const STRINGS = [
{name: 'E', midi: 40},
{name: 'A', midi: 45},
{name: 'D', midi: 50},
{name: 'G', midi: 55},
{name: 'B', midi: 59},
{name: 'e', midi: 64},
]
/**
* Guitar tab for one written line (a bass player reads the bottom four
* strings an octave down). Notes stay in one hand position where they can.
*/
export function lineToTab(line: WrittenLine) {
const midi = pitches(line.notes, 47) // around B2: riff territory
const rows = STRINGS.map(() => [] as string[])
let fret = 3
line.notes.forEach((n, i) => {
let pick: {s: number; f: number} | null = null
STRINGS.forEach((st, s) => {
const f = midi[i] - st.midi
if (f < 0 || f > 15) return
if (!pick || Math.abs(f - fret) < Math.abs(pick.f - fret)) pick = {s, f}
})
const p = pick ?? {s: 0, f: Math.max(midi[i] - 40, 0)}
fret = p.f
const cell = String(p.f)
// Quick notes sit closer together, so the tab hints at the rhythm
const width = Math.max(cell.length + 1, n.eighth ? 2 : 3)
rows.forEach((r, s) =>
r.push(s === p.s ? cell.padEnd(width, '-') : '-'.repeat(width)),
)
if (n.phraseEnd && line.notes[i + 1]) rows.forEach((r) => r.push('|-'))
})
return STRINGS.map((st, s) => ({st, s}))
.reverse()
.map(({st, s}) => `${st.name}|-${rows[s].join('')}|`)
}
|