@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.
- package/LICENSE +21 -0
- package/README.md +71 -0
- package/esm/_dnt.polyfills.d.ts +11 -0
- package/esm/_dnt.polyfills.js +15 -0
- package/esm/_dnt.shims.d.ts +5 -0
- package/esm/_dnt.shims.js +61 -0
- package/esm/cli.d.ts +2 -0
- package/esm/cli.js +59 -0
- package/esm/deps/jsr.io/@std/cli/1.0.32/_data.d.ts +8 -0
- package/esm/deps/jsr.io/@std/cli/1.0.32/_data.js +17 -0
- package/esm/deps/jsr.io/@std/cli/1.0.32/_run_length.d.ts +8 -0
- package/esm/deps/jsr.io/@std/cli/1.0.32/_run_length.js +34 -0
- package/esm/deps/jsr.io/@std/cli/1.0.32/mod.d.ts +17 -0
- package/esm/deps/jsr.io/@std/cli/1.0.32/mod.js +18 -0
- package/esm/deps/jsr.io/@std/cli/1.0.32/parse_args.d.ts +342 -0
- package/esm/deps/jsr.io/@std/cli/1.0.32/parse_args.js +372 -0
- package/esm/deps/jsr.io/@std/cli/1.0.32/prompt_secret.d.ts +27 -0
- package/esm/deps/jsr.io/@std/cli/1.0.32/prompt_secret.js +109 -0
- package/esm/deps/jsr.io/@std/cli/1.0.32/unicode_width.d.ts +39 -0
- package/esm/deps/jsr.io/@std/cli/1.0.32/unicode_width.js +74 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/_os.d.ts +1 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/_os.js +14 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/os.d.ts +2 -0
- package/esm/deps/jsr.io/@std/internal/1.0.14/os.js +5 -0
- package/esm/package.json +3 -0
- package/esm/src/mod.d.ts +3 -0
- package/esm/src/mod.js +2 -0
- package/esm/src/split.d.ts +16 -0
- package/esm/src/split.js +265 -0
- package/package.json +36 -0
- package/script/_dnt.polyfills.d.ts +11 -0
- package/script/_dnt.polyfills.js +16 -0
- package/script/_dnt.shims.d.ts +5 -0
- package/script/_dnt.shims.js +65 -0
- package/script/package.json +3 -0
- package/script/src/mod.d.ts +3 -0
- package/script/src/mod.js +7 -0
- package/script/src/split.d.ts +16 -0
- 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
|
+
}
|