@denaudio/den 0.0.0-stage → 0.1.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 (191) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/LICENSE-APACHE +202 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +102 -2
  5. package/dist/biquad-eq.d.ts +11 -0
  6. package/dist/biquad-eq.js +35 -0
  7. package/dist/catalog-filter-math.d.ts +4 -0
  8. package/dist/catalog-filter-math.js +30 -0
  9. package/dist/character-filter.d.ts +13 -0
  10. package/dist/character-filter.js +53 -0
  11. package/dist/convolution-spectrum.d.ts +25 -0
  12. package/dist/convolution-spectrum.js +82 -0
  13. package/dist/convolution.d.ts +15 -0
  14. package/dist/convolution.js +81 -0
  15. package/dist/crossover.d.ts +11 -0
  16. package/dist/crossover.js +17 -0
  17. package/dist/curve-shaper.d.ts +26 -0
  18. package/dist/curve-shaper.js +73 -0
  19. package/dist/delay-fx.d.ts +37 -0
  20. package/dist/delay-fx.js +84 -0
  21. package/dist/delay-readhead.d.ts +19 -0
  22. package/dist/delay-readhead.js +54 -0
  23. package/dist/delay-settings.d.ts +55 -0
  24. package/dist/delay-settings.js +20 -0
  25. package/dist/drive.d.ts +24 -0
  26. package/dist/drive.js +116 -0
  27. package/dist/dual-head-delay.d.ts +28 -0
  28. package/dist/dual-head-delay.js +77 -0
  29. package/dist/dynamics.d.ts +61 -0
  30. package/dist/dynamics.js +130 -0
  31. package/dist/envelope.d.ts +34 -0
  32. package/dist/envelope.js +62 -0
  33. package/dist/filter.d.ts +9 -0
  34. package/dist/filter.js +34 -0
  35. package/dist/filterbank-vocoder.d.ts +35 -0
  36. package/dist/filterbank-vocoder.js +52 -0
  37. package/dist/formant-bank.d.ts +15 -0
  38. package/dist/formant-bank.js +28 -0
  39. package/dist/freeze-reverb.d.ts +35 -0
  40. package/dist/freeze-reverb.js +61 -0
  41. package/dist/frequency-shifter.d.ts +24 -0
  42. package/dist/frequency-shifter.js +57 -0
  43. package/dist/gate.d.ts +2 -0
  44. package/dist/gate.js +12 -0
  45. package/dist/index.d.ts +7 -0
  46. package/dist/index.js +12 -0
  47. package/dist/instrument-example.d.ts +12 -0
  48. package/dist/instrument-example.js +29 -0
  49. package/dist/instrument-settings.d.ts +95 -0
  50. package/dist/instrument-settings.js +36 -0
  51. package/dist/instrument.d.ts +42 -0
  52. package/dist/instrument.js +166 -0
  53. package/dist/lfo.d.ts +19 -0
  54. package/dist/lfo.js +55 -0
  55. package/dist/live-buffer.d.ts +39 -0
  56. package/dist/live-buffer.js +52 -0
  57. package/dist/live-granular.d.ts +41 -0
  58. package/dist/live-granular.js +64 -0
  59. package/dist/lookahead-limiter.d.ts +31 -0
  60. package/dist/lookahead-limiter.js +67 -0
  61. package/dist/loop-crossfade.d.ts +28 -0
  62. package/dist/loop-crossfade.js +72 -0
  63. package/dist/modulation-fx.d.ts +47 -0
  64. package/dist/modulation-fx.js +58 -0
  65. package/dist/modulation.d.ts +132 -0
  66. package/dist/modulation.js +228 -0
  67. package/dist/mpe-expression.d.ts +36 -0
  68. package/dist/mpe-expression.js +73 -0
  69. package/dist/multiband-dynamics.d.ts +50 -0
  70. package/dist/multiband-dynamics.js +60 -0
  71. package/dist/musical-controls.d.ts +76 -0
  72. package/dist/musical-controls.js +125 -0
  73. package/dist/musical-examples.d.ts +21 -0
  74. package/dist/musical-examples.js +142 -0
  75. package/dist/musical-materials.d.ts +110 -0
  76. package/dist/musical-materials.js +56 -0
  77. package/dist/oscillator.d.ts +10 -0
  78. package/dist/oscillator.js +35 -0
  79. package/dist/oversampled-drive.d.ts +19 -0
  80. package/dist/oversampled-drive.js +99 -0
  81. package/dist/performance.d.ts +61 -0
  82. package/dist/performance.js +241 -0
  83. package/dist/pitch-quantizer.d.ts +26 -0
  84. package/dist/pitch-quantizer.js +88 -0
  85. package/dist/prepared-convolution.d.ts +22 -0
  86. package/dist/prepared-convolution.js +99 -0
  87. package/dist/resident-recorder.d.ts +28 -0
  88. package/dist/resident-recorder.js +68 -0
  89. package/dist/resident-time-stretch.d.ts +44 -0
  90. package/dist/resident-time-stretch.js +135 -0
  91. package/dist/resonator.d.ts +32 -0
  92. package/dist/resonator.js +63 -0
  93. package/dist/reverb.d.ts +27 -0
  94. package/dist/reverb.js +52 -0
  95. package/dist/sample.d.ts +113 -0
  96. package/dist/sample.js +212 -0
  97. package/dist/source.d.ts +58 -0
  98. package/dist/source.js +113 -0
  99. package/dist/spatial-chains.d.ts +48 -0
  100. package/dist/spatial-chains.js +76 -0
  101. package/dist/spectral-fft.d.ts +21 -0
  102. package/dist/spectral-fft.js +74 -0
  103. package/dist/spectral-freeze.d.ts +22 -0
  104. package/dist/spectral-freeze.js +74 -0
  105. package/dist/spectral-gate.d.ts +21 -0
  106. package/dist/spectral-gate.js +58 -0
  107. package/dist/spectral-stft.d.ts +13 -0
  108. package/dist/spectral-stft.js +69 -0
  109. package/dist/spectral-texture.d.ts +42 -0
  110. package/dist/spectral-texture.js +141 -0
  111. package/dist/spectral.d.ts +16 -0
  112. package/dist/spectral.js +10 -0
  113. package/dist/state-variable-filter.d.ts +17 -0
  114. package/dist/state-variable-filter.js +25 -0
  115. package/dist/stereo-delay.d.ts +45 -0
  116. package/dist/stereo-delay.js +70 -0
  117. package/dist/virtual-analog.d.ts +26 -0
  118. package/dist/virtual-analog.js +66 -0
  119. package/dist/voice-policy.d.ts +37 -0
  120. package/dist/voice-policy.js +245 -0
  121. package/dist/wavetable-bands.d.ts +37 -0
  122. package/dist/wavetable-bands.js +145 -0
  123. package/dist/wavetable-read.d.ts +6 -0
  124. package/dist/wavetable-read.js +11 -0
  125. package/dist/wavetable.d.ts +29 -0
  126. package/dist/wavetable.js +49 -0
  127. package/dist/windowed-pitch-shift.d.ts +34 -0
  128. package/dist/windowed-pitch-shift.js +51 -0
  129. package/docs/catalog-audition.md +76 -0
  130. package/docs/catalog-filters.md +125 -0
  131. package/docs/catalog-modulation-fx.md +220 -0
  132. package/docs/catalog-status.md +431 -0
  133. package/docs/character-filter.md +139 -0
  134. package/docs/contracts.md +131 -0
  135. package/docs/control-modulation.md +194 -0
  136. package/docs/convolution.md +45 -0
  137. package/docs/curve-shaper.md +117 -0
  138. package/docs/delay-fx-capture-findings.json +134 -0
  139. package/docs/delay-fx.md +202 -0
  140. package/docs/delay-readhead.md +121 -0
  141. package/docs/delay-settings-candidate.json +333 -0
  142. package/docs/delay-settings.md +111 -0
  143. package/docs/dependency-findings.md +61 -0
  144. package/docs/deployment-status.md +37 -0
  145. package/docs/drive.md +152 -0
  146. package/docs/dual-head-delay.md +131 -0
  147. package/docs/dynamics.md +180 -0
  148. package/docs/envelope-integer-staging.md +64 -0
  149. package/docs/filter-materialization-results.json +71 -0
  150. package/docs/filter-materialization.md +135 -0
  151. package/docs/filter-oscillator.md +126 -0
  152. package/docs/filterbank-vocoder-entry.md +58 -0
  153. package/docs/freeze-reverb.md +179 -0
  154. package/docs/frequency-shifter.md +133 -0
  155. package/docs/frontend-audition.md +22 -0
  156. package/docs/gain-schedule-verification.md +53 -0
  157. package/docs/initial-acceptance.md +79 -0
  158. package/docs/instrument-performance.md +191 -0
  159. package/docs/instrument-settings.md +130 -0
  160. package/docs/instrument.md +204 -0
  161. package/docs/integration-candidate.md +114 -0
  162. package/docs/live-buffer.md +163 -0
  163. package/docs/live-granular.md +179 -0
  164. package/docs/lookahead-multiband.md +183 -0
  165. package/docs/loop-crossfade.md +99 -0
  166. package/docs/mpe-expression.md +65 -0
  167. package/docs/musical-clock-seek-boundary.md +31 -0
  168. package/docs/musical-controls-contract.md +144 -0
  169. package/docs/musical-examples.md +255 -0
  170. package/docs/oversampled-drive.md +153 -0
  171. package/docs/performance.md +53 -0
  172. package/docs/pitch-quantizer.md +55 -0
  173. package/docs/prepared-convolution.md +188 -0
  174. package/docs/realtime-audition.md +63 -0
  175. package/docs/resident-recorder.md +151 -0
  176. package/docs/resident-time-stretch.md +190 -0
  177. package/docs/sample.md +191 -0
  178. package/docs/sound-audition-headroom.md +103 -0
  179. package/docs/sources.md +81 -0
  180. package/docs/spatial-chains-entry.md +250 -0
  181. package/docs/spectral-freeze-entry.md +130 -0
  182. package/docs/spectral-gate-entry.md +130 -0
  183. package/docs/spectral-large-candidate.md +74 -0
  184. package/docs/spectral-texture-entry.md +174 -0
  185. package/docs/spectral.md +118 -0
  186. package/docs/virtual-analog.md +77 -0
  187. package/docs/voice-policy.md +38 -0
  188. package/docs/wavetable-bands.md +138 -0
  189. package/docs/wavetable.md +101 -0
  190. package/docs/windowed-pitch-shift.md +112 -0
  191. package/package.json +269 -4
