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