npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

octavian

v3.0.0

Published

Type-safe music theory utilities for notes, intervals, chords, and scales.

Readme

Octavian

Type-safe music theory utilities for working with notes, intervals, chords, and scales in TypeScript.

Installation

npm install octavian
pnpm add octavian
yarn add octavian
bun add octavian

[!NOTE] Works in Node 22+ (ESM), Bun 1.3+, and any browser bundler (Vite, webpack, Rollup). A single browser-safe ESM bundle is published — resolution is automatic via the exports map.

Design Goals

  • Immutable value objects for Note, Chord, and Scale
  • Runtime validation at every trust boundary
  • Theory-first spelling for interval, chord, and scale construction
  • Sharp-preferred simplification by default when a pitch is derived from raw MIDI or semitone math; flat-preferred available via 'flats' preference
  • 100% test coverage with full validation gates

Quick Start

import { Chord, Note, Scale } from 'octavian';

const cSharp = Note.create('C#4');
const eb = cSharp.transpose('minorThird');
const cMajorSeven = Chord.create('C4', 'maj7');
const cMajor = Scale.create('C4', 'major');

console.log(String(eb)); // "E4"
console.log(cMajorSeven.notes.map(String)); // ["C4", "E4", "G4", "B4"]
console.log(cMajor.mode('dorian').toString()); // "D dorian"

Types

All public types are importable directly from 'octavian':

import type {
  NoteName,
  ChordSuffix,
  ScaleType,
  Interval,
  MidiKey,
  Frequency,
  Semitones,
  Octave,
  Tuning,
  CadenceType,
  HarmonicFunction,
  RomanNumeralLike,
  KeySignatureMode,
  IntervalConsonance,
  FiguredBass,
} from 'octavian';

Key exported type names:

  • NoteName: A valid note spelling such as 'C#' or 'Bb'.
  • ChordSuffix: A canonical chord suffix such as 'majorSeventh' or 'minorTriad'.
  • ScaleType: A canonical scale type such as 'major' or 'melodicMinor'.
  • Interval: A canonical interval name such as 'majorThird' or 'perfectFifth'.
  • MidiKey: A branded number in the range 0..127.
  • Frequency: A branded number in hertz.
  • Semitones: A branded integer semitone distance.
  • Octave: A branded octave value in the range -1..9. Use createOctave(n) to produce one.
  • Tuning: A tuning reference, e.g. { reference: 'A4', frequency: createFrequency(440) }.
  • CadenceType: A cadence label such as 'authentic-perfect', 'half', or 'phrygian'.
  • HarmonicFunction: 'tonic' | 'predominant' | 'dominant'.
  • KeySignatureMode: 'major' | 'minor'.
  • IntervalConsonance: 'perfect-consonance' | 'imperfect-consonance' | 'mild-dissonance' | 'sharp-dissonance'.
  • FiguredBass: An array of FiguredBassFigure objects representing a figured-bass stack.

Core Types

Notes

Note is the base value object for pitch spelling, MIDI conversion, and frequency conversion.

import { Note } from 'octavian';

const note = Note.create('Bb3');

note.note; // "Bb"
note.octave; // 3
note.midi; // 58
note.frequency; // 233.08... (always standard tuning A4 = 440 Hz)
note.chromaticIndex; // pitch class 0–11
note.enharmonics; // ["A#", "Cbb"]

Constructing notes

import { Note, createFrequency, STANDARD_TUNING } from 'octavian';

Note.create('C#4'); // from a note-name-with-octave string
Note.create({ note: 'C#', octave: 4 }); // from a structured object
Note.fromMidi(61); // "C#4" — sharp-preferred by default
Note.fromMidi(61, 'flats'); // "Db4" — flat-preferred
Note.nearestTo(440); // nearest equal-tempered note under standard tuning
Note.nearestTo(432, { reference: 'A4', frequency: createFrequency(432) }); // alternate tuning
Note.nearestTo(277.18, STANDARD_TUNING, 'flats'); // "Db4"

Transposition and movement

const c = Note.create('C4');

