@opendaw/studio-core-wasm 0.0.2

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 (79) hide show
  1. package/dist/WasmEngine.d.ts +14 -0
  2. package/dist/WasmEngine.d.ts.map +1 -0
  3. package/dist/WasmEngine.js +152 -0
  4. package/dist/boot.d.ts +14 -0
  5. package/dist/boot.d.ts.map +1 -0
  6. package/dist/boot.js +91 -0
  7. package/dist/device-linker.d.ts +47 -0
  8. package/dist/device-linker.d.ts.map +1 -0
  9. package/dist/device-linker.js +109 -0
  10. package/dist/engine-exports.d.ts +86 -0
  11. package/dist/engine-exports.d.ts.map +1 -0
  12. package/dist/engine-exports.js +13 -0
  13. package/dist/engine-modules.d.ts +26 -0
  14. package/dist/engine-modules.d.ts.map +1 -0
  15. package/dist/engine-modules.js +62 -0
  16. package/dist/index.d.ts +5 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +6 -0
  19. package/dist/nam-bridge.d.ts +7 -0
  20. package/dist/nam-bridge.d.ts.map +1 -0
  21. package/dist/nam-bridge.js +265 -0
  22. package/dist/nam-loader.d.ts +4 -0
  23. package/dist/nam-loader.d.ts.map +1 -0
  24. package/dist/nam-loader.js +1 -0
  25. package/dist/protocol.d.ts +22 -0
  26. package/dist/protocol.d.ts.map +1 -0
  27. package/dist/protocol.js +3 -0
  28. package/dist/script-bridge.d.ts +11 -0
  29. package/dist/script-bridge.d.ts.map +1 -0
  30. package/dist/script-bridge.js +343 -0
  31. package/dist/script-spielwerk.d.ts +24 -0
  32. package/dist/script-spielwerk.d.ts.map +1 -0
  33. package/dist/script-spielwerk.js +179 -0
  34. package/dist/soundfont-simplify.d.ts +28 -0
  35. package/dist/soundfont-simplify.d.ts.map +1 -0
  36. package/dist/soundfont-simplify.js +143 -0
  37. package/dist/sync/loopback.d.ts +8 -0
  38. package/dist/sync/loopback.d.ts.map +1 -0
  39. package/dist/sync/loopback.js +23 -0
  40. package/dist/sync/serialize-update-tasks.d.ts +4 -0
  41. package/dist/sync/serialize-update-tasks.d.ts.map +1 -0
  42. package/dist/sync/serialize-update-tasks.js +40 -0
  43. package/dist/utf8.d.ts +2 -0
  44. package/dist/utf8.d.ts.map +1 -0
  45. package/dist/utf8.js +19 -0
  46. package/dist/wasm/engine.wasm +0 -0
  47. package/dist/wasm/plugins/device_apparat.wasm +0 -0
  48. package/dist/wasm/plugins/device_arpeggio.wasm +0 -0
  49. package/dist/wasm/plugins/device_compressor.wasm +0 -0
  50. package/dist/wasm/plugins/device_crusher.wasm +0 -0
  51. package/dist/wasm/plugins/device_dattorro_reverb.wasm +0 -0
  52. package/dist/wasm/plugins/device_delay.wasm +0 -0
  53. package/dist/wasm/plugins/device_fold.wasm +0 -0
  54. package/dist/wasm/plugins/device_gate.wasm +0 -0
  55. package/dist/wasm/plugins/device_maximizer.wasm +0 -0
  56. package/dist/wasm/plugins/device_nano.wasm +0 -0
  57. package/dist/wasm/plugins/device_neural_amp.wasm +0 -0
  58. package/dist/wasm/plugins/device_pitch.wasm +0 -0
  59. package/dist/wasm/plugins/device_playfield_sample.wasm +0 -0
  60. package/dist/wasm/plugins/device_revamp.wasm +0 -0
  61. package/dist/wasm/plugins/device_reverb.wasm +0 -0
  62. package/dist/wasm/plugins/device_soundfont.wasm +0 -0
  63. package/dist/wasm/plugins/device_spielwerk.wasm +0 -0
  64. package/dist/wasm/plugins/device_stereo_tool.wasm +0 -0
  65. package/dist/wasm/plugins/device_tidal.wasm +0 -0
  66. package/dist/wasm/plugins/device_vaporisateur.wasm +0 -0
  67. package/dist/wasm/plugins/device_velocity.wasm +0 -0
  68. package/dist/wasm/plugins/device_vocoder.wasm +0 -0
  69. package/dist/wasm/plugins/device_waveshaper.wasm +0 -0
  70. package/dist/wasm/plugins/device_werkstatt.wasm +0 -0
  71. package/dist/wasm/plugins/device_zeitgeist.wasm +0 -0
  72. package/dist/wasm-offline-worker.js +97 -0
  73. package/dist/wasm-offline-worker.js.map +7 -0
  74. package/dist/wasm-processor.js +97 -0
  75. package/dist/wasm-processor.js.map +7 -0
  76. package/dist/worklet-scope.d.ts +2 -0
  77. package/dist/worklet-scope.d.ts.map +1 -0
  78. package/dist/worklet-scope.js +10 -0
  79. package/package.json +53 -0
