@ugfoundation/swaralipi-js 0.7.7 → 0.8.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/dist/{batch-6MCPHHKF.js → batch-NJMT3M5Z.js} +5 -5
- package/dist/{batch-6MCPHHKF.js.map → batch-NJMT3M5Z.js.map} +1 -1
- package/dist/{chunk-ALN4SWMS.js → chunk-4HMHTXDE.js} +15 -4
- package/dist/chunk-4HMHTXDE.js.map +1 -0
- package/dist/{chunk-QM4CXKW2.js → chunk-4HNT6OAT.js} +24 -3
- package/dist/chunk-4HNT6OAT.js.map +1 -0
- package/dist/{chunk-TKTQCUKD.js → chunk-4IVUPMRK.js} +27 -9
- package/dist/chunk-4IVUPMRK.js.map +1 -0
- package/dist/{chunk-QQC6WVLJ.js → chunk-YFVAVICF.js} +4 -4
- package/dist/{chunk-QQC6WVLJ.js.map → chunk-YFVAVICF.js.map} +1 -1
- package/dist/cli.cjs +13 -2
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +3 -3
- package/dist/convert/index.cjs +12 -1
- package/dist/convert/index.cjs.map +1 -1
- package/dist/convert/index.d.cts +1 -1
- package/dist/convert/index.d.ts +1 -1
- package/dist/convert/index.js +3 -3
- package/dist/index.cjs +39 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/{taals-CjzBuZ35.d.ts → lay-Csa4CD46.d.cts} +38 -1
- package/dist/{taals-CjzBuZ35.d.cts → lay-Ct9ky_EE.d.ts} +38 -1
- package/dist/player/index.cjs +276 -117
- package/dist/player/index.cjs.map +1 -1
- package/dist/player/index.d.cts +22 -9
- package/dist/player/index.d.ts +22 -9
- package/dist/player/index.js +230 -114
- package/dist/player/index.js.map +1 -1
- package/dist/{schedule-BpBYGXPL.d.cts → schedule-B_UaluBi.d.cts} +27 -3
- package/dist/{schedule-Tzk01FKj.d.ts → schedule-CdILTNsk.d.ts} +27 -3
- package/dist/{schema-DqBXPGkL.d.cts → schema-DGA_bNVZ.d.cts} +97 -1
- package/dist/{schema-DqBXPGkL.d.ts → schema-DGA_bNVZ.d.ts} +97 -1
- package/dist/spec/index.cjs +39 -1
- package/dist/spec/index.cjs.map +1 -1
- package/dist/spec/index.d.cts +16 -4
- package/dist/spec/index.d.ts +16 -4
- package/dist/spec/index.js +2 -2
- package/dist/viewer/index.cjs +114 -12
- package/dist/viewer/index.cjs.map +1 -1
- package/dist/viewer/index.d.cts +10 -5
- package/dist/viewer/index.d.ts +10 -5
- package/dist/viewer/index.js +43 -7
- package/dist/viewer/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-ALN4SWMS.js.map +0 -1
- package/dist/chunk-QM4CXKW2.js.map +0 -1
- package/dist/chunk-TKTQCUKD.js.map +0 -1
package/dist/player/index.cjs
CHANGED
|
@@ -115,6 +115,27 @@ function sCurve(x, steepness = 8) {
|
|
|
115
115
|
return (logistic(t) - lo) / (logistic(1) - lo);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
// src/spec/lay.ts
|
|
119
|
+
var LAYS = ["drut", "madhya", "vilambit"];
|
|
120
|
+
var LAY_SPEEDS = {
|
|
121
|
+
drut: 1,
|
|
122
|
+
madhya: 0.75,
|
|
123
|
+
vilambit: 0.5
|
|
124
|
+
};
|
|
125
|
+
function resolveLaySpeeds(doc, opt) {
|
|
126
|
+
return { ...LAY_SPEEDS, ...doc, ...opt };
|
|
127
|
+
}
|
|
128
|
+
var PRINTED = { \u09A6\u09CD\u09B0\u09C1\u09A4: "drut", \u09AE\u09A7\u09CD\u09AF: "madhya", \u09AC\u09BF\u09B2\u09AE\u09CD\u09AC\u09BF\u09A4: "vilambit" };
|
|
129
|
+
function printedLay(notes) {
|
|
130
|
+
const first = notes[0];
|
|
131
|
+
if (first === void 0 || first.lyric === void 0) return void 0;
|
|
132
|
+
for (const k of Object.keys(first)) if (k !== "lyric") return void 0;
|
|
133
|
+
return PRINTED[first.lyric.normalize("NFC")];
|
|
134
|
+
}
|
|
135
|
+
function rowLay(row) {
|
|
136
|
+
return row.lay ?? printedLay(row.notes);
|
|
137
|
+
}
|
|
138
|
+
|
|
118
139
|
// src/player/schedule.ts
|
|
119
140
|
var UNDECODED_CELL = /[srgmpqnvtduka]/i;
|
|
120
141
|
var isSilent = (n) => n.svara === void 0 && n.rest !== true && n.sustain !== true && !(n.raw !== void 0 && UNDECODED_CELL.test(n.raw));
|
|
@@ -159,22 +180,38 @@ function buildSchedule(doc, opts = {}) {
|
|
|
159
180
|
const events = [];
|
|
160
181
|
let cursor = 0;
|
|
161
182
|
let lastPitched = -1;
|
|
162
|
-
let
|
|
183
|
+
let samMatra;
|
|
184
|
+
let matra = 0;
|
|
185
|
+
const speeds = resolveLaySpeeds(doc.meta.laySpeeds, opts.laySpeeds);
|
|
186
|
+
const withLay = opts.lay !== false;
|
|
187
|
+
const tempo = [{ beat: 0, endBeat: 0, scale: 1, lay: "drut" }];
|
|
188
|
+
let scale = 1;
|
|
163
189
|
doc.parts.forEach((part, p) => {
|
|
164
190
|
part.rows.forEach((row, r) => {
|
|
191
|
+
const lay = withLay ? rowLay(row) : void 0;
|
|
192
|
+
if (lay !== void 0 && speeds[lay] !== scale) {
|
|
193
|
+
scale = speeds[lay];
|
|
194
|
+
const last = tempo[tempo.length - 1];
|
|
195
|
+
if (last.beat === cursor) tempo[tempo.length - 1] = { beat: cursor, endBeat: cursor, scale, lay };
|
|
196
|
+
else {
|
|
197
|
+
last.endBeat = cursor;
|
|
198
|
+
tempo.push({ beat: cursor, endBeat: cursor, scale, lay });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
165
201
|
const glides = meendContinuations(row.notes);
|
|
166
202
|
row.notes.forEach((note, i) => {
|
|
167
|
-
if (
|
|
203
|
+
if (samMatra === void 0 && note.above !== void 0) {
|
|
168
204
|
for (const a of note.above) {
|
|
169
205
|
if (a.raw !== void 0 && SAM_TICK.test(a.raw)) {
|
|
170
|
-
|
|
206
|
+
samMatra = matra;
|
|
171
207
|
break;
|
|
172
208
|
}
|
|
173
209
|
}
|
|
174
210
|
}
|
|
175
211
|
if (note.bar !== void 0 || isSilent(note)) return;
|
|
176
212
|
const path = `${p}.${r}.${i}`;
|
|
177
|
-
const
|
|
213
|
+
const written = note.durationMatra ?? 1;
|
|
214
|
+
const dur = written / scale;
|
|
178
215
|
if (note.svara !== void 0) {
|
|
179
216
|
events.push({
|
|
180
217
|
beat: cursor,
|
|
@@ -193,12 +230,14 @@ function buildSchedule(doc, opts = {}) {
|
|
|
193
230
|
events.push({ beat: cursor, durationBeats: dur, midi: null, kan: [], path, kind: "rest" });
|
|
194
231
|
}
|
|
195
232
|
cursor += dur;
|
|
233
|
+
matra += written;
|
|
196
234
|
});
|
|
197
235
|
});
|
|
198
236
|
});
|
|
199
237
|
const numBeats = doc.meta.taal?.numBeats;
|
|
200
|
-
const
|
|
201
|
-
|
|
238
|
+
const pickupMatra = samMatra !== void 0 && numBeats !== void 0 && numBeats > 0 ? (numBeats - samMatra % numBeats) % numBeats : 0;
|
|
239
|
+
tempo[tempo.length - 1].endBeat = cursor;
|
|
240
|
+
return { events, totalBeats: cursor, pickupBeats: pickupMatra / tempo[0].scale, tempo };
|
|
202
241
|
}
|
|
203
242
|
|
|
204
243
|
// src/player/instruments.ts
|
|
@@ -497,6 +536,10 @@ function createTanpura(Tone, base, stems = TANPURA_PCS) {
|
|
|
497
536
|
|
|
498
537
|
// src/player/player.ts
|
|
499
538
|
var STOP_FADE_SEC = 0.5;
|
|
539
|
+
var LAY_TAIL_SEC = 0.12;
|
|
540
|
+
var TAAL_MODE_TEMPO = [
|
|
541
|
+
{ beat: 0, endBeat: Infinity, scale: 1, lay: "drut" }
|
|
542
|
+
];
|
|
500
543
|
var MEDIA_LEAD_SEC = 0.25;
|
|
501
544
|
function midiRange(schedule) {
|
|
502
545
|
let lo = Infinity;
|
|
@@ -541,8 +584,11 @@ var SwaralipiPlayer = class {
|
|
|
541
584
|
#tanpura;
|
|
542
585
|
#tanpuraKey = "";
|
|
543
586
|
#metronome;
|
|
544
|
-
/** The looping tabla —
|
|
545
|
-
|
|
587
|
+
/** The looping tabla — one loop per লয় segment, each a `GrainPlayer` when
|
|
588
|
+
* the tonic needs retuning or the segment's tempo needs stretching.
|
|
589
|
+
* `fileSpb` is the recorded loop's OWN beat length, which is the unit
|
|
590
|
+
* `quantizeLoopEnd` must snap in (loop points are buffer time). */
|
|
591
|
+
#tabla = [];
|
|
546
592
|
#drums;
|
|
547
593
|
#scheduleIds = [];
|
|
548
594
|
#fadeTimer;
|
|
@@ -553,6 +599,7 @@ var SwaralipiPlayer = class {
|
|
|
553
599
|
customInstruments: options.customInstruments,
|
|
554
600
|
tonic: options.tonic,
|
|
555
601
|
bpm: options.bpm,
|
|
602
|
+
laySpeeds: options.laySpeeds,
|
|
556
603
|
baseOctave: options.baseOctave ?? 4,
|
|
557
604
|
instrument: options.instrument ?? "piano",
|
|
558
605
|
instruments: options.instruments ?? Object.keys(INSTRUMENTS),
|
|
@@ -606,8 +653,7 @@ var SwaralipiPlayer = class {
|
|
|
606
653
|
get durationSec() {
|
|
607
654
|
if (this.#taal !== void 0) return Infinity;
|
|
608
655
|
if (this.#doc === void 0) return 0;
|
|
609
|
-
const
|
|
610
|
-
const full = playDurationSec(schedule, 60 / this.#effectiveBpm(), this.#opts.rhythm);
|
|
656
|
+
const full = playDurationSec(buildSchedule(this.#doc, this.#scheduleOpts()), 60 / this.#effectiveBpm(), this.#opts.rhythm);
|
|
611
657
|
return Math.min(full, this.#opts.previewLimitSec);
|
|
612
658
|
}
|
|
613
659
|
/** Seconds played so far, off the Transport's own clock — so it holds while
|
|
@@ -711,8 +757,7 @@ var SwaralipiPlayer = class {
|
|
|
711
757
|
transport.cancel(0);
|
|
712
758
|
transport.position = 0;
|
|
713
759
|
}
|
|
714
|
-
this.#
|
|
715
|
-
this.#tabla = void 0;
|
|
760
|
+
this.#disposeTabla();
|
|
716
761
|
this.#restoreMasterGain();
|
|
717
762
|
this.#scheduleIds = [];
|
|
718
763
|
this.#emitHighlight(null);
|
|
@@ -730,6 +775,17 @@ var SwaralipiPlayer = class {
|
|
|
730
775
|
this.#cancelFadeTimer();
|
|
731
776
|
this.#restoreMasterGain();
|
|
732
777
|
}
|
|
778
|
+
#disposeTabla() {
|
|
779
|
+
for (const { loop } of this.#tabla) loop.dispose();
|
|
780
|
+
this.#tabla = [];
|
|
781
|
+
}
|
|
782
|
+
/** Snap each loop to a whole number of ITS OWN beats now the buffer length is
|
|
783
|
+
* known, so a mistrimmed file phase-locks instead of drifting. */
|
|
784
|
+
#quantizeTablaLoops() {
|
|
785
|
+
for (const { loop, fileSpb } of this.#tabla) {
|
|
786
|
+
if (loop.buffer?.loaded) loop.loopEnd = quantizeLoopEnd(loop.buffer.duration, fileSpb);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
733
789
|
#restoreMasterGain() {
|
|
734
790
|
if (this.#master !== void 0 && this.#tone !== void 0) {
|
|
735
791
|
this.#master.gain.cancelScheduledValues(this.#tone.now());
|
|
@@ -815,10 +871,10 @@ var SwaralipiPlayer = class {
|
|
|
815
871
|
this.#glide.clear();
|
|
816
872
|
this.#tanpura?.dispose();
|
|
817
873
|
this.#metronome?.dispose();
|
|
818
|
-
this.#
|
|
874
|
+
this.#disposeTabla();
|
|
819
875
|
this.#drums?.dispose();
|
|
820
876
|
this.#master?.dispose();
|
|
821
|
-
this.#tanpura = this.#metronome = this.#
|
|
877
|
+
this.#tanpura = this.#metronome = this.#drums = this.#master = void 0;
|
|
822
878
|
}
|
|
823
879
|
/* ----------------------------------------------------------------- */
|
|
824
880
|
/* Internals */
|
|
@@ -873,6 +929,13 @@ var SwaralipiPlayer = class {
|
|
|
873
929
|
if (def.local === true && !this.#opts.soundsBaseUrl) return this.#fallbackInstrument();
|
|
874
930
|
return id;
|
|
875
931
|
}
|
|
932
|
+
#scheduleOpts() {
|
|
933
|
+
return {
|
|
934
|
+
tonic: this.#opts.tonic,
|
|
935
|
+
baseOctave: this.#opts.baseOctave,
|
|
936
|
+
laySpeeds: this.#opts.laySpeeds
|
|
937
|
+
};
|
|
938
|
+
}
|
|
876
939
|
/** Effective tempo: snapped to the taal's available tabla tempos while a
|
|
877
940
|
* tabla actually plays (so it never snaps to a bpm that taal lacks). */
|
|
878
941
|
#effectiveBpm() {
|
|
@@ -910,23 +973,18 @@ var SwaralipiPlayer = class {
|
|
|
910
973
|
const bpm = this.#effectiveBpm();
|
|
911
974
|
transport.bpm.value = bpm;
|
|
912
975
|
const spb = 60 / bpm;
|
|
913
|
-
const schedule = buildSchedule(this.#doc,
|
|
914
|
-
tonic: this.#opts.tonic,
|
|
915
|
-
baseOctave: this.#opts.baseOctave
|
|
916
|
-
});
|
|
976
|
+
const schedule = buildSchedule(this.#doc, this.#scheduleOpts());
|
|
917
977
|
const range = midiRange(schedule);
|
|
918
978
|
const id = this.#effectiveInstrument();
|
|
919
979
|
const def = this.#resolveDef(id) ?? INSTRUMENTS.harmonium;
|
|
920
980
|
const melody = this.#buildMelody(Tone, id, base, range);
|
|
921
981
|
const glide = schedule.events.some((ev) => ev.glide === true) ? this.#buildGlide(Tone, id, base, range) : void 0;
|
|
922
|
-
this.#
|
|
923
|
-
this.#
|
|
982
|
+
const shift = this.#opts.rhythm !== "none" ? schedule.pickupBeats * spb : 0;
|
|
983
|
+
this.#buildRhythm(Tone, transport, base, spb, schedule.tempo, shift);
|
|
984
|
+
this.#buildTanpura(Tone, transport, base, spb, schedule.tempo, shift);
|
|
924
985
|
await loadedOrWarn(Tone);
|
|
925
|
-
|
|
926
|
-
this.#tabla.loopEnd = quantizeLoopEnd(this.#tabla.buffer.duration, spb);
|
|
927
|
-
}
|
|
986
|
+
this.#quantizeTablaLoops();
|
|
928
987
|
const draw = Tone.getDraw();
|
|
929
|
-
const shift = this.#opts.rhythm !== "none" ? schedule.pickupBeats * spb : 0;
|
|
930
988
|
const mediaLead = Math.min(MEDIA_LEAD_SEC, spb);
|
|
931
989
|
schedule.events.forEach((ev, i) => {
|
|
932
990
|
const at = ev.beat * spb + shift;
|
|
@@ -989,11 +1047,9 @@ var SwaralipiPlayer = class {
|
|
|
989
1047
|
const bpm = this.#effectiveBpm();
|
|
990
1048
|
transport.bpm.value = bpm;
|
|
991
1049
|
const spb = 60 / bpm;
|
|
992
|
-
this.#buildRhythm(Tone, transport, base, spb);
|
|
1050
|
+
this.#buildRhythm(Tone, transport, base, spb, TAAL_MODE_TEMPO, 0);
|
|
993
1051
|
await loadedOrWarn(Tone);
|
|
994
|
-
|
|
995
|
-
this.#tabla.loopEnd = quantizeLoopEnd(this.#tabla.buffer.duration, spb);
|
|
996
|
-
}
|
|
1052
|
+
this.#quantizeTablaLoops();
|
|
997
1053
|
const draw = Tone.getDraw();
|
|
998
1054
|
const n = Math.max(1, this.#taal.numBeats);
|
|
999
1055
|
let beat = 0;
|
|
@@ -1035,52 +1091,92 @@ var SwaralipiPlayer = class {
|
|
|
1035
1091
|
this.#glide.set(id, entry);
|
|
1036
1092
|
return entry;
|
|
1037
1093
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1094
|
+
/**
|
|
1095
|
+
* Accompaniment, one লয় segment at a time. A tempo change under the melody
|
|
1096
|
+
* is a real tempo change under the drum: the tabla loop is rebuilt at the
|
|
1097
|
+
* segment's own bpm — its own recorded file when the set has one at that
|
|
1098
|
+
* tempo, else the nearest file time-stretched by `GrainPlayer` (which
|
|
1099
|
+
* stretches *without* transposing, unlike a plain playbackRate) — and the
|
|
1100
|
+
* click / drum / drone patterns re-space their interval. A document with no
|
|
1101
|
+
* লয় marks has exactly one segment, so this is the old single-loop path.
|
|
1102
|
+
*/
|
|
1103
|
+
#buildRhythm(Tone, transport, base, spb, segments, shift) {
|
|
1104
|
+
this.#disposeTabla();
|
|
1041
1105
|
const master = this.#ensureMaster(Tone);
|
|
1042
1106
|
const taalId = this.#taalId();
|
|
1043
1107
|
const wantsTabla = this.#opts.rhythm === "tabla" && taalId !== void 0 && SAMPLED_TAALS.has(taalId) && base !== void 0;
|
|
1044
|
-
if (wantsTabla)
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
this.#tabla = (shift === 0 ? new Tone.Player({ url, loop: true }) : new Tone.GrainPlayer({
|
|
1048
|
-
url,
|
|
1049
|
-
loop: true,
|
|
1050
|
-
detune: shift * 100,
|
|
1051
|
-
grainSize: TABLA_GRAIN_SEC,
|
|
1052
|
-
overlap: TABLA_GRAIN_OVERLAP_SEC
|
|
1053
|
-
})).connect(master);
|
|
1054
|
-
this.#tabla.sync().start(0);
|
|
1055
|
-
return;
|
|
1056
|
-
}
|
|
1057
|
-
if (this.#opts.rhythm === "drums") {
|
|
1058
|
-
this.#drums ??= createDrumKit(Tone).connect(master);
|
|
1059
|
-
const drums = this.#drums;
|
|
1060
|
-
const taal = this.#taal ?? (taalId !== void 0 ? TAALS[taalId] : this.#doc?.meta.taal) ?? { numBeats: 4, sam: 0 };
|
|
1061
|
-
const pattern = drumPattern(taal);
|
|
1062
|
-
let beat2 = 0;
|
|
1063
|
-
const id2 = transport.scheduleRepeat((time) => {
|
|
1064
|
-
const voice = pattern[beat2 % pattern.length];
|
|
1065
|
-
if (drums.has(voice)) drums.player(voice).start(time);
|
|
1066
|
-
beat2 += 1;
|
|
1067
|
-
}, spb, 0);
|
|
1068
|
-
this.#scheduleIds.push(id2);
|
|
1069
|
-
return;
|
|
1070
|
-
}
|
|
1071
|
-
if (this.#opts.rhythm === "none" || base === void 0) return;
|
|
1072
|
-
this.#metronome ??= createMetronome(Tone, base).connect(master);
|
|
1073
|
-
const metro = this.#metronome;
|
|
1108
|
+
if (!wantsTabla && this.#opts.rhythm !== "drums" && (this.#opts.rhythm === "none" || base === void 0)) return;
|
|
1109
|
+
const taal = this.#taal ?? (taalId !== void 0 ? TAALS[taalId] : this.#doc?.meta.taal) ?? { numBeats: 4, sam: 0 };
|
|
1110
|
+
const pattern = this.#opts.rhythm === "drums" ? drumPattern(taal) : [];
|
|
1074
1111
|
const cycle = this.#taal?.numBeats ?? this.#doc?.meta.taal?.numBeats ?? 4;
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1112
|
+
segments.forEach((seg, i) => {
|
|
1113
|
+
const from = i === 0 ? 0 : seg.beat * spb + shift;
|
|
1114
|
+
const to = i === segments.length - 1 ? Infinity : seg.endBeat * spb + shift;
|
|
1115
|
+
const beatSec = spb / seg.scale;
|
|
1116
|
+
const span = to === Infinity ? void 0 : to - from;
|
|
1117
|
+
if (wantsTabla) {
|
|
1118
|
+
const bpm = 60 / beatSec;
|
|
1119
|
+
const fileBpm = nearestTablaBpm(bpm, tablaBpms(taalId));
|
|
1120
|
+
const rate = bpm / fileBpm;
|
|
1121
|
+
const semis = tablaShiftSemitones(this.#tonicSemitone());
|
|
1122
|
+
const url = tablaUrl(base, taalId, fileBpm, this.#variation());
|
|
1123
|
+
const loop = (rate === 1 && semis === 0 ? new Tone.Player({ url, loop: true }) : new Tone.GrainPlayer({
|
|
1124
|
+
url,
|
|
1125
|
+
loop: true,
|
|
1126
|
+
playbackRate: rate,
|
|
1127
|
+
detune: semis * 100,
|
|
1128
|
+
grainSize: TABLA_GRAIN_SEC,
|
|
1129
|
+
overlap: TABLA_GRAIN_OVERLAP_SEC
|
|
1130
|
+
})).connect(master);
|
|
1131
|
+
loop.sync().start(from);
|
|
1132
|
+
if (span !== void 0) {
|
|
1133
|
+
this.#scheduleIds.push(
|
|
1134
|
+
transport.scheduleOnce((t) => {
|
|
1135
|
+
loop.volume.setValueAtTime(0, t);
|
|
1136
|
+
loop.volume.linearRampToValueAtTime(-40, t + LAY_TAIL_SEC);
|
|
1137
|
+
}, to)
|
|
1138
|
+
);
|
|
1139
|
+
loop.stop(to + LAY_TAIL_SEC);
|
|
1140
|
+
}
|
|
1141
|
+
this.#tabla.push({ loop, fileSpb: 60 / fileBpm });
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
if (this.#opts.rhythm === "drums") {
|
|
1145
|
+
this.#drums ??= createDrumKit(Tone).connect(master);
|
|
1146
|
+
const drums = this.#drums;
|
|
1147
|
+
let beat2 = 0;
|
|
1148
|
+
this.#scheduleIds.push(
|
|
1149
|
+
transport.scheduleRepeat(
|
|
1150
|
+
(time) => {
|
|
1151
|
+
const voice = pattern[beat2 % pattern.length];
|
|
1152
|
+
if (drums.has(voice)) drums.player(voice).start(time);
|
|
1153
|
+
beat2 += 1;
|
|
1154
|
+
},
|
|
1155
|
+
beatSec,
|
|
1156
|
+
from,
|
|
1157
|
+
span
|
|
1158
|
+
)
|
|
1159
|
+
);
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
this.#metronome ??= createMetronome(Tone, base).connect(master);
|
|
1163
|
+
const metro = this.#metronome;
|
|
1164
|
+
let beat = 0;
|
|
1165
|
+
this.#scheduleIds.push(
|
|
1166
|
+
transport.scheduleRepeat(
|
|
1167
|
+
(time) => {
|
|
1168
|
+
metro.volume.setValueAtTime(beat % cycle === 0 ? 0 : -8, time);
|
|
1169
|
+
metro.start(time);
|
|
1170
|
+
beat += 1;
|
|
1171
|
+
},
|
|
1172
|
+
beatSec,
|
|
1173
|
+
from,
|
|
1174
|
+
span
|
|
1175
|
+
)
|
|
1176
|
+
);
|
|
1177
|
+
});
|
|
1082
1178
|
}
|
|
1083
|
-
#buildTanpura(Tone, transport, base, spb) {
|
|
1179
|
+
#buildTanpura(Tone, transport, base, spb, segments, shift) {
|
|
1084
1180
|
if (!this.#opts.tanpura || base === void 0) return;
|
|
1085
1181
|
const tonicPc = (this.#tonicSemitone() % 12 + 12) % 12;
|
|
1086
1182
|
const sa = TANPURA_PCS[tonicPc];
|
|
@@ -1094,13 +1190,23 @@ var SwaralipiPlayer = class {
|
|
|
1094
1190
|
const players = this.#tanpura;
|
|
1095
1191
|
players.volume.value = -14;
|
|
1096
1192
|
const pattern = [pa, sa, sa, sa];
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
const
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1193
|
+
segments.forEach((seg, k) => {
|
|
1194
|
+
const from = k === 0 ? 0 : seg.beat * spb + shift;
|
|
1195
|
+
const to = k === segments.length - 1 ? Infinity : seg.endBeat * spb + shift;
|
|
1196
|
+
let i = 0;
|
|
1197
|
+
this.#scheduleIds.push(
|
|
1198
|
+
transport.scheduleRepeat(
|
|
1199
|
+
(time) => {
|
|
1200
|
+
const stem = pattern[i % pattern.length];
|
|
1201
|
+
if (players.has(stem)) players.player(stem).start(time);
|
|
1202
|
+
i += 1;
|
|
1203
|
+
},
|
|
1204
|
+
spb * 2 / seg.scale,
|
|
1205
|
+
from,
|
|
1206
|
+
to === Infinity ? void 0 : to - from
|
|
1207
|
+
)
|
|
1208
|
+
);
|
|
1209
|
+
});
|
|
1104
1210
|
}
|
|
1105
1211
|
#tonicSemitone() {
|
|
1106
1212
|
return parseTonic(this.#opts.tonic ?? this.#doc?.meta.tonic);
|
|
@@ -1108,6 +1214,7 @@ var SwaralipiPlayer = class {
|
|
|
1108
1214
|
};
|
|
1109
1215
|
|
|
1110
1216
|
// src/player/export.ts
|
|
1217
|
+
var LAY_TAIL_SEC2 = 0.12;
|
|
1111
1218
|
function midiRange2(schedule) {
|
|
1112
1219
|
let lo = Infinity;
|
|
1113
1220
|
let hi = -Infinity;
|
|
@@ -1145,7 +1252,11 @@ async function exportAudio(doc, options = {}) {
|
|
|
1145
1252
|
const rhythm = options.rhythm ?? "none";
|
|
1146
1253
|
const withMelody = options.melody ?? true;
|
|
1147
1254
|
const withTanpura = options.tanpura ?? false;
|
|
1148
|
-
const schedule = buildSchedule(doc, {
|
|
1255
|
+
const schedule = buildSchedule(doc, {
|
|
1256
|
+
tonic: options.tonic,
|
|
1257
|
+
baseOctave: options.baseOctave,
|
|
1258
|
+
laySpeeds: options.laySpeeds
|
|
1259
|
+
});
|
|
1149
1260
|
const range = midiRange2(schedule);
|
|
1150
1261
|
const taalId = resolveTaal(doc, options.taal);
|
|
1151
1262
|
const variation = options.taal === void 0 || options.taal === "auto" ? doc.meta.taal?.variation : void 0;
|
|
@@ -1198,39 +1309,67 @@ async function exportAudio(doc, options = {}) {
|
|
|
1198
1309
|
}
|
|
1199
1310
|
});
|
|
1200
1311
|
}
|
|
1201
|
-
|
|
1202
|
-
if (rhythm
|
|
1203
|
-
const drums = createDrumKit(Tone).connect(master);
|
|
1312
|
+
const tablas = [];
|
|
1313
|
+
if (rhythm !== "none" && base) {
|
|
1204
1314
|
const taal = taalId !== void 0 ? TAALS[taalId] : doc.meta.taal ?? { numBeats: 4, sam: 0 };
|
|
1205
|
-
const pattern = drumPattern(taal);
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
const
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1315
|
+
const pattern = rhythm === "drums" ? drumPattern(taal) : [];
|
|
1316
|
+
const drums = rhythm === "drums" ? createDrumKit(Tone).connect(master) : void 0;
|
|
1317
|
+
const metro = rhythm !== "drums" && !usesTabla ? createMetronome(Tone, base).connect(master) : void 0;
|
|
1318
|
+
const cycle = taalId !== void 0 ? TAALS[taalId].numBeats : doc.meta.taal?.numBeats ?? 4;
|
|
1319
|
+
const semis = tablaShiftSemitones(parseTonic(options.tonic ?? doc.meta.tonic));
|
|
1320
|
+
schedule.tempo.forEach((seg, i) => {
|
|
1321
|
+
const from = i === 0 ? 0 : seg.beat * spb + shift;
|
|
1322
|
+
const to = i === schedule.tempo.length - 1 ? Infinity : seg.endBeat * spb + shift;
|
|
1323
|
+
const beatSec = spb / seg.scale;
|
|
1324
|
+
const span = to === Infinity ? void 0 : to - from;
|
|
1325
|
+
if (usesTabla && taalId) {
|
|
1326
|
+
const segBpm = 60 / beatSec;
|
|
1327
|
+
const fileBpm = nearestTablaBpm(segBpm, tablaBpms(taalId));
|
|
1328
|
+
const rate = segBpm / fileBpm;
|
|
1329
|
+
const url = tablaUrl(base, taalId, fileBpm, variation);
|
|
1330
|
+
const loop = (rate === 1 && semis === 0 ? new Tone.Player({ url, loop: true }) : new Tone.GrainPlayer({
|
|
1331
|
+
url,
|
|
1332
|
+
loop: true,
|
|
1333
|
+
playbackRate: rate,
|
|
1334
|
+
detune: semis * 100,
|
|
1335
|
+
grainSize: TABLA_GRAIN_SEC,
|
|
1336
|
+
overlap: TABLA_GRAIN_OVERLAP_SEC
|
|
1337
|
+
})).connect(master);
|
|
1338
|
+
loop.sync().start(from);
|
|
1339
|
+
if (span !== void 0) {
|
|
1340
|
+
transport.scheduleOnce((t) => {
|
|
1341
|
+
loop.volume.setValueAtTime(0, t);
|
|
1342
|
+
loop.volume.linearRampToValueAtTime(-40, t + LAY_TAIL_SEC2);
|
|
1343
|
+
}, to);
|
|
1344
|
+
loop.stop(to + LAY_TAIL_SEC2);
|
|
1345
|
+
}
|
|
1346
|
+
tablas.push({ loop, fileSpb: 60 / fileBpm });
|
|
1347
|
+
} else if (drums !== void 0) {
|
|
1348
|
+
let beat = 0;
|
|
1349
|
+
transport.scheduleRepeat(
|
|
1350
|
+
(time) => {
|
|
1351
|
+
const voice = pattern[beat % pattern.length];
|
|
1352
|
+
if (drums.has(voice)) drums.player(voice).start(time);
|
|
1353
|
+
beat += 1;
|
|
1354
|
+
},
|
|
1355
|
+
beatSec,
|
|
1356
|
+
from,
|
|
1357
|
+
span
|
|
1358
|
+
);
|
|
1359
|
+
} else if (metro !== void 0) {
|
|
1360
|
+
let beat = 0;
|
|
1361
|
+
transport.scheduleRepeat(
|
|
1362
|
+
(time) => {
|
|
1363
|
+
metro.volume.setValueAtTime(beat % cycle === 0 ? 0 : -8, time);
|
|
1364
|
+
metro.start(time);
|
|
1365
|
+
beat += 1;
|
|
1366
|
+
},
|
|
1367
|
+
beatSec,
|
|
1368
|
+
from,
|
|
1369
|
+
span
|
|
1370
|
+
);
|
|
1371
|
+
}
|
|
1372
|
+
});
|
|
1234
1373
|
}
|
|
1235
1374
|
if (withTanpura && base) {
|
|
1236
1375
|
const tonicPc = (parseTonic(options.tonic ?? doc.meta.tonic) % 12 + 12) % 12;
|
|
@@ -1239,15 +1378,26 @@ async function exportAudio(doc, options = {}) {
|
|
|
1239
1378
|
const players = createTanpura(Tone, base, [sa, pa]).connect(master);
|
|
1240
1379
|
players.volume.value = -14;
|
|
1241
1380
|
const pattern = [pa, sa, sa, sa];
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
const
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1381
|
+
schedule.tempo.forEach((seg, k) => {
|
|
1382
|
+
const from = k === 0 ? 0 : seg.beat * spb + shift;
|
|
1383
|
+
const to = k === schedule.tempo.length - 1 ? Infinity : seg.endBeat * spb + shift;
|
|
1384
|
+
let i = 0;
|
|
1385
|
+
transport.scheduleRepeat(
|
|
1386
|
+
(time) => {
|
|
1387
|
+
const stem = pattern[i % pattern.length];
|
|
1388
|
+
if (players.has(stem)) players.player(stem).start(time);
|
|
1389
|
+
i += 1;
|
|
1390
|
+
},
|
|
1391
|
+
spb * 2 / seg.scale,
|
|
1392
|
+
from,
|
|
1393
|
+
to === Infinity ? void 0 : to - from
|
|
1394
|
+
);
|
|
1395
|
+
});
|
|
1248
1396
|
}
|
|
1249
1397
|
await Tone.loaded();
|
|
1250
|
-
|
|
1398
|
+
for (const { loop, fileSpb } of tablas) {
|
|
1399
|
+
if (loop.buffer?.loaded) loop.loopEnd = quantizeLoopEnd(loop.buffer.duration, fileSpb);
|
|
1400
|
+
}
|
|
1251
1401
|
if (fadeOutSec > 0) {
|
|
1252
1402
|
master.gain.setValueAtTime(1, Math.max(durationSec - fadeOutSec, 0));
|
|
1253
1403
|
master.gain.linearRampToValueAtTime(0, durationSec);
|
|
@@ -1468,7 +1618,11 @@ function safeName2(title) {
|
|
|
1468
1618
|
return (title ?? "").replace(/[\\/:*?"<>|]+/g, "").trim() || "swaralipi";
|
|
1469
1619
|
}
|
|
1470
1620
|
function exportMidi(doc, options = {}) {
|
|
1471
|
-
const schedule = buildSchedule(doc, {
|
|
1621
|
+
const schedule = buildSchedule(doc, {
|
|
1622
|
+
tonic: options.tonic,
|
|
1623
|
+
baseOctave: options.baseOctave,
|
|
1624
|
+
laySpeeds: options.laySpeeds
|
|
1625
|
+
});
|
|
1472
1626
|
const bpm = options.bpm ?? doc.meta.bpm ?? 90;
|
|
1473
1627
|
const taalId = doc.meta.taal?.id;
|
|
1474
1628
|
const taal = taalId !== void 0 && taalId in TAALS ? TAALS[taalId] : { numBeats: 4, sam: 0 };
|
|
@@ -1485,6 +1639,8 @@ exports.ARC_RAW_RE = ARC_RAW_RE;
|
|
|
1485
1639
|
exports.DEFAULT_SOUNDS_BASE = DEFAULT_SOUNDS_BASE;
|
|
1486
1640
|
exports.DRUM_KIT_BASE = DRUM_KIT_BASE;
|
|
1487
1641
|
exports.INSTRUMENTS = INSTRUMENTS;
|
|
1642
|
+
exports.LAYS = LAYS;
|
|
1643
|
+
exports.LAY_SPEEDS = LAY_SPEEDS;
|
|
1488
1644
|
exports.MEEND_HOLD = MEEND_HOLD;
|
|
1489
1645
|
exports.OCTAVE_OFFSET = OCTAVE_OFFSET;
|
|
1490
1646
|
exports.SALAMANDER_BASE = SALAMANDER_BASE;
|
|
@@ -1511,8 +1667,11 @@ exports.nearestSampleKey = nearestSampleKey;
|
|
|
1511
1667
|
exports.nearestTablaBpm = nearestTablaBpm;
|
|
1512
1668
|
exports.normalizeBase = normalizeBase;
|
|
1513
1669
|
exports.parseTonic = parseTonic;
|
|
1670
|
+
exports.printedLay = printedLay;
|
|
1514
1671
|
exports.probeUrl = probeUrl;
|
|
1515
1672
|
exports.quantizeLoopEnd = quantizeLoopEnd;
|
|
1673
|
+
exports.resolveLaySpeeds = resolveLaySpeeds;
|
|
1674
|
+
exports.rowLay = rowLay;
|
|
1516
1675
|
exports.sCurve = sCurve;
|
|
1517
1676
|
exports.sampleFileName = sampleFileName;
|
|
1518
1677
|
exports.santoorUrls = santoorUrls;
|