c.transpose('majorThird').toString(); // "E4"  — theory-correct spelling
c.transposeBy(1).toString(); // "C#4" — sharp-preferred by default
c.transposeBy(1, 'flats').toString(); // "Db4"
c.up().toString(); // "C5"
c.up(2).toString(); // "C6"
c.down().toString(); // "C3"
c.withOctave(5).toString(); // "C5"
c.simplify().toString(); // "C4" — sharp-preferred common spelling
c.simplify('flats').toString(); // flat-preferred common spelling

Comparison and distance

const a = Note.create('C4');
const b = Note.create('G4');

a.distanceTo(b); // "perfectFifth"
a.semitonesTo(b); // 7
a.equals(Note.create('C4')); // true
a.isEnharmonicTo(Note.create('B#3')); // true
Note.compare(a, b); // -1 | 0 | 1 (by MIDI key)

Frequency and tuning

note.frequency always returns the standard-tuning (A4 = 440 Hz) value. For alternate tunings, use frequencyAt or the free function noteToFrequency:

import { Note, noteToFrequency, createFrequency } from 'octavian';

const a4 = Note.create('A4');
const tuning432 = { reference: 'A4', frequency: createFrequency(432) };

a4.frequency; // 440 (always standard tuning)
a4.frequencyAt(tuning432); // 432
noteToFrequency(a4, tuning432); // 432
noteToFrequency('A4'); // 440 (accepts any NoteLike, defaults to standard tuning)

Serialization

const note = Note.create('C4');
note.toString(); // "C4"
note.valueOf(); // 60 (MIDI key — makes notes sortable and comparable as numbers)
note.toJSON(); // { note: "C", octave: 4, midi: 60, frequency: 261.63 }
Note.create(note.toJSON()); // round-trips via Note.create
[...note]; // [note, octave, midi, frequency] — iterable

Intervals

The library exports a full interval catalog and helpers for alias resolution.

import {
  INTERVALS,
  applyInterval,
  findCanonicalIntervalBySemitonesAndDegree,
  resolveInterval,
} from 'octavian';

resolveInterval('tone'); // "majorSecond"
resolveInterval('P5'); // "perfectFifth"
findCanonicalIntervalBySemitonesAndDegree(6, 5); // "diminishedFifth"
applyInterval(Note.create('F#4'), 'majorThird').toString(); // "A#4"
INTERVALS.majorSixth.symbol; // "M6"
INTERVALS.majorSixth.semitones; // 9
INTERVALS.majorSixth.quality; // "major"
INTERVALS.majorSixth.degree; // 6

Chords

Chord normalizes symbols and suffix aliases into a canonical suffix while keeping immutable note collections.

Use Chord.create(root, suffix) to construct a chord. To recreate a chord from serialized data, use Chord.fromJSON(serialized).

import { Chord, Note } from 'octavian';

const chord = Chord.create('C4', 'maj7');

chord.name; // "Cmaj7"
chord.symbol; // "maj7"
chord.suffix; // "majorSeventh"
chord.quality; // "major"
chord.root.toString(); // "C4"
chord.bass.toString(); // "C4"
chord.notes.map(String); // ["C4", "E4", "G4", "B4"]
chord.midi; // [60, 64, 67, 71]
chord.size; // 4

Inversions:

chord.invert().name; // "Cmaj7/E"
chord.invert(2).name; // "Cmaj7/G"
chord.inversion(1).name; // "Cmaj7/E"
chord.lowerFromTop(2).notes.map(String); // ["G3", "C4", "E4", "B4"]
chord.closeVoicing().notes.map(String); // close-position voicing

Chord editing stays catalog-backed:

Chord.create('C4', 'maj7').omit('majorSeventh').name; // "C"
Chord.create('C4', 'major').add('majorSeventh').name; // "Cmaj7"
Chord.create('C4', 'major').alter('perfectFifth', 'augmentedFifth').name; // "Caug"
Chord.create('C4', 'major').slash(Note.create('G3')).name; // "C/G"

Chord catalog helpers:

import { CHORDS, chordQualityForSuffix, createChordName, createSlashChordName } from 'octavian';

CHORDS.majorSeventh.intervals; // ["unison", "majorThird", "perfectFifth", "majorSeventh"]
chordQualityForSuffix('minorSeventh'); // "minor"
createChordName('C', 'maj7'); // "Cmaj7"
createSlashChordName('C', 'maj7', 'E'); // "Cmaj7/E"

