@ugfoundation/swaralipi-js 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +347 -0
  2. package/README.md +257 -0
  3. package/dist/batch-NF2Q2CFS.js +47 -0
  4. package/dist/batch-NF2Q2CFS.js.map +1 -0
  5. package/dist/chunk-C6O7DYU5.js +115 -0
  6. package/dist/chunk-C6O7DYU5.js.map +1 -0
  7. package/dist/chunk-GWXI2HJA.js +14 -0
  8. package/dist/chunk-GWXI2HJA.js.map +1 -0
  9. package/dist/chunk-N3NNWAOW.js +593 -0
  10. package/dist/chunk-N3NNWAOW.js.map +1 -0
  11. package/dist/cli.cjs +913 -0
  12. package/dist/cli.cjs.map +1 -0
  13. package/dist/cli.d.cts +1 -0
  14. package/dist/cli.d.ts +1 -0
  15. package/dist/cli.js +91 -0
  16. package/dist/cli.js.map +1 -0
  17. package/dist/convert/index.cjs +720 -0
  18. package/dist/convert/index.cjs.map +1 -0
  19. package/dist/convert/index.d.cts +270 -0
  20. package/dist/convert/index.d.ts +270 -0
  21. package/dist/convert/index.js +5 -0
  22. package/dist/convert/index.js.map +1 -0
  23. package/dist/index.cjs +143 -0
  24. package/dist/index.cjs.map +1 -0
  25. package/dist/index.d.cts +4 -0
  26. package/dist/index.d.ts +4 -0
  27. package/dist/index.js +4 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/player/index.cjs +775 -0
  30. package/dist/player/index.cjs.map +1 -0
  31. package/dist/player/index.d.cts +285 -0
  32. package/dist/player/index.d.ts +285 -0
  33. package/dist/player/index.js +743 -0
  34. package/dist/player/index.js.map +1 -0
  35. package/dist/schema-gxhG45OK.d.cts +467 -0
  36. package/dist/schema-gxhG45OK.d.ts +467 -0
  37. package/dist/spec/index.cjs +143 -0
  38. package/dist/spec/index.cjs.map +1 -0
  39. package/dist/spec/index.d.cts +70 -0
  40. package/dist/spec/index.d.ts +70 -0
  41. package/dist/spec/index.js +4 -0
  42. package/dist/spec/index.js.map +1 -0
  43. package/dist/taals-DguYW0wf.d.cts +60 -0
  44. package/dist/taals-DguYW0wf.d.ts +60 -0
  45. package/dist/viewer/index.cjs +998 -0
  46. package/dist/viewer/index.cjs.map +1 -0
  47. package/dist/viewer/index.d.cts +285 -0
  48. package/dist/viewer/index.d.ts +285 -0
  49. package/dist/viewer/index.js +814 -0
  50. package/dist/viewer/index.js.map +1 -0
  51. package/package.json +93 -0
