@ugfoundation/swaralipi-js
v0.2.1
Published
JSON spec, Unicode-Bengali viewer, and Tone.js player for Rabindrasangeet swaralipi (আকারমাত্রিক notation).
Downloads
1,168
Maintainers
Readme
swaralipi-js
@ugfoundation/swaralipi-js — a JSON spec, a Unicode-Bengali viewer, a Tone.js player, and an NLTR 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 / yarnQuick 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 | atikomalduration — 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 matrassustain (আ-কার) and melisma
{ "sustain": true, "lyric": "৹" } // hold prev pitch+vowel (আ-কার)
{ "svara": ["taar", "s"], "sustain": true, "lyric": "৹" } // melisma: pitch moves, vowel continuesrest (বিরাম)
{ "rest": true, "durationMatra": 1 } // silence for one matrabars — দাঁড়ি / দণ্ড
{ "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 songbrackets — 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 endlyrics 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:
{ "svara": ["madhyam", "m"], "lyric": "অ",
"above": [{ "svara": ["madhyam", "p"] }] } // alt note printed aboveraw — 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 throwSee 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-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.
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.
Play: SwaralipiPlayer
A Tone.js player turns a document into audio — melody on santoor / harmonium / violin / flute / grand piano, a tabla loop (metronome for unrecorded taals), and a tanpura drone — and drives the viewer's highlight in sync. Tone is 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), setRhythm('tabla'|'metronome'|'none'), setTanpura(bool), pause() / stop() / dispose(). Each song loads only the samples covering its pitch range.
- Self-host samples: override
soundsBaseUrl(layout:santoor/<note>.mp3,tabla/<taal>/<taal><bpm>bpm.mp3,tanpura/<pc>.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, svaraToMidi, parseTonic, INSTRUMENTS, …) for a custom transport.
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' | '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, 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.
Convert: NLTR HTML → JSON
NLTR's Rabindra Rachanabali renders swaralipi as HTML grids in the ASCII-encoded Swarabitan webfont. The converter decodes that codec into spec JSON, losslessly. Across the full 1,738-song corpus: 100% of files convert, 99.95% of notation cells decode semantically, the rest preserved raw and reported.
swaralipi convert 4270.html # spec JSON to stdout
swaralipi convert nltr/html -o out/ --report out/_report.jsonimport { 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 --noEmitNode 20 LTS (.nvmrc), pnpm 9. Contributions are welcome under a CLA — see CONTRIBUTING.md.
Credits
- NLTR (Society for Natural Language Technology Research) — the Rabindra Rachanabali digital edition and its Swarabitan-encoded swaralipi are the source corpus this package decodes. Tagore's notation itself is in the public domain.
- 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. - Audio samples, all CC-BY: the default Salamander Grand Piano by Alexander Holm; harmonium / violin / flute from tonejs-instruments by Nicholaus Brosowsky; santoor / tabla / tanpura / metronome redistributed on the project CDN.
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.
