@vessel-dsp/runtime 0.2.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.
Files changed (60) hide show
  1. package/README.md +178 -0
  2. package/dist/admission.d.ts +94 -0
  3. package/dist/admission.d.ts.map +1 -0
  4. package/dist/admission.js +209 -0
  5. package/dist/admission.js.map +1 -0
  6. package/dist/calibrate.d.ts +104 -0
  7. package/dist/calibrate.d.ts.map +1 -0
  8. package/dist/calibrate.js +420 -0
  9. package/dist/calibrate.js.map +1 -0
  10. package/dist/chain-advisories.d.ts +71 -0
  11. package/dist/chain-advisories.d.ts.map +1 -0
  12. package/dist/chain-advisories.js +268 -0
  13. package/dist/chain-advisories.js.map +1 -0
  14. package/dist/chain-scale.d.ts +82 -0
  15. package/dist/chain-scale.d.ts.map +1 -0
  16. package/dist/chain-scale.js +121 -0
  17. package/dist/chain-scale.js.map +1 -0
  18. package/dist/chain-slot.d.ts +130 -0
  19. package/dist/chain-slot.d.ts.map +1 -0
  20. package/dist/chain-slot.js +105 -0
  21. package/dist/chain-slot.js.map +1 -0
  22. package/dist/chain.d.ts +113 -0
  23. package/dist/chain.d.ts.map +1 -0
  24. package/dist/chain.js +355 -0
  25. package/dist/chain.js.map +1 -0
  26. package/dist/index.d.ts +14 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +18 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/numeric-pivot.d.ts +4 -0
  31. package/dist/numeric-pivot.d.ts.map +1 -0
  32. package/dist/numeric-pivot.js +192 -0
  33. package/dist/numeric-pivot.js.map +1 -0
  34. package/dist/reference-runtime.d.ts +1080 -0
  35. package/dist/reference-runtime.d.ts.map +1 -0
  36. package/dist/reference-runtime.js +6743 -0
  37. package/dist/reference-runtime.js.map +1 -0
  38. package/dist/settle.d.ts +359 -0
  39. package/dist/settle.d.ts.map +1 -0
  40. package/dist/settle.js +900 -0
  41. package/dist/settle.js.map +1 -0
  42. package/dist/supply-ground.d.ts +22 -0
  43. package/dist/supply-ground.d.ts.map +1 -0
  44. package/dist/supply-ground.js +62 -0
  45. package/dist/supply-ground.js.map +1 -0
  46. package/dist/supply.d.ts +30 -0
  47. package/dist/supply.d.ts.map +1 -0
  48. package/dist/supply.js +10 -0
  49. package/dist/supply.js.map +1 -0
  50. package/dist/taper.d.ts +10 -0
  51. package/dist/taper.d.ts.map +1 -0
  52. package/dist/taper.js +34 -0
  53. package/dist/taper.js.map +1 -0
  54. package/dist/v2-wasm-engine.d.ts +91 -0
  55. package/dist/v2-wasm-engine.d.ts.map +1 -0
  56. package/dist/v2-wasm-engine.js +296 -0
  57. package/dist/v2-wasm-engine.js.map +1 -0
  58. package/dist/wasm/v2_dsp.cjs +22 -0
  59. package/dist/wasm/v2_dsp.wasm +0 -0
  60. package/package.json +62 -0
