@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,265 @@
1
+ import * as dntShim from "../_dnt.shims.js";
2
+ import { Bag, GeneratorList, Instrument, ModulatorList, parse, PresetHeader, RangeValue, SampleHeader, SoundFont, write, } from "@marmooo/soundfont";
3
+ import { createDefaultEncoder } from "@marmooo/sf3-codec/encoder";
4
+ import { createDefaultDecoder } from "@marmooo/sf3-codec/decoder";
5
+ function cloneGenerator(g) {
6
+ if (g.value instanceof RangeValue) {
7
+ return new GeneratorList(g.code, new RangeValue(g.value.lo, g.value.hi));
8
+ }
9
+ return new GeneratorList(g.code, g.value);
10
+ }
11
+ function cloneModulator(m) {
12
+ return new ModulatorList(m.sourceOper, m.destinationOper, m.amount, m.amountSourceOper, m.transOper);
13
+ }
14
+ function isTerminalMod(m) {
15
+ return (m.destinationOper === 0 &&
16
+ m.amount === 0 &&
17
+ m.transOper === 0 &&
18
+ m.sourceOper.toValue() === 0 &&
19
+ m.amountSourceOper.toValue() === 0);
20
+ }
21
+ function defaultConcurrency() {
22
+ const nav = dntShim.dntGlobalThis.navigator;
23
+ return nav?.hardwareConcurrency ?? 4;
24
+ }
25
+ // Extract a single preset (by index into soundFont.presetHeaders) into a
26
+ // minimal SoundFont that contains only the instruments and samples that
27
+ // preset depends on. Indices are remapped so the result is a valid
28
+ // standalone SF2/SF3.
29
+ export function extractPreset(soundFont, presetHeaderIndex) {
30
+ const headers = soundFont.presetHeaders;
31
+ if (presetHeaderIndex < 0 ||
32
+ presetHeaderIndex >= headers.length ||
33
+ headers[presetHeaderIndex].isEnd) {
34
+ throw new Error(`invalid presetHeaderIndex: ${presetHeaderIndex}`);
35
+ }
36
+ const ph = headers[presetHeaderIndex];
37
+ const nextPh = headers[presetHeaderIndex + 1];
38
+ // Zones for this preset: bags [ph.presetBagIndex, nextBagIndex)
39
+ // The terminal bag that bounds the last zone is not a zone itself.
40
+ const bagFrom = ph.presetBagIndex;
41
+ const bagTo = nextPh
42
+ ? nextPh.presetBagIndex
43
+ : soundFont.presetZone.length - 1;
44
+ const instrumentIdSet = new Set();
45
+ const presetZoneSlice = [];
46
+ const presetGenSlice = [];
47
+ const presetModSlice = [];
48
+ let genCursor = 0;
49
+ let modCursor = 0;
50
+ for (let zi = bagFrom; zi < bagTo; zi++) {
51
+ const bag = soundFont.presetZone[zi];
52
+ const nextBag = soundFont.presetZone[zi + 1];
53
+ if (!nextBag)
54
+ break;
55
+ const gens = soundFont.presetGenerators
56
+ .slice(bag.generatorIndex, nextBag.generatorIndex)
57
+ .map(cloneGenerator);
58
+ const mods = soundFont.presetModulators
59
+ .slice(bag.modulatorIndex, nextBag.modulatorIndex)
60
+ .map(cloneModulator);
61
+ for (const g of gens) {
62
+ if (g.type === "instrument" && typeof g.value === "number") {
63
+ instrumentIdSet.add(g.value);
64
+ }
65
+ }
66
+ presetZoneSlice.push(new Bag(genCursor, modCursor));
67
+ presetGenSlice.push(...gens);
68
+ presetModSlice.push(...mods);
69
+ genCursor += gens.length;
70
+ modCursor += mods.length;
71
+ }
72
+ // Terminal bag for preset zones
73
+ presetZoneSlice.push(new Bag(genCursor, modCursor));
74
+ if (presetModSlice.length === 0 ||
75
+ !isTerminalMod(presetModSlice[presetModSlice.length - 1])) {
76
+ presetModSlice.push(ModulatorList.end());
77
+ }
78
+ if (presetGenSlice.length === 0 ||
79
+ !presetGenSlice[presetGenSlice.length - 1].isEnd) {
80
+ presetGenSlice.push(GeneratorList.end());
81
+ }
82
+ const instrumentIds = [...instrumentIdSet].sort((a, b) => a - b);
83
+ const instMap = new Map();
84
+ instrumentIds.forEach((id, i) => instMap.set(id, i));
85
+ for (const g of presetGenSlice) {
86
+ if (g.type === "instrument" && typeof g.value === "number") {
87
+ const mapped = instMap.get(g.value);
88
+ if (mapped === undefined) {
89
+ throw new Error(`instrument ${g.value} not collected`);
90
+ }
91
+ g.value = mapped;
92
+ }
93
+ }
94
+ const sampleIdSet = new Set();
95
+ const instruments = [];
96
+ const instrumentZone = [];
97
+ const instrumentGenerators = [];
98
+ const instrumentModulators = [];
99
+ let iGenCursor = 0;
100
+ let iModCursor = 0;
101
+ for (const oldInstId of instrumentIds) {
102
+ const inst = soundFont.instruments[oldInstId];
103
+ const nextInst = soundFont.instruments[oldInstId + 1];
104
+ const iBagFrom = inst.instrumentBagIndex;
105
+ const iBagTo = nextInst
106
+ ? nextInst.instrumentBagIndex
107
+ : soundFont.instrumentZone.length - 1;
108
+ const newInst = new Instrument();
109
+ newInst.instrumentName = inst.instrumentName;
110
+ newInst.instrumentBagIndex = instrumentZone.length;
111
+ instruments.push(newInst);
112
+ for (let zi = iBagFrom; zi < iBagTo; zi++) {
113
+ const bag = soundFont.instrumentZone[zi];
114
+ const nextBag = soundFont.instrumentZone[zi + 1];
115
+ if (!nextBag)
116
+ break;
117
+ const gens = soundFont.instrumentGenerators
118
+ .slice(bag.generatorIndex, nextBag.generatorIndex)
119
+ .map(cloneGenerator);
120
+ const mods = soundFont.instrumentModulators
121
+ .slice(bag.modulatorIndex, nextBag.modulatorIndex)
122
+ .map(cloneModulator);
123
+ for (const g of gens) {
124
+ if (g.type === "sampleID" && typeof g.value === "number") {
125
+ sampleIdSet.add(g.value);
126
+ const sh = soundFont.sampleHeaders[g.value];
127
+ if (sh && sh.sampleLink !== 0 && !sh.isEnd) {
128
+ sampleIdSet.add(sh.sampleLink);
129
+ }
130
+ }
131
+ }
132
+ instrumentZone.push(new Bag(iGenCursor, iModCursor));
133
+ instrumentGenerators.push(...gens);
134
+ instrumentModulators.push(...mods);
135
+ iGenCursor += gens.length;
136
+ iModCursor += mods.length;
137
+ }
138
+ }
139
+ instrumentZone.push(new Bag(iGenCursor, iModCursor));
140
+ if (instrumentModulators.length === 0 ||
141
+ !isTerminalMod(instrumentModulators[instrumentModulators.length - 1])) {
142
+ instrumentModulators.push(ModulatorList.end());
143
+ }
144
+ if (instrumentGenerators.length === 0 ||
145
+ !instrumentGenerators[instrumentGenerators.length - 1].isEnd) {
146
+ instrumentGenerators.push(GeneratorList.end());
147
+ }
148
+ const sampleIds = [...sampleIdSet].sort((a, b) => a - b);
149
+ const sampleMap = new Map();
150
+ sampleIds.forEach((id, i) => sampleMap.set(id, i));
151
+ for (const g of instrumentGenerators) {
152
+ if (g.type === "sampleID" && typeof g.value === "number") {
153
+ const mapped = sampleMap.get(g.value);
154
+ if (mapped === undefined) {
155
+ throw new Error(`sample ${g.value} not collected`);
156
+ }
157
+ g.value = mapped;
158
+ }
159
+ }
160
+ const sampleHeaders = [];
161
+ const samples = [];
162
+ for (const oldId of sampleIds) {
163
+ const sh = soundFont.sampleHeaders[oldId];
164
+ const sample = soundFont.samples[oldId];
165
+ const newLink = sh.sampleLink !== 0 && sampleMap.has(sh.sampleLink)
166
+ ? sampleMap.get(sh.sampleLink)
167
+ : 0;
168
+ sampleHeaders.push(new SampleHeader(sh.sampleName, sh.start, sh.end, sh.loopStart, sh.loopEnd, sh.sampleRate, sh.originalPitch, sh.pitchCorrection, newLink, sh.sampleType));
169
+ samples.push(sample);
170
+ }
171
+ const newPh = new PresetHeader(ph.presetName, ph.preset, ph.bank, 0, ph.library, ph.genre, ph.morphology);
172
+ return new SoundFont({
173
+ presetHeaders: [newPh],
174
+ presetZone: presetZoneSlice,
175
+ presetModulators: presetModSlice,
176
+ presetGenerators: presetGenSlice,
177
+ instruments,
178
+ instrumentZone,
179
+ instrumentModulators,
180
+ instrumentGenerators,
181
+ sampleHeaders,
182
+ samples,
183
+ samplingData: soundFont.samplingData,
184
+ info: soundFont.info,
185
+ });
186
+ }
187
+ // Split every preset in the soundfont into its own file under outDir:
188
+ // outDir/{bank:03d}/{preset:03d}.sf3
189
+ //
190
+ // Presets are processed in parallel. Sample encodes share a single encoder
191
+ // pool sized by `options.concurrency` (so the budget is global, not
192
+ // per-preset).
193
+ export async function splitSoundFont(input, outDir, options = {}) {
194
+ const bytes = typeof input === "string" ? dntShim.Deno.readFileSync(input) : input;
195
+ const soundFont = parse(bytes);
196
+ const toSf3 = options.toSf3 !== false;
197
+ const concurrency = Math.max(1, options.concurrency ?? defaultConcurrency());
198
+ await dntShim.Deno.mkdir(outDir, { recursive: true });
199
+ // Shared encoder pool for the whole job. Callers that supply their own
200
+ // encode via sf2ToSf3 are not used here - we own the pool so we can dispose
201
+ // it and let the process exit (workers otherwise keep the event loop alive).
202
+ let encode;
203
+ let decode;
204
+ if (toSf3) {
205
+ encode = createDefaultEncoder({
206
+ quality: options.quality,
207
+ poolSize: concurrency,
208
+ });
209
+ if (options.recompress) {
210
+ decode = createDefaultDecoder({ poolSize: concurrency });
211
+ }
212
+ }
213
+ try {
214
+ const headers = soundFont.presetHeaders;
215
+ const jobs = [];
216
+ for (let i = 0; i < headers.length; i++) {
217
+ if (!headers[i].isEnd)
218
+ jobs.push({ index: i, ph: headers[i] });
219
+ }
220
+ // Cap how many presets we assemble at once (encode pool is the real
221
+ // limiter; this just bounds peak memory from parallel write()s).
222
+ const presetParallel = concurrency;
223
+ const results = new Array(jobs.length);
224
+ let nextJob = 0;
225
+ const runWorker = async () => {
226
+ while (true) {
227
+ const jobIndex = nextJob++;
228
+ if (jobIndex >= jobs.length)
229
+ return;
230
+ const { index, ph } = jobs[jobIndex];
231
+ const extracted = extractPreset(soundFont, index);
232
+ const bankDir = `${outDir}/${String(ph.bank).padStart(3, "0")}`;
233
+ await dntShim.Deno.mkdir(bankDir, { recursive: true });
234
+ const ext = toSf3 ? "sf3" : "sf2";
235
+ const fileName = `${String(ph.preset).padStart(3, "0")}.${ext}`;
236
+ const path = `${bankDir}/${fileName}`;
237
+ const outBytes = toSf3
238
+ ? await write(extracted, {
239
+ // write() may schedule many encodes; the pool caps actual work.
240
+ encode: encode,
241
+ decode,
242
+ concurrency,
243
+ })
244
+ : await write(extracted);
245
+ dntShim.Deno.writeFileSync(path, outBytes);
246
+ const presetName = ph.presetName.replace(/\0+$/, "").trim();
247
+ results[jobIndex] = {
248
+ bank: ph.bank,
249
+ preset: ph.preset,
250
+ presetName,
251
+ path,
252
+ byteLength: outBytes.byteLength,
253
+ };
254
+ console.log(`wrote ${path} (${outBytes.byteLength} bytes) - ${presetName}`);
255
+ }
256
+ };
257
+ const workers = Array.from({ length: Math.min(presetParallel, jobs.length) }, () => runWorker());
258
+ await Promise.all(workers);
259
+ return results;
260
+ }
261
+ finally {
262
+ encode?.dispose?.();
263
+ decode?.dispose?.();
264
+ }
265
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@marmooo/soundfont-split",
3
+ "version": "0.0.1",
4
+ "description": "Split a SoundFont (SF2 / SF3) into per-preset SF3 files.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/marmooo/soundfont-split.git"
8
+ },
9
+ "license": "MIT",
10
+ "bugs": {
11
+ "url": "https://github.com/marmooo/soundfont-split/issues"
12
+ },
13
+ "main": "./script/src/mod.js",
14
+ "module": "./esm/src/mod.js",
15
+ "exports": {
16
+ ".": {
17
+ "import": "./esm/src/mod.js",
18
+ "require": "./script/src/mod.js"
19
+ }
20
+ },
21
+ "scripts": {
22
+ "test": "node test_runner.cjs"
23
+ },
24
+ "bin": {
25
+ "soundfont-split": "./esm/cli.js"
26
+ },
27
+ "dependencies": {
28
+ "@marmooo/sf3-codec": "^0.0.2",
29
+ "@marmooo/soundfont": "^0.3.2",
30
+ "@deno/shim-deno": "~0.18.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^20.9.0"
34
+ },
35
+ "_generatedBy": "dnt@0.43.2"
36
+ }
@@ -0,0 +1,11 @@
1
+ declare global {
2
+ interface Object {
3
+ /**
4
+ * Determines whether an object has a property with the specified name.
5
+ * @param o An object.
6
+ * @param v A property name.
7
+ */
8
+ hasOwn(o: object, v: PropertyKey): boolean;
9
+ }
10
+ }
11
+ export {};
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ // https://github.com/tc39/proposal-accessible-object-hasownproperty/blob/main/polyfill.js
4
+ if (!Object.hasOwn) {
5
+ Object.defineProperty(Object, "hasOwn", {
6
+ value: function (object, property) {
7
+ if (object == null) {
8
+ throw new TypeError("Cannot convert undefined or null to object");
9
+ }
10
+ return Object.prototype.hasOwnProperty.call(Object(object), property);
11
+ },
12
+ configurable: true,
13
+ enumerable: false,
14
+ writable: true,
15
+ });
16
+ }
@@ -0,0 +1,5 @@
1
+ import { Deno } from "@deno/shim-deno";
2
+ export { Deno } from "@deno/shim-deno";
3
+ export declare const dntGlobalThis: Omit<typeof globalThis, "Deno"> & {
4
+ Deno: typeof Deno;
5
+ };
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dntGlobalThis = exports.Deno = void 0;
4
+ const shim_deno_1 = require("@deno/shim-deno");
5
+ var shim_deno_2 = require("@deno/shim-deno");
6
+ Object.defineProperty(exports, "Deno", { enumerable: true, get: function () { return shim_deno_2.Deno; } });
7
+ const dntGlobals = {
8
+ Deno: shim_deno_1.Deno,
9
+ };
10
+ exports.dntGlobalThis = createMergeProxy(globalThis, dntGlobals);
11
+ function createMergeProxy(baseObj, extObj) {
12
+ return new Proxy(baseObj, {
13
+ get(_target, prop, _receiver) {
14
+ if (prop in extObj) {
15
+ return extObj[prop];
16
+ }
17
+ else {
18
+ return baseObj[prop];
19
+ }
20
+ },
21
+ set(_target, prop, value) {
22
+ if (prop in extObj) {
23
+ delete extObj[prop];
24
+ }
25
+ baseObj[prop] = value;
26
+ return true;
27
+ },
28
+ deleteProperty(_target, prop) {
29
+ let success = false;
30
+ if (prop in extObj) {
31
+ delete extObj[prop];
32
+ success = true;
33
+ }
34
+ if (prop in baseObj) {
35
+ delete baseObj[prop];
36
+ success = true;
37
+ }
38
+ return success;
39
+ },
40
+ ownKeys(_target) {
41
+ const baseKeys = Reflect.ownKeys(baseObj);
42
+ const extKeys = Reflect.ownKeys(extObj);
43
+ const extKeysSet = new Set(extKeys);
44
+ return [...baseKeys.filter((k) => !extKeysSet.has(k)), ...extKeys];
45
+ },
46
+ defineProperty(_target, prop, desc) {
47
+ if (prop in extObj) {
48
+ delete extObj[prop];
49
+ }
50
+ Reflect.defineProperty(baseObj, prop, desc);
51
+ return true;
52
+ },
53
+ getOwnPropertyDescriptor(_target, prop) {
54
+ if (prop in extObj) {
55
+ return Reflect.getOwnPropertyDescriptor(extObj, prop);
56
+ }
57
+ else {
58
+ return Reflect.getOwnPropertyDescriptor(baseObj, prop);
59
+ }
60
+ },
61
+ has(_target, prop) {
62
+ return prop in extObj || prop in baseObj;
63
+ },
64
+ });
65
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,3 @@
1
+ import "../_dnt.polyfills.js";
2
+ export { extractPreset, splitSoundFont } from "./split.js";
3
+ export type { SplitOptions, SplitResult } from "./split.js";
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.splitSoundFont = exports.extractPreset = void 0;
4
+ require("../_dnt.polyfills.js");
5
+ var split_js_1 = require("./split.js");
6
+ Object.defineProperty(exports, "extractPreset", { enumerable: true, get: function () { return split_js_1.extractPreset; } });
7
+ Object.defineProperty(exports, "splitSoundFont", { enumerable: true, get: function () { return split_js_1.splitSoundFont; } });
@@ -0,0 +1,16 @@
1
+ import { SoundFont } from "@marmooo/soundfont";
2
+ export interface SplitOptions {
3
+ quality?: number;
4
+ concurrency?: number;
5
+ toSf3?: boolean;
6
+ recompress?: boolean;
7
+ }
8
+ export interface SplitResult {
9
+ bank: number;
10
+ preset: number;
11
+ presetName: string;
12
+ path: string;
13
+ byteLength: number;
14
+ }
15
+ export declare function extractPreset(soundFont: SoundFont, presetHeaderIndex: number): SoundFont;
16
+ export declare function splitSoundFont(input: Uint8Array | string, outDir: string, options?: SplitOptions): Promise<SplitResult[]>;