@ugfoundation/swaralipi-js 0.14.0 → 0.14.1

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 (39) hide show
  1. package/README.md +4 -0
  2. package/dist/{batch-D4HRATTH.js → batch-J72GGBG2.js} +4 -4
  3. package/dist/{batch-D4HRATTH.js.map → batch-J72GGBG2.js.map} +1 -1
  4. package/dist/{chunk-QQOC6LRA.js → chunk-4WDAVRET.js} +3 -3
  5. package/dist/{chunk-QQOC6LRA.js.map → chunk-4WDAVRET.js.map} +1 -1
  6. package/dist/{chunk-YBFM5NLW.js → chunk-GIOGYJMM.js} +28 -3
  7. package/dist/chunk-GIOGYJMM.js.map +1 -0
  8. package/dist/{chunk-UV5OJYT3.js → chunk-KUZSW4FN.js} +3 -3
  9. package/dist/{chunk-UV5OJYT3.js.map → chunk-KUZSW4FN.js.map} +1 -1
  10. package/dist/cli.cjs +1 -1
  11. package/dist/cli.cjs.map +1 -1
  12. package/dist/cli.js +2 -2
  13. package/dist/convert/index.cjs +1 -1
  14. package/dist/convert/index.cjs.map +1 -1
  15. package/dist/convert/index.js +2 -2
  16. package/dist/index.cjs +1 -1
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.js +1 -1
  19. package/dist/{schedule-oJgg_bnc.d.cts → instruments-DTIuTlda.d.cts} +171 -162
  20. package/dist/{schedule-DsVESieT.d.ts → instruments-NcUb0ozc.d.ts} +171 -162
  21. package/dist/player/index.cjs +162 -62
  22. package/dist/player/index.cjs.map +1 -1
  23. package/dist/player/index.d.cts +34 -14
  24. package/dist/player/index.d.ts +34 -14
  25. package/dist/player/index.js +137 -65
  26. package/dist/player/index.js.map +1 -1
  27. package/dist/spec/index.cjs +1 -1
  28. package/dist/spec/index.cjs.map +1 -1
  29. package/dist/spec/index.d.cts +1 -1
  30. package/dist/spec/index.d.ts +1 -1
  31. package/dist/spec/index.js +1 -1
  32. package/dist/viewer/index.cjs +126 -3
  33. package/dist/viewer/index.cjs.map +1 -1
  34. package/dist/viewer/index.d.cts +62 -2
  35. package/dist/viewer/index.d.ts +62 -2
  36. package/dist/viewer/index.js +123 -4
  37. package/dist/viewer/index.js.map +1 -1
  38. package/package.json +1 -1
  39. package/dist/chunk-YBFM5NLW.js.map +0 -1
@@ -146,6 +146,31 @@ function rowLay(row) {
146
146
  }
147
147
 
148
148
  // src/player/lyrics.ts
149
+ function lyricCues(events, map, text) {
150
+ const tokens = [...text.matchAll(/\S+/g)].map((m) => ({
151
+ text: m[0],
152
+ start: m.index,
153
+ end: m.index + m[0].length,
154
+ line: text.slice(0, m.index).split("\n").length - 1
155
+ }));
156
+ const cues = [];
157
+ for (const ev of events) {
158
+ const span = map.get(ev.path);
159
+ const index = span ? tokens.findIndex((t) => t.start <= span[0] && span[0] < t.end) : -1;
160
+ if (index < 0) continue;
161
+ const token = tokens[index];
162
+ let cue = cues.at(-1);
163
+ if (!cue || cue.line !== token.line || index < cue.last) {
164
+ cue = { beat: ev.beat, line: token.line, last: -1, words: [] };
165
+ cues.push(cue);
166
+ }
167
+ if (index !== cue.last) {
168
+ cue.words.push({ beat: ev.beat, text: token.text });
169
+ cue.last = index;
170
+ }
171
+ }
172
+ return cues.map(({ beat, words }) => ({ beat, words }));
173
+ }
149
174
  var MATCH = 2;