@@ -0,0 +1,268 @@
1
+ // What a chain can say about itself that no single program can.
2
+ //
3
+ // `ChainRuntime` scales between slots by the ratio of one slot's output full scale to the next
4
+ // slot's input full scale (`Program.portFullScaleVolts`, derived in
5
+ // `../compiler/port-full-scale.ts`). When either side is `null` the program did not state a
6
+ // supply to derive the bound from, so there is no ratio to apply and the signal crosses at 1.0 --
7
+ // which is the old behaviour, and is a guess about level rather than a statement about it.
8
+ //
9
+ // It then applies the **impedance divider** at the same seam (`seamDivider`), which is a different
10
+ // question from the same connection: full scale says what units a number is in, loading says how
11
+ // much of the signal survives the connection at all. Both default to 1 when unstated, and neither
12
+ // invents a figure.
13
+ //
14
+ // Advisory rather than fail-closed, the same shape as `./supply-ground.ts`: the chain still runs,
15
+ // and what is missing is a source fact, not a runtime capability.
16
+ /**
17
+ * How far along the amplification path each coverage sits, so a rule can say "at or beyond" instead
18
+ * of naming one value and silently missing the others.
19
+ *
20
+ * Introduced because `speaker-signal-into-input` tested `produces === "speaker-electrical"` and
21
+ * therefore let **`miked` into an instrument input pass in silence** -- a full-rig capture feeding a
22
+ * pedal, which is the same kind error one rung further along. Ordering the vocabulary is what makes
23
+ * that class of miss impossible rather than a thing to remember.
24
+ */
25
+ const COVERAGE_RANK = {
26
+ instrument: 0,
27
+ preamp: 1,
28
+ "speaker-electrical": 2,
29
+ miked: 3,
30
+ };
31
+ /**
32
+ * Seams where one side states no full scale. Pure, so a host can ask before playing.
33
+ *
34
+ * A one-slot chain has no seam and never reports.
35
+ */
36
+ export function chainAdvisories(chain) {
37
+ const advisories = [];
38
+ for (let slot = 1; slot < chain.length; slot += 1) {
39
+ const upstream = chain[slot - 1]?.portFullScaleVolts.output ?? null;
40
+ const downstream = chain[slot]?.portFullScaleVolts.input ?? null;
41
+ if (upstream !== null && downstream !== null) {
42
+ continue;
43
+ }
44
+ const missing = upstream === null && downstream === null
45
+ ? "neither side"
46
+ : upstream === null
47
+ ? `slot ${slot - 1}'s output`
48
+ : `slot ${slot}'s input`;
49
+ advisories.push({
50
+ code: "unscalable-seam",
51
+ slots: [slot - 1, slot],
52
+ message: `between slot ${slot - 1} and slot ${slot}, ${missing} states a full-scale ` +
53
+ "voltage, so the signal crosses unscaled: a level mismatch here is not corrected " +
54
+ "and the receiving slot may be driven far above or below what it expects. The " +
55
+ "bound is derived from the program's own supplies, so a program declaring none " +
56
+ "cannot supply one",
57
+ });
58
+ }
59
+ // **A speaker terminal is not an instrument signal.** `speaker-electrical` means the output node
60
+ // *is* the transformer secondary -- tens of volts across an 8 or 16 ohm load, not the fraction of
61
+ // a volt a jack expects. Feeding that to a slot that wants instrument level is wrong in kind as
62
+ // well as level, and it is the shape of mistake that put an amp's preamp tap and another amp's
63
+ // speaker terminal in the same "plausible" column for a day.
64
+ //
65
+ // **Narrowed to what the receiver expects.** This used to fire whenever a `speaker-electrical`
66
+ // output fed *anything*, which would report the single most ordinary amp chain there is: an amp
67
+ // into a cabinet. A cab stage is exactly what a speaker terminal is for.
68
+ //
69
+ // **"At or beyond a speaker terminal", not "is a speaker terminal".** The first version of this
70
+ // tested one value and let `miked` through: a full-rig capture into a pedal reported nothing,
71
+ // though it is the same error one rung further along. `COVERAGE_RANK` is what closes that.
72
+ for (let slot = 1; slot < chain.length; slot += 1) {
73
+ const produces = chain[slot - 1]?.produces;
74
+ if (produces === undefined ||
75
+ COVERAGE_RANK[produces] < COVERAGE_RANK["speaker-electrical"]) {
76
+ continue;
77
+ }
78
+ if (chain[slot]?.expects !== "instrument") {
79
+ continue;
80
+ }
81
+ advisories.push({
82
+ code: "speaker-signal-into-input",
83
+ slots: [slot - 1, slot],
84
+ message: `slot ${slot - 1} produces a ${produces} signal -- at or past the speaker terminal -- ` +
85
+ `and it feeds slot ${slot}, whose input expects an instrument-level signal: this is a ` +
86
+ "difference of kind, not only of level, and no scaling makes it a valid connection",
87
+ });
88
+ }
89
+ // **The other direction is milder, and deliberately narrower.** A cabinet stage fed a signal that
90
+ // never passed a power amp is thin rather than wrong -- nothing is over-driven and nothing is
91
+ // damaged -- so it is reported, not treated as a kind error.
92
+ //
93
+ // **`preamp` is excluded on purpose, and that exclusion is load-bearing.** A preamp into an
94
+ // impulse response is an established, ordinary rig, and `mesa-boogie-mark-v` renders `preamp`
95
+ // coverage precisely because the signal-chain plan's decision 2 says to honour its declared
96
+ // handoff. Firing here on rank 1 would report that decision as a mistake on every chain.
97
+ //
98
+ // So only rank 0 into a speaker-terminal stage reports: an instrument-level signal with no
99
+ // amplification of any kind between it and a cabinet.
100
+ for (let slot = 1; slot < chain.length; slot += 1) {
101
+ const produces = chain[slot - 1]?.produces;
102
+ const expects = chain[slot]?.expects;
103
+ if (produces === undefined || expects === undefined) {
104
+ continue;
105
+ }
106
+ if (COVERAGE_RANK[produces] !== COVERAGE_RANK.instrument ||
107
+ COVERAGE_RANK[expects] < COVERAGE_RANK["speaker-electrical"]) {
108
+ continue;
109
+ }
110
+ advisories.push({
111
+ code: "instrument-into-speaker-stage",
112
+ slots: [slot - 1, slot],
113
+ message: `slot ${slot} expects a ${expects} signal -- one that has passed a power amp -- and ` +
114
+ `slot ${slot - 1} produces instrument level with no amplification stage between ` +
115
+ "them: the chain runs, but the cabinet colours a signal that never saw a power " +
116
+ "stage, which is audibly thin rather than incorrect",
117
+ });
118
+ }
119
+ // **A miked signal must not be miked again.** Now expressible, because the cabinet is a slot:
120
+ // two consecutive slots both *producing* `miked` means a cab stage was applied to a signal that
121
+ // already carries one.
122
+ //
123
+ // This is the only warnable cab arrangement, and deliberately so. A NAM amp capture produces
124
+ // `speaker-electrical`, so NAM into an IR is silent -- it is what most people do. A whitebox
125
+ // amp into an IR is silent for the same reason. **No cab stage at all is silent too**: a full-rig
126
+ // NAM, or a player who wants none, is a complete chain and nothing here should nag for one.
127
+ for (let slot = 1; slot < chain.length; slot += 1) {
128
+ if (chain[slot - 1]?.produces !== "miked" ||
129
+ chain[slot]?.produces !== "miked") {
130
+ continue;
131
+ }
132
+ advisories.push({
133
+ code: "cab-after-miked",
134
+ slots: [slot - 1, slot],
135
+ message: `slot ${slot - 1} already produces a miked signal -- a full-rig capture, or a ` +
136
+ `cabinet stage -- and slot ${slot} applies another one: the cabinet and microphone ` +
137
+ "colour the signal twice, which is audible as a hollow, over-filtered tone rather " +
138
+ "than a level error",
139
+ });
140
+ }
141
+ // **Only the asymmetric case is worth a word.** A divider needs an impedance on both sides, and
142
+ // the two ways it can be missing are not equally interesting:
143
+ //
144
+ // - *Neither side declares* — the ordinary state of every corpus document today, and reporting
145
+ // it would be a line of noise on every seam of every chain. Silence.
146
+ // - *Either side is a processor* — a NAM or an IR has no impedance to declare, so there is
147
+ // nothing missing. Silence, and this is why `portImpedanceOhms` is nullable as a whole
148
+ // object rather than only per port.
149
+ // - *One electrical side declares and the other does not* — someone measured half of a real
150
+ // interaction and the other half silently defaulted to no loading at all. That is worth
151
+ // naming, because the declared number looks like it is doing something and is not.
152
+ for (let slot = 1; slot < chain.length; slot += 1) {
153
+ const source = chain[slot - 1]?.portImpedanceOhms;
154
+ const load = chain[slot]?.portImpedanceOhms;
155
+ if (source === null ||
156
+ source === undefined ||
157
+ load === null ||
158
+ load === undefined) {
159
+ continue;
160
+ }
161
+ const out = source.output;
162
+ const inn = load.input;
163
+ if ((out === null) === (inn === null)) {
164
+ continue;
165
+ }
166
+ advisories.push({
167
+ code: "undividable-seam",
168
+ slots: [slot - 1, slot],
169
+ message: out === null
170
+ ? `slot ${slot} declares a ${inn} Ω input impedance but slot ${slot - 1} declares ` +
171
+ "no output impedance, so no loading is applied and the declared figure has no " +
172
+ "effect: a low input impedance is meant to load what drives it"
173
+ : `slot ${slot - 1} declares a ${out} Ω output impedance but slot ${slot} declares ` +
174
+ "no input impedance, so the signal crosses without loading and the declared " +
175
+ "figure has no effect",
176
+ });
177
+ }
178
+ return advisories;
179
+ }
180
+ /**
181
+ * Bypass requested on a packet that declares no bypass switch.
182
+ *
183
+ * `audio.bypass: "none"` is a declared absence (37 core-only lanes + 1 passive volume, 38 total
184
+ * after backfill) — not a missing field. A chain "bypass" on such a slot has nothing in the
185
+ * circuit to mean; wire is the only honest choice, and the slot must say so via advisory, not
186
+ * log. Hard-coded to the Program's carried declaration when present; missing field (pre-backfill
187
+ * programs) is treated as "switch" (has bypass) until Y lands `program.bypass`.
188
+ */
189
+ export function bypassNotModelledAdvisories(slots) {
190
+ const advisories = [];
191
+ for (let idx = 0; idx < slots.length; idx++) {
192
+ const slot = slots[idx];
193
+ if (slot === undefined || slot.kind !== "program")
194
+ continue;
195
+ const bypassMode = slot.bypassMode ?? "effect";
196
+ if (bypassMode !== "wire" && bypassMode !== "buffer")
197
+ continue;
198
+ const bypass = slot.program.bypass;
199
+ if (bypass.declared === "none") {
200
+ advisories.push({
201
+ code: "bypass-not-modelled",
202
+ slots: [idx, idx],
203
+ message: `slot ${idx} bypass not modelled by this packet; slot is wired through (audio.bypass: "none" — declared absence, not a guess)`,
204
+ });
205
+ }
206
+ else if (bypass.kind === "not-in-audio-path") {
207
+ advisories.push({
208
+ code: "bypass-not-modelled",
209
+ slots: [idx, idx],
210
+ message: `slot ${idx} bypass switch is not in the audio path; slot is wired through`,
211
+ });
212
+ }
213
+ }
214
+ return advisories;
215
+ }
216
+ /**
217
+ * The multiplier applied to the signal crossing into `slot`.
218
+ *
219
+ * Two independent factors, multiplied:
220
+ *
221
+ * - the **full-scale ratio**, converting between the two slots' own voltage normalisations, or 1
222
+ * where either side states no supply to derive a bound from;
223
+ * - the **impedance divider** `Zin / (Zout + Zin)`, the loading one slot's input puts on the
224
+ * previous slot's output, or 1 where either electrical side declares none.
225
+ *
226
+ * They are separate questions and neither substitutes for the other: full scale is about the units
227
+ * a number is in, loading is about how much of the signal survives the connection. A seam can scale
228
+ * and not divide, or divide and not scale.
229
+ */
230
+ export function seamScale(chain, slot) {
231
+ const upstream = chain[slot - 1]?.portFullScaleVolts.output ?? null;
232
+ const downstream = chain[slot]?.portFullScaleVolts.input ?? null;
233
+ const ratio = upstream === null || downstream === null || downstream === 0
234
+ ? 1
235
+ : upstream / downstream;
236
+ return ratio * seamDivider(chain, slot);
237
+ }
238
+ /**
239
+ * The resistive divider at a seam, or `1` when it cannot be computed.
240
+ *
241
+ * **Resistive and one-directional, which is a stated approximation rather than an oversight.** Real
242
+ * interaction between two stages is bidirectional and frequency-dependent, and only co-solving the
243
+ * adjacent blocks captures it -- the fidelity ceiling the unified plan §5.3 names and this
244
+ * deliberately does not attempt. What this does capture is the audible first-order half: a fuzz with
245
+ * a low input impedance loads the pickup or the pedal ahead of it, which is a large part of what that
246
+ * pedal *is*.
247
+ *
248
+ * **No default is invented anywhere.** A missing impedance yields 1, never an assumed "standard
249
+ * 1 MΩ input" -- a fabricated convention would be indistinguishable from a measurement in every
250
+ * downstream number. Measured on the corpus as of 2026-08-21 that means this returns 1 for every
251
+ * seam: **zero input jacks declare a typed impedance**, and the only four declarations anywhere are
252
+ * output impedances (`marshall-blues-breaker` 1 MΩ, `mxr-m117r-flanger` 1 kΩ,
253
+ * `mxr-noise-gate-line-driver` 10 kΩ, `pigtronix-philosophers-tone` 150 Ω). The mechanism is
254
+ * therefore inert until sources carry the input side, and `undividable-seam` is what says so out
255
+ * loud on a chain built from one of those four.
256
+ */
257
+ export function seamDivider(chain, slot) {
258
+ const source = chain[slot - 1]?.portImpedanceOhms;
259
+ const load = chain[slot]?.portImpedanceOhms;
260
+ const out = source?.output ?? null;
261
+ const inn = load?.input ?? null;
262
+ if (out === null || inn === null) {
263
+ return 1;
264
+ }
265
+ const total = out + inn;
266
+ return total <= 0 ? 1 : inn / total;
267
+ }
268
+ //# sourceMappingURL=chain-advisories.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chain-advisories.js","sourceRoot":"","sources":["../src/chain-advisories.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,82 @@
1
+ import type { Program } from "@vessel-dsp/compiler";
2
+ /**
3
+ * Pure arithmetic: the multiplier `ChainRuntime` applies to one boundary's samples, from the
4
+ * two adjacent ports' full-scale voltages. `null` on either side is unity — see this
5
+ * module's header for why that is not a refusal.
6
+ */
7
+ export declare function chainScaleFactor(outFullScaleVolts: number | null, inFullScaleVolts: number | null): number;
8
+ /** One boundary where the chain fell back to unity because a full scale was `null`. */
9
+ export type ChainScaleAdvisory = {
10
+ /** The boundary sits between this slot and the next. */
11
+ readonly slot: number;
12
+ readonly message: string;
13
+ };
14
+ /**
15
+ * Every boundary in an ordered chain where at least one side's full-scale voltage is `null`,
16
+ * so `ChainRuntime` applied unity there instead of the declared ratio.
17
+ *
18
+ * Mirrors `supplyGroundConflicts`'s shape deliberately: a chain-level fact, computed from
19
+ * the programs alone, carried alongside a successful load rather than blocking one.
20
+ */
21
+ export declare function chainScaleAdvisories(programs: readonly Program[]): readonly ChainScaleAdvisory[];
22
+ /**
23
+ * The multiplier the **worklet** applies to the chain's last slot before the samples reach the
24
+ * DAC, so a program that swings to its supply rail does not peg the output against it.
25
+ *
26
+ * **This lives here because it was living in exactly one place and that place was not reachable
27
+ * from a report.** It was inline in `src/web/v2-audio-worklet.ts`, which no instrument imports, so
28
+ * every corpus measurement was taken on the program's output in volts while the app played that
29
+ * output divided by up to 558.61 (`marshall-1959-super-lead-plexi`). A packet could read a healthy
30
+ * 5e-3 V and reach the speaker at -101 dBFS. Both halves have to read the same rule or the
31
+ * measurement is not of the product.
32
+ *
33
+ * Unity below 1.0 V rather than a gain: this normalises a rail down, it never boosts a quiet
34
+ * program up to meet the ceiling.
35
+ */
36
+ export declare function dacScaleFactor(fullScaleOutVolts: number | null): number;
37
+ /**
38
+ * A program's output level in the domain a listener is in: dBFS at the DAC, after
39
+ * `dacScaleFactor`.
40
+ *
41
+ * **Volts and dBFS are not interchangeable and the corpus has been treating them as if they
42
+ * were.** `report-input-dependence.ts`'s `AUDIBLE_FLOOR` comment reconciles its absolute-volts bar
43
+ * against a tier's `-50 dBFS` one by taking "full scale 1.0" -- true of no packet in the corpus,
44
+ * where the declared full scale runs 4.5 V to 558.61 V. Converted through the rule the app
45
+ * actually applies, a single volts threshold is a different dBFS bar for every packet: 1e-3 V is
46
+ * -60 dBFS at full scale 1.0, -79 dBFS on a 9 V pedal and -115 dBFS on the plexi.
47
+ */
48
+ export declare function outputDbfs(rmsVolts: number, fullScaleOutVolts: number | null): number;
49
+ /**
50
+ * The number a level conversion divides an output by: the port's declared 0 dBFS
51
+ * reference where it states one, the derived ceiling for speaker-electrical outputs,
52
+ * or the instrument operating reference convention.
53
+ *
54
+ * These are different quantities -- `link.ts` says so and keeps them apart -- and a folding
55
+ * consumer has to pick the one its question needs. A conversion into a level (a +/-1 file,
56
+ * a DAC scale, a dBFS audibility judgement) wants the **reference** where a source states it:
57
+ * it is what the source says full scale *means* rather than what the node can reach.
58
+ *
59
+ * For `speaker-electrical` stage coverage (power amp secondary into speaker tap), the
60
+ * transformer-stepped ceiling (30 V to 500 V) is physically right to prevent DAC wrapping.
61
+ *
62
+ * For `instrument` stage coverage (stompboxes/pedals), the DC supply rail (e.g. 9 V) is an
63
+ * electrical clipping ceiling, never an operating reference level. Falling back to the rail
64
+ * caused pedals without an explicit output reference to play 19 dB quieter than input
65
+ * (0.3 V / 9 V = -29.5 dBFS). When no output reference is declared:
66
+ * 1. Read `portReferenceVolts.input` if declared (preserving the pedal's declared operating scale).
67
+ * 2. Fall back to 1.0 V, the convention matching digital full scale (1.0 digital = 1.0 V),
68
+ * which is the median and mode of the declared corpus population (59 of 65 declared inputs).
69
+ */
70
+ export declare function outputConversionFullScale(contract: {
71
+ readonly portFullScaleVolts: {
72
+ readonly input: number | null;
73
+ readonly output: number | null;
74
+ };
75
+ readonly portReferenceVolts?: {
76
+ readonly input: number | null;
77
+ readonly output: number | null;
78
+ };
79
+ readonly stageCoverage?: Program["stageCoverage"];
80
+ readonly produces?: Program["stageCoverage"];
81
+ }): number | null;
82
+ //# sourceMappingURL=chain-scale.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chain-scale.d.ts","sourceRoot":"","sources":["../src/chain-scale.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAEpD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC/B,iBAAiB,EAAE,MAAM,GAAG,IAAI,EAChC,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAC7B,MAAM,CAKR;AAED,uFAAuF;AACvF,MAAM,MAAM,kBAAkB,GAAG;IAChC,wDAAwD;IACxD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CACnC,QAAQ,EAAE,SAAS,OAAO,EAAE,GAC1B,SAAS,kBAAkB,EAAE,CAgB/B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,iBAAiB,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAEvE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAErF;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,yBAAyB,CACxC,QAAQ,EAAE;IACT,QAAQ,CAAC,kBAAkB,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAC/F,QAAQ,CAAC,kBAAkB,CAAC,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAChG,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;IAClD,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC;CAC7C,GACC,MAAM,GAAG,IAAI,CAqBf"}
@@ -0,0 +1,121 @@
1
+ // The chain's level convention: multiply between slots by the ratio of declared full-scale
2
+ // voltages, per the signal-chain plan
3
+ // (thoughts/shared/plans/2026-08-21-signal-chain-and-amp-output-stage.md) §3.1 —
4
+ //
5
+ // x_normalized(slot n+1) = y_normalized(slot n) * (fullScale(out n) / fullScale(in n+1))
6
+ //
7
+ // **Unity, not a refusal, when either side is `null`.** A program that declares no supply
8
+ // gives `Program.ports` nothing to derive a ceiling from — see `../compiler/port-full-
9
+ // scale.ts` — and the chain still has to produce audio: it is not wrong to play it, only
10
+ // unable to say the boundary's level is correct. `chainScaleAdvisories` is the visible half
11
+ // of that (structured, matching `../runtime/supply-ground.ts`'s pattern rather than a
12
+ // console.warn), so a host can surface it without the render itself refusing.
13
+ /**
14
+ * Pure arithmetic: the multiplier `ChainRuntime` applies to one boundary's samples, from the
15
+ * two adjacent ports' full-scale voltages. `null` on either side is unity — see this
16
+ * module's header for why that is not a refusal.
17
+ */
18
+ export function chainScaleFactor(outFullScaleVolts, inFullScaleVolts) {
19
+ if (outFullScaleVolts === null || inFullScaleVolts === null) {
20
+ return 1;
21
+ }
22
+ return outFullScaleVolts / inFullScaleVolts;
23
+ }
24
+ /**
25
+ * Every boundary in an ordered chain where at least one side's full-scale voltage is `null`,
26
+ * so `ChainRuntime` applied unity there instead of the declared ratio.
27
+ *
28
+ * Mirrors `supplyGroundConflicts`'s shape deliberately: a chain-level fact, computed from
29
+ * the programs alone, carried alongside a successful load rather than blocking one.
30
+ */
31
+ export function chainScaleAdvisories(programs) {
32
+ const advisories = [];
33
+ for (let slot = 0; slot < programs.length - 1; slot++) {
34
+ const out = programs[slot]?.portFullScaleVolts.output ?? null;
35
+ const nextIn = programs[slot + 1]?.portFullScaleVolts.input ?? null;
36
+ if (out === null || nextIn === null) {
37
+ advisories.push({
38
+ slot,
39
+ message: `slot ${slot}'s output or slot ${slot + 1}'s input declares no full-scale ` +
40
+ "voltage (its program declares no supply), so the chain applies unity " +
41
+ "scaling at this boundary instead of the declared ratio",
42
+ });
43
+ }
44
+ }
45
+ return advisories;
46
+ }
47
+ /**
48
+ * The multiplier the **worklet** applies to the chain's last slot before the samples reach the
49
+ * DAC, so a program that swings to its supply rail does not peg the output against it.
50
+ *
51
+ * **This lives here because it was living in exactly one place and that place was not reachable
52
+ * from a report.** It was inline in `src/web/v2-audio-worklet.ts`, which no instrument imports, so
53
+ * every corpus measurement was taken on the program's output in volts while the app played that
54
+ * output divided by up to 558.61 (`marshall-1959-super-lead-plexi`). A packet could read a healthy
55
+ * 5e-3 V and reach the speaker at -101 dBFS. Both halves have to read the same rule or the
56
+ * measurement is not of the product.
57
+ *
58
+ * Unity below 1.0 V rather than a gain: this normalises a rail down, it never boosts a quiet
59
+ * program up to meet the ceiling.
60
+ */
61
+ export function dacScaleFactor(fullScaleOutVolts) {
62
+ return fullScaleOutVolts !== null && fullScaleOutVolts > 1.0 ? 1.0 / fullScaleOutVolts : 1.0;
63
+ }
64
+ /**
65
+ * A program's output level in the domain a listener is in: dBFS at the DAC, after
66
+ * `dacScaleFactor`.
67
+ *
68
+ * **Volts and dBFS are not interchangeable and the corpus has been treating them as if they
69
+ * were.** `report-input-dependence.ts`'s `AUDIBLE_FLOOR` comment reconciles its absolute-volts bar
70
+ * against a tier's `-50 dBFS` one by taking "full scale 1.0" -- true of no packet in the corpus,
71
+ * where the declared full scale runs 4.5 V to 558.61 V. Converted through the rule the app
72
+ * actually applies, a single volts threshold is a different dBFS bar for every packet: 1e-3 V is
73
+ * -60 dBFS at full scale 1.0, -79 dBFS on a 9 V pedal and -115 dBFS on the plexi.
74
+ */
75
+ export function outputDbfs(rmsVolts, fullScaleOutVolts) {
76
+ return 20 * Math.log10(Math.max(rmsVolts * dacScaleFactor(fullScaleOutVolts), 1e-12));
77
+ }
78
+ /**
79
+ * The number a level conversion divides an output by: the port's declared 0 dBFS
80
+ * reference where it states one, the derived ceiling for speaker-electrical outputs,
81
+ * or the instrument operating reference convention.
82
+ *
83
+ * These are different quantities -- `link.ts` says so and keeps them apart -- and a folding
84
+ * consumer has to pick the one its question needs. A conversion into a level (a +/-1 file,
85
+ * a DAC scale, a dBFS audibility judgement) wants the **reference** where a source states it:
86
+ * it is what the source says full scale *means* rather than what the node can reach.
87
+ *
88
+ * For `speaker-electrical` stage coverage (power amp secondary into speaker tap), the
89
+ * transformer-stepped ceiling (30 V to 500 V) is physically right to prevent DAC wrapping.
90
+ *
91
+ * For `instrument` stage coverage (stompboxes/pedals), the DC supply rail (e.g. 9 V) is an
92
+ * electrical clipping ceiling, never an operating reference level. Falling back to the rail
93
+ * caused pedals without an explicit output reference to play 19 dB quieter than input
94
+ * (0.3 V / 9 V = -29.5 dBFS). When no output reference is declared:
95
+ * 1. Read `portReferenceVolts.input` if declared (preserving the pedal's declared operating scale).
96
+ * 2. Fall back to 1.0 V, the convention matching digital full scale (1.0 digital = 1.0 V),
97
+ * which is the median and mode of the declared corpus population (59 of 65 declared inputs).
98
+ */
99
+ export function outputConversionFullScale(contract) {
100
+ const declared = contract.portReferenceVolts?.output ?? null;
101
+ if (declared !== null && declared > 0) {
102
+ return declared;
103
+ }
104
+ const coverage = contract.stageCoverage ?? contract.produces;
105
+ if (coverage === "speaker-electrical") {
106
+ return contract.portFullScaleVolts.output;
107
+ }
108
+ if (coverage === "instrument") {
109
+ const inputRef = contract.portReferenceVolts?.input ?? null;
110
+ if (inputRef !== null && inputRef > 0) {
111
+ return inputRef;
112
+ }
113
+ return 1.0;
114
+ }
115
+ const inputRef = contract.portReferenceVolts?.input ?? null;
116
+ if (inputRef !== null && inputRef > 0) {
117
+ return inputRef;
118
+ }
119
+ return contract.portFullScaleVolts.output;
120
+ }
121
+ //# sourceMappingURL=chain-scale.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chain-scale.js","sourceRoot":"","sources":["../src/chain-scale.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,130 @@
1
+ import type { Program } from "@vessel-dsp/compiler";
2
+ /** How far down the amplification chain a signal already is. The compiler's own vocabulary. */
3
+ export type StageCoverage = Program["stageCoverage"];
4
+ /**
5
+ * What the chain needs to know about a slot, whatever runs inside it.
6
+ *
7
+ * `Program` satisfies the produced/full-scale half structurally, which is deliberate: the seam
8
+ * arithmetic is identical for a compiled circuit and an injected processor, and writing it twice
9
+ * would be how the two drift apart.
10
+ */
11
+ export type SlotContract = {
12
+ /** The coverage of this slot's **output**. */
13
+ readonly produces: StageCoverage;
14
+ /** The coverage this slot's **input** is meant to receive. */
15
+ readonly expects: StageCoverage;
16
+ readonly portFullScaleVolts: {
17
+ readonly input: number | null;
18
+ readonly output: number | null;
19
+ };
20
+ /**
21
+ * The port's declared 0 dBFS reference where the source states one, separate from the derived ceiling.
22
+ */
23
+ readonly portReferenceVolts?: {
24
+ readonly input: number | null;
25
+ readonly output: number | null;
26
+ };
27
+ /**
28
+ * Port impedances for the seam divider, or **`null` when this slot's ports are not electrical**.
29
+ *
30
+ * The two cases are different and must not collapse into one. A *circuit* seam with no declared
31
+ * impedance is **unknown** -- undeclared data, worth reporting once sources carry it. A
32
+ * *processor* seam has **nothing to declare**: a NAM capture already contains whatever loading
33
+ * existed when it was taken, and an impulse response or cab simulation is a filter rather than a
34
+ * source or a load. §3.2's own table says as much -- "IR / cabMic: unity, a filter, not a source".
35
+ *
36
+ * So a whole-object `null` means *not applicable*, and an inner `null` means *not stated*. Without
37
+ * that distinction the divider's advisory would fire on every NAM + cabinet chain, complaining
38
+ * about data that cannot exist.
39
+ */
40
+ readonly portImpedanceOhms: {
41
+ readonly input: number | null;
42
+ readonly output: number | null;
43
+ } | null;
44
+ };
45
+ /**
46
+ * A processor the host supplies: a NAM profile, an impulse response, the cab simulation.
47
+ *
48
+ * `process` takes one buffer and returns one buffer of the same length, sample-for-sample causal in
49
+ * the same sense `ReferenceRuntime.process` is -- the chain hands whole buffers between slots and
50
+ * relies on that.
51
+ *
52
+ * **A NAM amp capture declares `produces: "speaker-electrical"`, not `"miked"`.** A load-box
53
+ * capture is taken at the speaker terminal, which is what that value means -- the same coverage a
54
+ * whitebox amp's own output carries, so NAM + IR and amp + IR are one case rather than two. The great
55
+ * majority of captures on TONE3000 are taken through a reactive load box with no speaker and no
56
+ * microphone, precisely so the player adds their own IR, and NAM + IR is the ordinary chain. Only a
57
+ * profile that states it is a full-rig capture declares `miked`, and nothing here may infer that from
58
+ * a file name or a title.
59
+ */
60
+ export type ExternalProcessor = SlotContract & {
61
+ /** Named in advisories and telemetry, since a processor has no program to point at. */
62
+ readonly id: string;
63
+ prepare(sampleRate: number): void;
64
+ process(buffer: Float64Array): Float64Array;
65
+ };
66
+ /**
67
+ * **Nothing here distinguishes a NAM from an IR, on purpose.** Both are buffer-in, buffer-out black
68
+ * boxes, so the seam arithmetic, the ordering and the advisories are identical; `id` is a label for
69
+ * messages, never a discriminator. What the chain asks is not *what kind of processor is this* but
70
+ * *what coverage does it produce* -- which is why a NAM amp capture and a whitebox amp's own output
71
+ * collapse to one case, and why a cab simulator and an impulse response do too.
72
+ *
73
+ * They are not, however, the same *kind* of process, and one consequence survives the abstraction.
74
+ * An impulse response is linear and time-invariant: an unscaled seam into it is purely a level error,
75
+ * and a gain after it undoes the damage exactly. A NAM is a neural capture of a **nonlinear** amp, so
76
+ * the same signal at half the level does not merely arrive quieter -- it arrives with different
77
+ * harmonic content, and no later gain recovers it. So an `unscalable-seam` reported into a nonlinear
78
+ * slot is a **tone** error, where into a linear one it is only a level error.
79
+ *
80
+ * That distinction is recorded rather than encoded. Adding a `linear` flag to grade the advisory's
81
+ * severity would be a field with one speculative consumer, and the advisory already names the seam;
82
+ * a host that knows its processor is nonlinear knows to take that seam more seriously.
83
+ */
84
+ export type BypassMode = "wire" | "buffer" | "effect";
85
+ /**
86
+ * Row 8 tail step 1 — host mapping: bypass state → chain mode, per `program.bypass.kind`.
87
+ *
88
+ * The Studio's slot toggle already reads ENGAGED/BYPASSED, but the worklet chain omitted
89
+ * bypassed slots, so a bypassed buffered pedal ran no buffer and the mode plumbing
90
+ * (`setBypassMode`, `effectiveBypassMode`) never engaged in product. The host now keeps
91
+ * the slot and states the mode instead of dropping it.
92
+ *
93
+ * Returns `null` when no honest mode exists yet: `buffered` bypass needs the compiler's
94
+ * `bufferProgram` derivation (row 8 step 2), and requesting `buffer` without one falls back
95
+ * to the full program — which would play the effect under bypass. The caller omits those
96
+ * slots exactly as before, until step 2 lands. Everything else maps:
97
+ * engaged → `effect`; bypassed `true-bypass` → `wire`; `hardwire` → `wire`, with the input
98
+ * loading a hardwired output switch leaves behind still unmodelled (23 pedals by census —
99
+ * named here, not silent); `none` / `not-in-audio-path` → `wire`, agreeing with the
100
+ * runtime's own demotion guard.
101
+ */
102
+ export declare function resolveBypassMode(bypass: Program["bypass"], engaged: boolean): BypassMode | null;
103
+ export type ChainSlot = {
104
+ readonly kind: "program";
105
+ readonly program: Program;
106
+ readonly bufferProgram?: Program;
107
+ readonly bufferProcessor?: ExternalProcessor;
108
+ readonly bypassMode?: BypassMode;
109
+ } | {
110
+ readonly kind: "processor";
111
+ readonly processor: ExternalProcessor;
112
+ readonly bufferProcessor?: ExternalProcessor;
113
+ readonly bypassMode?: BypassMode;
114
+ };
115
+ /** A compiled circuit as a slot. `bufferProgram` is the buffer-only stages for buffered bypass. */
116
+ export declare function programSlot(program: Program, bufferProgram?: Program): ChainSlot;
117
+ /** A compiled circuit as a slot with explicit buffer processor (WASM). */
118
+ export declare function programSlotWithBufferProcessor(program: Program, bufferProcessor: ExternalProcessor, bufferProgram?: Program): ChainSlot;
119
+ /** An injected processor as a slot. */
120
+ export declare function processorSlot(processor: ExternalProcessor): ChainSlot;
121
+ /**
122
+ * The contract a slot presents to the seam arithmetic.
123
+ *
124
+ * A program's `expects` is `instrument` by construction -- its input is a jack. Nothing infers it,
125
+ * and no compiler field is needed for it.
126
+ */
127
+ export declare function slotContract(slot: ChainSlot): SlotContract;
128
+ /** How a slot names itself in a message. */
129
+ export declare function slotLabel(slot: ChainSlot, index: number): string;
130
+ //# sourceMappingURL=chain-slot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chain-slot.d.ts","sourceRoot":"","sources":["../src/chain-slot.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAEpD,+FAA+F;AAC/F,MAAM,MAAM,aAAa,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;AAErD;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG;IAC1B,8CAA8C;IAC9C,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC,8DAA8D;IAC9D,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,kBAAkB,EAAE;QAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;KAC/B,CAAC;IACF;;OAEG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE;QAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;KAC/B,CAAC;IACF;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,iBAAiB,EAAE;QAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;KAC/B,GAAG,IAAI,CAAC;CACT,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG;IAC9C,uFAAuF;IACvF,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,OAAO,CAAC,MAAM,EAAE,YAAY,GAAG,YAAY,CAAC;CAC5C,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEtD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAChC,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,EACzB,OAAO,EAAE,OAAO,GACd,UAAU,GAAG,IAAI,CAsBnB;AAED,MAAM,MAAM,SAAS,GAClB;IACA,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,eAAe,CAAC,EAAE,iBAAiB,CAAC;IAC7C,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC;CAChC,GACD;IACA,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,iBAAiB,CAAC;IACtC,QAAQ,CAAC,eAAe,CAAC,EAAE,iBAAiB,CAAC;IAC7C,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC;CAChC,CAAC;AAEL,mGAAmG;AACnG,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,aAAa,CAAC,EAAE,OAAO,GAAG,SAAS,CAEhF;AAED,0EAA0E;AAC1E,wBAAgB,8BAA8B,CAC7C,OAAO,EAAE,OAAO,EAChB,eAAe,EAAE,iBAAiB,EAClC,aAAa,CAAC,EAAE,OAAO,GACrB,SAAS,CAEX;AAED,uCAAuC;AACvC,wBAAgB,aAAa,CAAC,SAAS,EAAE,iBAAiB,GAAG,SAAS,CAErE;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,GAAG,YAAY,CAa1D;AAED,4CAA4C;AAC5C,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAIhE"}