All files / Bandstand/src/lib abc.ts

100% Statements 52/52
83.33% Branches 15/18
100% Functions 6/6
100% Lines 52/52

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 732x               2x 3x 3x 3x 3x 3x 3x 3x 3x   3x 3x     2x 1x 1x 1x 1x 1x   1x 1x 1x 7x 7x 7x 7x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x               2x 4x 4x     2x 6x 6x 6x  
import {transposeKey} from './chordpro'
 
/**
 * ABC notation (abcnotation.com): music as text. `"C"B2 B2 | "G"B4 |]` is
 * two bars with chord symbols. People type only the music; the header ABC
 * requires is added here, in the song's key so that transposing the chart
 * transposes the notation the same way.
 */
export function withAbcHeader(abc: string, key = 'C') {
  const text = abc.replace(/\r\n?/g, '\n').trim()
  const has = (field: string) => new RegExp(`^${field}:`, 'm').test(text)
  const head = [
    has('X') ? null : 'X:1',
    has('M') ? null : 'M:4/4',
    has('L') ? null : 'L:1/8',
    has('K') ? null : `K:${key}`,
  ].filter(Boolean)
  // K: must be the last header line, so ours go before anything typed
  return [...head, text].join('\n')
}
 
/** Starter patterns for the notation editor, in `key` (the song's key). */
export function abcPresets(key: string | null) {
  const k = key ?? 'C'
  const I = k
  const IV = transposeKey(k, 5)
  const V = transposeKey(k, 7)
  const rhythm = `K:${k} style=rhythm\n`
  // Scale steps up from the key's note (ABC: C-B, then c-b an octave up)
  const LETTERS = 'CDEFGAB'
  const root = Math.max(0, LETTERS.indexOf(k[0]))
  const n = (step: number) => {
    const i = root + step
    const letter = LETTERS[i % 7]
    return i < 7 ? letter : letter.toLowerCase()
  }
  return [
    {
      name: 'Straight 8ths',
      abc: `${rhythm}"${I}"BBBB BBBB | "${V}"BBBB BBBB |]`,
    },
    {
      name: 'Push the and of 4',
      abc: `${rhythm}"${I}"B2 B2 B2 B"${IV}"B- | B8 |]`,
    },
    {
      name: 'Stabs on 1 and the and of 2',
      abc: `${rhythm}"${I}"B2 zB z4 | "${V}"B2 zB z4 |]`,
    },
    {name: 'Whole notes', abc: `${rhythm}"${I}"B8 | "${IV}"B8 |]`},
    {
      name: 'A melody line',
      abc: `K:${k}\n"${I}"${n(0)}2 ${n(1)}${n(2)} ${n(3)}2 ${n(2)}${n(1)} | "${V}"${n(0)}8 |]`,
    },
  ]
}
 
/**
 * Which tab a lick reads best as: a "% instrument: bass" line in the ABC
 * (ABC comments are ignored by the renderer) asks for 4-string bass tab;
 * "% instrument: keys" (a two-hand part) has no tab; anything else gets
 * guitar tab.
 */
export function abcTabInstrument(abc: string): 'guitar' | 'bass' | 'keys' {
  return abcPart(abc) ?? 'guitar'
}
 
/** The instrument a lick is written for, if its ABC says. */
export function abcPart(abc: string): 'guitar' | 'bass' | 'keys' | null {
  const m = abc.match(/^%\s*instrument:\s*(guitar|bass|keys)\b/im)
  return (m?.[1] as 'guitar' | 'bass' | 'keys' | undefined) ?? null
}