150
175
  var GAP = 1;
151
176
  var JUMP = 4;
@@ -2141,6 +2166,38 @@ function midiRange2(schedule) {
2141
2166
  }
2142
2167
  return lo <= hi ? { lo, hi } : void 0;
2143
2168
  }
2169
+ var AHEAD_SEC = 2;
2170
+ async function renderOffline(Tone, build, durationSec, channels, sampleRate) {
2171
+ const chunked = "suspend" in OfflineAudioContext.prototype && "positionX" in AudioListener.prototype;
2172
+ const context = chunked ? new Tone.OfflineContext(new OfflineAudioContext(channels, Math.ceil(durationSec * sampleRate), sampleRate)) : new Tone.OfflineContext(channels, durationSec, sampleRate);
2173
+ const raw = context.rawContext;
2174
+ const original = Tone.getContext();
2175
+ Tone.setContext(context);
2176
+ try {
2177
+ await build(context);
2178
+ } finally {
2179
+ Tone.setContext(original);
2180
+ }
2181
+ const clock = context;
2182
+ const run = (until) => {
2183
+ while (clock._currentTime <= Math.min(until, durationSec)) {
2184
+ clock.emit("tick");
2185
+ clock._currentTime += 128 / sampleRate;
2186
+ }
2187
+ };
2188
+ if (chunked) {
2189
+ run(2 * AHEAD_SEC);
2190
+ for (let t = AHEAD_SEC; t < durationSec; t += AHEAD_SEC) {
2191
+ void raw.suspend(t).then(() => {
2192
+ run(t + 2 * AHEAD_SEC);
2193
+ void raw.resume();
2194
+ });
2195
+ }
2196
+ } else {
2197
+ run(Infinity);
2198
+ }
2199
+ return raw.startRendering();
2200
+ }
2144
2201
  function pickDef(id, o) {
2145
2202
  const def = o.customInstruments?.[id] ?? INSTRUMENTS[id];
2146
2203
  if (def === void 0) return INSTRUMENTS.harmonium;
@@ -2169,7 +2226,8 @@ async function exportAudio(doc, options = {}) {
2169
2226
  tonic: options.tonic,
2170
2227
  baseOctave: options.baseOctave,
2171
2228
  laySpeeds: options.laySpeeds,
2172
- repeats: options.repeats ?? true
2229
+ repeats: options.repeats ?? true,
2230
+ excerpt: options.excerpt
2173
2231
  });
2174
2232
  const range = midiRange2(schedule);
2175
2233
  const taalId = resolveTaal(doc, options.taal);
