@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
package/dist/settle.js ADDED
@@ -0,0 +1,900 @@
1
+ // When has a rendered circuit stopped moving?
2
+ //
3
+ // **This exists because the answer was assumed in every measurement that needed it, and the
4
+ // assumption was wrong by up to 62 dB.** `report-worklet-scoreboard.ts` rendered a fixed 0.5 s
5
+ // after a 0.1 s warm-up; measured settle times across the amp corpus run 1.5 s to 16 s, so every
6
+ // amp row was read mid-transient. `vox-ac15-top-boost` reported -15.3 dBFS, among the healthiest
7
+ // in the corpus, against -77.1 dBFS settled. `marshall-jcm800` moves 54 dB. A `fender-5e3`
8
+ // "self-oscillation" finding was raised and retracted the same day from a 0.4 s window.
9
+ //
10
+ // **And fixing it in one script is not enough, which is the reason this is here rather than
11
+ // there.** Within hours of the scoreboard fix landing, a throwaway node-probe rendered 10 s and
12
+ // measured the next second on an amp that settles at 9 s, and reported rail drift as 250 V of
13
+ // signal. A settling rule that lives inside one instrument is a rule every other instrument has
14
+ // to rediscover. This is the same move as `dacScaleFactor()` in `./chain-scale.ts`: the rule the
15
+ // product applies gets one home, and everything that needs it reads that one.
16
+ //
17
+ // **A modulation effect never settles by this criterion, and that is not a defect.** A phaser,
18
+ // tremolo or chorus sweeps its output amplitude by design, so "the level holds still" is a
19
+ // condition it cannot meet. `mxr-phase-45` reads 8.0e-2 V — around -41 dBFS, plainly audible —
20
+ // and this reports it UNSETTLED forever. A caller sweeping a corpus must read UNSETTLED as
21
+ // "no level could be taken", never as "no level exists": check the amplitude before concluding
22
+ // anything, because an unsettled row at 1e-9 and one at 8e-2 are opposite findings.
23
+ //
24
+ // **What "settled" means here, stated so a caller does not over-read it: the OUTPUT LEVEL stopped
25
+ // moving.** It does not mean the circuit reached its operating point. A packet whose output is
26
+ // flat at zero satisfies this immediately while its supply is still charging, so a settled verdict
27
+ // at or near zero is *unclassified*, not *silent* — `isFlatAtZero` is provided to say so, and
28
+ // callers reporting audibility must split on it. Closing that gap properly means watching a supply
29
+ // rail as well, which needs a rail the caller can name.
30
+ import { ReferenceRuntime } from "./reference-runtime.js";
31
+ /**
32
+ * Defaults measured against the corpus rather than chosen: 0.5 s resolves a 1.5 s settle without
33
+ * splitting hairs, and 0.25 dB is below the run-to-run spread.
34
+ *
35
+ * **The top rung is deliberately above anything the corpus reaches, and that is the point.** It was
36
+ * 16 s, justified as "covers `marshall-jcm800`, the slowest row found (settles at 16.0 s)" — which
37
+ * is not a bound, it is a coincidence. A ladder whose maximum equals the population maximum cannot
38
+ * tell *settled at the last rung* from *never settled*: both produce the same row, and jcm800's
39
+ * gain of 0.00 — the packet called the one real remaining amp defect all afternoon — was read at
40
+ * exactly that edge every time.
41
+ *
42
+ * Re-measured on a 64 s ladder, jcm800 settles at **10 s** and holds flat to five digits out to
43
+ * 64 s, so the diagnosis survives and the 16.0 s figure was an artifact of the two settling
44
+ * criteria this module used to have (see `windowLevel`). The rung is raised anyway: the reason to
45
+ * have headroom is not that anything needed it, it is that without it a maximum reading is
46
+ * ambiguous. Same argument as a screen needing a *no valid measurement* state distinct from a
47
+ * negative result. It costs nothing for a packet that settles earlier.
48
+ */
49
+ export const SETTLE_DEFAULTS = {
50
+ windowSeconds: 0.5,
51
+ consecutive: 2,
52
+ stableDb: 0.25,
53
+ ladderSeconds: [2, 4, 8, 16, 32],
54
+ };
55
+ /** A level below this is numerical noise, not a measurement. */
56
+ export const FLAT_AT_ZERO_RMS = 1e-6;
57
+ /** `true` when a settled verdict cannot distinguish a dead circuit from one still coming up. */
58
+ export function isFlatAtZero(rms) {
59
+ return rms < FLAT_AT_ZERO_RMS;
60
+ }
61
+ /**
62
+ * The first window at which the level holds still, or `null` if it never does.
63
+ *
64
+ * Pure, so an instrument that renders its own audio — in a browser page, through the WASM console,
65
+ * or anywhere else this module cannot reach — can still share the policy by handing over its
66
+ * window levels.
67
+ */
68
+ export function findSettled(windowRms, options = SETTLE_DEFAULTS) {
69
+ let stable = 0;
70
+ for (let i = 1; i < windowRms.length; i += 1) {
71
+ const previous = 20 * Math.log10(Math.max(windowRms[i - 1] ?? 0, 1e-12));
72
+ const current = 20 * Math.log10(Math.max(windowRms[i] ?? 0, 1e-12));
73
+ stable = Math.abs(current - previous) < options.stableDb ? stable + 1 : 0;
74
+ if (stable >= options.consecutive) {
75
+ return {
76
+ rms: windowRms[i] ?? 0,
77
+ seconds: (i + 1) * options.windowSeconds,
78
+ };
79
+ }
80
+ }
81
+ return null;
82
+ }
83
+ /**
84
+ * The level of one window: rms **about the mean**, so a DC pedestal is not counted as audio.
85
+ *
86
+ * **This exists because the module had two settling criteria and they disagreed.** `measureSettled`
87
+ * centred each window; the settling loops inside `measureInputAttributable` and
88
+ * `measureSettledSignal` squared the raw samples. Same program, same stimulus, same ladder, and on
89
+ * `marshall-jcm800` the first settles the idle render at 10 s while the second never settles it at
90
+ * all -- because that packet's DC offset is still drifting long after its AC has arrived. That
91
+ * disagreement was read as "jcm800 settles at 16.0 s, at the very top of the ladder", which made a
92
+ * packet look like it was measured at the edge of the instrument's range when it had in fact
93
+ * arrived at 10 s and held flat to five digits out to 64 s.
94
+ *
95
+ * A shared policy module with two spellings of its own policy is the defect it was created to
96
+ * remove, so there is one now.
97
+ */
98
+ function windowLevel(output, perWindow) {
99
+ let sum = 0;
100
+ for (let i = 0; i < perWindow; i += 1)
101
+ sum += output[i] ?? 0;
102
+ const dc = sum / perWindow;
103
+ let square = 0;
104
+ for (let i = 0; i < perWindow; i += 1)
105
+ square += ((output[i] ?? 0) - dc) ** 2;
106
+ return Math.sqrt(square / perWindow);
107
+ }
108
+ /**
109
+ * Render a program until its output level stops moving, and report that level.
110
+ *
111
+ * The runtime is stateful across `process()` calls, so this walks forward through one continuous
112
+ * render rather than restarting per rung — which both costs less and is the only honest way to
113
+ * measure a circuit that is still charging.
114
+ *
115
+ * `sampleAt` is the stimulus, in volts, as a function of absolute sample index; passing the same
116
+ * function every instrument uses is what makes two measurements comparable.
117
+ */
118
+ export function measureSettled(program, sampleRate, sampleAt, options = SETTLE_DEFAULTS) {
119
+ const runtime = new ReferenceRuntime(program);
120
+ runtime.prepare(sampleRate);
121
+ const perWindow = Math.round(options.windowSeconds * sampleRate);
122
+ const maxWindows = Math.ceil((options.ladderSeconds[options.ladderSeconds.length - 1] ?? 16) /
123
+ options.windowSeconds);
124
+ const windows = [];
125
+ let index = 0;
126
+ let peak = 0;
127
+ for (let w = 0; w < maxWindows; w += 1) {
128
+ const input = new Float64Array(perWindow);
129
+ for (let i = 0; i < perWindow; i += 1)
130
+ input[i] = sampleAt(index + i);
131
+ index += perWindow;
132
+ const output = runtime.process(input);
133
+ let sum = 0;
134
+ for (let i = 0; i < perWindow; i += 1)
135
+ sum += output[i] ?? 0;
136
+ const dc = sum / perWindow;
137
+ let square = 0;
138
+ let windowPeak = 0;
139
+ for (let i = 0; i < perWindow; i += 1) {
140
+ const centred = (output[i] ?? 0) - dc;
141
+ square += centred * centred;
142
+ if (Math.abs(centred) > windowPeak)
143
+ windowPeak = Math.abs(centred);
144
+ }
145
+ windows.push(Math.sqrt(square / perWindow));
146
+ peak = windowPeak;
147
+ const settled = findSettled(windows, options);
148
+ if (settled !== null) {
149
+ return {
150
+ rms: settled.rms,
151
+ peak,
152
+ settleSeconds: settled.seconds,
153
+ renderedSeconds: (w + 1) * options.windowSeconds,
154
+ flatAtZero: isFlatAtZero(settled.rms),
155
+ };
156
+ }
157
+ }
158
+ return {
159
+ rms: null,
160
+ peak,
161
+ settleSeconds: null,
162
+ renderedSeconds: maxWindows * options.windowSeconds,
163
+ flatAtZero: false,
164
+ };
165
+ }
166
+ /**
167
+ * Per-node AC attributable to the input: render twice, once driven and once silent, and
168
+ * difference the two.
169
+ *
170
+ * **A plain per-node amplitude list is unreadable on anything with a mains supply.**
171
+ * `mesa-boogie-dual-rectifier` carries 495 V of rail ripple, which occupies the top of any such
172
+ * list and buries a preamp running at 132 V — the signal was invisible until the two renders were
173
+ * differenced, and then the fault (power-tube grids sitting on the fixed bias rail with no drive
174
+ * node at all) fell out in one reading. Pedals have supply ripple too, so this is not an
175
+ * amp-only tool.
176
+ *
177
+ * Both renders settle first, via the same policy as everything else here, because differencing
178
+ * two unsettled renders subtracts one transient from another.
179
+ */
180
+ export function measureInputAttributable(program, sampleRate, sampleAt, options = SETTLE_DEFAULTS) {
181
+ const sweep = (drive) => {
182
+ const runtime = new ReferenceRuntime(program);
183
+ runtime.prepare(sampleRate);
184
+ const perWindow = Math.round(options.windowSeconds * sampleRate);
185
+ const maxWindows = Math.ceil((options.ladderSeconds[options.ladderSeconds.length - 1] ?? 16) /
186
+ options.windowSeconds);
187
+ const windows = [];
188
+ let index = 0;
189
+ for (let w = 0; w < maxWindows; w += 1) {
190
+ const input = new Float64Array(perWindow);
191
+ for (let i = 0; i < perWindow; i += 1)
192
+ input[i] = drive * sampleAt(index + i);
193
+ index += perWindow;
194
+ const output = runtime.process(input);
195
+ windows.push(windowLevel(output, perWindow));
196
+ if (findSettled(windows, options) !== null)
197
+ break;
198
+ }
199
+ // One further window, sampled per node, now that the level has stopped moving.
200
+ const lo = new Map();
201
+ const hi = new Map();
202
+ const single = new Float64Array(1);
203
+ for (let i = 0; i < perWindow; i += 1) {
204
+ single[0] = drive * sampleAt(index + i);
205
+ runtime.process(single);
206
+ for (const snapshot of runtime.nodeVoltageSnapshot()) {
207
+ let low = lo.get(snapshot.blockId);
208
+ let high = hi.get(snapshot.blockId);
209
+ if (low === undefined || high === undefined) {
210
+ low = new Array(snapshot.nodeCount).fill(Number.POSITIVE_INFINITY);
211
+ high = new Array(snapshot.nodeCount).fill(Number.NEGATIVE_INFINITY);
212
+ lo.set(snapshot.blockId, low);
213
+ hi.set(snapshot.blockId, high);
214
+ }
215
+ for (let k = 0; k < snapshot.nodeCount; k += 1) {
216
+ const value = snapshot.voltages[k] ?? 0;
217
+ if (value < low[k])
218
+ low[k] = value;
219
+ if (value > high[k])
220
+ high[k] = value;
221
+ }
222
+ }
223
+ }
224
+ const amplitude = new Map();
225
+ for (const [blockId, low] of lo) {
226
+ const high = hi.get(blockId) ?? [];
227
+ amplitude.set(blockId, low.map((value, k) => ((high[k] ?? 0) - value) / 2));
228
+ }
229
+ return amplitude;
230
+ };
231
+ const loud = sweep(1);
232
+ const silent = sweep(0);
233
+ const rows = [];
234
+ for (const [blockId, amps] of loud) {
235
+ const quiet = silent.get(blockId) ?? [];
236
+ for (let node = 0; node < amps.length; node += 1) {
237
+ const loudVolts = amps[node] ?? 0;
238
+ const silentVolts = quiet[node] ?? 0;
239
+ rows.push({
240
+ blockId,
241
+ node,
242
+ signalVolts: loudVolts - silentVolts,
243
+ loudVolts,
244
+ silentVolts,
245
+ });
246
+ }
247
+ }
248
+ return rows.sort((a, b) => b.signalVolts - a.signalVolts);
249
+ }
250
+ /**
251
+ * How many fine windows make a coarse one.
252
+ *
253
+ * **The coarse pass is derived from the fine pass's retained windows and renders nothing.** The
254
+ * detector already runs on windowed rms (`windowLevel`), so the only defect on a modulated packet
255
+ * is that a 0.5 s window is *shorter than the modulation* and its rms still wobbles. A coarse
256
+ * window is just eight consecutive fine ones, and the dedup already keeps every window it renders —
257
+ * so the fallback costs **no extra render**, which is what makes it a fallback rather than a
258
+ * trade.
259
+ *
260
+ * **A blanket coarse window was measured and refused.** On four packets that settle today it
261
+ * reproduced every published digit of every rms — the statistic is sound — but `consecutive: 2`
262
+ * at 4 s means a packet settling at 4.5 s cannot be declared settled before 16 s, and
263
+ * `marshall-jtm45` nearly quadrupled. That is the best members paying for the worst, which is the
264
+ * same shape as a shared stopping condition and is refused for the same reason.
265
+ *
266
+ * **So: the coarse pass may SUPPLY a settle point, never REPLACE one.** It runs only where the
267
+ * fine pass found none, so it can only add measurements and never change existing ones — which is
268
+ * also why the settling-packet control set stays valid across this change.
269
+ */
270
+ const COARSE_FACTOR = 8;
271
+ function renderToSharedWindow(program, sampleRate, sampleAt, drives, options) {
272
+ const perWindow = Math.round(options.windowSeconds * sampleRate);
273
+ const maxWindows = Math.ceil((options.ladderSeconds[options.ladderSeconds.length - 1] ?? 16) /
274
+ options.windowSeconds);
275
+ const inputFor = (drive, w) => {
276
+ const input = new Float64Array(perWindow);
277
+ for (let i = 0; i < perWindow; i += 1)
278
+ input[i] = drive * sampleAt(w * perWindow + i);
279
+ return input;
280
+ };
281
+ const runtimes = drives.map(() => {
282
+ const runtime = new ReferenceRuntime(program);
283
+ runtime.prepare(sampleRate);
284
+ return runtime;
285
+ });
286
+ // Every window each drive renders, so reaching the shared window later never re-renders.
287
+ const rendered = drives.map(() => []);
288
+ const settleAt = drives.map(() => -1);
289
+ // Kept per drive rather than per loop: the coarse fallback re-tests these same levels.
290
+ const levels = drives.map(() => []);
291
+ for (const [d, drive] of drives.entries()) {
292
+ for (let w = 0; w < maxWindows; w += 1) {
293
+ const output = runtimes[d].process(inputFor(drive, w));
294
+ rendered[d].push(Float64Array.from(output));
295
+ levels[d].push(windowLevel(output, perWindow));
296
+ if (findSettled(levels[d], options) !== null) {
297
+ settleAt[d] = w + 1;
298
+ break;
299
+ }
300
+ }
301
+ }
302
+ let latest = Math.max(...settleAt);
303
+ let criterion = latest >= 0 ? "fine" : "unsettled";
304
+ let coarseGroup = -1;
305
+ if (latest < 0) {
306
+ // Nothing settled on the fine window. Re-test the SAME rendered data at coarse granularity:
307
+ // group the per-window levels in eights and ask the same detector. A modulated output whose
308
+ // 0.5 s rms wobbles is stationary in 4 s rms once the window covers the modulation.
309
+ const coarseAt = drives.map((_unused, d) => {
310
+ const fine = levels[d] ?? [];
311
+ const coarse = [];
312
+ for (let g = 0; (g + 1) * COARSE_FACTOR <= fine.length; g += 1) {
313
+ let power = 0;
314
+ for (let i = 0; i < COARSE_FACTOR; i += 1) {
315
+ const v = fine[g * COARSE_FACTOR + i] ?? 0;
316
+ power += v * v;
317
+ }
318
+ coarse.push(Math.sqrt(power / COARSE_FACTOR));
319
+ if (findSettled(coarse, options) !== null)
320
+ return g + 1;
321
+ }
322
+ return -1;
323
+ });
324
+ const latestCoarse = Math.max(...coarseAt);
325
+ if (latestCoarse >= 0) {
326
+ coarseGroup = latestCoarse;
327
+ // The shared window is the last fine window of the settled coarse group, so the samples
328
+ // handed back are ones already rendered.
329
+ latest = Math.min(latestCoarse * COARSE_FACTOR + COARSE_FACTOR - 1, maxWindows - 1);
330
+ criterion = "coarse";
331
+ }
332
+ }
333
+ const settled = latest >= 0;
334
+ const skip = settled ? latest : maxWindows - 1;
335
+ void coarseGroup;
336
+ const windows = drives.map((drive, d) => {
337
+ // Advance this drive from where it stopped to the shared window. A drive that already
338
+ // passed it -- one that never settled while another did -- simply has it in hand.
339
+ for (let w = rendered[d].length; w <= skip; w += 1) {
340
+ rendered[d].push(Float64Array.from(runtimes[d].process(inputFor(drive, w))));
341
+ }
342
+ return rendered[d][skip];
343
+ });
344
+ return { settleAt, windows, skip, settled, criterion };
345
+ }
346
+ /**
347
+ * Measure several drive levels over ONE window, after every one of them has settled.
348
+ *
349
+ * **Two drives measured at their own settle points are not two views of one state.** Mains ripple
350
+ * has a phase tied to absolute time, so comparing or subtracting windows captured at different
351
+ * offsets does not cancel it and can double it — measured directly on 2026-09-09, where letting a
352
+ * driven and a silent render each stop at its own settle time reported `fender-twin-reverb` with
353
+ * more signal than total output and `hiwatt-dr103`, then believed to be a working amp, with a
354
+ * signal of exactly zero.
355
+ *
356
+ * `measureSettledSignal` is the two-drive difference case of this. This exists because callers
357
+ * needing three or more levels — quiet, loud and silent, which is what a liveness screen compares —
358
+ * were otherwise reimplementing the settle-then-capture policy themselves, and **a shared policy
359
+ * module only helps if the callers can get what they need from it.** Every instrument that
360
+ * hardcoded its own render window instead was wrong by up to 42 dB on the amp corpus.
361
+ *
362
+ * The window is the latest settle point across the drives, so every render is steady across it. A
363
+ * drive that never settles does not veto the others: it contributes nothing to the choice of
364
+ * window and is reported over the same one, which is a level taken late rather than no level.
365
+ */
366
+ export function measureSharedWindow(program, sampleRate, sampleAt, drives, options = SETTLE_DEFAULTS) {
367
+ const perWindow = Math.round(options.windowSeconds * sampleRate);
368
+ const rendered = renderToSharedWindow(program, sampleRate, sampleAt, drives, options);
369
+ const skip = rendered.skip;
370
+ const settled = rendered.settled;
371
+ const measurements = rendered.windows.map((output) => {
372
+ let sum = 0;
373
+ for (let i = 0; i < perWindow; i += 1)
374
+ sum += output[i] ?? 0;
375
+ const dc = sum / perWindow;
376
+ let square = 0;
377
+ let peak = 0;
378
+ for (let i = 0; i < perWindow; i += 1) {
379
+ const centred = (output[i] ?? 0) - dc;
380
+ square += centred * centred;
381
+ if (Math.abs(centred) > peak)
382
+ peak = Math.abs(centred);
383
+ }
384
+ return { rms: Math.sqrt(square / perWindow), dc, peak };
385
+ });
386
+ return {
387
+ settleSeconds: settled ? skip * options.windowSeconds : null,
388
+ criterion: rendered.criterion,
389
+ measurements,
390
+ };
391
+ }
392
+ export function measureSettledSignal(program, sampleRate, sampleAt, options = SETTLE_DEFAULTS) {
393
+ // **Both renders must be sampled over the SAME absolute sample range**, which is why this
394
+ // settles first and captures second rather than capturing at each render's own settle point.
395
+ // Mains ripple has a phase tied to absolute time, so differencing two windows taken at
396
+ // different offsets does not cancel it — it can double it. Measured directly: letting the
397
+ // driven and silent renders each stop at their own settle time reported `fender-twin-reverb`
398
+ // with more signal than total output (-51% ripple share) and `hiwatt-dr103`, a working amp at
399
+ // gain 89, with a signal of exactly 0.00.
400
+ const rendered = renderToSharedWindow(program, sampleRate, sampleAt, [1, 0], options);
401
+ const drivenWindows = rendered.settleAt[0] ?? -1;
402
+ if (drivenWindows < 0) {
403
+ // Never settled: no level rather than a wrong one, and no attribution either -- a
404
+ // fraction computed from a level that does not exist is the shape this type exists to
405
+ // prevent.
406
+ return {
407
+ rms: null,
408
+ peak: 0,
409
+ total: 0,
410
+ attributable: null,
411
+ settleSeconds: null,
412
+ renderedSeconds: 0,
413
+ flatAtZero: false,
414
+ };
415
+ }
416
+ // The later of the two settle points, so both circuits are steady over the captured window --
417
+ // and both windows come out of the SAME render, so they cannot drift apart.
418
+ const skip = rendered.skip;
419
+ const perWindow = Math.round(options.windowSeconds * sampleRate);
420
+ const driven = rendered.windows[0];
421
+ const silent = rendered.windows[1];
422
+ let sum = 0;
423
+ for (let i = 0; i < perWindow; i += 1)
424
+ sum += (driven[i] ?? 0) - (silent[i] ?? 0);
425
+ const dc = sum / perWindow;
426
+ let square = 0;
427
+ let peak = 0;
428
+ for (let i = 0; i < perWindow; i += 1) {
429
+ const centred = (driven[i] ?? 0) - (silent[i] ?? 0) - dc;
430
+ square += centred * centred;
431
+ if (Math.abs(centred) > peak)
432
+ peak = Math.abs(centred);
433
+ }
434
+ const rms = Math.sqrt(square / perWindow);
435
+ // The total, from the SAME captured window as the signal -- see `AttributedMeasurement`.
436
+ let drivenSum = 0;
437
+ for (let i = 0; i < perWindow; i += 1)
438
+ drivenSum += driven[i] ?? 0;
439
+ const drivenDc = drivenSum / perWindow;
440
+ let drivenSquare = 0;
441
+ for (let i = 0; i < perWindow; i += 1) {
442
+ const centred = (driven[i] ?? 0) - drivenDc;
443
+ drivenSquare += centred * centred;
444
+ }
445
+ const total = Math.sqrt(drivenSquare / perWindow);
446
+ return {
447
+ rms,
448
+ peak,
449
+ total,
450
+ attributable: total === 0 ? null : Math.min(1, rms / total),
451
+ settleSeconds: skip * options.windowSeconds,
452
+ renderedSeconds: (skip + 1) * options.windowSeconds,
453
+ flatAtZero: isFlatAtZero(rms),
454
+ };
455
+ }
456
+ /**
457
+ * Known-answer controls for the measurement primitives here.
458
+ *
459
+ * **Every measurement function should have an input whose answer is known independently of the
460
+ * measurement.** Today both broken instruments were in the position of never having been pointed
461
+ * at one: broadband plate correlation was never checked against a circuit whose phase relationship
462
+ * was known, and `measureSettledSignal` shipped with an alignment defect that a single identity
463
+ * assertion would have caught before it produced a table with more signal than total output.
464
+ *
465
+ * The identity here is the cheapest control that exists: **difference a render against itself and
466
+ * the answer must be exactly zero.** Any misalignment, window mismatch, or off-by-one in the sample
467
+ * range breaks it immediately and loudly.
468
+ */
469
+ export function settleSelfCheck(program, sampleRate, sampleAt, options = SETTLE_DEFAULTS) {
470
+ const results = [];
471
+ // Identity: the same render differenced against itself is zero, exactly.
472
+ const perWindow = Math.round(options.windowSeconds * sampleRate);
473
+ const render = (drive, skip) => {
474
+ const runtime = new ReferenceRuntime(program);
475
+ runtime.prepare(sampleRate);
476
+ for (let w = 0; w < skip; w += 1) {
477
+ const input = new Float64Array(perWindow);
478
+ for (let i = 0; i < perWindow; i += 1)
479
+ input[i] = drive * sampleAt(w * perWindow + i);
480
+ runtime.process(input);
481
+ }
482
+ const tail = new Float64Array(perWindow);
483
+ for (let i = 0; i < perWindow; i += 1)
484
+ tail[i] = drive * sampleAt(skip * perWindow + i);
485
+ return Float64Array.from(runtime.process(tail));
486
+ };
487
+ const a = render(1, 4);
488
+ const b = render(1, 4);
489
+ let worst = 0;
490
+ for (let i = 0; i < perWindow; i += 1)
491
+ worst = Math.max(worst, Math.abs((a[i] ?? 0) - (b[i] ?? 0)));
492
+ results.push({
493
+ name: "identity: render minus itself is exactly zero",
494
+ ok: worst === 0,
495
+ detail: `worst absolute difference ${worst.toExponential(3)} V`,
496
+ });
497
+ // **"signal <= ceiling" was asserted here as a law and it is not one.** It fired on
498
+ // `dumble-overdrive-special` -- signal 6.933e-1 against a ceiling of 6.655e-1 -- and the
499
+ // measurement was right while the assertion was wrong. The law is the conservation identity:
500
+ //
501
+ // rms(driven)^2 = rms(signal)^2 + rms(idle)^2 + 2 * cov(signal, idle)
502
+ //
503
+ // `signal <= ceiling` follows from it **only when cov >= 0**, and nothing guarantees that sign.
504
+ // On dumble cov is -3.827e-2: the idle content is anti-correlated with the response to the
505
+ // stimulus, so removing it leaves more than was there in total. So the identity is what gets
506
+ // asserted, with a tolerance, and the inequality is not asserted at all. An anti-correlated
507
+ // idle passes; a window mismatch fails.
508
+ //
509
+ // **cov must be measured, not derived.** Solving the identity for cov and then checking the
510
+ // identity is circular -- over any two arrays it is just the expansion of a variance and cannot
511
+ // fail. cov is therefore taken from the samples of a shared-window capture, while the three
512
+ // terms come from the three public functions, each settling on its own. The check is then a
513
+ // real one: it asks whether those three functions are describing the same window. The alignment
514
+ // defect fixed earlier today -- driven and silent settled independently, so a time-phased ripple
515
+ // subtracted at the wrong offset -- is exactly what breaks it.
516
+ //
517
+ // **A control that cries wolf costs what a control that misses costs, and both directions
518
+ // belong in the rule.** Fourteen of today's defects were controls that missed. This was nearly
519
+ // the first of the other kind, and it would have blocked valid work rather than passing invalid
520
+ // work.
521
+ const ceiling = measureSettled(program, sampleRate, sampleAt, options);
522
+ const signal = measureSettledSignal(program, sampleRate, sampleAt, options);
523
+ if (ceiling.rms !== null && signal.rms !== null) {
524
+ const idle = measureSettled(program, sampleRate, () => 0, options);
525
+ const idleRms = idle.rms ?? 0;
526
+ // cov from the samples, over one window both renders share.
527
+ const skip = Math.max(4, Math.round((signal.settleSeconds ?? 0) / options.windowSeconds));
528
+ const drivenTail = render(1, skip);
529
+ const silentTail = render(0, skip);
530
+ let meanSignal = 0;
531
+ let meanIdle = 0;
532
+ for (let i = 0; i < perWindow; i += 1) {
533
+ meanSignal += (drivenTail[i] ?? 0) - (silentTail[i] ?? 0);
534
+ meanIdle += silentTail[i] ?? 0;
535
+ }
536
+ meanSignal /= perWindow;
537
+ meanIdle /= perWindow;
538
+ let covariance = 0;
539
+ for (let i = 0; i < perWindow; i += 1) {
540
+ covariance +=
541
+ ((drivenTail[i] ?? 0) - (silentTail[i] ?? 0) - meanSignal) *
542
+ ((silentTail[i] ?? 0) - meanIdle);
543
+ }
544
+ covariance /= perWindow;
545
+ const residual = ceiling.rms ** 2 - (signal.rms ** 2 + idleRms ** 2 + 2 * covariance);
546
+ const relative = ceiling.rms === 0 ? 0 : Math.abs(residual) / ceiling.rms ** 2;
547
+ // 10% of the driven power. Calibrated rather than chosen: legitimate window-to-window drift
548
+ // between two independently-settled measurements is a few percent (`fender-twin-reverb`
549
+ // reads 4.1242 settled independently against 4.2108 in the shared window, 2.1%), while the
550
+ // alignment defect this is aimed at produced errors around 50% -- a ripple term doubled
551
+ // instead of cancelled, reported as a -51.2% ripple share.
552
+ //
553
+ // **What this proves and what it cannot.** Measured by injecting a deliberate offset
554
+ // between the driven and silent captures: on `fender-twin-reverb` one window of slip takes
555
+ // the residual from 0.29% to **208%**, so the check does fail for the reason claimed, on
556
+ // the very amp the original alignment defect was found on. On `marshall-1959-super-lead-
557
+ // plexi` every offset stays at 0.07%, and on `dumble-overdrive-special` at 0.27-0.59%.
558
+ //
559
+ // So it is **only sensitive where the idle is both large and time-phased.** The plexi's
560
+ // idle is 1.374e-4 V against a 7.18 V signal -- five decades down, so sliding it changes
561
+ // nothing measurable -- and dumble's idle is broadband rather than periodic, so an offset
562
+ // does not decorrelate it. A pass on a quiet-idle circuit says the arithmetic is consistent,
563
+ // not that the windows are aligned. Run it on a hum-heavy amp to exercise the alignment
564
+ // claim.
565
+ results.push({
566
+ name: "driven power = signal + idle + 2cov, with cov measured from the samples",
567
+ ok: relative <= 0.1,
568
+ detail: `driven ${ceiling.rms.toExponential(3)}, signal ${signal.rms.toExponential(3)}, ` +
569
+ `idle ${idleRms.toExponential(3)}, cov ${covariance.toExponential(3)}; ` +
570
+ `residual ${(relative * 100).toFixed(2)}% of driven power`,
571
+ });
572
+ }
573
+ // **The coarse fallback must be proven able to fire, and able NOT to.** An unfired fallback is
574
+ // an untested path, and this session already shipped one gate that was broken exactly where
575
+ // nothing had ever run it -- the instrument digest, which died at module scope because
576
+ // `fileURLToPath` was never imported and no invocation had reached the line.
577
+ //
578
+ // The construction is the SIGNAL, not a packet: this program passes its input through, so a
579
+ // stimulus whose amplitude is modulated at 0.5 Hz has a 2 s period -- **longer than one fine
580
+ // window (0.5 s) and shorter than one coarse window (8 x 0.5 = 4 s)**. Its windowed rms
581
+ // therefore wobbles at fine granularity and is stationary at coarse. That inequality IS the
582
+ // fallback's specification, and it had not been written down anywhere until this control.
583
+ const hz = 440;
584
+ const modulated = (index) => Math.sin((2 * Math.PI * hz * index) / sampleRate) *
585
+ (1 + 0.9 * Math.sin((2 * Math.PI * 0.5 * index) / sampleRate));
586
+ const plain = (index) => Math.sin((2 * Math.PI * hz * index) / sampleRate);
587
+ const fires = measureSharedWindow(program, sampleRate, modulated, [1], options);
588
+ results.push({
589
+ name: "coarse fallback fires on a 2 s modulation",
590
+ ok: fires.criterion === "coarse",
591
+ detail: `criterion=${fires.criterion} settle=${String(fires.settleSeconds)} -- a period longer than one fine window and shorter than one coarse window must be rejected finely and accepted coarsely`,
592
+ });
593
+ const doesNotFire = measureSharedWindow(program, sampleRate, plain, [1], options);
594
+ results.push({
595
+ name: "coarse fallback does NOT fire on a steady tone",
596
+ ok: doesNotFire.criterion === "fine",
597
+ detail: `criterion=${doesNotFire.criterion} settle=${String(doesNotFire.settleSeconds)} -- the mirror case: a fallback that always fires is not a fallback`,
598
+ });
599
+ return results;
600
+ }
601
+ /**
602
+ * **Render until settled, or refuse.** The shortest path to a level in this repo -- shorter than
603
+ * `new ReferenceRuntime(p); r.prepare(sr); r.process(new Float64Array(n))`, which is the three
604
+ * lines it exists to displace.
605
+ *
606
+ * **Why it is shaped to be the easy path rather than the correct-but-optional one.** Four ad-hoc
607
+ * probes on 2026-09-10 rendered a fixed window shorter than the packet's settle time and produced
608
+ * four false findings -- an amp "with no B+", an "unstable" packet that was damping 43 dB, an idle
609
+ * "instability" of 7.7 V that settles to 1.7e-5, and a gain of 0.000 on an amp that makes 234.
610
+ * Every one was written by someone who knew about `settle.ts` and reached for
611
+ * `ReferenceRuntime` anyway, because it was two fewer decisions. A helper that has to be
612
+ * remembered gets bypassed; `bg_start` was bypassed three times through three different doors on
613
+ * the same day. So this is deliberately the least typing available: one call, no window to choose,
614
+ * and choosing your own is strictly more work than not.
615
+ *
616
+ * The settle policy is `findSettled` and the level is `windowLevel` -- the module's single
617
+ * spelling of each, not a second copy.
618
+ */
619
+ /** `sampleAt` if given, else a tone from `hz`/`drive`, else `null` for silence. */
620
+ function resolveStimulus(options) {
621
+ if (options.sampleAt !== undefined)
622
+ return options.sampleAt;
623
+ const hz = options.hz ?? 0;
624
+ const drive = options.drive ?? 0;
625
+ if (hz === 0 || drive === 0)
626
+ return null;
627
+ const sampleRate = options.sampleRate ?? 48_000;
628
+ return (index) => drive * Math.sin((2 * Math.PI * hz * index) / sampleRate);
629
+ }
630
+ export function settledRender(program, options = {}) {
631
+ const sampleRate = options.sampleRate ?? 48_000;
632
+ const settle = options.settle ?? SETTLE_DEFAULTS;
633
+ const stimulus = resolveStimulus(options);
634
+ const perWindow = Math.max(1, Math.round(settle.windowSeconds * sampleRate));
635
+ const maxSeconds = settle.ladderSeconds[settle.ladderSeconds.length - 1] ?? 32;
636
+ const maxWindows = Math.ceil(maxSeconds / settle.windowSeconds);
637
+ const runtime = new ReferenceRuntime(program);
638
+ runtime.prepare(sampleRate);
639
+ for (const [id, position] of options.controls ?? [])
640
+ runtime.setControl(id, position);
641
+ const levels = [];
642
+ let index = 0;
643
+ for (let w = 0; w < maxWindows; w += 1) {
644
+ const input = new Float64Array(perWindow);
645
+ if (stimulus !== null) {
646
+ for (let i = 0; i < perWindow; i += 1)
647
+ input[i] = stimulus(index + i);
648
+ }
649
+ index += perWindow;
650
+ const output = runtime.process(input);
651
+ for (let i = 0; i < perWindow; i += 1) {
652
+ if (!Number.isFinite(output[i])) {
653
+ return { status: "nonfinite", atSample: index - perWindow + i };
654
+ }
655
+ }
656
+ levels.push(windowLevel(output, perWindow));
657
+ const found = findSettled(levels, settle);
658
+ if (found !== null) {
659
+ let sum = 0;
660
+ for (let i = 0; i < perWindow; i += 1)
661
+ sum += output[i] ?? 0;
662
+ const dc = sum / perWindow;
663
+ let peak = 0;
664
+ for (let i = 0; i < perWindow; i += 1) {
665
+ const centred = Math.abs((output[i] ?? 0) - dc);
666
+ if (centred > peak)
667
+ peak = centred;
668
+ }
669
+ return {
670
+ status: "settled",
671
+ seconds: found.seconds,
672
+ rms: found.rms,
673
+ peak,
674
+ dc,
675
+ tail: Float64Array.from(output),
676
+ };
677
+ }
678
+ }
679
+ return { status: "unsettled", triedSeconds: maxSeconds, windows: levels };
680
+ }
681
+ /** Thrown by `settledLevel` rather than returning a level it could not settle. */
682
+ export class SettleRefusal extends Error {
683
+ }
684
+ /**
685
+ * `settledRender`'s level as a bare number, throwing rather than returning one it cannot stand
686
+ * behind. The one-line form for a scratch probe: `const rms = settledLevel(program)`.
687
+ *
688
+ * Throwing is the point. A scratch script that ignores a return code still reports a number; one
689
+ * that throws stops, which is the loud failure the three-outcome rule asks for.
690
+ */
691
+ export function settledLevel(program, options = {}) {
692
+ const result = settledRender(program, options);
693
+ if (result.status === "settled")
694
+ return result.rms;
695
+ throw new SettleRefusal(result.status === "nonfinite"
696
+ ? `render went non-finite at sample ${result.atSample}`
697
+ : `never settled in ${result.triedSeconds}s; last windows ${result.windows
698
+ .slice(-4)
699
+ .map((v) => v.toExponential(2))
700
+ .join(", ")}`);
701
+ }
702
+ /**
703
+ * Settle a program once, then measure it at each control setting in turn **on the same runtime**.
704
+ *
705
+ * **Why this and not a settle-point cache.** The obvious saving is to remember each packet's
706
+ * settle time and skip the search, but measurement says the search is not the cost: a windowed
707
+ * `settledRender` runs within **3%** of a single `process()` call of the same duration, because
708
+ * `findSettled` already stops the moment the level holds. What costs is *rendering to settle at
709
+ * all*, and a fresh runtime pays the full start-up transient every time — charging supplies over
710
+ * ten seconds to move one knob.
711
+ *
712
+ * Continuing on a runtime that has already settled pays it once:
713
+ *
714
+ * | packet | initial settle | re-settle after a knob move |
715
+ * | --- | --- | --- |
716
+ * | `marshall-jcm800` | 10 s, 15.9 s wall | 1.5 s, 2.36 s — **6.7x cheaper** |
717
+ * | `hiwatt-dr103` | 9 s, 23.0 s wall | 1.5 s, 3.84 s — **6.0x cheaper** |
718
+ * | `orange-rockerverb` | 1.5 s | 1.5 s — 1.0x, already at the floor |
719
+ *
720
+ * The floor is `consecutive` stable windows, currently 1.5 s, so a packet that settles that fast
721
+ * gains nothing and loses nothing. The gain is concentrated where the expense is.
722
+ *
723
+ * **The cost of the trick, stated rather than hidden: this is order-dependent by construction.**
724
+ * A circuit with memory — a latch, a sag-dependent bias, an envelope with a long tail — can land
725
+ * somewhere different depending on which setting preceded it, where independent renders cannot.
726
+ * That is also *more* physical than restarting, since a real pedal is never power-cycled between
727
+ * knob positions. `sweepIsOrderIndependent` checks it directly by sweeping forwards and backwards,
728
+ * and a caller measuring a latching circuit should use `settledRender` per point instead.
729
+ */
730
+ export function settledSweep(program, points, options = {}) {
731
+ const sampleRate = options.sampleRate ?? 48_000;
732
+ const settle = options.settle ?? SETTLE_DEFAULTS;
733
+ const stimulus = resolveStimulus(options);
734
+ const perWindow = Math.max(1, Math.round(settle.windowSeconds * sampleRate));
735
+ const maxWindows = Math.ceil((settle.ladderSeconds[settle.ladderSeconds.length - 1] ?? 32) /
736
+ settle.windowSeconds);
737
+ const runtime = new ReferenceRuntime(program);
738
+ runtime.prepare(sampleRate);
739
+ for (const [id, position] of options.controls ?? [])
740
+ runtime.setControl(id, position);
741
+ let index = 0;
742
+ /** Render windows on the shared runtime until the level holds, or the ladder runs out. */
743
+ const settleHere = () => {
744
+ const levels = [];
745
+ for (let w = 0; w < maxWindows; w += 1) {
746
+ const input = new Float64Array(perWindow);
747
+ if (stimulus !== null) {
748
+ for (let i = 0; i < perWindow; i += 1)
749
+ input[i] = stimulus(index + i);
750
+ }
751
+ index += perWindow;
752
+ const output = runtime.process(input);
753
+ for (let i = 0; i < perWindow; i += 1) {
754
+ if (!Number.isFinite(output[i])) {
755
+ return { status: "nonfinite", atSample: index - perWindow + i };
756
+ }
757
+ }
758
+ levels.push(windowLevel(output, perWindow));
759
+ const found = findSettled(levels, settle);
760
+ if (found !== null) {
761
+ let sum = 0;
762
+ for (let i = 0; i < perWindow; i += 1)
763
+ sum += output[i] ?? 0;
764
+ const dc = sum / perWindow;
765
+ let peak = 0;
766
+ for (let i = 0; i < perWindow; i += 1) {
767
+ const centred = Math.abs((output[i] ?? 0) - dc);
768
+ if (centred > peak)
769
+ peak = centred;
770
+ }
771
+ return {
772
+ status: "settled",
773
+ seconds: found.seconds,
774
+ rms: found.rms,
775
+ peak,
776
+ dc,
777
+ tail: Float64Array.from(output),
778
+ };
779
+ }
780
+ }
781
+ return {
782
+ status: "unsettled",
783
+ triedSeconds: settle.ladderSeconds[settle.ladderSeconds.length - 1] ?? 32,
784
+ windows: levels,
785
+ };
786
+ };
787
+ // Pay the start-up transient once, before any point is measured.
788
+ settleHere();
789
+ return points.map((point) => {
790
+ // **Reset to defaults first, or points leak into each other.** Sharing one runtime shares
791
+ // its control state as well as its circuit state, so a point that sets only the knob it
792
+ // cares about inherits every knob the previous point moved. Each point is therefore a
793
+ // complete assignment: declared defaults, then this call's base `controls`, then the
794
+ // point. The circuit state is what is being reused; the panel is not.
795
+ for (const control of program.controls ?? []) {
796
+ try {
797
+ runtime.setControl(control.id, control.defaultPosition);
798
+ }
799
+ catch {
800
+ // A control the runtime does not know is not a reason to fail the sweep.
801
+ }
802
+ }
803
+ for (const [id, position] of options.controls ?? [])
804
+ runtime.setControl(id, position);
805
+ for (const [id, position] of point)
806
+ runtime.setControl(id, position);
807
+ return settleHere();
808
+ });
809
+ }
810
+ /**
811
+ * Does sharing a runtime change the answer? Compare a sweep against independent renders.
812
+ *
813
+ * **This replaces an order check, which was the wrong question.** `sweepIsOrderIndependent` asked
814
+ * whether sweeping forwards and backwards agreed, and on `boss-lm-2` — a limiter — it answered
815
+ * **true** while the sweep was wrong by **84 dB**:
816
+ *
817
+ * ```
818
+ * swept: 1.067e-6 1.057e-6 1.067e-6 1.058e-6
819
+ * independent: 1.821e-2 1.056e-6 1.821e-2 1.056e-6
820
+ * ```
821
+ *
822
+ * The hazard is not the *sequence* of points, it is sharing state at all. `settledSweep` settles
823
+ * once before the first point, and for an envelope circuit that initial settle drives the
824
+ * compressor into gain reduction that no later point recovers from — so every point is poisoned
825
+ * *equally*, forwards and backwards alike, and an order check sees perfect agreement.
826
+ *
827
+ * The only sound validation is against the thing the sweep is an optimisation of. Circuits at risk
828
+ * are the ones carrying state across a control change: compressors, limiters, noise gates,
829
+ * envelope filters, slow LFOs, anything with a long-tailed bias.
830
+ */
831
+ export function sweepMatchesIndependent(program, points, options = {}, toleranceDb = 0.5) {
832
+ const swept = settledSweep(program, points, options);
833
+ return points.every((point, i) => {
834
+ const alone = settledRender(program, {
835
+ ...options,
836
+ controls: [...(options.controls ?? []), ...point],
837
+ });
838
+ const a = swept[i];
839
+ if (a?.status !== "settled" || alone.status !== "settled") {
840
+ return a?.status === alone.status;
841
+ }
842
+ const ratio = Math.max(a.rms, 1e-30) / Math.max(alone.rms, 1e-30);
843
+ return Math.abs(20 * Math.log10(ratio)) <= toleranceDb;
844
+ });
845
+ }
846
+ /**
847
+ * **Sweep where it is safe, and fall back where it is not.**
848
+ *
849
+ * Verifies one point against an independent render — the cheapest check that can catch a shared
850
+ * state hazard — and re-measures the whole packet independently when they disagree. A packet that
851
+ * carries no state across a control change pays one extra render; one that does gets the right
852
+ * answer instead of a fast wrong one.
853
+ *
854
+ * The verified point is the **first**, because that is the one immediately after the shared
855
+ * initial settle and therefore the most exposed to it.
856
+ */
857
+ export function settledSweepVerified(program, points, options = {}, toleranceDb = 0.5) {
858
+ const swept = settledSweep(program, points, options);
859
+ const first = points[0];
860
+ if (first === undefined)
861
+ return { results: swept, shared: true };
862
+ const alone = settledRender(program, {
863
+ ...options,
864
+ controls: [...(options.controls ?? []), ...first],
865
+ });
866
+ const a = swept[0];
867
+ const agrees = a?.status === "settled" && alone.status === "settled"
868
+ ? Math.abs(20 * Math.log10(Math.max(a.rms, 1e-30) / Math.max(alone.rms, 1e-30))) <= toleranceDb
869
+ : a?.status === alone.status;
870
+ if (agrees)
871
+ return { results: swept, shared: true };
872
+ return {
873
+ results: points.map((point) => settledRender(program, {
874
+ ...options,
875
+ controls: [...(options.controls ?? []), ...point],
876
+ })),
877
+ shared: false,
878
+ };
879
+ }
880
+ /**
881
+ * Does this program give the same swept answers backwards as forwards?
882
+ *
883
+ * `settledSweep`'s saving comes from not restarting, which makes it order-dependent for any
884
+ * circuit carrying state across a control change. This is the check: sweep the points, sweep them
885
+ * reversed, and compare the levels. `false` means the packet needs independent renders.
886
+ */
887
+ export function sweepIsOrderIndependent(program, points, options = {}, tolerance = 1e-9) {
888
+ const forward = settledSweep(program, points, options);
889
+ const backward = [
890
+ ...settledSweep(program, [...points].reverse(), options),
891
+ ].reverse();
892
+ return forward.every((a, i) => {
893
+ const b = backward[i];
894
+ if (a.status !== "settled" || b === undefined || b.status !== "settled") {
895
+ return a.status === b?.status;
896
+ }
897
+ return Math.abs(a.rms - b.rms) <= tolerance * Math.max(Math.abs(a.rms), 1);
898
+ });
899
+ }
900
+ //# sourceMappingURL=settle.js.map