@marmooo/soundfont-split 0.0.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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +71 -0
  3. package/esm/_dnt.polyfills.d.ts +11 -0
  4. package/esm/_dnt.polyfills.js +15 -0
  5. package/esm/_dnt.shims.d.ts +5 -0
  6. package/esm/_dnt.shims.js +61 -0
  7. package/esm/cli.d.ts +2 -0
  8. package/esm/cli.js +59 -0
  9. package/esm/deps/jsr.io/@std/cli/1.0.32/_data.d.ts +8 -0
  10. package/esm/deps/jsr.io/@std/cli/1.0.32/_data.js +17 -0
  11. package/esm/deps/jsr.io/@std/cli/1.0.32/_run_length.d.ts +8 -0
  12. package/esm/deps/jsr.io/@std/cli/1.0.32/_run_length.js +34 -0
  13. package/esm/deps/jsr.io/@std/cli/1.0.32/mod.d.ts +17 -0
  14. package/esm/deps/jsr.io/@std/cli/1.0.32/mod.js +18 -0
  15. package/esm/deps/jsr.io/@std/cli/1.0.32/parse_args.d.ts +342 -0
  16. package/esm/deps/jsr.io/@std/cli/1.0.32/parse_args.js +372 -0
  17. package/esm/deps/jsr.io/@std/cli/1.0.32/prompt_secret.d.ts +27 -0
  18. package/esm/deps/jsr.io/@std/cli/1.0.32/prompt_secret.js +109 -0
  19. package/esm/deps/jsr.io/@std/cli/1.0.32/unicode_width.d.ts +39 -0
  20. package/esm/deps/jsr.io/@std/cli/1.0.32/unicode_width.js +74 -0
  21. package/esm/deps/jsr.io/@std/internal/1.0.14/_os.d.ts +1 -0
  22. package/esm/deps/jsr.io/@std/internal/1.0.14/_os.js +14 -0
  23. package/esm/deps/jsr.io/@std/internal/1.0.14/os.d.ts +2 -0
  24. package/esm/deps/jsr.io/@std/internal/1.0.14/os.js +5 -0
  25. package/esm/package.json +3 -0
  26. package/esm/src/mod.d.ts +3 -0
  27. package/esm/src/mod.js +2 -0
  28. package/esm/src/split.d.ts +16 -0
  29. package/esm/src/split.js +265 -0
  30. package/package.json +36 -0
  31. package/script/_dnt.polyfills.d.ts +11 -0
  32. package/script/_dnt.polyfills.js +16 -0
  33. package/script/_dnt.shims.d.ts +5 -0
  34. package/script/_dnt.shims.js +65 -0
  35. package/script/package.json +3 -0
  36. package/script/src/mod.d.ts +3 -0
  37. package/script/src/mod.js +7 -0
  38. package/script/src/split.d.ts +16 -0
  39. package/script/src/split.js +302 -0
