@ugfoundation/swaralipi-js 0.9.2 → 0.10.1
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.
- package/README.md +17 -0
- package/dist/{batch-IHEXFEGC.js → batch-5ZFEKXKS.js} +5 -5
- package/dist/{batch-IHEXFEGC.js.map → batch-5ZFEKXKS.js.map} +1 -1
- package/dist/{chunk-TZBDIRIA.js → chunk-R6VNIBMG.js} +7 -7
- package/dist/chunk-R6VNIBMG.js.map +1 -0
- package/dist/{chunk-MYVIQU4O.js → chunk-SR7NFEGL.js} +5 -3
- package/dist/chunk-SR7NFEGL.js.map +1 -0
- package/dist/{chunk-F23LZMIU.js → chunk-UPJYLXYX.js} +3 -3
- package/dist/{chunk-F23LZMIU.js.map → chunk-UPJYLXYX.js.map} +1 -1
- package/dist/{chunk-SEN266BY.js → chunk-VCLLSNAK.js} +27 -4
- package/dist/chunk-VCLLSNAK.js.map +1 -0
- package/dist/cli.cjs +7 -5
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +3 -3
- package/dist/convert/index.cjs +7 -5
- package/dist/convert/index.cjs.map +1 -1
- package/dist/convert/index.js +3 -3
- package/dist/index.cjs +4 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/{lay-CZIzR9cM.d.ts → lay-0LnKv1LL.d.ts} +7 -1
- package/dist/{lay-CfDlPhH2.d.cts → lay-J17-gS6O.d.cts} +7 -1
- package/dist/player/index.cjs +439 -51
- package/dist/player/index.cjs.map +1 -1
- package/dist/player/index.d.cts +78 -13
- package/dist/player/index.d.ts +78 -13
- package/dist/player/index.js +414 -53
- package/dist/player/index.js.map +1 -1
- package/dist/{schedule-CU-VECDH.d.cts → schedule-B1S53131.d.cts} +28 -2
- package/dist/{schedule-BaovCtmf.d.ts → schedule-DtcYB-o4.d.ts} +28 -2
- package/dist/spec/index.cjs +4 -2
- package/dist/spec/index.cjs.map +1 -1
- package/dist/spec/index.d.cts +2 -2
- package/dist/spec/index.d.ts +2 -2
- package/dist/spec/index.js +2 -2
- package/dist/viewer/index.cjs +148 -7
- package/dist/viewer/index.cjs.map +1 -1
- package/dist/viewer/index.d.cts +22 -5
- package/dist/viewer/index.d.ts +22 -5
- package/dist/viewer/index.js +146 -8
- package/dist/viewer/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-MYVIQU4O.js.map +0 -1
- package/dist/chunk-SEN266BY.js.map +0 -1
- package/dist/chunk-TZBDIRIA.js.map +0 -1
package/dist/player/index.cjs
CHANGED
|
@@ -19,8 +19,10 @@ var TAALS = {
|
|
|
19
19
|
// আড়াঠেকা
|
|
20
20
|
jhampak: { id: "jhampak", numBeats: 5, bhaags: [3, 2], sam: 0 },
|
|
21
21
|
// ঝম্পক
|
|
22
|
-
dhamaar: { id: "dhamaar", numBeats: 14, bhaags: [5, 2, 3, 4], sam: 0, khaali: [7] }
|
|
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 }
|
|
25
|
+
// রূপকড়া
|
|
24
26
|
};
|
|
25
27
|
function sameTaal(a, b) {
|
|
26
28
|
if (a === b) return true;
|
|
@@ -185,6 +187,29 @@ function graceTouches(ev) {
|
|
|
185
187
|
...ev.resh.map((midi, g) => ({ midi, beat: ev.beat + GRACE_STEP * (g + 1) }))
|
|
186
188
|
];
|
|
187
189
|
}
|
|
190
|
+
var rowOf = (path) => path.slice(0, path.lastIndexOf("."));
|
|
191
|
+
function scheduleLines(schedule) {
|
|
192
|
+
const lines = [];
|
|
193
|
+
schedule.events.forEach((ev, i) => {
|
|
194
|
+
if (lines.at(-1)?.row !== rowOf(ev.path)) lines.push({ row: rowOf(ev.path), first: i, beat: ev.beat, endBeat: 0 });
|
|
195
|
+
});
|
|
196
|
+
lines.forEach((line, k) => line.endBeat = lines[k + 1]?.beat ?? schedule.totalBeats);
|
|
197
|
+
return lines;
|
|
198
|
+
}
|
|
199
|
+
function eventAt(events, path) {
|
|
200
|
+
const [p = 0, r = 0, n = 0] = path.split(".").map(Number);
|
|
201
|
+
const inRow = (ev) => ev !== void 0 && rowOf(ev.path) === `${p}.${r}`;
|
|
202
|
+
let i = events.findIndex((ev) => {
|
|
203
|
+
const [ep = 0, er = 0, en = 0] = ev.path.split(".").map(Number);
|
|
204
|
+
return ep > p || ep === p && (er > r || er === r && en >= n);
|
|
205
|
+
});
|
|
206
|
+
if (!inRow(events[i])) {
|
|
207
|
+
const last = (i < 0 ? events.length : i) - 1;
|
|
208
|
+
if (inRow(events[last])) i = last;
|
|
209
|
+
}
|
|
210
|
+
while (i > 0 && events[i].glide === true) i--;
|
|
211
|
+
return i;
|
|
212
|
+
}
|
|
188
213
|
var SAM_TICK = /^\d+[fF]$/;
|
|
189
214
|
function buildSchedule(doc, opts = {}) {
|
|
190
215
|
const tonicPc = parseTonic(opts.tonic ?? doc.meta.tonic);
|
|
@@ -557,10 +582,24 @@ function createTanpura(Tone, base, stems = TANPURA_PCS) {
|
|
|
557
582
|
// src/player/player.ts
|
|
558
583
|
var STOP_FADE_SEC = 0.5;
|
|
559
584
|
var LAY_TAIL_SEC = 0.12;
|
|
585
|
+
var START_PREROLL_SEC = 0.02;
|
|
560
586
|
var TAAL_MODE_TEMPO = [
|
|
561
587
|
{ beat: 0, endBeat: Infinity, scale: 1, lay: "drut" }
|
|
562
588
|
];
|
|
563
589
|
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
|
+
var seekableGrain = (Tone) => class extends Tone.GrainPlayer {
|
|
599
|
+
_start(time, offset, duration) {
|
|
600
|
+
super._start(time, offset === void 0 ? offset : this.toSeconds(offset) * this.playbackRate, duration);
|
|
601
|
+
}
|
|
602
|
+
};
|
|
564
603
|
function midiRange(schedule) {
|
|
565
604
|
let lo = Infinity;
|
|
566
605
|
let hi = -Infinity;
|
|
@@ -609,9 +648,28 @@ var SwaralipiPlayer = class {
|
|
|
609
648
|
* `fileSpb` is the recorded loop's OWN beat length, which is the unit
|
|
610
649
|
* `quantizeLoopEnd` must snap in (loop points are buffer time). */
|
|
611
650
|
#tabla = [];
|
|
651
|
+
#grain;
|
|
612
652
|
#drums;
|
|
613
653
|
#scheduleIds = [];
|
|
614
654
|
#fadeTimer;
|
|
655
|
+
/* Practice: where the playhead is, what repeats, whose turn it is. */
|
|
656
|
+
/** The last note played or sought — what "this line" means to the controls. */
|
|
657
|
+
#cursor = null;
|
|
658
|
+
/** Where the next fresh play() starts (a seek made while stopped). */
|
|
659
|
+
#startAt = null;
|
|
660
|
+
#loop = null;
|
|
661
|
+
#echo = false;
|
|
662
|
+
/** With echo on, every other pass is the learner's: the melody rests. */
|
|
663
|
+
#turn = false;
|
|
664
|
+
/** The arranged timeline, so a seek or a loop can be placed while playing. */
|
|
665
|
+
#timeline;
|
|
666
|
+
/** Listeners on the bound sheet, removed on rebind. */
|
|
667
|
+
#unbind;
|
|
668
|
+
#sheet;
|
|
669
|
+
/** Preview cap, counted in listening time so a seek or a loop can't dodge it. */
|
|
670
|
+
#listened = 0;
|
|
671
|
+
#listenedSince = 0;
|
|
672
|
+
#limitId;
|
|
615
673
|
constructor(options = {}) {
|
|
616
674
|
this.#opts = {
|
|
617
675
|
soundsBaseUrl: options.soundsBaseUrl ?? DEFAULT_SOUNDS_BASE,
|
|
@@ -627,6 +685,7 @@ var SwaralipiPlayer = class {
|
|
|
627
685
|
tanpura: options.tanpura ?? true,
|
|
628
686
|
previewLimitSec: options.previewLimitSec ?? Infinity,
|
|
629
687
|
onHighlight: options.onHighlight,
|
|
688
|
+
onLoop: options.onLoop,
|
|
630
689
|
onEvent: options.onEvent,
|
|
631
690
|
onBeat: options.onBeat,
|
|
632
691
|
onState: options.onState,
|
|
@@ -679,7 +738,19 @@ var SwaralipiPlayer = class {
|
|
|
679
738
|
/** Seconds played so far, off the Transport's own clock — so it holds while
|
|
680
739
|
* paused, and reads 0 before the first play and after stop(). */
|
|
681
740
|
get positionSec() {
|
|
682
|
-
return this.#tone?.getTransport().seconds ?? 0;
|
|
741
|
+
return Math.max(this.#tone?.getTransport().seconds ?? 0, 0);
|
|
742
|
+
}
|
|
743
|
+
/** The note last played or sought (`part.row.note`), or null. */
|
|
744
|
+
get cursor() {
|
|
745
|
+
return this.#cursor;
|
|
746
|
+
}
|
|
747
|
+
/** The repeated lines, or null. */
|
|
748
|
+
get loop() {
|
|
749
|
+
return this.#loop && { ...this.#loop };
|
|
750
|
+
}
|
|
751
|
+
/** Whether each repeat is followed by a silent pass for the learner. */
|
|
752
|
+
get echo() {
|
|
753
|
+
return this.#echo;
|
|
683
754
|
}
|
|
684
755
|
/* ----------------------------------------------------------------- */
|
|
685
756
|
/* Loading + transport control */
|
|
@@ -691,6 +762,7 @@ var SwaralipiPlayer = class {
|
|
|
691
762
|
this.#taal = void 0;
|
|
692
763
|
this.#taalOverride = void 0;
|
|
693
764
|
this.#bpmTweak = void 0;
|
|
765
|
+
this.#resetPractice();
|
|
694
766
|
this.#setState("ready");
|
|
695
767
|
}
|
|
696
768
|
/**
|
|
@@ -707,43 +779,137 @@ var SwaralipiPlayer = class {
|
|
|
707
779
|
this.#doc = void 0;
|
|
708
780
|
this.#taalOverride = void 0;
|
|
709
781
|
this.#bpmTweak = void 0;
|
|
782
|
+
this.#resetPractice();
|
|
710
783
|
this.#setState("ready");
|
|
711
784
|
}
|
|
712
|
-
/** Start (or resume) playback
|
|
713
|
-
*
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
if (
|
|
785
|
+
/** Start (or resume) playback — from `from` (`part.row.note`, or `part.row`
|
|
786
|
+
* for a line's start) when given, which also jumps there mid-play. Must be
|
|
787
|
+
* triggered from a user gesture so the browser permits `AudioContext`
|
|
788
|
+
* resume. */
|
|
789
|
+
async play(from) {
|
|
790
|
+
if (this.#doc === void 0 && this.#taal === void 0) return;
|
|
791
|
+
if (from !== void 0) this.seek(from);
|
|
792
|
+
if (this.#state === "playing" || this.#state === "loading") return;
|
|
793
|
+
const resume = this.#state === "paused";
|
|
794
|
+
if (!resume) this.#setState("loading");
|
|
795
|
+
let Tone;
|
|
796
|
+
try {
|
|
797
|
+
Tone = await this.#ensureTone();
|
|
798
|
+
await Tone.start();
|
|
799
|
+
} catch (err) {
|
|
800
|
+
if (!resume) this.#setState("ready");
|
|
801
|
+
throw err;
|
|
802
|
+
}
|
|
803
|
+
if (resume) {
|
|
719
804
|
Tone.getTransport().start();
|
|
720
805
|
this.#setState("playing");
|
|
806
|
+
this.#armLimit();
|
|
721
807
|
return;
|
|
722
808
|
}
|
|
723
809
|
this.#cancelFade();
|
|
724
|
-
Tone.getTransport()
|
|
725
|
-
|
|
810
|
+
const transport = Tone.getTransport();
|
|
811
|
+
if (transport.state !== "stopped") transport.stop();
|
|
726
812
|
await this.#arrange(Tone);
|
|
727
|
-
|
|
813
|
+
transport.start(void 0, this.#startSec() - this.#preroll());
|
|
814
|
+
this.#startAt = null;
|
|
728
815
|
this.#setState("playing");
|
|
729
|
-
|
|
730
|
-
if (Number.isFinite(limit)) {
|
|
731
|
-
Tone.getTransport().scheduleOnce(() => {
|
|
732
|
-
Tone.getDraw().schedule(() => {
|
|
733
|
-
if (this.#state === "playing") {
|
|
734
|
-
this.stop();
|
|
735
|
-
this.#opts.onLimit?.();
|
|
736
|
-
}
|
|
737
|
-
}, this.#tone.now());
|
|
738
|
-
}, limit);
|
|
739
|
-
}
|
|
816
|
+
this.#armLimit();
|
|
740
817
|
}
|
|
741
818
|
/** Pause; `play()` resumes from the same position. */
|
|
742
819
|
pause() {
|
|
743
820
|
if (this.#tone === void 0 || this.#state !== "playing") return;
|
|
744
821
|
this.#tone.getTransport().pause();
|
|
822
|
+
this.#disarmLimit();
|
|
745
823
|
this.#setState("paused");
|
|
746
824
|
}
|
|
825
|
+
/**
|
|
826
|
+
* Move the playhead to `path` (`part.row.note`, or `part.row` for a line's
|
|
827
|
+
* start). Playing, it jumps and keeps playing; paused or stopped, the next
|
|
828
|
+
* `play()` starts there. A repeat follows the playhead: seeking outside the
|
|
829
|
+
* repeated lines moves the repeat to the new line.
|
|
830
|
+
*/
|
|
831
|
+
seek(path) {
|
|
832
|
+
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;
|
|
836
|
+
const at = plan.schedule.events[i].path;
|
|
837
|
+
this.#cursor = at;
|
|
838
|
+
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 };
|
|
842
|
+
this.#emitLoop();
|
|
843
|
+
if (this.#timeline !== void 0 && (this.#state === "playing" || this.#state === "paused")) {
|
|
844
|
+
this.#applyLoop();
|
|
845
|
+
this.#jump(this.#eventSec(i));
|
|
846
|
+
if (this.#state === "paused") this.#emitHighlight(at);
|
|
847
|
+
} else {
|
|
848
|
+
this.#startAt = at;
|
|
849
|
+
this.#emitHighlight(at);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Repeat lines `from`..`to` (`part.row`, either order, inclusive; `to`
|
|
854
|
+
* defaults to `from`). With no argument, the line under the playhead.
|
|
855
|
+
* `null` stops repeating, and ends the sing-back with it. A playhead
|
|
856
|
+
* outside the new repeat moves to its start; live while playing.
|
|
857
|
+
*/
|
|
858
|
+
setLoop(from, to) {
|
|
859
|
+
this.#turn = false;
|
|
860
|
+
if (from === null) {
|
|
861
|
+
this.#loop = null;
|
|
862
|
+
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);
|
|
871
|
+
}
|
|
872
|
+
this.#applyLoop();
|
|
873
|
+
this.#emitLoop();
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Follow every pass of the repeat with a silent one: the tabla and tanpura
|
|
877
|
+
* keep time and the highlight keeps moving, but the melody rests so the
|
|
878
|
+
* learner can sing the line back — the teacher sings, the student answers.
|
|
879
|
+
* Turning it on with nothing repeating repeats the current line.
|
|
880
|
+
*/
|
|
881
|
+
setEcho(on) {
|
|
882
|
+
this.#echo = on;
|
|
883
|
+
this.#turn = false;
|
|
884
|
+
if (on && this.#loop === null) this.setLoop();
|
|
885
|
+
else this.#emitLoop();
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Move by whole lines. Like a player's previous-track button, a step back
|
|
889
|
+
* from more than a matra into the line restarts that line first.
|
|
890
|
+
* `±Infinity` goes to the first / last line.
|
|
891
|
+
*/
|
|
892
|
+
stepLine(delta) {
|
|
893
|
+
const plan = this.#plan();
|
|
894
|
+
if (plan === void 0 || plan.lines.length === 0) return;
|
|
895
|
+
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;
|
|
898
|
+
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);
|
|
900
|
+
}
|
|
901
|
+
/** Move by single notes (rests and holds included — each is a column). */
|
|
902
|
+
stepNote(delta) {
|
|
903
|
+
const events = this.#plan()?.schedule.events ?? [];
|
|
904
|
+
let i = this.#cursor === null ? -1 : eventAt(events, this.#cursor);
|
|
905
|
+
for (let left = Math.abs(delta); left > 0; left--) {
|
|
906
|
+
do
|
|
907
|
+
i += Math.sign(delta);
|
|
908
|
+
while (events[i]?.glide === true);
|
|
909
|
+
}
|
|
910
|
+
const ev = events[Math.min(Math.max(i, 0), events.length - 1)];
|
|
911
|
+
if (ev !== void 0) this.seek(ev.path);
|
|
912
|
+
}
|
|
747
913
|
/**
|
|
748
914
|
* Stop and rewind to the start. When sounding, the accompaniment (tabla /
|
|
749
915
|
* metronome / tanpura, which otherwise loop) is gently faded out over
|
|
@@ -751,6 +917,13 @@ var SwaralipiPlayer = class {
|
|
|
751
917
|
* playing or cut abruptly; otherwise it halts immediately.
|
|
752
918
|
*/
|
|
753
919
|
stop() {
|
|
920
|
+
this.#disarmLimit();
|
|
921
|
+
this.#listened = 0;
|
|
922
|
+
this.#startAt = null;
|
|
923
|
+
if (this.#turn) {
|
|
924
|
+
this.#turn = false;
|
|
925
|
+
this.#emitLoop();
|
|
926
|
+
}
|
|
754
927
|
if (this.#tone === void 0 || this.#state === "idle") {
|
|
755
928
|
this.#halt();
|
|
756
929
|
return;
|
|
@@ -771,11 +944,15 @@ var SwaralipiPlayer = class {
|
|
|
771
944
|
* looping accompaniment, restore the master gain for the next play. */
|
|
772
945
|
#halt() {
|
|
773
946
|
this.#cancelFadeTimer();
|
|
947
|
+
this.#disarmLimit();
|
|
948
|
+
this.#timeline = void 0;
|
|
949
|
+
this.#fence = 0;
|
|
774
950
|
if (this.#tone !== void 0) {
|
|
775
951
|
const transport = this.#tone.getTransport();
|
|
776
952
|
transport.stop();
|
|
777
953
|
transport.cancel(0);
|
|
778
954
|
transport.position = 0;
|
|
955
|
+
transport.loop = false;
|
|
779
956
|
}
|
|
780
957
|
this.#disposeTabla();
|
|
781
958
|
this.#restoreMasterGain();
|
|
@@ -806,6 +983,27 @@ var SwaralipiPlayer = class {
|
|
|
806
983
|
if (loop.buffer?.loaded) loop.loopEnd = quantizeLoopEnd(loop.buffer.duration, fileSpb);
|
|
807
984
|
}
|
|
808
985
|
}
|
|
986
|
+
/** Count down whatever listening time the preview cap has left. On the
|
|
987
|
+
* audio clock, which keeps running while the page is hidden. */
|
|
988
|
+
#armLimit() {
|
|
989
|
+
const left = this.#opts.previewLimitSec - this.#listened;
|
|
990
|
+
const context = this.#tone?.getContext();
|
|
991
|
+
if (!Number.isFinite(left) || context === void 0) return;
|
|
992
|
+
this.#listenedSince = context.now();
|
|
993
|
+
this.#limitId = context.setTimeout(() => {
|
|
994
|
+
this.#limitId = void 0;
|
|
995
|
+
if (this.#state !== "playing") return;
|
|
996
|
+
this.stop();
|
|
997
|
+
this.#opts.onLimit?.();
|
|
998
|
+
}, Math.max(left, 0));
|
|
999
|
+
}
|
|
1000
|
+
#disarmLimit() {
|
|
1001
|
+
const context = this.#tone?.getContext();
|
|
1002
|
+
if (this.#limitId === void 0 || context === void 0) return;
|
|
1003
|
+
context.clearTimeout(this.#limitId);
|
|
1004
|
+
this.#limitId = void 0;
|
|
1005
|
+
this.#listened += context.now() - this.#listenedSince;
|
|
1006
|
+
}
|
|
809
1007
|
#restoreMasterGain() {
|
|
810
1008
|
if (this.#master !== void 0 && this.#tone !== void 0) {
|
|
811
1009
|
this.#master.gain.cancelScheduledValues(this.#tone.now());
|
|
@@ -845,18 +1043,81 @@ var SwaralipiPlayer = class {
|
|
|
845
1043
|
this.#taalAuto = id === null;
|
|
846
1044
|
this.#reapply();
|
|
847
1045
|
}
|
|
848
|
-
/**
|
|
849
|
-
*
|
|
1046
|
+
/**
|
|
1047
|
+
* Wire highlight output to a `<swaralipi-sheet>` (or any highlight target).
|
|
1048
|
+
* Pass `{ autoScroll }` to keep the active note in view while playing.
|
|
1049
|
+
*
|
|
1050
|
+
* A sheet element also becomes the practice surface: a tap on a line plays
|
|
1051
|
+
* from the note under it (Shift stretches the repeat to that line), and
|
|
1052
|
+
* while it has focus — only then, so no key is taken from the page — Space
|
|
1053
|
+
* plays/pauses, ↑/↓ move by line, ←/→ by note, Home/End jump to the first /
|
|
1054
|
+
* last line, R repeats the line, T turns sing-back on/off, Esc stops the
|
|
1055
|
+
* repeat and Shift+↑/↓ stretch it. To gate a tap-to-play (a play quota, say),
|
|
1056
|
+
* `preventDefault()` its `swaralipi-seek` event in a capturing listener.
|
|
1057
|
+
*/
|
|
850
1058
|
bindSheet(target, opts = {}) {
|
|
851
1059
|
const block = opts.autoScroll === true ? "center" : opts.autoScroll || void 0;
|
|
852
|
-
this.#opts.onHighlight = (path) => {
|
|
1060
|
+
this.#opts.onHighlight = (path, yourTurn) => {
|
|
853
1061
|
target.clearHighlights();
|
|
854
1062
|
if (path === null) return;
|
|
855
|
-
const
|
|
856
|
-
if (block !== void 0 &&
|
|
857
|
-
|
|
1063
|
+
const el2 = target.highlight(path, yourTurn);
|
|
1064
|
+
if (block !== void 0 && el2 !== null) {
|
|
1065
|
+
el2.scrollIntoView({ block, inline: "nearest", behavior: "smooth" });
|
|
858
1066
|
}
|
|
859
1067
|
};
|
|
1068
|
+
this.#sheet = target;
|
|
1069
|
+
target.markLoop?.(this.#loop, this.#turn);
|
|
1070
|
+
this.#unbind?.abort();
|
|
1071
|
+
if (!(target instanceof EventTarget)) return;
|
|
1072
|
+
const el = target;
|
|
1073
|
+
const { signal } = this.#unbind = new AbortController();
|
|
1074
|
+
el.setAttribute("seekable", "");
|
|
1075
|
+
signal.addEventListener("abort", () => el.removeAttribute("seekable"));
|
|
1076
|
+
el.addEventListener("swaralipi-seek", (e) => this.#onTap(e), { signal });
|
|
1077
|
+
el.addEventListener("keydown", (e) => this.#onKey(e), { signal });
|
|
1078
|
+
}
|
|
1079
|
+
#onTap(e) {
|
|
1080
|
+
if (e.defaultPrevented) return;
|
|
1081
|
+
if (e.detail.extend) this.#extendLoop(e.detail.path);
|
|
1082
|
+
else void this.play(e.detail.path);
|
|
1083
|
+
}
|
|
1084
|
+
#onKey(e) {
|
|
1085
|
+
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
|
1086
|
+
const shift = e.shiftKey;
|
|
1087
|
+
const act = {
|
|
1088
|
+
Space: () => this.#state === "playing" ? this.pause() : void this.play(),
|
|
1089
|
+
ArrowUp: () => shift ? this.#extendBy(-1) : this.stepLine(-1),
|
|
1090
|
+
ArrowDown: () => shift ? this.#extendBy(1) : this.stepLine(1),
|
|
1091
|
+
ArrowLeft: () => this.stepNote(-1),
|
|
1092
|
+
ArrowRight: () => this.stepNote(1),
|
|
1093
|
+
Home: () => this.stepLine(-Infinity),
|
|
1094
|
+
End: () => this.stepLine(Infinity),
|
|
1095
|
+
KeyR: () => this.setLoop(this.#loop === null ? void 0 : null),
|
|
1096
|
+
KeyT: () => this.setEcho(!this.#echo),
|
|
1097
|
+
Escape: this.#loop === null ? void 0 : () => this.setLoop(null)
|
|
1098
|
+
};
|
|
1099
|
+
const run = act[e.code];
|
|
1100
|
+
if (run === void 0) return;
|
|
1101
|
+
e.preventDefault();
|
|
1102
|
+
run();
|
|
1103
|
+
}
|
|
1104
|
+
/** Grow the repeat (or start one from the current line) to take in `path`'s line. */
|
|
1105
|
+
#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);
|
|
1112
|
+
}
|
|
1113
|
+
/** Shift+↑/↓: one more line above / below the repeat. */
|
|
1114
|
+
#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);
|
|
860
1121
|
}
|
|
861
1122
|
/**
|
|
862
1123
|
* Check whether an instrument's samples are reachable (its CDN is up / the
|
|
@@ -885,6 +1146,8 @@ var SwaralipiPlayer = class {
|
|
|
885
1146
|
/** Release all audio resources. The instance is unusable afterward. */
|
|
886
1147
|
dispose() {
|
|
887
1148
|
this.#halt();
|
|
1149
|
+
this.#unbind?.abort();
|
|
1150
|
+
this.#tone?.getTransport().off("loop", this.#onPass);
|
|
888
1151
|
for (const { sampler } of this.#samplers.values()) sampler.dispose();
|
|
889
1152
|
this.#samplers.clear();
|
|
890
1153
|
for (const { buffers } of this.#glide.values()) buffers.dispose();
|
|
@@ -905,9 +1168,110 @@ var SwaralipiPlayer = class {
|
|
|
905
1168
|
this.#state = state;
|
|
906
1169
|
this.#opts.onState?.(state);
|
|
907
1170
|
}
|
|
908
|
-
#emitHighlight(path) {
|
|
909
|
-
this.#opts.onHighlight?.(path);
|
|
910
|
-
}
|
|
1171
|
+
#emitHighlight(path, yourTurn = false) {
|
|
1172
|
+
this.#opts.onHighlight?.(path, yourTurn);
|
|
1173
|
+
}
|
|
1174
|
+
#emitLoop() {
|
|
1175
|
+
this.#opts.onLoop?.({ loop: this.loop, echo: this.#echo, yourTurn: this.#turn });
|
|
1176
|
+
this.#sheet?.markLoop?.(this.#loop, this.#turn);
|
|
1177
|
+
}
|
|
1178
|
+
/** A new document starts with nothing repeated, from the top. */
|
|
1179
|
+
#resetPractice() {
|
|
1180
|
+
this.#cursor = this.#startAt = this.#loop = null;
|
|
1181
|
+
this.#echo = this.#turn = false;
|
|
1182
|
+
this.#listened = 0;
|
|
1183
|
+
this.#emitLoop();
|
|
1184
|
+
}
|
|
1185
|
+
/** The timeline to navigate: the arranged one while playing, else built
|
|
1186
|
+
* fresh — it is pure and cheap, and a stopped player may have a new tonic
|
|
1187
|
+
* or tempo since. */
|
|
1188
|
+
#plan() {
|
|
1189
|
+
if (this.#timeline !== void 0) return this.#timeline;
|
|
1190
|
+
if (this.#doc === void 0) return void 0;
|
|
1191
|
+
const schedule = buildSchedule(this.#doc, this.#scheduleOpts());
|
|
1192
|
+
return { schedule, lines: scheduleLines(schedule) };
|
|
1193
|
+
}
|
|
1194
|
+
#lines() {
|
|
1195
|
+
return this.#plan()?.lines ?? [];
|
|
1196
|
+
}
|
|
1197
|
+
/** 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)];
|
|
1200
|
+
}
|
|
1201
|
+
/** The repeat's lines on the arranged timeline, cached for the per-note
|
|
1202
|
+
* checks (#hears), which run on every note. */
|
|
1203
|
+
#loopLines = [0, -1];
|
|
1204
|
+
/** Whether line `k` may sound: any line, unless a repeat holds others. */
|
|
1205
|
+
#hears(k) {
|
|
1206
|
+
return this.#loop === null || k >= this.#loopLines[0] && k <= this.#loopLines[1];
|
|
1207
|
+
}
|
|
1208
|
+
/** Transport seconds of event `i`, pulled back by its কণ so the grace is
|
|
1209
|
+
* heard too. */
|
|
1210
|
+
#eventSec(i) {
|
|
1211
|
+
const { schedule, spb, shift } = this.#timeline;
|
|
1212
|
+
const ev = schedule.events[i];
|
|
1213
|
+
return Math.max((ev.beat - GRACE_STEP * ev.kan.length) * spb + shift, 0);
|
|
1214
|
+
}
|
|
1215
|
+
/** The repeat in transport seconds: from its first note's own start (its
|
|
1216
|
+
* কণ included) for exactly the lines' length, so every pass keeps the
|
|
1217
|
+
* tabla's phase. */
|
|
1218
|
+
#loopSec() {
|
|
1219
|
+
const { lines, spb } = this.#timeline;
|
|
1220
|
+
const [a, b] = this.#loopLines;
|
|
1221
|
+
const start = this.#eventSec(lines[a].first);
|
|
1222
|
+
return [start, start + (lines[b].endBeat - lines[a].beat) * spb];
|
|
1223
|
+
}
|
|
1224
|
+
/** Put the repeat on the transport, or take it off. */
|
|
1225
|
+
#applyLoop() {
|
|
1226
|
+
const transport = this.#tone?.getTransport();
|
|
1227
|
+
if (transport === void 0 || this.#timeline === void 0) return;
|
|
1228
|
+
this.#loopLines = this.#loopSpan(this.#timeline.lines);
|
|
1229
|
+
if (this.#loop === null) {
|
|
1230
|
+
transport.loop = false;
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
transport.setLoopPoints(...this.#loopSec());
|
|
1234
|
+
transport.loop = true;
|
|
1235
|
+
}
|
|
1236
|
+
/** Where a fresh play() starts: a seek made while stopped, else the repeat,
|
|
1237
|
+
* else the top. */
|
|
1238
|
+
#startSec() {
|
|
1239
|
+
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];
|
|
1242
|
+
}
|
|
1243
|
+
/** Move the transport; notes still ringing from the old place are let go. */
|
|
1244
|
+
#jump(sec) {
|
|
1245
|
+
const Tone = this.#tone;
|
|
1246
|
+
const transport = Tone.getTransport();
|
|
1247
|
+
this.#fence = Tone.now();
|
|
1248
|
+
if (transport.loop) {
|
|
1249
|
+
transport.loopEnd = this.#loopSec()[1] + 3600;
|
|
1250
|
+
Tone.getContext().setTimeout(() => this.#applyLoop(), 0.15);
|
|
1251
|
+
}
|
|
1252
|
+
transport.seconds = sec - this.#preroll();
|
|
1253
|
+
this.#voice?.releaseAll();
|
|
1254
|
+
}
|
|
1255
|
+
/** Audio time before which a note callback belongs to the timeline a seek
|
|
1256
|
+
* just left (see #jump). */
|
|
1257
|
+
#fence = 0;
|
|
1258
|
+
/** START_PREROLL_SEC in whole transport ticks: an offset between ticks
|
|
1259
|
+
* would shift every later repeat wrap by the remainder — measured, a
|
|
1260
|
+
* 0.8 ms splice in the tabla at each pass. */
|
|
1261
|
+
#preroll() {
|
|
1262
|
+
const transport = this.#tone.getTransport();
|
|
1263
|
+
const perSec = transport.bpm.value / 60 * transport.PPQ;
|
|
1264
|
+
return Math.ceil(START_PREROLL_SEC * perSec) / perSec;
|
|
1265
|
+
}
|
|
1266
|
+
/** The melody voice of the arrangement, for #jump. */
|
|
1267
|
+
#voice;
|
|
1268
|
+
/** The repeat wrapped. With sing-back on, the turn passes between the
|
|
1269
|
+
* player and the learner; the sheet is told at the wrap itself. */
|
|
1270
|
+
#onPass = (time) => {
|
|
1271
|
+
if (this.#loop === null || !this.#echo) return;
|
|
1272
|
+
this.#turn = !this.#turn;
|
|
1273
|
+
this.#tone?.getDraw().schedule(() => this.#emitLoop(), time);
|
|
1274
|
+
};
|
|
911
1275
|
/** The taal one accompaniment segment plays: taal mode's, an explicit
|
|
912
1276
|
* setTaal override (pinned across the whole song, তালফেরতা included), else
|
|
913
1277
|
* the segment's own. `null` = a মুক্তছন্দ passage. */
|
|
@@ -977,11 +1341,13 @@ var SwaralipiPlayer = class {
|
|
|
977
1341
|
return this.#tone;
|
|
978
1342
|
}
|
|
979
1343
|
/** Hard stop → rebuild → restart, only while playing; otherwise just store.
|
|
980
|
-
* Uses #halt (not the stop() fade) so a setting change restarts instantly
|
|
1344
|
+
* Uses #halt (not the stop() fade) so a setting change restarts instantly,
|
|
1345
|
+
* from the note it was on — slowing down a repeated line keeps repeating it. */
|
|
981
1346
|
#reapply() {
|
|
982
1347
|
if (this.#state !== "playing") return;
|
|
1348
|
+
const at = this.#cursor;
|
|
983
1349
|
this.#halt();
|
|
984
|
-
void this.play();
|
|
1350
|
+
void this.play(at ?? void 0);
|
|
985
1351
|
}
|
|
986
1352
|
/** The master bus everything routes through (created once, → destination). */
|
|
987
1353
|
#ensureMaster(Tone) {
|
|
@@ -997,6 +1363,7 @@ var SwaralipiPlayer = class {
|
|
|
997
1363
|
const transport = Tone.getTransport();
|
|
998
1364
|
transport.cancel(0);
|
|
999
1365
|
transport.position = 0;
|
|
1366
|
+
transport.off("loop", this.#onPass).on("loop", this.#onPass);
|
|
1000
1367
|
this.#scheduleIds = [];
|
|
1001
1368
|
this.#ensureMaster(Tone);
|
|
1002
1369
|
this.#restoreMasterGain();
|
|
@@ -1004,24 +1371,34 @@ var SwaralipiPlayer = class {
|
|
|
1004
1371
|
transport.bpm.value = bpm;
|
|
1005
1372
|
const spb = 60 / bpm;
|
|
1006
1373
|
const schedule = buildSchedule(this.#doc, this.#scheduleOpts());
|
|
1374
|
+
const lines = scheduleLines(schedule);
|
|
1007
1375
|
const range = midiRange(schedule);
|
|
1008
1376
|
const id = this.#effectiveInstrument();
|
|
1009
1377
|
const def = this.#resolveDef(id) ?? INSTRUMENTS.harmonium;
|
|
1010
1378
|
const melody = this.#buildMelody(Tone, id, base, range);
|
|
1379
|
+
this.#voice = melody;
|
|
1011
1380
|
const glide = schedule.events.some((ev) => ev.glide === true) ? this.#buildGlide(Tone, id, base, range) : void 0;
|
|
1012
1381
|
const shift = this.#opts.rhythm !== "none" ? schedule.pickupBeats * spb : 0;
|
|
1382
|
+
this.#timeline = { schedule, lines, spb, shift };
|
|
1013
1383
|
this.#buildRhythm(Tone, transport, base, spb, schedule.tempo, shift);
|
|
1014
1384
|
this.#buildTanpura(Tone, transport, base, spb, schedule.tempo, shift);
|
|
1015
1385
|
await loadedOrWarn(Tone);
|
|
1016
1386
|
this.#quantizeTablaLoops();
|
|
1017
1387
|
const draw = Tone.getDraw();
|
|
1018
1388
|
const mediaLead = Math.min(MEDIA_LEAD_SEC, spb);
|
|
1389
|
+
let line = 0;
|
|
1019
1390
|
schedule.events.forEach((ev, i) => {
|
|
1391
|
+
while (lines[line + 1] !== void 0 && lines[line + 1].first <= i) line++;
|
|
1392
|
+
const k = line;
|
|
1393
|
+
const heard = (time) => time >= this.#fence && this.#hears(k);
|
|
1394
|
+
const sounds = (time) => heard(time) && !this.#turn;
|
|
1020
1395
|
const at = ev.beat * spb + shift;
|
|
1021
1396
|
if (ev.glide !== true) {
|
|
1022
1397
|
for (const g of graceTouches(ev)) {
|
|
1023
1398
|
const graceName = midiToSampleName(g.midi);
|
|
1024
|
-
transport.
|
|
1399
|
+
transport.schedule((time) => {
|
|
1400
|
+
if (sounds(time)) melody.triggerAttackRelease(graceName, spb * 0.4, time);
|
|
1401
|
+
}, Math.max(g.beat * spb + shift, 0));
|
|
1025
1402
|
}
|
|
1026
1403
|
}
|
|
1027
1404
|
if (ev.midi !== null && ev.glide !== true) {
|
|
@@ -1035,7 +1412,8 @@ var SwaralipiPlayer = class {
|
|
|
1035
1412
|
...targets.map((t) => ({ midi: t.midi, at: (t.beat - ev.beat) * spb }))
|
|
1036
1413
|
];
|
|
1037
1414
|
const end = (last.beat + last.durationBeats - ev.beat) * spb;
|
|
1038
|
-
transport.
|
|
1415
|
+
transport.schedule((time) => {
|
|
1416
|
+
if (!sounds(time)) return;
|
|
1039
1417
|
triggerGlide(
|
|
1040
1418
|
Tone,
|
|
1041
1419
|
glide.buffers,
|
|
@@ -1047,14 +1425,24 @@ var SwaralipiPlayer = class {
|
|
|
1047
1425
|
);
|
|
1048
1426
|
}, at);
|
|
1049
1427
|
} else {
|
|
1050
|
-
transport.
|
|
1428
|
+
transport.schedule((time) => {
|
|
1429
|
+
if (sounds(time)) melody.triggerAttackRelease(name, dur, time);
|
|
1430
|
+
}, at);
|
|
1051
1431
|
}
|
|
1052
1432
|
}
|
|
1053
1433
|
if (this.#opts.onEvent !== void 0) {
|
|
1054
|
-
transport.
|
|
1434
|
+
transport.schedule((time) => {
|
|
1435
|
+
if (heard(time)) this.#opts.onEvent?.(ev.path);
|
|
1436
|
+
}, Math.max(at - mediaLead, 0));
|
|
1055
1437
|
}
|
|
1056
|
-
transport.
|
|
1438
|
+
transport.schedule((time) => {
|
|
1439
|
+
if (!heard(time)) return;
|
|
1440
|
+
const turn = this.#turn;
|
|
1441
|
+
this.#cursor = ev.path;
|
|
1442
|
+
draw.schedule(() => this.#emitHighlight(ev.path, turn), time);
|
|
1443
|
+
}, at);
|
|
1057
1444
|
});
|
|
1445
|
+
this.#applyLoop();
|
|
1058
1446
|
const endAt = playDurationSec(schedule, spb, this.#opts.rhythm);
|
|
1059
1447
|
transport.scheduleOnce((time) => {
|
|
1060
1448
|
this.#opts.onEvent?.(null);
|
|
@@ -1146,13 +1534,15 @@ var SwaralipiPlayer = class {
|
|
|
1146
1534
|
const to = i === segments.length - 1 ? Infinity : seg.endBeat * spb + shift;
|
|
1147
1535
|
const beatSec = spb / seg.scale;
|
|
1148
1536
|
const span = to === Infinity ? void 0 : to - from;
|
|
1537
|
+
const beatAt = (time) => Math.round((transport.getSecondsAtTime(time) - from) / beatSec);
|
|
1149
1538
|
if (rhythm === "tabla" && taalId !== void 0 && SAMPLED_TAALS.has(taalId) && base !== void 0) {
|
|
1150
1539
|
const bpm = 60 / beatSec;
|
|
1151
1540
|
const fileBpm = nearestTablaBpm(bpm, tablaBpms(taalId));
|
|
1152
1541
|
const rate = bpm / fileBpm;
|
|
1153
1542
|
const semis = tablaShiftSemitones(this.#tonicSemitone());
|
|
1154
1543
|
const url = tablaUrl(base, taalId, fileBpm, this.#variation(own));
|
|
1155
|
-
|
|
1544
|
+
this.#grain ??= seekableGrain(Tone);
|
|
1545
|
+
const loop = (rate === 1 && semis === 0 ? new Tone.Player({ url, loop: true }) : new this.#grain({
|
|
1156
1546
|
url,
|
|
1157
1547
|
loop: true,
|
|
1158
1548
|
playbackRate: rate,
|
|
@@ -1163,9 +1553,10 @@ var SwaralipiPlayer = class {
|
|
|
1163
1553
|
loop.sync().start(from);
|
|
1164
1554
|
if (span !== void 0) {
|
|
1165
1555
|
this.#scheduleIds.push(
|
|
1166
|
-
transport.
|
|
1556
|
+
transport.schedule((t) => {
|
|
1167
1557
|
loop.volume.setValueAtTime(0, t);
|
|
1168
1558
|
loop.volume.linearRampToValueAtTime(-40, t + LAY_TAIL_SEC);
|
|
1559
|
+
loop.volume.setValueAtTime(0, t + LAY_TAIL_SEC + 0.05);
|
|
1169
1560
|
}, to)
|
|
1170
1561
|
);
|
|
1171
1562
|
loop.stop(to + LAY_TAIL_SEC);
|
|
@@ -1177,13 +1568,11 @@ var SwaralipiPlayer = class {
|
|
|
1177
1568
|
this.#drums ??= createDrumKit(Tone).connect(master);
|
|
1178
1569
|
const drums = this.#drums;
|
|
1179
1570
|
const pattern = drumPattern(this.#taal ?? (taalId !== void 0 ? TAALS[taalId] : own) ?? { numBeats: 4, sam: 0 });
|
|
1180
|
-
let beat2 = 0;
|
|
1181
1571
|
this.#scheduleIds.push(
|
|
1182
1572
|
transport.scheduleRepeat(
|
|
1183
1573
|
(time) => {
|
|
1184
|
-
const voice = pattern[
|
|
1574
|
+
const voice = pattern[beatAt(time) % pattern.length];
|
|
1185
1575
|
if (drums.has(voice)) drums.player(voice).start(time);
|
|
1186
|
-
beat2 += 1;
|
|
1187
1576
|
},
|
|
1188
1577
|
beatSec,
|
|
1189
1578
|
from,
|
|
@@ -1195,13 +1584,11 @@ var SwaralipiPlayer = class {
|
|
|
1195
1584
|
this.#metronome ??= createMetronome(Tone, base).connect(master);
|
|
1196
1585
|
const metro = this.#metronome;
|
|
1197
1586
|
const cycle = (this.#taal ?? seg.taal)?.numBeats ?? 4;
|
|
1198
|
-
let beat = 0;
|
|
1199
1587
|
this.#scheduleIds.push(
|
|
1200
1588
|
transport.scheduleRepeat(
|
|
1201
1589
|
(time) => {
|
|
1202
|
-
metro.volume.setValueAtTime(
|
|
1590
|
+
metro.volume.setValueAtTime(beatAt(time) % cycle === 0 ? 0 : -8, time);
|
|
1203
1591
|
metro.start(time);
|
|
1204
|
-
beat += 1;
|
|
1205
1592
|
},
|
|
1206
1593
|
beatSec,
|
|
1207
1594
|
from,
|
|
@@ -1227,15 +1614,14 @@ var SwaralipiPlayer = class {
|
|
|
1227
1614
|
segments.forEach((seg, k) => {
|
|
1228
1615
|
const from = k === 0 ? 0 : seg.beat * spb + shift;
|
|
1229
1616
|
const to = k === segments.length - 1 ? Infinity : seg.endBeat * spb + shift;
|
|
1230
|
-
|
|
1617
|
+
const every = spb * 2 / seg.scale;
|
|
1231
1618
|
this.#scheduleIds.push(
|
|
1232
1619
|
transport.scheduleRepeat(
|
|
1233
1620
|
(time) => {
|
|
1234
|
-
const stem = pattern[
|
|
1621
|
+
const stem = pattern[Math.round((transport.getSecondsAtTime(time) - from) / every) % pattern.length];
|
|
1235
1622
|
if (players.has(stem)) players.player(stem).start(time);
|
|
1236
|
-
i += 1;
|
|
1237
1623
|
},
|
|
1238
|
-
|
|
1624
|
+
every,
|
|
1239
1625
|
from,
|
|
1240
1626
|
to === Infinity ? void 0 : to - from
|
|
1241
1627
|
)
|
|
@@ -1700,6 +2086,7 @@ exports.drumPattern = drumPattern;
|
|
|
1700
2086
|
exports.encodeMidi = encodeMidi;
|
|
1701
2087
|
exports.encodeMp3 = encodeMp3;
|
|
1702
2088
|
exports.encodeWav = encodeWav;
|
|
2089
|
+
exports.eventAt = eventAt;
|
|
1703
2090
|
exports.exportAudio = exportAudio;
|
|
1704
2091
|
exports.exportMidi = exportMidi;
|
|
1705
2092
|
exports.glideTargets = glideTargets;
|
|
@@ -1720,6 +2107,7 @@ exports.rowLay = rowLay;
|
|
|
1720
2107
|
exports.sCurve = sCurve;
|
|
1721
2108
|
exports.sampleFileName = sampleFileName;
|
|
1722
2109
|
exports.santoorUrls = santoorUrls;
|
|
2110
|
+
exports.scheduleLines = scheduleLines;
|
|
1723
2111
|
exports.subsetUrls = subsetUrls;
|
|
1724
2112
|
exports.svaraSemitone = svaraSemitone;
|
|
1725
2113
|
exports.svaraToMidi = svaraToMidi;
|