@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,153 @@
1
+ # Bounded oversampled memoryless drive
2
+
3
+ Status: **CANDIDATE**.
4
+
5
+ This is a fixed 2x/4x polyphase interpolation, memoryless waveshaper, and filtered
6
+ phase-zero decimation composition. It is not a generic wrapper around arbitrary
7
+ or stateful graphs, and does not create a second processing runtime.
8
+
9
+ ## Public entry
10
+
11
+ `@denaudio/den/oversampled-drive` exports `oversampledDrive`,
12
+ `OversampledDriveConfig`, and `OVERSAMPLED_DRIVE_LATENCY_SAMPLES = 32`.
13
+
14
+ Config: host `sampleRate` finite in [8000,192000] Hz; immutable `factor: 2 | 4`;
15
+ fixed `curve: 'hard' | 'soft' | 'asymmetric' | 'fold'`. Existing drive modules are
16
+ not invoked at an unsupported inner sample rate. The curve is a pure arithmetic
17
+ memoryless function evaluated at the explicit high-rate phases.
18
+
19
+ `tick(input, gain, mix, reset)` once per host sample. Input intentionally clamps
20
+ to [-8,8], linear gain to [0,32], mix to [0,1]. NaN becomes zero and infinities
21
+ saturate. Dry uses the same sanitized input. Gain multiplies the base-rate input
22
+ before interpolation. There is no hidden control smoothing or loudness matching.
23
+ Mix is applied after decimation. Factor and curve cannot switch at runtime.
24
+ Aliasing already present in the host input, or created by the host-rate gain
25
+ multiplication, is not reversed by this interpolation stage.
26
+
27
+ ## Filter and phase contract
28
+
29
+ For L=factor, each lowpass is N=32*L+1 taps, symmetric around D=16*L, using cutoff
30
+ fc=0.45/L cycles per high-rate sample:
31
+
32
+ h0[k] = sinc(2*fc*(k-D)) * 2*fc * Blackman(k),
33
+ Blackman(k) = 0.42 - 0.5*cos(2*pi*k/(N-1)) + 0.08*cos(4*pi*k/(N-1)).
34
+
35
+ Both endpoints are exactly zero; h=h0/sum(h0). Interpolation uses zero stuffing
36
+ followed by L*h, evaluated as fixed polyphase sums. At high-rate index L*n+r,
37
+ up[n,r] = L * sum_j h[r+L*j] * driven[n-j]. The memoryless curve is evaluated at
38
+ each upsampled value. A second h filters the shaped stream, then phase-zero
39
+ samples at high-rate L*n are retained. There is no unfiltered decimation.
40
+
41
+ The linear cascade's group delay is 2*D/L = 32 host samples for both factors.
42
+ This is bulk delay, not a requirement for silence before sample 32: the causal
43
+ response has precursors and a 65-tap support spanning 64 sample intervals.
44
+ Its dry counterpart is g[j] = L*(h convolved with h)[L*j], j=0..64. This is the
45
+ exact same linear interpolation/decimation response: identical delay and
46
+ rolloff, not merely a pure delay approximating the nonlinear path's phase.
47
+ Mix zero therefore remains filtered and delayed; it is not transparent bypass.
48
+ No sample-accurate constant group delay is claimed for arbitrary nonlinear
49
+ signals, as opposed to the surrounding linear filtering.
50
+
51
+ ## State and bounds
52
+
53
+ Native fixed rings retain 33 driven base samples, 65 dry base samples, and N
54
+ shaped high-rate samples, plus cursors and valid counts. Reset invalidates old
55
+ history before consuming the current host sample; no stale samples may return.
56
+ Held reset repeatedly applies this rule. Same-schema native snapshots preserve
57
+ all histories and phase. State scaling retains recoverable f32 subnormals.
58
+
59
+ Finite FIR coefficient L1 bounds cover interpolation overshoot and dry output.
60
+ The curve limits every shaped high-rate value to [-1,1] (asymmetric [-0.5,1]);
61
+ therefore the wet output is bounded by sum(abs(h)). Transient overshoot above
62
+ unity is permitted and documented, never hidden with a final output clip.
63
+ There is no optional DC blocker or automatic limiter.
64
+
65
+ ## Verification coverage
66
+
67
+ - Independent literal zero stuffing and time-domain convolution, including phase
68
+ zero versus a deliberately wrong decimation-phase counterexample
69
+ - Symmetry, impulse support/peak, constant gain, linear dry/wet null, near-Nyquist
70
+ attenuation and measured anti-imaging/stopband response
71
+ - Fixed synthetic folded-alias metrics and fundamental gain for direct versus
72
+ 2x/4x; no universal bandlimit or monotonic audible-quality promise
73
+ - 44.1/48/96 kHz, gain/mix changes, reset before current sample, held reset,
74
+ independent instances, same-schema continuation, tiny and non-finite controls
75
+ - Maximum-factor graph/compile/WASM/memory/cost diagnostic and isolated packed
76
+ public declarations, actual rendering, hashes, and native parameter edits
77
+
78
+ Any generated audio is **CANDIDATE**. Offline evidence does not establish browser
79
+ quantum deadlines, maximum simultaneous instance count, or listening approval.
80
+
81
+ The independent integration owner approved this bounded entry before DSP.
82
+ Existing `/drive` and `/curve-shaper` source and semantics remain untouched.
83
+
84
+ ## Filter bandwidth and residuals
85
+
86
+ The design's individual filter magnitude is approximately 0 dB through
87
+ 0.35*hostRate, -0.34 dB at 0.4*hostRate, -6.02 dB at 0.45*hostRate, -28.37 dB at
88
+ host Nyquist, and -79 dB at 0.55*hostRate. The band between 0.35 and 0.55*hostRate
89
+ is a transition, not a flat audio passband. This deliberately sacrifices high
90
+ treble near host Nyquist to attenuate images and decimation aliases.
91
+
92
+ The base-rate linear response must be computed from g, not assumed to be exactly
93
+ one filter's squared magnitude: finite residual image terms survive the
94
+ interpolation/decimation cascade. In particular the mirrored transition term
95
+ matters at host Nyquist. The per-phase interpolator's constant-input gain ripple
96
+ is about 1.7e-6 before shaping. Finite filters and a finite internal sample rate
97
+ leave residual aliases. Increasing factor alone does not make arbitrary driven
98
+ waveforms bandlimited, restore existing aliases, or guarantee better perception.
99
+
100
+ For the specified kernels, sum(abs(h)) is about 1.853 at 2x and 1.847 at 4x.
101
+ The matched dry kernel has L1 norm about 1.879, so an arbitrary bounded dry input
102
+ can reach a conservative 15.04 transient bound. This is a stability bound, not
103
+ a recommended signal level. Wet output may also exceed unity because of the
104
+ post-shaper FIR. No final clip masks those filter transients.
105
+
106
+ ## Measured candidate evidence
107
+
108
+ The 11 focused tests render actual native WASM at 44.1,48,96 kHz. All fixed
109
+ curves and both factors match an independent literal zero-insertion/high-rate
110
+ convolution reference at f32 precision for the tested input, gain, mix and reset
111
+ sequences. That oracle does not use the implementation's polyphase indices or
112
+ precomputed dry kernel. A deliberately wrong decimation phase differs enough to
113
+ fail the same oracle. Linear dry/wet null, symmetric impulse peak at 32, nonzero
114
+ precursors, exact finite tail drain, resets, same-schema continuation, independent
115
+ silent instances, invalid controls and recoverable tiny impulses are checked.
116
+
117
+ A 1001-point frequency sweep of the independently generated individual FIR
118
+ finds maximum passband magnitude error below 0.000167 through 0.35*hostRate, and
119
+ stopband attenuation at least 75.27 dB from 0.55*hostRate to the internal Nyquist.
120
+ This is the stated guardband, not a promise of that rejection immediately above
121
+ host Nyquist. The native convolution-oracle checks also bind the implementation
122
+ to this filter design.
123
+
124
+ Alias fixtures use 8192-frame coherent sines at bins 997 and 1709, amplitude 3,
125
+ after a complete period of warmup. Projection removes DC and harmonics below
126
+ host Nyquist; the residual is measured without loudness normalization. Across
127
+ all four curves at the three rates, folded RMS falls 13.75–27.78 dB at 2x and
128
+ 29.98–48.63 dB at 4x versus direct shaping. The requirements remain 6 dB and 10 dB
129
+ respectively. Fundamental amplitudes are recorded separately, and are preserved
130
+ to about 5e-5 absolute amplitude in these specific fixtures. Different inputs,
131
+ gain modulation, and frequencies can produce different outcomes.
132
+
133
+ `tests/oversampled-drive-packed.test.mjs` installs the actual tarball into a locked
134
+ isolated consumer. Strict public declarations, native a-rate gain/mix writes,
135
+ all 24 factor/curve/rate combinations, independent convolution, exact native
136
+ snapshot continuation, tail drain and source/package/audio hashes are required.
137
+ Whole offline-renderer output must exactly equal the manually driven native WASM
138
+ instance, rather than a recreated JavaScript processor.
139
+
140
+ The public maximum-factor candidate has at most about 0.44 MB of serialized
141
+ graph, 52,391 bytes of WASM, and fixed 65,536-byte WASM memory per instance in the
142
+ fixture. Timings are recorded separately from correctness: local process medians
143
+ are below one millisecond in these runs, but cold quanta have exceeded 19 ms.
144
+ The cold cost alone rules out claiming a verified browser deadline. These are
145
+ single-instance local Node diagnostics with host jitter and no input/output copy
146
+ or browser scheduling cost. No real-time capacity, browser worklet, device,
147
+ true-peak, or listening approval is implied.
148
+
149
+ Use the normal native construction API, for example
150
+ `instantiate(oversampledDrive, {sampleRate, factor:4, curve:'soft'}, {name:'drive'})`,
151
+ then call `tick(input, gain, mix, reset)` inside the enclosing `forSample`.
152
+ The 32-sample bulk-delay constant is useful when designing a chain, but the full
153
+ matched dry response remains necessary for this module's own linear null.
@@ -0,0 +1,53 @@
1
+ # Bounded performance candidates
2
+
3
+ Status: CANDIDATE. Catalog §4 extension; existing `voicePolicy`, instrument, presets and MIDI runtime are unchanged. This is not general MPE support, hardware validation, real-time acceptance or human-approved sound.
4
+
5
+ ## Public surface
6
+
7
+ `@denaudio/den/performance` exports `performancePolicy`, `pitchGlide` and `tunedFrequency`, plus their TypeScript control/configuration types. Use existing unworklet 0.4.1 native MIDI, AudioParams and snapshots.
8
+
9
+ `performancePolicy({mode, capacity, heldCapacity, legato})` composes the existing allocator. Mono requires one voice; poly defaults to four, maximum four. The held-identity ledger defaults to eight, maximum eight, and must be at least voice capacity. Methods are `bindMidi(input)`, `reset(when)`, `overflowed()`, and `voices[]`. Each voice retains `read()`, `takeRetrigger()` and `releaseFinished(done)` and adds `takeReset()` for a latched DSP-clear pulse. `read()` adds normalized `channelPressure`, `polyPressure`, `bend`, `timbre` and boolean `sustain` lanes. Bind once in declaration scope. Do not also bind the wrapped allocator.
10
+
11
+ ### Identity, sustain and cleanup contract
12
+
13
+ - Each accepted positive-velocity note-on creates one bounded channel/key identity. FIFO note-off pairing is with the oldest physically-down matching identity, including stolen identities. Velocity-zero note-on is note-off.
14
+ - With sustain down (CC64 >=64), note-off marks that identity physically released but keeps it held in the original allocator. Its gate stays high and it retains held-note allocation priority. The allocator still prefers free voices, then ordinary released voices, then steals the oldest held voice, which can be a pedal-held voice.
15
+ - Pedal-up releases deferred identities in their original note-on order, oldest first. Ordinary nondeferred key releases take effect immediately. Mono therefore keeps the original last-held-note priority among physically-held and pedal-held identities; pedal-up may fall back to another held note without retrigger when legato is true.
16
+ - Repeated same-key identities remain separate. A pedal-held older identity does not consume a later physical note-off twice. Pedal-up forwards exactly one oldest allocator note-off for each deferred identity. Stolen identities stay tracked until their own release, preventing a stale note-off from releasing a newer duplicate.
17
+ - A full ledger rejects new positive note-ons and latches overflow. It never evicts an accepted held identity. MIDI 1.0 cannot identify a late note-off belonging to a rejected duplicate, so FIFO pairing remains explicit. Reset clears overflow; per-channel cleanup does not clear the global diagnostic.
18
+ - CC123 marks all physically-down keys on its channel released. Pedal-down defers them; pedal-up releases them. It does not change expression or pedal state.
19
+ - CC120 immediately clears that channel's keys and active/releasing voices, sets its pedal up and latches `takeReset()` for slots that were using that channel. It retains other expression controls. Mono may fall back to a different channel's held key, following the original allocator policy. The DSP-clear latch belongs to the physical slot and must be consumed by composition code before sounding any replacement there.
20
+ - CC121 centers bend, clears channel pressure, timbre and that channel's active poly-pressure values, raises its pedal and releases its deferred identities. Physically-down notes stay held.
21
+ - `reset(true)` clears every held identity, allocation, pedal and expression lane, pending retrigger and overflow, and latches every slot's DSP reset. Held reset repeatedly clears. `reset(false)` leaves state unchanged. Ordering follows graph invocation; a composition wanting panic to win over same-quantum MIDI must invoke reset after MIDI dispatch at the start of process, as the example does.
22
+ - Release completion is forwarded to the original allocator only after the current envelope is done. Sustain-held notes cannot be freed by a stale completion because their allocator owner remains held. Completion must describe the current slot envelope; callers must not reuse completion from a previous owner.
23
+
24
+ ### Expression contract
25
+
26
+ - Pitch bend 0..16383 maps to -1..1, center 8192 maps to exactly zero. Negative values divide by 8192; positive values divide by 8191. Bend range is a composition choice in semitones.
27
+ - Channel pressure and CC74 timbre normalize 0..127 to 0..1 and persist per channel until updated, CC121 or reset. They affect held and releasing voices on that channel.
28
+ - Native `aftertouch` is polyphonic key pressure. It updates all currently active matching channel/key voices, including release tails. Pressure received before a matching voice exists is ignored. New assignment/retrigger clears that slot's poly pressure; a mono legato pitch/channel change also clears it. Another same-key voice's existing pressure is preserved until the next matching aftertouch message. This is key-addressed expression, not a distinct-note MIDI 2.0 identifier.
29
+ - Inactive voice expression outputs are zero. Unsupported controllers and message families are ignored. CC74 is a raw timbre lane; this module does not invent a destination or smoothing rule.
30
+ - unworklet 0.4.1 drains MIDI in FIFO order before each 128-sample process quantum. This layer preserves that timing. Several changes in a quantum coalesce to the final assignment and latched start/reset flags. It does not implement sample-accurate MIDI scheduling.
31
+ - Every performance/held-key/expression state slot is native transient state. A snapshot excludes held notes/pedals/controllers. Fresh-instance restoration starts from their declaration defaults; in-place native restore preserves their current values rather than clearing them. For a cold live restore, explicitly reset the allocation/expression/DSP composition after restoring and render a quantum before new notes. DSP composition must reset inactive voices to prevent an orphan envelope tail.
32
+
33
+ ## Glide and tuning contract
34
+
35
+ `pitchGlide({sampleRate}).tick({target,seconds,reset,snap})` returns MIDI-note semitones. Supported construction rates are integer 8000..192000 Hz. Finite target is clamped to [-128,255], finite seconds to [0,30], duration is floor(seconds * sampleRate + 0.5). The first call snaps to target. A changed target starts a new constant-time linear glide from the last emitted value; a change during a glide replaces its destination and duration. The first step is emitted on the change sample and step N is the exact endpoint. Zero/sub-half-sample durations snap. Seconds changes without a target change do not retime the current glide. `snap` and `reset` are level-sensitive and follow the current target immediately while held; releasing them does not invent a new glide. Native persistent state resumes exactly at the same sample rate. Different-rate snapshot restoration is outside this candidate contract.
36
+
37
+ `tunedFrequency({note,transpose,cents,a4}, maxFrequency)` is a stateless 12-TET mapping: A4 × 2^((note−69+transpose+cents/100)/12), with note [-128,255], transpose [-48,48], cents [-1200,1200], A4 [220,880]. maxFrequency is a finite positive construction value no greater than the largest finite f32 value. The combined musical exponent is calculated before the single final frequency clamp. Controls must be finite. The bounded conversion uses degree-12 f64 exp(x/32) polynomial arithmetic and five squarings. Native 0.4.1 pow and exp both demote through an approximate f32 bridge; the independent tuning oracle caught this path's 0.05–0.098 Hz error before replacement. The independent oracle's threshold was preserved.
38
+
39
+ ## Candidate evidence
40
+
41
+ - Native receiver entry passes at 44.1/48/96 kHz, including channel pressure, aftertouch, pitch bend and CC. An event scheduled at sample 129 is observed at boundary 128, demonstrating the native quantum timing rather than claiming sample timing.
42
+ - 33 focused tests cover closed-form interrupted glide, duration latching, held reset/snap, tiny signed pitch, tuning and AudioParam automation, independent object/queue FIFO identity and sustain oracle, all controller cleanup paths, capacity/overflow, mono legato owner changes and same-key pedal/off orders. The combined focused and unchanged allocator/envelope/instrument regression run passes 94 tests.
43
+ - The isolated packed consumer imports only public subpaths and typechecks strictly. At all three rates it renders the existing envelope and sine oscillator, checks held-pedal release, bend, pressure and timbre, and compares against independent Math.sin and linear-envelope equations. A separate chord exercises all four active voices. Tuning and automation outputs match the independent f32 oracle; all runs report zero scrubbed samples.
44
+ - Performance state is absent from snapshots, while the composed envelope/oscillator/glide state is persistent. Restoring a snapshot from a sustained note into a fresh offline instance produces exact silence; a fresh note after that restore restarts safely. This does not establish clearing of an already live browser instance. The pitch-glide primitive separately proves exact same-rate continuation.
45
+ - The maximum declared four-voice/eight-identity diagnostic composition compiles to 747,451 WASM bytes with 65,536 bytes of memory, unchanged across 256 driver blocks. This is a local fixed-memory check, not a timing or hardware acceptance claim.
46
+
47
+ The packed gate retains source/package/lock hashes, exact source commit and dirty-state flag, per-rate numerical results, and CANDIDATE status in its manifest. The first source-development packed run measured maximum audio error below 1.5e-8; subsequent clean-head results are in the generated manifest/result pair.
48
+
49
+ The composition's choices are deliberately explicit: two-semitone bend range; normalized channel/poly pressure maximum scaled to amplitude 0.5..1; CC74 scaled to amplitude 0.5..1; gain 0.2; instantaneous attack/decay; a parameterized linear release; and sine source. These are diagnostic values and routing, not an approved preset. Pitch and frequency intermediates are materialized to bound the unworklet graph. Existing instrument and voice-policy behavior is unchanged.
50
+
51
+ ## Remaining boundaries
52
+
53
+ This policy does not implement MPE. The separate `/mpe-expression` entry adds a manually configured lower-zone expression subset; full MPE zone/pedal/RPN behavior remains outside both contracts. No hardware MIDI-host integration, browser-rate expansion, real-time CPU budget, acoustic/listening approval, cross-rate snapshot migration or approved golden is claimed. The full integrated catalog gate remains the integration owner's responsibility.
@@ -0,0 +1,55 @@
1
+ # Octave-periodic musical pitch quantizer
2
+
3
+ `@denaudio/den/pitch-quantizer` exports `pitchQuantizer`, `PitchQuantizerConfig`, and `PitchQuantizerOutput`. This is the catalog group 5 musical-quantize candidate. It quantizes a pitch-control signal using native unworklet 0.4.1 DSP. It does not detect pitch in audio, tune an audio recording, quantize event timing, or deliver MIDI to a host.
4
+
5
+ ## Construction and use
6
+
7
+ ```ts
8
+ import { instantiate } from '@unworklet/core';
9
+ import { pitchQuantizer } from '@denaudio/den/pitch-quantizer';
10
+
11
+ // Inside defineProcessor, outside forSample:
12
+ const quantizer = instantiate(pitchQuantizer, {
13
+ pitchClasses: [0, 2, 4, 5, 7, 9, 11],
14
+ hysteresis: 0.125,
15
+ }, { name: 'cMajor' });
16
+
17
+ // Inside forSample, exactly once per sample:
18
+ const result = quantizer.tick(pitchControl, reset);
19
+ // result.pitch is f32; result.degree and result.octave are i32.
20
+ ```
21
+
22
+ Pitch uses semitones on the MIDI-coordinate axis: 60 is C4; negative and fractional controls are valid. Each allowed pitch is `12 * octave + pitchClasses[degree]`. `octave` is that integer multiplier, not a printed musical octave number. For example, pitch 60 has octave multiplier 5. To transpose a scale, prepare its transposed offsets in `[0,12)` before construction. No tuning file loader, tonic control, Hz conversion, gate policy, or scale-name registry is included.
23
+
24
+ - `pitchClasses`: 1 through 128 numbers. Entries must be finite in `[0,12)`, including after conversion to IEEE f32. The module copies, f32-rounds, and sorts them ascending. Exact duplicates and duplicates after rounding, including 0/-0, are rejected. The input order has no musical meaning; returned degree indexes the sorted canonical list. A value just below 12 that rounds to 12 is rejected rather than silently wrapping.
25
+ - Fractional and microtonal offsets are supported, including representable f32 subnormals. This is not restricted to twelve-tone equal temperament. Offsets finer than f32 precision round to the same value and therefore cannot be distinct degrees. The period remains exactly twelve semitones; non-octave-repeating tunings are outside this module.
26
+ - `hysteresis`: optional finite number in `[0,12]` semitones, default 0, converted to f32. It is a construction-time margin, not a time constant. Positive values smaller than the least positive f32 round to 0. Scale structure and margin cannot be changed through a per-sample control.
27
+ - Invalid construction inputs throw `RangeError`. These bounded inputs control graph size; no callback-time array allocation or arbitrary JavaScript execution is used.
28
+
29
+ ## Nearest, precision, and invalid controls
30
+
31
+ On every sample, NaN input maps to pitch 0. All other inputs, including infinities, clamp to `[-16384,16384]`. Quantization then searches the scale; the output itself is not clipped to that range. For example, offset `[6]` can produce pitch 16386 from input 16384. This preserves scale membership at the input-domain endpoints.
32
+
33
+ Without hysteresis, choose the candidate with the smallest native binary64 absolute distance from the sanitized f32 input. Computed distance equality chooses the lower computed binary64 pitch. If different degrees collapse to that same binary64 pitch, the lower sorted degree index wins. Each class compares the distances of its two bracketing octaves; a midpoint shortcut would not preserve this computational tie policy for all tiny controls. The result is then converted to f32. Thus arithmetic and output rounding are explicit parts of the contract; this is not arbitrary-precision tuning. Fractional degrees that differ in their construction values can produce the same f32 output at a large octave, and exceptionally tiny offsets can also collapse during binary64 octave arithmetic.
34
+
35
+ The chosen degree and octave are retained as integer state, never reconstructed from the rounded output. They are returned along with pitch, so callers can inspect the selected scale identity even where two f32 output pitches coincide. Internal distance/pitch staging uses exact power-of-two scaling to preserve subnormal f32 values through unworklet 0.4.1's scalar-state denormal floor.
36
+
37
+ ## Hysteresis, reset, and rate
38
+
39
+ For positive margin `h`, the current degree remains selected while the sanitized input is within the inclusive interval
40
+
41
+ `[(previousPitch + heldPitch)/2 - h, (heldPitch + nextPitch)/2 + h]`.
42
+
43
+ Previous and next mean adjacent entries in the periodic sorted scale, including across an octave boundary. Bounds are evaluated as binary64 local-degree midpoints plus the integer octave base. Both boundary equalities retain the held degree. Outside the interval, the result jumps directly to ordinary nearest for the current input; it does not walk one scale degree per sample. For large margins, several neighbors can be passed before a change. A one-degree scale uses the same degree in adjacent octaves as its neighbors.
44
+
45
+ The first call and every reset-high sample ignore history and emit ordinary nearest immediately. Holding reset high therefore continues to quantize moving input, rather than muting output or pinning one pitch. The next reset-low sample starts hysteresis from the last reset-selected degree. At margin 0, every sample uses ordinary nearest, including its lower-pitch tie policy; there is no history-dependent tie retention.
46
+
47
+ There is no smoothing, event queue, lookahead, clock, time constant, or sample-rate-dependent state. Call once per sample of the chosen control stream. The same trajectory has the same result at 44.1, 48, and 96 kHz. Existing unworklet snapshots restore degree, octave, initialization, and internal staging state; no new serialization API is introduced. Different named instances have independent state.
48
+
49
+ ## Evaluation and limitations
50
+
51
+ The source tests exercise native rendering against an independent exhaustive local lattice rather than copying the per-class search. Coverage includes signed octaves, scale/octave midpoints and adjacent f32 values, input clamp endpoints, NaN/infinities, one-note and microtonal scales, subnormal offsets, f32-output collapse, duplicate-after-rounding rejection, maximum 128-degree capacity, hysteresis equality and leaps, held reset, and exact snapshot continuation. The isolated packed-consumer gate additionally checks public TypeScript declarations, package resolution, actual native rendering at 44.1/48/96 kHz, zero scrubbed samples, and fixed memory.
52
+
53
+ Work per sample and captured graph size are linear in the fixed degree count. All candidates are evaluated; this is not a promise of automatic control-rate scheduling. No browser/hardware real-time acceptance, auditory approval, note-host delivery, adaptive tuning, or perceptual pitch quality is claimed. Test and audition artifacts remain CANDIDATE.
54
+
55
+ Scope source: [den catalog, group 5](https://app.notion.com/p/3ef449b4e81d817db1acf1fff48779b3). The numerical policy above is this implementation's explicit bounded interpretation of musical quantize, not a feature-equivalence claim about a referenced product.
@@ -0,0 +1,188 @@
1
+ # Prepared-spectrum convolution candidate
2
+
3
+ `@denaudio/den/prepared-convolution` exports `preparedConvolution`, the host-only
4
+ `prepareConvolutionSpectrum`, and `PreparedConvolutionConfig` /
5
+ `PreparedConvolutionPacket`. This is an offline-validated **CANDIDATE**, without
6
+ browser, realtime-capacity or human-audition clearance.
7
+
8
+ ## Fixed boundary
9
+
10
+ - Mono, block size B=128, FFT N=256, P=64 partitions. Configuration must be exactly
11
+ `{ blockSize: 128, partitions: 64 }`; unsupported configurations throw.
12
+ - Original IR: 1..8192 finite real taps, each in [-1,1], with absolute sum <=4.
13
+ No normalization, truncation, sample-rate conversion or output limiter.
14
+ - Audio input must be finite and normalized to [-1,1]. Output can exceed unity.
15
+ - Nominal latency is 128 samples: 2.902 ms at 44.1 kHz, 2.667 ms at 48 kHz,
16
+ 1.333 ms at 96 kHz. IR leading zeros and its own tail are additional.
17
+ - Maximum original IR duration is 185.76 / 170.67 / 85.33 ms at those rates.
18
+ This is a bounded cabinet/body/short-space candidate, not arbitrary long-IR
19
+ room convolution. The existing small `partitionedConvolution` is unchanged.
20
+ - The nominal target is original direct FIR delayed by B. Quantized spectra
21
+ introduce small bounded circular/padding leakage, so exact onset/support
22
+ zeros and exact original-IR gain are not promised after quantization.
23
+
24
+ ## Host preparation and native delivery
25
+
26
+ Call `prepareConvolutionSpectrum(impulse, config)` outside audio processing.
27
+ Accepted inputs are arrays, Float32Array or Float64Array. Invalid input/config
28
+ and excessive reconstruction error throw RangeError; the helper never changes
29
+ IR gain to make an asset pass.
30
+
31
+ The returned native packet contains:
32
+
33
+ | Field | Value |
34
+ | --- | --- |
35
+ | formatVersion | 1 |
36
+ | blockSize | 128 |
37
+ | partitions | 64 |
38
+ | impulseFrames | Original integer length, 1..8192 |
39
+ | declaredValues | 32768 |
40
+ | spectrum | Float32Array, exactly 32768 values |
41
+
42
+ Bin k of partition p starts at `2*(p*256+k)`, real then imaginary. Forward sign
43
+ is `exp(-i*2*pi*k*n/256)`, without normalization. Each original partition has
44
+ 128 taps followed by 128 zeros. Unused partitions are zero. Preparation enforces
45
+ conjugate symmetry, exact zero DC/Nyquist imaginary components, and Float32
46
+ rounding. The returned packet remains mutable: send it **unchanged**.
47
+
48
+ The caller explicitly declares a native event and forwards its graph payload:
49
+
50
+ ```ts
51
+ import { CAPACITY_16, event, instantiate } from '@unworklet/core';
52
+ import {
53
+ preparedConvolution, type PreparedConvolutionPacket,
54
+ } from '@denaudio/den/prepared-convolution';
55
+
56
+ const convolver = instantiate(preparedConvolution,
57
+ { blockSize: 128, partitions: 64 }, { name: 'convolver' });
58
+ const ir = event<PreparedConvolutionPacket>({
59
+ from: 'main', name: 'ir', capacity: CAPACITY_16, payloadCapacity: 131072,
60
+ });
61
+ ir.onReceive(packet => convolver.load(packet));
62
+ // Inside stride-1 forSample: convolver.tick(input, reset, everyNSamples).
63
+ ```
64
+
65
+ No event, loader, acknowledgement or asset-file abstraction is created by the
66
+ module. The caller uses existing native host message facilities and owns IR
67
+ sample-rate conversion. Native transport can truncate oversized typed arrays:
68
+ validate type and length before enqueueing. The independent declaredValues
69
+ field detects accidental truncation only when it truthfully states original
70
+ length; it cannot authenticate a forged packet.
71
+
72
+ The native handler checks format/capacity/frame count/decoded length and scans
73
+ all 32768 components for NaN, infinity and abs(component)>4. This is malformed
74
+ number protection, **not** inverse-domain IR/L1/symmetry/provenance validation.
75
+ Correctness and error claims require the unchanged host-prepared packet.
76
+
77
+ ## Approximation certificate
78
+
79
+ Host preparation independently reconstructs each quantized partition through a
80
+ direct inverse DFT over all 256 samples. It sums absolute real error from the
81
+ original zero-padded partition plus absolute imaginary residual over every
82
+ partition/sample, adds a 1e-8 host numerical guard, and rejects above 4e-6.
83
+ The numerical guard is conservative relative to the measured modulo-reduced
84
+ host DFT error; this is not a formal cross-engine bound on JavaScript Math.
85
+
86
+ For any output block phase, the two overlapping input blocks select complementary
87
+ halves of each circular error impulse. With |input|<=1 their total error is at
88
+ most that partition's reconstruction L1 error. This includes quantized padded-
89
+ half leakage and circular wrap, rather than assuming those values remain zero.
90
+ The independent test also checks the looser spectral estimate
91
+ `2B * sum_p(max_k(complexMagnitude(deltaH[p,k])))`.
92
+
93
+ A Float32 quantization sanity estimate from Parseval/Cauchy-Schwarz is
94
+ `sqrt(N)*2^-24*sum_p(L1(partition[p]))`, at most 3.815e-6 for N256/L1<=4,
95
+ plus the absolute subnormal term `P*sqrt(2*N)*2^-150` (about 1.02e-42).
96
+ The per-asset inverse-domain certificate remains the actual rejection gate.
97
+
98
+ Native arithmetic/history/FFT/products/OLA use Float64, coefficients use
99
+ Float32, and input/history are scaled by exact 2^128 to preserve signed
100
+ subnormal audio against core 0.4.1's tiny-state flushing. Output is Float32.
101
+ Tiny IR coefficients can quantize to zero; **no relative tiny-IR accuracy** is
102
+ promised. A preparation certificate is not a proof of native arithmetic error.
103
+
104
+ Acceptance tests separately target native-vs-quantized-operator absolute error
105
+ <1e-6 and original-FIR error <5e-6. These are measured test targets, **not a
106
+ universal total-error guarantee**. Corresponding 4.000005 gain headroom is a
107
+ practical test budget, not a proven bound for every implementation/environment.
108
+
109
+ ## Load, reset, state and scheduling
110
+
111
+ `tick(input, reset, everyNSamples)` returns `{ output, loaded, rejected,
112
+ revision }`. Call it once per sample inside stride-1 native forSample. This
113
+ implementation requires the core 0.4.1 128-sample quantum.
114
+
115
+ - Valid full packet: loaded=true, rejected=false. A shorter IR still carries
116
+ the complete padded 32768-value table, so previous coefficients are replaced.
117
+ - Explicit unload: matching format/B/P, impulseFrames=0, declaredValues=0 and
118
+ empty Float32Array. loaded=false, rejected=false.
119
+ - Malformed load: loaded=false, rejected=true; no prefix is accepted as an IR.
120
+ - Every load attempt, including rejection/unload, increments wrapping i32
121
+ revision. Compare for inequality rather than ordering. Do not round-trip
122
+ revision through an f32 audio channel if exact i32 identity matters.
123
+ - Every attempt invalidates old input/spectrum history and OLA tail without
124
+ resetting cursor/hop phase. A valid load accepts the next audio sample.
125
+ - Reset keeps the IR and revision, discards its own input sample, invalidates
126
+ history/tail and preserves phase. Held reset is silent. Release resumes with
127
+ nominal B latency. Reset never causes sample-scheduler rephasing.
128
+ - Missing/unloaded/rejected assets are silent. Invalidation is logical, not
129
+ secure erasure; bytes may remain in buffers/snapshots until overwritten.
130
+
131
+ FFT+64-partition MAC+IFFT+OLA run eagerly once every 128 samples, including while
132
+ unloaded. Indices are explicit; native counted loops avoid graph expansion by
133
+ P*N. B equals quantum size, so the dispatch phase is identical at every native
134
+ snapshot boundary. Named persistent cursor, spectral cursor, valid counts,
135
+ coefficients/status/revision, histories and overlap fully describe continuation.
136
+ Same-graph/config/rate native snapshots continue bit-identically. Pending queued
137
+ messages, cross-configuration migration and live-swap crossfades are outside
138
+ this contract. Transient FFT/mix buffers are overwritten before use.
139
+
140
+ ## Loading and resource costs
141
+
142
+ The native coefficient table is 128 KiB, complex input history 256 KiB, and
143
+ 16-slot native payload ring 2 MiB. Scratch/history/OLA, scalar state and I/O add
144
+ more; use the complete packed profile for actual fixed WASM memory.
145
+ Sixteen queued loads copy 2 MiB and scan 524288 coefficient components at one
146
+ boundary. Preparation allocates host arrays and performs direct DFT/certificate
147
+ work outside the callback. Native copy/scan occurs during audio message dispatch,
148
+ not outside the callback. Preload with audible playback gated off. postMessage
149
+ fallback may allocate. No allocation-free-loading, realtime live-swap or
150
+ concurrent-instance capacity claim is made.
151
+
152
+ The public core driver has no message-ingress method. The packed profile
153
+ therefore reports unloaded full-loop cold/warm quantum cost separately from
154
+ loaded one-quantum `renderOffline` end-to-end cost for 0/1/16 queued packets.
155
+ End-to-end includes instantiate, enqueue, copy/scan, DSP, I/O and snapshot; it is
156
+ not an isolated handler measurement. Do not infer audio deadline clearance from
157
+ those timings. Complete measured numbers are recorded with exact source and
158
+ package hashes in the packed artifact manifest.
159
+
160
+ ## Evidence
161
+
162
+ - Three-rate focused tests: full8192 impulse tails, dense original FIR,
163
+ independent quantized circular operator, complex transfer, superposition,
164
+ all128 reset offsets/rate, held reset, malformed→unload→short replacement,
165
+ wrapped i32 revision and signed subnormal input.
166
+ - 24 focused and 21 packed bit-identical snapshot continuations across
167
+ load/reject/unload/short replacement and reset states; zero output scrubs.
168
+ - Separate native Float32 full-capacity entry and host precision probes retain
169
+ independent assertions. The earlier Float64 failure is preserved as a manual
170
+ red reproduction: core 0.4.1 payload.length seals the field as Float32 even
171
+ after Float64 copy, reporting twice the intended element count. No upstream
172
+ dependency patch or byte reinterpretation is used.
173
+ - Packed public typecheck, actual native-message render and Vite worklet build.
174
+ Test audio is CANDIDATE; no human approval or browser realtime clearance.
175
+
176
+ The preliminary packed gate passed in16.23 s (759 MB process-group peak RSS).
177
+ At48 kHz its fresh local profile recorded26,747-byte WASM and2,555,904 bytes
178
+ fixed memory, including the2 MiB payload ring. Unloaded full-loop cold quantum
179
+ was2.063 ms; startup maximum4.218 ms; warm512-quantum p50/p99/max including
180
+ I/O copies was0.142/0.539/2.141 ms. Dense8192-tap host preparation was58.6 ms.
181
+ The16-load one-quantum offline end-to-end median/max was6.244/11.178 ms.
182
+ These measurements include host jitter and do not establish an audio deadline
183
+ guarantee; in particular the startup maximum exceeds the48 kHz deadline.
184
+
185
+ Packed three-rate original-FIR error was2.235e-8 and full impulse error5.372e-9.
186
+ The first focused run passed all14 tests in24.15 s with677 MB process-group peak RSS.
187
+ Measured dense original-FIR error was1.027e-9, native-vs-quantized operator
188
+ 8.808e-10, superposition2.329e-9, complex transfer6.597e-8 at all three rates.
@@ -0,0 +1,63 @@
1
+ # Sustained audition deadline regression
2
+
3
+ The manual `node scripts/probe-realtime.mjs` command builds its own isolated
4
+ packed consumer before capture. It does not require or alter public `site-dist`,
5
+ and it does not restore the removed public audition page. The capture modes and
6
+ assertions below are unchanged.
7
+
8
+ The first browser tests proved sample values and lifecycle behavior, but did not prove that an AudioWorklet could produce those samples on time. In particular, waiting only for AudioContext time lets an overloaded renderer eventually finish and hides wall-clock drift. This gap allowed a numerically correct sustained sine to crackle during real playback.
9
+
10
+ ## Reproduction and isolation
11
+
12
+ At reviewed frontend head `5d42072`, the audition compiled to 1,122,834 bytes of WASM. In this execution environment, warmed native WASM calls over 128-frame blocks measured a median 2.82 ms and p95 8.90 ms, exceeding the 48 kHz budget of 2.667 ms. A real Chromium capture took about 8.09 wall-clock seconds for five seconds of graph output and reported 839 playback underrun events. The raw graph samples remained continuous. Disabling waveform drawing did not remove the problem; a separate native sine context could render without underruns. The independent reviewer also reproduced excessive AudioWorklet process cost and phase-dependent stalls with depth zero.
13
+
14
+ This isolates a deadline failure in the composed DSP, rather than a phase reset in its sample sequence. The published 0.4.1 arithmetic emitter recursively emits child expressions; retaining a Node in a JavaScript variable does not materialize its computed value. The repeated polynomial/squaring expressions used by pitch modulation amplify the composed expression tree. Explicitly materializing the subgraph boundaries reduces emitted size and processing cost while preserving samples. This is a den composition fix, not an upstream patch or a claim that every compiler path has been audited.
15
+
16
+ The fix stores envelope level, LFO output and final pitch in three existing unworklet f32 transient state slots, then reads each later in the same sample. It adds no sample delay, custom state mechanism, preset or routing system. Each subgraph still ticks once per sample. The independent sine-times-envelope error remains 8.15e-8. WASM becomes 34,679 bytes; a warmed run measured p50 0.109 ms and p95 0.199 ms. These are measurements on this runner, not guarantees for every physical device or the future polyphonic instrument.
17
+
18
+ The final regression harness was also run against the unchanged `5d42072` audition source: its native control recorded zero underruns, while den windows recorded 589/588/594/592 events and took 6.1–6.4 seconds for five seconds of samples. It exited with `full: real-time underrun events` (589 != 0). Restoring the materialized source passed the same checks.
19
+
20
+ ## Regression check
21
+
22
+ `tests/realtime/audition.test.mjs` builds an isolated packed production consumer and runs after the existing numerical and packed/browser tests, rather than competing with them for CPU. The packed test files run serially because each invokes the shared package prepack build; concurrent TypeScript writes can otherwise race tarball reads. It uses a test-only native AudioWorklet recorder with preallocated storage and no per-quantum allocations. The recorder does not replace the production DSP or transport.
23
+
24
+ Each capture warms the recorder for one audio-clock second, then collects 240,000 real-time samples (five seconds at 48 kHz). Conditions are: native sine reference, den held tone with UI, LFO modulation, repeated parameter changes, and UI drawing disabled. Existing audition tests separately cover touch release/cancel, repeated Start/Stop, unavailable contexts and asynchronous startup.
25
+
26
+ For every den window and its conservative delayed-observation interval, the test requires zero new playback underrun events and zero underrun duration, five seconds of output within 5.5 wall-clock seconds, no consecutive zero samples, bounded adjacent/quantum-boundary steps, and a fitted 220 Hz sine residual below 1e-6 for unmodulated windows. Native oscillator results are recorded as an environment control; its implementation is not den's numerical contract. A warmed 1,000-block native WASM cost probe additionally requires p95 below half the quantum budget. Unsupported playback statistics fail the test instead of silently substituting offline evidence.
27
+
28
+ Raw capture precedes device fallback, so continuous samples alone cannot rule out crackling. `AudioContext.playbackStats` checks that separate playback layer. Recorder allocations initially caused small measurement artifacts, and native controls sometimes exposed host scheduling noise; the committed recorder avoids per-block array views. Neither existing sample tolerances nor expected audio have been relaxed.
29
+
30
+ `artifacts/realtime/current` is cleared at test start and holds fresh raw f32 files, actual browser version, playback statistics, wall/audio timing, continuity results and source/audio hashes. Each completed window is saved immediately; the manifest is written before assertions and on failure, including capture errors. CI uploads the scoped artifacts directory even when checks fail. A Node-side 15-second bound on each capture ensures a stalled worklet reaches teardown. The capture endpoint is taken on buffer reception before converting samples to a JavaScript array; conversion time and post-conversion statistics are recorded separately for both native and den windows. Immediate counter snapshots are not treated as final evidence. It is CANDIDATE evidence, never an approved golden. The unchanged production transport remains unworklet's postMessage fallback where cross-origin isolation is absent; no headers, protection settings, permissions or credentials change. Stop still closes the context, consistent with the risk described in upstream #81; each build runs in a fresh process, avoiding the repeated-build cache scenario in #101. Those reports are not being claimed as newly reproduced here.
31
+
32
+ Physical iOS/Android playback and human listening still require verification. This regression covers Chromium's actual 48 kHz real-time path and does not establish 44.1 kHz browser support or full-instrument CPU capacity.
33
+
34
+ ## Materialization contract limits
35
+
36
+ All existing AudioParam declarations, names, ranges and automation rates are unchanged. Each new scratch slot is f32, transient and unpublished; the envelope/LFO/oscillator persistent slots retain their names and types. Writes precede reads within the same sample and every scratch value is overwritten before use, including after restoration. A separate dynamic probe changed frequency, rate and depth, then released the envelope: old/fixed PCM and split-render same-schema continuation were bit-identical. Snapshot inspection excluded all three scratch slots.
37
+
38
+ Transient declarations still change unworklet's schema hash: `31f5b44c3627df618038170d9b2af9be` becomes `34898cbf87b8f22e8754fff7b664da5d`. Version 0.4.1 falls back to name-matched restoration when no migration exists; this specific old-to-new continuation also matched exactly, but arbitrary cross-schema compatibility remains unsupported by den's entry contract. No preset migration framework is added.
39
+
40
+ This technique is not a blanket rewrite rule for arbitrary subgraphs: moving a read across a state write can change semantics, storing f64 intermediates into f32 loses precision, and existing unworklet state stores flush magnitudes below 1e-30 to zero. Here the materialized boundaries already return f32, with the audition's bounded normal-range controls. Future instrument/filter/delay compositions must establish their own ordering, precision, restoration and real-time evidence. The modules themselves are unchanged.
41
+
42
+ ## Remaining sporadic browser underruns
43
+
44
+ The sustained old-graph overload is reproducibly removed, but some local Chromium runs still report isolated playback underruns. Raw captured samples remain continuous; that does not disprove playback-layer fallback. A fixed three-round native/den × recorder/no-recorder comparison had no underruns or container CPU throttling, and a separate traced run had GC activity without underruns. Conversely, a later capture using the pre-conversion endpoint recorded five native-reference events and one den held-tone event, with unchanged immediate counter snapshots across array conversion. Because counters update asynchronously, those snapshots cannot exclude events reported later. These observations do not establish a browser-version, GC, recorder or host-scheduling cause. Preserve failed runs alongside successful CI evidence; do not waive the zero-underrun assertion or infer universal device clearance.
45
+
46
+ ## Delayed playback statistics
47
+
48
+ The [Web Audio stats update algorithm](https://webaudio.github.io/web-audio-api/#updating-the-stats) updates counters once per second while running and visible. Reading them only at buffer delivery can miss a terminal underrun. The harness keeps the context running, polls every 100 ms, and requires `totalDuration - underrunDuration >= captureEndAudioTime`, after at least 1.25 seconds of observation. Observation is bounded at four seconds; stale or unavailable counters fail with the complete poll history saved. The outer 15-second capture deadline still bounds stalled page/worklet execution.
49
+
50
+ The raw sample/wall-time window remains five seconds. The zero-underrun assertion conservatively includes the interval from the baseline's last reported counters through the delayed final counters; this may include pre-capture reporting lag and post-capture rendering/result handling. No late event is automatically attributed to conversion or discarded. Every observation retains wall time, audio time, covered audio time, visibility and counters.
51
+
52
+ A test-only 100 ms stall on the final recorded quantum of a native sine produced zero immediate events but nine delayed events (0.09018 seconds) in the local probe. The counterexample uses the same observation logic and requires the normal zero-underrun assertion to reject the capture. Fault audio is labeled separately from candidate audio. Passing earlier immediate-only checks does not establish end-of-window playback correctness.
53
+
54
+ ## Independent LFO frequency assertion
55
+
56
+ The LFO capture also measures positive zero-crossing periods. An independent
57
+ least-squares fit of log-frequency against 4 Hz sine/cosine bases verifies the
58
+ 0.5-semitone modulation depth, carrier and residual while allowing arbitrary LFO
59
+ phase at capture start. A plain 220 Hz signal cannot pass. The offline packed
60
+ fixture applies the same oracle to nonzero depth and requires zero depth to be
61
+ rejected. Disabling depth in a separate copy of the audition composition makes
62
+ that packed render fail specifically at the pitch-depth assertion. This adds a
63
+ numerical check without changing DSP, continuity thresholds or expected audio.