@@ -0,0 +1,343 @@
1
+ // The SCRIPT BRIDGE: the JS side of the three scriptable devices (Werkstatt / Apparat / Spielwerk). Their Rust
2
+ // side-modules do no DSP — per block they call the `host_script_*` env imports defined here, which run the user
3
+ // `Processor` (registered by the app at `globalThis.openDAW.<registry>[uuid]`) over the engine's shared linear
4
+ // memory. So the SAME user JavaScript executes in the WASM engine as in the pure-TS engine; only the plumbing
5
+ // differs. This is the one place the engine's zero-JS-in-render rule is relaxed, and it is contained entirely
6
+ // here + in the three thin device crates. The bridge mirrors the TS `WerkstattDeviceProcessor` /
7
+ // `ApparatDeviceProcessor` / `SpielwerkDeviceProcessor`: hot-swap by the registry `update` counter, the same
8
+ // output validation (NaN / overflow -> silence), the SimpleLimiter (Apparat), and the param value mapping.
9
+ //
10
+ // Identity: the device's `init` calls `host_script_create(uuidPtr, kind, statePtr)` -> a handle; every later
11
+ // call passes that handle. Buffers are byte offsets into the ONE shared memory; we re-derive `memory.buffer`
12
+ // views EVERY call, since the SharedArrayBuffer can grow / detach (talc), never caching a typed-array view.
13
+ import { isDefined, UUID, ValueMapping } from "@opendaw/lib-std";
14
+ import { SimpleLimiter } from "@opendaw/lib-dsp";
15
+ import { runSpielwerk, SpielwerkRuntime } from "./script-spielwerk";
16
+ const RENDER_QUANTUM = 128;
17
+ // ~1 second of render calls (128-frame quanta at 48k) before a scriptless device is reported — long enough to
18
+ // cover a script still compiling / addModule-ing async, short enough to surface a genuine misconfiguration.
19
+ const MISSING_GRACE_CALLS = 375;
20
+ // Device kinds (mirror abi DEVICE_KIND_*) and their script registries.
21
+ const KIND_INSTRUMENT = 0;
22
+ const KIND_AUDIO_EFFECT = 1;
23
+ const KIND_MIDI_EFFECT = 2;
24
+ const REGISTRY_BY_KIND = {
25
+ [KIND_INSTRUMENT]: "apparatProcessors",
26
+ [KIND_AUDIO_EFFECT]: "werkstattProcessors",
27
+ [KIND_MIDI_EFFECT]: "spielwerkProcessors"
28
+ };
29
+ // Parameter wire kinds (mirror abi PARAM_KIND_*): UNIT is a 0..1 automation value to MAP; the rest are already
30
+ // the real (un-automated) field value, used directly.
31
+ const PARAM_KIND_UNIT = 0;
32
+ const MAX_AMPLITUDE = 1000.0; // ~60 dB; matches the TS processors' validateOutput
33
+ // One scriptable device instance's JS-side state, keyed by the handle `host_script_create` returned.
34
+ class Bridge {
35
+ uuid;
36
+ kind;
37
+ registryName;
38
+ proc = null;
39
+ currentUpdate = -1;
40
+ silenced = false;
41
+ // index -> {label, valueMapping}; built from the registry entry's parsed @param declarations.
42
+ paramMappings = new Map();
43
+ // index -> the raw (kind, value) the engine last delivered. Cached so a value pushed BEFORE the proc loads
44
+ // (the engine pushes initial / automated params at bind, before the first render builds the mappings) is
45
+ // replayed once the proc + mappings exist, and re-mapped through the NEW mapping on a hot-swap.
46
+ rawParams = new Map();
47
+ // index -> label, for @sample slots (Apparat); the resolved sample handle is cached so a hot-swap re-delivers.
48
+ sampleLabels = new Map();
49
+ sampleHandles = new Map(); // index -> resolved engine sample handle (-1 = no sample; 0 is valid)
50
+ limiter = null;
51
+ spielwerk = null;
52
+ // A scriptable device with no registered Processor renders silence. That is legitimate for a block or two
53
+ // while the script loads async, but a device that stays scriptless is an anomaly the host must surface (not
54
+ // swallow). Count the render calls that found no registry entry; warn ONCE past the grace window.
55
+ missingCalls = 0;
56
+ warnedMissing = false;
57
+ constructor(uuid, kind, registryName) {
58
+ this.uuid = uuid;
59
+ this.kind = kind;
60
+ this.registryName = registryName;
61
+ }
62
+ }
63
+ const resolveMapping = (declaration) => {
64
+ // Mirrors ScriptDeclaration.resolveValueMapping EXACTLY (same lib-std calls), so the bridge maps a param's
65
+ // automation value identically to the TS engine -> parity is by construction.
66
+ switch (declaration.mapping) {
67
+ case "unipolar": return ValueMapping.unipolar();
68
+ case "linear": return ValueMapping.linear(declaration.min, declaration.max);
69
+ case "exp": return ValueMapping.exponential(declaration.min, declaration.max);
70
+ case "int": return ValueMapping.linearInteger(declaration.min, declaration.max);
71
+ case "bool": return ValueMapping.linearInteger(0, 1);
72
+ default: return ValueMapping.unipolar();
73
+ }
74
+ };
75
+ const validateOutput = (channels, s0, s1) => {
76
+ for (let channel = 0; channel < channels.length; channel++) {
77
+ const samples = channels[channel];
78
+ for (let i = s0; i < s1; i++) {
79
+ const sample = samples[i];
80
+ if (sample !== sample) {
81
+ return `NaN detected in output channel ${channel} at sample ${i}`;
82
+ }
83
+ if (sample > MAX_AMPLITUDE || sample < -MAX_AMPLITUDE) {
84
+ return `Signal overflow in channel ${channel} at sample ${i} (amplitude: ${sample.toFixed(1)})`;
85
+ }
86
+ }
87
+ }
88
+ return null;
89
+ };
90
+ export class ScriptBridges {
91
+ #memory;
92
+ #engine;
93
+ #sampleRate;
94
+ #onMessage;
95
+ #bridges = new Map();
96
+ #byUuid = new Map();
97
+ #nextHandle = 1;
98
+ constructor(memory, engine, sampleRate, onMessage = () => { }) {
99
+ this.#memory = memory;
100
+ this.#engine = engine;
101
+ this.#sampleRate = sampleRate;
102
+ this.#onMessage = onMessage;
103
+ }
104
+ /// The `host_script_*` (+ no engine `host_self_uuid`) closures to bind into each scriptable device's `env`.
105
+ imports() {
106
+ return {
107
+ host_script_create: (uuidPtr, kind, statePtr) => this.#create(uuidPtr, kind, statePtr),
108
+ host_script_audio: (handle, srcL, srcR, outL, outR, s0, s1, index, p0, p1, bpm, flags) => this.#audio(handle, srcL, srcR, outL, outR, s0, s1, index, p0, p1, bpm, flags),
109
+ host_script_note_on: (handle, pitch, velocity, cent, id) => this.#noteOn(handle, pitch, velocity, cent, id),
110
+ host_script_note_off: (handle, id) => this.#noteOff(handle, id),
111
+ host_script_reset: (handle) => this.#reset(handle),
112
+ host_script_param: (handle, index, kind, value) => this.#param(handle, index, kind, value),
113
+ host_script_sample: (handle, index, sampleHandle, present) => this.#sample(handle, index, sampleHandle, present),
114
+ host_script_notes: (handle, inPtr, inCount, outPtr, outMax, from, to, bpm, flags, s0, s1) => this.#notes(handle, inPtr, inCount, outPtr, outMax, from, to, bpm, flags, s0, s1),
115
+ host_script_release: (handle) => this.#release(handle)
116
+ };
117
+ }
118
+ // A `create` for a uuid that already has a live bridge REPLACES it: release the old one first (its
119
+ // Processor + limiter + runtime), so a rebind the engine's `terminate` hasn't (yet, or ever) reached for
120
+ // never orphans the previous bridge — the dedup a bare `#nextHandle++` per call was missing entirely.
121
+ #create(uuidPtr, kind, _statePtr) {
122
+ const uuid = UUID.toString(new Uint8Array(this.#memory.buffer, uuidPtr, 16).slice());
123
+ const existingHandle = this.#byUuid.get(uuid);
124
+ if (isDefined(existingHandle)) {
125
+ this.#release(existingHandle);
126
+ }
127
+ const handle = this.#nextHandle++;
128
+ this.#bridges.set(handle, new Bridge(uuid, kind, REGISTRY_BY_KIND[kind] ?? "werkstattProcessors"));
129
+ this.#byUuid.set(uuid, handle);
130
+ return handle;
131
+ }
132
+ // Poll the registry and hot-swap the user Processor when its `update` changed (the TS #tryLoad/#swapProcessor),
133
+ // then re-apply the cached params + samples. Returns the live, non-silenced proc, or null.
134
+ #ensureProc(bridge) {
135
+ const registry = globalThis.openDAW?.[bridge.registryName]?.[bridge.uuid];
136
+ if (registry === undefined && bridge.currentUpdate === -1) {
137
+ // Never loaded and still absent: tolerate the async-load grace window, then report once so a scriptable
138
+ // device silently muting its chain (the "Open Up renders silent" failure) can never pass unnoticed.
139
+ if (!bridge.warnedMissing && ++bridge.missingCalls > MISSING_GRACE_CALLS) {
140
+ bridge.warnedMissing = true;
141
+ this.#onMessage(bridge.uuid, `No Processor registered at globalThis.openDAW.${bridge.registryName}[${bridge.uuid}] — device is rendering silence`);
142
+ }
143
+ return null;
144
+ }
145
+ if (registry !== undefined && registry.update !== bridge.currentUpdate) {
146
+ try {
147
+ const proc = new registry.create();
148
+ bridge.paramMappings = new Map(registry.params.map(declaration => [declaration.index, { label: declaration.label, mapping: resolveMapping(declaration) }]));
149
+ bridge.sampleLabels = new Map(registry.samples.map(declaration => [declaration.index, declaration.label]));
150
+ if (bridge.kind === KIND_INSTRUMENT) {
151
+ proc.samples = {};
152
+ for (const label of bridge.sampleLabels.values()) {
153
+ proc.samples[label] = null;
154
+ }
155
+ bridge.limiter ??= new SimpleLimiter(this.#sampleRate);
156
+ }
157
+ if (bridge.kind === KIND_MIDI_EFFECT) {
158
+ bridge.spielwerk = new SpielwerkRuntime();
159
+ }
160
+ bridge.proc = proc;
161
+ bridge.currentUpdate = registry.update;
162
+ bridge.silenced = false;
163
+ // Re-apply the parameters + samples the engine pushed before this swap (re-mapping each raw value
164
+ // through the new script's @param mapping).
165
+ for (const [index, raw] of bridge.rawParams) {
166
+ this.#applyParam(bridge, index, raw.kind, raw.value);
167
+ }
168
+ for (const [index, sampleHandle] of bridge.sampleHandles) {
169
+ this.#deliverSample(bridge, index, sampleHandle);
170
+ }
171
+ }
172
+ catch (error) {
173
+ this.#silence(bridge, `Failed to instantiate Processor: ${error}`);
174
+ }
175
+ }
176
+ return bridge.silenced ? null : bridge.proc;
177
+ }
178
+ #audio(handle, srcL, srcR, outL, outR, s0, s1, index, p0, p1, bpm, flags) {
179
+ const bridge = this.#bridges.get(handle);
180
+ if (bridge === undefined) {
181
+ return 1;
182
+ }
183
+ const proc = this.#ensureProc(bridge);
184
+ if (proc === null) {
185
+ return 1;
186
+ }
187
+ const buffer = this.#memory.buffer;
188
+ const outLeft = new Float32Array(buffer, outL, RENDER_QUANTUM);
189
+ const outRight = new Float32Array(buffer, outR, RENDER_QUANTUM);
190
+ const block = { index, p0, p1, s0, s1, bpm, flags };
191
+ try {
192
+ if (bridge.kind === KIND_AUDIO_EFFECT) {
193
+ const io = { src: [new Float32Array(buffer, srcL, RENDER_QUANTUM), new Float32Array(buffer, srcR, RENDER_QUANTUM)], out: [outLeft, outRight] };
194
+ proc.process(io, block);
195
+ }
196
+ else {
197
+ // Re-resolve the @sample slots every block (mirrors the TS `#pollSamples`): a sample's PCM loads
198
+ // ASYNC after bind, so the handle is delivered before its frames are resident; re-resolving flips
199
+ // `samples[label]` from null to the AudioData once ready. Re-deriving the views each block also
200
+ // keeps them valid if the SharedArrayBuffer grew (same reason `#audio` re-derives its outputs).
201
+ for (const [sampleIndex, sampleHandle] of bridge.sampleHandles) {
202
+ this.#deliverSample(bridge, sampleIndex, sampleHandle);
203
+ }
204
+ outLeft.fill(0.0, s0, s1);
205
+ outRight.fill(0.0, s0, s1);
206
+ proc.process([outLeft, outRight], block);
207
+ }
208
+ }
209
+ catch (error) {
210
+ this.#silence(bridge, `Runtime error: ${error}`);
211
+ return 1;
212
+ }
213
+ const error = validateOutput([outLeft, outRight], s0, s1);
214
+ if (error !== null) {
215
+ outLeft.fill(0.0, s0, s1);
216
+ outRight.fill(0.0, s0, s1);
217
+ this.#silence(bridge, error);
218
+ return 1;
219
+ }
220
+ if (bridge.kind === KIND_INSTRUMENT && bridge.limiter !== null) {
221
+ // SimpleLimiter.replace only reads `buffer.channels()`; a duck-typed view over our two channels reuses
222
+ // the exact limiter code (and its persistent envelope) without an intermediate AudioBuffer copy.
223
+ bridge.limiter.replace({ channels: () => [outLeft, outRight] }, s0, s1);
224
+ }
225
+ return 0;
226
+ }
227
+ // Notes can arrive BEFORE the first audio block, so ensure the user Processor is loaded here too (the engine
228
+ // delivers note-on/off, then calls process; the TS processor has the proc loaded before any note).
229
+ #noteOn(handle, pitch, velocity, cent, id) {
230
+ const bridge = this.#bridges.get(handle);
231
+ if (bridge !== undefined) {
232
+ this.#ensureProc(bridge)?.noteOn?.(pitch, velocity, cent, id);
233
+ }
234
+ }
235
+ #noteOff(handle, id) {
236
+ const bridge = this.#bridges.get(handle);
237
+ if (bridge !== undefined) {
238
+ this.#ensureProc(bridge)?.noteOff?.(id);
239
+ }
240
+ }
241
+ #reset(handle) {
242
+ const bridge = this.#bridges.get(handle);
243
+ bridge?.proc?.reset?.();
244
+ bridge?.spielwerk?.reset();
245
+ }
246
+ #param(handle, index, kind, value) {
247
+ const bridge = this.#bridges.get(handle);
248
+ if (bridge === undefined) {
249
+ return;
250
+ }
251
+ bridge.rawParams.set(index, { kind, value }); // cache for replay once the proc + mappings exist
252
+ this.#applyParam(bridge, index, kind, value);
253
+ }
254
+ // Map one raw (kind, value) through the param's @param mapping and hand it to the user proc. A no-op when the
255
+ // proc / mappings are not loaded yet — `#ensureProc` replays `rawParams` once they are.
256
+ #applyParam(bridge, index, kind, value) {
257
+ const entry = bridge.paramMappings.get(index);
258
+ if (entry === undefined) {
259
+ return;
260
+ }
261
+ const mapped = kind === PARAM_KIND_UNIT ? entry.mapping.y(value) : value;
262
+ bridge.proc?.paramChanged?.(entry.label, mapped);
263
+ }
264
+ // The engine's sample handles are 0-based slot indices, so 0 is a VALID handle; `present` is the absence
265
+ // signal. Cache -1 when no sample so `#deliverSample` can tell "handle 0, resolved" from "no sample".
266
+ #sample(handle, index, sampleHandle, present) {
267
+ const bridge = this.#bridges.get(handle);
268
+ if (bridge === undefined) {
269
+ return;
270
+ }
271
+ const resolved = present !== 0 ? sampleHandle : -1;
272
+ bridge.sampleHandles.set(index, resolved);
273
+ this.#deliverSample(bridge, index, resolved);
274
+ }
275
+ #notes(handle, inPtr, inCount, outPtr, outMax, from, to, bpm, flags, s0, s1) {
276
+ const bridge = this.#bridges.get(handle);
277
+ if (bridge === undefined) {
278
+ return 0;
279
+ }
280
+ const proc = this.#ensureProc(bridge);
281
+ if (proc === null || bridge.spielwerk === null) {
282
+ return 0;
283
+ }
284
+ try {
285
+ return runSpielwerk(bridge.spielwerk, proc, this.#memory.buffer, inPtr, inCount, outPtr, outMax, from, to, bpm, flags, s0, s1);
286
+ }
287
+ catch (error) {
288
+ this.#silence(bridge, `Runtime error: ${error}`);
289
+ return 0;
290
+ }
291
+ }
292
+ // Resolve a sample handle's resident planar frames and hand the user proc an AudioData view (no copy).
293
+ #deliverSample(bridge, index, sampleHandle) {
294
+ if (bridge.proc === null) {
295
+ return;
296
+ }
297
+ const label = bridge.sampleLabels.get(index);
298
+ if (label === undefined) {
299
+ return;
300
+ }
301
+ if (sampleHandle < 0) {
302
+ bridge.proc.samples[label] = null;
303
+ return;
304
+ }
305
+ const scratch = this.#engine.input_reserve(16);
306
+ if (this.#engine.host_resolve_sample(sampleHandle, scratch) === 0) {
307
+ bridge.proc.samples[label] = null;
308
+ return;
309
+ }
310
+ const view = new DataView(this.#memory.buffer, scratch, 16);
311
+ const framesPtr = view.getUint32(0, true);
312
+ const frameCount = view.getUint32(4, true);
313
+ const channelCount = view.getUint32(8, true);
314
+ const sampleRate = view.getFloat32(12, true);
315
+ const frames = [];
316
+ for (let channel = 0; channel < channelCount; channel++) {
317
+ frames.push(new Float32Array(this.#memory.buffer, framesPtr + channel * frameCount * 4, frameCount));
318
+ }
319
+ bridge.proc.samples[label] = { sampleRate, numberOfFrames: frameCount, numberOfChannels: channelCount, frames };
320
+ }
321
+ #silence(bridge, message) {
322
+ bridge.silenced = true;
323
+ this.#onMessage(bridge.uuid, message);
324
+ }
325
+ // THIS device's instance is dying (a genuine removal, never a chain-edit survivor, called from the
326
+ // engine's `terminate` export — or a `#create` dedup replacing a stale bridge for the same uuid): drop
327
+ // the bridge (its Processor + limiter + runtime are then just garbage-collected JS objects).
328
+ #release(handle) {
329
+ const bridge = this.#bridges.get(handle);
330
+ if (!isDefined(bridge)) {
331
+ return;
332
+ }
333
+ this.#bridges.delete(handle);
334
+ if (this.#byUuid.get(bridge.uuid) === handle) {
335
+ this.#byUuid.delete(bridge.uuid);
336
+ }
337
+ }
338
+ /// Test-only introspection: how many bridges are currently live, proving a rebind's `#create` dedup
339
+ /// released the previous one instead of orphaning it.
340
+ liveBridgeCount() {
341
+ return this.#bridges.size;
342
+ }
343
+ }
@@ -0,0 +1,24 @@
1
+ type ScheduledNote = {
2
+ position: number;
3
+ duration: number;
4
+ pitch: number;
5
+ velocity: number;
6
+ cent: number;
7
+ };
8
+ type RetainedNote = {
9
+ id: number;
10
+ position: number;
11
+ duration: number;
12
+ pitch: number;
13
+ velocity: number;
14
+ cent: number;
15
+ };
16
+ export declare class SpielwerkRuntime {
17
+ readonly retained: RetainedNote[];
18
+ readonly scheduled: ScheduledNote[];
19
+ readonly sourceToOutput: Map<number, Set<number>>;
20
+ reset(): void;
21
+ }
22
+ export declare const runSpielwerk: (runtime: SpielwerkRuntime, proc: any, memory: ArrayBufferLike, inPtr: number, inCount: number, outPtr: number, outMax: number, from: number, to: number, bpm: number, flags: number, s0: number, s1: number) => number;
23
+ export {};
24
+ //# sourceMappingURL=script-spielwerk.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"script-spielwerk.d.ts","sourceRoot":"","sources":["../src/script-spielwerk.ts"],"names":[],"mappings":"AAgBA,KAAK,aAAa,GAAG;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC,CAAA;AACxG,KAAK,YAAY,GAAG;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC,CAAA;AAKnH,qBAAa,gBAAgB;IACzB,QAAQ,CAAC,QAAQ,EAAE,YAAY,EAAE,CAAK;IACtC,QAAQ,CAAC,SAAS,EAAE,aAAa,EAAE,CAAK;IACxC,QAAQ,CAAC,cAAc,2BAAiC;IAExD,KAAK,IAAI,IAAI;CAKhB;AA6BD,eAAO,MAAM,YAAY,GAAI,SAAS,gBAAgB,EAAE,MAAM,GAAG,EAAE,QAAQ,eAAe,EAC7D,OAAO,MAAM,EAAE,SAAS,MAAM,EAAE,QAAQ,MAAM,EAAE,QAAQ,MAAM,EAC9D,MAAM,MAAM,EAAE,IAAI,MAAM,EAAE,KAAK,MAAM,EAAE,OAAO,MAAM,EAAE,IAAI,MAAM,EAAE,IAAI,MAAM,KAAG,MA0F3G,CAAA"}
@@ -0,0 +1,179 @@
1
+ // Spielwerk's stateful note-transform orchestration (mirrors the TS `SpielwerkDeviceProcessor.processNotes`),
2
+ // run JS-side by the script bridge. It reads the upstream input `EventRecord`s the device pulled (note-on /
3
+ // note-off), builds the `UserEvent` stream, runs the user generator `*process(block, events)`, validates each
4
+ // yielded note, schedules future-block notes, and correlates note-on -> note-off via a retainer so it can emit
5
+ // the stops. The persistent state (retainer + scheduler) lives in `SpielwerkRuntime` across blocks.
6
+ //
7
+ // EventRecord (abi, 40 bytes, little-endian): position f64@0, offset u32@8, kind u32@12, id u32@16,
8
+ // pitch u32@20, velocity f32@24, cent f32@28, duration f64@32. kind: 0 = note-on, 1 = note-off. `duration` is
9
+ // the note's pulse length, carried on the note-on so the user script's `event.duration` is exact.
10
+ const RECORD_SIZE = 40;
11
+ const KIND_NOTE_ON = 0;
12
+ const KIND_NOTE_OFF = 1;
13
+ const MAX_NOTES_PER_BLOCK = 128;
14
+ const MAX_SCHEDULED_NOTES = 128;
15
+ let nextOutputId = 1;
16
+ // One Spielwerk instance's cross-block state.
17
+ export class SpielwerkRuntime {
18
+ retained = []; // emitted notes awaiting their note-off (position + duration reached)
19
+ scheduled = []; // notes the script placed in a future block
20
+ sourceToOutput = new Map(); // upstream note-on id -> emitted output ids it spawned
21
+ reset() {
22
+ this.retained.length = 0;
23
+ this.scheduled.length = 0;
24
+ this.sourceToOutput.clear();
25
+ }
26
+ }
27
+ const writeRecord = (view, slot, kind, position, id, pitch, velocity, cent, duration) => {
28
+ const base = slot * RECORD_SIZE;
29
+ view.setFloat64(base + 0, position, true);
30
+ view.setUint32(base + 8, 0, true); // offset: a midi-fx leaves it for the consumer to resolve from `position`
31
+ view.setUint32(base + 12, kind, true);
32
+ view.setUint32(base + 16, id, true);
33
+ view.setUint32(base + 20, pitch, true);
34
+ view.setFloat32(base + 24, velocity, true);
35
+ view.setFloat32(base + 28, cent, true);
36
+ view.setFloat64(base + 32, duration, true);
37
+ };
38
+ const validateNote = (note, from) => {
39
+ if (note === undefined || note === null) {
40
+ return "process yielded undefined";
41
+ }
42
+ if (typeof note.pitch !== "number" || note.pitch !== note.pitch) {
43
+ return `Invalid pitch: ${note.pitch}`;
44
+ }
45
+ if (note.pitch < 0 || note.pitch > 127) {
46
+ return `Pitch out of range: ${note.pitch} (must be 0-127)`;
47
+ }
48
+ if (typeof note.velocity !== "number" || note.velocity !== note.velocity) {
49
+ return `Invalid velocity: ${note.velocity}`;
50
+ }
51
+ if (note.velocity < 0 || note.velocity > 1) {
52
+ return `Velocity out of range: ${note.velocity} (must be 0-1)`;
53
+ }
54
+ if (typeof note.duration !== "number" || note.duration !== note.duration) {
55
+ return `Invalid duration: ${note.duration}`;
56
+ }
57
+ if (note.duration <= 0) {
58
+ return `Duration must be positive: ${note.duration}`;
59
+ }
60
+ if (typeof note.position !== "number" || note.position !== note.position) {
61
+ return `Invalid position: ${note.position}`;
62
+ }
63
+ if (note.position < from) {
64
+ return `Position ${note.position} is in the past (block starts at ${from})`;
65
+ }
66
+ return null;
67
+ };
68
+ // Run one pulled range through the user generator + tracking, writing output records, returning the count.
69
+ // Throws on a script error / validation failure / flood (the bridge catches it and silences).
70
+ export const runSpielwerk = (runtime, proc, memory, inPtr, inCount, outPtr, outMax, from, to, bpm, flags, s0, s1) => {
71
+ const input = new DataView(memory, inPtr, inCount * RECORD_SIZE);
72
+ const out = new DataView(memory, outPtr, outMax * RECORD_SIZE);
73
+ let outCount = 0;
74
+ const emit = (kind, position, id, pitch, velocity, cent, duration) => {
75
+ if (outCount >= outMax) {
76
+ throw new Error(`Note flood: exceeded ${outMax} output notes per range`);
77
+ }
78
+ writeRecord(out, outCount++, kind, position, id, pitch, velocity, cent, duration);
79
+ };
80
+ // Release retained notes whose span completed within this range, emitting their note-off.
81
+ for (let i = runtime.retained.length - 1; i >= 0; i--) {
82
+ const note = runtime.retained[i];
83
+ const end = note.position + note.duration;
84
+ if (end < to) {
85
+ runtime.retained.splice(i, 1);
86
+ emit(KIND_NOTE_OFF, end, note.id, note.pitch, 0, 0, 0);
87
+ }
88
+ }
89
+ // Build the upstream UserEvent stream; a note-off cancels the outputs its source note-on spawned.
90
+ const events = [];
91
+ for (let i = 0; i < inCount; i++) {
92
+ const base = i * RECORD_SIZE;
93
+ const kind = input.getUint32(base + 12, true);
94
+ const id = input.getUint32(base + 16, true);
95
+ const position = input.getFloat64(base + 0, true);
96
+ const pitch = input.getUint32(base + 20, true);
97
+ if (kind === KIND_NOTE_ON) {
98
+ events.push({ gate: true, id, position, duration: input.getFloat64(base + 32, true), pitch, velocity: input.getFloat32(base + 24, true), cent: input.getFloat32(base + 28, true) });
99
+ }
100
+ else if (kind === KIND_NOTE_OFF) {
101
+ const outputs = runtime.sourceToOutput.get(id);
102
+ if (outputs !== undefined) {
103
+ for (const outputId of outputs) {
104
+ const index = runtime.retained.findIndex(note => note.id === outputId);
105
+ if (index >= 0) {
106
+ const note = runtime.retained.splice(index, 1)[0];
107
+ emit(KIND_NOTE_OFF, position, note.id, note.pitch, 0, 0, 0);
108
+ }
109
+ }
110
+ runtime.sourceToOutput.delete(id);
111
+ }
112
+ events.push({ gate: false, id, position, pitch });
113
+ }
114
+ }
115
+ // Replay any scheduled notes that fall in this range.
116
+ for (let i = runtime.scheduled.length - 1; i >= 0; i--) {
117
+ const note = runtime.scheduled[i];
118
+ if (note.position >= from && note.position < to) {
119
+ runtime.scheduled.splice(i, 1);
120
+ emit(KIND_NOTE_ON, note.position, retain(runtime, note), note.pitch, note.velocity, note.cent, note.duration);
121
+ }
122
+ }
123
+ // Run the user generator, tracking the current source note-on id (so emitted notes correlate for note-off).
124
+ let currentSourceId = -1;
125
+ const tracked = {
126
+ [Symbol.iterator]() {
127
+ let index = 0;
128
+ return {
129
+ next() {
130
+ if (index >= events.length) {
131
+ currentSourceId = -1;
132
+ return { done: true, value: undefined };
133
+ }
134
+ const value = events[index++];
135
+ if (value.gate) {
136
+ currentSourceId = value.id;
137
+ }
138
+ return { done: false, value };
139
+ }
140
+ };
141
+ }
142
+ };
143
+ const block = { from, to, bpm, s0, s1, flags };
144
+ let noteCount = 0;
145
+ for (const yielded of proc.process(block, tracked)) {
146
+ if (++noteCount > MAX_NOTES_PER_BLOCK) {
147
+ throw new Error(`Note flood: exceeded ${MAX_NOTES_PER_BLOCK} notes per block`);
148
+ }
149
+ const error = validateNote(yielded, from);
150
+ if (error !== null) {
151
+ throw new Error(error);
152
+ }
153
+ const note = { position: yielded.position, duration: yielded.duration, pitch: yielded.pitch, velocity: yielded.velocity, cent: yielded.cent ?? 0 };
154
+ if (note.position >= to) {
155
+ if (runtime.scheduled.length >= MAX_SCHEDULED_NOTES) {
156
+ throw new Error(`Scheduler full: exceeded ${MAX_SCHEDULED_NOTES} scheduled notes`);
157
+ }
158
+ runtime.scheduled.push(note);
159
+ }
160
+ else {
161
+ const id = retain(runtime, note);
162
+ if (currentSourceId >= 0) {
163
+ let set = runtime.sourceToOutput.get(currentSourceId);
164
+ if (set === undefined) {
165
+ set = new Set();
166
+ runtime.sourceToOutput.set(currentSourceId, set);
167
+ }
168
+ set.add(id);
169
+ }
170
+ emit(KIND_NOTE_ON, note.position, id, note.pitch, note.velocity, note.cent, note.duration);
171
+ }
172
+ }
173
+ return outCount;
174
+ };
175
+ const retain = (runtime, note) => {
176
+ const id = nextOutputId++;
177
+ runtime.retained.push({ id, position: note.position, duration: note.duration, pitch: note.pitch, velocity: note.velocity, cent: note.cent });
178
+ return id;
179
+ };
@@ -0,0 +1,28 @@
1
+ import type { SoundFont2 } from "soundfont2";
2
+ export type SimpleSample = {
3
+ pcm: Float32Array;
4
+ sampleRate: number;
5
+ loopStart: number;
6
+ loopEnd: number;
7
+ };
8
+ export type SimpleRegion = {
9
+ keyLo: number;
10
+ keyHi: number;
11
+ velLo: number;
12
+ velHi: number;
13
+ sampleIndex: number;
14
+ rootKey: number;
15
+ loopMode: number;
16
+ pan: number;
17
+ attack: number;
18
+ decay: number;
19
+ sustain: number;
20
+ release: number;
21
+ };
22
+ export type SimpleSoundfont = {
23
+ samples: ReadonlyArray<SimpleSample>;
24
+ presets: ReadonlyArray<ReadonlyArray<SimpleRegion>>;
25
+ };
26
+ export declare const encodeSoundfont: ({ samples, presets }: SimpleSoundfont) => ArrayBuffer;
27
+ export declare const simplifySoundfont: (soundfont: SoundFont2) => ArrayBuffer;
28
+ //# sourceMappingURL=soundfont-simplify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"soundfont-simplify.d.ts","sourceRoot":"","sources":["../src/soundfont-simplify.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,YAAY,CAAA;AAqB1C,MAAM,MAAM,YAAY,GAAG;IACvB,GAAG,EAAE,YAAY,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACvB,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;IAC1D,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAClE,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC1B,OAAO,EAAE,aAAa,CAAC,YAAY,CAAC,CAAA;IACpC,OAAO,EAAE,aAAa,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC,CAAA;CACtD,CAAA;AAUD,eAAO,MAAM,eAAe,GAAI,sBAAoB,eAAe,KAAG,WAqDrE,CAAA;AAYD,eAAO,MAAM,iBAAiB,GAAI,WAAW,UAAU,KAAG,WA6CzD,CAAA"}