string-fingerings

string-fingerings

Utilities for calculating fingerings on string instruments: parse note names, convert to/from MIDI numbers, and compute all possible fingerings for a set of notes on a configurable instrument (e.g. violin, double bass with extensions).

npm install string-fingerings

The library provides:

  • Note parsing — Parse note names (e.g. C4, Bb3, F#5) and pitch classes (e.g. C, Db) with full enharmonic support (#, b, x, bb).
  • MIDI conversion — Convert between note names and MIDI note numbers (middle C = 60).
  • Instrument model — Define instruments by strings (open note, optional extra open semitones, max stops) and physical constraints (scale length, stretch limits).
  • Fingering calculation — Get all valid fingerings for one or more notes, with optional natural harmonics and validation (e.g. stretch, no gaps).
  • Note — A note with a specific octave and enharmonic spelling (name, octave, alteration, number). Has text() and abcnote().
  • PitchClass — Note name + alteration without octave (e.g. C, F#).
  • NoteInputNote | PitchClass; used when octave may be omitted.
  • Instrument — Instrument definition: name, strings, stops, scaleLength, hardStretch, maxStretch, optional transposition.
  • InstrumentString — One string: openNote (Note), optional additionalOpenSemitones (e.g. [-1,-2,-3,-4] for a C extension), optional per-string stops.
  • Stop — A single fingering: stringIndex, stopIndex (semitones above open), noteNumber, naturalHarmonic.
Function Description
parse(noteText) Parse a note name (e.g. C4, Gb5) and return a Note. Throws on error.
tryParse(noteText) Like parse but returns Note | string (error message).
tryParseInput(noteText) Parse with optional octave: with octave → Note, without → PitchClass (e.g. C, C#).
isNote(input) Type guard: true if NoteInput is a Note.
noteNumber(noteName) MIDI number for a note name, or a string error message.
nn(noteName) Same as noteNumber but throws on invalid input.
noteName(noteNumber) Note name string (e.g. C4) for a MIDI number (0–127).
abcnote(noteNumber) ABC notation for a MIDI number.

Example

import { parse, noteNumber, noteName, tryParseInput, isNote } from 'string-fingerings'

parse('C4') // Note { name: 'C', octave: 5, number: 60, ... }
noteNumber('C4') // 60
noteName(60) // 'C4'
noteNumber('Bb3') // 58

const input = tryParseInput('G')
isNote(input) // false — it's a PitchClass
input.text() // 'G'
Function Description
expandPitchClass(pc, minMidi, maxMidi) All Notes for that pitch class in the MIDI range.
generateNoteCombinations(inputs, minMidi, maxMidi) All Note[] combinations; Note fixed, PitchClass expanded per octave (Cartesian product).

Example

import { tryParseInput, expandPitchClass, generateNoteCombinations, parse } from 'string-fingerings'

const c = tryParseInput('C')
expandPitchClass(c, 55, 100) // [C4, C5, C6, C7] as Note[]

// One fixed note + one pitch class → several combinations
generateNoteCombinations([c, parse('G5')], 55, 100)
// e.g. [ [C4, G5], [C5, G5], [C6, G5], [C7, G5] ]
Function Description
calculateFingerings(instrument, notes, validations?, includeNaturalHarmonics?) All valid fingerings for the given MIDI notes. Each fingering is a Stop[]. Optional validations: array of (instrument, fingering) => boolean.
getStopRelPos(stopIndex) Relative position on the string (0–1) for a stop in semitones above open.
fingeringHardness(instrument, fingering) Stretch “hardness” for a fingering (0.1, 0.5, or 1.0). 1.0 = beyond max stretch.
hasNoGaps(_instrument, fingering) True if used strings are consecutive (no skipped string).
hasPossibleStretch(instrument, fingering) True if fingering is within stretch limits (fingeringHardness !== 1.0).

Example: define an instrument and get fingerings

import {
parse,
calculateFingerings,
hasNoGaps,
hasPossibleStretch,
type Instrument,
type InstrumentString,
} from 'string-fingerings'

const violin: Instrument<InstrumentString> = {
name: 'Violin',
stops: 24,
scaleLength: 330,
hardStretch: 78,
maxStretch: 92,
strings: [
{ openNote: parse('G3') },
{ openNote: parse('D4') },
{ openNote: parse('A4') },
{ openNote: parse('E5') },
],
}

// Single note: all ways to play that note
const midiNotes = [72] // C5
const fingerings = calculateFingerings(violin, midiNotes, [], false)
// e.g. [ [{ stringIndex: 0, stopIndex: 17, noteNumber: 72, naturalHarmonic: false }], ... ]

// Chord with validations: only fingerings with no gaps and possible stretch
const chord = [60, 64, 67] // C major
const chordFingerings = calculateFingerings(
violin,
chord,
[hasNoGaps, hasPossibleStretch],
false
)

Example: double bass with C extension

const bassWithCExt: Instrument<InstrumentString> = {
name: 'Double bass with C-ext',
stops: 24,
scaleLength: 1049,
hardStretch: 116,
maxStretch: 120,
strings: [
{
openNote: parse('E1'),
additionalOpenSemitones: [-1, -2, -3, -4], // D, Db, C, B
},
{ openNote: parse('A1') },
{ openNote: parse('D2') },
{ openNote: parse('G2') },
],
}

// D1 on C-ext + A1 open
calculateFingerings(bassWithCExt, [38, 45], [], false)
// e.g. [ [{ stringIndex: 0, stopIndex: -2, noteNumber: 38, ... }, { stringIndex: 1, stopIndex: 0, noteNumber: 45, ... }] ]
  • Octave in parsed notes: internal octave is scientific-style (middle C = octave 5). parse('C4') gives a note with number 60 and internal octave 5; text() returns 'C4'.
  • MIDI note numbers are standard (0–127; middle C = 60).

MIT