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

@miadi/ava8-core

v0.3.1

Published

Headless ABC-notation music core absorbed from jgwill/Ava8: ABC text model, ABC->notes transcription, notes<->MIDI, instruments, tempo, a swappable glyph cosmology and symphony composition. ESM only. No DOM, no React.

Readme

@miadi/ava8-core

The headless music layer of Ava8 — ABC notation text model, ABC→notes transcription, notes↔MIDI, an instrument registry, tempo naming, a swappable glyph cosmology, and symphony composition.

No window, no document, no React, no abcjs. One runtime dependency: midi-writer-js, loaded lazily so that importing this package touches nothing outside it.

Rendering and playback live one layer up in @miadi/ava8-abcjs; components live in @miadi/ava8-react. This package is the part that has no opinion about where it runs.

0.2.0

Breaking: notesToMidiBytes is now async. It returns Promise<Uint8Array>; await it, or chain .then().

-const bytes = notesToMidiBytes(notes, { tempo: 84 })
+const bytes = await notesToMidiBytes(notes, { tempo: 84 })

[email protected] is pure CommonJS — its build/index.js ends in module.exports = main and declares no ESM export, even though the package's exports map serves that same file under the import condition. Statically importing it made this whole package fail to link in a browser and in an ESM-first bundler, with does not provide an export named 'default', before a line of code ran. 0.1.0's claim that it "imports cleanly … in a browser" was false, and it broke @miadi/ava8-abcjs transitively.

0.2.0 moves that import inside the one function that needs it, which is what makes the function async. There is no notesToMidiBytesSync: a synchronous path would need either a top-level import (the bug) or node:module's createRequire (a node-only import in a package that must run in a browser).

Also new in 0.2.0:

  • notesToMidiBytes takes chords, rests and per-note durations and velocities — see Notes and MIDI. Every string[] that worked in 0.1.0 still works and still produces the same bytes.
  • abcBodyToNotes transcribes an ABC tune into those note specs, so a melody is written once instead of twice.
  • UnsupportedAbcError, NoteSpec, AbcNotesOptions, abcKeyAccidentals.

ESM only

This package is "type": "module" and ships no CommonJS build. Its exports map declares an import condition and nothing else.

  • require('@miadi/ava8-core') fails with ERR_PACKAGE_PATH_NOT_EXPORTED.
  • Under TypeScript's moduleResolution: node16 / nodenext, importing it from a CommonJS file fails at compile time with TS1479 ("the current file is a CommonJS module") and, if you reach for a top-level await, TS1541.

To consume it you need one of: a bundler (Vite, esbuild, Rollup, webpack 5, Next.js), "type": "module" in your own package.json, an .mjs file extension, or a runtime-only await import('@miadi/ava8-core') from CommonJS.

Node 18.17 or newer. Browsers: any that support ES modules — with one extra import-map entry that only MIDI export needs, below.

Install

npm i @miadi/ava8-core
# or: pnpm add @miadi/ava8-core   /   yarn add @miadi/ava8-core

Browser, without a bundler

A bundler (Vite, webpack, esbuild, Next.js) resolves everything this package reaches for, and there is nothing to configure. A plain page with an import map has to resolve two specifiers itself, and they are not equally urgent:

<script type="importmap">
{
  "imports": {
    "@miadi/ava8-core": "/node_modules/@miadi/ava8-core/dist/index.js",
    "midi-writer-js": "https://cdn.jsdelivr.net/npm/[email protected]/+esm"
  }
}
</script>

The package itself is the only entry you need to import it and use it. The ABC model, transcription, notes, pitches, instruments, tempo, glyphs and symphony composition all run with nothing else mapped — engraving and playback, one layer up in @miadi/ava8-abcjs, need nothing else either.

midi-writer-js is needed by exactly one function. notesToMidiBytes reaches its CommonJS dependency through a dynamic import() — deliberately, so that a CommonJS module never enters this package's ESM graph (see Why notesToMidiBytes is async). That defers the resolution to the moment MIDI bytes are asked for, so a missing mapping is not a load error but a call-time one:

await notesToMidiBytes(['C4'])
// MidiWriterUnavailableError: midi-writer-js could not be loaded:
// Failed to resolve module specifier 'midi-writer-js'. notesToMidiBytes resolves it
// with a dynamic import() at call time, so it must be resolvable from @miadi/ava8-core.
// A bundler does this for you; a browser page with no bundler needs the specifier in
// its import map: "midi-writer-js": "https://cdn.jsdelivr.net/npm/[email protected]/+esm"

The CDN copy above is verified against [email protected]. To stay offline, point at the installed one instead — build/index.browser.js is the ESM build in the same tarball:

