@ugfoundation/swaralipi-js 0.14.2 → 0.15.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 (47) hide show
  1. package/README.md +3 -1
  2. package/dist/{batch-QEIW6XAA.js → batch-SYLHDJJL.js} +4 -4
  3. package/dist/{batch-QEIW6XAA.js.map → batch-SYLHDJJL.js.map} +1 -1
  4. package/dist/{chunk-CJI7A6AM.js → chunk-J5JYJL5N.js} +3 -3
  5. package/dist/{chunk-CJI7A6AM.js.map → chunk-J5JYJL5N.js.map} +1 -1
  6. package/dist/{chunk-WEXTZRRB.js → chunk-SPZO4573.js} +35 -7
  7. package/dist/chunk-SPZO4573.js.map +1 -0
  8. package/dist/{chunk-6AKH2NLW.js → chunk-WBYJUYI6.js} +3 -3
  9. package/dist/{chunk-6AKH2NLW.js.map → chunk-WBYJUYI6.js.map} +1 -1
  10. package/dist/cli.cjs +1 -1
  11. package/dist/cli.cjs.map +1 -1
  12. package/dist/cli.js +2 -2
  13. package/dist/convert/index.cjs +1 -1
  14. package/dist/convert/index.cjs.map +1 -1
  15. package/dist/convert/index.d.cts +1 -1
  16. package/dist/convert/index.d.ts +1 -1
  17. package/dist/convert/index.js +2 -2
  18. package/dist/index.cjs +1 -1
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +2 -2
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.js +1 -1
  23. package/dist/{instruments-CfHu4bSQ.d.cts → instruments-C-KtHQiW.d.cts} +12 -9
  24. package/dist/{instruments-C3CbtYdm.d.ts → instruments-SoUQBZNd.d.ts} +12 -9
  25. package/dist/{lay-CFLy_wd_.d.ts → lay-CWlEhImP.d.ts} +1 -1
  26. package/dist/{lay-Bi87MnkT.d.cts → lay-CXhC-HRO.d.cts} +1 -1
  27. package/dist/player/index.cjs +93 -31
  28. package/dist/player/index.cjs.map +1 -1
  29. package/dist/player/index.d.cts +25 -5
  30. package/dist/player/index.d.ts +25 -5
  31. package/dist/player/index.js +62 -28
  32. package/dist/player/index.js.map +1 -1
  33. package/dist/spec/index.cjs +1 -1
  34. package/dist/spec/index.cjs.map +1 -1
  35. package/dist/spec/index.d.cts +12 -12
  36. package/dist/spec/index.d.ts +12 -12
  37. package/dist/spec/index.js +1 -1
  38. package/dist/viewer/index.cjs +43 -10
  39. package/dist/viewer/index.cjs.map +1 -1
  40. package/dist/viewer/index.d.cts +6 -4
  41. package/dist/viewer/index.d.ts +6 -4
  42. package/dist/viewer/index.js +10 -5
  43. package/dist/viewer/index.js.map +1 -1
  44. package/package.json +1 -1
  45. package/dist/chunk-WEXTZRRB.js.map +0 -1
  46. package/dist/{schema-DyL0bMCf.d.ts → schema-DZag9qkF.d.cts} +36 -36
  47. package/dist/{schema-DyL0bMCf.d.cts → schema-DZag9qkF.d.ts} +36 -36