@@ -0,0 +1,302 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.extractPreset = extractPreset;
37
+ exports.splitSoundFont = splitSoundFont;
38
+ const dntShim = __importStar(require("../_dnt.shims.js"));
39
+ const soundfont_1 = require("@marmooo/soundfont");
40
+ const encoder_1 = require("@marmooo/sf3-codec/encoder");
41
+ const decoder_1 = require("@marmooo/sf3-codec/decoder");
42
+ function cloneGenerator(g) {
43
+ if (g.value instanceof soundfont_1.RangeValue) {
44
+ return new soundfont_1.GeneratorList(g.code, new soundfont_1.RangeValue(g.value.lo, g.value.hi));
45
+ }
46
+ return new soundfont_1.GeneratorList(g.code, g.value);
47
+ }
48
+ function cloneModulator(m) {
49
+ return new soundfont_1.ModulatorList(m.sourceOper, m.destinationOper, m.amount, m.amountSourceOper, m.transOper);
50
+ }
51
+ function isTerminalMod(m) {
52
+ return (m.destinationOper === 0 &&
53
+ m.amount === 0 &&
54
+ m.transOper === 0 &&
55
+ m.sourceOper.toValue() === 0 &&
56
+ m.amountSourceOper.toValue() === 0);
57
+ }
58
+ function defaultConcurrency() {
59
+ const nav = dntShim.dntGlobalThis.navigator;
60
+ return nav?.hardwareConcurrency ?? 4;
61
+ }
62
+ // Extract a single preset (by index into soundFont.presetHeaders) into a
63
+ // minimal SoundFont that contains only the instruments and samples that
64
+ // preset depends on. Indices are remapped so the result is a valid
65
+ // standalone SF2/SF3.
66
+ function extractPreset(soundFont, presetHeaderIndex) {
67
+ const headers = soundFont.presetHeaders;
68
+ if (presetHeaderIndex < 0 ||
69
+ presetHeaderIndex >= headers.length ||
70
+ headers[presetHeaderIndex].isEnd) {
71
+ throw new Error(`invalid presetHeaderIndex: ${presetHeaderIndex}`);
72
+ }
73
+ const ph = headers[presetHeaderIndex];
74
+ const nextPh = headers[presetHeaderIndex + 1];
75
+ // Zones for this preset: bags [ph.presetBagIndex, nextBagIndex)
76
+ // The terminal bag that bounds the last zone is not a zone itself.
77
+ const bagFrom = ph.presetBagIndex;
78
+ const bagTo = nextPh
79
+ ? nextPh.presetBagIndex
80
+ : soundFont.presetZone.length - 1;
81
+ const instrumentIdSet = new Set();
82
+ const presetZoneSlice = [];
83
+ const presetGenSlice = [];
84
+ const presetModSlice = [];
85
+ let genCursor = 0;
86
+ let modCursor = 0;
87
+ for (let zi = bagFrom; zi < bagTo; zi++) {
88
+ const bag = soundFont.presetZone[zi];
89
+ const nextBag = soundFont.presetZone[zi + 1];
90
+ if (!nextBag)
91
+ break;
92
+ const gens = soundFont.presetGenerators
93
+ .slice(bag.generatorIndex, nextBag.generatorIndex)
94
+ .map(cloneGenerator);
95
+ const mods = soundFont.presetModulators
96
+ .slice(bag.modulatorIndex, nextBag.modulatorIndex)
97
+ .map(cloneModulator);
98
+ for (const g of gens) {
99
+ if (g.type === "instrument" && typeof g.value === "number") {
100
+ instrumentIdSet.add(g.value);
101
+ }
102
+ }
103
+ presetZoneSlice.push(new soundfont_1.Bag(genCursor, modCursor));
104
+ presetGenSlice.push(...gens);
105
+ presetModSlice.push(...mods);
106
+ genCursor += gens.length;
107
+ modCursor += mods.length;
108
+ }
109
+ // Terminal bag for preset zones
110
+ presetZoneSlice.push(new soundfont_1.Bag(genCursor, modCursor));
111
+ if (presetModSlice.length === 0 ||
112
+ !isTerminalMod(presetModSlice[presetModSlice.length - 1])) {
113
+ presetModSlice.push(soundfont_1.ModulatorList.end());
114
+ }
115
+ if (presetGenSlice.length === 0 ||
116
+ !presetGenSlice[presetGenSlice.length - 1].isEnd) {
117
+ presetGenSlice.push(soundfont_1.GeneratorList.end());
118
+ }
119
+ const instrumentIds = [...instrumentIdSet].sort((a, b) => a - b);
120
+ const instMap = new Map();
121
+ instrumentIds.forEach((id, i) => instMap.set(id, i));
122
+ for (const g of presetGenSlice) {
123
+ if (g.type === "instrument" && typeof g.value === "number") {
124
+ const mapped = instMap.get(g.value);
125
+ if (mapped === undefined) {
126
+ throw new Error(`instrument ${g.value} not collected`);
127
+ }
128
+ g.value = mapped;
129
+ }
130
+ }
131
+ const sampleIdSet = new Set();
132
+ const instruments = [];
133
+ const instrumentZone = [];
134
+ const instrumentGenerators = [];
135
+ const instrumentModulators = [];
136
+ let iGenCursor = 0;
137
+ let iModCursor = 0;
138
+ for (const oldInstId of instrumentIds) {
139
+ const inst = soundFont.instruments[oldInstId];
140
+ const nextInst = soundFont.instruments[oldInstId + 1];
141
+ const iBagFrom = inst.instrumentBagIndex;
142
+ const iBagTo = nextInst
143
+ ? nextInst.instrumentBagIndex
144
+ : soundFont.instrumentZone.length - 1;
145
+ const newInst = new soundfont_1.Instrument();
146
+ newInst.instrumentName = inst.instrumentName;
147
+ newInst.instrumentBagIndex = instrumentZone.length;
148
+ instruments.push(newInst);
149
+ for (let zi = iBagFrom; zi < iBagTo; zi++) {
150
+ const bag = soundFont.instrumentZone[zi];
151
+ const nextBag = soundFont.instrumentZone[zi + 1];
152
+ if (!nextBag)
153
+ break;
154
+ const gens = soundFont.instrumentGenerators
155
+ .slice(bag.generatorIndex, nextBag.generatorIndex)
156
+ .map(cloneGenerator);
157
+ const mods = soundFont.instrumentModulators
158
+ .slice(bag.modulatorIndex, nextBag.modulatorIndex)
159
+ .map(cloneModulator);
160
+ for (const g of gens) {
161
+ if (g.type === "sampleID" && typeof g.value === "number") {
162
+ sampleIdSet.add(g.value);
163
+ const sh = soundFont.sampleHeaders[g.value];
164
+ if (sh && sh.sampleLink !== 0 && !sh.isEnd) {
165
+ sampleIdSet.add(sh.sampleLink);
166
+ }
167
+ }
168
+ }
169
+ instrumentZone.push(new soundfont_1.Bag(iGenCursor, iModCursor));
170
+ instrumentGenerators.push(...gens);
171
+ instrumentModulators.push(...mods);
172
+ iGenCursor += gens.length;
173
+ iModCursor += mods.length;
174
+ }
175
+ }
176
+ instrumentZone.push(new soundfont_1.Bag(iGenCursor, iModCursor));
177
+ if (instrumentModulators.length === 0 ||
178
+ !isTerminalMod(instrumentModulators[instrumentModulators.length - 1])) {
179
+ instrumentModulators.push(soundfont_1.ModulatorList.end());
180
+ }
181
+ if (instrumentGenerators.length === 0 ||
182
+ !instrumentGenerators[instrumentGenerators.length - 1].isEnd) {
183
+ instrumentGenerators.push(soundfont_1.GeneratorList.end());
184
+ }
185
+ const sampleIds = [...sampleIdSet].sort((a, b) => a - b);
186
+ const sampleMap = new Map();
187
+ sampleIds.forEach((id, i) => sampleMap.set(id, i));
188
+ for (const g of instrumentGenerators) {
189
+ if (g.type === "sampleID" && typeof g.value === "number") {
190
+ const mapped = sampleMap.get(g.value);
191
+ if (mapped === undefined) {
192
+ throw new Error(`sample ${g.value} not collected`);
193
+ }
194
+ g.value = mapped;
195
+ }
196
+ }
197
+ const sampleHeaders = [];
198
+ const samples = [];
199
+ for (const oldId of sampleIds) {
200
+ const sh = soundFont.sampleHeaders[oldId];
201
+ const sample = soundFont.samples[oldId];
202
+ const newLink = sh.sampleLink !== 0 && sampleMap.has(sh.sampleLink)
203
+ ? sampleMap.get(sh.sampleLink)
204
+ : 0;
205
+ sampleHeaders.push(new soundfont_1.SampleHeader(sh.sampleName, sh.start, sh.end, sh.loopStart, sh.loopEnd, sh.sampleRate, sh.originalPitch, sh.pitchCorrection, newLink, sh.sampleType));
206
+ samples.push(sample);
207
+ }
208
+ const newPh = new soundfont_1.PresetHeader(ph.presetName, ph.preset, ph.bank, 0, ph.library, ph.genre, ph.morphology);
209
+ return new soundfont_1.SoundFont({
210
+ presetHeaders: [newPh],
211
+ presetZone: presetZoneSlice,
212
+ presetModulators: presetModSlice,
213
+ presetGenerators: presetGenSlice,
214
+ instruments,
215
+ instrumentZone,
216
+ instrumentModulators,
217
+ instrumentGenerators,
218
+ sampleHeaders,
219
+ samples,
220
+ samplingData: soundFont.samplingData,
221
+ info: soundFont.info,
222
+ });
223
+ }
224
+ // Split every preset in the soundfont into its own file under outDir:
225
+ // outDir/{bank:03d}/{preset:03d}.sf3
226
+ //
227
+ // Presets are processed in parallel. Sample encodes share a single encoder
228
+ // pool sized by `options.concurrency` (so the budget is global, not
229
+ // per-preset).
230
+ async function splitSoundFont(input, outDir, options = {}) {
231
+ const bytes = typeof input === "string" ? dntShim.Deno.readFileSync(input) : input;
232
+ const soundFont = (0, soundfont_1.parse)(bytes);
233
+ const toSf3 = options.toSf3 !== false;
234
+ const concurrency = Math.max(1, options.concurrency ?? defaultConcurrency());
235
+ await dntShim.Deno.mkdir(outDir, { recursive: true });
236
+ // Shared encoder pool for the whole job. Callers that supply their own
237
+ // encode via sf2ToSf3 are not used here - we own the pool so we can dispose
238
+ // it and let the process exit (workers otherwise keep the event loop alive).
239
+ let encode;
240
+ let decode;
241
+ if (toSf3) {
242
+ encode = (0, encoder_1.createDefaultEncoder)({
243
+ quality: options.quality,
244
+ poolSize: concurrency,
245
+ });
246
+ if (options.recompress) {
247
+ decode = (0, decoder_1.createDefaultDecoder)({ poolSize: concurrency });
248
+ }
249
+ }
250
+ try {
251
+ const headers = soundFont.presetHeaders;
252
+ const jobs = [];
253
+ for (let i = 0; i < headers.length; i++) {
254
+ if (!headers[i].isEnd)
255
+ jobs.push({ index: i, ph: headers[i] });
256
+ }
257
+ // Cap how many presets we assemble at once (encode pool is the real
258
+ // limiter; this just bounds peak memory from parallel write()s).
259
+ const presetParallel = concurrency;
260
+ const results = new Array(jobs.length);
261
+ let nextJob = 0;
262
+ const runWorker = async () => {
263
+ while (true) {
264
+ const jobIndex = nextJob++;
265
+ if (jobIndex >= jobs.length)
266
+ return;
267
+ const { index, ph } = jobs[jobIndex];
268
+ const extracted = extractPreset(soundFont, index);
269
+ const bankDir = `${outDir}/${String(ph.bank).padStart(3, "0")}`;
270
+ await dntShim.Deno.mkdir(bankDir, { recursive: true });
271
+ const ext = toSf3 ? "sf3" : "sf2";
272
+ const fileName = `${String(ph.preset).padStart(3, "0")}.${ext}`;
273
+ const path = `${bankDir}/${fileName}`;
274
+ const outBytes = toSf3
275
+ ? await (0, soundfont_1.write)(extracted, {
276
+ // write() may schedule many encodes; the pool caps actual work.
277
+ encode: encode,
278
+ decode,
279
+ concurrency,
280
+ })
281
+ : await (0, soundfont_1.write)(extracted);
282
+ dntShim.Deno.writeFileSync(path, outBytes);
283
+ const presetName = ph.presetName.replace(/\0+$/, "").trim();
284
+ results[jobIndex] = {
285
+ bank: ph.bank,
286
+ preset: ph.preset,
287
+ presetName,
288
+ path,
289
+ byteLength: outBytes.byteLength,
290
+ };
291
+ console.log(`wrote ${path} (${outBytes.byteLength} bytes) - ${presetName}`);
292
+ }
293
+ };
294
+ const workers = Array.from({ length: Math.min(presetParallel, jobs.length) }, () => runWorker());
295
+ await Promise.all(workers);
296
+ return results;
297
+ }
298
+ finally {
299
+ encode?.dispose?.();
300
+ decode?.dispose?.();
301
+ }
302
+ }