@ugfoundation/swaralipi-js 0.12.0 → 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 (43) hide show
  1. package/README.md +7 -1
  2. package/dist/{batch-ZNVWIGEL.js → batch-BS2AIZOR.js} +5 -4
  3. package/dist/batch-BS2AIZOR.js.map +1 -0
  4. package/dist/{chunk-A7R3VFIV.js → chunk-3ZBNANDF.js} +3 -3
  5. package/dist/{chunk-A7R3VFIV.js.map → chunk-3ZBNANDF.js.map} +1 -1
  6. package/dist/{chunk-ISNAL632.js → chunk-5N6HGPFH.js} +5 -165
  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-A6Z7X2LX.js → chunk-ZNJTMQIX.js} +227 -63
  11. package/dist/chunk-ZNJTMQIX.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
@@ -1,8 +1,8 @@
1
1
  import { S as Swaralipi, T as Taal, O as Octave, a as SvaraLetter, N as Note } from '../schema-DZag9qkF.cjs';
2
2
  import { a as LaySpeeds, T as TaalId } from '../lay-CXhC-HRO.cjs';
3
3
  export { b as LAYS, c as LAY_SPEEDS, L as Lay, p as printedLay, r as resolveLaySpeeds, d as rowLay } from '../lay-CXhC-HRO.cjs';
4
- import { I as InstrumentId, a as InstrumentDef, S as Schedule, D as DrumVoice, T as TempoSegment } from '../schedule-CRpjr6JH.cjs';
5
- export { A as ARC_RAW_RE, b as DEFAULT_SOUNDS_BASE, c as DRUM_KIT_BASE, G as GRACE_STEP, d as GlideLeg, e as INSTRUMENTS, L as Line, M as MEEND_HOLD, P as PlayEvent, f as SALAMANDER_BASE, g as SAMPLED_TAALS, h as ScheduleOptions, i as TABLA_BPMS, j as TABLA_VARIATIONS, k as TONEJS_INSTRUMENTS_BASE, l as ToneModule, m as buildSchedule, n as createBuffers, o as drumPattern, p as eventAt, q as glideTargets, r as graceTouches, s as instrumentSampleUrl, t as isArcRaw, u as meendContinuations, v as nearestSampleKey, w as nearestTablaBpm, x as normalizeBase, y as probeUrl, z as quantizeLoopEnd, B as santoorUrls, C as scheduleLines, E as subsetUrls, F as tablaBpms, H as tablaUrl, J as triggerGlide } from '../schedule-CRpjr6JH.cjs';
4
+ import { I as InstrumentId, a as InstrumentDef, S as Schedule, D as DrumVoice, T as TempoSegment } from '../schedule-V-fgepvP.cjs';
5
+ export { A as ARC_RAW_RE, b as DEFAULT_SOUNDS_BASE, c as DRUM_KIT_BASE, G as GRACE_STEP, d as GlideLeg, e as INSTRUMENTS, L as Line, M as MEEND_HOLD, P as PlayEvent, f as SALAMANDER_BASE, g as SAMPLED_TAALS, h as ScheduleOptions, i as TABLA_BPMS, j as TABLA_VARIATIONS, k as TONEJS_INSTRUMENTS_BASE, l as ToneModule, m as buildSchedule, n as createBuffers, o as drumPattern, p as eventAt, q as glideTargets, r as graceTouches, s as instrumentSampleUrl, t as isArcRaw, u as meendContinuations, v as nearestSampleKey, w as nearestTablaBpm, x as normalizeBase, y as probeUrl, z as quantizeLoopEnd, B as santoorUrls, C as scheduleLines, E as subsetUrls, F as tablaBpms, H as tablaUrl, J as triggerGlide } from '../schedule-V-fgepvP.cjs';
6
6
  import 'zod';
7
7
  import 'tone';
8
8
 
