@ugfoundation/swaralipi-js 0.3.2 → 0.3.3

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.
@@ -390,6 +390,9 @@ var SwaralipiPlayer = class {
390
390
  /** Per-song setBpm tweak: beats meta.bpm but is cleared by the next load(). */
391
391
  #bpmTweak;
392
392
  #doc;
393
+ /** Taal mode (`loadTaal`): loops one avartan of this taal's theka with no
394
+ * melody. Mutually exclusive with `#doc`. */
395
+ #taal;
393
396
  #state = "idle";
394
397
  #tone;
395
398
  /** Master bus everything routes through, so stop() can fade it out. */
@@ -418,6 +421,7 @@ var SwaralipiPlayer = class {
418
421
  tanpura: options.tanpura ?? true,
419
422
  previewLimitSec: options.previewLimitSec ?? Infinity,
420
423
  onHighlight: options.onHighlight,
424
+ onBeat: options.onBeat,
421
425
  onState: options.onState,
422
426
  onLimit: options.onLimit
423
427
  };
@@ -457,6 +461,23 @@ var SwaralipiPlayer = class {
457
461
  load(doc) {
458
462
  this.#halt();
459
463
  this.#doc = doc;
464
+ this.#taal = void 0;
465
+ this.#taalOverride = void 0;
466
+ this.#bpmTweak = void 0;
467
+ this.#setState("ready");
468
+ }
469
+ /**
470
+ * Taal mode: loop one avartan of a taal's theka (rhythm only, no melody) —
471
+ * for a standalone theka player. `play()` loops indefinitely (tabla loop /
472
+ * drum sequence) and fires `onBeat(beatInCycle)` once per matra on the draw
473
+ * thread so the consumer can highlight the active bol / drum-staff note. The
474
+ * taal's `id` should be a sampled taal (see `SAMPLED_TAALS`) for tabla audio;
475
+ * `variation` picks a tabla loop variation. Stops any current playback.
476
+ */
477
+ loadTaal(taal) {
478
+ this.#halt();
479
+ this.#taal = taal;
480
+ this.#doc = void 0;
460
481
  this.#taalOverride = void 0;
461
482
  this.#bpmTweak = void 0;
462
483
  this.#setState("ready");
@@ -464,7 +485,7 @@ var SwaralipiPlayer = class {
464
485
  /** Start (or resume) playback. Must be triggered from a user gesture so the
465
486
  * browser permits `AudioContext` resume. */
466
487
  async play() {
467
- if (this.#doc === void 0 || this.#state === "playing") return;
488
+ if (this.#doc === void 0 && this.#taal === void 0 || this.#state === "playing") return;
468
489
  const Tone = await this.#ensureTone();
469
490
  await Tone.start();
470
491
  if (this.#state === "paused") {
@@ -512,7 +533,7 @@ var SwaralipiPlayer = class {
512
533
  this.#master.gain.cancelScheduledValues(now);
513
534
  this.#master.gain.rampTo(0, STOP_FADE_SEC, now);
514
535
  this.#emitHighlight(null);
515
- this.#setState(this.#doc ? "ready" : "idle");
536
+ this.#setState(this.#doc || this.#taal ? "ready" : "idle");
516
537
  this.#cancelFadeTimer();
517
538
  this.#fadeTimer = setTimeout(() => this.#halt(), STOP_FADE_SEC * 1e3 + 60);
518
539
  return;
@@ -534,7 +555,7 @@ var SwaralipiPlayer = class {
534
555
  this.#restoreMasterGain();
535
556
  this.#scheduleIds = [];
536
557
  this.#emitHighlight(null);
537
- if (this.#state !== "idle") this.#setState(this.#doc ? "ready" : "idle");
558
+ if (this.#state !== "idle") this.#setState(this.#doc || this.#taal ? "ready" : "idle");
538
559
  }
539
560
  #cancelFadeTimer() {
540
561
  if (this.#fadeTimer !== void 0) {
@@ -648,15 +669,16 @@ var SwaralipiPlayer = class {
648
669
  #emitHighlight(path) {
649
670
  this.#opts.onHighlight?.(path);
650
671
  }
651
- /** The tabla variation to play: the doc's own (in auto mode), else none — an
652
- * explicit setTaal override plays the taal's default variation. */
672
+ /** The tabla variation to play: taal-mode taal's, the doc's own (auto mode),
673
+ * else none — an explicit setTaal override plays the taal's default variation. */
653
674
  #variation() {
675
+ if (this.#taal !== void 0) return this.#taal.variation;
654
676
  return this.#taalAuto ? this.#doc?.meta.taal?.variation : void 0;
655
677
  }
656
- /** Resolve the active taal: an explicit override, else the doc's. */
678
+ /** Resolve the active taal id: taal mode's taal, an explicit override, else
679
+ * the doc's — but only when it names a sampled taal. */
657
680
  #taalId() {
658
- if (!this.#taalAuto && this.#taalOverride !== void 0) return this.#taalOverride;
659
- const id = this.#doc?.meta.taal?.id;
681
+ const id = this.#taal?.id ?? (!this.#taalAuto && this.#taalOverride !== void 0 ? this.#taalOverride : this.#doc?.meta.taal?.id);
660
682
  return id !== void 0 && id in TAALS ? id : void 0;
661
683
  }
662
684
  /** Resolve an instrument id to its def (custom overrides built-ins; the
@@ -713,6 +735,7 @@ var SwaralipiPlayer = class {
713
735
  /** Build the melody instrument(s)/rhythm/drone for the current settings and
714
736
  * wait for their buffers, then schedule everything on the Transport. */
715
737
  async #arrange(Tone) {
738
+ if (this.#taal !== void 0) return this.#arrangeTaal(Tone);
716
739
  if (this.#doc === void 0) return;
717
740
  const base = this.#opts.soundsBaseUrl;
718
741
  const transport = Tone.getTransport();
@@ -758,6 +781,40 @@ var SwaralipiPlayer = class {
758
781
  }, schedule.totalBeats * spb);
759
782
  transport.scheduleOnce(() => this.stop(), endAt);
760
783
  }
784
+ /** Taal mode: loop one avartan forever (tabla loop / drum sequence via
785
+ * #buildRhythm), firing onBeat + a `String(beatInCycle)` highlight each matra
786
+ * on the draw thread. No melody, no tanpura, no auto-stop — it plays until
787
+ * stop(). */
788
+ async #arrangeTaal(Tone) {
789
+ if (this.#taal === void 0) return;
790
+ const base = this.#opts.soundsBaseUrl;
791
+ const transport = Tone.getTransport();
792
+ transport.cancel(0);
793
+ transport.position = 0;
794
+ this.#scheduleIds = [];
795
+ this.#ensureMaster(Tone);
796
+ this.#restoreMasterGain();
797
+ const bpm = this.#effectiveBpm();
798
+ transport.bpm.value = bpm;
799
+ const spb = 60 / bpm;
800
+ this.#buildRhythm(Tone, transport, base, spb);
801
+ await Tone.loaded();
802
+ if (this.#tabla?.buffer?.loaded) {
803
+ this.#tabla.loopEnd = quantizeLoopEnd(this.#tabla.buffer.duration, spb);
804
+ }
805
+ const draw = Tone.getDraw();
806
+ const n = Math.max(1, this.#taal.numBeats);
807
+ let beat = 0;
808
+ const id = transport.scheduleRepeat((time) => {
809
+ const b = beat % n;
810
+ draw.schedule(() => {
811
+ this.#emitHighlight(String(b));
812
+ this.#opts.onBeat?.(b);
813
+ }, time);
814
+ beat += 1;
815
+ }, spb, 0);
816
+ this.#scheduleIds.push(id);
817
+ }
761
818
  /** Build (or reuse) the melody sampler for `id`, loading only the samples
762
819
  * covering `range`. A cached sampler is reused while it still covers the
763
820
  * range; otherwise it's rebuilt (so changing songs/tonic widens it lazily). */
@@ -788,7 +845,7 @@ var SwaralipiPlayer = class {
788
845
  if (this.#opts.rhythm === "drums") {
789
846
  this.#drums ??= createDrumKit(Tone).connect(master);
790
847
  const drums = this.#drums;
791
- const taal = taalId !== void 0 ? TAALS[taalId] : { numBeats: 4, sam: 0 };
848
+ const taal = this.#taal ?? (taalId !== void 0 ? TAALS[taalId] : { numBeats: 4, sam: 0 });
792
849
  const pattern = drumPattern(taal);
793
850
  let beat2 = 0;
794
851
  const id2 = transport.scheduleRepeat((time) => {
@@ -802,7 +859,7 @@ var SwaralipiPlayer = class {
802
859
  if (this.#opts.rhythm === "none" || base === void 0) return;
803
860
  this.#metronome ??= createMetronome(Tone, base).connect(master);
804
861
  const metro = this.#metronome;
805
- const cycle = taalId !== void 0 ? TAALS[taalId].numBeats : 4;
862
+ const cycle = this.#taal?.numBeats ?? (taalId !== void 0 ? TAALS[taalId].numBeats : 4);
806
863
  let beat = 0;
807
864
  const id = transport.scheduleRepeat((time) => {
808
865
  metro.volume.setValueAtTime(beat % cycle === 0 ? 0 : -8, time);