@@ -1,8 +1,8 @@
1
- import { S as Swaralipi, T as Taal, O as Octave, a as SvaraLetter, N as Note } from '../schema-DyL0bMCf.cjs';
2
- import { a as LaySpeeds, T as TaalId } from '../lay-Bi87MnkT.cjs';
3
- export { b as LAYS, c as LAY_SPEEDS, L as Lay, p as printedLay, r as resolveLaySpeeds, d as rowLay } from '../lay-Bi87MnkT.cjs';
4
- import { I as InstrumentId, a as InstrumentDef, E as Excerpt, S as Schedule, D as DrumVoice, T as TempoSegment } from '../instruments-CfHu4bSQ.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, f as LyricMap, M as MEEND_HOLD, P as PlayEvent, g as SALAMANDER_BASE, h as SAMPLED_TAALS, i as ScheduleOptions, j as TABLA_BPMS, k as TABLA_VARIATIONS, l as TONEJS_INSTRUMENTS_BASE, m as ToneModule, n as alignLyrics, o as buildSchedule, p as createBuffers, q as drumPattern, r as eventAt, s as glideTargets, t as graceTouches, u as instrumentSampleUrl, v as isArcRaw, w as lyricCues, x as meendContinuations, y as nearestSampleKey, z as nearestTablaBpm, B as normalizeBase, C as probeUrl, F as quantizeLoopEnd, H as santoorUrls, J as scheduleLines, K as subsetUrls, N as tablaBpms, O as tablaUrl, Q as triggerGlide } from '../instruments-CfHu4bSQ.cjs';
1
+ import { S as Swaralipi, T as Taal, O as Octave, a as SvaraLetter, N as Note } from '../schema-DZag9qkF.cjs';
2
+ import { a as LaySpeeds, T as TaalId } from '../lay-CXhC-HRO.cjs';
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, E as Excerpt, S as Schedule, D as DrumVoice, T as TempoSegment } from '../instruments-C-KtHQiW.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, f as LyricMap, M as MEEND_HOLD, P as PlayEvent, g as SALAMANDER_BASE, h as SAMPLED_TAALS, i as ScheduleOptions, j as TABLA_BPMS, k as TABLA_VARIATIONS, l as TONEJS_INSTRUMENTS_BASE, m as ToneModule, n as alignLyrics, o as buildSchedule, p as createBuffers, q as drumPattern, r as eventAt, s as glideTargets, t as graceTouches, u as instrumentSampleUrl, v as isArcRaw, w as lyricCues, x as meendContinuations, y as nearestSampleKey, z as nearestTablaBpm, B as normalizeBase, C as probeUrl, F as quantizeLoopEnd, H as santoorUrls, J as scheduleLines, K as subsetUrls, N as tablaBpms, O as tablaUrl, Q as triggerGlide } from '../instruments-C-KtHQiW.cjs';
6
6
  import 'zod';
7
7
  import 'tone';
8
8
 