Scales

Scale builds theory-correct spellings from a root note and normalized scale type.

import { Scale, Note } from 'octavian';

const scale = Scale.create(Note.create('C4'), 'major');

scale.notes.map(String); // ["C4", "D4", "E4", "F4", "G4", "A4", "B4"]
scale.root.toString(); // "C4"
scale.type; // "major"
scale.size; // 7

Navigation and relationships:

scale.relative('naturalMinor').toString(); // "A naturalMinor"
scale.parallel('naturalMinor').toString(); // "C naturalMinor"
scale.mode('lydian').toString(); // "F lydian"
scale.modes().map((m) => m.toString()); // all 7 modes
scale.rotate(2).toString(); // rotation by 2 positions
scale.next().toString(); // next scale root (semitone up)
scale.previous().toString(); // previous scale root

Degrees and chords:

scale.degree(1).toString(); // "C4"
scale.degree(5).toString(); // "G4"
scale.degreeOf(Note.create('E4')); // 3
scale.chord(5, 'seventh').name; // "G7"
scale.chords().map((c) => c.name); // triads for every degree
scale.seventhChords().map((c) => c.name); // seventh chords for every degree
scale.triad(1).name; // "C"

Ascending/descending helpers return notes from a given starting pitch:

scale.ascendingFrom(Note.create('C3')).map(String);
scale.descendingFrom(Note.create('C5')).map(String);

Scale catalog:

import { SCALES, resolveScaleType, scaleTypeForMode, isDiatonicModeFamily } from 'octavian';

SCALES.major.intervals; // ["unison", "majorSecond", "majorThird", ...]
resolveScaleType('ionian'); // "major"
scaleTypeForMode('dorian'); // "major"
isDiatonicModeFamily('lydian'); // true
isDiatonicModeFamily('blues'); // false

MIDI and Frequency

The library uses standard equal temperament with A4 = 440 Hz.

import {
  Note,
  STANDARD_TUNING,
  midiToFrequency,
  midiToNoteNameWithOctave,
  noteNameToMidi,
} from 'octavian';

STANDARD_TUNING; // { reference: 'A4', frequency: 440 }
Note.fromMidi(69).toString(); // "A4"
Note.fromMidi(61, 'flats').toString(); // "Db4"
Note.nearestTo(440); // Note at A4
midiToFrequency(69); // 440
midiToNoteNameWithOctave(69); // { note: "A", octave: 4 }
midiToNoteNameWithOctave(61, 'flats'); // { note: "Db", octave: 4 }
noteNameToMidi('A', createOctave(4)); // 69

For alternate tunings, pass a Tuning object:

import { createFrequency, noteToFrequency } from 'octavian';

const tuning432 = { reference: 'A4', frequency: createFrequency(432) };

Note.nearestTo(432, tuning432).toString(); // "A4" (nearest note under that tuning)
noteToFrequency('A4', tuning432); // 432

Parsing and Validation

import {
  isChordSuffix,
  isChordSymbol,
  isInterval,
  isNoteName,
  isNoteNameWithOctave,
  isScaleType,
  parseNoteName,
  parseNoteNameWithOctave,
} from 'octavian';

isNoteName('Db'); // true
isNoteNameWithOctave('Db4'); // true
isInterval('sharpEleven'); // true
isChordSuffix('minorTriad'); // true
isChordSymbol('m7b5'); // true
isScaleType('mixolydian'); // true

parseNoteName('C#'); // { note: "C#", natural: "C", accidental: "#" }
parseNoteNameWithOctave('Bb3'); // { note: "Bb", octave: 3 }

Invalid input throws TypeError (wrong shape) or RangeError (out-of-bounds value) from constructors and factory methods instead of failing silently.

Random Selection

For applications like ear-training, the library provides random-pick helpers with an injectable RNG for reproducible tests or seeded quizzes.

import { randomNote, randomInterval, INTERVALS } from 'octavian';

// Pick a random note in a MIDI range (inclusive, sharp-preferred spelling)
const note = randomNote({ range: ['C4', Note.fromMidi(72)] });

// Pick from an explicit pool
const rootNote = randomNote({ pool: ['C4', 'D4', 'E4', 'F4', 'G4', 'A4', 'B4'] });