@@ -2182,7 +2240,8 @@ async function exportAudio(doc, options = {}) {
2182
2240
  const def = pickDef(id, options);
2183
2241
  const shift = rhythm !== "none" ? schedule.pickupBeats * spb : 0;
2184
2242
  const durationSec = Math.max(schedule.totalBeats * spb + shift + (def.release ?? 1) + fadeOutSec + 0.5, 0.5);
2185
- const rendered = await Tone.Offline(
2243
+ const rendered = await renderOffline(
2244
+ Tone,
2186
2245
  async ({ transport }) => {
2187
2246
  transport.bpm.value = bpm;
2188
2247
  const master = new Tone.Gain(1).toDestination();
@@ -2329,11 +2388,16 @@ async function exportAudio(doc, options = {}) {
2329
2388
  for (let c = 0; c < rendered.numberOfChannels; c++) chans.push(rendered.getChannelData(c));
2330
2389
  const bytes = format === "mp3" ? await encodeMp3(chans, rendered.sampleRate, options.mp3Kbps) : encodeWav(chans, rendered.sampleRate);
2331
2390
  const mimeType = format === "mp3" ? "audio/mpeg" : "audio/wav";
2391
+ const cues = options.lyrics === void 0 ? void 0 : lyricCues(schedule.events, alignLyrics(doc, options.lyrics), options.lyrics).map((cue) => ({
2392
+ sec: cue.beat * spb + shift,
2393
+ words: cue.words.map((word) => ({ sec: word.beat * spb + shift, text: word.text }))
2394
+ }));
2332
2395
  return {
2333
2396
  blob: new Blob([bytes], { type: mimeType }),
2334
2397
  filename: `${safeName(doc.meta.title)}.${format}`,
2335
2398
  mimeType,
2336
- durationSec: rendered.duration
2399
+ durationSec: rendered.duration,
2400
+ ...cues === void 0 ? {} : { cues }
2337
2401
  };
2338
2402
  }
2339
2403
  function toInt16(f) {
@@ -2400,22 +2464,32 @@ async function loadLame() {
2400
2464
  );
2401
2465
  }
2402
2466
  }
2403
- async function encodeMp3(channels, sampleRate, kbps = 192) {
2467
+ async function mp3Stream(sampleRate = 44100, kbps = 192, channels = 1) {
2404
2468
  const Mp3Encoder = await loadLame();
2405
- const numCh = Math.min(channels.length, 2);
2406
- const enc = new Mp3Encoder(numCh, sampleRate, kbps);
2407
- const left = floatToInt16(channels[0] ?? new Float32Array());
2408
- const right = numCh > 1 ? floatToInt16(channels[1]) : void 0;
2409
- const block = 1152;
2469
+ const enc = new Mp3Encoder(channels, sampleRate, kbps);
2410
2470
  const out = [];
2411
- for (let i = 0; i < left.length; i += block) {
2412
- const l = left.subarray(i, i + block);
2413
- const chunk = right ? enc.encodeBuffer(l, right.subarray(i, i + block)) : enc.encodeBuffer(l);
2471
+ const keep = (chunk) => {
2414
2472
  if (chunk.length > 0) out.push(new Uint8Array(chunk));
2415
- }
2416
- const tail = enc.flush();
2417
- if (tail.length > 0) out.push(new Uint8Array(tail));
2418
- return concatBytes(out);
2473
+ };
2474
+ const block = 1152;
2475
+ return {
2476
+ push(left, right) {
2477
+ for (let i = 0; i < left.length; i += block) {
2478
+ const l = left.subarray(i, i + block);
2479
+ keep(right ? enc.encodeBuffer(l, right.subarray(i, i + block)) : enc.encodeBuffer(l));
2480
+ }
2481
+ },
2482
+ end() {
2483
+ keep(enc.flush());
2484
+ return out;
2485
+ }
2486
+ };
2487
+ }
2488
+ async function encodeMp3(channels, sampleRate, kbps = 192) {
2489
+ const numCh = Math.min(channels.length, 2);
2490
+ const mp3 = await mp3Stream(sampleRate, kbps, numCh);
2491
+ mp3.push(floatToInt16(channels[0] ?? new Float32Array()), numCh > 1 ? floatToInt16(channels[1]) : void 0);
2492
+ return concatBytes(mp3.end());
2419
2493
  }
2420
2494
 
2421
2495
  // src/player/midi.ts
@@ -2461,6 +2535,11 @@ function trackChunk(raw, head) {
2461
2535
  const track = [...head];
2462
2536
  let last = 0;
2463
2537
  for (const e of raw) {
2538
+ if (e.meta) {
2539
+ track.push(...vlq(e.tick - last), ...e.meta);
2540
+ last = e.tick;
2541
+ continue;
2542
+ }
2464
2543
  const status = e.status !== void 0 ? e.status | e.ch & 15 : (e.on ? 144 : 128) | e.ch & 15;
2465
2544
  track.push(...vlq(e.tick - last), status, e.note & 127, e.vel & 127);
2466
2545
  last = e.tick;
@@ -2468,22 +2547,22 @@ function trackChunk(raw, head) {
2468
2547
  track.push(...vlq(0), 255, 47, 0);
2469
2548
  return [77, 84, 114, 107, ...u32be(track.length), ...track];
2470
2549
  }
2471
- function encodeMidi(schedule, opts) {
2550
+ function melodyEvents(schedule, offset = 0) {
2472
2551
  const melody = [];
2473
2552
  const hasGlide = schedule.events.some((ev) => ev.glide === true);
2474
2553
  if (hasGlide) melody.push(...bendRangeSetup());
2475
2554
  schedule.events.forEach((ev, i) => {
2476
2555
  if (ev.glide === true) return;
2477
2556
  for (const g of graceTouches(ev)) {
2478
- const on = Math.max(Math.round(g.beat * PPQ), 0);
2557
+ const on = Math.max(Math.round(g.beat * PPQ) + offset, 0);
2479
2558
  melody.push({ tick: on, on: true, note: g.midi, vel: 70, ch: 0 });
2480
- melody.push({ tick: Math.max(on + 1, Math.round((g.beat + GRACE_STEP) * PPQ)), on: false, note: g.midi, vel: 0, ch: 0 });
2559
+ melody.push({ tick: Math.max(on + 1, Math.round((g.beat + GRACE_STEP) * PPQ) + offset), on: false, note: g.midi, vel: 0, ch: 0 });
2481
2560
  }
2482
2561
  if (ev.midi === null) return;
2483
- const start = Math.round(ev.beat * PPQ);
2562
+ const start = Math.round(ev.beat * PPQ) + offset;
2484
2563
  const targets = glideTargets(schedule.events, i);
2485
2564
  const last = targets[targets.length - 1] ?? ev;
2486
- const end = Math.max(Math.round((last.beat + last.durationBeats) * PPQ), start + 1);
2565
+ const end = Math.max(Math.round((last.beat + last.durationBeats) * PPQ) + offset, start + 1);
2487
2566
  melody.push({ tick: start, on: true, note: ev.midi, vel: 100, ch: 0 });
2488
2567
  melody.push({ tick: end, on: false, note: ev.midi, vel: 0, ch: 0 });
2489
2568
  let from = ev;
@@ -2493,71 +2572,89 @@ function encodeMidi(schedule, opts) {
2493
2572
  const u = n / BEND_STEPS;
2494
2573
  const beat = glideStart + (to.beat - glideStart) * u;
2495
2574
  const semis = from.midi - ev.midi + (to.midi - from.midi) * sCurve(u);
2496
- melody.push(bendEvent(Math.round(beat * PPQ), semis));
2575
+ melody.push(bendEvent(Math.round(beat * PPQ) + offset, semis));
2497
2576
  }
2498
2577
  from = to;
2499
2578
  }
2500
2579
  if (targets.length > 0) melody.push(bendEvent(end, 0));
2501
2580
  });
2581
+ return melody;
2582
+ }
2583
+ function drumEvents(schedule, drums, offset = 0) {
2584
+ const drum = [];
2585
+ for (const seg of schedule.tempo) {
2586
+ const pattern = typeof drums === "function" ? drums(seg) : drums;
2587
+ if (!pattern?.length) continue;
2588
+ const end = seg === schedule.tempo[schedule.tempo.length - 1] ? Math.ceil(schedule.totalBeats) : seg.endBeat;
2589
+ for (let k = 0; seg.beat + k / seg.scale < end - 1e-6; k++) {
2590
+ const note = DRUM_NOTE[pattern[k % pattern.length]];
2591
+ const t = Math.round((seg.beat + k / seg.scale) * PPQ) + offset;
2592
+ drum.push({ tick: t, on: true, note, vel: 100, ch: 9 });
2593
+ drum.push({ tick: t + Math.round(PPQ / 4), on: false, note, vel: 0, ch: 9 });
2594
+ }
2595
+ }
2596
+ return drum;
2597
+ }
2598
+ function midiFile(chunks) {
2599
+ const format = chunks.length > 1 ? 1 : 0;
2600
+ const header = [77, 84, 104, 100, ...u32be(6), 0, format, chunks.length >> 8 & 255, chunks.length & 255, PPQ >> 8 & 255, PPQ & 255];
2601
+ return new Uint8Array([...header, ...chunks.flat()]);
2602
+ }
2603
+ function meta(type, data) {
2604
+ return [255, type, ...vlq(data.length), ...data];
2605
+ }
2606
+ function encodeMidi(schedule, opts) {
2502
2607
  const usPerBeat = Math.round(6e7 / opts.bpm);
2503
2608
  const head = [];
2504
2609
  if (opts.name) head.push(...vlq(0), 255, 3, ...vlq(utf8(opts.name).length), ...utf8(opts.name));
2505
2610
  head.push(...vlq(0), 255, 81, 3, usPerBeat >> 16 & 255, usPerBeat >> 8 & 255, usPerBeat & 255);
2506
- const chunks = [trackChunk(melody, head)];
2611
+ const chunks = [trackChunk(melodyEvents(schedule), head)];
2507
2612
  if (typeof opts.drums === "function" || opts.drums && opts.drums.length > 0) {
2508
- const drum = [];
2509
- for (const seg of schedule.tempo) {
2510
- const pattern = typeof opts.drums === "function" ? opts.drums(seg) : opts.drums;
2511
- if (!pattern?.length) continue;
2512
- const end = seg === schedule.tempo[schedule.tempo.length - 1] ? Math.ceil(schedule.totalBeats) : seg.endBeat;
2513
- for (let k = 0; seg.beat + k / seg.scale < end - 1e-6; k++) {
2514
- const note = DRUM_NOTE[pattern[k % pattern.length]];
2515
- const t = Math.round((seg.beat + k / seg.scale) * PPQ);
2516
- drum.push({ tick: t, on: true, note, vel: 100, ch: 9 });
2517
- drum.push({ tick: t + Math.round(PPQ / 4), on: false, note, vel: 0, ch: 9 });
2518
- }
2519
- }
2613
+ const drum = drumEvents(schedule, opts.drums);
2520
2614
  chunks.push(trackChunk(drum, [...vlq(0), 255, 3, ...vlq(5), 68, 114, 117, 109, 115]));
2521
2615
  }
2522
- const format = chunks.length > 1 ? 1 : 0;
2523
- const header = [
2524
- 77,
2525
- 84,
2526
- 104,
2527
- 100,
2528
- ...u32be(6),
2529
- 0,
2530
- format,
2531
- chunks.length >> 8 & 255,
2532
- chunks.length & 255,
2533
- PPQ >> 8 & 255,
2534
- PPQ & 255
2535
- ];
2536
- return new Uint8Array([...header, ...chunks.flat()]);
2616
+ return midiFile(chunks);
2617
+ }
2618
+ function exportSchedule(doc, o) {
2619
+ return buildSchedule(doc, { tonic: o.tonic, baseOctave: o.baseOctave, laySpeeds: o.laySpeeds, repeats: o.repeats ?? true, excerpt: o.excerpt });
2620
+ }
2621
+ function taalDrums(seg) {
2622
+ if (seg.taal === null) return void 0;
2623
+ const id = seg.taal?.id;
2624
+ return drumPattern(id !== void 0 && id in TAALS ? TAALS[id] : { numBeats: 4, sam: 0 });
2537
2625
  }
2538
2626
  function safeName2(title) {
2539
2627
  return (title ?? "").replace(/[\\/:*?"<>|]+/g, "").trim() || "swaralipi";
2540
2628
  }
2541
2629
  function exportMidi(doc, options = {}) {
2542
- const schedule = buildSchedule(doc, {
2543
- tonic: options.tonic,
2544
- baseOctave: options.baseOctave,
2545
- laySpeeds: options.laySpeeds,
2546
- repeats: options.repeats ?? true
2547
- });
2630
+ const schedule = exportSchedule(doc, options);
2548
2631
  const bpm = options.bpm ?? doc.meta.bpm ?? 90;
2549
- const drums = options.drums ? (seg) => {
2550
- const id = seg.taal?.id;
2551
- if (seg.taal === null) return void 0;
2552
- return drumPattern(id !== void 0 && id in TAALS ? TAALS[id] : { numBeats: 4, sam: 0 });
2553
- } : void 0;
2554
- const bytes = encodeMidi(schedule, { bpm, name: options.name ?? doc.meta.title, drums });
2632
+ const bytes = encodeMidi(schedule, { bpm, name: options.name ?? doc.meta.title, drums: options.drums ? taalDrums : void 0 });
2555
2633
  return {
2556
2634
  blob: new Blob([bytes], { type: "audio/midi" }),
2557
2635
  filename: `${safeName2(doc.meta.title)}.mid`,
2558
2636
  mimeType: "audio/midi"
2559
2637
  };
2560
2638
  }
2639
+ function exportMidiSet(items, name) {
2640
+ const melody = [];
2641
+ const drum = [];
2642
+ let beat = 0;
2643
+ for (const { doc, options = {} } of items) {
2644
+ const schedule = exportSchedule(doc, options);
2645
+ const tick = beat * PPQ;
2646
+ const usPerBeat = Math.round(6e7 / (options.bpm ?? doc.meta.bpm ?? 90));
2647
+ const at = (bytes2) => ({ tick, on: false, note: 0, vel: 0, ch: 0, meta: bytes2 });
2648
+ melody.push(at(meta(81, [usPerBeat >> 16 & 255, usPerBeat >> 8 & 255, usPerBeat & 255])));
2649
+ melody.push(at(meta(6, utf8(options.name ?? doc.meta.title ?? ""))), ...melodyEvents(schedule, tick));
2650
+ if (options.drums) drum.push(...drumEvents(schedule, taalDrums, tick));
2651
+ beat += Math.ceil(schedule.totalBeats) + 2;
2652
+ }
2653
+ const chunks = [trackChunk(melody, [...vlq(0), ...meta(3, utf8(name))])];
2654
+ if (drum.length > 0) chunks.push(trackChunk(drum, [...vlq(0), ...meta(3, utf8("Drums"))]));
2655
+ const bytes = midiFile(chunks);
2656
+ return { blob: new Blob([bytes], { type: "audio/midi" }), filename: `${safeName2(name)}.mid`, mimeType: "audio/midi" };
2657
+ }
2561
2658
 
2562
2659
  exports.ARC_RAW_RE = ARC_RAW_RE;
2563
2660
  exports.DEFAULT_SOUNDS_BASE = DEFAULT_SOUNDS_BASE;
@@ -2585,12 +2682,15 @@ exports.encodeWav = encodeWav;
2585
2682
  exports.eventAt = eventAt;
2586
2683
  exports.exportAudio = exportAudio;
2587
2684
  exports.exportMidi = exportMidi;
2685
+ exports.exportMidiSet = exportMidiSet;
2588
2686
  exports.glideTargets = glideTargets;
2589
2687
  exports.graceTouches = graceTouches;
2590
2688
  exports.instrumentSampleUrl = instrumentSampleUrl;
2591
2689
  exports.isArcRaw = isArcRaw;
2690
+ exports.lyricCues = lyricCues;
2592
2691
  exports.meendContinuations = meendContinuations;
2593
2692
  exports.midiToSampleName = midiToSampleName;
2693
+ exports.mp3Stream = mp3Stream;
2594
2694
  exports.nearestSampleKey = nearestSampleKey;
2595
2695
  exports.nearestTablaBpm = nearestTablaBpm;
2596
2696
  exports.normalizeBase = normalizeBase;