@ugfoundation/swaralipi-js 0.12.0 → 0.13.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 (43) hide show
  1. package/README.md +7 -1
  2. package/dist/{batch-ZNVWIGEL.js → batch-XVQRHLTN.js} +5 -4
  3. package/dist/batch-XVQRHLTN.js.map +1 -0
  4. package/dist/chunk-BSZ3WQUP.js +164 -0
  5. package/dist/chunk-BSZ3WQUP.js.map +1 -0
  6. package/dist/{chunk-A7R3VFIV.js → chunk-GKS4WZZ6.js} +3 -3
  7. package/dist/{chunk-A7R3VFIV.js.map → chunk-GKS4WZZ6.js.map} +1 -1
  8. package/dist/{chunk-A6Z7X2LX.js → chunk-L6YZ3GC6.js} +227 -63
  9. package/dist/chunk-L6YZ3GC6.js.map +1 -0
  10. package/dist/{chunk-ISNAL632.js → chunk-MORX7G6E.js} +5 -165
  11. package/dist/chunk-MORX7G6E.js.map +1 -0
  12. package/dist/cli.cjs +1 -1
  13. package/dist/cli.cjs.map +1 -1
  14. package/dist/cli.js +2 -2
  15. package/dist/convert/index.cjs +1 -1
  16. package/dist/convert/index.cjs.map +1 -1
  17. package/dist/convert/index.js +3 -2
  18. package/dist/index.cjs +1 -1
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/player/index.cjs +470 -122
  22. package/dist/player/index.cjs.map +1 -1
  23. package/dist/player/index.d.cts +12 -2
  24. package/dist/player/index.d.ts +12 -2
  25. package/dist/player/index.js +85 -60
  26. package/dist/player/index.js.map +1 -1
  27. package/dist/{schedule-DN-8X17d.d.ts → schedule-ClyON98C.d.ts} +22 -10
  28. package/dist/{schedule-CRpjr6JH.d.cts → schedule-V-fgepvP.d.cts} +22 -10
  29. package/dist/spec/index.cjs +1 -1
  30. package/dist/spec/index.cjs.map +1 -1
  31. package/dist/spec/index.d.cts +1 -1
  32. package/dist/spec/index.d.ts +1 -1
  33. package/dist/spec/index.js +1 -1
  34. package/dist/viewer/index.cjs +366 -61
  35. package/dist/viewer/index.cjs.map +1 -1
  36. package/dist/viewer/index.d.cts +1 -1
  37. package/dist/viewer/index.d.ts +1 -1
  38. package/dist/viewer/index.js +2 -1
  39. package/dist/viewer/index.js.map +1 -1
  40. package/package.json +1 -1
  41. package/dist/batch-ZNVWIGEL.js.map +0 -1
  42. package/dist/chunk-A6Z7X2LX.js.map +0 -1
  43. package/dist/chunk-ISNAL632.js.map +0 -1
@@ -145,9 +145,305 @@ function rowLay(row) {
145
145
  return row.lay ?? printedLay(row.notes);
146
146
  }
147
147
 