// Pick a random interval from an explicit pool (aliases are normalized; duplicates weight the distribution)
const interval = randomInterval({ pool: ['majorThird', 'perfectFifth', 'minorSeventh'] });

// Inject a seeded RNG for reproducible results
import seedrandom from 'seedrandom'; // any [0,1) RNG
const rng = seedrandom('my-seed');
const seededNote = randomNote({ range: ['C4', 'C5'], random: rng });

Both functions require either range or pool — there is no default. The injected random function must return values in [0, 1).

Branded Type Constructors

The library exposes constructors for every branded primitive, which are needed when calling APIs that require branded types directly:

import {
  createOctave,
  createMidiKey,
  createFrequency,
  createSemitones,
  createChromaticIndex,
  OCTAVES,
  CHROMATIC_INDEXES,
} from 'octavian';

createOctave(4); // Octave (validated: must be -1..9)
createMidiKey(60); // MidiKey (validated: must be 0..127)
createFrequency(440); // Frequency (validated: must be > 0)
createSemitones(7); // Semitones
createChromaticIndex(0); // ChromaticIndex (0..11)
OCTAVES; // [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9] as const
CHROMATIC_INDEXES; // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] as const

Note-Name Utilities

Low-level helpers for working with note spellings directly:

import {
  NATURALS,
  ACCIDENTALS,
  ALL_NOTE_NAMES,
  FLAT_PREFERRED_NOTE_NAMES,
  SHARP_PREFERRED_NOTE_NAMES,
  NATURAL_CHROMATIC_INDEXES,
  ACCIDENTAL_OFFSETS,
  buildNoteName,
  simplifyNoteName,
  enharmonicsForNoteName,
  normalizeChromaticIndex,
  noteNameToChromaticIndex,
} from 'octavian';

NATURALS; // ["C", "D", "E", "F", "G", "A", "B"]
ACCIDENTALS; // ["", "#", "b", "##", "bb"]
ALL_NOTE_NAMES; // all 35 note names (all naturals × all accidentals)
SHARP_PREFERRED_NOTE_NAMES; // 12-note chromatic scale, sharp spellings
FLAT_PREFERRED_NOTE_NAMES; // 12-note chromatic scale, flat spellings

buildNoteName('C', 1); // "C#"   (natural + accidental offset)
simplifyNoteName('Bbb'); // "A"  (sharp-preferred common spelling)
enharmonicsForNoteName('C#'); // ["Db", "Bx"] (all enharmonic spellings)
normalizeChromaticIndex(13); // 1 (wraps to 0..11)
noteNameToChromaticIndex('C#'); // 1

Chord and Scale Symbols

import { CHORD_SYMBOLS, resolveChordSuffix } from 'octavian';

CHORD_SYMBOLS; // ["m", "m7", "maj7", "dim", ...] — short display symbols
resolveChordSuffix('m7'); // "minorSeventh"
resolveChordSuffix('Δ7'); // "majorSeventh"

Key Signatures

import { KEY_SIGNATURES, keySignatureFor, keySignatureFromAccidentals } from 'octavian';

keySignatureFor('C', 'major');
// { tonic: 'C', mode: 'major', accidentalCount: 0, accidentals: [], order: 'none', ... }

keySignatureFor('F#', 'major');
// { tonic: 'F#', mode: 'major', accidentalCount: 6, accidentals: ['F#','C#','G#','D#','A#','E#'], order: 'sharps' }

keySignatureFromAccidentals(3, 'flats');
// { tonic: 'Eb', mode: 'major', ... }

KEY_SIGNATURES['C major'].accidentalCount; // 0
KEY_SIGNATURES['G major'].accidentals; // ['F#']

The catalog covers all 30 standard keys (15 major + 15 minor), including enharmonic pairs like C♯/D♭ major and A♯/B♭ minor. Theoretical keys (G♯ major, F♭ major, etc.) are present in KEY_SIGNATURES for reference but are not constructible as Key instances.

Key

Key models a tonal center with its diatonic scale, key signature, and relationships to neighboring keys.

import { Key } from 'octavian';

const cMajor = Key.create('C', 'major');

