@ugfoundation/swaralipi-js 0.7.4 → 0.7.6

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.
@@ -48,6 +48,15 @@ interface PlayerOptions {
48
48
  onLimit?: () => void;
49
49
  /** Called with the active note's `part.row.note` path, or null when idle. */
50
50
  onHighlight?: (path: string | null) => void;
51
+ /** The same path, but fired on the AUDIO clock — Tone's worker ticker, the one
52
+ * that fires the notes themselves — so it keeps arriving while the page is
53
+ * hidden. `onHighlight` cannot: it runs on Tone.Draw, i.e.
54
+ * `requestAnimationFrame`, which stops the moment a screen locks, so anything
55
+ * hanging off it freezes until the reader looks at the page again. Use this for
56
+ * consumers that are not drawing (media-session metadata, analytics); it fires
57
+ * early — one transport lookahead (~0.1s) plus MEDIA_LEAD_SEC — and only in
58
+ * doc mode. */
59
+ onEvent?: (path: string | null) => void;
51
60
  /** Taal mode only (`loadTaal`): called on the draw thread once per matra with
52
61
  * the beat index within the cycle (0-based, `< numBeats`), so a consumer can
53
62
  * highlight the active theka bol / drum-staff note. Never fired in doc mode. */
@@ -84,6 +93,19 @@ declare class SwaralipiPlayer {
84
93
  get taalId(): TaalId | undefined;
85
94
  /** Whether the effective rhythm will actually be a sampled tabla. */
86
95
  get usesTabla(): boolean;
96
+ /**
97
+ * Length of one play-through in seconds, for a progress bar (the OS media
98
+ * session's, typically). Derived from the loaded notation on every read and
99
+ * never stored, because every input can move: re-engrave the document, drag
100
+ * the tempo, switch the rhythm off, and this changes with it. `Infinity` in
101
+ * taal mode (one avartan looping, no end — the value the Media Session spec
102
+ * reserves for exactly that), 0 with nothing loaded, and clamped by a preview
103
+ * cap when one is set, since that is when playback will really stop.
104
+ */
105
+ get durationSec(): number;
106
+ /** Seconds played so far, off the Transport's own clock — so it holds while
107
+ * paused, and reads 0 before the first play and after stop(). */
108
+ get positionSec(): number;
87
109
  /** Set the document to play. Stops any current playback. */
88
110
  load(doc: Swaralipi): void;
89
111
  /**
@@ -48,6 +48,15 @@ interface PlayerOptions {
48
48
  onLimit?: () => void;
49
49
  /** Called with the active note's `part.row.note` path, or null when idle. */
50
50
  onHighlight?: (path: string | null) => void;
51
+ /** The same path, but fired on the AUDIO clock — Tone's worker ticker, the one
52
+ * that fires the notes themselves — so it keeps arriving while the page is
53
+ * hidden. `onHighlight` cannot: it runs on Tone.Draw, i.e.
54
+ * `requestAnimationFrame`, which stops the moment a screen locks, so anything
55
+ * hanging off it freezes until the reader looks at the page again. Use this for
56
+ * consumers that are not drawing (media-session metadata, analytics); it fires
57
+ * early — one transport lookahead (~0.1s) plus MEDIA_LEAD_SEC — and only in
58
+ * doc mode. */
59
+ onEvent?: (path: string | null) => void;
51
60
  /** Taal mode only (`loadTaal`): called on the draw thread once per matra with
52
61
  * the beat index within the cycle (0-based, `< numBeats`), so a consumer can
53
62
  * highlight the active theka bol / drum-staff note. Never fired in doc mode. */
@@ -84,6 +93,19 @@ declare class SwaralipiPlayer {
84
93
  get taalId(): TaalId | undefined;
85
94
  /** Whether the effective rhythm will actually be a sampled tabla. */
86
95
  get usesTabla(): boolean;
96
+ /**
97
+ * Length of one play-through in seconds, for a progress bar (the OS media
98
+ * session's, typically). Derived from the loaded notation on every read and
99
+ * never stored, because every input can move: re-engrave the document, drag
100
+ * the tempo, switch the rhythm off, and this changes with it. `Infinity` in
101
+ * taal mode (one avartan looping, no end — the value the Media Session spec
102
+ * reserves for exactly that), 0 with nothing loaded, and clamped by a preview
103
+ * cap when one is set, since that is when playback will really stop.
104
+ */
105
+ get durationSec(): number;
106
+ /** Seconds played so far, off the Transport's own clock — so it holds while
107
+ * paused, and reads 0 before the first play and after stop(). */
108
+ get positionSec(): number;
87
109
  /** Set the document to play. Stops any current playback. */
88
110
  load(doc: Swaralipi): void;
89
111
  /**
@@ -1,9 +1,10 @@
1
- import { DEFAULT_SOUNDS_BASE, INSTRUMENTS, SAMPLED_TAALS, instrumentSampleUrl, probeUrl, normalizeBase, nearestTablaBpm, tablaBpms, buildSchedule, quantizeLoopEnd, midiToSampleName, glideTargets, triggerGlide, createSampler, createBuffers, tablaUrl, tablaShiftSemitones, TABLA_GRAIN_OVERLAP_SEC, TABLA_GRAIN_SEC, createDrumKit, drumPattern, createMetronome, TANPURA_PCS, createTanpura, parseTonic, MEEND_HOLD, sCurve } from '../chunk-2SAZWTTK.js';
1
+ import { DEFAULT_SOUNDS_BASE, INSTRUMENTS, SAMPLED_TAALS, buildSchedule, instrumentSampleUrl, probeUrl, normalizeBase, nearestTablaBpm, tablaBpms, quantizeLoopEnd, midiToSampleName, glideTargets, triggerGlide, createSampler, createBuffers, tablaUrl, tablaShiftSemitones, TABLA_GRAIN_OVERLAP_SEC, TABLA_GRAIN_SEC, createDrumKit, drumPattern, createMetronome, TANPURA_PCS, createTanpura, parseTonic, MEEND_HOLD, sCurve } from '../chunk-2SAZWTTK.js';
2
2
  export { ARC_RAW_RE, DEFAULT_SOUNDS_BASE, DRUM_KIT_BASE, INSTRUMENTS, MEEND_HOLD, OCTAVE_OFFSET, SALAMANDER_BASE, SAMPLED_TAALS, SHUDDHA, TABLA_BPMS, TABLA_VARIATIONS, TONEJS_INSTRUMENTS_BASE, buildSchedule, createBuffers, drumPattern, glideTargets, instrumentSampleUrl, isArcRaw, meendContinuations, midiToSampleName, nearestSampleKey, nearestTablaBpm, normalizeBase, parseTonic, probeUrl, quantizeLoopEnd, sCurve, sampleFileName, santoorUrls, subsetUrls, svaraSemitone, svaraToMidi, tablaBpms, tablaUrl, triggerGlide } from '../chunk-2SAZWTTK.js';
3
3
  import { TAALS } from '../chunk-UF5OSJRP.js';
4
4
 
5
5
  // src/player/player.ts
6
6
  var STOP_FADE_SEC = 0.5;
7
+ var MEDIA_LEAD_SEC = 0.25;
7
8
  function midiRange(schedule) {
8
9
  let lo = Infinity;
9
10
  let hi = -Infinity;
@@ -17,6 +18,9 @@ function midiRange(schedule) {
17
18
  }
18
19
  return lo <= hi ? { lo, hi } : void 0;
19
20
  }
21
+ function playDurationSec(schedule, spb, rhythm) {
22
+ return schedule.totalBeats * spb + (rhythm !== "none" ? schedule.pickupBeats * spb : 0) + spb;
23
+ }
20
24
  async function loadedOrWarn(Tone) {
21
25
  try {
22
26
  await Tone.loaded();
@@ -63,6 +67,7 @@ var SwaralipiPlayer = class {
63
67
  tanpura: options.tanpura ?? true,
64
68
  previewLimitSec: options.previewLimitSec ?? Infinity,
65
69
  onHighlight: options.onHighlight,
70
+ onEvent: options.onEvent,
66
71
  onBeat: options.onBeat,
67
72
  onState: options.onState,
68
73
  onLimit: options.onLimit
@@ -96,6 +101,27 @@ var SwaralipiPlayer = class {
96
101
  const id = this.#taalId();
97
102
  return this.#opts.rhythm === "tabla" && id !== void 0 && SAMPLED_TAALS.has(id) && !!this.#opts.soundsBaseUrl;
98
103
  }
104
+ /**
105
+ * Length of one play-through in seconds, for a progress bar (the OS media
106
+ * session's, typically). Derived from the loaded notation on every read and
107
+ * never stored, because every input can move: re-engrave the document, drag
108
+ * the tempo, switch the rhythm off, and this changes with it. `Infinity` in
109
+ * taal mode (one avartan looping, no end — the value the Media Session spec
110
+ * reserves for exactly that), 0 with nothing loaded, and clamped by a preview
111
+ * cap when one is set, since that is when playback will really stop.
112
+ */
113
+ get durationSec() {
114
+ if (this.#taal !== void 0) return Infinity;
115
+ if (this.#doc === void 0) return 0;
116
+ const schedule = buildSchedule(this.#doc, { tonic: this.#opts.tonic, baseOctave: this.#opts.baseOctave });
117
+ const full = playDurationSec(schedule, 60 / this.#effectiveBpm(), this.#opts.rhythm);
118
+ return Math.min(full, this.#opts.previewLimitSec);
119
+ }
120
+ /** Seconds played so far, off the Transport's own clock — so it holds while
121
+ * paused, and reads 0 before the first play and after stop(). */
122
+ get positionSec() {
123
+ return this.#tone?.getTransport().seconds ?? 0;
124
+ }
99
125
  /* ----------------------------------------------------------------- */
100
126
  /* Loading + transport control */
101
127
  /* ----------------------------------------------------------------- */
@@ -408,6 +434,7 @@ var SwaralipiPlayer = class {
408
434
  }
409
435
  const draw = Tone.getDraw();
410
436
  const shift = this.#opts.rhythm !== "none" ? schedule.pickupBeats * spb : 0;
437
+ const mediaLead = Math.min(MEDIA_LEAD_SEC, spb);
411
438
  schedule.events.forEach((ev, i) => {
412
439
  const at = ev.beat * spb + shift;
413
440
  if (ev.midi !== null && ev.glide !== true) {
@@ -441,12 +468,14 @@ var SwaralipiPlayer = class {
441
468
  transport.scheduleOnce((time) => melody.triggerAttackRelease(name, dur, time), at);
442
469
  }
443
470
  }
444
- transport.scheduleOnce((time) => {
445
- draw.schedule(() => this.#emitHighlight(ev.path), time);
446
- }, at);
471
+ if (this.#opts.onEvent !== void 0) {
472
+ transport.scheduleOnce(() => this.#opts.onEvent?.(ev.path), Math.max(at - mediaLead, 0));
473
+ }
474
+ transport.scheduleOnce((time) => draw.schedule(() => this.#emitHighlight(ev.path), time), at);
447
475
  });
448
- const endAt = schedule.totalBeats * spb + shift + spb;
476
+ const endAt = playDurationSec(schedule, spb, this.#opts.rhythm);
449
477
  transport.scheduleOnce((time) => {
478
+ this.#opts.onEvent?.(null);
450
479
  draw.schedule(() => this.#emitHighlight(null), time);
451
480
  }, schedule.totalBeats * spb + shift);
452
481
  transport.scheduleOnce(() => this.stop(), endAt);