@@ -0,0 +1,180 @@
1
+ # Dynamics and envelope follower candidate
2
+
3
+ Status: **CANDIDATE**. Implements the single-band detector/gain-law part of
4
+ [catalog sections 5 and 9](https://app.notion.com/p/3ef449b4e81d817db1acf1fff48779b3).
5
+ It is not a lookahead/true-peak limiter, multiband processor, or an assertion of
6
+ listening approval. The original voice feedback concerned earlier candidates,
7
+ not these algorithms or generated clips. Independent final review and aggregate integration verification remain
8
+ separate gates.
9
+
10
+ ## Composition and boundaries
11
+
12
+ Public candidate subpath: `@denaudio/den/dynamics`, added with integration-owner
13
+ approval. The packed fixture requires this export and records its tested import. No new routing, parameter, snapshot, asset or runtime API is
14
+ introduced. Use the existing unworklet `instantiate` and stable unique names.
15
+
16
+ - `envelopeFollower`: reusable mono peak or RMS detector. Configuration is
17
+ `{ sampleRate, mode: 'peak' | 'rms' }`; `tick(input, controls)` returns a
18
+ nonnegative f32 amplitude. Controls are `attack`, `release`, `reset`.
19
+ - `compressorGainDb(levelDb, controls)` and `expanderGainDb(levelDb, controls)`:
20
+ stateless downward gain laws, returning dB gain in `[-rangeDb, 0]`.
21
+ - `dynamics`: stereo feedforward composition. Configuration adds
22
+ `operation: 'compressor' | 'expander' | 'gate' | 'duck'` and optional fixed
23
+ `hysteresisDb` (default 3 dB, allowed 0–24 dB).
24
+ - Call `tick(left, right, sidechainLeft, sidechainRight, controls)` exactly once
25
+ per sample. Results are `left`, `right`, `envelope`, linear `gain` and `gainDb`.
26
+ Pass the program L/R again as sidechain L/R for internal detection. Mono callers
27
+ duplicate their input explicitly. An external key never becomes audible audio.
28
+ - Controls use existing f32 Nodes. `reset` is a boolean Node. Construction rate
29
+ must be an integer in 8000–192000 Hz and must match the processor's compile rate.
30
+ Mode and operation are construction-time choices, not dynamic CPU bypasses.
31
+
32
+ No makeup gain, automatic normalization, mix, bypass, sidechain filter, hold,
33
+ lookahead, feedback topology, channel adaptation or latency compensation is
34
+ hidden in this composition. The program path has zero sample delay. Finite
35
+ program audio is not clipped at full scale, and gain never exceeds unity.
36
+
37
+ ## Detector and gain ballistics
38
+
39
+ For a peak detector, the target is `abs(input)`. RMS uses `input²` as its target,
40
+ smooths **power**, then returns the square root. RMS is an exponentially weighted
41
+ power measurement, not a rectangular finite window or a true-peak estimate.
42
+
43
+ For either target, choose attack when target exceeds prior state and release
44
+ otherwise. With positive time `t`, the one-pole coefficient is
45
+ `c = 1 - exp(-1 / max(1, t * sampleRate))`; update by `(1-c)*previous + c*target`.
46
+ Times are seconds, clamped to 0–30. Positive sub-sample times use one sample as
47
+ the time constant. Exactly zero is instantaneous. A constant rising peak step
48
+ reaches `1-exp(-1)` after one time constant; an RMS power step reaches its square
49
+ root at that time. These meanings differ from a 10–90% timing convention.
50
+
51
+ In `dynamics`, `detectorAttack`/`detectorRelease` control the above measurement.
52
+ `attack`/`release` separately smooth the requested positive attenuation in dB:
53
+
54
+ - Compressor/duck: attack increases attenuation; release returns toward unity.
55
+ - Expander/gate: attack opens the gain; release increases attenuation.
56
+
57
+ Time edits take effect on the current sample without clearing history. There is
58
+ no implicit gain smoothing in the two standalone gain-law functions. Using zero
59
+ times or editing range/threshold abruptly can create discontinuities. Gain
60
+ smoothing prevents a hard keyed jump when its times are nonzero, but this is not
61
+ a perceptual guarantee against pumping, clicks or modulation artifacts.
62
+
63
+ ## Static laws and stereo behavior
64
+
65
+ `DynamicsGainControls` contains:
66
+
67
+ | Control | Unit and range | Nonfinite fallback |
68
+ | --- | --- | --- |
69
+ | thresholdDb | dBFS, -120…24 | 0 |
70
+ | ratio | 1…100 | 1 |
71
+ | kneeDb | full width, 0…48 dB | 0 |
72
+ | rangeDb | maximum attenuation, 0…120 dB | 120 |
73
+
74
+ For compressor input level `x` and threshold `T`, the hard-knee output level is
75
+ `x` below threshold, and `T + (x-T)/ratio` above it. Within a positive knee width
76
+ `W`, the gain is `(1/ratio-1)*(x-T+W/2)²/(2W)`. The outer pieces meet with
77
+ continuous value and first derivative.
78
+
79
+ For downward expansion, the output slope below threshold is `ratio`, and the
80
+ slope above threshold is 1. Within the knee, gain is
81
+ `-(ratio-1)*(x-T-W/2)²/(2W)`. The attenuation of both laws is capped by `rangeDb`.
82
+ Ratio 1 or range 0 gives unity. Gain-law level inputs are bounded to -900…900 dB,
83
+ with -900 dB substituted for nonfinite values. This is a defined numerical
84
+ boundary, not a limiter ceiling.
85
+
86
+ Gate/duck use a hysteretic comparator. The key becomes active at or above
87
+ threshold; it stays active until its measured level reaches or drops below
88
+ `threshold-hysteresisDb`. Gate requests zero attenuation when active and
89
+ `rangeDb` otherwise; duck does the reverse. Ratio and knee are ignored by these
90
+ two operations. There is no separate hold timer. At nonzero levels the meter
91
+ uses unworklet 0.4.1's approximate logarithm, so its threshold transition has
92
+ small numerical uncertainty, approximately 0.0001 dB within tested ranges.
93
+
94
+ Both operations use the same detector linkage: maximum absolute L/R **before**
95
+ power/peak ballistics. This deliberately preserves a unilateral transient; it
96
+ is not a summed or mean-power stereo RMS measurement. Both program channels
97
+ receive precisely the same gain. Opposite polarity keys cannot cancel one
98
+ another. Sidechain filtering can be composed outside this module.
99
+
100
+ ## Reset, finite inputs and state
101
+
102
+ Reset clears detector and attenuation histories and clears the gate/duck active
103
+ flag **before consuming the current sample**. Current input can immediately
104
+ reopen the key. The attenuation history starts at zero (unity); a cold/reset gate
105
+ with a silent key therefore closes according to release, not instantly unless
106
+ release is zero. A reset can change gain abruptly and is not bypass.
107
+
108
+ Every nonfinite audio or sidechain sample (NaN/±Infinity) is explicitly replaced
109
+ by zero. All finite f32 magnitudes, including values above full scale and
110
+ subnormals, remain valid. Nonfinite time controls use zero; finite out-of-range
111
+ controls clamp. Tests require `scrubbedSamples == 0`, so upstream output scrubbing
112
+ is not accepted as evidence of finite processing.
113
+
114
+ The detector's two fixed f64 slots are a scaled history and an exponent scratch
115
+ slot. The full composition has seven fixed scalar slots, no buffers and no
116
+ runtime allocation. unworklet 0.4.1 flushes scalar stores smaller than `1e-30`;
117
+ RMS power would otherwise lose recoverable audio below roughly `1e-15`. Histories
118
+ are therefore stored with the exact power-of-two scale `2**256`. Squared minimum
119
+ f32 input remains representable above that flush boundary even with a 30-second
120
+ attack; maximum scaled f32 power stays below `2**512`. Any eventual flushed tail
121
+ is already too small to return as f32 audio. Scale/unscale is exact in binary64.
122
+
123
+ Coefficient evaluation uses a degree-13 polynomial for `1-exp(-x)` on `(0,1]`,
124
+ with remainder at most `1/14!`. This avoids cancellation and long-time stalling
125
+ from `1-f32(exp(-x))`. History uses a convex weighted sum to preserve a tiny
126
+ instantaneous target after a large prior level. dB-to-linear gain uses the pinned
127
+ unworklet exp approximation; measured gain error is covered by independent tests.
128
+ The -600 dB meter floor affects only the gain-law level, not the returned envelope
129
+ or the program audio. Supported thresholds are at least -120 dB.
130
+
131
+ Same-schema, same-rate snapshots preserve detector, attenuation and hysteresis
132
+ continuation. Cross-mode/rate/schema migration is not promised. There is no
133
+ serialized preset format in this module.
134
+
135
+ ## Verification and remaining gates
136
+
137
+ `tests/dynamics.spec.ts` independently checks analytical peak/RMS responses,
138
+ step/burst/reset and a-rate time edits, hard/soft knee transfer curves, ratio-one
139
+ and range limits, stereo swapping/tracking, external keys, hysteresis, snapshot
140
+ continuation, malformed controls, full finite f32 range, subnormals, large→tiny
141
+ transitions and finite diagnostics. All three routine rates (44100/48000/96000)
142
+ are covered; RMS steady sine energy and very slow tiny attacks are checked too.
143
+ State count and a <64 KiB compiled-artifact guard bound construction growth.
144
+
145
+ `tests/dynamics-packed.test.mjs` packs the actual package, installs a fresh
146
+ locked consumer, type-checks declarations strictly, and renders all four modes
147
+ at those three rates against independently computed detector and input/output
148
+ transfer formulas. It writes 12 stereo WAV candidates and a hash manifest under
149
+ `artifacts/dynamics/<trial>/`; repeated runs keep earlier trials rather than
150
+ overwriting timing outliers. Generated audio is never an approved golden. Snapshot
151
+ continuation, stereo ratios and zero scrub diagnostics are mandatory assertions.
152
+
153
+ The packed fixture also records 2048 warmed 128-frame Node-driver wall-clock
154
+ measurements per operation, artifact bytes and fixed memory size at 48 kHz.
155
+ These are local diagnostics, exclude I/O copies and browser scheduling, and are
156
+ **not** an AudioWorklet deadline gate. Worst observations are retained.
157
+
158
+ Not yet established by this lane: browser rendering or controls, hardware-output
159
+ continuity, cross-browser deadline behavior, listening judgement of pumping,
160
+ transients, release, low-frequency distortion or stereo image, and aggregate public API
161
+ integration. This candidate adds no
162
+ true-peak, multiband or overall sound-quality claim. Full repository verification,
163
+ independent final review and exact-head CI belong to integration before merge.
164
+
165
+ ## Initial local verification record
166
+
167
+ Node 24.19.0 with locked unworklet 0.4.1: clean `npm ci --offline`, strict
168
+ `npm run check`, and all 45 focused tests passed using Vitest's supported threads
169
+ pool. The isolated packed consumer also passed all 12 renders. Its worst observed
170
+ audio/reference residual was 1.280e-6, gain residual 9.538e-6 dB and envelope
171
+ residual 2.979e-8, with zero scrubbed samples. This does not claim the full
172
+ repository aggregate suite ran in this lane; integration owns that check.
173
+
174
+ The initial local Node diagnostic measured 8,683–12,335-byte WASM and a fixed
175
+ 65,536-byte memory for the packed consumer (including its audio and parameter
176
+ ports). Median 128-frame process time was approximately 35–57 microseconds. One
177
+ expander wall-clock observation exceeded the nominal 2.667 ms quantum budget
178
+ (maximum 3.250 ms); other operations in that trial had zero such observations.
179
+ This is retained evidence under a shared host, not a pass/fail deadline verdict
180
+ or a reason to infer a DSP defect without isolating scheduling effects.
@@ -0,0 +1,64 @@
1
+ # Envelope integer expression staging
2
+
3
+ `envelope.tick` captures the previous `stage` and `remaining`, then temporarily
4
+ stores `active` and `count` in those existing integer slots. Reading each staged
5
+ value materializes its expression through unworklet's supported lexical read
6
+ capture. Old-history decisions use the captured old values. The original final
7
+ writes still run unconditionally, including reset. No declaration, snapshot
8
+ schema, floating-point intermediate store, or upstream dependency changes.
9
+
10
+ The subgraph exposes only `tick`. Its returned Nodes and caller-supplied control
11
+ Nodes capture memory reads before subsequent stores. Worklet snapshot requests
12
+ execute outside synchronous DSP execution. Direct concurrent inspection of raw
13
+ working memory in the middle of processing is not a compatibility guarantee.
14
+
15
+ ## Regression evidence
16
+
17
+ - `envelope.spec.ts` and the closed-form ADSR and maximum-i32-count tests in
18
+ `envelope-integer-staging.spec.ts` use independent numerical expectations.
19
+ - The other staging tests compare against a frozen copy of the pre-change
20
+ implementation from `c65e2a8586eb0325cbccc3963af3764c7f15c926`. This is a
21
+ differential baseline, not an independent numerical oracle. Do not update
22
+ that fixture to match production.
23
+ - Tests cover 44.1/48/96 kHz, reset/retrigger/automation, tiny and extreme finite
24
+ controls, complete state bytes, schema hash, bidirectional restore at five
25
+ boundaries, continuous-vs-restored output, a cold-start negative control,
26
+ independent instances, and delayed returned Nodes after caller state writes
27
+ and a second tick. Multiple ticks are an adversarial capture-order probe,
28
+ not a change to the once-per-sample usage contract.
29
+
30
+ Reproduce numerical regressions with:
31
+
32
+ ```sh
33
+ npm ci
34
+ npm run check
35
+ npx vitest run tests/envelope.spec.ts tests/envelope-integer-staging.spec.ts
36
+ ```
37
+
38
+ ## Limited performance evidence
39
+
40
+ One fixed A/B/B/A diagnostic used main `c65e2a8` dependencies with the unchanged
41
+ instrument fixture from `baa4bd50` (instrument is not present on that main).
42
+ Chrome 151.0.7922.173, 48 kHz / 128 frames, four held A4 notes, identical controls,
43
+ five seconds warmup then six seconds measurement, no capture/recording/analyser.
44
+ All trials, including outliers and failures, were retained.
45
+
46
+ | Trial | Callback CPU median, us | Kernel CPU/callback, us | CPU overruns / callbacks | Output underruns |
47
+ | --- | ---: | ---: | ---: | ---: |
48
+ | Baseline A1 | 426 | 508.85 | 3/2249 | 0 |
49
+ | Candidate B1 | 322 | 446.85 | 11/2242 | 0 |
50
+ | Candidate B2 | 322 | 421.15 | 11/2187 | 6 |
51
+ | Baseline A2 | 434 | 577.72 | 10/2214 | 1 |
52
+
53
+ Median callback CPU decreased 24–26% in this workload. One-second medians were
54
+ 415–438 us for baseline and 316–332 us for the candidate. Instrument WASM shrank
55
+ from 1043330 to 936570 bytes. These are workload-specific sustained-cost results,
56
+ not a universal performance guarantee or a runtime-gate pass.
57
+
58
+ Tail latency and output underruns remain unresolved: candidate maximum callback
59
+ CPU was 39105/7492 us. A separate existing realtime audition gate also failed
60
+ with two output underruns and was not retried. The prior done-only reuse proposal
61
+ had no sustained timing improvement and is not included. Initial verification
62
+ failures from missing browser installation and temporary disk exhaustion were
63
+ retained; after correcting those environment issues, the seven packed checks
64
+ passed. None of these results constitutes listening approval.
@@ -0,0 +1,71 @@
1
+ {
2
+ "baseCommit": "e12adc5019307183a68e7f8ff08aaa5a33562033",
3
+ "delayFixtureCommit": "7ef7ce670531b1230111f1d2c89302bfe44935b2",
4
+ "runtime": "unworklet 0.4.1; Node v24.19.0; Chromium 151.0.7922.173; Linux x64",
5
+ "sampleRate": 48000,
6
+ "framesPerQuantum": 128,
7
+ "secondsPerTrial": 12,
8
+ "compatibility": {
9
+ "filterRates": [
10
+ 8000,
11
+ 44100,
12
+ 48000,
13
+ 96000,
14
+ 192000
15
+ ],
16
+ "inputLevels": [
17
+ 1,
18
+ 1e-20,
19
+ 1e-28,
20
+ 1e-29,
21
+ 1e-30,
22
+ 1e-31,
23
+ 1e-38,
24
+ 1e-44
25
+ ],
26
+ "delayCases": 18,
27
+ "delayPCMAndSnapshotBytes": "identical",
28
+ "schema": "unchanged"
29
+ },
30
+ "limitations": [
31
+ "Host-specific measurements with trace overhead, no CPU profiling.",
32
+ "All eight completed trials retained; no warmup exclusion from cold reporting.",
33
+ "No hardware-output deadline or sound approval."
34
+ ],
35
+ "filterSourceSHA256": "61d49d6b1e52b17791237e00dec5c5089e594ee0030ecaa37f2c632ecb441278",
36
+ "settings": {
37
+ "timeLeft": 0.075,
38
+ "timeRight": 0.1,
39
+ "feedback": 0.95,
40
+ "mix": 1,
41
+ "cutoff": 1000,
42
+ "sync": 0,
43
+ "bpm": 120,
44
+ "beatsLeft": 1,
45
+ "beatsRight": 1.5,
46
+ "rate": 2,
47
+ "depth": 0.001,
48
+ "bypass": 0,
49
+ "reset": 0
50
+ },
51
+ "wasm": {
52
+ "baseline": {
53
+ "bytes": 45935,
54
+ "sha256": "25202e821cecfc39de34a7c6e153d89091ff5b29db893856d22861370c21b6f1"
55
+ },
56
+ "candidate": {
57
+ "bytes": 24707,
58
+ "sha256": "74ab305c15c932d3d7bc32e7d4a0499439adfdfb107355f329e46d1ffea9fdb0"
59
+ }
60
+ },
61
+ "browserTrials": [
62
+ {"label":"baseline","trial":0,"audioSeconds":12.016,"quanta":4506,"firstWallMs":3.071,"firstCpuMs":2.849,"hotWall":{"meanMs":0.09881576026637134,"p99Ms":0.217,"maxMs":2.166},"hotCpu":{"meanMs":0.09697758046614784,"p99Ms":0.215,"maxMs":2.164},"overBudget":[{"quantum":0,"wallMs":3.071,"cpuMs":2.849,"clockAnomaly":false}],"lazyCompile":[{"name":"wasm.CompileLazy","dur":1115,"tdur":1114,"withinFirst":true}],"trace":"baseline-0-trace.json","traceSha256":"ff79728834d1426b8570072aecdc8f1f9a9cef35e55968e3764bcc5b05e7dde7"},
63
+ {"label":"candidate","trial":0,"audioSeconds":12.016,"quanta":4506,"firstWallMs":1.9,"firstCpuMs":1.897,"hotWall":{"meanMs":0.09993407325194262,"p99Ms":0.194,"maxMs":1.863},"hotCpu":{"meanMs":0.09836825749167738,"p99Ms":0.189,"maxMs":2.435},"overBudget":[],"lazyCompile":[{"name":"wasm.CompileLazy","dur":609,"tdur":608,"withinFirst":true}],"trace":"candidate-0-trace.json","traceSha256":"536c163ec4ed26943707605efe036a3c2b6f2e40172d9ca6f452714fa6e2cd8e"},
64
+ {"label":"candidate","trial":1,"audioSeconds":12.016,"quanta":4506,"firstWallMs":1.857,"firstCpuMs":1.855,"hotWall":{"meanMs":0.09728479467258735,"p99Ms":0.176,"maxMs":0.719},"hotCpu":{"meanMs":0.09542264150943501,"p99Ms":0.174,"maxMs":0.53},"overBudget":[],"lazyCompile":[{"name":"wasm.CompileLazy","dur":599,"tdur":598,"withinFirst":true}],"trace":"candidate-1-trace.json","traceSha256":"898b0abe99a1cdf4dc17199ba3f3d0da2387cb7ca1ff622b156efd838a18af9c"},
65
+ {"label":"baseline","trial":1,"audioSeconds":12.016,"quanta":4506,"firstWallMs":2.815,"firstCpuMs":2.743,"hotWall":{"meanMs":0.09903240843507195,"p99Ms":0.227,"maxMs":2.553},"hotCpu":{"meanMs":0.09767635960044414,"p99Ms":0.223,"maxMs":2.552},"overBudget":[{"quantum":0,"wallMs":2.815,"cpuMs":2.743,"clockAnomaly":false}],"lazyCompile":[{"name":"wasm.CompileLazy","dur":1120,"tdur":1119,"withinFirst":true}],"trace":"baseline-1-trace.json","traceSha256":"df0af807969fad55a30f796c8204cdbeb566eb41fa7c41af23fa9d1a86e5d939"},
66
+ {"label":"baseline","trial":2,"audioSeconds":12.016,"quanta":4506,"firstWallMs":2.959,"firstCpuMs":2.948,"hotWall":{"meanMs":0.09628768035516228,"p99Ms":0.172,"maxMs":1.476},"hotCpu":{"meanMs":0.09439134295227555,"p99Ms":0.171,"maxMs":1.41},"overBudget":[{"quantum":0,"wallMs":2.959,"cpuMs":2.948,"clockAnomaly":false}],"lazyCompile":[{"name":"wasm.CompileLazy","dur":1221,"tdur":1212,"withinFirst":true}],"trace":"baseline-2-trace.json","traceSha256":"37ac48f5dfc304181c61935f8a23734b1331358ad69264223dcf3c0c1efea6b5"},
67
+ {"label":"candidate","trial":2,"audioSeconds":12.016,"quanta":4506,"firstWallMs":1.68,"firstCpuMs":1.678,"hotWall":{"meanMs":0.09407169811320897,"p99Ms":0.183,"maxMs":1.043},"hotCpu":{"meanMs":0.09290166481687012,"p99Ms":0.183,"maxMs":2.289},"overBudget":[],"lazyCompile":[{"name":"wasm.CompileLazy","dur":604,"tdur":603,"withinFirst":true}],"trace":"candidate-2-trace.json","traceSha256":"7de84358f73ff46cd6d3cac5f524d1e4e7e0270749c31711cc29dcb77b5a2e3f"},
68
+ {"label":"candidate","trial":3,"audioSeconds":12.016,"quanta":4506,"firstWallMs":1.659,"firstCpuMs":1.656,"hotWall":{"meanMs":0.09528990011098971,"p99Ms":0.19,"maxMs":1.942},"hotCpu":{"meanMs":0.09338046614872407,"p99Ms":0.189,"maxMs":1.941},"overBudget":[],"lazyCompile":[{"name":"wasm.CompileLazy","dur":553,"tdur":551,"withinFirst":true}],"trace":"candidate-3-trace.json","traceSha256":"a530dfa4dc293a431f7f4011bbb2ae7341d63776d7949ac28d4b8f765c6fd786"},
69
+ {"label":"baseline","trial":3,"audioSeconds":12.016,"quanta":4506,"firstWallMs":6.584,"firstCpuMs":2.92,"hotWall":{"meanMs":0.09562841287458462,"p99Ms":0.184,"maxMs":1.514},"hotCpu":{"meanMs":0.09368967813540517,"p99Ms":0.178,"maxMs":1.512},"overBudget":[{"quantum":0,"wallMs":6.584,"cpuMs":2.92,"clockAnomaly":false}],"lazyCompile":[{"name":"wasm.CompileLazy","dur":1180,"tdur":1180,"withinFirst":true}],"trace":"baseline-3-trace.json","traceSha256":"b32ca895b5844f0e73e2c4b112f07800a08931c2a3ce04389db631d0187deb59"}
70
+ ]
71
+ }
@@ -0,0 +1,135 @@
1
+ # Filter coefficient materialization
2
+
3
+ The low-pass expression repeats its Taylor-polynomial coefficient when unworklet
4
+ 0.4.1 lowers the expression tree. Materializing only `g` reduces emitted WASM and
5
+ first-callback lazy compilation without changing coefficient arithmetic.
6
+
7
+ `band.read()` and `low.read()` first capture the old integrator values into WASM
8
+ locals. The existing `band` slot then stages `g`; a subsequent read captures that
9
+ coefficient, and the original final integrator writes replace the staged value
10
+ before `tick` returns. There are no added declarations, persistent/transient
11
+ slots, sample delays, changed coefficients, or reordered final integrator updates.
12
+ Snapshot schema, slot names/types, and saved bytes remain identical. The method
13
+ still runs exactly once per sample per instance, on the existing audio thread.
14
+
15
+ Only the bounded positive coefficient is staged. Across the supported sample-rate
16
+ and cutoff range, `g` exceeds 3e-4, far above the pinned runtime's 1e-30 state-write
17
+ flush threshold. Audio intermediates `v1`/`v2` must not be materialized via new
18
+ scalar states: doing so introduces an extra flush before a caller can amplify the
19
+ returned sample. For example, at 48 kHz, cutoff 1000 Hz, Q 0.707, input 1e-30,
20
+ and a finite post-filter gain of 1e30, the original output is nonzero whereas that
21
+ alternative becomes zero. Adding transient declarations also changes the schema
22
+ hash, even when saved slots appear unchanged.
23
+
24
+ ## Verification
25
+
26
+ `tests/filter-materialization.spec.ts` compares against the frozen pre-change
27
+ implementation from main `e12adc5`; this is a compatibility reference, not a new
28
+ correctness oracle. The existing independent biquad/response tests remain intact.
29
+ The compatibility tests cover:
30
+
31
+ - 8/44.1/48/96/192 kHz, cutoff/Q clamps and per-sample modulation;
32
+ - eight input levels from 1 to 1e-44, plus finite post-filter gains;
33
+ - resets around quantum boundaries, isolated instances and same-sample cascades;
34
+ - identical schema, saved bytes, and bidirectional snapshot continuation;
35
+ - the amplified tiny-constant counterexample and a 262144-sample resonant tail,
36
+ preserving the existing flush behavior including any tiny residual.
37
+
38
+ The separate Delay FX fixture at `7ef7ce6` was packed with each filter version.
39
+ All four output/status channels and complete snapshot bytes matched across 18
40
+ cases: 44.1/48/96 kHz × low/high/dynamic tone × modulation off/on, 8192 samples
41
+ per case, including varying times/mix, bypass, reset and rejected sync requests.
42
+ Its independent 31 numerical tests and corrected 48-kHz packed browser gate also
43
+ passed. Offline rate coverage remains distinct from 48-kHz browser coverage.
44
+
45
+ ## Timing reproduction
46
+
47
+ The fixture is in **this repository**, on the separate Delay FX PR #23 history;
48
+ it is not a file on this filter PR's main-based checkout. Fetch its exact commit
49
+ before looking for it:
50
+
51
+ - Repository: <https://github.com/yuichkun/den>
52
+ - Fixture commit: `7ef7ce670531b1230111f1d2c89302bfe44935b2`
53
+ - [Packed test at that commit](https://github.com/yuichkun/den/blob/7ef7ce670531b1230111f1d2c89302bfe44935b2/tests/delay-fx-packed.test.mjs)
54
+ - [Consumer directory at that commit](https://github.com/yuichkun/den/tree/7ef7ce670531b1230111f1d2c89302bfe44935b2/tests/delay-fx-consumer)
55
+ - Filter/probe revision: `ca3f18ee516cdf477260e7ab88ea0780c6bd049f`
56
+
57
+ Run this Bash recipe with Node 24, npm and Chromium's system dependencies
58
+ available. Optionally export `CHROMIUM_PATH` to an installed Chromium executable;
59
+ otherwise the recipe installs Playwright's pinned Chromium. The clone and all
60
+ worktrees are disposable. The candidate starts at the same fixture commit as the
61
+ baseline and receives **only** `src/filter.ts` from the specified filter revision.
62
+ Separate `TMPDIR` directories make both generated consumer paths unambiguous.
63
+
64
+ ```bash
65
+ set -euo pipefail
66
+ repro_root=$(mktemp -d)
67
+ export npm_config_cache="$repro_root/npm-cache"
68
+ export PLAYWRIGHT_BROWSERS_PATH="$repro_root/playwright"
69
+ fixture_rev=7ef7ce670531b1230111f1d2c89302bfe44935b2
70
+ filter_rev=ca3f18ee516cdf477260e7ab88ea0780c6bd049f
71
+ git clone https://github.com/yuichkun/den.git "$repro_root/repo"
72
+ git -C "$repro_root/repo" fetch origin "$fixture_rev" "$filter_rev"
73
+ git -C "$repro_root/repo" worktree add --detach "$repro_root/baseline" "$fixture_rev"
74
+ git -C "$repro_root/repo" worktree add --detach "$repro_root/candidate" "$fixture_rev"
75
+ git -C "$repro_root/repo" worktree add --detach "$repro_root/tools" "$filter_rev"
76
+ git -C "$repro_root/repo" show "$filter_rev:src/filter.ts" > "$repro_root/candidate/src/filter.ts"
77
+ (
78
+ cd "$repro_root/tools"
79
+ npm ci
80
+ if [ -z "${CHROMIUM_PATH:-}" ]; then
81
+ npm exec -- playwright install chromium
82
+ fi
83
+ )
84
+ mkdir -p "$repro_root/tmp/baseline" "$repro_root/tmp/candidate"
85
+ for variant in baseline candidate; do
86
+ (
87
+ cd "$repro_root/$variant"
88
+ npm ci
89
+ TMPDIR="$repro_root/tmp/$variant" node --test tests/delay-fx-packed.test.mjs
90
+ ) > "$repro_root/$variant-packed.log" 2>&1
91
+ done
92
+ baseline_consumers=("$repro_root/tmp/baseline"/den-delay-fx-*)
93
+ candidate_consumers=("$repro_root/tmp/candidate"/den-delay-fx-*)
94
+ test "${#baseline_consumers[@]}" -eq 1
95
+ test "${#candidate_consumers[@]}" -eq 1
96
+ test -f "${baseline_consumers[0]}/package-lock.json"
97
+ test -f "${candidate_consumers[0]}/package-lock.json"
98
+ node "$repro_root/tools/tests/probes/filter-browser-timing.mjs" "${baseline_consumers[0]}" "${candidate_consumers[0]}" "$repro_root/evidence" > "$repro_root/timing.log" 2>&1
99
+ printf 'Retained reproduction directory: %s\n' "$repro_root"
100
+ ```
101
+
102
+ A failed packed gate stops the recipe; inspect the corresponding `*-packed.log`
103
+ and retained `artifacts/delay-fx` directory rather than selecting a passing retry.
104
+ `evidence/browser-timing.json` includes every timing trial, WASM hash/size and raw
105
+ trace hash. Expect the same packed WASM sizes/hashes recorded below; timing values
106
+ are machine-dependent and must not be required to match the original numbers.
107
+
108
+ The probe appends a recorder-free entry to the disposable consumer pages, rebuilds
109
+ with the existing plugin, and alternates four fresh-browser trials per variant.
110
+ Each trial runs for 12 seconds at 48 kHz. It retains every trace and reports the
111
+ first callback separately from all later callbacks; no initial samples or slow
112
+ trials are removed. CPU profiling is disabled. Chromium tracing can still affect
113
+ timing, and thread-CPU durations occasionally exceeding wall time are explicitly
114
+ flagged as clock/accounting anomalies. No hardware-output deadline guarantee or
115
+ listening approval follows from these diagnostics.
116
+
117
+ ## Observed results
118
+
119
+ On Node 24.19.0 / Chromium 151.0.7922.173, with all eight trials retained:
120
+
121
+ | Measurement | Original | Coefficient-only staging |
122
+ | --- | --- | --- |
123
+ | Packed Delay WASM bytes | 45,935 | 24,707 |
124
+ | First callback wall time, four runs | 2.815–6.584 ms | 1.659–1.900 ms |
125
+ | First callback thread CPU, four runs | 2.743–2.948 ms | 1.656–1.897 ms |
126
+ | Later callback wall mean, range across runs | 0.0956–0.0990 ms | 0.0941–0.0999 ms |
127
+ | Later callback wall p99, range across runs | 0.172–0.227 ms | 0.176–0.194 ms |
128
+
129
+ The original first callback contains synchronous `wasm.CompileLazy`; the smaller
130
+ artifact reduces the observed startup cost. These results do not establish a
131
+ hot-path speedup or a universal deadline guarantee. The separate direct-driver
132
+ benchmark measured means of 0.0784 ms (original) and 0.0849 ms (candidate), excluding
133
+ browser scheduling and copy overhead; it likewise does not support a hot-speedup
134
+ claim. Full per-trial timings, settings, WASM hashes, outliers and trace hashes are
135
+ in [filter-materialization-results.json](filter-materialization-results.json).
@@ -0,0 +1,126 @@
1
+ # Filter and oscillator numerical contract
2
+
3
+ GEN-612 and GEN-613. Implemented as unworklet 0.4.1 `defineSubgraph`
4
+ declarations, with required immutable configuration and independent named state.
5
+ Call each instance's `tick` once per sample, inside `forSample`. All signal
6
+ arguments are finite `Node<'f32'>`; reset is `Node<'bool'>`. Configuration rate
7
+ must equal the compiled/rendered rate. The numerical construction domain is
8
+ 8000–192000 Hz; acceptance renders are separately 44100, 48000 and 96000 Hz.
9
+ No parameter, MIDI, routing, state codec, noise generator or loader is added.
10
+ For percussion noise, compose the existing unworklet `noiseSource` in the engine.
11
+
12
+ ## Low-pass
13
+
14
+ `instantiate(filter, { sampleRate }, { name })` returns
15
+ `tick(input, cutoffHz, resonanceQ, reset) -> Node<'f32'>`.
16
+
17
+ The two-integrator trapezoidal state-variable method preserves meaningful
18
+ integrator state during coefficient edits. Cutoff clamps to
19
+ `[20, min(20000, 0.45 * sampleRate)]`; Q clamps to `[0.5, 10]`.
20
+ Reset clears both histories **before** consuming the input at that sample.
21
+ There is no smoothing, gain compensation, saturation or limiter. The caller
22
+ owns modulation depth and gain staging. Slots `band` and `low` are f64 to avoid
23
+ low-cutoff cancellation/accumulation; audio output remains f32.
24
+
25
+ Let `g=tan(pi*cutoff/rate)` and `k=1/Q`. Solving the trapezoidal integrator pair
26
+ simultaneously gives `band=(s1+g*(input-s2))/(1+g*(g+k))`,
27
+ `low=s2+g*band`, followed by `s1=2*band-s1`, `s2=2*low-s2`.
28
+ This is derived from the integrator equations in Andrew Simper's
29
+ [trapezoidal SVF analysis](https://cytomic.com/files/dsp/SvfLinearTrapOptimised2.pdf)
30
+ (2013, updated 2016). No reference source code or PDF is vendored; repository
31
+ MIT/Apache notices are preserved. Compared with direct-form coefficient edits,
32
+ the SVF retains integrator coordinates; compared with forward-Euler SVF it avoids
33
+ an explicit-step high-cutoff stability restriction.
34
+
35
+ The independent oracle is a direct-form biquad from the bilinear transform of
36
+ `H(s)=1/(s²+s/Q+1)`, not a copy of the SVF recurrence. Its magnitude is
37
+ `1/sqrt((1-u²)²+(u/Q)²)`, `u=tan(pi*f/rate)/tan(pi*cutoff/rate)`.
38
+ DC gain is one; gain at cutoff is Q. The static response peak is one for
39
+ Q <= 1/sqrt(2), otherwise `Q/sqrt(1-1/(4Q²))` (10.013 at Q=10).
40
+ Consequently a feedback multiplier of 0.95 with Q=10 is **not** a guaranteed
41
+ stable delay loop. Use Q <= 1/sqrt(2) for a nonpeaking fixed feedback tone, or
42
+ prove the complete engine's loop gain separately. Time-varying coefficients
43
+ require complete-loop testing even at low Q; the module tests are not that proof.
44
+
45
+ Numerical acceptance: absolute impulse/direct-form error <2e-6 over cutoff
46
+ 20/1000/maximum and Q 0.5/1/sqrt(2)/10; steady sine response uses independent
47
+ sample expectations and gain error <0.02 at 250/1000/8000 Hz. Alternating
48
+ minimum/maximum cutoff every sample with Q edits remains finite, peak <25 for
49
+ unit 173-Hz input, and reset removes all residual output. This peak is a fixture
50
+ bound, not a universal amplitude guarantee. DC, slowest-pole decay, reset with
51
+ nonzero input, clamp equivalence, silent second instance, and exact snapshot
52
+ continuation have separate assertions.
53
+
54
+ unworklet 0.4.1 lowers transcendental functions through f32 approximations even
55
+ for f64 input. The initial tan call failed the 2e-6 reference budget. Instead,
56
+ coefficient construction evaluates degree-13 sine / degree-14 cosine Taylor
57
+ polynomials in f64 on `[0, 0.45*pi]`. The first omitted terms bound sine error by
58
+ `(0.45*pi)^15/15!` and cosine error by `(0.45*pi)^16/16!`.
59
+ No upstream code is changed and the reference tolerance remains unchanged.
60
+
61
+ ## Oscillator
62
+
63
+ `instantiate(oscillator, { sampleRate, waveform: 'sine' | 'saw' }, { name })`
64
+ returns `tick(frequencyHz, reset) -> Node<'f32'>`.
65
+
66
+ One f64 `phase` slot stores cycles in `[0,1)`. Emit current phase, then advance
67
+ by clamped frequency/rate. Frequency clamps to `[0, 0.45*sampleRate]`; zero and
68
+ negative frequencies hold phase. Reset uses phase zero for that sample, then
69
+ advances normally. A continuously true reset pins the emitted phase. A4=440 Hz
70
+ is supplied by the caller; equal-tempered MIDI mapping is `440*2^((note-69)/12)`.
71
+ There is no second MIDI receiver or tuning state.
72
+
73
+ Sine uses quadrant folding and a degree-13 Taylor polynomial on
74
+ `[-pi/2,pi/2]`, whose first omitted term is <7e-10. This avoids the dependency's
75
+ sine overshoot while retaining the original sample-error budget (<2e-6) and
76
+ nominal unit amplitude. Saw uses a four-sample polynomial step correction:
77
+ convolve four unit-width boxes to obtain the cubic B-spline kernel, integrate
78
+ it, and correct both sides of the periodic discontinuity. For distance x>=0
79
+ in sample intervals, the step complement is
80
+ `C(x)=1/2-2x/3+x³/3-x⁴/8` for x<1,
81
+ `C(x)=(2-x)^4/24` for 1<=x<2, otherwise zero.
82
+ The saw is `2p-1+2*(C(p/dt)-C((1-p)/dt))`. Both corrections are added when
83
+ supports overlap near Nyquist. No mutable BLEP buffer is required.
84
+
85
+ This is an independently derived polynomial smoothing kernel, not an ideal
86
+ brick-wall bandlimit. [Finke's published PolyBLEP example](https://www.martin-finke.de/articles/audio-plugins-018-polyblep-oscillator/)
87
+ illustrates the shorter two-sample alternative and the additive tradeoff.
88
+ No code from that example is included. The two-sample candidate measured
89
+ -43.43 dBFS folded RMS at bin 37/8192 and missed the -50 dBFS bass/pad criterion.
90
+ The four-sample method is selected by the same orthogonal-projection test:
91
+
92
+ | Fundamental / rate | Folded RMS dBFS | Improvement over naive saw |
93
+ | --- | ---: | ---: |
94
+ | 37/8192 | -53.38 | 26.01 dB |
95
+ | 997/8192 | -45.37 | 31.89 dB |
96
+ | 3501/8192 | -71.41 or lower | 62.57 dB or better |
97
+
98
+ All three sample rates pass. Projection removes only below-Nyquist integer
99
+ harmonics, so the remaining RMS measures aliased energy (plus numerical/DC
100
+ error). The naive counterexample must be >=12 dB worse. Fundamental amplitude
101
+ is independently checked against `(2/pi)*sinc(pi*f/rate)^4` within 0.002.
102
+ The kernel intentionally attenuates high harmonics and high-pitch fundamental;
103
+ there is no hidden normalization. Saw output stays within [-1,1]. At zero Hz
104
+ it emits the held naive ramp value (phase zero gives -1); starting/stopping can
105
+ therefore change the corrected value even though phase is preserved.
106
+
107
+ Bass and pad can use the saw; sine supplies percussion body/pitch sweeps.
108
+ Hard reset, abrupt pitch changes and arbitrary audio-rate FM are discontinuous
109
+ and do not inherit the stationary alias bounds. They require engine envelope
110
+ and listening assessment. No hard-sync/PM anti-alias guarantee is made.
111
+
112
+ ## Packaging and integration
113
+
114
+ `npm test` covers numerical tests and a packed isolated locked consumer, including
115
+ strict declaration checking and actual oscillator -> filter rendering against
116
+ analytic sine plus the independent biquad. It emits raw f32 candidate audio,
117
+ static SVG plots, alias measurements and a manifest with source commit/hashes,
118
+ package integrity, dependency lock hash, rates and all fixture settings.
119
+ Snapshot tests are same-schema/rate only. Nothing is an approved golden.
120
+ The separate existing browser gate stays at 48000 Hz.
121
+
122
+ The packed test imports `@denaudio/den/filter` and `@denaudio/den/oscillator`
123
+ through their public package subpaths. It checks strict declarations and renders
124
+ both modules together from an isolated, locked installation. No installed-file
125
+ path escape is required. These are the same shared implementations used by the
126
+ instrument and Delay FX.
@@ -0,0 +1,58 @@
1
+ # Fixed filter-bank vocoder candidate
2
+
3
+ Catalog group 11 explicitly includes a filter-bank vocoder. This entry is a bounded native implementation on unworklet 0.4.1, using the reviewed den TPT state-variable filter and peak envelope follower. It is independent of the STFT/phase-vocoder processors. It does not claim speech intelligibility, commercial equivalence, or human-approved sound quality.
4
+
5
+ ## Contract
6
+
7
+ `filterBankVocoder({ sampleRate, bands })` constructs a mono processor. `sampleRate` is an integer from 8,000 through 192,000 Hz and must equal the actual processing rate. `bands` is a dense array of 1–8 `{ frequencyHz, q, gain }` records. Centers must be strictly increasing, from 20 Hz through `min(20000, .45 * sampleRate)`; Q is .5–10 and gain is a nonnegative linear factor from 0–4. Invalid values, empty/oversized/sparse arrays, duplicate centers and descending centers reject at graph construction. Values are copied into the graph; mutating the configuration later does not retune it. Structural changes require a new graph.
8
+
9
+ For every band, one second-order unity-peak bandpass analyzes the modulator, and an identical second-order bandpass filters the externally supplied carrier. The center/Q controls use the existing SVF's f32 rounding, bounds, and bounded coefficient approximation. There is no automatic logarithmic placement, channel adaptation, unvoiced/noise source, high-frequency bypass, or crossover-perfect reconstruction claim.
10
+
11
+ `tick(modulator, carrier, { attack, release, reset })` is called exactly once per sample. It returns `{ output, envelopes }`, with envelopes in configuration order. All sample operations and persistent histories are native unworklet graph operations. JavaScript loops construct the graph; they do not process audio on the host.
12
+
13
+ For band `i`, the output is exactly the implemented f32 endpoint of:
14
+
15
+ analysis_i = BP_i(modulator)
16
+ carrier_i = BP_i(carrier)
17
+ target_i = abs(analysis_i)
18
+ envelope_i = previous_i * (1 - coefficient) + target_i * coefficient
19
+ output = sum_i(gain_i * carrier_i * envelope_i)
20
+
21
+ The recurrence uses f64 history, the exposed envelope and bandpass endpoints are f32, and the output sum uses f64 before its f32 endpoint. No hidden band-count division, automatic gain control, peak/RMS calibration, makeup gain, clipping, or limiter is present. Gains of zero still process the band's history. Parallel bands overlap and have phase, so their sum is not a flat unity transfer. A constant carrier gain does not imply a constant output gain.
22
+
23
+ ## Envelope and gain interpretation
24
+
25
+ The peak follower here smooths rectified amplitude, not windowed peaks or RMS. Attack is selected when the current target exceeds previous envelope; otherwise release is selected. Each time is in seconds, clamped to 0–30; nonfinite times become zero. Zero time is immediate. A positive time uses `tauSamples = max(1, seconds * sampleRate)` and `coefficient = 1 - exp(-1 / tauSamples)`, with the reviewed bounded polynomial implementation. One time constant traverses approximately 63.2% of a constant target difference. Filter ringing also affects a band envelope after a burst.
26
+
27
+ For a center-frequency sine of amplitude A, the unity-peak filter preserves the settled sine amplitude. Under equal, slow attack/release, the envelope approaches the sampled rectified mean with ripple. `2A/pi` is the ideal continuous-time rectified-sine mean, not an exact finite sampled mean or a calibration applied by this module. Unequal attack/release can produce a different average. With both times zero, the envelope is the instantaneous absolute band sample. The user-specified gains control synthesis level explicitly.
28
+
29
+ Finite input is not clipped, including values above full scale. Nonfinite modulator/carrier samples become zero before entering their filters; earlier finite filter/envelope histories continue. The caller must provide practical headroom: finite extreme inputs/products/sums can exceed f32 output range. There is no arbitrary-overload guarantee. Ordinary IEEE product underflow can occur at tiny levels even though reused filter/follower history scaling preserves their respective histories.
30
+
31
+ ## Reset and persistence
32
+
33
+ Reset clears both bandpass integrators and the envelope history before the current sample. The current sample is still processed; reset is not an output mute. Holding reset high recomputes the first-sample response for each input. It does not switch off processing.
34
+
35
+ All histories live in unworklet state under stable per-band analysis/synthesis/envelope names. Same-schema snapshot/restore continues filter ringing and envelopes. Tests must compare noninitial continuation, output and final state, including quantum boundaries. No custom snapshot codec or migration is introduced. Changing band count/order/centers/Q/gain or sample rate is a new configuration; cross-configuration restore compatibility is not promised.
36
+
37
+ Core 0.4.1 restores persistent worklet state before the host restores saved
38
+ AudioParams. In-place restoration across different live controls is not atomic.
39
+ The browser composition renders every saved control before calling restore.
40
+ It saves a nonzero slowly decaying envelope with zero modulator input, clears
41
+ the live history, then proves that the stable saved controls leave that mutation
42
+ silent. Restoration must recover both the envelope and actual carrier output.
43
+ Measured native-clock brackets bound the expected 30-second exponential decay,
44
+ with eight quanta allowed for delivery/publication and analyser alignment. This
45
+ is an observation allowance, not increased DSP accuracy or restored timing;
46
+ exact sample-aligned filter/history continuation remains an offline assertion.
47
+
48
+ There is no block lookahead or explicit delay buffer. Filter phase and attack/release cause frequency-dependent temporal response, so this is not a zero-phase or transient-transparent effect.
49
+
50
+ ## Validation boundary
51
+
52
+ The targeted gate checks 44.1/48/96 kHz against independent RBJ direct-form-I bandpasses and a direct exponential envelope recurrence, plus center transfer, out-of-band rejection, carrier modulation, no implicit normalization, malformed configuration, reset-current-sample behavior, held reset, snapshot continuation, and instance isolation. The packed consumer must typecheck the public subpath, render actual native WASM, and compile the worklet with unworklet 0.4.1. Candidate audio, cost data and source/package hashes are evidence, not approved goldens. Browser/realtime, concurrency and listening acceptance remain separate.
53
+
54
+ The hosted 48 kHz browser gate uses native sine sources and a one-band worklet.
55
+ It checks rectified-amplitude calibration, doubled modulation, a silent carrier,
56
+ rendered control telemetry, the noninitial restore described above and reset.
57
+ It does not establish speech intelligibility or maximum-bank deadline capacity.
58
+ The explicit local browser-skip switch is rejected in hosted CI.