cMajor.tonic.toString(); // "C4"
cMajor.mode; // "major"
cMajor.toString(); // "C major"
cMajor.toJSON(); // { tonic: 'C', mode: 'major' }

Relationships

cMajor.relativeKey.toString(); // "A minor"
cMajor.parallelKey.toString(); // "C minor"
cMajor.dominantKey.toString(); // "G major"
cMajor.subdominantKey.toString(); // "F major"

Key.adjacentKeys(cMajor);
// { dominant: Key("G major"), subdominant: Key("F major") }

Key.enharmonicEquivalent(Key.create('F#', 'major')).toString(); // "Gb major"
Key.distanceInFifths(Key.create('C', 'major'), Key.create('G', 'major')); // 1

Diatonic chords

cMajor.diatonicChords().map((c) => c.name);
// ["C", "Dm", "Em", "F", "G", "Am", "Bdim"]

cMajor.diatonicSeventhChords().map((c) => c.name);
// ["Cmaj7", "Dm7", "Em7", "Fmaj7", "G7", "Am7", "Bm7b5"]

cMajor.contains(Note.create('E4')); // true
cMajor.contains(Chord.create('G4', 'dominant7')); // true

Transposition

cMajor.transpose('majorSecond').toString(); // "D major"
cMajor.transposeBy(7).toString(); // "G major"

Circle of Fifths

import {
  CIRCLE_OF_FIFTHS_MAJOR,
  CIRCLE_OF_FIFTHS_MINOR,
  circleOfFifths,
  distanceInFifths,
  adjacentKeys,
  enharmonicEquivalent,
  isOnCircleOfFifths,
} from 'octavian';

CIRCLE_OF_FIFTHS_MAJOR.map((k) => k.tonic);
// ['C', 'G', 'D', 'A', 'E', 'B', 'F#', 'Db', 'Ab', 'Eb', 'Bb', 'F']

circleOfFifths('C', 'major', 2);  // moves 2 steps clockwise → G major signature
distanceInFifths({ tonic: 'C', mode: 'major', ... }, { tonic: 'G', mode: 'major', ... }); // 1

isOnCircleOfFifths('F#', 'major'); // true
isOnCircleOfFifths('G#', 'major'); // false (theoretical key)

Key.adjacentKeys and Key.enharmonicEquivalent are the ergonomic wrappers for most use cases; the free functions above operate on raw KeySignatureInformation objects for catalog-level work.

Roman Numerals

import { RomanNumeral, romanNumeralFor, chordFromRomanNumeral } from 'octavian';

const rn = RomanNumeral.create('V7');
rn.degree; // 5
rn.quality; // "major"
rn.inversion; // "7"
rn.toString(); // "V7"

RomanNumeral.create('bVII').alteration; // "flat"

// Analysis: chord → Roman numeral
const cMajor = Key.create('C', 'major');
romanNumeralFor(cMajor, Chord.create('G4', 'dominant7')).toString(); // "V7"
romanNumeralFor(cMajor, Chord.create('E4', 'minor')).toString(); // "iii"

// Synthesis: Roman numeral → chord
chordFromRomanNumeral(cMajor, 'IV').name; // "F"
chordFromRomanNumeral(cMajor, 'V7').name; // "G7"

Harmonic Function

Classifies a chord's role within a key as tonic, predominant, or dominant.

import {
  harmonicFunctionFor,
  harmonicFunctionForNumeral,
  harmonicFunctionForAsAlias,
} from 'octavian';

const cMajor = Key.create('C', 'major');

harmonicFunctionFor(cMajor, Chord.create('C4', 'major')); // "tonic"
harmonicFunctionFor(cMajor, Chord.create('F4', 'major')); // "predominant"
harmonicFunctionFor(cMajor, Chord.create('G4', 'dominant7')); // "dominant"
harmonicFunctionFor(cMajor, Chord.create('D4', 'minor')); // "predominant"

// From a RomanNumeral directly (avoids re-parsing the chord)
harmonicFunctionForNumeral(RomanNumeral.create('ii'), 'major'); // "predominant"

// Alias form: returns 'subdominant' instead of 'predominant'
harmonicFunctionForAsAlias(cMajor, Chord.create('F4', 'major')); // "subdominant"

