@ugfoundation/swaralipi-js 0.13.1 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/dist/{batch-XVQRHLTN.js → batch-J72GGBG2.js} +4 -4
- package/dist/{batch-XVQRHLTN.js.map → batch-J72GGBG2.js.map} +1 -1
- package/dist/{chunk-MORX7G6E.js → chunk-4WDAVRET.js} +3 -3
- package/dist/{chunk-MORX7G6E.js.map → chunk-4WDAVRET.js.map} +1 -1
- package/dist/{chunk-L6YZ3GC6.js → chunk-GIOGYJMM.js} +180 -19
- package/dist/chunk-GIOGYJMM.js.map +1 -0
- package/dist/{chunk-GKS4WZZ6.js → chunk-KUZSW4FN.js} +3 -3
- package/dist/{chunk-GKS4WZZ6.js.map → chunk-KUZSW4FN.js.map} +1 -1
- 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/{schedule-V-fgepvP.d.cts → instruments-DTIuTlda.d.cts} +174 -140
- package/dist/{schedule-ClyON98C.d.ts → instruments-NcUb0ozc.d.ts} +174 -140
- package/dist/player/index.cjs +330 -84
- package/dist/player/index.cjs.map +1 -1
- package/dist/player/index.d.cts +40 -16
- package/dist/player/index.d.ts +40 -16
- package/dist/player/index.js +150 -69
- package/dist/player/index.js.map +1 -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 +189 -31
- package/dist/viewer/index.cjs.map +1 -1
- package/dist/viewer/index.d.cts +62 -2
- package/dist/viewer/index.d.ts +62 -2
- package/dist/viewer/index.js +123 -4
- package/dist/viewer/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-L6YZ3GC6.js.map +0 -1
package/dist/player/index.cjs
CHANGED
|
@@ -145,6 +145,170 @@ function rowLay(row) {
|
|
|
145
145
|
return row.lay ?? printedLay(row.notes);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
// src/player/lyrics.ts
|
|
149
|
+
function lyricCues(events, map, text) {
|
|
150
|
+
const tokens = [...text.matchAll(/\S+/g)].map((m) => ({
|
|
151
|
+
text: m[0],
|
|
152
|
+
start: m.index,
|
|
153
|
+
end: m.index + m[0].length,
|
|
154
|
+
line: text.slice(0, m.index).split("\n").length - 1
|
|
155
|
+
}));
|
|
156
|
+
const cues = [];
|
|
157
|
+
for (const ev of events) {
|
|
158
|
+
const span = map.get(ev.path);
|
|
159
|
+
const index = span ? tokens.findIndex((t) => t.start <= span[0] && span[0] < t.end) : -1;
|
|
160
|
+
if (index < 0) continue;
|
|
161
|
+
const token = tokens[index];
|
|
162
|
+
let cue = cues.at(-1);
|
|
163
|
+
if (!cue || cue.line !== token.line || index < cue.last) {
|
|
164
|
+
cue = { beat: ev.beat, line: token.line, last: -1, words: [] };
|
|
165
|
+
cues.push(cue);
|
|
166
|
+
}
|
|
167
|
+
if (index !== cue.last) {
|
|
168
|
+
cue.words.push({ beat: ev.beat, text: token.text });
|
|
169
|
+
cue.last = index;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return cues.map(({ beat, words }) => ({ beat, words }));
|
|
173
|
+
}
|
|
174
|
+
var MATCH = 2;
|
|
175
|
+
var GAP = 1;
|
|
176
|
+
var JUMP = 4;
|
|
177
|
+
var RUN = 3;
|
|
178
|
+
var STRAY = 8;
|
|
179
|
+
var EPS = 1e-6;
|
|
180
|
+
var [DIAG, UP, LEFT, JUMPED] = [1, 2, 3, 4];
|
|
181
|
+
var letters = (s) => s.normalize("NFD").replace(/ৎ/g, "\u09A4").replace(/[^\p{L}\p{M}]|[়্]/gu, "").toLowerCase();
|
|
182
|
+
function alignLyrics(doc, text) {
|
|
183
|
+
const sung = [];
|
|
184
|
+
doc.parts.forEach(
|
|
185
|
+
(part, p) => part.rows.forEach(
|
|
186
|
+
(row, r) => row.notes.forEach((note, i) => {
|
|
187
|
+
for (const letter of letters(note.lyric ?? "")) sung.push({ letter, path: `${p}.${r}.${i}` });
|
|
188
|
+
})
|
|
189
|
+
)
|
|
190
|
+
);
|
|
191
|
+
const said = [];
|
|
192
|
+
const starts = [];
|
|
193
|
+
let at = 0;
|
|
194
|
+
let gap = true;
|
|
195
|
+
for (const ch of text) {
|
|
196
|
+
const ls = letters(ch);
|
|
197
|
+
if (ls === "") {
|
|
198
|
+
gap ||= /[\s\p{P}]/u.test(ch);
|
|
199
|
+
if (/[\p{M}\p{Cf}]/u.test(ch) && said.at(-1)?.to === at) said.at(-1).to += ch.length;
|
|
200
|
+
} else {
|
|
201
|
+
if (gap) starts.push(said.length);
|
|
202
|
+
gap = false;
|
|
203
|
+
}
|
|
204
|
+
for (const letter of ls) said.push({ letter, from: at, to: at + ch.length });
|
|
205
|
+
at += ch.length;
|
|
206
|
+
}
|
|
207
|
+
const n = sung.length;
|
|
208
|
+
const m = said.length;
|
|
209
|
+
const w = m + 1;
|
|
210
|
+
const from = new Uint8Array((n + 1) * w);
|
|
211
|
+
const best = new Int32Array(n + 1);
|
|
212
|
+
let prev = new Int32Array(w).fill(-1073741824);
|
|
213
|
+
prev[0] = 0;
|
|
214
|
+
let cur = new Int32Array(w);
|
|
215
|
+
const settle = (i, row) => {
|
|
216
|
+
for (let j2 = 1; j2 < w; j2++) if (row[j2] > row[best[i]]) best[i] = j2;
|
|
217
|
+
for (const s of starts) {
|
|
218
|
+
if (row[best[i]] - JUMP > row[s]) {
|
|
219
|
+
row[s] = row[best[i]] - JUMP;
|
|
220
|
+
from[i * w + s] = JUMPED;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
for (let j2 = 1; j2 < w; j2++) {
|
|
224
|
+
if (row[j2 - 1] - GAP > row[j2]) {
|
|
225
|
+
row[j2] = row[j2 - 1] - GAP;
|
|
226
|
+
from[i * w + j2] = LEFT;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
settle(0, prev);
|
|
231
|
+
for (let i = 1; i <= n; i++) {
|
|
232
|
+
cur[0] = prev[0] - GAP;
|
|
233
|
+
from[i * w] = UP;
|
|
234
|
+
for (let j2 = 1; j2 < w; j2++) {
|
|
235
|
+
const diag = prev[j2 - 1] + (sung[i - 1].letter === said[j2 - 1].letter ? MATCH : -GAP);
|
|
236
|
+
const up = prev[j2] - GAP;
|
|
237
|
+
cur[j2] = Math.max(diag, up);
|
|
238
|
+
from[i * w + j2] = diag >= up ? DIAG : UP;
|
|
239
|
+
}
|
|
240
|
+
best[i] = 0;
|
|
241
|
+
settle(i, cur);
|
|
242
|
+
[prev, cur] = [cur, prev];
|
|
243
|
+
}
|
|
244
|
+
const pairs = [];
|
|
245
|
+
let j = 0;
|
|
246
|
+
for (let k = 1; k < w; k++) if (prev[k] > prev[j]) j = k;
|
|
247
|
+
for (let i = n; i > 0; ) {
|
|
248
|
+
const step = from[i * w + j];
|
|
249
|
+
if (step === JUMPED) j = best[i];
|
|
250
|
+
else if (step === LEFT) j--;
|
|
251
|
+
else if (step === UP) i--;
|
|
252
|
+
else {
|
|
253
|
+
if (sung[i - 1].letter === said[j - 1].letter) pairs.push([i - 1, j - 1]);
|
|
254
|
+
i--;
|
|
255
|
+
j--;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
pairs.reverse();
|
|
259
|
+
const wordStart = new Set(starts);
|
|
260
|
+
const follows = (k) => pairs[k][0] === pairs[k - 1][0] + 1 && (pairs[k][1] === pairs[k - 1][1] + 1 || wordStart.has(pairs[k][1]));
|
|
261
|
+
const out = /* @__PURE__ */ new Map();
|
|
262
|
+
const last = /* @__PURE__ */ new Map();
|
|
263
|
+
for (let a = 0, b = 1; a < pairs.length; a = b++) {
|
|
264
|
+
while (b < pairs.length && follows(b)) b++;
|
|
265
|
+
if (b - a < RUN) continue;
|
|
266
|
+
for (const [s, t] of pairs.slice(a, b)) {
|
|
267
|
+
const { path } = sung[s];
|
|
268
|
+
const { from: lo, to: hi } = said[t];
|
|
269
|
+
const span = out.get(path);
|
|
270
|
+
if (span === void 0) out.set(path, [lo, hi]);
|
|
271
|
+
else if (t === last.get(path) + 1) span[1] = hi;
|
|
272
|
+
else continue;
|
|
273
|
+
last.set(path, t);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return out;
|
|
277
|
+
}
|
|
278
|
+
var mod = (a, n) => (a % n + n) % n;
|
|
279
|
+
var sings = (v) => v.note.svara !== void 0 || v.note.sustain === true;
|
|
280
|
+
function excerptVisits(order, at, { lyrics, ranges }, cycle) {
|
|
281
|
+
const pos = order.map((v) => lyrics.get(`${v.p}.${v.r}.${v.i}`)?.[0]);
|
|
282
|
+
for (let a = 0; a < order.length; a++) {
|
|
283
|
+
if (pos[a] !== void 0 || letters(order[a].note.lyric ?? "") === "") continue;
|
|
284
|
+
let b = a;
|
|
285
|
+
let stray = 0;
|
|
286
|
+
while (b < order.length && pos[b] === void 0) stray += letters(order[b++].note.lyric ?? "").length;
|
|
287
|
+
if (stray >= STRAY) pos.fill(-1, a, b);
|
|
288
|
+
a = b;
|
|
289
|
+
}
|
|
290
|
+
const visits = [];
|
|
291
|
+
let first;
|
|
292
|
+
let end = 0;
|
|
293
|
+
for (const [lo, hi] of ranges) {
|
|
294
|
+
const inside = (k) => pos[k] === void 0 ? void 0 : pos[k] >= lo && pos[k] < hi;
|
|
295
|
+
const a = order.findIndex((_, k) => inside(k) === true);
|
|
296
|
+
if (a < 0) continue;
|
|
297
|
+
let b = a + 1;
|
|
298
|
+
while (b < order.length && inside(b) !== false) b++;
|
|
299
|
+
while (b - 1 > a && !sings(order[b - 1])) b--;
|
|
300
|
+
const n = cycle(order[a]);
|
|
301
|
+
if (first === void 0) first = a;
|
|
302
|
+
else if (n !== void 0) {
|
|
303
|
+
const gap = mod(at[a] - at[end], n);
|
|
304
|
+
if (gap > EPS) visits.push({ ...order[end - 1], note: { rest: true, durationMatra: gap } });
|
|
305
|
+
}
|
|
306
|
+
visits.push(...order.slice(a, b));
|
|
307
|
+
end = b;
|
|
308
|
+
}
|
|
309
|
+
return { visits, matra: first === void 0 ? 0 : at[first] };
|
|
310
|
+
}
|
|
311
|
+
|
|
148
312
|
// src/convert/codec.ts
|
|
149
313
|
var LETTERS = {
|
|
150
314
|
s: { letter: "s" },
|
|
@@ -310,7 +474,7 @@ function decodeCell(text) {
|
|
|
310
474
|
var UNDECODED_CELL = /[srgmpqnvtduka]/i;
|
|
311
475
|
var isSilent = (n) => n.svara === void 0 && n.rest !== true && n.sustain !== true && !(n.raw !== void 0 && UNDECODED_CELL.test(n.raw));
|
|
312
476
|
var STOP_MARK = /^\|+$/;
|
|
313
|
-
var
|
|
477
|
+
var EPS2 = 1e-6;
|
|
314
478
|
var edges = (n, kind, edge) => n.brackets?.filter((b) => b.kind === kind && b.edge === edge).length ?? 0;
|
|
315
479
|
var sounds = (n) => n.svara !== void 0 || n.rest === true || n.sustain === true;
|
|
316
480
|
var matra = (notes) => notes.reduce((sum, n) => sum + (n.durationMatra ?? 1), 0);
|
|
@@ -339,10 +503,10 @@ function playOrder(doc) {
|
|
|
339
503
|
owed = 0;
|
|
340
504
|
continue;
|
|
341
505
|
}
|
|
342
|
-
if (owed >
|
|
506
|
+
if (owed > EPS2 && t.timed) owed -= matra([t.note]);
|
|
343
507
|
else {
|
|
344
508
|
const sub = useChanged || again ? changed.get(k) : void 0;
|
|
345
|
-
const fits = sub !== void 0 && matra(sub) >= matra([t.note]) -
|
|
509
|
+
const fits = sub !== void 0 && matra(sub) >= matra([t.note]) - EPS2;
|
|
346
510
|
for (const note of fits ? sub : [t.note]) out.push({ p: t.p, r: t.r, i: t.i, note });
|
|
347
511
|
if (t.timed) last = k;
|
|
348
512
|
owed = fits ? matra(sub) - matra([t.note]) : 0;
|
|
@@ -535,8 +699,6 @@ function buildSchedule(doc, opts = {}) {
|
|
|
535
699
|
const events = [];
|
|
536
700
|
let cursor = 0;
|
|
537
701
|
let lastPitched = -1;
|
|
538
|
-
let samMatra;
|
|
539
|
-
let matra2 = 0;
|
|
540
702
|
const speeds = resolveLaySpeeds(doc.meta.laySpeeds, opts.laySpeeds);
|
|
541
703
|
const withLay = opts.lay !== false;
|
|
542
704
|
const tempo = [{ beat: 0, endBeat: 0, scale: 1, lay: "drut", taal: doc.meta.taal }];
|
|
@@ -555,7 +717,15 @@ function buildSchedule(doc, opts = {}) {
|
|
|
555
717
|
rows.set(`${p}.${r}`, { scale, lay: layNow, taal, glides: meendContinuations(row.notes) });
|
|
556
718
|
})
|
|
557
719
|
);
|
|
558
|
-
const
|
|
720
|
+
const order = opts.repeats === true ? playOrder(doc) : doc.parts.flatMap((part, p) => part.rows.flatMap((row, r) => row.notes.map((note, i) => ({ p, r, i, note }))));
|
|
721
|
+
const at = [0];
|
|
722
|
+
let samMatra;
|
|
723
|
+
for (const { note } of order) {
|
|
724
|
+
const m = at[at.length - 1];
|
|
725
|
+
if (samMatra === void 0 && note.above?.some((a) => a.raw !== void 0 && SAM_TICK.test(a.raw))) samMatra = m;
|
|
726
|
+
at.push(note.bar === void 0 && !isSilent(note) ? m + (note.durationMatra ?? 1) : m);
|
|
727
|
+
}
|
|
728
|
+
const { visits, matra: matra2 } = opts.excerpt ? excerptVisits(order, at, opts.excerpt, (v) => rows.get(`${v.p}.${v.r}`).taal?.numBeats) : { visits: order, matra: 0 };
|
|
559
729
|
let rowNow = "";
|
|
560
730
|
for (const { p, r, i, note } of visits) {
|
|
561
731
|
const row = rows.get(`${p}.${r}`);
|
|
@@ -571,14 +741,6 @@ function buildSchedule(doc, opts = {}) {
|
|
|
571
741
|
}
|
|
572
742
|
}
|
|
573
743
|
}
|
|
574
|
-
if (samMatra === void 0 && note.above !== void 0) {
|
|
575
|
-
for (const a of note.above) {
|
|
576
|
-
if (a.raw !== void 0 && SAM_TICK.test(a.raw)) {
|
|
577
|
-
samMatra = matra2;
|
|
578
|
-
break;
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
744
|
if (note.bar !== void 0 || isSilent(note)) continue;
|
|
583
745
|
const path = `${p}.${r}.${i}`;
|
|
584
746
|
const written = note.durationMatra ?? 1;
|
|
@@ -604,10 +766,9 @@ function buildSchedule(doc, opts = {}) {
|
|
|
604
766
|
events.push({ beat: cursor, durationBeats: dur, midi: null, kan, resh, path, kind: "rest" });
|
|
605
767
|
}
|
|
606
768
|
cursor += dur;
|
|
607
|
-
matra2 += written;
|
|
608
769
|
}
|
|
609
770
|
const numBeats = tempo[0].taal?.numBeats;
|
|
610
|
-
const pickupMatra =
|
|
771
|
+
const pickupMatra = numBeats !== void 0 && numBeats > 0 ? ((matra2 - (samMatra ?? 0)) % numBeats + numBeats) % numBeats : 0;
|
|
611
772
|
tempo[tempo.length - 1].endBeat = cursor;
|
|
612
773
|
return { events, totalBeats: cursor, pickupBeats: pickupMatra / tempo[0].scale, tempo };
|
|
613
774
|
}
|
|
@@ -946,6 +1107,8 @@ var SwaralipiPlayer = class {
|
|
|
946
1107
|
/** Per-song setBpm tweak: beats meta.bpm but is cleared by the next load(). */
|
|
947
1108
|
#bpmTweak;
|
|
948
1109
|
#doc;
|
|
1110
|
+
/** Play only these stretches of the song's text (see `load`). */
|
|
1111
|
+
#excerpt;
|
|
949
1112
|
/** Taal mode (`loadTaal`): loops one avartan of this taal's theka with no
|
|
950
1113
|
* melody. Mutually exclusive with `#doc`. */
|
|
951
1114
|
#taal;
|
|
@@ -988,6 +1151,8 @@ var SwaralipiPlayer = class {
|
|
|
988
1151
|
/** Listeners on the bound sheet, removed on rebind. */
|
|
989
1152
|
#unbind;
|
|
990
1153
|
#sheet;
|
|
1154
|
+
/** The bound sheet's highlight — beside, never instead of, the consumer's `onHighlight`. */
|
|
1155
|
+
#paint;
|
|
991
1156
|
/** Preview cap, counted in listening time so a seek or a loop can't dodge it. */
|
|
992
1157
|
#listened = 0;
|
|
993
1158
|
#listenedSince = 0;
|
|
@@ -1078,10 +1243,13 @@ var SwaralipiPlayer = class {
|
|
|
1078
1243
|
/* ----------------------------------------------------------------- */
|
|
1079
1244
|
/* Loading + transport control */
|
|
1080
1245
|
/* ----------------------------------------------------------------- */
|
|
1081
|
-
/** Set the document to play. Stops any current playback.
|
|
1082
|
-
|
|
1246
|
+
/** Set the document to play. Stops any current playback. With `excerpt`,
|
|
1247
|
+
* only the notes singing those stretches of the song's text play, in order
|
|
1248
|
+
* (see `alignLyrics`); the length, seeks and repeats all keep to them. */
|
|
1249
|
+
load(doc, { excerpt } = {}) {
|
|
1083
1250
|
this.#halt();
|
|
1084
1251
|
this.#doc = doc;
|
|
1252
|
+
this.#excerpt = excerpt;
|
|
1085
1253
|
this.#taal = void 0;
|
|
1086
1254
|
this.#taalOverride = void 0;
|
|
1087
1255
|
this.#bpmTweak = void 0;
|
|
@@ -1404,7 +1572,7 @@ var SwaralipiPlayer = class {
|
|
|
1404
1572
|
*/
|
|
1405
1573
|
bindSheet(target, opts = {}) {
|
|
1406
1574
|
const block = opts.autoScroll === true ? "center" : opts.autoScroll || void 0;
|
|
1407
|
-
this.#
|
|
1575
|
+
this.#paint = (path, yourTurn) => {
|
|
1408
1576
|
target.clearHighlights();
|
|
1409
1577
|
if (path === null) return;
|
|
1410
1578
|
const el2 = target.highlight(path, yourTurn);
|
|
@@ -1516,6 +1684,7 @@ var SwaralipiPlayer = class {
|
|
|
1516
1684
|
this.#opts.onState?.(state);
|
|
1517
1685
|
}
|
|
1518
1686
|
#emitHighlight(path, yourTurn = false) {
|
|
1687
|
+
this.#paint?.(path, yourTurn);
|
|
1519
1688
|
this.#opts.onHighlight?.(path, yourTurn);
|
|
1520
1689
|
}
|
|
1521
1690
|
#emitLoop() {
|
|
@@ -1673,7 +1842,8 @@ var SwaralipiPlayer = class {
|
|
|
1673
1842
|
tonic: this.#opts.tonic,
|
|
1674
1843
|
baseOctave: this.#opts.baseOctave,
|
|
1675
1844
|
laySpeeds: this.#opts.laySpeeds,
|
|
1676
|
-
repeats: this.#opts.repeats
|
|
1845
|
+
repeats: this.#opts.repeats,
|
|
1846
|
+
excerpt: this.#excerpt
|
|
1677
1847
|
};
|
|
1678
1848
|
}
|
|
1679
1849
|
/** Effective tempo: snapped to the taal's available tabla tempos while a
|
|
@@ -1996,6 +2166,38 @@ function midiRange2(schedule) {
|
|
|
1996
2166
|
}
|
|
1997
2167
|
return lo <= hi ? { lo, hi } : void 0;
|
|
1998
2168
|
}
|
|
2169
|
+
var AHEAD_SEC = 2;
|
|
2170
|
+
async function renderOffline(Tone, build, durationSec, channels, sampleRate) {
|
|
2171
|
+
const chunked = "suspend" in OfflineAudioContext.prototype && "positionX" in AudioListener.prototype;
|
|
2172
|
+
const context = chunked ? new Tone.OfflineContext(new OfflineAudioContext(channels, Math.ceil(durationSec * sampleRate), sampleRate)) : new Tone.OfflineContext(channels, durationSec, sampleRate);
|
|
2173
|
+
const raw = context.rawContext;
|
|
2174
|
+
const original = Tone.getContext();
|
|
2175
|
+
Tone.setContext(context);
|
|
2176
|
+
try {
|
|
2177
|
+
await build(context);
|
|
2178
|
+
} finally {
|
|
2179
|
+
Tone.setContext(original);
|
|
2180
|
+
}
|
|
2181
|
+
const clock = context;
|
|
2182
|
+
const run = (until) => {
|
|
2183
|
+
while (clock._currentTime <= Math.min(until, durationSec)) {
|
|
2184
|
+
clock.emit("tick");
|
|
2185
|
+
clock._currentTime += 128 / sampleRate;
|
|
2186
|
+
}
|
|
2187
|
+
};
|
|
2188
|
+
if (chunked) {
|
|
2189
|
+
run(2 * AHEAD_SEC);
|
|
2190
|
+
for (let t = AHEAD_SEC; t < durationSec; t += AHEAD_SEC) {
|
|
2191
|
+
void raw.suspend(t).then(() => {
|
|
2192
|
+
run(t + 2 * AHEAD_SEC);
|
|
2193
|
+
void raw.resume();
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
} else {
|
|
2197
|
+
run(Infinity);
|
|
2198
|
+
}
|
|
2199
|
+
return raw.startRendering();
|
|
2200
|
+
}
|
|
1999
2201
|
function pickDef(id, o) {
|
|
2000
2202
|
const def = o.customInstruments?.[id] ?? INSTRUMENTS[id];
|
|
2001
2203
|
if (def === void 0) return INSTRUMENTS.harmonium;
|
|
@@ -2024,7 +2226,8 @@ async function exportAudio(doc, options = {}) {
|
|
|
2024
2226
|
tonic: options.tonic,
|
|
2025
2227
|
baseOctave: options.baseOctave,
|
|
2026
2228
|
laySpeeds: options.laySpeeds,
|
|
2027
|
-
repeats: options.repeats ?? true
|
|
2229
|
+
repeats: options.repeats ?? true,
|
|
2230
|
+
excerpt: options.excerpt
|
|
2028
2231
|
});
|
|
2029
2232
|
const range = midiRange2(schedule);
|
|
2030
2233
|
const taalId = resolveTaal(doc, options.taal);
|
|
@@ -2037,7 +2240,8 @@ async function exportAudio(doc, options = {}) {
|
|
|
2037
2240
|
const def = pickDef(id, options);
|
|
2038
2241
|
const shift = rhythm !== "none" ? schedule.pickupBeats * spb : 0;
|
|
2039
2242
|
const durationSec = Math.max(schedule.totalBeats * spb + shift + (def.release ?? 1) + fadeOutSec + 0.5, 0.5);
|
|
2040
|
-
const rendered = await
|
|
2243
|
+
const rendered = await renderOffline(
|
|
2244
|
+
Tone,
|
|
2041
2245
|
async ({ transport }) => {
|
|
2042
2246
|
transport.bpm.value = bpm;
|
|
2043
2247
|
const master = new Tone.Gain(1).toDestination();
|
|
@@ -2184,11 +2388,16 @@ async function exportAudio(doc, options = {}) {
|
|
|
2184
2388
|
for (let c = 0; c < rendered.numberOfChannels; c++) chans.push(rendered.getChannelData(c));
|
|
2185
2389
|
const bytes = format === "mp3" ? await encodeMp3(chans, rendered.sampleRate, options.mp3Kbps) : encodeWav(chans, rendered.sampleRate);
|
|
2186
2390
|
const mimeType = format === "mp3" ? "audio/mpeg" : "audio/wav";
|
|
2391
|
+
const cues = options.lyrics === void 0 ? void 0 : lyricCues(schedule.events, alignLyrics(doc, options.lyrics), options.lyrics).map((cue) => ({
|
|
2392
|
+
sec: cue.beat * spb + shift,
|
|
2393
|
+
words: cue.words.map((word) => ({ sec: word.beat * spb + shift, text: word.text }))
|
|
2394
|
+
}));
|
|
2187
2395
|
return {
|
|
2188
2396
|
blob: new Blob([bytes], { type: mimeType }),
|
|
2189
2397
|
filename: `${safeName(doc.meta.title)}.${format}`,
|
|
2190
2398
|
mimeType,
|
|
2191
|
-
durationSec: rendered.duration
|
|
2399
|
+
durationSec: rendered.duration,
|
|
2400
|
+
...cues === void 0 ? {} : { cues }
|
|
2192
2401
|
};
|
|
2193
2402
|
}
|
|
2194
2403
|
function toInt16(f) {
|
|
@@ -2245,8 +2454,8 @@ function concatBytes(chunks) {
|
|
|
2245
2454
|
}
|
|
2246
2455
|
async function loadLame() {
|
|
2247
2456
|
try {
|
|
2248
|
-
const
|
|
2249
|
-
const Mp3Encoder =
|
|
2457
|
+
const mod2 = await import('@breezystack/lamejs');
|
|
2458
|
+
const Mp3Encoder = mod2.Mp3Encoder ?? mod2.default?.Mp3Encoder;
|
|
2250
2459
|
if (!Mp3Encoder) throw new Error("no Mp3Encoder export");
|
|
2251
2460
|
return Mp3Encoder;
|
|
2252
2461
|
} catch {
|
|
@@ -2255,22 +2464,32 @@ async function loadLame() {
|
|
|
2255
2464
|
);
|
|
2256
2465
|
}
|
|
2257
2466
|
}
|
|
2258
|
-
async function
|
|
2467
|
+
async function mp3Stream(sampleRate = 44100, kbps = 192, channels = 1) {
|
|
2259
2468
|
const Mp3Encoder = await loadLame();
|
|
2260
|
-
const
|
|
2261
|
-
const enc = new Mp3Encoder(numCh, sampleRate, kbps);
|
|
2262
|
-
const left = floatToInt16(channels[0] ?? new Float32Array());
|
|
2263
|
-
const right = numCh > 1 ? floatToInt16(channels[1]) : void 0;
|
|
2264
|
-
const block = 1152;
|
|
2469
|
+
const enc = new Mp3Encoder(channels, sampleRate, kbps);
|
|
2265
2470
|
const out = [];
|
|
2266
|
-
|
|
2267
|
-
const l = left.subarray(i, i + block);
|
|
2268
|
-
const chunk = right ? enc.encodeBuffer(l, right.subarray(i, i + block)) : enc.encodeBuffer(l);
|
|
2471
|
+
const keep = (chunk) => {
|
|
2269
2472
|
if (chunk.length > 0) out.push(new Uint8Array(chunk));
|
|
2270
|
-
}
|
|
2271
|
-
const
|
|
2272
|
-
|
|
2273
|
-
|
|
2473
|
+
};
|
|
2474
|
+
const block = 1152;
|
|
2475
|
+
return {
|
|
2476
|
+
push(left, right) {
|
|
2477
|
+
for (let i = 0; i < left.length; i += block) {
|
|
2478
|
+
const l = left.subarray(i, i + block);
|
|
2479
|
+
keep(right ? enc.encodeBuffer(l, right.subarray(i, i + block)) : enc.encodeBuffer(l));
|
|
2480
|
+
}
|
|
2481
|
+
},
|
|
2482
|
+
end() {
|
|
2483
|
+
keep(enc.flush());
|
|
2484
|
+
return out;
|
|
2485
|
+
}
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
async function encodeMp3(channels, sampleRate, kbps = 192) {
|
|
2489
|
+
const numCh = Math.min(channels.length, 2);
|
|
2490
|
+
const mp3 = await mp3Stream(sampleRate, kbps, numCh);
|
|
2491
|
+
mp3.push(floatToInt16(channels[0] ?? new Float32Array()), numCh > 1 ? floatToInt16(channels[1]) : void 0);
|
|
2492
|
+
return concatBytes(mp3.end());
|
|
2274
2493
|
}
|
|
2275
2494
|
|
|
2276
2495
|
// src/player/midi.ts
|
|
@@ -2316,6 +2535,11 @@ function trackChunk(raw, head) {
|
|
|
2316
2535
|
const track = [...head];
|
|
2317
2536
|
let last = 0;
|
|
2318
2537
|
for (const e of raw) {
|
|
2538
|
+
if (e.meta) {
|
|
2539
|
+
track.push(...vlq(e.tick - last), ...e.meta);
|
|
2540
|
+
last = e.tick;
|
|
2541
|
+
continue;
|
|
2542
|
+
}
|
|
2319
2543
|
const status = e.status !== void 0 ? e.status | e.ch & 15 : (e.on ? 144 : 128) | e.ch & 15;
|
|
2320
2544
|
track.push(...vlq(e.tick - last), status, e.note & 127, e.vel & 127);
|
|
2321
2545
|
last = e.tick;
|
|
@@ -2323,22 +2547,22 @@ function trackChunk(raw, head) {
|
|
|
2323
2547
|
track.push(...vlq(0), 255, 47, 0);
|
|
2324
2548
|
return [77, 84, 114, 107, ...u32be(track.length), ...track];
|
|
2325
2549
|
}
|
|
2326
|
-
function
|
|
2550
|
+
function melodyEvents(schedule, offset = 0) {
|
|
2327
2551
|
const melody = [];
|
|
2328
2552
|
const hasGlide = schedule.events.some((ev) => ev.glide === true);
|
|
2329
2553
|
if (hasGlide) melody.push(...bendRangeSetup());
|
|
2330
2554
|
schedule.events.forEach((ev, i) => {
|
|
2331
2555
|
if (ev.glide === true) return;
|
|
2332
2556
|
for (const g of graceTouches(ev)) {
|
|
2333
|
-
const on = Math.max(Math.round(g.beat * PPQ), 0);
|
|
2557
|
+
const on = Math.max(Math.round(g.beat * PPQ) + offset, 0);
|
|
2334
2558
|
melody.push({ tick: on, on: true, note: g.midi, vel: 70, ch: 0 });
|
|
2335
|
-
melody.push({ tick: Math.max(on + 1, Math.round((g.beat + GRACE_STEP) * PPQ)), on: false, note: g.midi, vel: 0, ch: 0 });
|
|
2559
|
+
melody.push({ tick: Math.max(on + 1, Math.round((g.beat + GRACE_STEP) * PPQ) + offset), on: false, note: g.midi, vel: 0, ch: 0 });
|
|
2336
2560
|
}
|
|
2337
2561
|
if (ev.midi === null) return;
|
|
2338
|
-
const start = Math.round(ev.beat * PPQ);
|
|
2562
|
+
const start = Math.round(ev.beat * PPQ) + offset;
|
|
2339
2563
|
const targets = glideTargets(schedule.events, i);
|
|
2340
2564
|
const last = targets[targets.length - 1] ?? ev;
|
|
2341
|
-
const end = Math.max(Math.round((last.beat + last.durationBeats) * PPQ), start + 1);
|
|
2565
|
+
const end = Math.max(Math.round((last.beat + last.durationBeats) * PPQ) + offset, start + 1);
|
|
2342
2566
|
melody.push({ tick: start, on: true, note: ev.midi, vel: 100, ch: 0 });
|
|
2343
2567
|
melody.push({ tick: end, on: false, note: ev.midi, vel: 0, ch: 0 });
|
|
2344
2568
|
let from = ev;
|
|
@@ -2348,71 +2572,89 @@ function encodeMidi(schedule, opts) {
|
|
|
2348
2572
|
const u = n / BEND_STEPS;
|
|
2349
2573
|
const beat = glideStart + (to.beat - glideStart) * u;
|
|
2350
2574
|
const semis = from.midi - ev.midi + (to.midi - from.midi) * sCurve(u);
|
|
2351
|
-
melody.push(bendEvent(Math.round(beat * PPQ), semis));
|
|
2575
|
+
melody.push(bendEvent(Math.round(beat * PPQ) + offset, semis));
|
|
2352
2576
|
}
|
|
2353
2577
|
from = to;
|
|
2354
2578
|
}
|
|
2355
2579
|
if (targets.length > 0) melody.push(bendEvent(end, 0));
|
|
2356
2580
|
});
|
|
2581
|
+
return melody;
|
|
2582
|
+
}
|
|
2583
|
+
function drumEvents(schedule, drums, offset = 0) {
|
|
2584
|
+
const drum = [];
|
|
2585
|
+
for (const seg of schedule.tempo) {
|
|
2586
|
+
const pattern = typeof drums === "function" ? drums(seg) : drums;
|
|
2587
|
+
if (!pattern?.length) continue;
|
|
2588
|
+
const end = seg === schedule.tempo[schedule.tempo.length - 1] ? Math.ceil(schedule.totalBeats) : seg.endBeat;
|
|
2589
|
+
for (let k = 0; seg.beat + k / seg.scale < end - 1e-6; k++) {
|
|
2590
|
+
const note = DRUM_NOTE[pattern[k % pattern.length]];
|
|
2591
|
+
const t = Math.round((seg.beat + k / seg.scale) * PPQ) + offset;
|
|
2592
|
+
drum.push({ tick: t, on: true, note, vel: 100, ch: 9 });
|
|
2593
|
+
drum.push({ tick: t + Math.round(PPQ / 4), on: false, note, vel: 0, ch: 9 });
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
return drum;
|
|
2597
|
+
}
|
|
2598
|
+
function midiFile(chunks) {
|
|
2599
|
+
const format = chunks.length > 1 ? 1 : 0;
|
|
2600
|
+
const header = [77, 84, 104, 100, ...u32be(6), 0, format, chunks.length >> 8 & 255, chunks.length & 255, PPQ >> 8 & 255, PPQ & 255];
|
|
2601
|
+
return new Uint8Array([...header, ...chunks.flat()]);
|
|
2602
|
+
}
|
|
2603
|
+
function meta(type, data) {
|
|
2604
|
+
return [255, type, ...vlq(data.length), ...data];
|
|
2605
|
+
}
|
|
2606
|
+
function encodeMidi(schedule, opts) {
|
|
2357
2607
|
const usPerBeat = Math.round(6e7 / opts.bpm);
|
|
2358
2608
|
const head = [];
|
|
2359
2609
|
if (opts.name) head.push(...vlq(0), 255, 3, ...vlq(utf8(opts.name).length), ...utf8(opts.name));
|
|
2360
2610
|
head.push(...vlq(0), 255, 81, 3, usPerBeat >> 16 & 255, usPerBeat >> 8 & 255, usPerBeat & 255);
|
|
2361
|
-
const chunks = [trackChunk(
|
|
2611
|
+
const chunks = [trackChunk(melodyEvents(schedule), head)];
|
|
2362
2612
|
if (typeof opts.drums === "function" || opts.drums && opts.drums.length > 0) {
|
|
2363
|
-
const drum =
|
|
2364
|
-
for (const seg of schedule.tempo) {
|
|
2365
|
-
const pattern = typeof opts.drums === "function" ? opts.drums(seg) : opts.drums;
|
|
2366
|
-
if (!pattern?.length) continue;
|
|
2367
|
-
const end = seg === schedule.tempo[schedule.tempo.length - 1] ? Math.ceil(schedule.totalBeats) : seg.endBeat;
|
|
2368
|
-
for (let k = 0; seg.beat + k / seg.scale < end - 1e-6; k++) {
|
|
2369
|
-
const note = DRUM_NOTE[pattern[k % pattern.length]];
|
|
2370
|
-
const t = Math.round((seg.beat + k / seg.scale) * PPQ);
|
|
2371
|
-
drum.push({ tick: t, on: true, note, vel: 100, ch: 9 });
|
|
2372
|
-
drum.push({ tick: t + Math.round(PPQ / 4), on: false, note, vel: 0, ch: 9 });
|
|
2373
|
-
}
|
|
2374
|
-
}
|
|
2613
|
+
const drum = drumEvents(schedule, opts.drums);
|
|
2375
2614
|
chunks.push(trackChunk(drum, [...vlq(0), 255, 3, ...vlq(5), 68, 114, 117, 109, 115]));
|
|
2376
2615
|
}
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
chunks.length >> 8 & 255,
|
|
2387
|
-
chunks.length & 255,
|
|
2388
|
-
PPQ >> 8 & 255,
|
|
2389
|
-
PPQ & 255
|
|
2390
|
-
];
|
|
2391
|
-
return new Uint8Array([...header, ...chunks.flat()]);
|
|
2616
|
+
return midiFile(chunks);
|
|
2617
|
+
}
|
|
2618
|
+
function exportSchedule(doc, o) {
|
|
2619
|
+
return buildSchedule(doc, { tonic: o.tonic, baseOctave: o.baseOctave, laySpeeds: o.laySpeeds, repeats: o.repeats ?? true, excerpt: o.excerpt });
|
|
2620
|
+
}
|
|
2621
|
+
function taalDrums(seg) {
|
|
2622
|
+
if (seg.taal === null) return void 0;
|
|
2623
|
+
const id = seg.taal?.id;
|
|
2624
|
+
return drumPattern(id !== void 0 && id in TAALS ? TAALS[id] : { numBeats: 4, sam: 0 });
|
|
2392
2625
|
}
|
|
2393
2626
|
function safeName2(title) {
|
|
2394
2627
|
return (title ?? "").replace(/[\\/:*?"<>|]+/g, "").trim() || "swaralipi";
|
|
2395
2628
|
}
|
|
2396
2629
|
function exportMidi(doc, options = {}) {
|
|
2397
|
-
const schedule =
|
|
2398
|
-
tonic: options.tonic,
|
|
2399
|
-
baseOctave: options.baseOctave,
|
|
2400
|
-
laySpeeds: options.laySpeeds,
|
|
2401
|
-
repeats: options.repeats ?? true
|
|
2402
|
-
});
|
|
2630
|
+
const schedule = exportSchedule(doc, options);
|
|
2403
2631
|
const bpm = options.bpm ?? doc.meta.bpm ?? 90;
|
|
2404
|
-
const
|
|
2405
|
-
const id = seg.taal?.id;
|
|
2406
|
-
if (seg.taal === null) return void 0;
|
|
2407
|
-
return drumPattern(id !== void 0 && id in TAALS ? TAALS[id] : { numBeats: 4, sam: 0 });
|
|
2408
|
-
} : void 0;
|
|
2409
|
-
const bytes = encodeMidi(schedule, { bpm, name: options.name ?? doc.meta.title, drums });
|
|
2632
|
+
const bytes = encodeMidi(schedule, { bpm, name: options.name ?? doc.meta.title, drums: options.drums ? taalDrums : void 0 });
|
|
2410
2633
|
return {
|
|
2411
2634
|
blob: new Blob([bytes], { type: "audio/midi" }),
|
|
2412
2635
|
filename: `${safeName2(doc.meta.title)}.mid`,
|
|
2413
2636
|
mimeType: "audio/midi"
|
|
2414
2637
|
};
|
|
2415
2638
|
}
|
|
2639
|
+
function exportMidiSet(items, name) {
|
|
2640
|
+
const melody = [];
|
|
2641
|
+
const drum = [];
|
|
2642
|
+
let beat = 0;
|
|
2643
|
+
for (const { doc, options = {} } of items) {
|
|
2644
|
+
const schedule = exportSchedule(doc, options);
|
|
2645
|
+
const tick = beat * PPQ;
|
|
2646
|
+
const usPerBeat = Math.round(6e7 / (options.bpm ?? doc.meta.bpm ?? 90));
|
|
2647
|
+
const at = (bytes2) => ({ tick, on: false, note: 0, vel: 0, ch: 0, meta: bytes2 });
|
|
2648
|
+
melody.push(at(meta(81, [usPerBeat >> 16 & 255, usPerBeat >> 8 & 255, usPerBeat & 255])));
|
|
2649
|
+
melody.push(at(meta(6, utf8(options.name ?? doc.meta.title ?? ""))), ...melodyEvents(schedule, tick));
|
|
2650
|
+
if (options.drums) drum.push(...drumEvents(schedule, taalDrums, tick));
|
|
2651
|
+
beat += Math.ceil(schedule.totalBeats) + 2;
|
|
2652
|
+
}
|
|
2653
|
+
const chunks = [trackChunk(melody, [...vlq(0), ...meta(3, utf8(name))])];
|
|
2654
|
+
if (drum.length > 0) chunks.push(trackChunk(drum, [...vlq(0), ...meta(3, utf8("Drums"))]));
|
|
2655
|
+
const bytes = midiFile(chunks);
|
|
2656
|
+
return { blob: new Blob([bytes], { type: "audio/midi" }), filename: `${safeName2(name)}.mid`, mimeType: "audio/midi" };
|
|
2657
|
+
}
|
|
2416
2658
|
|
|
2417
2659
|
exports.ARC_RAW_RE = ARC_RAW_RE;
|
|
2418
2660
|
exports.DEFAULT_SOUNDS_BASE = DEFAULT_SOUNDS_BASE;
|
|
@@ -2430,6 +2672,7 @@ exports.SwaralipiPlayer = SwaralipiPlayer;
|
|
|
2430
2672
|
exports.TABLA_BPMS = TABLA_BPMS;
|
|
2431
2673
|
exports.TABLA_VARIATIONS = TABLA_VARIATIONS;
|
|
2432
2674
|
exports.TONEJS_INSTRUMENTS_BASE = TONEJS_INSTRUMENTS_BASE;
|
|
2675
|
+
exports.alignLyrics = alignLyrics;
|
|
2433
2676
|
exports.buildSchedule = buildSchedule;
|
|
2434
2677
|
exports.createBuffers = createBuffers;
|
|
2435
2678
|
exports.drumPattern = drumPattern;
|
|
@@ -2439,12 +2682,15 @@ exports.encodeWav = encodeWav;
|
|
|
2439
2682
|
exports.eventAt = eventAt;
|
|
2440
2683
|
exports.exportAudio = exportAudio;
|
|
2441
2684
|
exports.exportMidi = exportMidi;
|
|
2685
|
+
exports.exportMidiSet = exportMidiSet;
|
|
2442
2686
|
exports.glideTargets = glideTargets;
|
|
2443
2687
|
exports.graceTouches = graceTouches;
|
|
2444
2688
|
exports.instrumentSampleUrl = instrumentSampleUrl;
|
|
2445
2689
|
exports.isArcRaw = isArcRaw;
|
|
2690
|
+
exports.lyricCues = lyricCues;
|
|
2446
2691
|
exports.meendContinuations = meendContinuations;
|
|
2447
2692
|
exports.midiToSampleName = midiToSampleName;
|
|
2693
|
+
exports.mp3Stream = mp3Stream;
|
|
2448
2694
|
exports.nearestSampleKey = nearestSampleKey;
|
|
2449
2695
|
exports.nearestTablaBpm = nearestTablaBpm;
|
|
2450
2696
|
exports.normalizeBase = normalizeBase;
|