@vessel-dsp/runtime 0.2.2 → 0.3.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.
@@ -0,0 +1,209 @@
1
+ import type { ControlId, Program } from "@vessel-dsp/compiler";
2
+ import type { ChainAdvisory } from "../chain-advisories";
3
+ import type { StageCoverage } from "../chain-slot";
4
+ import type { SupplyGroundConflict } from "../supply-ground";
5
+ export declare const v2WorkletProcessorName = "v2-pedal-processor";
6
+ /**
7
+ * The minimum port surface the worklet helper needs. `MessagePort` rather than a
8
+ * narrower type is what the workbench copy takes; this structural form accepts the
9
+ * same object while keeping this headless package free of a DOM lib import, so a
10
+ * host (or a bun test) can pass any `{ postMessage }` holder, including a real port.
11
+ */
12
+ export type V2WorkletPort = {
13
+ postMessage(message: V2WorkletInboundMessage): void;
14
+ };
15
+ /**
16
+ * Send an inbound message to the worklet, type-checked.
17
+ *
18
+ * **This exists because `MessagePort.postMessage` takes `any`.** When the `load` message changed
19
+ * from `programs` to `slots`, `tsc` stayed silent on the one call site that still sent the old
20
+ * shape -- the worklet would have read `message.slots` as `undefined` and gone quiet, with no
21
+ * compile error anywhere. A one-line wrapper is the difference between that and a build failure.
22
+ */
23
+ export declare function postV2WorkletMessage(port: V2WorkletPort, message: V2WorkletInboundMessage): void;
24
+ /**
25
+ * One slot in the chain the worklet is asked to build.
26
+ *
27
+ * **A discriminated union rather than a `Program[]`, because a chain slot is no longer always a
28
+ * compiled circuit.** The runtime has accepted injected processors since heterogeneous chain slots
29
+ * landed; this is the message shape that lets a caller send one. `programs` is gone rather than
30
+ * kept alongside -- an obsolete path, not a compatibility surface.
31
+ *
32
+ * `ir` covers **both** an impulse response and the cab simulation, and that is the design rather
33
+ * than a shortcut: nothing in `ChainSlot` distinguishes them, since both are buffer-in/buffer-out
34
+ * filters producing a miked signal, so a second kind would be two names for one behaviour.
35
+ *
36
+ * `nam` is its own kind for the opposite reason -- not because the chain treats it differently, but
37
+ * because building one needs different *inputs*: wasm bytes and a model document rather than taps.
38
+ * The `ExternalProcessor` it becomes is indistinguishable to the chain.
39
+ */
40
+ export type BypassMode = "wire" | "buffer" | "effect";
41
+ export type V2WorkletSlot = {
42
+ readonly kind: "program";
43
+ readonly program: Program;
44
+ readonly bufferProgram?: Program;
45
+ readonly bypassMode?: BypassMode;
46
+ } | {
47
+ readonly kind: "ir";
48
+ /** Named in advisories and telemetry. A label, never a discriminator. */
49
+ readonly id: string;
50
+ /**
51
+ * Impulse response taps at the destination `AudioContext` rate. A `Float32Array`
52
+ * rather than a number list: structured clone carries typed arrays, and an IR is
53
+ * thousands of taps where JSON would be both large and lossy about precision.
54
+ * Resampling is the caller's, via `cab-mic-external-ir-resample.ts`.
55
+ */
56
+ readonly taps: Float32Array;
57
+ readonly produces?: StageCoverage;
58
+ readonly expects?: StageCoverage;
59
+ /**
60
+ * Optional scalar on the taps. Omitted means 1 and no normalisation -- the worklet
61
+ * invents no level. See `IrProcessorOptions.gain`.
62
+ */
63
+ readonly gain?: number;
64
+ readonly bypassMode?: BypassMode;
65
+ } | {
66
+ readonly kind: "nam";
67
+ /** Named in advisories and telemetry. A label, never a discriminator. */
68
+ readonly id: string;
69
+ /**
70
+ * The bytes of `web/nam-engine.wasm`.
71
+ *
72
+ * **Passed in rather than fetched, which is a requirement and not a preference:** an
73
+ * `AudioWorkletGlobalScope` has no `fetch`, so the module cannot load itself there. The
74
+ * worklet memoises instantiation, so sending these on a second load costs a structured
75
+ * clone and nothing more -- it will not recompile the module, which is the thing that
76
+ * crashed iOS tabs in the architecture this replaced.
77
+ */
78
+ readonly wasmBytes: ArrayBuffer;
79
+ /** The `.nam` document's text. The engine parses it; nothing here does. */
80
+ readonly modelJson: string;
81
+ /**
82
+ * Defaults to `speaker-electrical` -- a load-box capture is taken *at* the speaker
83
+ * terminal. Pass `miked` only for a profile that states it is a full-rig capture; nothing
84
+ * infers that from a file name or a title.
85
+ */
86
+ readonly produces?: StageCoverage;
87
+ readonly expects?: StageCoverage;
88
+ /** A2/slimmable size in [0,1]; negative or omitted selects full size. */
89
+ readonly slimSize?: number;
90
+ };
91
+ /** Main thread -> worklet. */
92
+ export type V2WorkletInboundMessage = {
93
+ readonly type: "load";
94
+ /** An ordered chain of slots. A compiled Program round-trips through emit() already
95
+ * (src/compiler/emit.ts) and an IR's taps are a typed array, so structured clone
96
+ * carries the whole list intact. Slot 0 runs first; its output feeds slot 1, and so
97
+ * on. */
98
+ readonly slots: readonly V2WorkletSlot[];
99
+ /**
100
+ * When present, every `program` slot runs on the C++/WASM console instead of the
101
+ * TS `ReferenceRuntime` — same Program ROM, different console. The host supplies
102
+ * the wasm binary because an AudioWorkletGlobalScope cannot fetch; the glue is
103
+ * bundled statically like the NAM engine's. Absent means the TS console, which is
104
+ * also the fallback for nothing: a wasm load failure is a reported error, never a
105
+ * silent downgrade — two consoles that can disagree must never be swapped quietly.
106
+ */
107
+ readonly wasmConsole?: {
108
+ readonly wasmBytes: ArrayBuffer;
109
+ };
110
+ } | {
111
+ readonly type: "setControl";
112
+ /** Which slot's program owns this control. */
113
+ readonly slot: number;
114
+ readonly id: ControlId;
115
+ /** 0..1. That slot's own runtime applies its program's own taper; nothing here does. */
116
+ readonly position: number;
117
+ } | {
118
+ readonly type: "setBypassMode";
119
+ /** Which slot to change bypass mode for. */
120
+ readonly slot: number;
121
+ readonly mode: BypassMode;
122
+ };
123
+ /** Worklet -> main thread. */
124
+ export type V2WorkletOutboundMessage = {
125
+ readonly type: "loaded";
126
+ /** Every slot's controls, labelled by slot so a listener can address `setControl`
127
+ * correctly even when two slots declare the same control id. */
128
+ readonly controls: readonly {
129
+ readonly slot: number;
130
+ readonly id: ControlId;
131
+ }[];
132
+ /**
133
+ * Chain-level supply-ground conflicts (`src/runtime/supply-ground.ts`), empty when
134
+ * the chain agrees.
135
+ *
136
+ * Carried on `loaded` rather than as its own message, and that is the point: it is
137
+ * a property of the chain that just loaded, so it cannot arrive stale, be missed by
138
+ * a listener that subscribed late, or outlive the chain it describes. It rides
139
+ * *with* a successful load because the chain plays correctly either way -- each
140
+ * program is solved against its own rails -- and what it warns about is the board a
141
+ * listener would have to build to hear this on hardware.
142
+ *
143
+ * The type comes from the runtime module that computes it rather than being
144
+ * restated here, so the two cannot drift.
145
+ */
146
+ readonly supplyGroundConflicts: readonly SupplyGroundConflict[];
147
+ /**
148
+ * What the chain can say about itself that no single program can
149
+ * (`src/runtime/chain-advisories.ts`): a seam it could not scale, or a slot whose
150
+ * speaker-terminal output feeds another slot's input. Empty when the chain is sound.
151
+ *
152
+ * Carried on `loaded` for the same reason as the conflicts above: it describes the
153
+ * chain that just loaded. A level mismatch is not corrected at such a seam, which is
154
+ * the old behaviour — the difference is that it is now stated rather than silent.
155
+ */
156
+ readonly chainAdvisories: readonly ChainAdvisory[];
157
+ } | {
158
+ readonly type: "telemetry";
159
+ /** Every slot's own telemetry, labelled by slot and never blended into one total --
160
+ * a non-converging pedal in slot 2 must stay visible even when slot 0 is clean.
161
+ * `heldSamples` is nonConvergedSamples + nonFiniteSamples from RuntimeTelemetry: a
162
+ * silent overrun must be visible, per this repository's Evidence Hygiene rule. */
163
+ readonly slots: readonly {
164
+ readonly slot: number;
165
+ readonly samples: number;
166
+ readonly heldSamples: number;
167
+ readonly operatingPointFailures: number;
168
+ readonly peakIterations: number;
169
+ }[];
170
+ /**
171
+ * The WASM console's per-slot iteration peak. Separate from `slots` rather than folded
172
+ * into it because a WASM slot is an `ExternalProcessor` to `ChainRuntime`, which reports
173
+ * no telemetry for it at all -- so `slots` arrives EMPTY on the WASM path and the UI read
174
+ * `Peak: 0 iters` while the engine was at 232% CPU. Reporting a synthetic slot row with
175
+ * `heldSamples: 0` would have been worse: this module already refuses to let a processor
176
+ * slot claim a zero held-sample count, because "never ran a solve" and "converged fine"
177
+ * must not print the same. So only the field the C ABI actually exposes is carried.
178
+ */
179
+ readonly wasmSlots?: readonly {
180
+ readonly slot: number;
181
+ /** `Engine::maxIterationsObserved()` -- a SESSION peak, never reset, not a window. */
182
+ readonly peakIterations: number;
183
+ }[];
184
+ /** True when the active program slots are executed on the WASM console. */
185
+ readonly isWasmConsole?: boolean;
186
+ /**
187
+ * MEAN CPU budget usage over the telemetry window (100% = the whole 2.67 ms quantum).
188
+ * A mean is the wrong statistic for a deadline and is kept only as context: an amp can
189
+ * average 101% while missing 616 deadlines, because what overruns is individual quanta,
190
+ * not the average. Judge with `cpuPeakPercent`/`cpuP95Percent`, and with `overrunCount`.
191
+ */
192
+ readonly cpuLoadPercent?: number;
193
+ /** Worst single quantum in the telemetry window, as a percentage of budget. */
194
+ readonly cpuPeakPercent?: number;
195
+ /** 95th-percentile quantum in the window, as a percentage of budget. */
196
+ readonly cpuP95Percent?: number;
197
+ /** Worst single quantum since the chain loaded -- a rare spike must not scroll away. */
198
+ readonly cpuSessionPeakPercent?: number;
199
+ /** Average compute time per 128-sample quantum in milliseconds. */
200
+ readonly quantumTimeMs?: number;
201
+ /** Total number of audio quantum deadlines missed (quantum compute time > budget). */
202
+ readonly overrunCount?: number;
203
+ } | {
204
+ /** A refusal or a per-sample throw. The worklet goes silent rather than
205
+ * killing the audio graph; this message is how a listener finds out why. */
206
+ readonly type: "error";
207
+ readonly message: string;
208
+ };
209
+ //# sourceMappingURL=v2-worklet-protocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"v2-worklet-protocol.d.ts","sourceRoot":"","sources":["../../src/worklet/v2-worklet-protocol.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ // Message protocol between the main thread and the v2 AudioWorkletProcessor
2
+ // (v2-audio-worklet.ts). Ported from the workbench reference copy at
3
+ // `workbench/src/web/v2-worklet-protocol.ts`, which the workbench keeps; this
4
+ // package copy is the npm-published one. Behaviour is identical; only the
5
+ // imports below are retargeted into this package, and the `postV2WorkletMessage`
6
+ // port parameter is structural so hosts without a DOM lib can call it.
7
+ // Deliberately separate from pedal-worklet-protocol-types.d.ts:
8
+ // that protocol is v1's and carries 19 block.type variants plus a 622-line control
9
+ // dispatch (see thoughts/shared/plans/2026-08-12-runtime-modules.md, "where the boundary
10
+ // actually breaks"). The v2 runtime is a generic console that executes whatever a
11
+ // compiled Program declares, so its protocol is two messages, not a per-pedal dispatch.
12
+ //
13
+ // Both the worklet (bundled separately, see scripts/build-worklet.ts) and the main-thread
14
+ // panel import this file. It carries no runtime code beyond a couple of trivial
15
+ // constructors, so being imported into two separate bundles duplicates nothing that
16
+ // matters.
17
+ //
18
+ // "load" carries an ordered chain (`src/runtime/chain.ts`), not a single Program: a single
19
+ // pedal is that chain's one-element case, not a separate message shape -- the same "no
20
+ // single-program entry point" decision `admission.ts` already made. "setControl" addresses
21
+ // a control by (slot, id) rather than id alone, because two identical pedals in a chain
22
+ // declare the same control ids and slot index is the only thing that disambiguates them.
23
+ export const v2WorkletProcessorName = "v2-pedal-processor";
24
+ /**
25
+ * Send an inbound message to the worklet, type-checked.
26
+ *
27
+ * **This exists because `MessagePort.postMessage` takes `any`.** When the `load` message changed
28
+ * from `programs` to `slots`, `tsc` stayed silent on the one call site that still sent the old
29
+ * shape -- the worklet would have read `message.slots` as `undefined` and gone quiet, with no
30
+ * compile error anywhere. A one-line wrapper is the difference between that and a build failure.
31
+ */
32
+ export function postV2WorkletMessage(port, message) {
33
+ port.postMessage(message);
34
+ }
35
+ //# sourceMappingURL=v2-worklet-protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"v2-worklet-protocol.js","sourceRoot":"","sources":["../../src/worklet/v2-worklet-protocol.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,qEAAqE;AACrE,8EAA8E;AAC9E,0EAA0E;AAC1E,iFAAiF;AACjF,uEAAuE;AACvE,gEAAgE;AAChE,mFAAmF;AACnF,yFAAyF;AACzF,kFAAkF;AAClF,wFAAwF;AACxF,EAAE;AACF,0FAA0F;AAC1F,gFAAgF;AAChF,oFAAoF;AACpF,WAAW;AACX,EAAE;AACF,2FAA2F;AAC3F,uFAAuF;AACvF,2FAA2F;AAC3F,wFAAwF;AACxF,yFAAyF;AAOzF,MAAM,CAAC,MAAM,sBAAsB,GAAG,oBAAoB,CAAC;AAY3D;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CACnC,IAAmB,EACnB,OAAgC;IAEhC,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;AAC3B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vessel-dsp/runtime",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Real-time MNA simulation runtime and solver console for VesselDSP Program ROMs.",
5
5
  "keywords": [
6
6
  "guitar-pedal",
@@ -33,6 +33,9 @@
33
33
  },