@@ -57,6 +57,10 @@ interface PlayerOptions {
57
57
  rhythm?: RhythmMode;
58
58
  /** Play the tanpura drone. Default true. */
59
59
  tanpura?: boolean;
60
+ /** Sing the song's form: each `{ }` twice, `( )` only the first time, the
61
+ * `[ ]` notes on the repeat, and back to the asthayi at each kali's end
62
+ * (when a ‖ says where to stop). Default true; false plays it as written. */
63
+ repeats?: boolean;
60
64
  /** Stop playback after this many seconds (a policy-neutral preview cap).
61
65
  * Default `Infinity` (unlimited). The library only enforces the number it is
62
66
  * handed; who is capped / the value / the upsell is the consumer's policy. */
@@ -260,6 +264,9 @@ interface AudioExportOptions {
260
264
  taal?: TaalId | 'auto';
261
265
  /** Include the tanpura drone. Default false. */
262
266
  tanpura?: boolean;
267
+ /** Sing the song's form (repeats, returns to the asthayi), as the player
268
+ * does. Default true; false keeps the written order. */
269
+ repeats?: boolean;
263
270
  /** Output format. Default `wav` (lossless, zero-dependency). `mp3` needs the
264
271
  * optional peer dep `@breezystack/lamejs`. */
265
272
  format?: AudioFormat;
@@ -336,6 +343,9 @@ interface MidiExportOptions {
336
343
  name?: string;
337
344
  /** Also export the taal's drumset pattern as a separate GM channel-10 track. */
338
345
  drums?: boolean;
346
+ /** Sing the song's form (repeats, returns to the asthayi), as the player
347
+ * does. Default true; false keeps the written order. */
348
+ repeats?: boolean;
339
349
  }
340
350
  interface MidiExportResult {
341
351
  blob: Blob;
@@ -1,8 +1,8 @@
1
1
  import { S as Swaralipi, T as Taal, O as Octave, a as SvaraLetter, N as Note } from '../schema-DZag9qkF.js';
2
2
  import { a as LaySpeeds, T as TaalId } from '../lay-CWlEhImP.js';
3
3
  export { b as LAYS, c as LAY_SPEEDS, L as Lay, p as printedLay, r as resolveLaySpeeds, d as rowLay } from '../lay-CWlEhImP.js';
4
- import { I as InstrumentId, a as InstrumentDef, S as Schedule, D as DrumVoice, T as TempoSegment } from '../schedule-DN-8X17d.js';
5
- export { A as ARC_RAW_RE, b as DEFAULT_SOUNDS_BASE, c as DRUM_KIT_BASE, G as GRACE_STEP, d as GlideLeg, e as INSTRUMENTS, L as Line, M as MEEND_HOLD, P as PlayEvent, f as SALAMANDER_BASE, g as SAMPLED_TAALS, h as ScheduleOptions, i as TABLA_BPMS, j as TABLA_VARIATIONS, k as TONEJS_INSTRUMENTS_BASE, l as ToneModule, m as buildSchedule, n as createBuffers, o as drumPattern, p as eventAt, q as glideTargets, r as graceTouches, s as instrumentSampleUrl, t as isArcRaw, u as meendContinuations, v as nearestSampleKey, w as nearestTablaBpm, x as normalizeBase, y as probeUrl, z as quantizeLoopEnd, B as santoorUrls, C as scheduleLines, E as subsetUrls, F as tablaBpms, H as tablaUrl, J as triggerGlide } from '../schedule-DN-8X17d.js';
4
+ import { I as InstrumentId, a as InstrumentDef, S as Schedule, D as DrumVoice, T as TempoSegment } from '../schedule-ClyON98C.js';
5
+ export { A as ARC_RAW_RE, b as DEFAULT_SOUNDS_BASE, c as DRUM_KIT_BASE, G as GRACE_STEP, d as GlideLeg, e as INSTRUMENTS, L as Line, M as MEEND_HOLD, P as PlayEvent, f as SALAMANDER_BASE, g as SAMPLED_TAALS, h as ScheduleOptions, i as TABLA_BPMS, j as TABLA_VARIATIONS, k as TONEJS_INSTRUMENTS_BASE, l as ToneModule, m as buildSchedule, n as createBuffers, o as drumPattern, p as eventAt, q as glideTargets, r as graceTouches, s as instrumentSampleUrl, t as isArcRaw, u as meendContinuations, v as nearestSampleKey, w as nearestTablaBpm, x as normalizeBase, y as probeUrl, z as quantizeLoopEnd, B as santoorUrls, C as scheduleLines, E as subsetUrls, F as tablaBpms, H as tablaUrl, J as triggerGlide } from '../schedule-ClyON98C.js';
6
6
  import 'zod';
7
7
  import 'tone';
8
8
 
@@ -57,6 +57,10 @@ interface PlayerOptions {
57
57
  rhythm?: RhythmMode;
58
58
  /** Play the tanpura drone. Default true. */
59
59
  tanpura?: boolean;
60
+ /** Sing the song's form: each `{ }` twice, `( )` only the first time, the
61
+ * `[ ]` notes on the repeat, and back to the asthayi at each kali's end
62
+ * (when a ‖ says where to stop). Default true; false plays it as written. */
63
+ repeats?: boolean;
60
64
  /** Stop playback after this many seconds (a policy-neutral preview cap).
61
65
  * Default `Infinity` (unlimited). The library only enforces the number it is
62
66
  * handed; who is capped / the value / the upsell is the consumer's policy. */
@@ -260,6 +264,9 @@ interface AudioExportOptions {
260
264
  taal?: TaalId | 'auto';
261
265
  /** Include the tanpura drone. Default false. */
262
266
  tanpura?: boolean;
267
+ /** Sing the song's form (repeats, returns to the asthayi), as the player
268
+ * does. Default true; false keeps the written order. */
269
+ repeats?: boolean;
263
270
  /** Output format. Default `wav` (lossless, zero-dependency). `mp3` needs the
264
271
  * optional peer dep `@breezystack/lamejs`. */
265
272
  format?: AudioFormat;
@@ -336,6 +343,9 @@ interface MidiExportOptions {
336
343
  name?: string;
337
344
  /** Also export the taal's drumset pattern as a separate GM channel-10 track. */
338
345
  drums?: boolean;
346
+ /** Sing the song's form (repeats, returns to the asthayi), as the player
347
+ * does. Default true; false keeps the written order. */
348
+ repeats?: boolean;
339
349
  }
340
350
  interface MidiExportResult {
341
351
  blob: Blob;
@@ -1,5 +1,6 @@
1
- import { DEFAULT_SOUNDS_BASE, INSTRUMENTS, SAMPLED_TAALS, buildSchedule, eventAt, quantizeLoopEnd, instrumentSampleUrl, probeUrl, scheduleLines, GRACE_STEP, normalizeBase, nearestTablaBpm, tablaBpms, graceTouches, midiToSampleName, glideTargets, triggerGlide, createSampler, createBuffers, tablaShiftSemitones, tablaUrl, TABLA_GRAIN_OVERLAP_SEC, TABLA_GRAIN_SEC, createDrumKit, drumPattern, createMetronome, TANPURA_PCS, createTanpura, parseTonic, MEEND_HOLD, sCurve } from '../chunk-A6Z7X2LX.js';
2
- export { ARC_RAW_RE, DEFAULT_SOUNDS_BASE, DRUM_KIT_BASE, GRACE_STEP, INSTRUMENTS, MEEND_HOLD, OCTAVE_OFFSET, SALAMANDER_BASE, SAMPLED_TAALS, SHUDDHA, TABLA_BPMS, TABLA_VARIATIONS, TONEJS_INSTRUMENTS_BASE, buildSchedule, createBuffers, drumPattern, eventAt, glideTargets, graceTouches, instrumentSampleUrl, isArcRaw, meendContinuations, midiToSampleName, nearestSampleKey, nearestTablaBpm, normalizeBase, parseTonic, probeUrl, quantizeLoopEnd, sCurve, sampleFileName, santoorUrls, scheduleLines, subsetUrls, svaraSemitone, svaraToMidi, tablaBpms, tablaUrl, triggerGlide } from '../chunk-A6Z7X2LX.js';
1
+ import { DEFAULT_SOUNDS_BASE, INSTRUMENTS, SAMPLED_TAALS, buildSchedule, eventAt, lineAt, byRow, lineFor, quantizeLoopEnd, instrumentSampleUrl, probeUrl, scheduleLines, GRACE_STEP, normalizeBase, nearestTablaBpm, tablaBpms, graceTouches, midiToSampleName, glideTargets, triggerGlide, createSampler, createBuffers, tablaShiftSemitones, tablaUrl, TABLA_GRAIN_OVERLAP_SEC, TABLA_GRAIN_SEC, createDrumKit, drumPattern, createMetronome, TANPURA_PCS, createTanpura, parseTonic, MEEND_HOLD, sCurve } from '../chunk-ZNJTMQIX.js';
2
+ export { ARC_RAW_RE, DEFAULT_SOUNDS_BASE, DRUM_KIT_BASE, GRACE_STEP, INSTRUMENTS, MEEND_HOLD, OCTAVE_OFFSET, SALAMANDER_BASE, SAMPLED_TAALS, SHUDDHA, TABLA_BPMS, TABLA_VARIATIONS, TONEJS_INSTRUMENTS_BASE, buildSchedule, createBuffers, drumPattern, eventAt, glideTargets, graceTouches, instrumentSampleUrl, isArcRaw, meendContinuations, midiToSampleName, nearestSampleKey, nearestTablaBpm, normalizeBase, parseTonic, probeUrl, quantizeLoopEnd, sCurve, sampleFileName, santoorUrls, scheduleLines, subsetUrls, svaraSemitone, svaraToMidi, tablaBpms, tablaUrl, triggerGlide } from '../chunk-ZNJTMQIX.js';
3
+ import '../chunk-BSZ3WQUP.js';
3
4
  import { TAALS } from '../chunk-KOHSKHXI.js';
4
5
  export { LAYS, LAY_SPEEDS, printedLay, resolveLaySpeeds, rowLay } from '../chunk-KOHSKHXI.js';
5
6
 
@@ -11,14 +12,6 @@ var TAAL_MODE_TEMPO = [
11
12
  { beat: 0, endBeat: Infinity, scale: 1, lay: "drut" }
12
13
  ];
13
14
  var MEDIA_LEAD_SEC = 0.25;
14
- function lineOf(lines, path) {
15
- const [p = 0, r = 0] = path.split(".").map(Number);
16
- const k = lines.findIndex((line) => {
17
- const [lp = 0, lr = 0] = line.row.split(".").map(Number);
18
- return lp > p || lp === p && lr >= r;
19
- });
20
- return k < 0 ? lines.length - 1 : k;
21
- }
22
15
  var seekableGrain = (Tone) => class extends Tone.GrainPlayer {
23
16
  _start(time, offset, duration) {
24
17
  super._start(time, offset === void 0 ? offset : this.toSeconds(offset) * this.playbackRate, duration);
@@ -79,9 +72,13 @@ var SwaralipiPlayer = class {
79
72
  /* Practice: where the playhead is, what repeats, whose turn it is. */
80
73
  /** The last note played or sought — what "this line" means to the controls. */
81
74
  #cursor = null;
75
+ /** The cursor as an event index: which pass of a line sung more than once. */
76
+ #at = -1;
82
77
  /** Where the next fresh play() starts (a seek made while stopped). */
83
78
  #startAt = null;
84
79
  #loop = null;
80
+ /** The repeat as line indices, the passes it holds. */
81
+ #span = null;
85
82
  #echo = false;
86
83
  /** With echo on, every other pass is the learner's: the melody rests. */
87
84
  #turn = false;
@@ -107,6 +104,7 @@ var SwaralipiPlayer = class {
107
104
  instruments: options.instruments ?? Object.keys(INSTRUMENTS),
108
105
  rhythm: options.rhythm ?? "tabla",
109
106
  tanpura: options.tanpura ?? true,
107
+ repeats: options.repeats ?? true,
110
108
  previewLimitSec: options.previewLimitSec ?? Infinity,
111
109
  onHighlight: options.onHighlight,
112
110
  onLoop: options.onLoop,
@@ -254,22 +252,25 @@ var SwaralipiPlayer = class {
254
252
  */
255
253
  seek(path) {
256
254
  const plan = this.#plan();
257
- const i = plan === void 0 ? -1 : eventAt(plan.schedule.events, path);
258
- if (plan === void 0 || i < 0) return;
259
- const { lines } = plan;
255
+ if (plan !== void 0) this.#seekTo(plan, eventAt(plan.schedule.events, path, this.#at));
256
+ }
257
+ /** seek(), by event index — the pass, not just the line. */
258
+ #seekTo(plan, i) {
259
+ if (i < 0) return;
260
260
  const at = plan.schedule.events[i].path;
261
261
  this.#cursor = at;
262
+ this.#at = i;
262
263
  this.#turn = false;
263
- const k = lineOf(lines, at);
264
- const [a, b] = this.#loopSpan(lines);
265
- if (this.#loop !== null && (k < a || k > b)) this.#loop = { from: lines[k].row, to: lines[k].row };
264
+ const k = lineAt(plan.lines, i);
265
+ const [a, b] = this.#loopSpan();
266
+ if (this.#span !== null && (k < a || k > b)) this.#setSpan(plan.lines, k, k);
266
267
  this.#emitLoop();
267
268
  if (this.#timeline !== void 0 && (this.#state === "playing" || this.#state === "paused")) {
268
269
  this.#applyLoop();
269
270
  this.#jump(this.#eventSec(i));
270
271
  if (this.#state === "paused") this.#emitHighlight(at);
271
272
  } else {
272
- this.#startAt = at;
273
+ this.#startAt = i;
273
274
  this.#emitHighlight(at);
274
275
  }
275
276
  }
@@ -282,20 +283,41 @@ var SwaralipiPlayer = class {
282
283
  setLoop(from, to) {
283
284
  this.#turn = false;
284
285
  if (from === null) {
285
- this.#loop = null;
286
+ this.#span = this.#loop = null;
286
287
  this.#echo = false;
287
- } else {
288
- const lines = this.#lines();
289
- if (lines.length === 0) return;
290
- const here = this.#cursor === null ? -1 : lineOf(lines, this.#cursor);
291
- const a = from === void 0 ? Math.max(here, 0) : lineOf(lines, from);
292
- const b = to === void 0 ? a : lineOf(lines, to);
293
- this.#loop = { from: lines[Math.min(a, b)].row, to: lines[Math.max(a, b)].row };
294
- if (here < Math.min(a, b) || here > Math.max(a, b)) return this.seek(this.#loop.from);
288
+ this.#applyLoop();
289
+ this.#emitLoop();
290
+ return;
295
291
  }
292
+ const plan = this.#plan();
293
+ if (plan === void 0 || plan.lines.length === 0) return;
294
+ const { lines } = plan;
295
+ const here = this.#at < 0 ? -1 : lineAt(lines, this.#at);
296
+ const a = from === void 0 ? Math.max(here, 0) : this.#lineOf(lines, from, here);
297
+ this.#repeat(plan, a, to === void 0 ? a : this.#lineOf(lines, to, a));
298
+ }
299
+ /** Repeat lines a..b (indices, either order); the playhead joins them. */
300
+ #repeat(plan, a, b) {
301
+ this.#setSpan(plan.lines, a, b);
302
+ const [lo, hi] = this.#loopSpan();
303
+ const here = this.#at < 0 ? -1 : lineAt(plan.lines, this.#at);
304
+ if (here < lo || here > hi) return this.#seekTo(plan, plan.lines[lo].first);
296
305
  this.#applyLoop();
297
306
  this.#emitLoop();
298
307
  }
308
+ /** The repeat's passes, and the rows they cover for the sheet's shading —
309
+ * written order, since a span can cross a jump back to an earlier row. */
310
+ #setSpan(lines, a, b) {
311
+ this.#span = [Math.min(a, b), Math.max(a, b)];
312
+ const rows = lines.slice(this.#span[0], this.#span[1] + 1).map((line) => line.row).sort(byRow);
313
+ this.#loop = { from: rows[0], to: rows.at(-1) };
314
+ }
315
+ /** The line `path` names, at its pass nearest line `near`; the last line
316
+ * when nothing sounds from there on. */
317
+ #lineOf(lines, path, near) {
318
+ const k = lineFor(lines, path, near);
319
+ return k < 0 ? lines.length - 1 : k;
320
+ }
299
321
  /**
300
322
  * Follow every pass of the repeat with a silent one: the tabla and tanpura
301
323
  * keep time and the highlight keeps moving, but the melody rests so the
@@ -317,22 +339,22 @@ var SwaralipiPlayer = class {
317
339
  const plan = this.#plan();
318
340
  if (plan === void 0 || plan.lines.length === 0) return;
319
341
  const { lines, schedule } = plan;
320
- const k = this.#cursor === null ? -1 : lineOf(lines, this.#cursor);
321
- const into = k < 0 ? 0 : schedule.events[eventAt(schedule.events, this.#cursor)].beat - lines[k].beat;
342
+ const k = this.#at < 0 ? -1 : lineAt(lines, this.#at);
343
+ const into = k < 0 ? 0 : schedule.events[this.#at].beat - lines[k].beat;
322
344
  const step = delta < 0 && this.#state === "playing" && into > 1 ? delta + 1 : delta;
323
- this.seek(lines[Math.min(Math.max(k + step, 0), lines.length - 1)].row);
345
+ this.#seekTo(plan, lines[Math.min(Math.max(k + step, 0), lines.length - 1)].first);
324
346
  }
325
347
  /** Move by single notes (rests and holds included — each is a column). */
326
348
  stepNote(delta) {
327
- const events = this.#plan()?.schedule.events ?? [];
328
- let i = this.#cursor === null ? -1 : eventAt(events, this.#cursor);
349
+ const plan = this.#plan();
350
+ const events = plan?.schedule.events ?? [];
351
+ let i = this.#at;
329
352
  for (let left = Math.abs(delta); left > 0; left--) {
330
353
  do
331
354
  i += Math.sign(delta);
332
355
  while (events[i]?.glide === true);
333
356
  }
334
- const ev = events[Math.min(Math.max(i, 0), events.length - 1)];
335
- if (ev !== void 0) this.seek(ev.path);
357
+ if (plan !== void 0 && events.length > 0) this.#seekTo(plan, Math.min(Math.max(i, 0), events.length - 1));
336
358
  }
337
359
  /**
338
360
  * Stop and rewind to the start. When sounding, the accompaniment (tabla /
@@ -527,21 +549,21 @@ var SwaralipiPlayer = class {
527
549
  }
528
550
  /** Grow the repeat (or start one from the current line) to take in `path`'s line. */
529
551
  #extendLoop(path) {
530
- const lines = this.#lines();
531
- if (lines.length === 0) return;
532
- const k = lineOf(lines, path);
533
- const here = this.#cursor === null ? k : lineOf(lines, this.#cursor);
534
- const [a, b] = this.#loop === null ? [here, here] : this.#loopSpan(lines);
535
- this.setLoop(lines[Math.min(a, k)].row, lines[Math.max(b, k)].row);
552
+ const plan = this.#plan();
553
+ if (plan === void 0 || plan.lines.length === 0) return;
554
+ const here = this.#at < 0 ? -1 : lineAt(plan.lines, this.#at);
555
+ const k = this.#lineOf(plan.lines, path, here);
556
+ const [a, b] = this.#span ?? (here < 0 ? [k, k] : [here, here]);
557
+ this.#repeat(plan, Math.min(a, k), Math.max(b, k));
536
558
  }
537
559
  /** Shift+↑/↓: one more line above / below the repeat. */
538
560
  #extendBy(delta) {
539
- const lines = this.#lines();
540
- if (lines.length === 0) return;
541
- const here = this.#cursor === null ? 0 : lineOf(lines, this.#cursor);
542
- const [a, b] = this.#loop === null ? [here, here] : this.#loopSpan(lines);
543
- const k = Math.min(Math.max((delta < 0 ? a : b) + delta, 0), lines.length - 1);
544
- this.#extendLoop(lines[k].row);
561
+ const plan = this.#plan();
562
+ if (plan === void 0 || plan.lines.length === 0) return;
563
+ const here = this.#at < 0 ? 0 : lineAt(plan.lines, this.#at);
564
+ const [a, b] = this.#span ?? [here, here];
565
+ const k = Math.min(Math.max((delta < 0 ? a : b) + delta, 0), plan.lines.length - 1);
566
+ this.#repeat(plan, Math.min(a, k), Math.max(b, k));
545
567
  }
546
568
  /**
547
569
  * Check whether an instrument's samples are reachable (its CDN is up / the
@@ -601,7 +623,8 @@ var SwaralipiPlayer = class {
601
623
  }
602
624
  /** A new document starts with nothing repeated, from the top. */
603
625
  #resetPractice() {
604
- this.#cursor = this.#startAt = this.#loop = null;
626
+ this.#cursor = this.#startAt = this.#loop = this.#span = null;
627
+ this.#at = -1;
605
628
  this.#echo = this.#turn = false;
606
629
  this.#listened = 0;
607
630
  this.#emitLoop();
@@ -615,12 +638,9 @@ var SwaralipiPlayer = class {
615
638
  const schedule = buildSchedule(this.#doc, this.#scheduleOpts());
616
639
  return { schedule, lines: scheduleLines(schedule) };
617
640
  }
618
- #lines() {
619
- return this.#plan()?.lines ?? [];
620
- }
621
641
  /** The repeat as line indices (`[0, -1]`, i.e. empty, when off). */
622
- #loopSpan(lines) {
623
- return this.#loop === null ? [0, -1] : [lineOf(lines, this.#loop.from), lineOf(lines, this.#loop.to)];
642
+ #loopSpan() {
643
+ return this.#span ?? [0, -1];
624
644
  }
625
645
  /** The repeat's lines on the arranged timeline, cached for the per-note
626
646
  * checks (#hears), which run on every note. */
@@ -649,8 +669,8 @@ var SwaralipiPlayer = class {
649
669
  #applyLoop() {
650
670
  const transport = this.#tone?.getTransport();
651
671
  if (transport === void 0 || this.#timeline === void 0) return;
652
- this.#loopLines = this.#loopSpan(this.#timeline.lines);
653
- if (this.#loop === null) {
672
+ this.#loopLines = this.#loopSpan();
673
+ if (this.#span === null) {
654
674
  transport.loop = false;
655
675
  return;
656
676
  }
@@ -661,8 +681,8 @@ var SwaralipiPlayer = class {
661
681
  * else the top. */
662
682
  #startSec() {
663
683
  const events = this.#timeline?.schedule.events ?? [];
664
- if (this.#startAt !== null) return this.#eventSec(Math.max(eventAt(events, this.#startAt), 0));
665
- return this.#loop === null ? 0 : this.#loopSec()[0];
684
+ if (this.#startAt !== null) return this.#eventSec(Math.min(this.#startAt, events.length - 1));
685
+ return this.#span === null ? 0 : this.#loopSec()[0];
666
686
  }
667
687
  /** Move the transport; notes still ringing from the old place are let go. */
668
688
  #jump(sec) {
@@ -751,7 +771,8 @@ var SwaralipiPlayer = class {
751
771
  return {
752
772
  tonic: this.#opts.tonic,
753
773
  baseOctave: this.#opts.baseOctave,
754
- laySpeeds: this.#opts.laySpeeds
774
+ laySpeeds: this.#opts.laySpeeds,
775
+ repeats: this.#opts.repeats
755
776
  };
756
777
  }
757
778
  /** Effective tempo: snapped to the taal's available tabla tempos while a
@@ -769,9 +790,10 @@ var SwaralipiPlayer = class {
769
790
  * from the note it was on — slowing down a repeated line keeps repeating it. */
770
791
  #reapply() {
771
792
  if (this.#state !== "playing") return;
772
- const at = this.#cursor;
793
+ const at = this.#at;
773
794
  this.#halt();
774
- void this.play(at ?? void 0);
795
+ this.#startAt = at < 0 ? null : at;
796
+ void this.play();
775
797
  }
776
798
  /** The master bus everything routes through (created once, → destination). */
777
799
  #ensureMaster(Tone) {
@@ -863,6 +885,7 @@ var SwaralipiPlayer = class {
863
885
  if (!heard(time)) return;
864
886
  const turn = this.#turn;
865
887
  this.#cursor = ev.path;
888
+ this.#at = i;
866
889
  draw.schedule(() => this.#emitHighlight(ev.path, turn), time);
867
890
  }, at);
868
891
  });
@@ -1099,7 +1122,8 @@ async function exportAudio(doc, options = {}) {
1099
1122
  const schedule = buildSchedule(doc, {
1100
1123
  tonic: options.tonic,
1101
1124
  baseOctave: options.baseOctave,
1102
- laySpeeds: options.laySpeeds
1125
+ laySpeeds: options.laySpeeds,
1126
+ repeats: options.repeats ?? true
1103
1127
  });
1104
1128
  const range = midiRange2(schedule);
1105
1129
  const taalId = resolveTaal(doc, options.taal);
@@ -1472,7 +1496,8 @@ function exportMidi(doc, options = {}) {
1472
1496
  const schedule = buildSchedule(doc, {
1473
1497
  tonic: options.tonic,
1474
1498
  baseOctave: options.baseOctave,
1475
- laySpeeds: options.laySpeeds
1499
+ laySpeeds: options.laySpeeds,
1500
+ repeats: options.repeats ?? true
1476
1501
  });
1477
1502
  const bpm = options.bpm ?? doc.meta.bpm ?? 90;
1478
1503
  const drums = options.drums ? (seg) => {