@benjamindehli/music-utils
v4.4.3
Published
Detect chord names from MIDI note input
Readme
@benjamindehli/music-utils
Music theory utilities for detecting chords and scales from MIDI note input. Zero dependencies.
Installation
npm install @benjamindehli/music-utilsQuick start
import { getChordsFromSelectedNotes, getScalesFromSelectedNotes } from "@benjamindehli/music-utils";
// Detect chords from held MIDI notes (C, E, G = C major)
const chords = getChordsFromSelectedNotes([60, 64, 67]);
// → [ChordMatch { chord: Chord { rootNote: C, chordType: major }, matchType: "exactRoot" }, ...]
// Detect scales from held MIDI notes (C major scale)
const scales = getScalesFromSelectedNotes([60, 62, 64, 65, 67, 69, 71]);
// → [ScaleMatch { scale: Scale { rootNote: C, scaleType: "major scale" }, matchType: "exactRoot" }, ...]Chord detection
getChordsFromSelectedNotes(noteNumbers)
Takes an array of absolute MIDI note numbers and returns all matching chords, ranked by match quality.
import { getChordsFromSelectedNotes } from "@benjamindehli/music-utils";
const results = getChordsFromSelectedNotes([60, 64, 67]); // C, E, G
for (const { chord, matchType } of results) {
console.log(`${chord.rootNote?.name} ${chord.chordType?.name} (${matchType})`);
// "C major (exactRoot)"
}Each result is a ChordMatch with a matchType that describes how the notes relate to the chord:
| matchType | Description |
|---|---|
| exactRoot | Notes match the chord exactly, with the root as the lowest note |
| invertedRoot | Notes match the chord, but in an inversion |
| nonRoot | Notes match a chord type rooted on a different note |
| slashChord | Notes match a chord with an explicit bass note (e.g. C/E) |
Slash chords include a bassNote on the chord object:
const results = getChordsFromSelectedNotes([64, 67, 72]); // E, G, C
const slash = results.find((r) => r.matchType === "slashChord");
console.log(slash?.chord.bassNote?.name); // "E"
// → C/EScale detection
getScalesFromSelectedNotes(noteNumbers)
Takes an array of absolute MIDI note numbers and returns all matching scales.
import { getScalesFromSelectedNotes } from "@benjamindehli/music-utils";
const results = getScalesFromSelectedNotes([60, 62, 64, 65, 67, 69, 71]); // C major
for (const { scale, matchType } of results) {
console.log(`${scale.rootNote?.name} ${scale.scaleType?.name} (${matchType})`);
// "C major scale (exactRoot)"
}matchType is either "exactRoot" (the lowest note is the scale root) or "nonRoot".
Note spelling (sharps vs flats)
A pitch class has several valid names (A# = Bb), and the correct one depends on context — in a diatonic scale each letter A–G is used once, and every note's letter is fixed by its interval above the root. Scale.getNotes() and Chord.getNotes() return notes spelled this way, so an F major scale reads Bb, not A#.
const scale = getScalesFromSelectedNotes([65, 67, 69, 70, 72, 74, 76])[0]?.scale; // F major
scale?.getNotes().map((n) => n.name);
// → ["F", "G", "A", "Bb", "C", "D", "E"]
const chord = getChordsFromSelectedNotes([65, 68, 72])[0]?.chord; // F minor
chord?.getNotes().map((n) => n.name);
// → ["F", "Ab", "C"] (not "F", "G#", "C")This also applies to the rootNote of a detected Scale or Chord (and a slash chord's bassNote): the root is spelled with the fewest accidentals for its type, so a detected Gb major scale reports rootNote.name === "Gb" (not "F#"), while an Eb minor chord reports "Eb" (not "D#"). The Note.number (pitch class) is unchanged, so this never affects detection — only naming.
const scale = getScalesFromSelectedNotes([66, 68, 70, 71, 73, 75, 77])[0]?.scale; // Gb major
scale?.rootNote?.name; // "Gb"The underlying getSpelledNotes(rootPitchClass, halfSteps, preferredRootName?) helper is exported for direct use; preferredRootName (e.g. "F#") pins the tonic spelling.
Web MIDI integration
The Midi class wraps the Web MIDI API and wires up your message handler to all connected devices.
import { Midi, getChordsFromSelectedNotes } from "@benjamindehli/music-utils";
const midi = new Midi();
const heldNotes = new Set<number>();
midi.init((event: MIDIMessageEvent) => {
const [status, note, velocity] = event.data;
const isNoteOn = (status & 0xf0) === 0x90 && velocity > 0;
const isNoteOff = (status & 0xf0) === 0x80 || ((status & 0xf0) === 0x90 && velocity === 0);
if (isNoteOn) heldNotes.add(note);
if (isNoteOff) heldNotes.delete(note);
const chords = getChordsFromSelectedNotes([...heldNotes]);
console.log(chords);
});API reference
Helper functions
| Function | Description |
|---|---|
| getChordsFromSelectedNotes(noteNumbers) | Detect chords from MIDI note numbers |
| getScalesFromSelectedNotes(noteNumbers) | Detect scales from MIDI note numbers |
| getNoteFromNoteNumber(noteNumber) | Look up a Note object from a MIDI note number |
| normalizeHalfStep(noteNumber) | Reduce a MIDI note number to a pitch class (0–11) |
| normalizeHalfSteps(noteNumbers) | Reduce an array to unique, sorted pitch classes (0–11) |
| getRelativeNoteNumber(noteNumber, rootNoteNumber) | Get semitone distance from a root note |
| getAbsoluteNoteNumber(relativeNoteNumber, rootNoteNumber) | Convert a relative interval back to a pitch class |
| getSpelledNotes(rootPitchClass, halfSteps, preferredRootName?) | Spell a scale/chord's notes with correct sharps/flats |
| parseNoteName(name) | Parse a note name (e.g. "Bb") into a letter index + accidental |
Classes
ChordMatch
| Property | Type | Description |
|---|---|---|
| chord | Chord | The matched chord |
| matchType | "exactRoot" \| "invertedRoot" \| "nonRoot" \| "slashChord" | How the notes relate to the chord |
Chord
| Property | Type | Description |
|---|---|---|
| rootNote | Note | The chord's root note |
| chordType | ChordType | The chord quality (major, minor7, etc.) |
| bassNote | Note \| undefined | The bass note for slash chords |
| getNotes() | () => Note[] | The chord's notes, correctly spelled relative to the root |
ScaleMatch
| Property | Type | Description |
|---|---|---|
| scale | Scale | The matched scale |
| matchType | "exactRoot" \| "nonRoot" | Whether the lowest note is the scale root |
Scale
| Property | Type | Description |
|---|---|---|
| rootNote | Note | The scale's root note |
| scaleType | ScaleType | The scale type (major scale, dorian mode, etc.) |
| getNotes() | () => Note[] | The scale's notes, correctly spelled (sharps or flats per key) |
Note
| Property | Type | Description |
|---|---|---|
| name | string | Note name, e.g. "C", "C#" |
| number | number | Pitch class 0–11 |
NoteSelection (base for ChordType / ScaleType)
| Property | Type | Description |
|---|---|---|
| name | string | e.g. "major", "natural minor scale" |
| halfSteps | number[] | Semitone intervals that define the chord or scale |
| getParsedHalfSteps() | () => number[] | Returns normalized (0–11), deduplicated, sorted half steps |
Data
The library ships with pre-built arrays you can import directly:
import { chordTypes, scaleTypes, notes, intervals } from "@benjamindehli/music-utils";| Export | Count | Description |
|---|---|---|
| chordTypes | 130 | ChordType[] — triads, sevenths, ninths, extended, altered, suspended, slash, and more |
| scaleTypes | 60+ | ScaleType[] — modes, pentatonics, blues, bebop, exotic/world scales |
| notes | 12 | Note[] — chromatic notes C through B (pitch classes 0–11) |
| intervals | 26 | Interval[] — P1 through A15, each with semitone count and full name |
Deep imports
All modules are individually accessible for tree-shaking in environments that need it:
import Chord from "@benjamindehli/music-utils/classes/Chord";
import { getChordsFromSelectedNotes } from "@benjamindehli/music-utils/helpers/matchHelpers";
import chordTypes from "@benjamindehli/music-utils/data/chordTypes";Available paths: classes/Chord, classes/ChordType, classes/Interval, classes/Midi, classes/Note, classes/NoteSelection, classes/Scale, classes/ScaleType, classes/SelectionType, data/chordTypes, data/intervals, data/notes, data/scaleTypes, data/selectionTypes, helpers/matchHelpers, helpers/noteHelpers, helpers/spellingHelpers.
Formats
Ships as both ESM and CommonJS. Node.js and bundlers pick the right format automatically.