"midi-writer-js": "/node_modules/midi-writer-js/build/index.browser.js"

Where that file sits under node_modules depends on your installer: npm and yarn hoist it to the root, pnpm nests it under the package that depends on it. example/vanilla/index.html in @miadi/ava8 probes both before it writes its map, and is the working page to copy from.

The swappable cosmology

The prototype's music knowledge was fused to four glyphs named spiral, puzzle, feather and crystal. Here that data is the default, not the design:

import { loadMusicData, resetMusicData, glyphIds, instruments } from '@miadi/ava8-core'

loadMusicData({
  glyphs: {
    ebb:   { name: 'Ebb',   emoji: '🌊', color: '#0ea5e9', description: 'Water withdrawing',
             notes: ['D4', 'A3', 'F3'], abcNotation: 'X:1\nT:Ebb\nM:3/4\nL:1/8\nK:Dm\nD2A,2F,2|' },
    flood: { name: 'Flood', emoji: '🌕', color: '#f97316', description: 'Water returning',
             notes: ['F3', 'A3', 'D4'], abcNotation: 'X:1\nT:Flood\nM:3/4\nL:1/8\nK:Dm\nF,2A,2D2|' },
  },
  instruments: { reed: { id: 'reed', name: 'Reed', icon: '🎋', description: 'A single reed',
                         oscillatorType: 'sawtooth',
                         envelope: { attack: 0.1, decay: 0.2, sustain: 0.8, release: 0.4 } } },
  glyphInstrumentSettings: { ebb: { oscillatorType: 'triangle' } },
})

glyphIds()                     // ['ebb', 'flood']
instruments.map((i) => i.id)   // ['reed']

resetMusicData()
glyphIds()                     // ['spiral', 'puzzle', 'feather', 'crystal']

loadMusicData copies what you give it, so mutating your object afterwards changes nothing here. A load that cannot be used throws InvalidMusicDataError and leaves the previous cosmology in force.

How instruments stays reactive — an ESM live binding

Everything derived from the cosmology is read at call time, never frozen at module load:

| Export | Mechanism | | --- | --- | | glyphs(), getGlyph(), glyphIds(), nextGlyph(), glyphInstrumentSettings() | accessor functions — they read the store on every call | | instruments | an ESM live binding (export let, reassigned when the cosmology changes) |

import { instruments } binds the export cell rather than copying the array, so after loadMusicData() the same imported identifier already points at the new registry — no re-import, no getter call. Namespace access (const api = await import('@miadi/ava8-core'); api.instruments) is live for the same reason: ESM namespace properties are getters. Because imported bindings are read-only, consumers cannot assign to it.

ABC text model — src/abc.ts

import { parseAbcHeaders, setAbcHeader, validateAbc, splitAbcTunes, joinAbcTunes } from '@miadi/ava8-core'

const tune = 'X:1\nT:The Spiral Awakens\nM:4/4\nL:1/8\nK:Cmin\nC2E2G2c2|G2E2C4|'

parseAbcHeaders(tune)
// { index: '1', title: 'The Spiral Awakens', meter: '4/4', unitNoteLength: '1/8', key: 'Cmin' }

setAbcHeader(tune, 'Q', '84')
// 'X:1\nT:The Spiral Awakens\nM:4/4\nL:1/8\nQ:84\nK:Cmin\nC2E2G2c2|G2E2C4|'
//  — inserted at its canonical rank, always before K:

validateAbc('X:1\nT:Keyless\nM:4/4')
// { valid: false, errors: ['Missing K: key header', ...], warnings: [] }

joinAbcTunes(splitAbcTunes(multiTuneFile))   // round-trips, X: renumbered 1..n

validateAbc is structural only — no abcjs, no music parsing. Blocking errors: no X:, no K:, tune body before K:, empty body. Non-blocking warnings: unknown header letters, missing T:, missing M:.

Q: is read best-effort from all three notations — Q:120, Q:1/4=96, Q:"Allegro" 1/4=168.

ABC → notes — src/abc-notes.ts

abcBodyToNotes is the path between this package's two representations of the same melody. Before it, a consumer wrote every tune twice — once as ABC text for the score, once as a string[] for MIDI — with nothing checking the two agreed.

import { abcBodyToNotes, notesToMidiBytes, UnsupportedAbcError } from '@miadi/ava8-core'

abcBodyToNotes('X:1\nM:4/4\nL:1/8\nK:C\nC2E2G2c2|')
// [ { pitch: 'C4', duration: '4' }, { pitch: 'E4', duration: '4' },
//   { pitch: 'G4', duration: '4' }, { pitch: 'C5', duration: '4' } ]

