@ugfoundation/swaralipi-js 0.7.6 → 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-5NMEPJ6E.js → batch-NJMT3M5Z.js} +5 -5
- package/dist/{batch-5NMEPJ6E.js.map → batch-NJMT3M5Z.js.map} +1 -1
- package/dist/{chunk-ITOP5O5M.js → chunk-4HMHTXDE.js} +15 -4
- package/dist/chunk-4HMHTXDE.js.map +1 -0
- package/dist/{chunk-UF5OSJRP.js → chunk-4HNT6OAT.js} +32 -3
- package/dist/chunk-4HNT6OAT.js.map +1 -0
- package/dist/{chunk-2SAZWTTK.js → chunk-4IVUPMRK.js} +29 -11
- package/dist/chunk-4IVUPMRK.js.map +1 -0
- package/dist/{chunk-NJGHFQJP.js → chunk-YFVAVICF.js} +4 -4
- package/dist/{chunk-NJGHFQJP.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 +49 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- 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 +286 -119
- 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.ts → schema-DGA_bNVZ.d.cts} +97 -1
- package/dist/{schema-DqBXPGkL.d.cts → schema-DGA_bNVZ.d.ts} +97 -1
- package/dist/spec/index.cjs +49 -1
- package/dist/spec/index.cjs.map +1 -1
- package/dist/spec/index.d.cts +31 -5
- package/dist/spec/index.d.ts +31 -5
- package/dist/spec/index.js +2 -2
- package/dist/viewer/index.cjs +134 -16
- package/dist/viewer/index.cjs.map +1 -1
- package/dist/viewer/index.d.cts +10 -32
- package/dist/viewer/index.d.ts +10 -32
- package/dist/viewer/index.js +44 -9
- package/dist/viewer/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-2SAZWTTK.js.map +0 -1
- package/dist/chunk-ITOP5O5M.js.map +0 -1
- package/dist/chunk-UF5OSJRP.js.map +0 -1
package/dist/player/index.cjs
CHANGED
|
@@ -29,6 +29,14 @@ function graceNotes(grace) {
|
|
|
29
29
|
return typeof grace[0] === "string" ? [{ svara: grace }] : grace;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// src/spec/svara.ts
|
|
33
|
+
function hasKomal(letter) {
|
|
34
|
+
return letter === "r" || letter === "g" || letter === "d" || letter === "n";
|
|
35
|
+
}
|
|
36
|
+
function hasTivra(letter) {
|
|
37
|
+
return letter === "m";
|
|
38
|
+
}
|
|
39
|
+
|
|
32
40
|
// src/player/pitch.ts
|
|
33
41
|
var OCTAVE_OFFSET = {
|
|
34
42
|
"ati-mandra": -2,
|
|
@@ -79,8 +87,8 @@ function svaraSemitone(note) {
|
|
|
79
87
|
if (note.svara === void 0) return null;
|
|
80
88
|
const [, letter] = note.svara;
|
|
81
89
|
let semi = SHUDDHA[letter];
|
|
82
|
-
if (note.komal === true || note.microtone !== void 0) semi -= 1;
|
|
83
|
-
if (note.tivra === true) semi += 1;
|
|
90
|
+
if ((note.komal === true || note.microtone !== void 0) && hasKomal(letter)) semi -= 1;
|
|
91
|
+
if (note.tivra === true && hasTivra(letter)) semi += 1;
|
|
84
92
|
return semi;
|
|
85
93
|
}
|
|
86
94
|
function svaraToMidi(note, tonicPc, baseOctave = 4) {
|
|
@@ -107,6 +115,27 @@ function sCurve(x, steepness = 8) {
|
|
|
107
115
|
return (logistic(t) - lo) / (logistic(1) - lo);
|
|
108
116
|
}
|
|
109
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
|
+
|
|
110
139
|
// src/player/schedule.ts
|
|
111
140
|
var UNDECODED_CELL = /[srgmpqnvtduka]/i;
|
|
112
141
|
var isSilent = (n) => n.svara === void 0 && n.rest !== true && n.sustain !== true && !(n.raw !== void 0 && UNDECODED_CELL.test(n.raw));
|
|
@@ -151,22 +180,38 @@ function buildSchedule(doc, opts = {}) {
|
|
|
151
180
|
const events = [];
|
|
152
181
|
let cursor = 0;
|
|
153
182
|
let lastPitched = -1;
|
|
154
|
-
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;
|
|
155
189
|
doc.parts.forEach((part, p) => {
|
|
156
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
|
+
}
|
|
157
201
|
const glides = meendContinuations(row.notes);
|
|
158
202
|
row.notes.forEach((note, i) => {
|
|
159
|
-
if (
|
|
203
|
+
if (samMatra === void 0 && note.above !== void 0) {
|
|
160
204
|
for (const a of note.above) {
|
|
161
205
|
if (a.raw !== void 0 && SAM_TICK.test(a.raw)) {
|
|
162
|
-
|
|
206
|
+
samMatra = matra;
|
|
163
207
|
break;
|
|
164
208
|
}
|
|
165
209
|
}
|
|
166
210
|
}
|
|
167
211
|
if (note.bar !== void 0 || isSilent(note)) return;
|
|
168
212
|
const path = `${p}.${r}.${i}`;
|
|
169
|
-
const
|
|
213
|
+
const written = note.durationMatra ?? 1;
|
|
214
|
+
const dur = written / scale;
|
|
170
215
|
if (note.svara !== void 0) {
|
|
171
216
|
events.push({
|
|
172
217
|
beat: cursor,
|
|
@@ -185,12 +230,14 @@ function buildSchedule(doc, opts = {}) {
|
|
|
185
230
|
events.push({ beat: cursor, durationBeats: dur, midi: null, kan: [], path, kind: "rest" });
|
|
186
231
|
}
|
|
187
232
|
cursor += dur;
|
|
233
|
+
matra += written;
|
|
188
234
|
});
|
|
189
235
|
});
|
|
190
236
|
});
|
|
191
237
|
const numBeats = doc.meta.taal?.numBeats;
|
|
192
|
-
const
|
|
193
|
-
|
|
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 };
|
|
194
241
|
}
|
|
195
242
|
|
|
196
243
|
// src/player/instruments.ts
|
|
@@ -489,6 +536,10 @@ function createTanpura(Tone, base, stems = TANPURA_PCS) {
|
|
|
489
536
|
|
|
490
537
|
// src/player/player.ts
|
|
491
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
|
+
];
|
|
492
543
|
var MEDIA_LEAD_SEC = 0.25;
|
|
493
544
|
function midiRange(schedule) {
|
|
494
545
|
let lo = Infinity;
|
|
@@ -533,8 +584,11 @@ var SwaralipiPlayer = class {
|
|
|
533
584
|
#tanpura;
|
|
534
585
|
#tanpuraKey = "";
|
|
535
586
|
#metronome;
|
|
536
|
-
/** The looping tabla —
|
|
537
|
-
|
|
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 = [];
|
|
538
592
|
#drums;
|
|
539
593
|
#scheduleIds = [];
|
|
540
594
|
#fadeTimer;
|
|
@@ -545,6 +599,7 @@ var SwaralipiPlayer = class {
|
|
|
545
599
|
customInstruments: options.customInstruments,
|
|
546
600
|
tonic: options.tonic,
|
|
547
601
|
bpm: options.bpm,
|
|
602
|
+
laySpeeds: options.laySpeeds,
|
|
548
603
|
baseOctave: options.baseOctave ?? 4,
|
|
549
604
|
instrument: options.instrument ?? "piano",
|
|
550
605
|
instruments: options.instruments ?? Object.keys(INSTRUMENTS),
|
|
@@ -598,8 +653,7 @@ var SwaralipiPlayer = class {
|
|
|
598
653
|
get durationSec() {
|
|
599
654
|
if (this.#taal !== void 0) return Infinity;
|
|
600
655
|
if (this.#doc === void 0) return 0;
|
|
601
|
-
const
|
|
602
|
-
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);
|
|
603
657
|
return Math.min(full, this.#opts.previewLimitSec);
|
|
604
658
|
}
|
|
605
659
|
/** Seconds played so far, off the Transport's own clock — so it holds while
|
|
@@ -703,8 +757,7 @@ var SwaralipiPlayer = class {
|
|
|
703
757
|
transport.cancel(0);
|
|
704
758
|
transport.position = 0;
|
|
705
759
|
}
|
|
706
|
-
this.#
|
|
707
|
-
this.#tabla = void 0;
|
|
760
|
+
this.#disposeTabla();
|
|
708
761
|
this.#restoreMasterGain();
|
|
709
762
|
this.#scheduleIds = [];
|
|
710
763
|
this.#emitHighlight(null);
|
|
@@ -722,6 +775,17 @@ var SwaralipiPlayer = class {
|
|
|
722
775
|
this.#cancelFadeTimer();
|
|
723
776
|
this.#restoreMasterGain();
|
|
724
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
|
+
}
|
|
725
789
|
#restoreMasterGain() {
|
|
726
790
|
if (this.#master !== void 0 && this.#tone !== void 0) {
|
|
727
791
|
this.#master.gain.cancelScheduledValues(this.#tone.now());
|
|
@@ -807,10 +871,10 @@ var SwaralipiPlayer = class {
|
|
|
807
871
|
this.#glide.clear();
|
|
808
872
|
this.#tanpura?.dispose();
|
|
809
873
|
this.#metronome?.dispose();
|
|
810
|
-
this.#
|
|
874
|
+
this.#disposeTabla();
|
|
811
875
|
this.#drums?.dispose();
|
|
812
876
|
this.#master?.dispose();
|
|
813
|
-
this.#tanpura = this.#metronome = this.#
|
|
877
|
+
this.#tanpura = this.#metronome = this.#drums = this.#master = void 0;
|
|
814
878
|
}
|
|
815
879
|
/* ----------------------------------------------------------------- */
|
|
816
880
|
/* Internals */
|
|
@@ -865,6 +929,13 @@ var SwaralipiPlayer = class {
|
|
|
865
929
|
if (def.local === true && !this.#opts.soundsBaseUrl) return this.#fallbackInstrument();
|
|
866
930
|
return id;
|
|
867
931
|
}
|
|
932
|
+
#scheduleOpts() {
|
|
933
|
+
return {
|
|
934
|
+
tonic: this.#opts.tonic,
|
|
935
|
+
baseOctave: this.#opts.baseOctave,
|
|
936
|
+
laySpeeds: this.#opts.laySpeeds
|
|
937
|
+
};
|
|
938
|
+
}
|
|
868
939
|
/** Effective tempo: snapped to the taal's available tabla tempos while a
|
|
869
940
|
* tabla actually plays (so it never snaps to a bpm that taal lacks). */
|
|
870
941
|
#effectiveBpm() {
|
|
@@ -902,23 +973,18 @@ var SwaralipiPlayer = class {
|
|
|
902
973
|
const bpm = this.#effectiveBpm();
|
|
903
974
|
transport.bpm.value = bpm;
|
|
904
975
|
const spb = 60 / bpm;
|
|
905
|
-
const schedule = buildSchedule(this.#doc,
|
|
906
|
-
tonic: this.#opts.tonic,
|
|
907
|
-
baseOctave: this.#opts.baseOctave
|
|
908
|
-
});
|
|
976
|
+
const schedule = buildSchedule(this.#doc, this.#scheduleOpts());
|
|
909
977
|
const range = midiRange(schedule);
|
|
910
978
|
const id = this.#effectiveInstrument();
|
|
911
979
|
const def = this.#resolveDef(id) ?? INSTRUMENTS.harmonium;
|
|
912
980
|
const melody = this.#buildMelody(Tone, id, base, range);
|
|
913
981
|
const glide = schedule.events.some((ev) => ev.glide === true) ? this.#buildGlide(Tone, id, base, range) : void 0;
|
|
914
|
-
this.#
|
|
915
|
-
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);
|
|
916
985
|
await loadedOrWarn(Tone);
|
|
917
|
-
|
|
918
|
-
this.#tabla.loopEnd = quantizeLoopEnd(this.#tabla.buffer.duration, spb);
|
|
919
|
-
}
|
|
986
|
+
this.#quantizeTablaLoops();
|
|
920
987
|
const draw = Tone.getDraw();
|
|
921
|
-
const shift = this.#opts.rhythm !== "none" ? schedule.pickupBeats * spb : 0;
|
|
922
988
|
const mediaLead = Math.min(MEDIA_LEAD_SEC, spb);
|
|
923
989
|
schedule.events.forEach((ev, i) => {
|
|
924
990
|
const at = ev.beat * spb + shift;
|
|
@@ -981,11 +1047,9 @@ var SwaralipiPlayer = class {
|
|
|
981
1047
|
const bpm = this.#effectiveBpm();
|
|
982
1048
|
transport.bpm.value = bpm;
|
|
983
1049
|
const spb = 60 / bpm;
|
|
984
|
-
this.#buildRhythm(Tone, transport, base, spb);
|
|
1050
|
+
this.#buildRhythm(Tone, transport, base, spb, TAAL_MODE_TEMPO, 0);
|
|
985
1051
|
await loadedOrWarn(Tone);
|
|
986
|
-
|
|
987
|
-
this.#tabla.loopEnd = quantizeLoopEnd(this.#tabla.buffer.duration, spb);
|
|
988
|
-
}
|
|
1052
|
+
this.#quantizeTablaLoops();
|
|
989
1053
|
const draw = Tone.getDraw();
|
|
990
1054
|
const n = Math.max(1, this.#taal.numBeats);
|
|
991
1055
|
let beat = 0;
|
|
@@ -1027,52 +1091,92 @@ var SwaralipiPlayer = class {
|
|
|
1027
1091
|
this.#glide.set(id, entry);
|
|
1028
1092
|
return entry;
|
|
1029
1093
|
}
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
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();
|
|
1033
1105
|
const master = this.#ensureMaster(Tone);
|
|
1034
1106
|
const taalId = this.#taalId();
|
|
1035
1107
|
const wantsTabla = this.#opts.rhythm === "tabla" && taalId !== void 0 && SAMPLED_TAALS.has(taalId) && base !== void 0;
|
|
1036
|
-
if (wantsTabla)
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
this.#tabla = (shift === 0 ? new Tone.Player({ url, loop: true }) : new Tone.GrainPlayer({
|
|
1040
|
-
url,
|
|
1041
|
-
loop: true,
|
|
1042
|
-
detune: shift * 100,
|
|
1043
|
-
grainSize: TABLA_GRAIN_SEC,
|
|
1044
|
-
overlap: TABLA_GRAIN_OVERLAP_SEC
|
|
1045
|
-
})).connect(master);
|
|
1046
|
-
this.#tabla.sync().start(0);
|
|
1047
|
-
return;
|
|
1048
|
-
}
|
|
1049
|
-
if (this.#opts.rhythm === "drums") {
|
|
1050
|
-
this.#drums ??= createDrumKit(Tone).connect(master);
|
|
1051
|
-
const drums = this.#drums;
|
|
1052
|
-
const taal = this.#taal ?? (taalId !== void 0 ? TAALS[taalId] : this.#doc?.meta.taal) ?? { numBeats: 4, sam: 0 };
|
|
1053
|
-
const pattern = drumPattern(taal);
|
|
1054
|
-
let beat2 = 0;
|
|
1055
|
-
const id2 = transport.scheduleRepeat((time) => {
|
|
1056
|
-
const voice = pattern[beat2 % pattern.length];
|
|
1057
|
-
if (drums.has(voice)) drums.player(voice).start(time);
|
|
1058
|
-
beat2 += 1;
|
|
1059
|
-
}, spb, 0);
|
|
1060
|
-
this.#scheduleIds.push(id2);
|
|
1061
|
-
return;
|
|
1062
|
-
}
|
|
1063
|
-
if (this.#opts.rhythm === "none" || base === void 0) return;
|
|
1064
|
-
this.#metronome ??= createMetronome(Tone, base).connect(master);
|
|
1065
|
-
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) : [];
|
|
1066
1111
|
const cycle = this.#taal?.numBeats ?? this.#doc?.meta.taal?.numBeats ?? 4;
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
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
|
+
});
|
|
1074
1178
|
}
|
|
1075
|
-
#buildTanpura(Tone, transport, base, spb) {
|
|
1179
|
+
#buildTanpura(Tone, transport, base, spb, segments, shift) {
|
|
1076
1180
|
if (!this.#opts.tanpura || base === void 0) return;
|
|
1077
1181
|
const tonicPc = (this.#tonicSemitone() % 12 + 12) % 12;
|
|
1078
1182
|
const sa = TANPURA_PCS[tonicPc];
|
|
@@ -1086,13 +1190,23 @@ var SwaralipiPlayer = class {
|
|
|
1086
1190
|
const players = this.#tanpura;
|
|
1087
1191
|
players.volume.value = -14;
|
|
1088
1192
|
const pattern = [pa, sa, sa, sa];
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
const
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
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
|
+
});
|
|
1096
1210
|
}
|
|
1097
1211
|
#tonicSemitone() {
|
|
1098
1212
|
return parseTonic(this.#opts.tonic ?? this.#doc?.meta.tonic);
|
|
@@ -1100,6 +1214,7 @@ var SwaralipiPlayer = class {
|
|
|
1100
1214
|
};
|
|
1101
1215
|
|
|
1102
1216
|
// src/player/export.ts
|
|
1217
|
+
var LAY_TAIL_SEC2 = 0.12;
|
|
1103
1218
|
function midiRange2(schedule) {
|
|
1104
1219
|
let lo = Infinity;
|
|
1105
1220
|
let hi = -Infinity;
|
|
@@ -1137,7 +1252,11 @@ async function exportAudio(doc, options = {}) {
|
|
|
1137
1252
|
const rhythm = options.rhythm ?? "none";
|
|
1138
1253
|
const withMelody = options.melody ?? true;
|
|
1139
1254
|
const withTanpura = options.tanpura ?? false;
|
|
1140
|
-
const schedule = buildSchedule(doc, {
|
|
1255
|
+
const schedule = buildSchedule(doc, {
|
|
1256
|
+
tonic: options.tonic,
|
|
1257
|
+
baseOctave: options.baseOctave,
|
|
1258
|
+
laySpeeds: options.laySpeeds
|
|
1259
|
+
});
|
|
1141
1260
|
const range = midiRange2(schedule);
|
|
1142
1261
|
const taalId = resolveTaal(doc, options.taal);
|
|
1143
1262
|
const variation = options.taal === void 0 || options.taal === "auto" ? doc.meta.taal?.variation : void 0;
|
|
@@ -1190,39 +1309,67 @@ async function exportAudio(doc, options = {}) {
|
|
|
1190
1309
|
}
|
|
1191
1310
|
});
|
|
1192
1311
|
}
|
|
1193
|
-
|
|
1194
|
-
if (rhythm
|
|
1195
|
-
const drums = createDrumKit(Tone).connect(master);
|
|
1312
|
+
const tablas = [];
|
|
1313
|
+
if (rhythm !== "none" && base) {
|
|
1196
1314
|
const taal = taalId !== void 0 ? TAALS[taalId] : doc.meta.taal ?? { numBeats: 4, sam: 0 };
|
|
1197
|
-
const pattern = drumPattern(taal);
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
const
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
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
|
+
});
|
|
1226
1373
|
}
|
|
1227
1374
|
if (withTanpura && base) {
|
|
1228
1375
|
const tonicPc = (parseTonic(options.tonic ?? doc.meta.tonic) % 12 + 12) % 12;
|
|
@@ -1231,15 +1378,26 @@ async function exportAudio(doc, options = {}) {
|
|
|
1231
1378
|
const players = createTanpura(Tone, base, [sa, pa]).connect(master);
|
|
1232
1379
|
players.volume.value = -14;
|
|
1233
1380
|
const pattern = [pa, sa, sa, sa];
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
const
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
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
|
+
});
|
|
1240
1396
|
}
|
|
1241
1397
|
await Tone.loaded();
|
|
1242
|
-
|
|
1398
|
+
for (const { loop, fileSpb } of tablas) {
|
|
1399
|
+
if (loop.buffer?.loaded) loop.loopEnd = quantizeLoopEnd(loop.buffer.duration, fileSpb);
|
|
1400
|
+
}
|
|
1243
1401
|
if (fadeOutSec > 0) {
|
|
1244
1402
|
master.gain.setValueAtTime(1, Math.max(durationSec - fadeOutSec, 0));
|
|
1245
1403
|
master.gain.linearRampToValueAtTime(0, durationSec);
|
|
@@ -1460,7 +1618,11 @@ function safeName2(title) {
|
|
|
1460
1618
|
return (title ?? "").replace(/[\\/:*?"<>|]+/g, "").trim() || "swaralipi";
|
|
1461
1619
|
}
|
|
1462
1620
|
function exportMidi(doc, options = {}) {
|
|
1463
|
-
const schedule = buildSchedule(doc, {
|
|
1621
|
+
const schedule = buildSchedule(doc, {
|
|
1622
|
+
tonic: options.tonic,
|
|
1623
|
+
baseOctave: options.baseOctave,
|
|
1624
|
+
laySpeeds: options.laySpeeds
|
|
1625
|
+
});
|
|
1464
1626
|
const bpm = options.bpm ?? doc.meta.bpm ?? 90;
|
|
1465
1627
|
const taalId = doc.meta.taal?.id;
|
|
1466
1628
|
const taal = taalId !== void 0 && taalId in TAALS ? TAALS[taalId] : { numBeats: 4, sam: 0 };
|
|
@@ -1477,6 +1639,8 @@ exports.ARC_RAW_RE = ARC_RAW_RE;
|
|
|
1477
1639
|
exports.DEFAULT_SOUNDS_BASE = DEFAULT_SOUNDS_BASE;
|
|
1478
1640
|
exports.DRUM_KIT_BASE = DRUM_KIT_BASE;
|
|
1479
1641
|
exports.INSTRUMENTS = INSTRUMENTS;
|
|
1642
|
+
exports.LAYS = LAYS;
|
|
1643
|
+
exports.LAY_SPEEDS = LAY_SPEEDS;
|
|
1480
1644
|
exports.MEEND_HOLD = MEEND_HOLD;
|
|
1481
1645
|
exports.OCTAVE_OFFSET = OCTAVE_OFFSET;
|
|
1482
1646
|
exports.SALAMANDER_BASE = SALAMANDER_BASE;
|
|
@@ -1503,8 +1667,11 @@ exports.nearestSampleKey = nearestSampleKey;
|
|
|
1503
1667
|
exports.nearestTablaBpm = nearestTablaBpm;
|
|
1504
1668
|
exports.normalizeBase = normalizeBase;
|
|
1505
1669
|
exports.parseTonic = parseTonic;
|
|
1670
|
+
exports.printedLay = printedLay;
|
|
1506
1671
|
exports.probeUrl = probeUrl;
|
|
1507
1672
|
exports.quantizeLoopEnd = quantizeLoopEnd;
|
|
1673
|
+
exports.resolveLaySpeeds = resolveLaySpeeds;
|
|
1674
|
+
exports.rowLay = rowLay;
|
|
1508
1675
|
exports.sCurve = sCurve;
|
|
1509
1676
|
exports.sampleFileName = sampleFileName;
|
|
1510
1677
|
exports.santoorUrls = santoorUrls;
|