@ugfoundation/swaralipi-js 0.1.5 → 0.2.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 +21 -0
- package/dist/{batch-KKRTXF52.js → batch-CIAXVKP5.js} +5 -5
- package/dist/{batch-KKRTXF52.js.map → batch-CIAXVKP5.js.map} +1 -1
- package/dist/{chunk-HTTURJ7A.js → chunk-2NBNF6JP.js} +4 -4
- package/dist/{chunk-HTTURJ7A.js.map → chunk-2NBNF6JP.js.map} +1 -1
- package/dist/{chunk-R6QU5CXY.js → chunk-H6HC44PZ.js} +5 -3
- package/dist/chunk-H6HC44PZ.js.map +1 -0
- package/dist/{chunk-5PLGGEPK.js → chunk-SH2INDZ2.js} +3 -3
- package/dist/{chunk-5PLGGEPK.js.map → chunk-SH2INDZ2.js.map} +1 -1
- package/dist/cli.cjs +4 -2
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +3 -3
- package/dist/convert/index.cjs +4 -2
- package/dist/convert/index.cjs.map +1 -1
- package/dist/convert/index.js +3 -3
- package/dist/index.cjs +4 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/player/index.cjs +271 -1
- package/dist/player/index.cjs.map +1 -1
- package/dist/player/index.d.cts +118 -2
- package/dist/player/index.d.ts +118 -2
- package/dist/player/index.js +265 -2
- package/dist/player/index.js.map +1 -1
- package/dist/spec/index.cjs +4 -2
- package/dist/spec/index.cjs.map +1 -1
- package/dist/spec/index.d.cts +2 -2
- package/dist/spec/index.d.ts +2 -2
- package/dist/spec/index.js +2 -2
- package/dist/{taals-DZU242TR.d.cts → taals-FmEulsjY.d.cts} +7 -0
- package/dist/{taals-DZU242TR.d.ts → taals-FmEulsjY.d.ts} +7 -0
- package/dist/viewer/index.cjs +4 -2
- package/dist/viewer/index.cjs.map +1 -1
- package/package.json +10 -1
- package/dist/chunk-R6QU5CXY.js.map +0 -1
package/dist/player/index.cjs
CHANGED
|
@@ -14,7 +14,9 @@ var TAALS = {
|
|
|
14
14
|
// ষষ্ঠী
|
|
15
15
|
teora: { id: "teora", numBeats: 7, bhaags: [3, 2, 2], sam: 0 },
|
|
16
16
|
// টেওরা
|
|
17
|
-
teentaal: { id: "teentaal", numBeats: 16, bhaags: [4, 4, 4, 4], sam: 0, khaali: [8] }
|
|
17
|
+
teentaal: { id: "teentaal", numBeats: 16, bhaags: [4, 4, 4, 4], sam: 0, khaali: [8] },
|
|
18
|
+
aratheka: { id: "aratheka", numBeats: 16, bhaags: [4, 4, 4, 4], sam: 0, khaali: [8] }
|
|
19
|
+
// আড়াঠেকা
|
|
18
20
|
};
|
|
19
21
|
|
|
20
22
|
// src/player/pitch.ts
|
|
@@ -790,6 +792,269 @@ var SwaralipiPlayer = class {
|
|
|
790
792
|
}
|
|
791
793
|
};
|
|
792
794
|
|
|
795
|
+
// src/player/export.ts
|
|
796
|
+
function midiRange2(schedule) {
|
|
797
|
+
let lo = Infinity;
|
|
798
|
+
let hi = -Infinity;
|
|
799
|
+
for (const ev of schedule.events) {
|
|
800
|
+
for (const m of [ev.midi, ev.kan]) {
|
|
801
|
+
if (m !== null) {
|
|
802
|
+
if (m < lo) lo = m;
|
|
803
|
+
if (m > hi) hi = m;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return lo <= hi ? { lo, hi } : void 0;
|
|
808
|
+
}
|
|
809
|
+
function pickDef(id, o) {
|
|
810
|
+
const def = o.customInstruments?.[id] ?? INSTRUMENTS[id];
|
|
811
|
+
if (def === void 0) return INSTRUMENTS.harmonium;
|
|
812
|
+
if (id === "piano" && o.pianoBaseUrl) return { ...def, baseUrl: normalizeBase(o.pianoBaseUrl) };
|
|
813
|
+
if (def.local === true && !o.soundsBaseUrl) return INSTRUMENTS.harmonium;
|
|
814
|
+
return def;
|
|
815
|
+
}
|
|
816
|
+
function resolveTaal(doc, taal) {
|
|
817
|
+
const id = taal !== void 0 && taal !== "auto" ? taal : doc.meta.taal?.id;
|
|
818
|
+
return id !== void 0 && id in TAALS ? id : void 0;
|
|
819
|
+
}
|
|
820
|
+
function safeName(title) {
|
|
821
|
+
return (title ?? "").replace(/[\\/:*?"<>|]+/g, "").trim() || "swaralipi";
|
|
822
|
+
}
|
|
823
|
+
async function exportAudio(doc, options = {}) {
|
|
824
|
+
const Tone = await import('tone');
|
|
825
|
+
const base = options.soundsBaseUrl ?? DEFAULT_SOUNDS_BASE;
|
|
826
|
+
const format = options.format ?? "wav";
|
|
827
|
+
const channels = options.channels ?? 1;
|
|
828
|
+
const sampleRate = options.sampleRate ?? 44100;
|
|
829
|
+
const fadeOutSec = options.fadeOutSec ?? 0.4;
|
|
830
|
+
const rhythm = options.rhythm ?? "none";
|
|
831
|
+
const withMelody = options.melody ?? true;
|
|
832
|
+
const withTanpura = options.tanpura ?? false;
|
|
833
|
+
const schedule = buildSchedule(doc, { tonic: options.tonic, baseOctave: options.baseOctave });
|
|
834
|
+
const range = midiRange2(schedule);
|
|
835
|
+
const taalId = resolveTaal(doc, options.taal);
|
|
836
|
+
const variation = options.taal === void 0 || options.taal === "auto" ? doc.meta.taal?.variation : void 0;
|
|
837
|
+
const usesTabla = rhythm === "tabla" && taalId !== void 0 && SAMPLED_TAALS.has(taalId) && !!base;
|
|
838
|
+
const reqBpm = options.bpm ?? doc.meta.bpm ?? 90;
|
|
839
|
+
const bpm = usesTabla && taalId ? nearestTablaBpm(reqBpm, tablaBpms(taalId)) : reqBpm;
|
|
840
|
+
const spb = 60 / bpm;
|
|
841
|
+
const id = options.instrument ?? "piano";
|
|
842
|
+
const def = pickDef(id, options);
|
|
843
|
+
const durationSec = Math.max(schedule.totalBeats * spb + (def.release ?? 1) + fadeOutSec + 0.5, 0.5);
|
|
844
|
+
const rendered = await Tone.Offline(
|
|
845
|
+
async ({ transport }) => {
|
|
846
|
+
transport.bpm.value = bpm;
|
|
847
|
+
const master = new Tone.Gain(1).toDestination();
|
|
848
|
+
if (withMelody) {
|
|
849
|
+
const sampler = createSampler(Tone, def, base, range).connect(master);
|
|
850
|
+
for (const ev of schedule.events) {
|
|
851
|
+
if (ev.midi === null) continue;
|
|
852
|
+
const at = ev.beat * spb;
|
|
853
|
+
const dur = Math.max(ev.durationBeats * spb, 0.05);
|
|
854
|
+
if (ev.kan !== null) {
|
|
855
|
+
const name2 = midiToSampleName(ev.kan);
|
|
856
|
+
transport.scheduleOnce((t) => sampler.triggerAttackRelease(name2, spb * 0.4, t), Math.max(at - spb * 0.12, 0));
|
|
857
|
+
}
|
|
858
|
+
const name = midiToSampleName(ev.midi);
|
|
859
|
+
transport.scheduleOnce((t) => sampler.triggerAttackRelease(name, dur, t), at);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
let tabla;
|
|
863
|
+
if (rhythm !== "none" && base) {
|
|
864
|
+
if (usesTabla && taalId) {
|
|
865
|
+
const url = tablaUrl(base, taalId, bpm, variation);
|
|
866
|
+
tabla = new Tone.Player({ url, loop: true }).connect(master);
|
|
867
|
+
tabla.sync().start(0);
|
|
868
|
+
} else {
|
|
869
|
+
const metro = createMetronome(Tone, base).connect(master);
|
|
870
|
+
const cycle = taalId !== void 0 ? TAALS[taalId].numBeats : 4;
|
|
871
|
+
let beat = 0;
|
|
872
|
+
transport.scheduleRepeat((time) => {
|
|
873
|
+
metro.volume.setValueAtTime(beat % cycle === 0 ? 0 : -8, time);
|
|
874
|
+
metro.start(time);
|
|
875
|
+
beat += 1;
|
|
876
|
+
}, spb, 0);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
if (withTanpura && base) {
|
|
880
|
+
const tonicPc = (parseTonic(options.tonic ?? doc.meta.tonic) % 12 + 12) % 12;
|
|
881
|
+
const sa = TANPURA_PCS[tonicPc];
|
|
882
|
+
const pa = TANPURA_PCS[(tonicPc + 7) % 12];
|
|
883
|
+
const players = createTanpura(Tone, base, [sa, pa]).connect(master);
|
|
884
|
+
players.volume.value = -14;
|
|
885
|
+
const pattern = [pa, sa, sa, sa];
|
|
886
|
+
let i = 0;
|
|
887
|
+
transport.scheduleRepeat((time) => {
|
|
888
|
+
const stem = pattern[i % pattern.length];
|
|
889
|
+
if (players.has(stem)) players.player(stem).start(time);
|
|
890
|
+
i += 1;
|
|
891
|
+
}, spb * 2, 0);
|
|
892
|
+
}
|
|
893
|
+
await Tone.loaded();
|
|
894
|
+
if (tabla?.buffer?.loaded) tabla.loopEnd = quantizeLoopEnd(tabla.buffer.duration, spb);
|
|
895
|
+
if (fadeOutSec > 0) {
|
|
896
|
+
master.gain.setValueAtTime(1, Math.max(durationSec - fadeOutSec, 0));
|
|
897
|
+
master.gain.linearRampToValueAtTime(0, durationSec);
|
|
898
|
+
}
|
|
899
|
+
transport.start(0);
|
|
900
|
+
},
|
|
901
|
+
durationSec,
|
|
902
|
+
channels,
|
|
903
|
+
sampleRate
|
|
904
|
+
);
|
|
905
|
+
const chans = [];
|
|
906
|
+
for (let c = 0; c < rendered.numberOfChannels; c++) chans.push(rendered.getChannelData(c));
|
|
907
|
+
const bytes = format === "mp3" ? await encodeMp3(chans, rendered.sampleRate, options.mp3Kbps) : encodeWav(chans, rendered.sampleRate);
|
|
908
|
+
const mimeType = format === "mp3" ? "audio/mpeg" : "audio/wav";
|
|
909
|
+
return {
|
|
910
|
+
blob: new Blob([bytes], { type: mimeType }),
|
|
911
|
+
filename: `${safeName(doc.meta.title)}.${format}`,
|
|
912
|
+
mimeType,
|
|
913
|
+
durationSec: rendered.duration
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
function toInt16(f) {
|
|
917
|
+
const s = f < -1 ? -1 : f > 1 ? 1 : f;
|
|
918
|
+
return s < 0 ? s * 32768 : s * 32767;
|
|
919
|
+
}
|
|
920
|
+
function encodeWav(channels, sampleRate) {
|
|
921
|
+
const numCh = channels.length;
|
|
922
|
+
const frames = channels[0]?.length ?? 0;
|
|
923
|
+
const bytesPerSample = 2;
|
|
924
|
+
const blockAlign = numCh * bytesPerSample;
|
|
925
|
+
const dataSize = frames * blockAlign;
|
|
926
|
+
const buf = new ArrayBuffer(44 + dataSize);
|
|
927
|
+
const view = new DataView(buf);
|
|
928
|
+
const str = (off2, s) => {
|
|
929
|
+
for (let i = 0; i < s.length; i++) view.setUint8(off2 + i, s.charCodeAt(i));
|
|
930
|
+
};
|
|
931
|
+
str(0, "RIFF");
|
|
932
|
+
view.setUint32(4, 36 + dataSize, true);
|
|
933
|
+
str(8, "WAVE");
|
|
934
|
+
str(12, "fmt ");
|
|
935
|
+
view.setUint32(16, 16, true);
|
|
936
|
+
view.setUint16(20, 1, true);
|
|
937
|
+
view.setUint16(22, numCh, true);
|
|
938
|
+
view.setUint32(24, sampleRate, true);
|
|
939
|
+
view.setUint32(28, sampleRate * blockAlign, true);
|
|
940
|
+
view.setUint16(32, blockAlign, true);
|
|
941
|
+
view.setUint16(34, bytesPerSample * 8, true);
|
|
942
|
+
str(36, "data");
|
|
943
|
+
view.setUint32(40, dataSize, true);
|
|
944
|
+
let off = 44;
|
|
945
|
+
for (let f = 0; f < frames; f++) {
|
|
946
|
+
for (let c = 0; c < numCh; c++) {
|
|
947
|
+
view.setInt16(off, toInt16(channels[c][f]), true);
|
|
948
|
+
off += 2;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
return new Uint8Array(buf);
|
|
952
|
+
}
|
|
953
|
+
function floatToInt16(f) {
|
|
954
|
+
const out = new Int16Array(f.length);
|
|
955
|
+
for (let i = 0; i < f.length; i++) out[i] = toInt16(f[i]);
|
|
956
|
+
return out;
|
|
957
|
+
}
|
|
958
|
+
function concatBytes(chunks) {
|
|
959
|
+
const total = chunks.reduce((n, c) => n + c.length, 0);
|
|
960
|
+
const out = new Uint8Array(total);
|
|
961
|
+
let off = 0;
|
|
962
|
+
for (const c of chunks) {
|
|
963
|
+
out.set(c, off);
|
|
964
|
+
off += c.length;
|
|
965
|
+
}
|
|
966
|
+
return out;
|
|
967
|
+
}
|
|
968
|
+
async function loadLame() {
|
|
969
|
+
try {
|
|
970
|
+
const mod = await import('@breezystack/lamejs');
|
|
971
|
+
const Mp3Encoder = mod.Mp3Encoder ?? mod.default?.Mp3Encoder;
|
|
972
|
+
if (!Mp3Encoder) throw new Error("no Mp3Encoder export");
|
|
973
|
+
return Mp3Encoder;
|
|
974
|
+
} catch {
|
|
975
|
+
throw new Error(
|
|
976
|
+
'MP3 export needs the optional peer dependency "@breezystack/lamejs" \u2014 install it, or export as WAV (format: "wav").'
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
async function encodeMp3(channels, sampleRate, kbps = 192) {
|
|
981
|
+
const Mp3Encoder = await loadLame();
|
|
982
|
+
const numCh = Math.min(channels.length, 2);
|
|
983
|
+
const enc = new Mp3Encoder(numCh, sampleRate, kbps);
|
|
984
|
+
const left = floatToInt16(channels[0] ?? new Float32Array());
|
|
985
|
+
const right = numCh > 1 ? floatToInt16(channels[1]) : void 0;
|
|
986
|
+
const block = 1152;
|
|
987
|
+
const out = [];
|
|
988
|
+
for (let i = 0; i < left.length; i += block) {
|
|
989
|
+
const l = left.subarray(i, i + block);
|
|
990
|
+
const chunk = right ? enc.encodeBuffer(l, right.subarray(i, i + block)) : enc.encodeBuffer(l);
|
|
991
|
+
if (chunk.length > 0) out.push(new Uint8Array(chunk));
|
|
992
|
+
}
|
|
993
|
+
const tail = enc.flush();
|
|
994
|
+
if (tail.length > 0) out.push(new Uint8Array(tail));
|
|
995
|
+
return concatBytes(out);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// src/player/midi.ts
|
|
999
|
+
var PPQ = 480;
|
|
1000
|
+
function vlq(n) {
|
|
1001
|
+
const out = [n & 127];
|
|
1002
|
+
n >>>= 7;
|
|
1003
|
+
while (n > 0) {
|
|
1004
|
+
out.unshift(n & 127 | 128);
|
|
1005
|
+
n >>>= 7;
|
|
1006
|
+
}
|
|
1007
|
+
return out;
|
|
1008
|
+
}
|
|
1009
|
+
function utf8(s) {
|
|
1010
|
+
return [...new TextEncoder().encode(s)];
|
|
1011
|
+
}
|
|
1012
|
+
function u32be(n) {
|
|
1013
|
+
return [n >>> 24 & 255, n >>> 16 & 255, n >>> 8 & 255, n & 255];
|
|
1014
|
+
}
|
|
1015
|
+
function encodeMidi(schedule, opts) {
|
|
1016
|
+
const raw = [];
|
|
1017
|
+
for (const ev of schedule.events) {
|
|
1018
|
+
if (ev.midi === null) continue;
|
|
1019
|
+
const start = Math.round(ev.beat * PPQ);
|
|
1020
|
+
const end = Math.max(Math.round((ev.beat + ev.durationBeats) * PPQ), start + 1);
|
|
1021
|
+
if (ev.kan !== null) {
|
|
1022
|
+
const kOn = Math.max(start - Math.round(0.12 * PPQ), 0);
|
|
1023
|
+
raw.push({ tick: kOn, on: true, note: ev.kan, vel: 70 });
|
|
1024
|
+
raw.push({ tick: start, on: false, note: ev.kan, vel: 0 });
|
|
1025
|
+
}
|
|
1026
|
+
raw.push({ tick: start, on: true, note: ev.midi, vel: 100 });
|
|
1027
|
+
raw.push({ tick: end, on: false, note: ev.midi, vel: 0 });
|
|
1028
|
+
}
|
|
1029
|
+
raw.sort((a, b) => a.tick - b.tick || Number(a.on) - Number(b.on));
|
|
1030
|
+
const track = [];
|
|
1031
|
+
if (opts.name) track.push(...vlq(0), 255, 3, ...vlq(utf8(opts.name).length), ...utf8(opts.name));
|
|
1032
|
+
const usPerBeat = Math.round(6e7 / opts.bpm);
|
|
1033
|
+
track.push(...vlq(0), 255, 81, 3, usPerBeat >> 16 & 255, usPerBeat >> 8 & 255, usPerBeat & 255);
|
|
1034
|
+
let last = 0;
|
|
1035
|
+
for (const e of raw) {
|
|
1036
|
+
track.push(...vlq(e.tick - last), e.on ? 144 : 128, e.note & 127, e.vel & 127);
|
|
1037
|
+
last = e.tick;
|
|
1038
|
+
}
|
|
1039
|
+
track.push(...vlq(0), 255, 47, 0);
|
|
1040
|
+
const header = [77, 84, 104, 100, ...u32be(6), 0, 0, 0, 1, PPQ >> 8 & 255, PPQ & 255];
|
|
1041
|
+
const trackChunk = [77, 84, 114, 107, ...u32be(track.length), ...track];
|
|
1042
|
+
return new Uint8Array([...header, ...trackChunk]);
|
|
1043
|
+
}
|
|
1044
|
+
function safeName2(title) {
|
|
1045
|
+
return (title ?? "").replace(/[\\/:*?"<>|]+/g, "").trim() || "swaralipi";
|
|
1046
|
+
}
|
|
1047
|
+
function exportMidi(doc, options = {}) {
|
|
1048
|
+
const schedule = buildSchedule(doc, { tonic: options.tonic, baseOctave: options.baseOctave });
|
|
1049
|
+
const bpm = options.bpm ?? doc.meta.bpm ?? 90;
|
|
1050
|
+
const bytes = encodeMidi(schedule, { bpm, name: options.name ?? doc.meta.title });
|
|
1051
|
+
return {
|
|
1052
|
+
blob: new Blob([bytes], { type: "audio/midi" }),
|
|
1053
|
+
filename: `${safeName2(doc.meta.title)}.mid`,
|
|
1054
|
+
mimeType: "audio/midi"
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
|
|
793
1058
|
exports.DEFAULT_SOUNDS_BASE = DEFAULT_SOUNDS_BASE;
|
|
794
1059
|
exports.INSTRUMENTS = INSTRUMENTS;
|
|
795
1060
|
exports.OCTAVE_OFFSET = OCTAVE_OFFSET;
|
|
@@ -801,6 +1066,11 @@ exports.TABLA_BPMS = TABLA_BPMS;
|
|
|
801
1066
|
exports.TABLA_VARIATIONS = TABLA_VARIATIONS;
|
|
802
1067
|
exports.TONEJS_INSTRUMENTS_BASE = TONEJS_INSTRUMENTS_BASE;
|
|
803
1068
|
exports.buildSchedule = buildSchedule;
|
|
1069
|
+
exports.encodeMidi = encodeMidi;
|
|
1070
|
+
exports.encodeMp3 = encodeMp3;
|
|
1071
|
+
exports.encodeWav = encodeWav;
|
|
1072
|
+
exports.exportAudio = exportAudio;
|
|
1073
|
+
exports.exportMidi = exportMidi;
|
|
804
1074
|
exports.instrumentSampleUrl = instrumentSampleUrl;
|
|
805
1075
|
exports.midiToSampleName = midiToSampleName;
|
|
806
1076
|
exports.nearestTablaBpm = nearestTablaBpm;
|