abcBodyToNotes('X:1\nL:1/8\nK:D\n[DFA]2 z2 d4-d4|')
// [ { pitch: ['D4', 'F#4', 'A4'], duration: '4' },   — a chord; K:D sharpened the F
//   { pitch: [], duration: '4', rest: true },        — a rest
//   { pitch: 'D5', duration: '1' } ]                 — the tie merged two halves

const bytes = await notesToMidiBytes(abcBodyToNotes(tune), { tempo: 84 })

It accepts a whole tune or a bare body. K: supplies the accidentals and L: the unit note length — falling back to M: and then to 1/8, as ABC v2.1 specifies.

Transcribed: note letters with octave marks (C is middle C, c is C5, C, is C3, c' is C6), accidentals ^ _ = ^^ __ with measure-long persistence, lengths C2 C/2 C/ C3/2, rests z and x, chords [CEG] and [CEG]2, ties between identical pitches, barlines and repeat marks as separators, slurs, decorations, "..." annotations, w: lyric lines.

Anything else raises UnsupportedAbcError, whose construct names what was found — grace notes, tuplets, broken rhythm, multi-measure rests, voice overlays, inline [K:…], a body K:/L:/M:/V:/P: line. It refuses rather than guessing, because a dropped grace note or a mis-read tuplet changes the music without saying so.

Two things it does not do: repeats are read as separators, not expanded; and Q: is not consulted — tempo is a render option, not a transcription.

abcBodyToNotes(abc, { applyKeySignature: false }) transcribes letter for letter. That is how the shipped cosmology spells its notes arrays — naturals even under K:Cmin and K:D.

What the shipped cosmology's own data says

src/data/music-data.json is absorbed verbatim from the prototype, where each glyph's abcNotation and notes array were written by hand and never compared. Transcribing the first and diffing it against the second is the first check they have ever had, and three of the four disagree. The test suite asserts these divergences exactly rather than smoothing them over — they are facts about the data, not about the transcriber:

| Glyph | notes vs. its own abcNotation | | --- | --- | | spiral | agree, note for note | | puzzle | the array sits one octave above the ABC (G, is G3, listed as G4) | | feather | the same octave shift | | crystal | same octave, but note 5 is listed as F5 where the ABC writes F |

All four arrays spell naturals, so none of them carries its tune's key signature: K:Cmin flattens spiral's E, K:D sharpens crystal's F, K:A sharpens feather's C.

When each note sounds — src/timeline.ts

abcBodyToNotes computes every duration exactly, in ticks, and then names each count as a midi-writer-js token — the right answer for a MIDI writer, and no answer at all for anything that has to know when. noteTimeline reads the same ticks through the same parse and states them as milliseconds:

import { noteTimeline, timelineDurationMs, noteAtMs } from '@miadi/ava8-core'

const timeline = noteTimeline('X:1\nM:4/4\nL:1/4\nQ:120\nK:C\nCDEF|')
// [ { index: 0, startMs: 0,    durationMs: 500, pitch: ['C4'], rest: false },
//   { index: 1, startMs: 500,  durationMs: 500, pitch: ['D4'], rest: false },
//   { index: 2, startMs: 1000, durationMs: 500, pitch: ['E4'], rest: false },
//   { index: 3, startMs: 1500, durationMs: 500, pitch: ['F4'], rest: false } ]

timelineDurationMs(timeline)   // 2000
noteAtMs(timeline, 500)        // the D — the interval is [startMs, startMs + durationMs)
noteAtMs(timeline, 2000)       // null — the piece is over
interface NoteOnset { index: number; startMs: number; durationMs: number; pitch: string[]; rest: boolean }

function noteTimeline(abc: string, opts?: NoteTimelineOptions): NoteOnset[]
function timelineDurationMs(timeline: NoteOnset[]): number
function noteAtMs(timeline: NoteOnset[], ms: number): NoteOnset | null
  • index is the index into abcBodyToNotes(abc, opts). Both functions read the tune through one parse, so timeline[i] describes notes[i] and the two lengths are equal for every input — by construction, not by coincidence.
  • Tempo resolution: opts.tempo, then the tune's own Q:, then ava8Config.defaults.tempo (84), folded through clampTempo. NoteTimelineOptions extends AbcNotesOptions, so applyKeySignature works here too.
  • Rests are entriesrest: true, pitch: [], a real durationMs. A millisecond inside a rest returns the rest, because silence is something the piece is doing.
  • A chord is one entry carrying every sounding pitch, at one duration.
  • A tie is one entry whose durationMs covers both halves, and the note after it starts when the tie ends.
  • noteAtMs is a binary search, because an animation asks it once per frame. null before the first onset and at or after the end; a non-finite ms is a TypeError rather than a silent "the piece is over".

Onsets are computed from each note's cumulative tick offset, never by summing the previous durationMs values. Ticks are exact; milliseconds at an arbitrary tempo are not, and a running sum carries every rounding forward until the cursor lands on the wrong note near the end of a long piece. Over 240 eighth notes at 84 bpm the two methods already differ, and the drifted value selects the note before the one that sounds — the suite asserts that difference rather than describing it.

Whatever abcBodyToNotes refuses, this refuses identically: UnsupportedAbcError naming the construct, InvalidNoteError, TypeError. Nothing is caught and degraded into a timeline missing a sound.

Notes and MIDI — src/notes.ts, src/midi.ts

import { noteToMidiNumber, midiNumberToNote, transposeNote, noteToFrequency,
         notesToMidiBytes, InvalidNoteError } from '@miadi/ava8-core'

noteToMidiNumber('C4')    // 60
noteToMidiNumber('C#4')   // 61
noteToMidiNumber('Bb3')   // 58   — flats, ♭/♯, double sharps, negative octaves
midiNumberToNote(58)      // 'A#3' — sharps spelling
transposeNote('Bb3', 2)   // 'C4'
noteToFrequency('A4')     // 440

noteToMidiNumber('H9')    // throws InvalidNoteError { value: 'H9' }

const bytes = await notesToMidiBytes(['C4', 'E4', 'G4', 'C5'], { tempo: 84, program: 10 })
bytes instanceof Uint8Array           // true — never a Blob
[...bytes.slice(0, 4)]                // [0x4d, 0x54, 0x68, 0x64] — 'MThd'

A sequence entry is either a note name or a NoteSpec:

type NoteSpec =
  | string
  | { pitch: string | string[]; duration?: string; velocity?: number; rest?: boolean }

function notesToMidiBytes(notes: NoteSpec[], opts?: MidiRenderOptions): Promise<Uint8Array>
  • pitch: string[] is a chord — one event, every pitch sounding together.
  • rest: true is a rest of duration (pass pitch: []). Consecutive rests add up; a trailing rest has no note left to delay and changes nothing.
  • duration and velocity override MidiRenderOptions for that event only.
await notesToMidiBytes([
  { pitch: ['C4', 'E4', 'G4'], duration: '2' },   // a half-note chord
  { pitch: [], duration: '4', rest: true },       // a quarter rest
  'A4',                                           // opts.duration, opts.velocity
  { pitch: ['D4', 'F4'], duration: '8', velocity: 40 },
])

Duration tokens are midi-writer-js': '1' whole, '2' half, '4' quarter, '8' eighth, 'd4' dotted quarter, 'T96' an explicit 96 ticks at 128 ticks per quarter note. abcBodyToNotes emits exactly these.

Writing the file is the caller's business: fs.writeFile(path, bytes) in node, new Blob([bytes], { type: 'audio/midi' }) in a browser. Core does not touch document or URL.createObjectURL.

Instruments and tempo — src/instruments.ts, src/tempo.ts

import { getInstrumentById, instrumentMidiProgram, tempoName, clampTempo } from '@miadi/ava8-core'

getInstrumentById('triangle').name   // 'Wooden Flute'
getInstrumentById('nonsense').id     // 'sine' — falls back to the first instrument
instrumentMidiProgram('triangle')    // 73 (GM Flute)

tempoName(59)    // 'Largo'
tempoName(60)    // 'Adagio'
clampTempo(9999) // 240

Glyphs and symphony — src/glyphs.ts, src/symphony.ts

import { getGlyph, nextGlyph, buildSymphony, symphonyToAbc, symphonyDurationBeats } from '@miadi/ava8-core'

getGlyph('spiral').notes            // ['C4','E4','G4','C5','G4','E4','C4']
nextGlyph('crystal', 'left')        // 'spiral' — wraps in both directions

const symphony = buildSymphony({ title: 'Ava8', tempo: 108 })
symphonyToAbc(symphony)             // multi-tune ABC, X: renumbered 1, 2, 3, 4
symphonyDurationBeats(symphony)     // 29

'left' (a right-to-left swipe) advances, 'right' goes back — the prototype's gesture semantics, preserved. getGlyph on an unknown id throws UnknownGlyphError carrying the ids that are available.

Errors

Ava8Error is the base class, so one catch covers the package. InvalidNoteError (unparseable or out-of-range pitch, carrying value), UnknownGlyphError (id absent from the loaded cosmology, carrying id and available), UnsupportedAbcError (an ABC construct abcBodyToNotes will not guess at, carrying construct and sometimes position), InvalidMusicDataError (unusable loadMusicData argument) and MidiWriterUnavailableError (midi-writer-js could not be loaded when notesToMidiBytes asked for it, carrying specifier and the original failure as cause) extend it.

Absorbed from

Source prototype: jgwill/Ava8@3cf080a (branch main). The prototype is read only — nothing in this package writes back to it.

| Module here | Absorbed from | Change on the way in | | --- | --- | --- | | src/data/music-data.json | lib/music-data.json | copied verbatim — 4 glyphs, 5 instruments, 4 glyphInstrumentSettings | | src/config.ts ava8Config | lib/config.json | values unchanged; typed as Ava8Config instead of a raw JSON import | | src/config.ts loadMusicData / resetMusicData | (new) | the programmatic form of the CLI's MUSIC_DATA_PATH; the prototype had no way to swap the cosmology | | src/notes.ts | lib/midi-export.ts noteToMidiNumber | accepts flats/♭/♯/double sharps/negative octaves; throws InvalidNoteError where the prototype returned NaN | | src/midi.ts notesToMidiBytes | lib/midi-export.ts createMidiFile | returns Uint8Array, not a Blob; async, so the CommonJS dependency stays out of the module graph; chords, rests and per-note rhythm; optional GM program change; tempo clamped | | — | lib/midi-export.ts downloadMidi / exportGlyphAsMidi | not absorbed — DOM helpers, they belong in ava8-abcjs/ava8-react | | src/instruments.ts registry | lib/instruments.ts | OscillatorType/BiquadFilterType DOM lib globals replaced with Ava8OscillatorType/string; the array became a live binding instead of a module-load snapshot | | src/instruments.ts instrumentMidiProgram | components/echo-composer.tsx:251-266 getInstrumentMidiProgram | lifted verbatim out of a React component — it is data, not UI | | src/tempo.ts tempoName | components/echo-composer.tsx:176-185 formatTempo | same lift; boundary fixed (see below) | | src/glyphs.ts nextGlyph | lib/music-data.ts getNextGlyph | swipe semantics preserved; reads the loaded cosmology instead of the JSON import | | src/glyphs.ts accessors | lib/music-data.ts abcNotations/glyphNotes/glyphDescriptions/glyphColors/glyphEmojis/glyphNames | six parallel maps hard-coded to four glyph ids became glyphs() / getGlyph() over any cosmology | | src/glyphs.ts glyphInstrumentSettings | lib/instruments.ts getGlyphInstrumentSettings | fallback to { oscillatorType: 'sine' } preserved | | src/abc.ts, src/symphony.ts | (new) | the prototype passed ABC strings around raw and played one glyph at a time | | src/abc-notes.ts | (new) | the prototype kept each melody's ABC text and note array side by side, unchecked |

The tempo boundary, deliberately changed

formatTempo compared with strict < against each range's max, so 59 bpm read as "Adagio" even though the config the prototype itself shipped declares largo: { min: 40, max: 59 }. Every boundary value landed one range too high. Here each range owns its own max: tempoName(59) === 'Largo', tempoName(60) === 'Adagio'.

Local typings for midi-writer-js

[email protected] ships .d.ts files but its exports map declares no types condition, so moduleResolution: NodeNext cannot reach them. Its main.d.ts also declares an ESM export default while the runtime module ends in module.exports = main. src/midi-writer-js.d.ts models exactly the surface src/midi.ts uses, as a type-only import that the compiler erases. Nothing from it leaks into this package's public .d.tsnotesToMidiBytes returns a plain Promise<Uint8Array>, and the built dist/ names midi-writer-js in one place: the dynamic import() inside notesToMidiBytes.

Build and test

npm run build   # tsc -p tsconfig.build.json, strict
npm test        # build, then node --test test/*.test.mjs

The suite asserts bytes, strings, numbers and thrown errors — MIDI header bytes and tempo meta-events, note-on/note-off counts and delta times parsed back out of the emitted file, every MIDI number round-tripped through midiNumberToNote, header parsing against all four real glyph tunes, ABC transcription against those same tunes' notes arrays, every onset of a 240-note tune against an independently computed millisecond, every tempoName boundary, nextGlyph wraparound both ways, a full cosmology swap and restore, and X: renumbering. One test walks the built output and fails if any DOM global or unexpected import appears in it. Another registers a resolver hook that makes midi-writer-js unreachable, imports the whole package anyway, and fails if evaluating it needed the CommonJS dependency.