Returns null for non-diatonic chords or altered/applied numerals.

Figured Bass

import {
  figuredBassForChord,
  figuredBassInversionFor,
  figuredBassToChord,
  parseFiguredBass,
  formatFiguredBass,
} from 'octavian';

const cMaj = Chord.create('C4', 'major');
const cMajFirstInv = cMaj.invert();

figuredBassForChord(cMaj).length; // 0 (root position — empty stack)
figuredBassInversionFor(cMaj); // "5/3"
figuredBassInversionFor(cMajFirstInv); // "6"

const g7 = Chord.create('G4', 'dominant7');
figuredBassInversionFor(g7.invert()); // "6/5"
figuredBassInversionFor(g7.invert(2)); // "4/3"
figuredBassInversionFor(g7.invert(3)); // "4/2"

// Parse a figured-bass string into a figure stack
parseFiguredBass('6/4'); // [{ digit: 6 }, { digit: 4 }]
parseFiguredBass('♭7'); // [{ digit: 7, accidental: 'flat' }]
parseFiguredBass('6#'); // [{ digit: 6, accidental: 'sharp' }]

// Format a figure stack back to strings
formatFiguredBass([{ digit: 6 }, { digit: 4 }]);
// { stacked: ['6', '4'], inline: '6/4' }

// Reconstruct a chord from a bass note, figures, and key context
const cMajor = Key.create('C', 'major');
figuredBassToChord('E4', '6', cMajor).name; // "C/E" (C major, first inversion)
figuredBassToChord('G4', '6/4', cMajor).name; // "C/G" (C major, second inversion)

Interval Operations

import {
  invertInterval,
  simplifyInterval,
  compoundInterval,
  consonanceOf,
  isConsonant,
  isDissonant,
} from 'octavian';

invertInterval('perfectFifth'); // "perfectFourth"
invertInterval('majorThird'); // "minorSixth"
invertInterval('augmentedFourth'); // "diminishedFifth"

simplifyInterval('perfectEleventh'); // "perfectFourth"  (P11 → P4)
simplifyInterval('majorNinth'); // "majorSecond"    (M9 → M2)

compoundInterval('perfectFourth', 1); // "perfectEleventh"
compoundInterval('majorSecond', 1); // "majorNinth"

consonanceOf('perfectFifth'); // "perfect-consonance"
consonanceOf('majorThird'); // "imperfect-consonance"
consonanceOf('majorSecond'); // "mild-dissonance"
consonanceOf('minorSecond'); // "sharp-dissonance"

isConsonant('perfectFifth'); // true
isDissonant('augmentedFourth'); // true

Cadences

CadenceInput accepts a Roman numeral string ('V', 'iv6'), a RomanNumeral object, or a Chord.

import { Key, identifyCadence, identifyCadenceSequence } from 'octavian';

const cMajor = Key.create('C', 'major');

// Via Key methods
cMajor.identifyCadence('V', 'I'); // "authentic-perfect"
cMajor.identifyCadence('V', 'I6'); // "authentic-imperfect"
cMajor.identifyCadence('ii', 'V'); // "half"
cMajor.identifyCadence('IV', 'I'); // "plagal"
cMajor.identifyCadence('V', 'vi'); // "deceptive"

const aMinor = Key.create('A', 'minor');
aMinor.identifyCadence('iv6', 'V'); // "phrygian"

// Non-cadential pair
cMajor.identifyCadence('I', 'IV'); // null

// Via free functions (same Key as first argument)
identifyCadence(cMajor, 'V', 'I'); // "authentic-perfect"
identifyCadence(cMajor, Chord.create('G4', 'dominant7'), Chord.create('C4', 'major'));
// "authentic-perfect"

// Scan a full progression for cadences
cMajor
  .identifyCadenceSequence(['I', 'IV', 'I', 'ii', 'V'])
  .map((c) => `index ${c.index}: ${c.type}`);
// ["index 0: plagal", "index 3: half"]

identifyCadenceSequence(cMajor, ['I', 'IV', 'V', 'I']);
// [{ index: 1, type: 'half' }, { index: 2, type: 'authentic-perfect' }]

Development

Run the local quality gates with Bun:

bun run typecheck
bun run lint
bun test
bun run build
bun run validate