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

@ugfoundation/swaralipi-js

v0.17.0

Published

JSON spec, Unicode-Bengali viewer, and Tone.js player for Rabindrasangeet swaralipi (আকারমাত্রিক notation).

Readme

npm downloads Total npm downloads @gitabitan_io on X

@ugfoundation/swaralipi-js — a JSON spec, a Unicode-Bengali viewer, a Tone.js player, and an archival-HTML converter for Rabindrasangeet swaralipi (Tagore's আকারমাত্রিক / akarmatrik notation).

The spec is the contract: write (or generate) a swaralipi JSON document, then hand the same document to the viewer, the player, or the printer. Built for gitabitan.io, published standalone under GPL-2.0-only.

pnpm add @ugfoundation/swaralipi-js      # or npm / yarn

Quick start

The smallest useful program: validate a document and render it.

import { parseSwaralipi } from '@ugfoundation/swaralipi-js/spec';
import { renderToHtml, SWARALIPI_CSS } from '@ugfoundation/swaralipi-js/viewer';

const doc = parseSwaralipi({
  version: '0.1.0',
  meta: { title: 'অকারণে', taal: { id: 'dadra', numBeats: 6 }, tonic: 'C' },
  parts: [{ name: 'asthayi', rows: [{ notes: [
    { svara: ['madhyam', 'm'], lyric: 'অ' },
    { svara: ['madhyam', 'p'], lyric: 'কা' },
    { sustain: true, lyric: '৹' },
    { bar: 'single' },
  ] }] }],
});

document.head.insertAdjacentHTML('beforeend', `<style>${SWARALIPI_CSS}</style>`);
document.body.innerHTML = renderToHtml(doc, { lang: 'bn' });

Writing a swaralipi document

A document is a single JSON object: { version, meta, parts }. A part is a section (আস্থায়ী / অন্তরা …) holding rows (written lines); a row holds notes (the tokens below). Build it by hand, generate it from a tool, or produce it with the converter. Everything else in this package consumes this shape.

{
  "version": "0.1.0",
  "meta": { /* see below */ },
  "parts": [
    { "name": "asthayi", "rows": [ { "notes": [ /* tokens */ ] } ] }
  ]
}

meta

Only title is required. The rest is optional but drives playback and the header.

"meta": {
  "title": "অকারণে অকালে মোর পড়ল যখন ডাক",
  "taal": { "id": "dadra", "numBeats": 6, "bhaags": [3, 3], "sam": 0, "khaali": [3] },
  "taalName": "দাদরা",        // verbatim source name (use when `taal.id` is unknown)
  "tonic": "C",               // base pitch — "C", "C#"/"cs", "Db" all accepted
  "bpm": 144,                 // default tempo, matra/min (player only; viewer ignores it)
  "avartan": 2,               // আবর্তন count, when the source states it
  "notationBy": "…", "source": "…"   // provenance
}

svara — pitched notes

A pitch is a [octave, letter] tuple. Octaves: ati-mandra, mandra, madhyam, taar, ati-taar. Letters: s r g m p d n (d=ধৈবত/dha, n=নিষাদ/ni). Sharps/flats are flags, not letters:

{ "svara": ["madhyam", "g"] }                       // শুদ্ধ গ
{ "svara": ["madhyam", "g"], "komal": true }        // কোমল গ (flat)
{ "svara": ["madhyam", "m"], "tivra": true }        // কড়ি/তীব্র ম (sharp)
{ "svara": ["taar", "s"] }                          // taar সা (high)
{ "svara": ["mandra", "p"] }                        // mandra প (low)
{ "svara": ["madhyam", "r"], "microtone": "atikomal" }  // sub-komal: alpakomal | atikomal

duration — matra groups

Default duration is 1 মাত্রা. Use durationMatra for fractional or held notes; a run of fractions summing to a whole matra renders as one beamed group (nsfa → নর্সা):

{ "svara": ["madhyam", "n"], "durationMatra": 0.5 }   // half-matra
{ "svara": ["taar", "s"],    "durationMatra": 0.5 }   // …grouped with the one above
{ "svara": ["madhyam", "p"], "durationMatra": 2 }     // held two matras

halfMatra: true prints ঃ after its note; if it ends only the first half, the cell stays open (-ঃমঃ, সঃ গরঃ = one matra, one column). A hand-set ঃ or aakar overrides the source raw marks in that cell. aakar: false hides that note’s া on the sheet; duration and playback stay unchanged, and rests always show their sign.

sustain (আ-কার) and melisma

{ "sustain": true, "lyric": "৹" }                       // hold prev pitch+vowel (আ-কার)
{ "svara": ["taar", "s"], "sustain": true, "lyric": "৹" } // melisma: pitch moves, vowel continues

rest (বিরাম)

{ "rest": true, "durationMatra": 1 }                    // silence for one matra

bars — দাঁড়ি / দণ্ড

{ "bar": "dari" }     // ।   vibhag separator inside the cycle
{ "bar": "single" }   // I   end of one taal cycle (ফেরা)
{ "bar": "double" }   // II  section / return-to-asthayi boundary
{ "bar": "quad" }     // IIII end of the song

brackets — repeat / skip / alt

A bracket spans a range; the first note carries edge: 'open', the last edge: 'close'. repeat = {} গুন্ডবন্ধনী · skip = () omit on repeat · alt = [] changed-on-repeat.

{ "svara": ["madhyam", "s"], "brackets": [{ "kind": "repeat", "edge": "open" }] }
{ "svara": ["madhyam", "r"], "brackets": [{ "kind": "repeat", "edge": "close" }] }

kan (grace) and meend (glide)

{ "svara": ["madhyam", "n"], "kan": ["taar", "s"] }   // কণ grace before the note
{ "svara": ["madhyam", "p"], "meend": "start" }       // মীড় slur start …
{ "svara": ["madhyam", "g"], "meend": "end" }         // … and end
{ "svara": ["madhyam", "d"], "meend": "both" }        // ends one মীড় and starts the next

lyrics and annotations

lyric rides on the note (column-aligned, per-syllable). above / below attach recursive notes for alt-on-repeat passages, stop marks, or beat labels. An above-note’s own above draws a second line over it, as in স্বরবিতান; only rows that use it grow, and sam ticks / ‖ there count for playback too:

{ "svara": ["madhyam", "m"], "lyric": "অ",
  "above": [{ "svara": ["madhyam", "p"] }] }           // alt note printed above
{ "above": [{ "raw": "[qa", "above": [{ "raw": "1f" }] }] } // taal mark over [ ] notes

raw — lossless passthrough

Anything the converter can't decode is preserved verbatim in raw and rendered dimmed — the spec is lossless by construction, nothing is silently dropped. You rarely write raw by hand.

{ "raw": "w", "below": [/* … */] }

Validate what you wrote

import { parseSwaralipi, safeParseSwaralipi } from '@ugfoundation/swaralipi-js/spec';

parseSwaralipi(json);            // returns typed Swaralipi, throws ZodError if invalid
safeParseSwaralipi(json);        // { success, data | error } — no throw

See fixtures/ for worked documents — octaves, microtones, matra-durations, repeat-brackets, and the hand-checked akarone anchor.


Deploying the document

View: <swaralipi-sheet>

Renders the document as Unicode Bengali + a CSS grid — no images, no embedded font, no framework. Arcs, kan, bars, beat labels and annotations all render.

<script type="module">
  import { defineSwaralipiSheet } from '@ugfoundation/swaralipi-js/viewer';
  defineSwaralipiSheet();
</script>

<swaralipi-sheet src="/notation/4270.json" lang="bn"></swaralipi-sheet>

Pass a URL via src or set the doc property directly. lang is bn | hi | en (Devanagari for hi, roman + octave dots for en); hide-header; highlight('part.row.note') for player sync. Styles are shadow-DOM-encapsulated, themeable via --sw-font, --sw-size, --sw-cell-gap, --sw-matra, --sw-header-min, --sw-hl.

SSR / static sites (Astro etc.) — renderToHtml is a pure function (string in, string out, no DOM); emit it alongside SWARALIPI_CSS. See Quick start.

Responsive / zoom

The sheet is one shared grid with one natural width, so it has to be fitted to a narrow screen. Two levers run in order, because they cost different things:

  1. Column spacing — --sw-cell-gap plus the --sw-matra column floor are 25–35 % of the natural width, and giving them up costs only air between the columns (the alignment is untouched: every row still borrows the same tracks). Compressed only as far as the container demands, down to min-spacing.
  2. Type size — then the sheet's font-size scales (font-size, not transform — keeps print pagination intact), stopping at min-size px. Past that the block pans inside its own scroller rather than becoming unreadable.
<!-- how tight the columns may get (0–1, default 0.35) and the smallest
     notation size in px the fit may produce (default 12) -->
<swaralipi-sheet src="…" min-spacing="0.35" min-size="12"></swaralipi-sheet>
sheet.minSpacing = 0.2;  // tighter columns, bigger glyphs (attribute-reflected)
sheet.minSize = 14;      // never below 14px; scroll instead
sheet.minSpacing = 1;    // opt out of compression entirely (old behaviour)

Measured on a 390 px phone: a দাদরা page goes from 11.7 px to 15.5 px type and still fits without scrolling; a wide তিনতালা goes from 6.1 px to the 12 px floor and pans. The নাম/তাল/আবর্তন/স্বরলিপিকার header keeps its own floor (--sw-header-min, default 0.95rem) — it is prose with no columns to align, so it stays legible while the notation shrinks.

Build your own zoom UI (buttons, a slider) against the element:

sheet.zoom;          // current zoom factor, 1 = fit-to-width
sheet.zoomIn();       // zoom *= ZOOM_STEP (1.2), clamped
sheet.zoomOut();      // zoom /= ZOOM_STEP, clamped
sheet.resetZoom();    // back to fit-to-width
sheet.zoom = 1.5;     // or set an explicit factor directly (clamped, re-fits)

MIN_ZOOM/MAX_ZOOM/ZOOM_STEP are exported from /viewer so a custom slider's bounds match the clamp. Zoom resets to fit-to-width on the next doc/lang change; a plain resize (rotating the phone) keeps it. Past 100% the sheet pans via native scrolling — same as zooming a photo. Pinch-to-zoom is left to the browser's own page zoom (we never set touch-action: none, so it isn't blocked); the buttons above are the explicit, always-available control.

SSR/manual-DOM consumers without the custom element call the same mechanism directly: fitSwaralipi(container, minScale?, zoom?, minSpacing?, minSize?) (all optional, /viewer export). minScale is a second, relative floor used by the print path, where paper cannot scroll.

Print / PDF

import { printSwaralipi, renderPrintDocument } from '@ugfoundation/swaralipi-js/viewer';

printSwaralipi(doc, { lang: 'bn', title: 'song-name' });   // hidden-iframe → print dialog
const page = renderPrintDocument(doc);                     // full HTML string (new tab / headless)

document.querySelector('swaralipi-sheet').print() is the element shortcut. PDF fidelity comes from the browser's own engine — the same Indic shaping (reph, conjuncts, matra) and CSS-drawn marks as the screen — so we deliberately avoid html2canvas/jsPDF, which can't shape Indic text.

Any paper, any orientation. The print is fitted to the width of the paper picked in the dialog and centred, and the browser breaks the pages for its height — again each time the paper or orientation changes.

The watermark is on every page, in every engine. WebKit (Safari, and every iOS browser) paints a position: fixed box once, centred on the whole document (bug 6790), so there the print decides its own page breaks and pins one mark per page. A consumer printing its own document can do the same: paginate(doc, { atoms, keep?, starts?, pageHeight, mark: 'sw-page-mark' }) (with watermarkCss(text) and printPageHeight(widthPx, heightMm), which is shorter in WebKit, whose page size CSS cannot set).

Play: SwaralipiPlayer

A Tone.js player turns a document into audio — melody on santoor / harmonium / violin / flute / grand piano, a tabla and a tanpura drone from tabla-js — and drives the viewer's highlight in sync. Tone and tabla-js are dynamically imported on first play(), so importing the module never touches AudioContext — safe in Node/SSR.

import { SwaralipiPlayer } from '@ugfoundation/swaralipi-js/player';

const player = new SwaralipiPlayer();        // zero-config: samples from the project CDN
player.load(doc);
player.bindSheet(document.querySelector('swaralipi-sheet'), { autoScroll: true });

playButton.onclick = () => player.play();    // must be a user gesture (AudioContext)

Controls: setTonic('C#'), setBpm(120), setInstrument(id), setTaal(id|null), setVariation(name|null), setFillers('auto'|'light'|'busy'|'off'), setTihai(bool), setRhythm('tabla'|'drums'|'metronome'|'none'), setTanpura(bool), setCountIn(bool), pause() / stop() / dispose(). Each song loads only the samples covering its pitch range.

  • Self-host samples: override soundsBaseUrl (layout: santoor/<note>.mp3, tabla/bols/<stroke>.wav, tanpura/strings/<set>-<string>.wav, metronome/metro2.mp3).
  • Add instruments: customInstruments: { sitar: { label, urls, baseUrl } }.
  • Liveness: await player.checkInstruments() → { id: available }, to grey out picker options whose samples are down.

The pure, audio-free scheduler is exported too (buildSchedule, planRhythm, svaraToMidi, parseTonic, INSTRUMENTS, …) for a custom transport.

The tabla is synthesised from single bols. tabla-js keeps each theka as data and plays it from one sample per stroke, at any tempo and in tune with the song's Sa. swaralipi-js only conducts it, so every stroke lands on its matra (measured: 0.000 ms off the transport):

  • Taals: the 24 that gitabitan.io uses and tabla-js has (TABLA_TAALS). Any other id still plays: tabla-js finds a taal of the same shape, or a plain theka on its vibhags.
  • Variations: meta.taal.variation (or a row's taal.variation) picks one by name, any case. Omitted, each taal plays its default, listed in src/player/tabla-defaults.ts. tablaVariations(taal) lists them for a picker.
  • Fillers (meta.fillers, default auto): a filler cycle wherever the singer holds or rests through a cycle's last vibhag, never twice running. auto plays light ones and every 4th busy.
  • Tihai (meta.tihai, default off): closes the song, landing on the first সম at or after its last note.
  • Custom taal: any id not in the catalogue, with its own numBeats, bhaags, sam and khaali. It plays a catalogue taal of the same vibhags and khaali, else a plain theka on its vibhags. A sam on another vibhag means each line opens before sam: the rhythm waits for it, and its theka is written from sam. The creator makes one ("Custom taal…", or "Start blank").
  • Custom theka: any taal can carry its own theka, fillers, tihai and bols — tabla-js's swaralipi-taal shape, as its composer exports it.
  • সম marks re-sync it. Every 1f/2F mark is a সম. If one comes early or late, whole cycles play, then the theka's last matras lead into the mark. A gap shorter than one cycle cuts that cycle. The metronome, the drums and the MIDI drums follow the same plan.
  • Tempo: any bpm; while a tabla plays it snaps to TABLA_BPMS (15–720, step 15).
  • The metronome claps the taal: sam loudest, a tali 4 dB under it, a plain matra 8, a khali 14 (clickLevels).
  • Tanpura: tabla-js's five-string tanpura, a drone at its own pace, an octave under the melody's Sa.

It sings the song's form, not just the page. { } twice, ( ) only the first time, the [ ] notes written above on the repeat. At each kali's closing II it goes back to the asthayi's opening II and sings up to the ‖ stop mark (I[ ]I sings the [ ] notes there). The tabla keeps its cycle through every jump. Existing JSON needs no change. repeats: false plays it as written; exportAudio / exportMidi take the same option. buildSchedule(doc, { repeats: true }) gives the sung timeline (its default stays as written). What is not handled yet: REPEATS.md.

The staff view prints the same form in Western marks, so a musician reading it plays what the player plays:

| Swaralipi | Staff | |---|---| | { } | 𝄆 𝄇 repeat bars | | ( ) up to } | 1st-time bar (volta "1"); elsewhere "(1st time only … )" | | [ ] notes above | a second voice over that spot, "2nd time" (or "on D.S." for I[ ]I) | | asthayi's opening II | 𝄋 segno | | ‖ | To Coda / FINE | | kali's closing II | D.S. al coda, then ⊕ where singing goes on; D.S. al fine at the end | | II · IIII | double bar · end bar | | রেশ · লয় / তাল change | after-grace · its name above the line |

A return that passes a { } says "with repeats on D.S." (a Western D.S. skips repeats otherwise). With drums, the drum staff takes every bar the melody has.

A pickup is sung first; the rhythm comes in on the সম. Many songs start before the সম (অনাগত). The সম comes from a 1f/2f tick. With no tick, a song that opens on loose notes takes it from its bars: the first line that opens on a bar starts on sam, else the first I. The melody starts at once; tabla, drums or metronome come in on the সম; the tanpura sounds from the start. countIn: true (or setCountIn(true)) puts the rhythm first: it plays the rest of the cycle and the melody comes in on its own beat. exportAudio and exportMidi take the same option, and the staff view writes the pickup as a short first bar. buildSchedule(doc) gives samBeat (where the rhythm comes in) and pickupBeats (the count-in).

Practice: start anywhere, repeat a line, sing it back

Learning a song means hearing one line again and again, then singing it back. Once bindSheet has run, the sheet is the practice surface — no progress bar to fiddle with:

  • Tap any line to play from the note under your finger (the whole line is the target; it picks the nearest note). Works playing, paused or stopped.
  • Keys, while the sheet has focus: Space play/pause · ↑ ↓ line · ← → note · Home End · R repeat the line · T my turn · Esc stop repeating · Shift+↑/↓ or Shift+tap stretch the repeat over more lines.
player.play('0.3.2');          // from part 0, line 3, note 2 (or '0.3' for the line's start)
player.stepLine(1);            // next line; stepLine(-1) restarts the line first, like ⏮
player.setLoop();              // repeat the line under the playhead (setLoop('0.2', '0.4') for a range, null to stop)
player.setEcho(true);          // after each pass, one silent pass: tabla + tanpura keep time,
                               // the highlight moves hollow — the learner's turn to sing it

A line sung more than once (a { }, a return to the asthayi) plays from the pass nearest the playhead, and ↑ ↓ walk the sung order. A repeat follows the playhead (tap or step to another line and it moves there), and a tempo or tonic change keeps both the repeat and the place. Wire buttons to the same calls and mirror them with onLoop: ({ loop, echo, yourTurn }) => …. A tap is a cancelable swaralipi-seek event on the sheet — preventDefault() it in a capturing listener to gate a play (a quota, say). previewLimitSec counts listening time, so repeats cannot run past it.

Follow the words, play an excerpt

alignLyrics(doc, text) places each note in the song's own text: 'part.row.note' → [start, end) offsets into text. It reads the notation's phonetic syllables (আ|বা|র্, চি|ৎ|ত) and its written-out refrains. A held ৹ has no entry.

import { SwaralipiPlayer, alignLyrics } from '@ugfoundation/swaralipi-js/player';

const lyrics = alignLyrics(doc, songText);
const player = new SwaralipiPlayer({
  onHighlight: (path) => path && lyrics.has(path) && markWord(lyrics.get(path)), // a repeat maps to the same words
});

// A dance drama quotes lines 1–4, then line 13: play only those, in order.
player.load(doc, { excerpt: { lyrics, ranges: [[line1Start, line4End], [line13Start, line13End]] } });
  • Each range plays the song's form while its words stay inside it: { } twice, a return to the asthayi included.
  • Between ranges, a short rest lands the next one on its own beat. The tabla never breaks.
  • Words the notation does not sing play nothing.
  • durationSec, seeks and repeats keep to the excerpt. buildSchedule(doc, { repeats: true, excerpt }) gives its timeline.

Export audio (WAV / MP3)

Render a document to a downloadable audio file entirely in the browser — an OfflineAudioContext bounces the same melody / tabla / tanpura arrangement faster than real time, so nothing hits a server (works on Chrome, Firefox, Safari incl. iOS, Edge, Android, PWAs).

import { exportAudio } from '@ugfoundation/swaralipi-js/player';

const { blob, filename } = await exportAudio(doc, {
  melody: true,        // the notation voice (default on)
  rhythm: 'tabla',     // 'tabla' | 'drums' | 'metronome' | 'none'  (default 'none')
  taal: 'auto',        // a taal id, or 'auto' from meta.taal
  tanpura: true,
  format: 'mp3',       // 'wav' (default) | 'mp3'
});
Object.assign(document.createElement('a'), { href: URL.createObjectURL(blob), download: filename }).click();

Also honours instrument, tonic, bpm, variation, fillers, tihai, mp3Kbps (192), channels (mono by default — the content is monophonic), sampleRate, and soundsBaseUrl. WAV is 16-bit PCM and needs no dependency; MP3 needs the optional peer dependency @breezystack/lamejs — see License.

excerpt plays only some lines (as the player's does); lyrics: songText returns cues — each sung line and word in seconds on the file's own timeline, ready for an LRC sidecar. mp3Stream() encodes PCM fed in pieces as one MP3, and exportMidiSet(items, name) writes several songs back to back into one .mid (a tempo and a marker at each song).

Convert: archival HTML → JSON

Digitised swaralipi is commonly published as HTML grids typeset in the ASCII-encoded Swarabitan webfont, where each cell is a glyph code rather than text. The converter decodes that encoding into spec JSON, losslessly — every cell keeps its original code alongside the decoded note, so nothing is lost even where a cell can't be interpreted.

swaralipi convert song.html                       # spec JSON to stdout
swaralipi convert html/ -o out/ --report out/_report.json
import { convertNltrHtml } from '@ugfoundation/swaralipi-js/convert';   // Node-only subpath
const { doc, report } = convertNltrHtml(html, { nltrId: 4270 });

Subpath exports

| Import | Environment | |--------|-------------| | @ugfoundation/swaralipi-js (root, re-exports /spec) | any | | @ugfoundation/swaralipi-js/spec | any | | @ugfoundation/swaralipi-js/viewer | browser / SSR | | @ugfoundation/swaralipi-js/player | browser (Node/SSR-safe import) | | @ugfoundation/swaralipi-js/convert | Node only | | swaralipi CLI | Node |


Develop

pnpm install
pnpm build   # tsup → dist/ (ESM + CJS + .d.ts)
pnpm test    # vitest
pnpm check   # tsc --noEmit

Node 20 LTS (.nvmrc), pnpm 9. Contributions are welcome under a CLA — see CONTRIBUTING.md.

Credits

  • Rabindranath Tagore's notation is in the public domain (copyright expired in India, 2001).
  • sargam-spec by Studio Kalavati (EPL-2.0) — swaralipi-js's spec is an independent reimplementation that extends sargam-spec's format; we keep its [octave, letter] svara tuple and octave/letter conventions.
  • Fonts and audio samples are third-party works with their own licences — see THIRD-PARTY.md.

License

GPL-2.0-only © Upal & Gopa Das Foundation. See LICENSE.

Optional MP3 export. exportAudio(…, { format: 'mp3' }) uses @breezystack/lamejs, which is LGPL-3.0 — a different licence from this package. It is declared an optional peer dependency, is loaded only via a dynamic import() when you actually request MP3, and is never bundled or redistributed by swaralipi-js, so it does not affect this package's GPL-2.0-only licensing. Install it yourself only if you want MP3 output; WAV export needs no extra dependency. If you ship an application bundle that includes lamejs, meeting its LGPL-3.0 terms (e.g. allowing the library to be replaced) is your responsibility.