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

tutts

v0.1.0

Published

Renderer-neutral MIDI to guitar-tab fingering engine (TypeScript port of tuttut).

Downloads

52

Readme

tutts

Renderer-neutral MIDI→guitar-tab fingering engine. A from-scratch, idiomatic TypeScript port of tuttut's algorithm: it frames fingering as a Hidden Markov Model (fretboard positions = hidden states, note onsets = observations) and uses Viterbi to pick the sequence that is easiest to play under a fret-height/span/string-change cost metric.

The core is pure (zero runtime dependencies), ESM-only, and speaks Ableton's beat-based note model. It emits renderer-neutral structured tab data plus a built-in ASCII renderer; a thin adapter converts the structured data to a specific tablature renderer's format.

Install

pnpm add tutts

Usage

import { generateTab, Tuning } from "tutts";

const tab = generateTab({
  notes: [
    { midi: 64, startBeats: 0, durationBeats: 1 }, // E4
    { midi: 60, startBeats: 1, durationBeats: 1 }, // C4
  ],
  tuning: Tuning.standardGuitar(),
  // timeSignatures defaults to a single 4/4 at beat 0
});

console.log(tab.toAscii()); // ASCII tab as one string
tab.toLines();              // string[]: one line per string (blank line between systems)
tab.toSystems();            // string[][]: one block of string-lines per system
tab.data;                   // TabData: the renderer-neutral result

NoteModel is { midi, startBeats, durationBeats } (C4 = 60, time in quarter-note beats): the Ableton-native projection of the Extensions SDK's note shape, so notes from an Ableton clip drop in with no adapter.

Input options

generateTab(input) accepts:

| Option | Type | Default | Effect | | --- | --- | --- | --- | | notes | NoteModel[] | required | Unmuted notes, any order (sorted internally). | | tuning | Tuning | required | E.g. Tuning.standardGuitar(), Tuning.ukulele(), or new Tuning(["D4", "A3", ...], nfrets). | | timeSignatures | TimeSignatureChange[] | one 4/4 at beat 0 | Piecewise measure grid. Notes before the first entry's startBeats are ignored (the grid starts there). | | weights | DifficultyWeights | all 1 | Cost-metric weights. b is the Laplace scale and must be greater than 0 (validated; throws otherwise). | | tempo | number | unset | BPM. Only used to emit an optional seconds field on each event. | | quantizeGrid | "1/4" \| "1/8" \| "1/16" \| "1/32" | unset | Snap note onsets to the grid before chord grouping, so near-simultaneous notes merge into one chord. |

ASCII rendering

toAscii(), toLines(), and toSystems() all take an optional AsciiRenderOptions:

tab.toAscii({ maxWidth: 80, timeSignature: true });
  • maxWidth: wrap the tab into stacked systems no wider than this many columns. Each system repeats the string-name header. A measure is never split: a lone measure wider than maxWidth overflows rather than breaking. Omitted or <= 0 produces a single unwrapped system.
  • timeSignature: render the time signature (stacked numerator over denominator on the center rows; inline 4/4 on a 1-string tuning) in the measure where it takes effect, including every mid-tab change. Default false.

Fret cells use a uniform width (the widest fret number in the tab), so multi-digit frets never skew the beat grid. renderAsciiTab(data, tuning, opts) is also exported for rendering a TabData you already have.

Output: TabData

tab.data is renderer-neutral and carries everything a tablature renderer needs:

interface TabData {
  tuning: number[]; // string pitches, thin → thick
  measures: {
    events: {
      beats: number;
      measureTiming: number;            // [0, 1) within the measure
      seconds?: number;                 // present iff `tempo` was supplied
      timeSignatureChange?: [number, number];
      notes?: {
        string: number;                 // 0 = thinnest string
        fret: number;
        midi: number;                   // sounding pitch
        durationBeats: number;          // drives note values & rests downstream
        degree: string;                 // e.g. "F#"
        octave: string;                 // e.g. "4"
      }[];
    }[];
  }[];
}

An event whose chord has no physically possible fingering carries notes: [] (see divergence 2 below). durationBeats is passed through from the input untouched, including zero or negative values.

Feeding a tablature renderer

The core stops at renderer-neutral positions plus rhythmic onsets and durations; turning those into a specific renderer's format is adapter work (the tutts-alphatab sibling maps TabData to alphaTex). The adapter owns two renderer-specific concerns:

  • String numbering. This core indexes string 0 as the thinnest; alphaTex/Guitar Pro use a 1-based numbering where string 1 is the highest-pitched. The adapter reconciles the two.
  • Note values & rests. durationBeats is a beat count; the adapter decomposes it into the renderer's note-value tokens and inserts rests, beams, and ties.

Everything the adapter needs is on TabData: per note string, fret, midi, durationBeats; per event the onset beats; plus tuning, time signature(s), and optional tempo.

Divergences from tuttut

This port reproduces tuttut's algorithm, but deliberately fixes three reference behaviors:

  1. Notes at the clip end are never dropped. tuttut derives the clip length from raw note end times, so a zero-duration note at the end of the clip (or a note whose onset quantization pushes past the last note's end) silently vanishes. This port computes the clip end from snapped onsets and guarantees every onset lands in a measure.
  2. Unplayable chords render as empty events, not phantom notes. In the reference, a chord with no possible fingering keeps a -1 observation, which numpy's negative indexing resolves to the last chord's emission column: the output then contains notes borrowed from an unrelated chord. This port skips unplayable chords in the Viterbi observation sequence and emits the event with notes: []; adjacent playable chords transition directly across the gap.
  3. The first chord gets the easiest fingering, not the hardest. tuttut seeds the initial Viterbi state distribution with isolated difficulty (the inverse of easiness) where every later transition is weighted by easiness. The result is that the hardest fingering of the first chord is the most probable start (a melody of repeated open-string E4s begins at the 19th fret). This is a confirmed inversion bug in the reference; this port seeds the initial distribution with easiness.

Other reference quirks are kept faithfully: open strings are costless transitions, and note naming follows pretty_midi (including the low C-1 spelling).

Notes on fidelity and performance

A dev-only parity harness (parity/, not shipped) diffs output against the Python reference. Behavioral equivalence outside the divergences above is the bar; where multiple fingerings tie on cost, the chosen one may differ from Python without being wrong. The harness is expected to report diffs on the first chord of each case (divergence 3) and on any case involving unplayable chords or notes at the exact clip end (divergences 1 and 2).

Decoding cost scales as O(T·F²), where T is the number of onsets and F the total fingering count across distinct chords (the transition matrix is dense, F×F). Melodic clips stay small (F in the low hundreds). Chord-dense clips with many distinct chords grow quadratically and can reach seconds of compute; if you feed long, harmonically dense material, consider splitting it into sections.

License

MIT