148
- // src/player/schedule.ts
148
+ // src/convert/codec.ts
149
+ var LETTERS = {
150
+ s: { letter: "s" },
151
+ r: { letter: "r" },
152
+ g: { letter: "g" },
153
+ m: { letter: "m" },
154
+ p: { letter: "p" },
155
+ q: { letter: "d" },
156
+ // ধ — shuddha dha
157
+ n: { letter: "n" },
158
+ d: { letter: "d", komal: true },
159
+ // দ
160
+ t: { letter: "g", komal: true },
161
+ // জ্ঞ
162
+ k: { letter: "m", tivra: true },
163
+ // ক্ষ
164
+ u: { letter: "n", komal: true },
165
+ // ণ
166
+ v: { letter: "r", komal: true }
167
+ // ঋ
168
+ };
169
+ var STRUCTURALS = {
170
+ "%": () => ({ svara: ["madhyam", "s"] }),
171
+ // precomposed সা
172
+ "&": () => ({ svara: ["madhyam", "n"] })
173
+ // precomposed নি
174
+ };
175
+ var BAR_RUN_RE = /^(-)?([lLA[\]]+)$/;
176
+ function decodeBarRun(body) {
177
+ const m = body.match(BAR_RUN_RE);
178
+ if (!m || !/[lLA]/.test(m[2])) return void 0;
179
+ const notes = [];
180
+ if (m[1]) notes.push({ sustain: true });
181
+ const run = m[2];
182
+ let i = 0;
183
+ while (i < run.length) {
184
+ const ch = run[i];
185
+ if (ch === "l" || ch === "L") {
186
+ let j = i;
187
+ while (j < run.length && run[j] === ch) j++;
188
+ const strokes = (j - i) * (ch === "L" ? 2 : 1);
189
+ notes.push({ bar: strokes >= 4 ? "quad" : strokes >= 2 ? "double" : "single" });
190
+ i = j;
191
+ } else if (ch === "A") {
192
+ notes.push({ bar: "dari" });
193
+ i++;
194
+ } else {
195
+ notes.push({ brackets: [{ kind: "alt", edge: ch === "[" ? "open" : "close" }] });
196
+ i++;
197
+ }
198
+ }
199
+ return notes;
200
+ }
201
+ var KNOWN_RAW_RE = [
202
+ /^[wxyjEJ$<>=_]+$/,
203
+ // arc & line drawing segments
204
+ /^\|+$/,
205
+ // thin yugal-dari stop mark
206
+ /^[0-9!@#]{1,2}[fF]?:?$/,
207
+ // beat-number labels incl. 10–16 (sam tick = f/F, e.g. 1f / 1F / 2:)
208
+ /^[oOe`+,.;:iI~^*bcBCWXYFHZz'"?]$/
209
+ // dots, ticks, duration marks, legacy forms
210
+ ];
211
+ var OPEN_BRACKETS = {
212
+ "{": "repeat",
213
+ "(": "skip",
214
+ "[": "alt"
215
+ };
216
+ var CLOSE_BRACKETS = {
217
+ "}": "repeat",
218
+ ")": "skip",
219
+ "]": "alt"
220
+ };
221
+ function octaveOf(mark) {
222
+ if (mark === "f" || mark === "F") return "taar";
223
+ if (mark === "h" || mark === "H") return "mandra";
224
+ return "madhyam";
225
+ }
226
+ var BODY_RE = /^([0-9!@][fF]?)?([zj]*)(-)?([i:])?(?:([SRGMPQNDTKUV])([fFhH])?)?((?:[srgmpqndtkuv][fh]?[i:]?)+)?(a)?([fh])?([i:])?(?:([SRGMPQNDTKUV])([fFhH])?)?([0-9!@][fF]?)?(-)?$/;
227
+ function decodeChunk(chunk) {
228
+ const opens = [];
229
+ const closes = [];
230
+ let body = chunk;
231
+ while (body.length > 0) {
232
+ const kind = OPEN_BRACKETS[body[0]];
233
+ if (kind === void 0) break;
234
+ opens.push(kind);
235
+ body = body.slice(1);
236
+ }
237
+ while (body.length > 0) {
238
+ const kind = CLOSE_BRACKETS[body[body.length - 1]];
239
+ if (kind === void 0) break;
240
+ closes.unshift(kind);
241
+ body = body.slice(0, -1);
242
+ }
243
+ const finish = (notes, kind) => {
244
+ if (notes.length === 0) notes.push({});
245
+ const first = notes[0];
246
+ const last = notes[notes.length - 1];
247
+ if (opens.length > 0) first.brackets = [...opens.map((kind_) => ({ kind: kind_, edge: "open" })), ...first.brackets ?? []];
248
+ if (closes.length > 0) last.brackets = [...last.brackets ?? [], ...closes.map((kind_) => ({ kind: kind_, edge: "close" }))];
249
+ return { notes, kind };
250
+ };
251
+ if (body === "") return finish([{}], "decoded");
252
+ const barRun = decodeBarRun(body);
253
+ if (barRun) return finish(barRun, "decoded");
254
+ const structural = STRUCTURALS[body];
255
+ if (structural) return finish([structural()], "decoded");
256
+ if (KNOWN_RAW_RE.some((re) => re.test(body))) return finish([{}], "known");
257
+ const m = body.match(BODY_RE);
258
+ if (m) {
259
+ const [, , , hyphen, , kanLetter, kanOct, unitsStr, aakar, postOct, , trailKan, trailKanOct] = m;
260
+ const attachKan = (note, letter, oct) => {
261
+ const def = LETTERS[letter.toLowerCase()];
262
+ note.kan = [octaveOf(oct?.toLowerCase()), def.letter];
263
+ };
264
+ if (unitsStr) {
265
+ const units = unitsStr.match(/[srgmpqndtkuv][fh]?/g) ?? [];
266
+ const notes = units.map((unit) => {
267
+ const def = LETTERS[unit[0]];
268
+ const note = { svara: [octaveOf(unit[1]), def.letter] };
269
+ if (def.komal) note.komal = true;
270
+ if (def.tivra) note.tivra = true;
271
+ if (units.length > 1) note.durationMatra = 1 / units.length;
272
+ return note;
273
+ });
274
+ const last = notes[notes.length - 1];
275
+ if (postOct && last.svara[0] === "madhyam") {
276
+ last.svara = [octaveOf(postOct), last.svara[1]];
277
+ }
278
+ if (hyphen) notes[0].sustain = true;
279
+ if (kanLetter) attachKan(notes[0], kanLetter, kanOct);
280
+ if (trailKan && last.kan === void 0) attachKan(last, trailKan, trailKanOct);
281
+ return finish(notes, "decoded");
282
+ }
283
+ if (aakar) {
284
+ const note = hyphen ? { sustain: true } : { rest: true };
285
+ if (trailKan) attachKan(note, trailKan, trailKanOct);
286
+ else if (kanLetter) attachKan(note, kanLetter, kanOct);
287
+ return finish([note], "decoded");
288
+ }
289
+ if (hyphen && !kanLetter && !trailKan) {
290
+ return finish([{ sustain: true }], "decoded");
291
+ }
292
+ }
293
+ return finish([{}], "unknown");
294
+ }
295
+ function decodeCell(text) {
296
+ const chunks = text.split(/\s+/).filter((c) => c.length > 0);
297
+ const out = { notes: [], unknownTokens: [], knownRawTokens: [], decoded: false };
298
+ for (const chunk of chunks) {
299
+ const { notes, kind } = decodeChunk(chunk);
300
+ notes[0].raw = chunk;
301
+ if (kind === "decoded") out.decoded = true;
302
+ else if (kind === "known") out.knownRawTokens.push(chunk);
303
+ else out.unknownTokens.push(chunk);
304
+ out.notes.push(...notes);
305
+ }
306
+ return out;
307
+ }
308
+
309
+ // src/player/repeats.ts
149
310
  var UNDECODED_CELL = /[srgmpqnvtduka]/i;
150
311
  var isSilent = (n) => n.svara === void 0 && n.rest !== true && n.sustain !== true && !(n.raw !== void 0 && UNDECODED_CELL.test(n.raw));
312
+ var STOP_MARK = /^\|+$/;
313
+ var EPS = 1e-6;
314
+ var edges = (n, kind, edge) => n.brackets?.filter((b) => b.kind === kind && b.edge === edge).length ?? 0;
315
+ var sounds = (n) => n.svara !== void 0 || n.rest === true || n.sustain === true;
316
+ var matra = (notes) => notes.reduce((sum, n) => sum + (n.durationMatra ?? 1), 0);
317
+ function playOrder(doc) {
318
+ const tokens = [];
319
+ doc.parts.forEach(
320
+ (part, p) => part.rows.forEach(
321
+ (row, r) => row.notes.forEach((note, i) => tokens.push({ p, r, i, note, timed: note.bar === void 0 && !isSilent(note) }))
322
+ )
323
+ );
324
+ const { asthayi, stop, returns } = form(tokens);
325
+ const changed = changedNotes(tokens);
326
+ const out = [];
327
+ let last = -1;
328
+ const sing = (lo, hi, useChanged) => {
329
+ const repeats = [];
330
+ let owed = 0;
331
+ for (let k = lo; k < hi; ) {
332
+ const t = tokens[k];
333
+ if (edges(t.note, "repeat", "open") > 0 && repeats.at(-1)?.open !== k) repeats.push({ open: k, again: false });
334
+ const again = repeats.at(-1)?.again === true;
335
+ const skipTo = again && edges(t.note, "skip", "open") > 0 ? skipEnd(tokens, k, hi) : -1;
336
+ if (skipTo >= 0) {
337
+ if (edges(tokens[skipTo].note, "repeat", "close") > 0) repeats.pop();
338
+ k = skipTo + 1;
339
+ owed = 0;
340
+ continue;
341
+ }
342
+ if (owed > EPS && t.timed) owed -= matra([t.note]);
343
+ else {
344
+ const sub = useChanged || again ? changed.get(k) : void 0;
345
+ const fits = sub !== void 0 && matra(sub) >= matra([t.note]) - EPS;
346
+ for (const note of fits ? sub : [t.note]) out.push({ p: t.p, r: t.r, i: t.i, note });
347
+ if (t.timed) last = k;
348
+ owed = fits ? matra(sub) - matra([t.note]) : 0;
349
+ }
350
+ const loop = edges(t.note, "repeat", "close") > 0 ? repeats.at(-1) : void 0;
351
+ if (loop !== void 0 && !loop.again) {
352
+ loop.again = true;
353
+ k = loop.open;
354
+ owed = 0;
355
+ continue;
356
+ }
357
+ if (loop !== void 0) repeats.pop();
358
+ k++;
359
+ }
360
+ };
361
+ let from = 0;
362
+ for (const ret of returns) {
363
+ sing(from, ret.resume, false);
364
+ if (last !== stop) sing(asthayi, stop + 1, ret.changed);
365
+ from = ret.resume;
366
+ }
367
+ sing(from, tokens.length, false);
368
+ return out;
369
+ }
370
+ function form(tokens) {
371
+ const first = /* @__PURE__ */ new Map();
372
+ const last = /* @__PURE__ */ new Map();
373
+ tokens.forEach((t, k) => {
374
+ if (!t.timed) return;
375
+ const row = `${t.p}.${t.r}`;
376
+ if (!first.has(row)) first.set(row, k);
377
+ last.set(row, k);
378
+ });
379
+ const opens = [];
380
+ let opening = false;
381
+ tokens.forEach((t, k) => {
382
+ if (t.note.bar !== "double") return;
383
+ const row = `${t.p}.${t.r}`;
384
+ const before = (first.get(row) ?? Infinity) < k;
385
+ const after = (last.get(row) ?? -1) > k;
386
+ opening = before !== after ? after : !opening;
387
+ if (opening) opens.push(k);
388
+ });
389
+ const returns = [];
390
+ for (let k = 0; k < tokens.length; k++) {
391
+ let at = -1;
392
+ let open = false;
393
+ let changed = false;
394
+ let end = false;
395
+ for (; k < tokens.length && !tokens[k].timed; k++) {
396
+ const n = tokens[k].note;
397
+ if (n.bar === "quad") end = true;
398
+ if (n.bar === "double" && !opens.includes(k)) at = k;
399
+ if (edges(n, "alt", "open") > 0) open = true;
400
+ if (open && edges(n, "alt", "close") > 0) {
401
+ at = k;
402
+ changed = true;
403
+ }
404
+ }
405
+ if (at >= 0 && !end) returns.push({ resume: at + 1, changed });
406
+ }
407
+ const top = opens[0];
408
+ const asthayi = top !== void 0 && top < (returns[0]?.resume ?? Infinity) ? top : 0;
409
+ const asthayiEnd = Math.min(returns[0]?.resume ?? Infinity, opens.find((k) => k > asthayi) ?? Infinity, tokens.length);
410
+ const lastSung = tokens.reduce((last2, t, k) => t.timed ? k : last2, -1);
411
+ const stop = tokens.findIndex(
412
+ (t, k) => k >= asthayi && k < asthayiEnd && k < lastSung && t.note.above?.some((a) => STOP_MARK.test(a.raw ?? "")) === true
413
+ );
414
+ return { asthayi, stop, returns: stop < 0 ? [] : returns };
415
+ }
416
+ function changedNotes(tokens) {
417
+ const out = /* @__PURE__ */ new Map();
418
+ let row = "";
419
+ let inside = false;
420
+ tokens.forEach((t, k) => {
421
+ if (row !== `${t.p}.${t.r}`) {
422
+ row = `${t.p}.${t.r}`;
423
+ inside = false;
424
+ }
425
+ const notes = [];
426
+ for (const above of t.note.above ?? []) {
427
+ const read = above.raw !== void 0 && !sounds(above) && above.brackets === void 0;
428
+ for (const a of read ? decodeCell(above.raw).notes : [above]) {
429
+ if (edges(a, "alt", "open") > 0) inside = true;
430
+ if (inside && sounds(a)) notes.push(a);
431
+ if (edges(a, "alt", "close") > 0) inside = false;
432
+ }
433
+ }
434
+ if (notes.length > 0) out.set(k, notes);
435
+ });
436
+ return out;
437
+ }
438
+ function skipEnd(tokens, k, hi) {
439
+ for (let depth = 0, j = k; j < hi; j++) {
440
+ depth += edges(tokens[j].note, "skip", "open") - edges(tokens[j].note, "skip", "close");
441
+ if (depth <= 0) return j;
442
+ }
443
+ return -1;
444
+ }
445
+
446
+ // src/player/schedule.ts
151
447
  var ARC_RAW_RE = /^[wWxXyYjJeEzZ$=_<>`\\]+$/;
152
448
  var isArcRaw = (raw) => raw !== void 0 && ARC_RAW_RE.test(raw);
153
449
  var MEEND_MARKS = { start: "w", end: "x", both: "xw" };
@@ -190,26 +486,45 @@ function graceTouches(ev) {
190
486
  ...ev.resh.map((midi, g) => ({ midi, beat: ev.beat + GRACE_STEP * (g + 1) }))
191
487
  ];
192
488
  }
193
- var rowOf = (path) => path.slice(0, path.lastIndexOf("."));
489
+ var rowOf = (path) => path.split(".").slice(0, 2).join(".");
490
+ var noteOf = (path) => Number(path.split(".")[2] ?? 0);
491
+ var byRow = (a, b) => {
492
+ const [ap = 0, ar = 0] = a.split(".").map(Number);
493
+ const [bp = 0, br = 0] = b.split(".").map(Number);
494
+ return ap - bp || ar - br;
495
+ };
194
496
  function scheduleLines(schedule) {
195
497
  const lines = [];
498
+ let note = Infinity;
196
499
  schedule.events.forEach((ev, i) => {
197
- if (lines.at(-1)?.row !== rowOf(ev.path)) lines.push({ row: rowOf(ev.path), first: i, beat: ev.beat, endBeat: 0 });
500
+ const n = noteOf(ev.path);
501
+ if (lines.at(-1)?.row !== rowOf(ev.path) || n < note) lines.push({ row: rowOf(ev.path), first: i, beat: ev.beat, endBeat: 0 });
502
+ note = n;
198
503
  });
199
504
  lines.forEach((line, k) => line.endBeat = lines[k + 1]?.beat ?? schedule.totalBeats);
200
505
  return lines;
201
506
  }
202
- function eventAt(events, path) {
203
- const [p = 0, r = 0, n = 0] = path.split(".").map(Number);
204
- const inRow = (ev) => ev !== void 0 && rowOf(ev.path) === `${p}.${r}`;
205
- let i = events.findIndex((ev) => {
206
- const [ep = 0, er = 0, en = 0] = ev.path.split(".").map(Number);
207
- return ep > p || ep === p && (er > r || er === r && en >= n);
507
+ function lineAt(lines, i) {
508
+ let k = 0;
509
+ while (k + 1 < lines.length && lines[k + 1].first <= i) k++;
510
+ return k;
511
+ }
512
+ function lineFor(lines, path, near) {
513
+ let row;
514
+ for (const line of lines) if (byRow(line.row, rowOf(path)) >= 0 && (row === void 0 || byRow(line.row, row) < 0)) row = line.row;
515
+ let best = -1;
516
+ lines.forEach((line, k) => {
517
+ if (line.row === row && (best < 0 || Math.abs(k - near) < Math.abs(best - near))) best = k;
208
518
  });
209
- if (!inRow(events[i])) {
210
- const last = (i < 0 ? events.length : i) - 1;
211
- if (inRow(events[last])) i = last;
212
- }
519
+ return best;
520
+ }
521
+ function eventAt(events, path, near = -1) {
522
+ const lines = scheduleLines({ events, totalBeats: 0 });
523
+ const k = lineFor(lines, path, near < 0 ? -1 : lineAt(lines, near));
524
+ if (k < 0) return -1;
525
+ const end = lines[k + 1]?.first ?? events.length;
526
+ let i = lines[k].first;
527
+ if (lines[k].row === rowOf(path)) while (i < end - 1 && noteOf(events[i].path) < noteOf(path)) i++;
213
528
  while (i > 0 && events[i].glide === true) i--;
214
529
  return i;
215
530
  }
@@ -221,67 +536,76 @@ function buildSchedule(doc, opts = {}) {
221
536
  let cursor = 0;
222
537
  let lastPitched = -1;
223
538
  let samMatra;
224
- let matra = 0;
539
+ let matra2 = 0;
225
540
  const speeds = resolveLaySpeeds(doc.meta.laySpeeds, opts.laySpeeds);
226
541
  const withLay = opts.lay !== false;
227
542
  const tempo = [{ beat: 0, endBeat: 0, scale: 1, lay: "drut", taal: doc.meta.taal }];
543
+ const rows = /* @__PURE__ */ new Map();
228
544
  let scale = 1;
545
+ let layNow = "drut";
229
546
  let taal = doc.meta.taal;
230
- doc.parts.forEach((part, p) => {
231
- part.rows.forEach((row, r) => {
547
+ doc.parts.forEach(
548
+ (part, p) => part.rows.forEach((row, r) => {
232
549
  const lay = withLay ? rowLay(row) : void 0;
233
- const layChanged = lay !== void 0 && speeds[lay] !== scale;
234
- const nextTaal = row.taal !== void 0 ? row.taal : taal;
235
- if (layChanged || !sameTaal(nextTaal, taal)) {
236
- if (layChanged) scale = speeds[lay];
237
- taal = nextTaal;
238
- const last = tempo[tempo.length - 1];
239
- const seg = { beat: cursor, endBeat: cursor, scale, lay: layChanged ? lay : last.lay, taal };
550
+ if (lay !== void 0 && speeds[lay] !== scale) {
551
+ scale = speeds[lay];
552
+ layNow = lay;
553
+ }
554
+ if (row.taal !== void 0) taal = row.taal;
555
+ rows.set(`${p}.${r}`, { scale, lay: layNow, taal, glides: meendContinuations(row.notes) });
556
+ })
557
+ );
558
+ const visits = opts.repeats === true ? playOrder(doc) : doc.parts.flatMap((part, p) => part.rows.flatMap((row, r) => row.notes.map((note, i) => ({ p, r, i, note }))));
559
+ let rowNow = "";
560
+ for (const { p, r, i, note } of visits) {
561
+ const row = rows.get(`${p}.${r}`);
562
+ if (rowNow !== `${p}.${r}`) {
563
+ rowNow = `${p}.${r}`;
564
+ const last = tempo[tempo.length - 1];
565
+ if (row.scale !== last.scale || !sameTaal(row.taal, last.taal)) {
566
+ const seg = { beat: cursor, endBeat: cursor, scale: row.scale, lay: row.lay, taal: row.taal };
240
567
  if (last.beat === cursor) tempo[tempo.length - 1] = seg;
241
568
  else {
242
569
  last.endBeat = cursor;
243
570
  tempo.push(seg);
244
571
  }
245
572
  }
246
- const glides = meendContinuations(row.notes);
247
- row.notes.forEach((note, i) => {
248
- if (samMatra === void 0 && note.above !== void 0) {
249
- for (const a of note.above) {
250
- if (a.raw !== void 0 && SAM_TICK.test(a.raw)) {
251
- samMatra = matra;
252
- break;
253
- }
254
- }
255
- }
256
- if (note.bar !== void 0 || isSilent(note)) return;
257
- const path = `${p}.${r}.${i}`;
258
- const written = note.durationMatra ?? 1;
259
- const dur = written / scale;
260
- const kan = graceMidis(note.kan, tonicPc, baseOctave);
261
- const resh = graceMidis(note.resh, tonicPc, baseOctave);
262
- if (note.svara !== void 0) {
263
- events.push({
264
- beat: cursor,
265
- durationBeats: dur,
266
- midi: svaraToMidi(note, tonicPc, baseOctave),
267
- kan,
268
- resh,
269
- path,
270
- kind: note.sustain === true ? "sustain" : "note",
271
- ...glides.has(i) && lastPitched >= 0 ? { glide: true } : {}
272
- });
273
- lastPitched = events.length - 1;
274
- } else if (note.sustain === true) {
275
- if (lastPitched >= 0) events[lastPitched].durationBeats += dur;
276
- events.push({ beat: cursor, durationBeats: dur, midi: null, kan, resh, path, kind: "sustain" });
277
- } else {
278
- events.push({ beat: cursor, durationBeats: dur, midi: null, kan, resh, path, kind: "rest" });
573
+ }
574
+ if (samMatra === void 0 && note.above !== void 0) {
575
+ for (const a of note.above) {
576
+ if (a.raw !== void 0 && SAM_TICK.test(a.raw)) {
577
+ samMatra = matra2;
578
+ break;
279
579
  }
280
- cursor += dur;
281
- matra += written;
580
+ }
581
+ }
582
+ if (note.bar !== void 0 || isSilent(note)) continue;
583
+ const path = `${p}.${r}.${i}`;
584
+ const written = note.durationMatra ?? 1;
585
+ const dur = written / row.scale;
586
+ const kan = graceMidis(note.kan, tonicPc, baseOctave);
587
+ const resh = graceMidis(note.resh, tonicPc, baseOctave);
588
+ if (note.svara !== void 0) {
589
+ events.push({
590
+ beat: cursor,
591
+ durationBeats: dur,
592
+ midi: svaraToMidi(note, tonicPc, baseOctave),
593
+ kan,
594
+ resh,
595
+ path,
596
+ kind: note.sustain === true ? "sustain" : "note",
597
+ ...row.glides.has(i) && lastPitched >= 0 ? { glide: true } : {}
282
598
  });
283
- });
284
- });
599
+ lastPitched = events.length - 1;
600
+ } else if (note.sustain === true) {
601
+ if (lastPitched >= 0) events[lastPitched].durationBeats += dur;
602
+ events.push({ beat: cursor, durationBeats: dur, midi: null, kan, resh, path, kind: "sustain" });
603
+ } else {
604
+ events.push({ beat: cursor, durationBeats: dur, midi: null, kan, resh, path, kind: "rest" });
605
+ }
606
+ cursor += dur;
607
+ matra2 += written;
608
+ }
285
609
  const numBeats = tempo[0].taal?.numBeats;
286
610
  const pickupMatra = samMatra !== void 0 && numBeats !== void 0 && numBeats > 0 ? (numBeats - samMatra % numBeats) % numBeats : 0;
287
611
  tempo[tempo.length - 1].endBeat = cursor;
@@ -589,14 +913,6 @@ var TAAL_MODE_TEMPO = [
589
913
  { beat: 0, endBeat: Infinity, scale: 1, lay: "drut" }
590
914
  ];
591
915
  var MEDIA_LEAD_SEC = 0.25;
592
- function lineOf(lines, path) {
593
- const [p = 0, r = 0] = path.split(".").map(Number);
594
- const k = lines.findIndex((line) => {
595
- const [lp = 0, lr = 0] = line.row.split(".").map(Number);
596
- return lp > p || lp === p && lr >= r;
597
- });
598
- return k < 0 ? lines.length - 1 : k;
599
- }
600
916
  var seekableGrain = (Tone) => class extends Tone.GrainPlayer {
601
917
  _start(time, offset, duration) {
602
918
  super._start(time, offset === void 0 ? offset : this.toSeconds(offset) * this.playbackRate, duration);
@@ -657,9 +973,13 @@ var SwaralipiPlayer = class {
657
973
  /* Practice: where the playhead is, what repeats, whose turn it is. */
658
974
  /** The last note played or sought — what "this line" means to the controls. */
659
975
  #cursor = null;
976
+ /** The cursor as an event index: which pass of a line sung more than once. */
977
+ #at = -1;
660
978
  /** Where the next fresh play() starts (a seek made while stopped). */
661
979
  #startAt = null;
662
980
  #loop = null;
981
+ /** The repeat as line indices, the passes it holds. */
982
+ #span = null;
663
983
  #echo = false;
664
984
  /** With echo on, every other pass is the learner's: the melody rests. */
665
985
  #turn = false;
@@ -685,6 +1005,7 @@ var SwaralipiPlayer = class {
685
1005
  instruments: options.instruments ?? Object.keys(INSTRUMENTS),
686
1006
  rhythm: options.rhythm ?? "tabla",
687
1007
  tanpura: options.tanpura ?? true,
1008
+ repeats: options.repeats ?? true,
688
1009
  previewLimitSec: options.previewLimitSec ?? Infinity,
689
1010
  onHighlight: options.onHighlight,
690
1011
  onLoop: options.onLoop,
@@ -832,22 +1153,25 @@ var SwaralipiPlayer = class {
832
1153
  */
833
1154
  seek(path) {
834
1155
  const plan = this.#plan();
835
- const i = plan === void 0 ? -1 : eventAt(plan.schedule.events, path);
836
- if (plan === void 0 || i < 0) return;
837
- const { lines } = plan;
1156
+ if (plan !== void 0) this.#seekTo(plan, eventAt(plan.schedule.events, path, this.#at));
1157
+ }
1158
+ /** seek(), by event index — the pass, not just the line. */
1159
+ #seekTo(plan, i) {
1160
+ if (i < 0) return;
838
1161
  const at = plan.schedule.events[i].path;
839
1162
  this.#cursor = at;
1163
+ this.#at = i;
840
1164
  this.#turn = false;
841
- const k = lineOf(lines, at);
842
- const [a, b] = this.#loopSpan(lines);
843
- if (this.#loop !== null && (k < a || k > b)) this.#loop = { from: lines[k].row, to: lines[k].row };
1165
+ const k = lineAt(plan.lines, i);
1166
+ const [a, b] = this.#loopSpan();
1167
+ if (this.#span !== null && (k < a || k > b)) this.#setSpan(plan.lines, k, k);
844
1168
  this.#emitLoop();
845
1169
  if (this.#timeline !== void 0 && (this.#state === "playing" || this.#state === "paused")) {
846
1170
  this.#applyLoop();
847
1171
  this.#jump(this.#eventSec(i));
848
1172
  if (this.#state === "paused") this.#emitHighlight(at);
849
1173
  } else {
850
- this.#startAt = at;
1174
+ this.#startAt = i;
851
1175
  this.#emitHighlight(at);
852
1176
  }
853
1177
  }
@@ -860,20 +1184,41 @@ var SwaralipiPlayer = class {
860
1184
  setLoop(from, to) {
861
1185
  this.#turn = false;
862
1186
  if (from === null) {
863
- this.#loop = null;
1187
+ this.#span = this.#loop = null;
864
1188
  this.#echo = false;
865
- } else {
866
- const lines = this.#lines();
867
- if (lines.length === 0) return;
868
- const here = this.#cursor === null ? -1 : lineOf(lines, this.#cursor);
869
- const a = from === void 0 ? Math.max(here, 0) : lineOf(lines, from);
870
- const b = to === void 0 ? a : lineOf(lines, to);
871
- this.#loop = { from: lines[Math.min(a, b)].row, to: lines[Math.max(a, b)].row };
872
- if (here < Math.min(a, b) || here > Math.max(a, b)) return this.seek(this.#loop.from);
1189
+ this.#applyLoop();
1190
+ this.#emitLoop();
1191
+ return;
873
1192
  }
1193
+ const plan = this.#plan();
1194
+ if (plan === void 0 || plan.lines.length === 0) return;
1195
+ const { lines } = plan;
1196
+ const here = this.#at < 0 ? -1 : lineAt(lines, this.#at);
1197
+ const a = from === void 0 ? Math.max(here, 0) : this.#lineOf(lines, from, here);
1198
+ this.#repeat(plan, a, to === void 0 ? a : this.#lineOf(lines, to, a));
1199
+ }
1200
+ /** Repeat lines a..b (indices, either order); the playhead joins them. */
1201
+ #repeat(plan, a, b) {
1202
+ this.#setSpan(plan.lines, a, b);
1203
+ const [lo, hi] = this.#loopSpan();
1204
+ const here = this.#at < 0 ? -1 : lineAt(plan.lines, this.#at);
1205
+ if (here < lo || here > hi) return this.#seekTo(plan, plan.lines[lo].first);
874
1206
  this.#applyLoop();
875
1207
  this.#emitLoop();
876
1208
  }
1209
+ /** The repeat's passes, and the rows they cover for the sheet's shading —
1210
+ * written order, since a span can cross a jump back to an earlier row. */
1211
+ #setSpan(lines, a, b) {
1212
+ this.#span = [Math.min(a, b), Math.max(a, b)];
1213
+ const rows = lines.slice(this.#span[0], this.#span[1] + 1).map((line) => line.row).sort(byRow);
1214
+ this.#loop = { from: rows[0], to: rows.at(-1) };
1215
+ }
1216
+ /** The line `path` names, at its pass nearest line `near`; the last line
1217
+ * when nothing sounds from there on. */
1218
+ #lineOf(lines, path, near) {
1219
+ const k = lineFor(lines, path, near);
1220
+ return k < 0 ? lines.length - 1 : k;
1221
+ }
877
1222
  /**
878
1223
  * Follow every pass of the repeat with a silent one: the tabla and tanpura
879
1224
  * keep time and the highlight keeps moving, but the melody rests so the
@@ -895,22 +1240,22 @@ var SwaralipiPlayer = class {
895
1240
  const plan = this.#plan();
896
1241
  if (plan === void 0 || plan.lines.length === 0) return;
897
1242
  const { lines, schedule } = plan;
898
- const k = this.#cursor === null ? -1 : lineOf(lines, this.#cursor);
899
- const into = k < 0 ? 0 : schedule.events[eventAt(schedule.events, this.#cursor)].beat - lines[k].beat;
1243
+ const k = this.#at < 0 ? -1 : lineAt(lines, this.#at);
1244
+ const into = k < 0 ? 0 : schedule.events[this.#at].beat - lines[k].beat;
900
1245
  const step = delta < 0 && this.#state === "playing" && into > 1 ? delta + 1 : delta;
901
- this.seek(lines[Math.min(Math.max(k + step, 0), lines.length - 1)].row);
1246
+ this.#seekTo(plan, lines[Math.min(Math.max(k + step, 0), lines.length - 1)].first);
902
1247
  }
903
1248
  /** Move by single notes (rests and holds included — each is a column). */
904
1249
  stepNote(delta) {
905
- const events = this.#plan()?.schedule.events ?? [];
906
- let i = this.#cursor === null ? -1 : eventAt(events, this.#cursor);
1250
+ const plan = this.#plan();
1251
+ const events = plan?.schedule.events ?? [];
1252
+ let i = this.#at;
907
1253
  for (let left = Math.abs(delta); left > 0; left--) {
908
1254
  do
909
1255
  i += Math.sign(delta);
910
1256
  while (events[i]?.glide === true);
911
1257
  }
912
- const ev = events[Math.min(Math.max(i, 0), events.length - 1)];
913
- if (ev !== void 0) this.seek(ev.path);
1258
+ if (plan !== void 0 && events.length > 0) this.#seekTo(plan, Math.min(Math.max(i, 0), events.length - 1));
914
1259
  }
915
1260
  /**
916
1261
  * Stop and rewind to the start. When sounding, the accompaniment (tabla /
@@ -1105,21 +1450,21 @@ var SwaralipiPlayer = class {
1105
1450
  }
1106
1451
  /** Grow the repeat (or start one from the current line) to take in `path`'s line. */
1107
1452
  #extendLoop(path) {
1108
- const lines = this.#lines();
1109
- if (lines.length === 0) return;
1110
- const k = lineOf(lines, path);
1111
- const here = this.#cursor === null ? k : lineOf(lines, this.#cursor);
1112
- const [a, b] = this.#loop === null ? [here, here] : this.#loopSpan(lines);
1113
- this.setLoop(lines[Math.min(a, k)].row, lines[Math.max(b, k)].row);
1453
+ const plan = this.#plan();
1454
+ if (plan === void 0 || plan.lines.length === 0) return;
1455
+ const here = this.#at < 0 ? -1 : lineAt(plan.lines, this.#at);
1456
+ const k = this.#lineOf(plan.lines, path, here);
1457
+ const [a, b] = this.#span ?? (here < 0 ? [k, k] : [here, here]);
1458
+ this.#repeat(plan, Math.min(a, k), Math.max(b, k));
1114
1459
  }
1115
1460
  /** Shift+↑/↓: one more line above / below the repeat. */
1116
1461
  #extendBy(delta) {
1117
- const lines = this.#lines();
1118
- if (lines.length === 0) return;
1119
- const here = this.#cursor === null ? 0 : lineOf(lines, this.#cursor);
1120
- const [a, b] = this.#loop === null ? [here, here] : this.#loopSpan(lines);
1121
- const k = Math.min(Math.max((delta < 0 ? a : b) + delta, 0), lines.length - 1);
1122
- this.#extendLoop(lines[k].row);
1462
+ const plan = this.#plan();
1463
+ if (plan === void 0 || plan.lines.length === 0) return;
1464
+ const here = this.#at < 0 ? 0 : lineAt(plan.lines, this.#at);
1465
+ const [a, b] = this.#span ?? [here, here];
1466
+ const k = Math.min(Math.max((delta < 0 ? a : b) + delta, 0), plan.lines.length - 1);
1467
+ this.#repeat(plan, Math.min(a, k), Math.max(b, k));
1123
1468
  }
1124
1469
  /**
1125
1470
  * Check whether an instrument's samples are reachable (its CDN is up / the
@@ -1179,7 +1524,8 @@ var SwaralipiPlayer = class {
1179
1524
  }
1180
1525
  /** A new document starts with nothing repeated, from the top. */
1181
1526
  #resetPractice() {
1182
- this.#cursor = this.#startAt = this.#loop = null;
1527
+ this.#cursor = this.#startAt = this.#loop = this.#span = null;
1528
+ this.#at = -1;
1183
1529
  this.#echo = this.#turn = false;
1184
1530
  this.#listened = 0;
1185
1531
  this.#emitLoop();
@@ -1193,12 +1539,9 @@ var SwaralipiPlayer = class {
1193
1539
  const schedule = buildSchedule(this.#doc, this.#scheduleOpts());
1194
1540
  return { schedule, lines: scheduleLines(schedule) };
1195
1541
  }
1196
- #lines() {
1197
- return this.#plan()?.lines ?? [];
1198
- }
1199
1542
  /** The repeat as line indices (`[0, -1]`, i.e. empty, when off). */
1200
- #loopSpan(lines) {
1201
- return this.#loop === null ? [0, -1] : [lineOf(lines, this.#loop.from), lineOf(lines, this.#loop.to)];
1543
+ #loopSpan() {
1544
+ return this.#span ?? [0, -1];
1202
1545
  }
1203
1546
  /** The repeat's lines on the arranged timeline, cached for the per-note
1204
1547
  * checks (#hears), which run on every note. */
@@ -1227,8 +1570,8 @@ var SwaralipiPlayer = class {
1227
1570
  #applyLoop() {
1228
1571
  const transport = this.#tone?.getTransport();
1229
1572
  if (transport === void 0 || this.#timeline === void 0) return;
1230
- this.#loopLines = this.#loopSpan(this.#timeline.lines);
1231
- if (this.#loop === null) {
1573
+ this.#loopLines = this.#loopSpan();
1574
+ if (this.#span === null) {
1232
1575
  transport.loop = false;
1233
1576
  return;
1234
1577
  }
@@ -1239,8 +1582,8 @@ var SwaralipiPlayer = class {
1239
1582
  * else the top. */
1240
1583
  #startSec() {
1241
1584
  const events = this.#timeline?.schedule.events ?? [];
1242
- if (this.#startAt !== null) return this.#eventSec(Math.max(eventAt(events, this.#startAt), 0));
1243
- return this.#loop === null ? 0 : this.#loopSec()[0];
1585
+ if (this.#startAt !== null) return this.#eventSec(Math.min(this.#startAt, events.length - 1));
1586
+ return this.#span === null ? 0 : this.#loopSec()[0];
1244
1587
  }
1245
1588
  /** Move the transport; notes still ringing from the old place are let go. */
1246
1589
  #jump(sec) {
@@ -1329,7 +1672,8 @@ var SwaralipiPlayer = class {
1329
1672
  return {
1330
1673
  tonic: this.#opts.tonic,
1331
1674
  baseOctave: this.#opts.baseOctave,
1332
- laySpeeds: this.#opts.laySpeeds
1675
+ laySpeeds: this.#opts.laySpeeds,
1676
+ repeats: this.#opts.repeats
1333
1677
  };
1334
1678
  }
1335
1679
  /** Effective tempo: snapped to the taal's available tabla tempos while a
@@ -1347,9 +1691,10 @@ var SwaralipiPlayer = class {
1347
1691
  * from the note it was on — slowing down a repeated line keeps repeating it. */
1348
1692
  #reapply() {
1349
1693
  if (this.#state !== "playing") return;
1350
- const at = this.#cursor;
1694
+ const at = this.#at;
1351
1695
  this.#halt();
1352
- void this.play(at ?? void 0);
1696
+ this.#startAt = at < 0 ? null : at;
1697
+ void this.play();
1353
1698
  }
1354
1699
  /** The master bus everything routes through (created once, → destination). */
1355
1700
  #ensureMaster(Tone) {
@@ -1393,13 +1738,13 @@ var SwaralipiPlayer = class {
1393
1738
  while (lines[line + 1] !== void 0 && lines[line + 1].first <= i) line++;
1394
1739
  const k = line;
1395
1740
  const heard = (time) => time >= this.#fence && this.#hears(k);
1396
- const sounds = (time) => heard(time) && !this.#turn;
1741
+ const sounds2 = (time) => heard(time) && !this.#turn;
1397
1742
  const at = ev.beat * spb + shift;
1398
1743
  if (ev.glide !== true) {
1399
1744
  for (const g of graceTouches(ev)) {
1400
1745
  const graceName = midiToSampleName(g.midi);
1401
1746
  transport.schedule((time) => {
1402
- if (sounds(time)) melody.triggerAttackRelease(graceName, spb * 0.4, time);
1747
+ if (sounds2(time)) melody.triggerAttackRelease(graceName, spb * 0.4, time);
1403
1748
  }, Math.max(g.beat * spb + shift, 0));
1404
1749
  }
1405
1750
  }
@@ -1415,7 +1760,7 @@ var SwaralipiPlayer = class {
1415
1760
  ];
1416
1761
  const end = (last.beat + last.durationBeats - ev.beat) * spb;
1417
1762
  transport.schedule((time) => {
1418
- if (!sounds(time)) return;
1763
+ if (!sounds2(time)) return;
1419
1764
  triggerGlide(
1420
1765
  Tone,
1421
1766
  glide.buffers,
@@ -1428,7 +1773,7 @@ var SwaralipiPlayer = class {
1428
1773
  }, at);
1429
1774
  } else {
1430
1775
  transport.schedule((time) => {
1431
- if (sounds(time)) melody.triggerAttackRelease(name, dur, time);
1776
+ if (sounds2(time)) melody.triggerAttackRelease(name, dur, time);
1432
1777
  }, at);
1433
1778
  }
1434
1779
  }
@@ -1441,6 +1786,7 @@ var SwaralipiPlayer = class {
1441
1786
  if (!heard(time)) return;
1442
1787
  const turn = this.#turn;
1443
1788
  this.#cursor = ev.path;
1789
+ this.#at = i;
1444
1790
  draw.schedule(() => this.#emitHighlight(ev.path, turn), time);
1445
1791
  }, at);
1446
1792
  });
@@ -1677,7 +2023,8 @@ async function exportAudio(doc, options = {}) {
1677
2023
  const schedule = buildSchedule(doc, {
1678
2024
  tonic: options.tonic,
1679
2025
  baseOctave: options.baseOctave,
1680
- laySpeeds: options.laySpeeds
2026
+ laySpeeds: options.laySpeeds,
2027
+ repeats: options.repeats ?? true
1681
2028
  });
1682
2029
  const range = midiRange2(schedule);
1683
2030
  const taalId = resolveTaal(doc, options.taal);
@@ -2050,7 +2397,8 @@ function exportMidi(doc, options = {}) {
2050
2397
  const schedule = buildSchedule(doc, {
2051
2398
  tonic: options.tonic,
2052
2399
  baseOctave: options.baseOctave,
2053
- laySpeeds: options.laySpeeds
2400
+ laySpeeds: options.laySpeeds,
2401
+ repeats: options.repeats ?? true
2054
2402
  });
2055
2403
  const bpm = options.bpm ?? doc.meta.bpm ?? 90;
2056
2404
  const drums = options.drums ? (seg) => {