@@ -0,0 +1,285 @@
1
+ import { S as Swaralipi, O as Octave, a as SvaraLetter, N as Note } from '../schema-gxhG45OK.js';
2
+ import { T as TaalId } from '../taals-DguYW0wf.js';
3
+ import * as tone from 'tone';
4
+ import 'zod';
5
+
6
+ /** The dynamically-imported Tone module, typed without importing it. */
7
+ type ToneModule = typeof tone;
8
+ /** A melodic instrument: a Tone.Sampler note→filename map + where to find it. */
9
+ interface InstrumentDef {
10
+ /** Human label for UIs. */
11
+ label: string;
12
+ /** Tone.Sampler `urls`: note name (`C#4`) → filename (`Cs4.mp3`). */
13
+ urls: Record<string, string>;
14
+ /** Sample directory: an absolute URL, or — when `local` — a path segment
15
+ * appended to the player's `soundsBaseUrl`. */
16
+ baseUrl: string;
17
+ /** `baseUrl` is relative to `soundsBaseUrl` (needs it set). */
18
+ local?: boolean;
19
+ /** Sampler release time in seconds. */
20
+ release?: number;
21
+ }
22
+ /** Built-in melody instrument ids (consumers may add their own via the
23
+ * player's `customInstruments`, so the runtime id type stays open). */
24
+ type InstrumentId = string;
25
+ /**
26
+ * Default host for the project's samples (santoor / tabla / tanpura /
27
+ * metronome) — CC-BY, redistribution-verified and CORS-enabled, so the player
28
+ * works with zero config. Override via the player's `soundsBaseUrl` to
29
+ * self-host. (Melody defaults to the CDN-hosted piano, so it needs no host.)
30
+ */
31
+ declare const DEFAULT_SOUNDS_BASE = "https://cdn.daserve.in/swaralipijs-instruments/";
32
+ /** CC-BY Salamander Grand Piano (Tone's de-facto piano), CDN-hosted. */
33
+ declare const SALAMANDER_BASE = "https://tonejs.github.io/audio/salamander/";
34
+ /** CC-BY tonejs-instruments sample CDN (harmonium/violin/flute). */
35
+ declare const TONEJS_INSTRUMENTS_BASE = "https://nbrosowsky.github.io/tonejs-instruments/samples/";
36
+ /** Built-in melody instruments. santoor is local; the rest are CDN-hosted
37
+ * (CC-BY) so they need only network — `pianoBaseUrl`/`customInstruments`
38
+ * override for self-hosting. */
39
+ declare const INSTRUMENTS: Record<string, InstrumentDef>;
40
+ /** Taals with recorded tabla loops (everything else → metronome fallback). */
41
+ declare const SAMPLED_TAALS: ReadonlySet<TaalId>;
42
+ /** Available tabla loop tempos (file suffix `…<bpm>bpm.mp3`). */
43
+ declare const TABLA_BPMS: readonly number[];
44
+ /** Ensure exactly one trailing slash. */
45
+ declare function normalizeBase(url: string): string;
46
+ /** The 37 santoor samples, as a Tone.Sampler `urls` map (`C4` → `c4.mp3`). */
47
+ declare function santoorUrls(): Record<string, string>;
48
+ /**
49
+ * Restrict a Sampler `urls` map to just the samples needed to cover MIDI
50
+ * `[lo, hi]`: every sample inside the range, plus the nearest one below and
51
+ * above it (so Tone interpolates the edges without extrapolating). Cuts a
52
+ * typical song from ~37 santoor files to a handful — a big win on mobile and
53
+ * never worse on fast links (Tone fetches what's left in parallel). The
54
+ * Sampler pitch-shifts from the nearest sample, so audio is unchanged.
55
+ */
56
+ declare function subsetUrls(urls: Record<string, string>, lo: number, hi: number): Record<string, string>;
57
+ /** Resolve a representative sample URL for an instrument (its first sample),
58
+ * used to probe availability. undefined when a local instrument has no
59
+ * `soundsBaseUrl` or the def has no samples. */
60
+ declare function instrumentSampleUrl(def: InstrumentDef, soundsBaseUrl?: string): string | undefined;
61
+ /**
62
+ * Liveness check for a sample URL: a single tiny ranged GET (one byte), the
63
+ * same CORS-enabled fetch Tone uses to load the buffer — so a pass means Tone
64
+ * can actually load it. Resolves false on any error/timeout/non-2xx, and false
65
+ * (no throw) when `fetch` is unavailable (Node/SSR). `Range: bytes=0-0` is a
66
+ * CORS-safelisted header, so no preflight.
67
+ */
68
+ declare function probeUrl(url: string, timeoutMs?: number): Promise<boolean>;
69
+ /** Snap an arbitrary bpm to the nearest available tabla tempo. */
70
+ declare function nearestTablaBpm(bpm: number): number;
71
+ /** URL of the tabla loop for a taal at a (snapped) bpm. */
72
+ declare function tablaUrl(base: string, taalId: string, bpm: number): string;
73
+
74
+ type PlayerState = 'idle' | 'loading' | 'ready' | 'playing' | 'paused';
75
+ type RhythmMode = 'tabla' | 'metronome' | 'none';
76
+ interface PlayerOptions {
77
+ /** Base URL for the project's samples (santoor/tabla/tanpura/metronome).
78
+ * Defaults to `DEFAULT_SOUNDS_BASE` (the project's CORS-enabled CDN), so the
79
+ * player works with zero config; override to self-host. */
80
+ soundsBaseUrl?: string;
81
+ /** Grand-piano sample host. Default: the Salamander CDN (see SALAMANDER_BASE).
82
+ * Set this to self-host the piano under your own URL. */
83
+ pianoBaseUrl?: string;
84
+ /** Tonic (Sa). Accepts `C`, `C#`, `cs`… Default: doc tonic, else `C`. */
85
+ tonic?: string;
86
+ /** Tempo in matra/min. Default 90. Snapped to the 15-grid while tabla plays. */
87
+ bpm?: number;
88
+ /** Octave of madhyam Sa (C4 = MIDI 60). Default 4. */
89
+ baseOctave?: number;
90
+ /** Active melody instrument id (a built-in or a `customInstruments` key).
91
+ * Default `piano` (CDN-hosted, zero-config). Built-ins: piano, santoor,
92
+ * harmonium, violin, flute. */
93
+ instrument?: InstrumentId;
94
+ /** Instrument ids the UI may expose. Default: all built-ins. Narrow it to
95
+ * e.g. `['santoor','harmonium']` to limit the picker. */
96
+ instruments?: InstrumentId[];
97
+ /** Extra melody instruments, keyed by id — e.g. a self-hosted sitar/sarod.
98
+ * (True sampled sitar/sarod/sarangi/esraj have no free web set; supply your
99
+ * own Tone.Sampler `urls` + `baseUrl` here and they slot into the picker.) */
100
+ customInstruments?: Record<string, InstrumentDef>;
101
+ /** Rhythm accompaniment. Default `tabla` (→ metronome for unsampled taals). */
102
+ rhythm?: RhythmMode;
103
+ /** Play the tanpura drone. Default true. */
104
+ tanpura?: boolean;
105
+ /** Stop playback after this many seconds (a policy-neutral preview cap).
106
+ * Default `Infinity` (unlimited). The library only enforces the number it is
107
+ * handed; who is capped / the value / the upsell is the consumer's policy. */
108
+ previewLimitSec?: number;
109
+ /** Called when the preview cap stops playback (so the consumer can show an
110
+ * upsell). Not called on a normal stop()/end. */
111
+ onLimit?: () => void;
112
+ /** Called with the active note's `part.row.note` path, or null when idle. */
113
+ onHighlight?: (path: string | null) => void;
114
+ /** Called on every state transition. */
115
+ onState?: (state: PlayerState) => void;
116
+ }
117
+ /** Minimal shape of the viewer element the player drives (avoids a hard
118
+ * dependency on the viewer module). `highlight` returns the highlighted node
119
+ * (or null) so bindSheet can optionally scroll it into view. */
120
+ interface HighlightTarget {
121
+ highlight(path: string): HTMLElement | null;
122
+ clearHighlights(): void;
123
+ }
124
+ interface BindSheetOptions {
125
+ /** Keep the active note visible while playing by calling `scrollIntoView`
126
+ * on it. `true` → `'center'` (keeps the note's lyric, rendered just below it,
127
+ * on screen too); or pass an explicit block position (`'nearest'`, `'start'`,
128
+ * …). Default off so it never fights a host that manages its own scrolling.
129
+ * Works through the shadow boundary, so it scrolls the page, not just the
130
+ * sheet. */
131
+ autoScroll?: boolean | ScrollLogicalPosition;
132
+ }
133
+ declare class SwaralipiPlayer {
134
+ #private;
135
+ constructor(options?: PlayerOptions);
136
+ get state(): PlayerState;
137
+ get bpm(): number;
138
+ /** The preview cap in seconds (`Infinity` = unlimited). */
139
+ get previewLimitSec(): number;
140
+ get instrument(): InstrumentId;
141
+ get instruments(): InstrumentId[];
142
+ /** The taal currently driving the rhythm (auto from the doc, or an override). */
143
+ get taalId(): TaalId | undefined;
144
+ /** Whether the effective rhythm will actually be a sampled tabla. */
145
+ get usesTabla(): boolean;
146
+ /** Set the document to play. Stops any current playback. */
147
+ load(doc: Swaralipi): void;
148
+ /** Start (or resume) playback. Must be triggered from a user gesture so the
149
+ * browser permits `AudioContext` resume. */
150
+ play(): Promise<void>;
151
+ /** Pause; `play()` resumes from the same position. */
152
+ pause(): void;
153
+ /**
154
+ * Stop and rewind to the start. When sounding, the accompaniment (tabla /
155
+ * metronome / tanpura, which otherwise loop) is gently faded out over
156
+ * STOP_FADE_SEC before the transport is halted, so the rhythm doesn't keep
157
+ * playing or cut abruptly; otherwise it halts immediately.
158
+ */
159
+ stop(): void;
160
+ setTonic(tonic: string): void;
161
+ setBpm(bpm: number): void;
162
+ /** Update the preview cap (`Infinity` lifts it). Takes effect on next play. */
163
+ setPreviewLimit(sec: number): void;
164
+ setInstrument(instrument: InstrumentId): void;
165
+ setRhythm(rhythm: RhythmMode): void;
166
+ setTanpura(on: boolean): void;
167
+ /** Override the taal (null restores auto-detection from `meta.taal`). */
168
+ setTaal(id: TaalId | null): void;
169
+ /** Wire highlight output to a `<swaralipi-sheet>` (or any highlight target).
170
+ * Pass `{ autoScroll }` to keep the active note in view while playing. */
171
+ bindSheet(target: HighlightTarget, opts?: BindSheetOptions): void;
172
+ /**
173
+ * Check whether an instrument's samples are reachable (its CDN is up / the
174
+ * local files exist), via a one-byte ranged fetch of a representative sample.
175
+ * A pass means Tone can actually load it. `false` for unknown ids, local
176
+ * instruments with no `soundsBaseUrl`, or any network failure/timeout.
177
+ */
178
+ checkInstrument(id: string, timeoutMs?: number): Promise<boolean>;
179
+ /**
180
+ * Probe several instruments at once (default: the exposed `instruments`),
181
+ * returning `{ id: available }`. Use it to disable picker options whose CDN
182
+ * or files are down. Probes run concurrently; a check never throws.
183
+ */
184
+ checkInstruments(ids?: string[], timeoutMs?: number): Promise<Record<string, boolean>>;
185
+ /** Release all audio resources. The instance is unusable afterward. */
186
+ dispose(): void;
187
+ }
188
+
189
+ /**
190
+ * Pure spec-doc → playback timeline. No Tone, no audio — the player turns
191
+ * this `Schedule` into Transport events, but it is fully unit-testable on its
192
+ * own and mirrors the viewer's spec-driven (not raw-driven) reading.
193
+ *
194
+ * Timing unit is the মাত্রা (matra / beat). One global beat cursor runs across
195
+ * every part and row: NLTR "systems" (rows) are wraps of one continuous line,
196
+ * and parts (asthayi → antara …) play back-to-back, so the music never pauses
197
+ * at a row or part boundary.
198
+ *
199
+ * Each audible column carries the same `data-note` path the viewer emits
200
+ * (`part.row.note`, note = absolute index into `row.notes`; see
201
+ * src/viewer/render.ts), so `SwaralipiPlayer` can drive `highlight()` directly.
202
+ */
203
+
204
+ interface PlayEvent {
205
+ /** Onset in matra from the start of the document. */
206
+ beat: number;
207
+ /** Ring length in matra (extended by following আ-কার holds). */
208
+ durationBeats: number;
209
+ /** Pitch as MIDI, or null for a rest / আ-কার hold column. */
210
+ midi: number | null;
211
+ /** Grace-note MIDI struck just before the main attack, when present. */
212
+ kan: number | null;
213
+ /** `part.row.note` highlight path. */
214
+ path: string;
215
+ kind: 'note' | 'rest' | 'sustain';
216
+ }
217
+ interface Schedule {
218
+ events: PlayEvent[];
219
+ /** Total length in matra (the cursor's final value). */
220
+ totalBeats: number;
221
+ }
222
+ interface ScheduleOptions {
223
+ /** Tonic; accepts `C`, `C#`, `cs`… (see parseTonic). Default C. */
224
+ tonic?: string;
225
+ /** Octave of madhyam Sa (C4 = MIDI 60). Default 4. */
226
+ baseOctave?: number;
227
+ }
228
+ /**
229
+ * Walk a document into a flat, time-ordered list of play events.
230
+ *
231
+ * - svara note → an attack at the cursor; advances by `durationMatra ?? 1`.
232
+ * - rest → a silent column that still advances the cursor.
233
+ * - sustain **with** svara (melisma) → a fresh attack (the pitch moves under
234
+ * a held vowel); advances.
235
+ * - sustain **without** svara (আ-কার hold) → no attack, but the previous
236
+ * audible note's ring is extended; still emitted (midi null) so the lyric
237
+ * column highlights; advances.
238
+ * - bar / bracket-only / carrier / raw-only → ignored (no event, no advance).
239
+ */
240
+ declare function buildSchedule(doc: Swaralipi, opts?: ScheduleOptions): Schedule;
241
+
242
+ /**
243
+ * Pure svara → pitch math for the player. No Tone, no audio — unit-testable
244
+ * in plain Node and shared by the scheduler.
245
+ *
246
+ * The model: a tonic (Sa) pitch-class + a per-note scale degree + octave
247
+ * register + komal/tivra/microtone accidentals → an absolute MIDI number,
248
+ * which the Tone.Sampler pitch-shifts from the nearest santoor sample.
249
+ */
250
+
251
+ /** Octave register offset in octaves, relative to madhyam (middle). */
252
+ declare const OCTAVE_OFFSET: Record<Octave, number>;
253
+ /**
254
+ * Shuddha (natural) scale degrees in semitones above Sa — the Bilawal/major
255
+ * thaat that Rabindrasangeet swaralipi notates against. `d` = dha, `n` = ni
256
+ * (standard sargam letter convention).
257
+ */
258
+ declare const SHUDDHA: Record<SvaraLetter, number>;
259
+ /**
260
+ * Parse a tonic string to a pitch-class 0–11. Accepts `C`, `C#`, `Db`, and
261
+ * the player/sample lowercase-`s` sharp form (`cs`, `Cs`); case-insensitive.
262
+ * Unknown input falls back to 0 (C).
263
+ */
264
+ declare function parseTonic(tonic: string | undefined): number;
265
+ /**
266
+ * Semitone of a note relative to Sa (degree + accidentals), ignoring octave.
267
+ * Returns null when the note carries no pitch (rest/sustain-hold/bar).
268
+ *
269
+ * Microtones (অল্পকোমল/অতিকোমল) are approximated to the komal pitch — a
270
+ * documented v1 limitation (the sampler can't bend a sub-komal microtone
271
+ * cleanly per note); matches the viewer's placeholder treatment.
272
+ */
273
+ declare function svaraSemitone(note: Note): number | null;
274
+ /**
275
+ * Absolute MIDI for a note, or null when it has no pitch. madhyam Sa lands on
276
+ * the tonic in octave `baseOctave` (C4 = MIDI 60). Out-of-sample registers
277
+ * are produced by the Sampler pitch-shifting from the nearest recorded note.
278
+ */
279
+ declare function svaraToMidi(note: Note, tonicPc: number, baseOctave?: number): number | null;
280
+ /** MIDI → Tone note name (`C#4`). */
281
+ declare function midiToSampleName(midi: number): string;
282
+ /** MIDI → santoor sample filename stem (`cs4`): lowercase, `#` → `s`. */
283
+ declare function sampleFileName(midi: number): string;
284
+
285
+ export { type BindSheetOptions, DEFAULT_SOUNDS_BASE, INSTRUMENTS, type InstrumentDef, type InstrumentId, OCTAVE_OFFSET, type PlayEvent, type PlayerOptions, type PlayerState, type RhythmMode, SALAMANDER_BASE, SAMPLED_TAALS, SHUDDHA, type Schedule, type ScheduleOptions, SwaralipiPlayer, TABLA_BPMS, TONEJS_INSTRUMENTS_BASE, type ToneModule, buildSchedule, instrumentSampleUrl, midiToSampleName, nearestTablaBpm, normalizeBase, parseTonic, probeUrl, sampleFileName, santoorUrls, subsetUrls, svaraSemitone, svaraToMidi, tablaUrl };