@ugfoundation/swaralipi-js 0.11.1 → 0.13.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 (50) hide show
  1. package/README.md +7 -1
  2. package/dist/{batch-RAM2S4QO.js → batch-BS2AIZOR.js} +6 -5
  3. package/dist/batch-BS2AIZOR.js.map +1 -0
  4. package/dist/{chunk-3EWAKXLM.js → chunk-3ZBNANDF.js} +3 -3
  5. package/dist/{chunk-3EWAKXLM.js.map → chunk-3ZBNANDF.js.map} +1 -1
  6. package/dist/{chunk-LRIAUFKF.js → chunk-5N6HGPFH.js} +8 -168
  7. package/dist/chunk-5N6HGPFH.js.map +1 -0
  8. package/dist/chunk-BSZ3WQUP.js +164 -0
  9. package/dist/chunk-BSZ3WQUP.js.map +1 -0
  10. package/dist/{chunk-SR7NFEGL.js → chunk-KOHSKHXI.js} +5 -3
  11. package/dist/chunk-KOHSKHXI.js.map +1 -0
  12. package/dist/{chunk-CF3JWIHP.js → chunk-ZNJTMQIX.js} +228 -64
  13. package/dist/chunk-ZNJTMQIX.js.map +1 -0
  14. package/dist/cli.cjs +6 -4
  15. package/dist/cli.cjs.map +1 -1
  16. package/dist/cli.js +3 -3
  17. package/dist/convert/index.cjs +6 -4
  18. package/dist/convert/index.cjs.map +1 -1
  19. package/dist/convert/index.js +4 -3
  20. package/dist/index.cjs +4 -2
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +1 -1
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.js +2 -2
  25. package/dist/{lay-DypMIIuf.d.ts → lay-CWlEhImP.d.ts} +8 -1
  26. package/dist/{lay-CvZeVon3.d.cts → lay-CXhC-HRO.d.cts} +8 -1
  27. package/dist/player/index.cjs +473 -123
  28. package/dist/player/index.cjs.map +1 -1
  29. package/dist/player/index.d.cts +14 -4
  30. package/dist/player/index.d.ts +14 -4
  31. package/dist/player/index.js +87 -62
  32. package/dist/player/index.js.map +1 -1
  33. package/dist/{schedule-DjcqM6A4.d.ts → schedule-ClyON98C.d.ts} +23 -11
  34. package/dist/{schedule-C4sP7uFu.d.cts → schedule-V-fgepvP.d.cts} +23 -11
  35. package/dist/spec/index.cjs +4 -2
  36. package/dist/spec/index.cjs.map +1 -1
  37. package/dist/spec/index.d.cts +2 -2
  38. package/dist/spec/index.d.ts +2 -2
  39. package/dist/spec/index.js +2 -2
  40. package/dist/viewer/index.cjs +369 -62
  41. package/dist/viewer/index.cjs.map +1 -1
  42. package/dist/viewer/index.d.cts +2 -2
  43. package/dist/viewer/index.d.ts +2 -2
  44. package/dist/viewer/index.js +3 -2
  45. package/dist/viewer/index.js.map +1 -1
  46. package/package.json +1 -1
  47. package/dist/batch-RAM2S4QO.js.map +0 -1
  48. package/dist/chunk-CF3JWIHP.js.map +0 -1
  49. package/dist/chunk-LRIAUFKF.js.map +0 -1
  50. package/dist/chunk-SR7NFEGL.js.map +0 -1
@@ -21,8 +21,10 @@ var TAALS = {
21
21
  // ঝম্পক
22
22
  dhamaar: { id: "dhamaar", numBeats: 14, bhaags: [5, 2, 3, 4], sam: 0, khaali: [7] },
23
23
  // ধামার
24
- rupakra: { id: "rupakra", numBeats: 8, bhaags: [3, 2, 3], sam: 0 }
24
+ rupakra: { id: "rupakra", numBeats: 8, bhaags: [3, 2, 3], sam: 0 },
25
25
  // রূপকড়া
26
+ khemta: { id: "khemta", numBeats: 12, bhaags: [3, 3, 3, 3], sam: 0, khaali: [6] }
27
+ // খেমটা
26
28
  };