34
34
  "./wasm/*": {
35
35
  "default": "./dist/wasm/*"
36
+ },
37
+ "./worklet.js": {
38
+ "default": "./dist/worklet/v2-audio-worklet.js"
36
39
  }
37
40
  },
38
41
  "main": "./dist/index.js",
@@ -44,12 +47,13 @@
44
47
  "LICENSE.md"
45
48
  ],
46
49
  "scripts": {
47
- "build": "tsc -p tsconfig.build.json && bun run ../../scripts/fix-dist-imports.ts dist && rm -rf dist/wasm && cp -r src/wasm dist/wasm",
50
+ "build": "tsc -p tsconfig.build.json && bun run ../../scripts/fix-dist-imports.ts dist && rm -rf dist/wasm && cp -r src/wasm dist/wasm && bun scripts/build-worklet.ts",
48
51
  "clean": "rm -rf dist",
49
52
  "prepack": "bun run --cwd ../core build && bun run --cwd ../compiler build && bun run build:wasm && bun run build",
50
53
  "pack:dry-run": "npm pack --dry-run",
51
54
  "typecheck": "tsc --noEmit -p tsconfig.json",
52
55
  "build:wasm": "bash scripts/build-wasm.sh",
56
+ "build:worklet": "bun scripts/build-worklet.ts",
53
57
  "test": "bun test tests",
54
58
  "generate-kernels": "bun scripts/generate-kernels.ts"
55
59
  },