@@ -55,6 +55,13 @@ interface PlayerOptions {
55
55
  * sam/tali/khaali structure — a genuine "drum atlas"-style translation,
56
56
  * not a loop; works for every taal (no per-taal sample requirement). */
57
57
  rhythm?: RhythmMode;
58
+ /** Rhythm first, for a song that opens on a pickup (অনাগত): the tabla /
59
+ * drums / metronome play the rest of that cycle and the melody comes in on
60
+ * its own beat — অতীত-গ্রহ, the tāla before the song. Default false: the
61
+ * melody starts at once and the rhythm comes in on the সম — অনাগত-গ্রহ,
62
+ * the song before the tāla (Kohala's Tālalakṣaṇa). The tanpura sounds from
63
+ * the start either way, and a song that opens on sam plays the same. */
64
+ countIn?: boolean;
58
65
  /** Play the tanpura drone. Default true. */
59
66
  tanpura?: boolean;
60
67
  /** Sing the song's form: each `{ }` twice, `( )` only the first time, the
@@ -140,6 +147,8 @@ declare class SwaralipiPlayer {
140
147
  get loop(): LineRange | null;
141
148
  /** Whether each repeat is followed by a silent pass for the learner. */
142
149
  get echo(): boolean;
150
+ /** Whether the rhythm leads a pickup in (see PlayerOptions.countIn). */
151
+ get countIn(): boolean;
143
152
  /** Set the document to play. Stops any current playback. With `excerpt`,
144
153
  * only the notes singing those stretches of the song's text play, in order
145
154
  * (see `alignLyrics`); the length, seeks and repeats all keep to them. */
@@ -204,6 +213,8 @@ declare class SwaralipiPlayer {
204
213
  setPreviewLimit(sec: number): void;
205
214
  setInstrument(instrument: InstrumentId): void;
206
215
  setRhythm(rhythm: RhythmMode): void;
216
+ /** Rhythm first (true) or the melody first (false) — see PlayerOptions.countIn. */
217
+ setCountIn(on: boolean): void;
207
218
  setTanpura(on: boolean): void;
208
219
  /** Override the taal (null restores auto-detection from `meta.taal`). */
209
220
  setTaal(id: TaalId | null): void;
@@ -270,6 +281,10 @@ interface AudioExportOptions {
270
281
  /** Taal for the rhythm: an explicit id, or `auto` (from `meta.taal`). Default
271
282
  * `auto`. Only used when `rhythm` isn't `none`. */
272
283
  taal?: TaalId | 'auto';
284
+ /** Rhythm first for a song that opens on a pickup, as the player's
285
+ * `countIn`. Default false: the melody starts at once and the rhythm comes
286
+ * in on the সম. */
287
+ countIn?: boolean;
273
288
  /** Include the tanpura drone. Default false. */
274
289
  tanpura?: boolean;
275
290
  /** Sing the song's form (repeats, returns to the asthayi), as the player
@@ -368,6 +383,10 @@ interface MidiExportOptions {
368
383
  name?: string;
369
384
  /** Also export the taal's drumset pattern as a separate GM channel-10 track. */
370
385
  drums?: boolean;
386
+ /** With `drums`, a song that opens on a pickup starts with the drums' count-in
387
+ * and the melody after it, as the player's `countIn`. Default false: the
388
+ * melody starts at once and the drums come in on the সম. */
389
+ countIn?: boolean;
371
390
  /** Sing the song's form (repeats, returns to the asthayi), as the player
372
391
  * does. Default true; false keeps the written order. */
373
392
  repeats?: boolean;
@@ -382,6 +401,7 @@ declare function encodeMidi(schedule: Schedule, opts: {
382
401
  bpm: number;
383
402
  name?: string;
384
403
  drums?: DrumVoice[] | ((seg: TempoSegment) => DrumVoice[] | undefined);
404
+ countIn?: boolean;
385
405
  }): Uint8Array;
386
406
  /**
387
407
  * Export a swaralipi doc to a `.mid` Blob (melody voice only). No browser APIs
@@ -1,8 +1,8 @@
1
- import { S as Swaralipi, T as Taal, O as Octave, a as SvaraLetter, N as Note } from '../schema-DyL0bMCf.js';
2
- import { a as LaySpeeds, T as TaalId } from '../lay-CFLy_wd_.js';
3
- export { b as LAYS, c as LAY_SPEEDS, L as Lay, p as printedLay, r as resolveLaySpeeds, d as rowLay } from '../lay-CFLy_wd_.js';
4
- import { I as InstrumentId, a as InstrumentDef, E as Excerpt, S as Schedule, D as DrumVoice, T as TempoSegment } from '../instruments-C3CbtYdm.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, f as LyricMap, M as MEEND_HOLD, P as PlayEvent, g as SALAMANDER_BASE, h as SAMPLED_TAALS, i as ScheduleOptions, j as TABLA_BPMS, k as TABLA_VARIATIONS, l as TONEJS_INSTRUMENTS_BASE, m as ToneModule, n as alignLyrics, o as buildSchedule, p as createBuffers, q as drumPattern, r as eventAt, s as glideTargets, t as graceTouches, u as instrumentSampleUrl, v as isArcRaw, w as lyricCues, x as meendContinuations, y as nearestSampleKey, z as nearestTablaBpm, B as normalizeBase, C as probeUrl, F as quantizeLoopEnd, H as santoorUrls, J as scheduleLines, K as subsetUrls, N as tablaBpms, O as tablaUrl, Q as triggerGlide } from '../instruments-C3CbtYdm.js';
1
+ import { S as Swaralipi, T as Taal, O as Octave, a as SvaraLetter, N as Note } from '../schema-DZag9qkF.js';
2
+ import { a as LaySpeeds, T as TaalId } from '../lay-CWlEhImP.js';
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, E as Excerpt, S as Schedule, D as DrumVoice, T as TempoSegment } from '../instruments-SoUQBZNd.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, f as LyricMap, M as MEEND_HOLD, P as PlayEvent, g as SALAMANDER_BASE, h as SAMPLED_TAALS, i as ScheduleOptions, j as TABLA_BPMS, k as TABLA_VARIATIONS, l as TONEJS_INSTRUMENTS_BASE, m as ToneModule, n as alignLyrics, o as buildSchedule, p as createBuffers, q as drumPattern, r as eventAt, s as glideTargets, t as graceTouches, u as instrumentSampleUrl, v as isArcRaw, w as lyricCues, x as meendContinuations, y as nearestSampleKey, z as nearestTablaBpm, B as normalizeBase, C as probeUrl, F as quantizeLoopEnd, H as santoorUrls, J as scheduleLines, K as subsetUrls, N as tablaBpms, O as tablaUrl, Q as triggerGlide } from '../instruments-SoUQBZNd.js';
6
6
  import 'zod';
7
7
  import 'tone';
8
8
 
@@ -55,6 +55,13 @@ interface PlayerOptions {
55
55
  * sam/tali/khaali structure — a genuine "drum atlas"-style translation,
56
56
  * not a loop; works for every taal (no per-taal sample requirement). */
57
57
  rhythm?: RhythmMode;
58
+ /** Rhythm first, for a song that opens on a pickup (অনাগত): the tabla /
59
+ * drums / metronome play the rest of that cycle and the melody comes in on
60
+ * its own beat — অতীত-গ্রহ, the tāla before the song. Default false: the
61
+ * melody starts at once and the rhythm comes in on the সম — অনাগত-গ্রহ,
62
+ * the song before the tāla (Kohala's Tālalakṣaṇa). The tanpura sounds from
63
+ * the start either way, and a song that opens on sam plays the same. */
64
+ countIn?: boolean;
58
65
  /** Play the tanpura drone. Default true. */
59
66
  tanpura?: boolean;
60
67
  /** Sing the song's form: each `{ }` twice, `( )` only the first time, the
@@ -140,6 +147,8 @@ declare class SwaralipiPlayer {
140
147
  get loop(): LineRange | null;
141
148
  /** Whether each repeat is followed by a silent pass for the learner. */
142
149
  get echo(): boolean;
150
+ /** Whether the rhythm leads a pickup in (see PlayerOptions.countIn). */
151
+ get countIn(): boolean;
143
152
  /** Set the document to play. Stops any current playback. With `excerpt`,
144
153
  * only the notes singing those stretches of the song's text play, in order
145
154
  * (see `alignLyrics`); the length, seeks and repeats all keep to them. */
@@ -204,6 +213,8 @@ declare class SwaralipiPlayer {
204
213
  setPreviewLimit(sec: number): void;
205
214
  setInstrument(instrument: InstrumentId): void;
206
215
  setRhythm(rhythm: RhythmMode): void;
216
+ /** Rhythm first (true) or the melody first (false) — see PlayerOptions.countIn. */
217
+ setCountIn(on: boolean): void;
207
218
  setTanpura(on: boolean): void;
208
219
  /** Override the taal (null restores auto-detection from `meta.taal`). */
209
220
  setTaal(id: TaalId | null): void;
@@ -270,6 +281,10 @@ interface AudioExportOptions {
270
281
  /** Taal for the rhythm: an explicit id, or `auto` (from `meta.taal`). Default
271
282
  * `auto`. Only used when `rhythm` isn't `none`. */
272
283
  taal?: TaalId | 'auto';
284
+ /** Rhythm first for a song that opens on a pickup, as the player's
285
+ * `countIn`. Default false: the melody starts at once and the rhythm comes
286
+ * in on the সম. */
287
+ countIn?: boolean;
273
288
  /** Include the tanpura drone. Default false. */
274
289
  tanpura?: boolean;
275
290
  /** Sing the song's form (repeats, returns to the asthayi), as the player
@@ -368,6 +383,10 @@ interface MidiExportOptions {
368
383
  name?: string;
369
384
  /** Also export the taal's drumset pattern as a separate GM channel-10 track. */
370
385
  drums?: boolean;
386
+ /** With `drums`, a song that opens on a pickup starts with the drums' count-in
387
+ * and the melody after it, as the player's `countIn`. Default false: the
388
+ * melody starts at once and the drums come in on the সম. */
389
+ countIn?: boolean;
371
390
  /** Sing the song's form (repeats, returns to the asthayi), as the player
372
391
  * does. Default true; false keeps the written order. */
373
392
  repeats?: boolean;
@@ -382,6 +401,7 @@ declare function encodeMidi(schedule: Schedule, opts: {
382
401
  bpm: number;
383
402
  name?: string;
384
403
  drums?: DrumVoice[] | ((seg: TempoSegment) => DrumVoice[] | undefined);
404
+ countIn?: boolean;
385
405
  }): Uint8Array;
386
406
  /**
387
407
  * Export a swaralipi doc to a `.mid` Blob (melody voice only). No browser APIs
@@ -1,5 +1,5 @@
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, lyricCues, alignLyrics, MEEND_HOLD, sCurve } from '../chunk-WEXTZRRB.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, alignLyrics, buildSchedule, createBuffers, drumPattern, eventAt, glideTargets, graceTouches, instrumentSampleUrl, isArcRaw, lyricCues, meendContinuations, midiToSampleName, nearestSampleKey, nearestTablaBpm, normalizeBase, parseTonic, probeUrl, quantizeLoopEnd, sCurve, sampleFileName, santoorUrls, scheduleLines, subsetUrls, svaraSemitone, svaraToMidi, tablaBpms, tablaUrl, triggerGlide } from '../chunk-WEXTZRRB.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, lyricCues, alignLyrics, MEEND_HOLD, sCurve } from '../chunk-SPZO4573.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, alignLyrics, buildSchedule, createBuffers, drumPattern, eventAt, glideTargets, graceTouches, instrumentSampleUrl, isArcRaw, lyricCues, meendContinuations, midiToSampleName, nearestSampleKey, nearestTablaBpm, normalizeBase, parseTonic, probeUrl, quantizeLoopEnd, sCurve, sampleFileName, santoorUrls, scheduleLines, subsetUrls, svaraSemitone, svaraToMidi, tablaBpms, tablaUrl, triggerGlide } from '../chunk-SPZO4573.js';
3
3
  import '../chunk-BSZ3WQUP.js';
4
4
  import { TAALS } from '../chunk-K73Y46WU.js';
5
5
  export { LAYS, LAY_SPEEDS, printedLay, resolveLaySpeeds, rowLay } from '../chunk-K73Y46WU.js';
@@ -30,8 +30,9 @@ function midiRange(schedule) {
30
30
  }
31
31
  return lo <= hi ? { lo, hi } : void 0;
32
32
  }
33
- function playDurationSec(schedule, spb, rhythm) {
34
- return schedule.totalBeats * spb + (rhythm !== "none" ? schedule.pickupBeats * spb : 0) + spb;
33
+ var leadIn = (schedule, spb, rhythm, countIn) => countIn && rhythm !== "none" ? schedule.pickupBeats * spb : 0;
34
+ function playDurationSec(schedule, spb, rhythm, countIn) {
35
+ return schedule.totalBeats * spb + leadIn(schedule, spb, rhythm, countIn) + spb;
35
36
  }
36
37
  async function loadedOrWarn(Tone) {
37
38
  try {
@@ -107,6 +108,7 @@ var SwaralipiPlayer = class {
107
108
  instrument: options.instrument ?? "piano",
108
109
  instruments: options.instruments ?? Object.keys(INSTRUMENTS),
109
110
  rhythm: options.rhythm ?? "tabla",
111
+ countIn: options.countIn ?? false,
110
112
  tanpura: options.tanpura ?? true,
111
113
  repeats: options.repeats ?? true,
112
114
  previewLimitSec: options.previewLimitSec ?? Infinity,
@@ -158,7 +160,12 @@ var SwaralipiPlayer = class {
158
160
  get durationSec() {
159
161
  if (this.#taal !== void 0) return Infinity;
160
162
  if (this.#doc === void 0) return 0;
161
- const full = playDurationSec(buildSchedule(this.#doc, this.#scheduleOpts()), 60 / this.#effectiveBpm(), this.#opts.rhythm);
163
+ const full = playDurationSec(
164
+ buildSchedule(this.#doc, this.#scheduleOpts()),
165
+ 60 / this.#effectiveBpm(),
166
+ this.#opts.rhythm,
167
+ this.#opts.countIn
168
+ );
162
169
  return Math.min(full, this.#opts.previewLimitSec);
163
170
  }
164
171
  /** Seconds played so far, off the Transport's own clock — so it holds while
@@ -178,6 +185,10 @@ var SwaralipiPlayer = class {
178
185
  get echo() {
179
186
  return this.#echo;
180
187
  }
188
+ /** Whether the rhythm leads a pickup in (see PlayerOptions.countIn). */
189
+ get countIn() {
190
+ return this.#opts.countIn;
191
+ }
181
192
  /* ----------------------------------------------------------------- */
182
193
  /* Loading + transport control */
183
194
  /* ----------------------------------------------------------------- */
@@ -486,6 +497,11 @@ var SwaralipiPlayer = class {
486
497
  this.#opts.rhythm = rhythm;
487
498
  this.#reapply();
488
499
  }
500
+ /** Rhythm first (true) or the melody first (false) — see PlayerOptions.countIn. */
501
+ setCountIn(on) {
502
+ this.#opts.countIn = on;
503
+ this.#reapply();
504
+ }
489
505
  setTanpura(on) {
490
506
  this.#opts.tanpura = on;
491
507
  this.#reapply();
@@ -833,9 +849,10 @@ var SwaralipiPlayer = class {
833
849
  const melody = this.#buildMelody(Tone, id, base, range);
834
850
  this.#voice = melody;
835
851
  const glide = schedule.events.some((ev) => ev.glide === true) ? this.#buildGlide(Tone, id, base, range) : void 0;
836
- const shift = this.#opts.rhythm !== "none" ? schedule.pickupBeats * spb : 0;
852
+ const shift = leadIn(schedule, spb, this.#opts.rhythm, this.#opts.countIn);
853
+ const entry = this.#opts.countIn ? 0 : schedule.samBeat * spb;
837
854
  this.#timeline = { schedule, lines, spb, shift };
838
- this.#buildRhythm(Tone, transport, base, spb, schedule.tempo, shift);
855
+ this.#buildRhythm(Tone, transport, base, spb, schedule.tempo, shift, entry);
839
856
  this.#buildTanpura(Tone, transport, base, spb, schedule.tempo, shift);
840
857
  await loadedOrWarn(Tone);
841
858
  this.#quantizeTablaLoops();
@@ -899,7 +916,7 @@ var SwaralipiPlayer = class {
899
916
  }, at);
900
917
  });
901
918
  this.#applyLoop();
902
- const endAt = playDurationSec(schedule, spb, this.#opts.rhythm);
919
+ const endAt = playDurationSec(schedule, spb, this.#opts.rhythm, this.#opts.countIn);
903
920
  transport.scheduleOnce((time) => {
904
921
  this.#opts.onEvent?.(null);
905
922
  draw.schedule(() => this.#emitHighlight(null), time);
@@ -922,7 +939,7 @@ var SwaralipiPlayer = class {
922
939
  const bpm = this.#effectiveBpm();
923
940
  transport.bpm.value = bpm;
924
941
  const spb = 60 / bpm;
925
- this.#buildRhythm(Tone, transport, base, spb, TAAL_MODE_TEMPO, 0);
942
+ this.#buildRhythm(Tone, transport, base, spb, TAAL_MODE_TEMPO, 0, 0);
926
943
  await loadedOrWarn(Tone);
927
944
  this.#quantizeTablaLoops();
928
945
  const draw = Tone.getDraw();
@@ -975,9 +992,10 @@ var SwaralipiPlayer = class {
975
992
  * click / drum / drone patterns re-space their interval. A তালফেরতা swaps the
976
993
  * theka the same way: each segment loops its own taal's file (or pattern, or
977
994
  * click cycle), and a মুক্তছন্দ segment rests. A document with neither mark
978
- * has exactly one segment, so this is the old single-loop path.
995
+ * has exactly one segment, so this is the old single-loop path. Nothing
996
+ * sounds before `entry` (transport seconds): the সম a pickup leads into.
979
997
  */
980
- #buildRhythm(Tone, transport, base, spb, segments, shift) {
998
+ #buildRhythm(Tone, transport, base, spb, segments, shift, entry) {
981
999
  this.#disposeTabla();
982
1000
  const master = this.#ensureMaster(Tone);
983
1001
  const rhythm = this.#opts.rhythm;
@@ -986,8 +1004,9 @@ var SwaralipiPlayer = class {
986
1004
  const own = this.#segmentTaal(seg);
987
1005
  if (own === null) return;
988
1006
  const taalId = own !== void 0 && own.id in TAALS ? own.id : void 0;
989
- const from = i === 0 ? 0 : seg.beat * spb + shift;
1007
+ const from = Math.max(i === 0 ? 0 : seg.beat * spb + shift, entry);
990
1008
  const to = i === segments.length - 1 ? Infinity : seg.endBeat * spb + shift;
1009
+ if (from >= to) return;
991
1010
  const beatSec = spb / seg.scale;
992
1011
  const span = to === Infinity ? void 0 : to - from;
993
1012
  const beatAt = (time) => Math.round((transport.getSecondsAtTime(time) - from) / beatSec);
@@ -1176,7 +1195,9 @@ async function exportAudio(doc, options = {}) {
1176
1195
  const spb = 60 / bpm;
1177
1196
  const id = options.instrument ?? "piano";
1178
1197
  const def = pickDef(id, options);
1179
- const shift = rhythm !== "none" ? schedule.pickupBeats * spb : 0;
1198
+ const countIn = options.countIn ?? false;
1199
+ const shift = countIn && rhythm !== "none" ? schedule.pickupBeats * spb : 0;
1200
+ const entry = countIn ? 0 : schedule.samBeat * spb;
1180
1201
  const durationSec = Math.max(schedule.totalBeats * spb + shift + (def.release ?? 1) + fadeOutSec + 0.5, 0.5);
1181
1202
  const rendered = await renderOffline(
1182
1203
  Tone,
@@ -1229,8 +1250,9 @@ async function exportAudio(doc, options = {}) {
1229
1250
  const own = pinned ?? seg.taal;
1230
1251
  if (own === null) return;
1231
1252
  const segId = own !== void 0 && own.id in TAALS ? own.id : void 0;
1232
- const from = i === 0 ? 0 : seg.beat * spb + shift;
1253
+ const from = Math.max(i === 0 ? 0 : seg.beat * spb + shift, entry);
1233
1254
  const to = i === schedule.tempo.length - 1 ? Infinity : seg.endBeat * spb + shift;
1255
+ if (from >= to) return;
1234
1256
  const beatSec = spb / seg.scale;
1235
1257
  const span = to === Infinity ? void 0 : to - from;
1236
1258
  if (rhythm === "tabla" && segId !== void 0 && SAMPLED_TAALS.has(segId)) {
@@ -1518,19 +1540,23 @@ function melodyEvents(schedule, offset = 0) {
1518
1540
  });
1519
1541
  return melody;
1520
1542
  }
1521
- function drumEvents(schedule, drums, offset = 0) {
1543
+ var leadIn2 = (schedule, countIn) => countIn ? schedule.pickupBeats : 0;
1544
+ function drumEvents(schedule, drums, offset = 0, countIn = false) {
1522
1545
  const drum = [];
1523
- for (const seg of schedule.tempo) {
1546
+ const shift = leadIn2(schedule, countIn);
1547
+ const entry = countIn ? 0 : schedule.samBeat;
1548
+ schedule.tempo.forEach((seg, i) => {
1524
1549
  const pattern = typeof drums === "function" ? drums(seg) : drums;
1525
- if (!pattern?.length) continue;
1526
- const end = seg === schedule.tempo[schedule.tempo.length - 1] ? Math.ceil(schedule.totalBeats) : seg.endBeat;
1527
- for (let k = 0; seg.beat + k / seg.scale < end - 1e-6; k++) {
1550
+ if (!pattern?.length) return;
1551
+ const from = Math.max(i === 0 ? 0 : seg.beat + shift, entry);
1552
+ const end = (i === schedule.tempo.length - 1 ? Math.ceil(schedule.totalBeats) : seg.endBeat) + shift;
1553
+ for (let k = 0; from + k / seg.scale < end - 1e-6; k++) {
1528
1554
  const note = DRUM_NOTE[pattern[k % pattern.length]];
1529
- const t = Math.round((seg.beat + k / seg.scale) * PPQ) + offset;
1555
+ const t = Math.round((from + k / seg.scale) * PPQ) + offset;
1530
1556
  drum.push({ tick: t, on: true, note, vel: 100, ch: 9 });
1531
1557
  drum.push({ tick: t + Math.round(PPQ / 4), on: false, note, vel: 0, ch: 9 });
1532
1558
  }
1533
- }
1559
+ });
1534
1560
  return drum;
1535
1561
  }
1536
1562
  function midiFile(chunks) {
@@ -1546,9 +1572,11 @@ function encodeMidi(schedule, opts) {
1546
1572
  const head = [];
1547
1573
  if (opts.name) head.push(...vlq(0), 255, 3, ...vlq(utf8(opts.name).length), ...utf8(opts.name));
1548
1574
  head.push(...vlq(0), 255, 81, 3, usPerBeat >> 16 & 255, usPerBeat >> 8 & 255, usPerBeat & 255);
1549
- const chunks = [trackChunk(melodyEvents(schedule), head)];
1550
- if (typeof opts.drums === "function" || opts.drums && opts.drums.length > 0) {
1551
- const drum = drumEvents(schedule, opts.drums);
1575
+ const withDrums = typeof opts.drums === "function" || opts.drums !== void 0 && opts.drums.length > 0;
1576
+ const lead = withDrums ? leadIn2(schedule, opts.countIn) : 0;
1577
+ const chunks = [trackChunk(melodyEvents(schedule, Math.round(lead * PPQ)), head)];
1578
+ if (withDrums) {
1579
+ const drum = drumEvents(schedule, opts.drums, 0, opts.countIn);
1552
1580
  chunks.push(trackChunk(drum, [...vlq(0), 255, 3, ...vlq(5), 68, 114, 117, 109, 115]));
1553
1581
  }
1554
1582
  return midiFile(chunks);
@@ -1567,7 +1595,12 @@ function safeName2(title) {
1567
1595
  function exportMidi(doc, options = {}) {
1568
1596
  const schedule = exportSchedule(doc, options);
1569
1597
  const bpm = options.bpm ?? doc.meta.bpm ?? 90;
1570
- const bytes = encodeMidi(schedule, { bpm, name: options.name ?? doc.meta.title, drums: options.drums ? taalDrums : void 0 });
1598
+ const bytes = encodeMidi(schedule, {
1599
+ bpm,
1600
+ name: options.name ?? doc.meta.title,
1601
+ drums: options.drums ? taalDrums : void 0,
1602
+ countIn: options.countIn
1603
+ });
1571
1604
  return {
1572
1605
  blob: new Blob([bytes], { type: "audio/midi" }),
1573
1606
  filename: `${safeName2(doc.meta.title)}.mid`,
@@ -1584,9 +1617,10 @@ function exportMidiSet(items, name) {
1584
1617
  const usPerBeat = Math.round(6e7 / (options.bpm ?? doc.meta.bpm ?? 90));
1585
1618
  const at = (bytes2) => ({ tick, on: false, note: 0, vel: 0, ch: 0, meta: bytes2 });
1586
1619
  melody.push(at(meta(81, [usPerBeat >> 16 & 255, usPerBeat >> 8 & 255, usPerBeat & 255])));
1587
- melody.push(at(meta(6, utf8(options.name ?? doc.meta.title ?? ""))), ...melodyEvents(schedule, tick));
1588
- if (options.drums) drum.push(...drumEvents(schedule, taalDrums, tick));
1589
- beat += Math.ceil(schedule.totalBeats) + 2;
1620
+ const lead = options.drums ? leadIn2(schedule, options.countIn) : 0;
1621
+ melody.push(at(meta(6, utf8(options.name ?? doc.meta.title ?? ""))), ...melodyEvents(schedule, tick + Math.round(lead * PPQ)));
1622
+ if (options.drums) drum.push(...drumEvents(schedule, taalDrums, tick, options.countIn));
1623
+ beat += Math.ceil(schedule.totalBeats + lead) + 2;
1590
1624
  }
1591
1625
  const chunks = [trackChunk(melody, [...vlq(0), ...meta(3, utf8(name))])];
1592
1626
  if (drum.length > 0) chunks.push(trackChunk(drum, [...vlq(0), ...meta(3, utf8("Drums"))]));