27
29
  function sameTaal(a, b) {
28
30
  if (a === b) return true;
@@ -143,9 +145,305 @@ function rowLay(row) {
143
145
  return row.lay ?? printedLay(row.notes);
144
146
  }
145
147
 
146
- // 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
147
310
  var UNDECODED_CELL = /[srgmpqnvtduka]/i;
148
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
+ const sing = (lo, hi, useChanged) => {
328
+ const repeats = [];
329
+ let owed = 0;
330
+ for (let k = lo; k < hi; ) {
331
+ const t = tokens[k];
332
+ if (edges(t.note, "repeat", "open") > 0 && repeats.at(-1)?.open !== k) repeats.push({ open: k, again: false });
333
+ const again = repeats.at(-1)?.again === true;
334
+ const skipTo = again && edges(t.note, "skip", "open") > 0 ? skipEnd(tokens, k, hi) : -1;
335
+ if (skipTo >= 0) {
336
+ if (edges(tokens[skipTo].note, "repeat", "close") > 0) repeats.pop();
337
+ k = skipTo + 1;
338
+ owed = 0;
339
+ continue;
340
+ }
341
+ if (owed > EPS && t.timed) owed -= matra([t.note]);
342
+ else {
343
+ const sub = useChanged || again ? changed.get(k) : void 0;
344
+ const fits = sub !== void 0 && matra(sub) >= matra([t.note]) - EPS;
345
+ for (const note of fits ? sub : [t.note]) out.push({ p: t.p, r: t.r, i: t.i, note });
346
+ owed = fits ? matra(sub) - matra([t.note]) : 0;
347
+ }
348
+ const loop = edges(t.note, "repeat", "close") > 0 ? repeats.at(-1) : void 0;
349
+ if (loop !== void 0 && !loop.again) {
350
+ loop.again = true;
351
+ k = loop.open;
352
+ owed = 0;
353
+ continue;
354
+ }
355
+ if (loop !== void 0) repeats.pop();
356
+ k++;
357
+ }
358
+ };
359
+ let from = 0;
360
+ for (const ret of returns) {
361
+ sing(from, ret.resume, false);
362
+ sing(asthayi, stop + 1, ret.changed);
363
+ from = ret.resume;
364
+ }
365
+ sing(from, tokens.length, false);
366
+ return out;
367
+ }
368
+ function form(tokens) {
369
+ const first = /* @__PURE__ */ new Map();
370
+ const last = /* @__PURE__ */ new Map();
371
+ tokens.forEach((t, k) => {
372
+ if (!t.timed) return;
373
+ const row = `${t.p}.${t.r}`;
374
+ if (!first.has(row)) first.set(row, k);
375
+ last.set(row, k);
376
+ });
377
+ const opens = [];
378
+ let opening = false;
379
+ tokens.forEach((t, k) => {
380
+ if (t.note.bar !== "double") return;
381
+ const row = `${t.p}.${t.r}`;
382
+ const before = (first.get(row) ?? Infinity) < k;
383
+ const after = (last.get(row) ?? -1) > k;
384
+ opening = before !== after ? after : !opening;
385
+ if (opening) opens.push(k);
386
+ });
387
+ const returns = [];
388
+ for (let k = 0; k < tokens.length; k++) {
389
+ let at = -1;
390
+ let open = false;
391
+ let changed = false;
392
+ let end = false;
393
+ for (; k < tokens.length && !tokens[k].timed; k++) {
394
+ const n = tokens[k].note;
395
+ if (n.bar === "quad") end = true;
396
+ if (n.bar === "double" && !opens.includes(k)) at = k;
397
+ if (edges(n, "alt", "open") > 0) open = true;
398
+ if (open && edges(n, "alt", "close") > 0) {
399
+ at = k;
400
+ changed = true;
401
+ }
402
+ }
403
+ if (at >= 0 && !end) returns.push({ resume: at + 1, changed });
404
+ }
405
+ const top = opens[0];
406
+ const asthayi = top !== void 0 && top < (returns[0]?.resume ?? Infinity) ? top : 0;
407
+ const asthayiEnd = Math.min(returns[0]?.resume ?? Infinity, opens.find((k) => k > asthayi) ?? Infinity, tokens.length);
408
+ const lastSung = tokens.reduce((last2, t, k) => t.timed ? k : last2, -1);
409
+ const stop = tokens.findIndex(
410
+ (t, k) => k >= asthayi && k < asthayiEnd && k < lastSung && t.note.above?.some((a) => STOP_MARK.test(a.raw ?? "")) === true
411
+ );
412
+ if (stop < 0) return { asthayi, stop, returns: [] };
413
+ const atClose = returns[0]?.resume === asthayiEnd && !tokens.slice(stop + 1, asthayiEnd).some((t) => t.timed);
414
+ return { asthayi, stop, returns: atClose ? returns.slice(1) : 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
149
447
  var ARC_RAW_RE = /^[wWxXyYjJeEzZ$=_<>`\\]+$/;
150
448
  var isArcRaw = (raw) => raw !== void 0 && ARC_RAW_RE.test(raw);
151
449
  var MEEND_MARKS = { start: "w", end: "x", both: "xw" };
@@ -188,26 +486,45 @@ function graceTouches(ev) {
188
486
  ...ev.resh.map((midi, g) => ({ midi, beat: ev.beat + GRACE_STEP * (g + 1) }))
189
487
  ];
190
488
  }
191
- 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
+ };
192
496
  function scheduleLines(schedule) {
193
497
  const lines = [];
498
+ let note = Infinity;
194
499
  schedule.events.forEach((ev, i) => {
195
- 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;
196
503
  });
197
504
  lines.forEach((line, k) => line.endBeat = lines[k + 1]?.beat ?? schedule.totalBeats);
198
505
  return lines;
199
506
  }
200
- function eventAt(events, path) {
201
- const [p = 0, r = 0, n = 0] = path.split(".").map(Number);
202
- const inRow = (ev) => ev !== void 0 && rowOf(ev.path) === `${p}.${r}`;
203
- let i = events.findIndex((ev) => {
204
- const [ep = 0, er = 0, en = 0] = ev.path.split(".").map(Number);
205
- 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;
206
518
  });
207
- if (!inRow(events[i])) {
208
- const last = (i < 0 ? events.length : i) - 1;
209
- if (inRow(events[last])) i = last;
210
- }
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++;
211
528
  while (i > 0 && events[i].glide === true) i--;
212
529
  return i;
213
530
  }
@@ -219,67 +536,76 @@ function buildSchedule(doc, opts = {}) {
219
536
  let cursor = 0;
220
537
  let lastPitched = -1;
221
538
  let samMatra;
222
- let matra = 0;
539
+ let matra2 = 0;
223
540
  const speeds = resolveLaySpeeds(doc.meta.laySpeeds, opts.laySpeeds);
224
541
  const withLay = opts.lay !== false;
225
542
  const tempo = [{ beat: 0, endBeat: 0, scale: 1, lay: "drut", taal: doc.meta.taal }];
543
+ const rows = /* @__PURE__ */ new Map();
226
544
  let scale = 1;
545
+ let layNow = "drut";
227
546
  let taal = doc.meta.taal;
228
- doc.parts.forEach((part, p) => {
229
- part.rows.forEach((row, r) => {
547
+ doc.parts.forEach(
548
+ (part, p) => part.rows.forEach((row, r) => {
230
549
  const lay = withLay ? rowLay(row) : void 0;
231
- const layChanged = lay !== void 0 && speeds[lay] !== scale;
232
- const nextTaal = row.taal !== void 0 ? row.taal : taal;
233
- if (layChanged || !sameTaal(nextTaal, taal)) {
234
- if (layChanged) scale = speeds[lay];
235
- taal = nextTaal;
236
- const last = tempo[tempo.length - 1];
237
- 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 };
238
567
  if (last.beat === cursor) tempo[tempo.length - 1] = seg;
239
568
  else {
240
569
  last.endBeat = cursor;
241
570
  tempo.push(seg);
242
571
  }
243
572
  }
244
- const glides = meendContinuations(row.notes);
245
- row.notes.forEach((note, i) => {
246
- if (samMatra === void 0 && note.above !== void 0) {
247
- for (const a of note.above) {
248
- if (a.raw !== void 0 && SAM_TICK.test(a.raw)) {
249
- samMatra = matra;
250
- break;
251
- }
252
- }
253
- }
254
- if (note.bar !== void 0 || isSilent(note)) return;
255
- const path = `${p}.${r}.${i}`;
256
- const written = note.durationMatra ?? 1;
257
- const dur = written / scale;
258
- const kan = graceMidis(note.kan, tonicPc, baseOctave);
259
- const resh = graceMidis(note.resh, tonicPc, baseOctave);
260
- if (note.svara !== void 0) {
261
- events.push({
262
- beat: cursor,
263
- durationBeats: dur,
264
- midi: svaraToMidi(note, tonicPc, baseOctave),
265
- kan,
266
- resh,
267
- path,
268
- kind: note.sustain === true ? "sustain" : "note",
269
- ...glides.has(i) && lastPitched >= 0 ? { glide: true } : {}
270
- });
271
- lastPitched = events.length - 1;
272
- } else if (note.sustain === true) {
273
- if (lastPitched >= 0) events[lastPitched].durationBeats += dur;
274
- events.push({ beat: cursor, durationBeats: dur, midi: null, kan, resh, path, kind: "sustain" });
275
- } else {
276
- 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;
277
579
  }
278
- cursor += dur;
279
- 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 } : {}
280
598
  });
281
- });
282
- });
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
+ }
283
609
  const numBeats = tempo[0].taal?.numBeats;
284
610
  const pickupMatra = samMatra !== void 0 && numBeats !== void 0 && numBeats > 0 ? (numBeats - samMatra % numBeats) % numBeats : 0;
285
611
  tempo[tempo.length - 1].endBeat = cursor;
@@ -587,14 +913,6 @@ var TAAL_MODE_TEMPO = [
587
913
  { beat: 0, endBeat: Infinity, scale: 1, lay: "drut" }
588
914
  ];
589
915
  var MEDIA_LEAD_SEC = 0.25;
590
- function lineOf(lines, path) {
591
- const [p = 0, r = 0] = path.split(".").map(Number);
592
- const k = lines.findIndex((line) => {
593
- const [lp = 0, lr = 0] = line.row.split(".").map(Number);
594
- return lp > p || lp === p && lr >= r;
595
- });
596
- return k < 0 ? lines.length - 1 : k;
597
- }
598
916
  var seekableGrain = (Tone) => class extends Tone.GrainPlayer {
599
917
  _start(time, offset, duration) {
600
918
  super._start(time, offset === void 0 ? offset : this.toSeconds(offset) * this.playbackRate, duration);
@@ -655,9 +973,13 @@ var SwaralipiPlayer = class {
655
973
  /* Practice: where the playhead is, what repeats, whose turn it is. */
656
974
  /** The last note played or sought — what "this line" means to the controls. */
657
975
  #cursor = null;
976
+ /** The cursor as an event index: which pass of a line sung more than once. */
977
+ #at = -1;
658
978
  /** Where the next fresh play() starts (a seek made while stopped). */
659
979
  #startAt = null;
660
980
  #loop = null;
981
+ /** The repeat as line indices, the passes it holds. */
982
+ #span = null;
661
983
  #echo = false;
662
984
  /** With echo on, every other pass is the learner's: the melody rests. */
663
985
  #turn = false;
@@ -683,6 +1005,7 @@ var SwaralipiPlayer = class {
683
1005
  instruments: options.instruments ?? Object.keys(INSTRUMENTS),
684
1006
  rhythm: options.rhythm ?? "tabla",
685
1007
  tanpura: options.tanpura ?? true,
1008
+ repeats: options.repeats ?? true,
686
1009
  previewLimitSec: options.previewLimitSec ?? Infinity,
687
1010
  onHighlight: options.onHighlight,
688
1011
  onLoop: options.onLoop,
@@ -830,22 +1153,25 @@ var SwaralipiPlayer = class {
830
1153
  */
831
1154
  seek(path) {
832
1155
  const plan = this.#plan();
833
- const i = plan === void 0 ? -1 : eventAt(plan.schedule.events, path);
834
- if (plan === void 0 || i < 0) return;
835
- 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;
836
1161
  const at = plan.schedule.events[i].path;
837
1162
  this.#cursor = at;
1163
+ this.#at = i;
838
1164
  this.#turn = false;
839
- const k = lineOf(lines, at);
840
- const [a, b] = this.#loopSpan(lines);
841
- 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);
842
1168
  this.#emitLoop();
843
1169
  if (this.#timeline !== void 0 && (this.#state === "playing" || this.#state === "paused")) {
844
1170
  this.#applyLoop();
845
1171
  this.#jump(this.#eventSec(i));
846
1172
  if (this.#state === "paused") this.#emitHighlight(at);
847
1173
  } else {
848
- this.#startAt = at;
1174
+ this.#startAt = i;
849
1175
  this.#emitHighlight(at);
850
1176
  }
851
1177
  }
@@ -858,20 +1184,41 @@ var SwaralipiPlayer = class {
858
1184
  setLoop(from, to) {
859
1185
  this.#turn = false;
860
1186
  if (from === null) {
861
- this.#loop = null;
1187
+ this.#span = this.#loop = null;
862
1188
  this.#echo = false;
863
- } else {
864
- const lines = this.#lines();
865
- if (lines.length === 0) return;
866
- const here = this.#cursor === null ? -1 : lineOf(lines, this.#cursor);
867
- const a = from === void 0 ? Math.max(here, 0) : lineOf(lines, from);
868
- const b = to === void 0 ? a : lineOf(lines, to);
869
- this.#loop = { from: lines[Math.min(a, b)].row, to: lines[Math.max(a, b)].row };
870
- 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;
871
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);
872
1206
  this.#applyLoop();
873
1207
  this.#emitLoop();
874
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
+ }
875
1222
  /**
876
1223
  * Follow every pass of the repeat with a silent one: the tabla and tanpura
877
1224
  * keep time and the highlight keeps moving, but the melody rests so the
@@ -893,22 +1240,22 @@ var SwaralipiPlayer = class {
893
1240
  const plan = this.#plan();
894
1241
  if (plan === void 0 || plan.lines.length === 0) return;
895
1242
  const { lines, schedule } = plan;
896
- const k = this.#cursor === null ? -1 : lineOf(lines, this.#cursor);
897
- 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;
898
1245
  const step = delta < 0 && this.#state === "playing" && into > 1 ? delta + 1 : delta;
899
- 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);
900
1247
  }
901
1248
  /** Move by single notes (rests and holds included — each is a column). */
902
1249
  stepNote(delta) {
903
- const events = this.#plan()?.schedule.events ?? [];
904
- 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;
905
1253
  for (let left = Math.abs(delta); left > 0; left--) {
906
1254
  do
907
1255
  i += Math.sign(delta);
908
1256
  while (events[i]?.glide === true);
909
1257
  }
910
- const ev = events[Math.min(Math.max(i, 0), events.length - 1)];
911
- 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));
912
1259
  }
913
1260
  /**
914
1261
  * Stop and rewind to the start. When sounding, the accompaniment (tabla /
@@ -1103,21 +1450,21 @@ var SwaralipiPlayer = class {
1103
1450
  }
1104
1451
  /** Grow the repeat (or start one from the current line) to take in `path`'s line. */
1105
1452
  #extendLoop(path) {
1106
- const lines = this.#lines();
1107
- if (lines.length === 0) return;
1108
- const k = lineOf(lines, path);
1109
- const here = this.#cursor === null ? k : lineOf(lines, this.#cursor);
1110
- const [a, b] = this.#loop === null ? [here, here] : this.#loopSpan(lines);
1111
- 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));
1112
1459
  }
1113
1460
  /** Shift+↑/↓: one more line above / below the repeat. */
1114
1461
  #extendBy(delta) {
1115
- const lines = this.#lines();
1116
- if (lines.length === 0) return;
1117
- const here = this.#cursor === null ? 0 : lineOf(lines, this.#cursor);
1118
- const [a, b] = this.#loop === null ? [here, here] : this.#loopSpan(lines);
1119
- const k = Math.min(Math.max((delta < 0 ? a : b) + delta, 0), lines.length - 1);
1120
- 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));
1121
1468
  }
1122
1469
  /**
1123
1470
  * Check whether an instrument's samples are reachable (its CDN is up / the
@@ -1177,7 +1524,8 @@ var SwaralipiPlayer = class {
1177
1524
  }
1178
1525
  /** A new document starts with nothing repeated, from the top. */
1179
1526
  #resetPractice() {
1180
- this.#cursor = this.#startAt = this.#loop = null;
1527
+ this.#cursor = this.#startAt = this.#loop = this.#span = null;
1528
+ this.#at = -1;
1181
1529
  this.#echo = this.#turn = false;
1182
1530
  this.#listened = 0;
1183
1531
  this.#emitLoop();
@@ -1191,12 +1539,9 @@ var SwaralipiPlayer = class {
1191
1539
  const schedule = buildSchedule(this.#doc, this.#scheduleOpts());
1192
1540
  return { schedule, lines: scheduleLines(schedule) };
1193
1541
  }
1194
- #lines() {
1195
- return this.#plan()?.lines ?? [];
1196
- }
1197
1542
  /** The repeat as line indices (`[0, -1]`, i.e. empty, when off). */
1198
- #loopSpan(lines) {
1199
- return this.#loop === null ? [0, -1] : [lineOf(lines, this.#loop.from), lineOf(lines, this.#loop.to)];
1543
+ #loopSpan() {
1544
+ return this.#span ?? [0, -1];
1200
1545
  }
1201
1546
  /** The repeat's lines on the arranged timeline, cached for the per-note
1202
1547
  * checks (#hears), which run on every note. */
@@ -1225,8 +1570,8 @@ var SwaralipiPlayer = class {
1225
1570
  #applyLoop() {
1226
1571
  const transport = this.#tone?.getTransport();
1227
1572
  if (transport === void 0 || this.#timeline === void 0) return;
1228
- this.#loopLines = this.#loopSpan(this.#timeline.lines);
1229
- if (this.#loop === null) {
1573
+ this.#loopLines = this.#loopSpan();
1574
+ if (this.#span === null) {
1230
1575
  transport.loop = false;
1231
1576
  return;
1232
1577
  }
@@ -1237,8 +1582,8 @@ var SwaralipiPlayer = class {
1237
1582
  * else the top. */
1238
1583
  #startSec() {
1239
1584
  const events = this.#timeline?.schedule.events ?? [];
1240
- if (this.#startAt !== null) return this.#eventSec(Math.max(eventAt(events, this.#startAt), 0));
1241
- 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];
1242
1587
  }
1243
1588
  /** Move the transport; notes still ringing from the old place are let go. */
1244
1589
  #jump(sec) {
@@ -1327,7 +1672,8 @@ var SwaralipiPlayer = class {
1327
1672
  return {
1328
1673
  tonic: this.#opts.tonic,
1329
1674
  baseOctave: this.#opts.baseOctave,
1330
- laySpeeds: this.#opts.laySpeeds
1675
+ laySpeeds: this.#opts.laySpeeds,
1676
+ repeats: this.#opts.repeats
1331
1677
  };
1332
1678
  }
1333
1679
  /** Effective tempo: snapped to the taal's available tabla tempos while a
@@ -1345,9 +1691,10 @@ var SwaralipiPlayer = class {
1345
1691
  * from the note it was on — slowing down a repeated line keeps repeating it. */
1346
1692
  #reapply() {
1347
1693
  if (this.#state !== "playing") return;
1348
- const at = this.#cursor;
1694
+ const at = this.#at;
1349
1695
  this.#halt();
1350
- void this.play(at ?? void 0);
1696
+ this.#startAt = at < 0 ? null : at;
1697
+ void this.play();
1351
1698
  }
1352
1699
  /** The master bus everything routes through (created once, → destination). */
1353
1700
  #ensureMaster(Tone) {
@@ -1391,13 +1738,13 @@ var SwaralipiPlayer = class {
1391
1738
  while (lines[line + 1] !== void 0 && lines[line + 1].first <= i) line++;
1392
1739
  const k = line;
1393
1740
  const heard = (time) => time >= this.#fence && this.#hears(k);
1394
- const sounds = (time) => heard(time) && !this.#turn;
1741
+ const sounds2 = (time) => heard(time) && !this.#turn;
1395
1742
  const at = ev.beat * spb + shift;
1396
1743
  if (ev.glide !== true) {
1397
1744
  for (const g of graceTouches(ev)) {
1398
1745
  const graceName = midiToSampleName(g.midi);
1399
1746
  transport.schedule((time) => {
1400
- if (sounds(time)) melody.triggerAttackRelease(graceName, spb * 0.4, time);
1747
+ if (sounds2(time)) melody.triggerAttackRelease(graceName, spb * 0.4, time);
1401
1748
  }, Math.max(g.beat * spb + shift, 0));
1402
1749
  }
1403
1750
  }
@@ -1413,7 +1760,7 @@ var SwaralipiPlayer = class {
1413
1760
  ];
1414
1761
  const end = (last.beat + last.durationBeats - ev.beat) * spb;
1415
1762
  transport.schedule((time) => {
1416
- if (!sounds(time)) return;
1763
+ if (!sounds2(time)) return;
1417
1764
  triggerGlide(
1418
1765
  Tone,
1419
1766
  glide.buffers,
@@ -1426,7 +1773,7 @@ var SwaralipiPlayer = class {
1426
1773
  }, at);
1427
1774
  } else {
1428
1775
  transport.schedule((time) => {
1429
- if (sounds(time)) melody.triggerAttackRelease(name, dur, time);
1776
+ if (sounds2(time)) melody.triggerAttackRelease(name, dur, time);
1430
1777
  }, at);
1431
1778
  }
1432
1779
  }
@@ -1439,6 +1786,7 @@ var SwaralipiPlayer = class {
1439
1786
  if (!heard(time)) return;
1440
1787
  const turn = this.#turn;
1441
1788
  this.#cursor = ev.path;
1789
+ this.#at = i;
1442
1790
  draw.schedule(() => this.#emitHighlight(ev.path, turn), time);
1443
1791
  }, at);
1444
1792
  });
@@ -1675,7 +2023,8 @@ async function exportAudio(doc, options = {}) {
1675
2023
  const schedule = buildSchedule(doc, {
1676
2024
  tonic: options.tonic,
1677
2025
  baseOctave: options.baseOctave,
1678
- laySpeeds: options.laySpeeds
2026
+ laySpeeds: options.laySpeeds,
2027
+ repeats: options.repeats ?? true
1679
2028
  });
1680
2029
  const range = midiRange2(schedule);
1681
2030
  const taalId = resolveTaal(doc, options.taal);
@@ -2048,7 +2397,8 @@ function exportMidi(doc, options = {}) {
2048
2397
  const schedule = buildSchedule(doc, {
2049
2398
  tonic: options.tonic,
2050
2399
  baseOctave: options.baseOctave,
2051
- laySpeeds: options.laySpeeds
2400
+ laySpeeds: options.laySpeeds,
2401
+ repeats: options.repeats ?? true
2052
2402
  });
2053
2403
  const bpm = options.bpm ?? doc.meta.bpm ?? 90;
2054
2404
  const drums = options.drums ? (seg) => {