Type-safe music theory utilities for working with notes, intervals, chords, and scales in TypeScript.
npm install octavian
pnpm add octavian
yarn add octavian
bun add octavianNote
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.
- Immutable value objects for
Note,Chord, andScale - 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
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"Pass played pitches to identifyChords or identifyPianoChords. Both accept note strings with
octaves, Note objects, and MIDI numbers. Guitar input follows the tuning's string order: null is
muted, 0 is open, and a positive integer is a fret.
import {
identifyChords,
identifyGuitarChords,
identifyPianoChords,
guitarFingeringsFor,
pianoVoicingsFor,
keyboardRange,
} from 'octavian';
identifyGuitarChords([null, 3, 2, 0, 1, 0])[0]?.name; // "C"
identifyPianoChords([64, 67, 72])[0]?.name; // "C/E"
identifyChords(['C3', 'E3', 'G3', 'A3'], { key: 'A minor' })[0]?.name; // "Am7/C"
const tuning = { strings: ['D2', 'A2', 'D3', 'G3', 'B3', 'E4'] };
for (const fingering of guitarFingeringsFor('D', { tuning, maxFret: 24 })) {
console.log(fingering.frets, fingering.fingers, fingering.barres);
break; // Stop whenever you have enough results.
}
for (const voicing of pianoVoicingsFor('Cmaj7', {
range: keyboardRange(48, 84),
hands: 'both',
})) {
console.log(voicing.leftHand, voicing.rightHand);
break;
}Recognition returns immutable candidates with name, root, canonical suffix, actual bass and
notes, omittedIntervals, match, and catalog aliases. Exact matches precede practical
omissions. Key context, root-position bass, chord size, spelling, and name break ties. Alternative
interpretations and enharmonic names remain available. Empty or unmatched input returns [];
malformed input throws.
Practical omissions are enabled by default: the perfect fifth may be absent except in power and
suspended chords; eleventh and thirteenth chords may omit the ninth, and thirteenth chords may omit
the unaltered eleventh. Other tones remain required. Set omissions: 'none' for complete chords.
The catalog includes power chords (C5) and suspended sevenths (C7sus2, C7sus4, or C7sus).
Rootless and non-chord-tone slash interpretations are excluded.
Guitar generation defaults to standard tuning, frets 0..24, four available fingers, barres
enabled, and a maximum stopped-fret difference of three. Configure tuning, minFret, maxFret,
availableFingers, allowBarres, and maxFretSpan. Open strings consume no fingers. Barre
endpoints sound at the barre fret; intervening strings may be stopped higher, but cannot be open,
muted, or stopped lower. Results include one preferred finger assignment per shape.
Piano generation defaults to 88 keys, five notes per hand, and twelve semitones of reach per hand.
Configure range, hands, maxNotesPerHand, and maxHandSpan. Hands never cross; 'both'
includes one-hand voicings too. Results contain keys grouped by hand without numbered fingers. Both
generators allow inversions unless the supplied chord specifies a slash bass.
Generators validate and snapshot inputs when called, then lazily enumerate every result within their constraints without truncation. Guitar order is lowest stopped fret, then fret pattern; piano order is lowest pitch, then keys and hand split. These are deterministic search orders, not difficulty rankings. Avoid collecting an unrestricted search into an array. Playability follows the documented conservative model and remains dependent on the player's reach and technique.
Recognition also retains supported double- and triple-accidental spellings explicitly supplied in
the input, while preferring conventional names. Custom tunings accept the same inputs as
Note.create, including Note objects and serialized notes.
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 brandednumberin the range0..127.Frequency: A brandednumberin hertz.Semitones: A branded integer semitone distance.Octave: A branded octave value in the range-1..9. UsecreateOctave(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 ofFiguredBassFigureobjects representing a figured-bass stack.
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"]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"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 spellingconst 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)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)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] — iterableThe 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; // 6Chord 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; // 4Inversions:
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 voicingChord 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"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; // 7Navigation 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 rootDegrees 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'); // falseThe 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)); // 69For 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); // 432import {
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.
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).
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 constLow-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#'); // 1import { CHORD_SYMBOLS, resolveChordSuffix } from 'octavian';
CHORD_SYMBOLS; // ["m", "m7", "maj7", "dim", ...] — short display symbols
resolveChordSuffix('m7'); // "minorSeventh"
resolveChordSuffix('Δ7'); // "majorSeventh"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 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' }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')); // 1cMajor.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')); // truecMajor.transpose('majorSecond').toString(); // "D major"
cMajor.transposeBy(7).toString(); // "G major"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.
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"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.
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)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'); // trueCadenceInput 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' }]Run the local quality gates with Bun:
bun run typecheck
bun run lint
bun test
bun run build
bun run validate