@denaudio/den 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/LICENSE-APACHE +202 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +107 -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 +17 -0
  14. package/dist/convolution.js +87 -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 +26 -0
  86. package/dist/prepared-convolution.js +104 -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 +46 -0
  90. package/dist/resident-time-stretch.js +136 -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 +50 -0
  100. package/dist/spatial-chains.js +76 -0
  101. package/dist/spectral-fft.d.ts +27 -0
  102. package/dist/spectral-fft.js +84 -0
  103. package/dist/spectral-freeze.d.ts +24 -0
  104. package/dist/spectral-freeze.js +74 -0
  105. package/dist/spectral-gate.d.ts +23 -0
  106. package/dist/spectral-gate.js +58 -0
  107. package/dist/spectral-stft.d.ts +14 -0
  108. package/dist/spectral-stft.js +70 -0
  109. package/dist/spectral-texture.d.ts +46 -0
  110. package/dist/spectral-texture.js +141 -0
  111. package/dist/spectral.d.ts +18 -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 +433 -0
  133. package/docs/character-filter.md +139 -0
  134. package/docs/contracts.md +141 -0
  135. package/docs/control-modulation.md +194 -0
  136. package/docs/convolution.md +50 -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 +58 -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 +197 -0
  174. package/docs/realtime-audition.md +63 -0
  175. package/docs/resident-recorder.md +151 -0
  176. package/docs/resident-time-stretch.md +196 -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 +251 -0
  181. package/docs/spectral-freeze-entry.md +132 -0
  182. package/docs/spectral-gate-entry.md +134 -0
  183. package/docs/spectral-large-candidate.md +81 -0
  184. package/docs/spectral-texture-entry.md +178 -0
  185. package/docs/spectral.md +127 -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,202 @@
1
+ # Stereo delay FX — GEN-618 / GEN-620
2
+
3
+ One `delayFx` unworklet subgraph composes the existing readhead, low-pass and LFO.
4
+ It has no loader, parameter registry, state codec, routing framework, extra effect
5
+ engine or FFT. Call `tick(left, right, controls)` once per sample. Audio and numeric
6
+ controls are finite f32 nodes; sync/bypass/reset are boolean nodes. Duplicate mono
7
+ input explicitly. State is independent per engine/channel; there is no crossfeed.
8
+
9
+ Configuration is immutable: `sampleRate` (integer 8000–192000, from processor ctx),
10
+ `maxDelaySeconds` (default 8), `tone` (`lowpass`, default, or `flat`) and
11
+ `stereoPhaseCycles` (default 0.5, wrapped modulo one). Eight seconds is a provisional
12
+ construction default covering a whole note at 30 BPM, not a product ceiling.
13
+ Allocation is two readhead buffers of `ceil(rate * capacity) + 2` f32 samples, plus
14
+ scalar filter/LFO/control state. At 96 kHz the default buffer storage is 6,144,016
15
+ bytes; callers own the memory budget. No audio-thread resize occurs.
16
+
17
+ ## Controls and sound
18
+
19
+ | Control | Unit / policy |
20
+ | --- | --- |
21
+ | timeLeftSeconds / timeRightSeconds | Independent base delays when sync is false; minimum one sample, maximum construction capacity. |
22
+ | sync, bpm, beatsLeft / beatsRight | Sync uses `60 * beats / bpm`; one beat is a quarter note. BPM domain 30–300; positive fractional beats express subdivisions, triplets and dotted values. No independent beat-count product ceiling; actual time must fit capacity. |
23
+ | feedback | Linear multiplier, clamped to [0,0.95]. Positive repeats only. |
24
+ | cutoffHz | Feedback tone cutoff clamped to [20,min(20000,0.24*rate)]; Q is fixed at 0.5. Flat mode bypasses tone in the feedback path. |
25
+ | mix | Linear dry/wet crossfade, clamped to [0,1]. No equal-power boost, limiter or normalization. |
26
+ | rateHz / depthSeconds | Shared LFO module, rate clamped to [0,20], depth [0,0.05]. Left reset phase 0, right reset phase stereoPhaseCycles. Both initialized on the first tick. |
27
+ | bypass | Unity dry, stop feeding new input, keep the internal feedback/tone/LFO running. Unbypass may reveal remaining tail. |
28
+ | reset | Clear logical delay history/pointers, filter history and LFO phases at this sample; new input at this sample begins new history. Dry contribution remains audible. Held reset silences wet. |
29
+
30
+ A read happens before the current input write. Per channel,
31
+ `wet[n] = delayRead(history, time[n])` and
32
+ `write[n] = admittedInput[n] + feedback[n] * tone(wet[n])`.
33
+ There is no additional sample in the feedback loop. The first repeat is unfiltered;
34
+ the tone shapes each subsequent circulation. Input stop (zeros) preserves decay.
35
+ Bypass and rejected timing admit zero new input. Reset is independent of bypass.
36
+
37
+ Time, tempo and stereo-tap edits move the existing readheads immediately. There
38
+ is no crossfade or smoothing: abrupt changes can click, skip/repeat source samples,
39
+ and cause Doppler pitch motion. LFO modulation uses the same moving-head behavior;
40
+ right phase 0.5 gives opposite motion, while phase 0 and equal taps preserve dual
41
+ mono. Dry/wet or bypass steps can also click. These discontinuities are explicit
42
+ policies, not a claim of sound approval. No arbitrary audio-rate modulation
43
+ anti-alias guarantee is made. GEN-621's two musical settings remain separate.
44
+
45
+ ## Capacity rejection and clipping
46
+
47
+ Both requested base times must fit the f32-representable bounds. In sync mode BPM
48
+ must also be in range. A bad stereo request (including a too-short/nonpositive
49
+ base time) sets `timingRejected=true`, emits unity dry on both channels and admits
50
+ zero new input. Internally, both previous accepted base times are retained so the
51
+ old tail continues. On reset, rejected times fall back to one sample with cleared
52
+ history. A later valid request is accepted immediately with the same moving-head
53
+ transition policy. There is no silently clamped rhythmic playback.
54
+
55
+ LFO depth is applied after an accepted base time. Final modulation saturates at
56
+ the buffer bounds and sets `modulationClipped=true` when clipping occurs. This
57
+ flag is separate from invalid base timing. Consumers should display rejection
58
+ and clipping using existing unworklet events/state if desired; no new transport
59
+ is supplied here. Runtime sample-rate-specific compilation is unchanged: browser
60
+ acceptance is 48 kHz only; 44.1/48/96 kHz offline coverage is separate.
61
+
62
+ ## Feedback bound, including cutoff modulation
63
+
64
+ A fixed-frequency low-pass magnitude bound alone does not prove a time-varying
65
+ loop stable. For the shared SVF with Q=0.5, let `g=tan(pi*cutoff/rate)`, and express
66
+ its stored state in coordinates `p=band+low`, `q=low`. The same equations are:
67
+
68
+ ```
69
+ u = (p + g*x)/(1+g)
70
+ y = (q + g*u)/(1+g)
71
+ pNext = r*p + (1-r)*x
72
+ qNext = r*q + (1-r)*u
73
+ r = (1-g)/(1+g)
74
+ ```
75
+
76
+ For `0<g<1`, every expression is a convex combination. The chosen 0.24*rate cap
77
+ keeps g below one with margin for the shared coefficient approximation. This
78
+ property holds under sample-by-sample cutoff changes, unlike a stationary-only
79
+ frequency response argument. Linear readhead interpolation is also convex, and
80
+ independent stereo channels do not add energy to each other. If input magnitude
81
+ is at most A and history starts clear, the common amplitude invariant is
82
+ `A/(1-0.95) = 20*A` for delay storage, tone coordinates and wet output, apart from
83
+ floating-point rounding. Thus arbitrary admitted time/cutoff modulation cannot
84
+ create unbounded self-oscillation under these control bounds. Feedback <1, finite
85
+ capacity and strictly positive bounded cutoff also give eventual decay after
86
+ input stops; the tests include the slowest-cutoff/highest-gain tail. There is no
87
+ threshold tail kill added by den; pinned unworklet flushes tiny values (<1e-30).
88
+ Flat tone has the same loop bound without the filter stage. At DC, unity input
89
+ can accumulate to 20; users must gain-stage the output. No hidden compensation
90
+ conceals this behavior.
91
+
92
+ ## State and composition cost
93
+
94
+ Readhead history remains a logical reset, not a physical memory wipe. Persistent
95
+ slots are inherited readhead/filter/LFO state plus `first`, `acceptedLeft` and
96
+ `acceptedRight`. Restore is supported only for the same schema, configuration and
97
+ rate, using unworklet's existing snapshot path after parameters have rendered.
98
+
99
+ Seven transient f32 scalar slots and one transient boolean materialize LFO
100
+ outputs, final times, wet reads, cutoff and timing validity within each sample.
101
+ The accepted-time state is read after its current-sample write, avoiding repeated
102
+ expansion of the same selection. They are overwritten before use and add no sample latency.
103
+ They prevent recursive expansion of the composite expression graph in pinned
104
+ unworklet 0.4.1; no compiler/runtime replacement or upstream edit is introduced.
105
+
106
+ ## Evidence
107
+
108
+ `tests/delay-fx.spec.ts` compares against an unbounded written-input timeline,
109
+ independent direct-form bilinear biquad and (for time-varying cutoff) two one-pole
110
+ cascade coordinates. It does not copy the circular storage or SVF recurrence.
111
+ It checks repeat positions/amplitudes, fractional taps, gain bounds, filtering,
112
+ bypass/reset/restart, stereo/mono/mix endpoints, tempo acceptance/rejection,
113
+ modulation clipping, discontinuity policy and same-schema continuation at all
114
+ three rates. Ordinary references allow 3e-6 absolute error, chorus/variable tone
115
+ 4e-6. Wrong waveforms fail. Extreme LFO-depth/cutoff modulation additionally checks
116
+ the derived common amplitude invariant, separately from sample comparisons to
117
+ avoid treating tiny sine-polynomial errors amplified by seconds-to-samples
118
+ conversion as filter recurrence errors. Each long-tail test runs 2^20 samples
119
+ (10.9–23.8 seconds depending on rate), with bounded peak and final tail <1e-6.
120
+
121
+ `tests/delay-fx-packed.test.mjs` installs the actual tarball into an isolated locked
122
+ consumer, strictly checks declarations, compares rendered feedback/tone against
123
+ a separate direct-form reference, builds the standard unworklet browser path and
124
+ exercises mix/bypass/reset/capacity rejection and snapshot restore at 48 kHz.
125
+
126
+ The sustained browser gate records 2^19 frames (10.92 seconds) downstream of the
127
+ worklet: actual input plus left/right output, with four seconds of sustained
128
+ 220-Hz input and more than six seconds of tail. `capture.ts` is a fixed test fixture
129
+ built with existing unworklet buffers, audio ports and snapshots. It writes three
130
+ channels on the audio thread, emits silence, and reads its snapshot after suspend.
131
+ A guard quantum protects the requested prefix after recording fills. The frame
132
+ counter must cover the entire requested prefix. No custom loader, transport or
133
+ capture framework is added; the product processor is unchanged.
134
+
135
+ Every captured output sample is compared to an independent reference driven by
136
+ captured input at the unchanged 6e-6 threshold. Input continuity retains its 1e-6
137
+ sine-recurrence threshold and exact four-second span. A ten-second native source
138
+ control always runs, including its full sustained span; all three captured control
139
+ channels must be identical. There are no passing-retry selection or skipped
140
+ startup samples. Raw PCM and source/hash manifests are retained. This verifies
141
+ graph-sample continuity, not hardware-output delivery or scheduler deadlines.
142
+
143
+ The original ScriptProcessor apparatus produced false discontinuities. In one
144
+ simultaneous recording, internal input was continuous and internal outputs matched
145
+ the independent reference (left 7.45e-9, right 0), while external capture first
146
+ diverged at sample 73728 and lost 640 source samples. A separate internal-only run
147
+ had the same reference accuracy. Another saved tail failure started exactly at
148
+ capture block 393216: shifting the reference by 128 samples restored right-channel
149
+ bit equality and left error below 5.83e-11. The source was already silent, so a
150
+ short native-source control could not diagnose that tail capture loss. This is
151
+ why the baseline now sustains for ten seconds. See `delay-fx-capture-findings.json`
152
+ for numerical results and hashes; raw evidence and diagnostic fixtures are kept
153
+ in `artifacts/delay-fx/capture-investigation/`. The original failures are preserved,
154
+ not converted into passing comparisons by alignment or tolerance changes.
155
+
156
+ Deadline safety remains a separate unresolved question. Real AudioWorklet traces
157
+ have included startup and non-startup CPU/wall-time budget exceedances even after
158
+ expression materialization; replacing the recorder does not clear those findings.
159
+ No listening approval is inferred from corrected waveform capture.
160
+
161
+ `performance.mjs` separately measures the existing driver with byte-identical
162
+ packed browser WASM. It records size/hash, controls, warm-up, mean/p99/max block
163
+ time and total wall time for 12 seconds of audio. This host-dependent direct-WASM
164
+ diagnostic excludes browser scheduling/copy overhead and does not replace the
165
+ real-time gate. After the composition audit, WASM decreased from 62,653 to 45,935 bytes. Local
166
+ direct-driver mean decreased from 0.366 to 0.071 ms per 128 frames. The latter run
167
+ still had a 4.677 ms maximum versus a 2.667 ms budget; average cost alone does not
168
+ establish real-time readiness. Against commit 6cf15d1, all four output/status
169
+ channels were bit-identical over 18 cases: 44.1/48/96 kHz, flat/lowpass, and LFO
170
+ rate/depth pairs 0/0, 4 Hz/2 ms, 12 Hz/20 ms. Each case rendered 16384 samples with
171
+ sample-varying time, cutoff, gain, mix, sync, bypass and reset. This equivalence
172
+ check supplements the independent numerical references; it is not a golden. That
173
+ composition audit changed no shared module or unworklet source; no general
174
+ real-time performance guarantee is made.
175
+
176
+ The subsequent main integration includes the filter materialization described in
177
+ `filter-materialization.md`. It preserves the persistent schema and tiny-value
178
+ behavior while reducing this consumer's WASM to 24,707 bytes (SHA-256
179
+ `74ab305c15c932d3d7bc32e7d4a0499439adfdfb107355f329e46d1ffea9fdb0`).
180
+ The prior 18-case comparison against the 7ef7ce6 delay engine also found identical
181
+ complete snapshot bytes, and bidirectional restored continuation matched continuous
182
+ rendering with an ignored-restore negative control. The delay engine, readhead and
183
+ LFO sources are unchanged by this main integration.
184
+
185
+ The exact same generated WASM is covered by the recorded four cold candidate
186
+ trials in `filter-materialization-results.json` (first callback wall time
187
+ 1.659–1.900 ms). Reusing matching source/WASM hashes avoids another timing sample;
188
+ these local measurements are not universal deadline clearance or evidence of a
189
+ hot-loop speedup. Non-first callback outliers, native-control observations and
190
+ clock anomalies remain separate limitations. The full numerical/packed suite also
191
+ checks the shared audition consumer introduced on main.
192
+
193
+ `artifacts/delay-fx/` contains CANDIDATE WAVs/raw browser PCM, static plots and a
194
+ manifest linking source hashes, exact settings/input, rates, dependency locks,
195
+ package tarball and verification results. No golden is approved or updated.
196
+
197
+ ## Public integration
198
+
199
+ `@denaudio/den/delay-fx` exports `delayFx`, `DelayFxConfig` and `DelayFxControls`.
200
+ The isolated consumer imports that public subpath. The engine fixture remains
201
+ separate from the Chorus/Rhythmic setting fixtures and the complete integrated
202
+ site. Shared exports/package/lock/contracts and the UI retain one integration owner.
@@ -0,0 +1,121 @@
1
+ # Delay readhead — GEN-615
2
+
3
+ `src/delay-readhead.ts` supplies `delayReadhead` and `DelayReadheadConfig`.
4
+ It is a mono unworklet subgraph, with no feedback, filter, stereo, mix, or tempo
5
+ policy. Instantiate with a required config object and a unique stable name:
6
+
7
+ ```ts
8
+ const cell = instantiate(delayReadhead, {
9
+ sampleRate: ctx.sampleRate,
10
+ maxDelaySeconds: 8,
11
+ }, { name: 'delayLeft' });
12
+ // Once per sample, inside forSample:
13
+ const { output, outOfRange } = cell.tick(input, delaySeconds, reset);
14
+ ```
15
+
16
+ For same-sample feedback or other processing of the delayed output, use the
17
+ read-before-write boundary instead of `tick`:
18
+
19
+ ```ts
20
+ const tap = cell.read(delaySeconds, reset);
21
+ tap.write(input.add(tap.output.mul(0.5)));
22
+ output.write(tap.output);
23
+ ```
24
+
25
+ Each sample must use exactly one `read` followed by exactly one call to **that
26
+ read's** `write`, or exactly one `tick`; do not combine the two forms on one instance.
27
+ `read` captures the current delayed output and reset-adjusted pointers. Its `write`
28
+ commits the current input and advances history once. This allows
29
+ `writeInput[n] = input[n] + gain * delayedOutput[n]`, with no previous-output bridge
30
+ and no extra circulation latency. The returned object and closure exist only while
31
+ constructing the unworklet graph, not during audio processing. `tick` delegates to
32
+ the same read/write pair. Reset masks old history before the read, then the write
33
+ starts new history at the reset sample. The minimal feedback test uses a closed-form
34
+ one-sample-delay impulse `[0, 1, 0.5, 0.25, ...]`, including reset and isolation at
35
+ all three offline rates; it does not implement the dependent feedback engine.
36
+
37
+ `input` / `delaySeconds` are `Node<'f32'>`; `reset` and `outOfRange` are
38
+ `Node<'bool'>`. Input audio must be finite. The sample rate must be the enclosing
39
+ processor's compile-time rate. Use existing AudioParams/events and snapshot APIs;
40
+ this module introduces no control, routing, buffer, or persistence framework.
41
+
42
+ ## Capacity and timeline
43
+
44
+ Construction allocates `ceil(sampleRate * maxDelaySeconds) + 2` f32 samples.
45
+ The default is two seconds, configurable at construction, with no resize during
46
+ processing. Eight seconds is covered by tests at all three offline rates. Capacity
47
+ must include at least one sample and fit signed i32 indexing; allocation is also
48
+ subject to the host's memory budget. These are representation constraints, not a
49
+ musical/product duration limit. Each instance has independent history and pointers.
50
+
51
+ At frame `n`, delay `d` in samples produces the linear interpolation of the input
52
+ at timeline position `n-d`. Reads precede the write of input `n`; one sample is the
53
+ minimum delay, and zero is not a dry feedthrough. For `d=k+f`, the weights are
54
+ `(1-f) * x[n-k] + f * x[n-k-1]`. Both adjacent integer indices wrap independently.
55
+ No fractional ring-coordinate subtraction is performed in f32: seconds are widened
56
+ to f64 before rate conversion, and the fractional weight is finally cast to f32.
57
+ This avoids losing small fractional offsets near the end of a long ring.
58
+
59
+ Finite times saturate to `[1/sampleRate, maxDelaySeconds]`. `outOfRange` reports
60
+ requests outside the **f32-representable seconds endpoints**, allowing an AudioParam
61
+ set to either endpoint without a spurious warning caused by f32 conversion. NaN
62
+ and negative infinity use the minimum; positive infinity uses the maximum, all
63
+ with `outOfRange=true`. Saturation ensures safe reads but does **not** establish
64
+ correct rhythmic playback. A later tempo consumer must use the flag to reject or
65
+ report unavailable times; it must not silently claim that a clamped rhythm is exact.
66
+
67
+ ## Acoustic choice
68
+
69
+ | Candidate | Time change behavior | Decision |
70
+ | --- | --- | --- |
71
+ | One continuously moving head, linear interpolation | Read velocity is `1 - delta(delaySamples)`; changing time bends pitch. Abrupt jumps can click or repeat/skip source material. | Selected: one explicit trajectory for modulation and steps, no hidden smoothing or extra transition state. |
72
+ | Two stationary heads crossfaded after a change | Avoids the sustained pitch glide but overlaps two different source times; correlated content can comb or cancel. Requires a fade duration and a policy for changes during a fade. | Deferred; does not express the selected moving-head modulation behavior. |
73
+ | Higher-order interpolation on a moving head | Retains Doppler behavior with a different high-frequency response; more taps and possible overshoot. | Not needed for this initial bounded two-tap readhead; a future quality change requires new evidence. |
74
+
75
+ Linear interpolation is not spectrally transparent: for fractional weight `f`,
76
+ `H(w)=(1-f)+f*exp(-jw)` apart from integer delay. At half-sample offset its magnitude
77
+ is `abs(cos(w/2))`, reaching zero at Nyquist. Nonnegative weights sum to one, so it
78
+ cannot overshoot the extrema of its two finite inputs. Arbitrary fast modulation
79
+ can alias; no anti-aliasing or click-free transition guarantee is made. Abrupt
80
+ controls take effect at their sample with no smoothing. A delay slope of +1 holds
81
+ the source position, and -1 doubles its traversal rate; analytic tests verify both.
82
+ Human sound evaluation remains a later batched instrument/FX step.
83
+
84
+ ## Reset and state
85
+
86
+ Reset applies before the read at the dispatched sample. It resets the logical
87
+ write pointer and valid-history count, producing zero immediately. The current
88
+ input is then written as the first sample of the new history. Held reset keeps
89
+ output silent. There is no pre-reset contribution to either fractional tap.
90
+
91
+ This is a constant-time **logical history clear**, not a physical memory wipe.
92
+ Pinned unworklet 0.4.1 has no buffer-clear DSL operation. Old storage bytes can
93
+ remain in a snapshot until overwritten but are unreadable by this readhead; this
94
+ is not a data-erasure API. Reset may click. `history` (persistent f32 buffer),
95
+ `cursor` (i32), and `valid` (i32) use unworklet state with stable instance prefixes.
96
+ Snapshot continuation is supported only with the same schema, capacity, and rate.
97
+ The tests restore history, compare continuation, and prove reset cannot resurrect it.
98
+
99
+ ## Evidence and integration
100
+
101
+ `tests/delay-readhead.spec.ts` uses an unbounded input-timeline reference, independent
102
+ of ring storage, and analytic impulse/pitch expectations. Tests cover minimum and
103
+ maximum, fractional wrap, steps, modulation, invalid controls, state isolation,
104
+ reset across block/wrap boundaries, and same-schema continuation. Sample comparisons
105
+ use absolute tolerance 2e-6 (4e-6 for analytic pitch tests); scrubbing must remain zero.
106
+ The fixture rejects an intentionally wrong waveform. No audio golden is used.
107
+
108
+ `tests/delay-packed.test.mjs` packs the package, installs it into an isolated locked
109
+ consumer, checks declarations strictly, renders offline at 44100/48000/96000, and
110
+ builds and renders Chromium at **48000 only**. It imports the public
111
+ `@denaudio/den/delay-readhead` subpath. It neither
112
+ imports repository source nor edits an installed dependency. The existing entry
113
+ test retains the other-rate browser rejection probes.
114
+
115
+ Artifacts in `artifacts/delay/` are CANDIDATE: raw WAVs, static waveforms, the packed
116
+ tarball/consumer lock, browser samples, and a manifest linking source/settings/input
117
+ and file hashes. CI uploads these through the existing artifact step.
118
+
119
+ The integration owner exposes `./delay-readhead` with its declarations and ESM
120
+ implementation. The packed fixture exercises that public import; package/lock,
121
+ shared consumers and shared contracts stay under one integration owner.
@@ -0,0 +1,333 @@
1
+ {
2
+ "status": "CANDIDATE",
3
+ "approval": null,
4
+ "previousApprovedAudio": null,
5
+ "description": "Initial reference candidates, not approved golden. No normalization or limiter.",
6
+ "sourceCommit": "ace4deafaf41f8c4336df7fdb532bbac10a6be42",
7
+ "sourceDirty": false,
8
+ "sourceFiles": {
9
+ "src/delay-settings.ts": "fb7a8ec25889514d9fd75a883c55afe58d2ffad412eb2c35918dd9ad8aa1d626",
10
+ "src/delay-fx.ts": "1df28860ffee67b594088ece185a81827fec90abb2acd81199ea90df047c420a",
11
+ "src/delay-readhead.ts": "b859b816bdccb9e638a79f07c8084761fb4f951a943ca2d48fb962162477ce38",
12
+ "src/filter.ts": "61d49d6b1e52b17791237e00dec5c5089e594ee0030ecaa37f2c632ecb441278",
13
+ "src/lfo.ts": "a2d73e0c389b91b62e3e833635dc7406d354dcce2d76e83d12e96abce7ca3daf",
14
+ "tests/fixtures/delay-settings.mjs": "ebf999b689f5f7608547a9d41b2cd72924712946216833992573786393a66318",
15
+ "scripts/render-delay-settings.mjs": "5da957ade4a82f7863d27d237250cbfc6c4b5cfb4a6260b9fa1ae00c81001815",
16
+ "scripts/plot-delay-settings.py": "e027a9958cadf9ea1321de1d64ccf689e3db2bba0e44728673f5e250ea21fe19",
17
+ "tests/delay-settings.spec.ts": "0e5eb94a49c484a871810d65f334f4da65e7db852213f132fac1495eadc18eb9",
18
+ "tests/delay-settings-packed.test.mjs": "947fdbfa4ef5d1e8cf82365bf4e4ece7640e75868dbd9528809ca83af96bd52a",
19
+ "package-lock.json": "9aebb16d62097a630ef0022e375f0570dfcc7c082746e74e4b9427637c6fa53a"
20
+ },
21
+ "dependencies": {
22
+ "den": "0.0.0",
23
+ "unworklet": "0.4.1",
24
+ "lockSHA256": "9aebb16d62097a630ef0022e375f0570dfcc7c082746e74e4b9427637c6fa53a"
25
+ },
26
+ "sampleRate": 48000,
27
+ "channels": 2,
28
+ "frames": 262144,
29
+ "durationSeconds": 5.461333333333333,
30
+ "initialState": "Fresh engine: zero history/filter states and initial LFO phase; no restored snapshot",
31
+ "seed": null,
32
+ "midi": null,
33
+ "audioEncoding": "IEEE float32 WAV",
34
+ "browserScope": "48kHz engine packed gate is separate; these candidate files are offline renders",
35
+ "cases": [
36
+ {
37
+ "id": "chorus-base",
38
+ "setting": {
39
+ "config": {
40
+ "maxDelaySeconds": 2,
41
+ "tone": "flat",
42
+ "stereoPhaseCycles": 0.5
43
+ },
44
+ "parameters": {
45
+ "timeLeftSeconds": 0.018,
46
+ "timeRightSeconds": 0.018,
47
+ "sync": false,
48
+ "bpm": 120,
49
+ "beatsLeft": 1,
50
+ "beatsRight": 1,
51
+ "feedback": 0,
52
+ "cutoffHz": 3200,
53
+ "mix": 0.45,
54
+ "rateHz": 0.65,
55
+ "depthSeconds": 0.003,
56
+ "bypass": false,
57
+ "reset": false
58
+ }
59
+ },
60
+ "events": [],
61
+ "source": "Identical mono L/R; 220/330/440Hz sine sum gains 0.12/0.06/0.03; 20ms linear attack, 50ms release; source stops at 2s.",
62
+ "input": {
63
+ "file": "chorus-base-input.wav",
64
+ "sha256": "7323fd06940d219a2c0e96af964394d19fa99d879d3b5a97390943e87d134473"
65
+ },
66
+ "raw": {
67
+ "file": "chorus-base-raw.wav",
68
+ "sha256": "3eeeffd71e1a7d5e23729f1b29f630ab9befd15994a58b36e3c529f1d7dc1461"
69
+ },
70
+ "peaks": [
71
+ 0.19121864438056946,
72
+ 0.19117864966392517
73
+ ],
74
+ "verification": {
75
+ "independentTimelineBiquadMaxError": [
76
+ 2.0489096641540527e-07,
77
+ 1.9371509552001953e-07
78
+ ],
79
+ "tolerance": 5e-06,
80
+ "scrubbedSamples": 0
81
+ },
82
+ "listeningFocus": "Slow widening and pitch motion; mono fold coloration; rate/depth edit can click.",
83
+ "waveform": {
84
+ "file": "chorus-base-waveform.png",
85
+ "sha256": "44840f45455ee91e93a918495e23f6a99070dd85de6960a72e618cf92c3eb450",
86
+ "bucketSamples": 128,
87
+ "amplitudeRange": [
88
+ -0.5,
89
+ 0.5
90
+ ]
91
+ }
92
+ },
93
+ {
94
+ "id": "chorus-edits",
95
+ "setting": {
96
+ "config": {
97
+ "maxDelaySeconds": 2,
98
+ "tone": "flat",
99
+ "stereoPhaseCycles": 0.5
100
+ },
101
+ "parameters": {
102
+ "timeLeftSeconds": 0.018,
103
+ "timeRightSeconds": 0.018,
104
+ "sync": false,
105
+ "bpm": 120,
106
+ "beatsLeft": 1,
107
+ "beatsRight": 1,
108
+ "feedback": 0,
109
+ "cutoffHz": 3200,
110
+ "mix": 0.45,
111
+ "rateHz": 0.65,
112
+ "depthSeconds": 0.003,
113
+ "bypass": false,
114
+ "reset": false
115
+ }
116
+ },
117
+ "events": [
118
+ {
119
+ "sample": 72000,
120
+ "values": {
121
+ "rateHz": 1.3,
122
+ "depthSeconds": 0.006
123
+ }
124
+ },
125
+ {
126
+ "sample": 144000,
127
+ "values": {
128
+ "bypass": true
129
+ }
130
+ },
131
+ {
132
+ "sample": 168000,
133
+ "values": {
134
+ "bypass": false
135
+ }
136
+ },
137
+ {
138
+ "sample": 216000,
139
+ "values": {
140
+ "reset": true
141
+ }
142
+ },
143
+ {
144
+ "sample": 216001,
145
+ "values": {
146
+ "reset": false
147
+ }
148
+ }
149
+ ],
150
+ "source": "Identical mono L/R; 220/330/440Hz sine sum gains 0.12/0.06/0.03; 20ms linear attack, 50ms release; source stops at 2s.",
151
+ "input": {
152
+ "file": "chorus-edits-input.wav",
153
+ "sha256": "7323fd06940d219a2c0e96af964394d19fa99d879d3b5a97390943e87d134473"
154
+ },
155
+ "raw": {
156
+ "file": "chorus-edits-raw.wav",
157
+ "sha256": "eaafa1a74a47b189a3bf6c26cf287c9c6c12e4813801f3fc6bc29c82e566d8a3"
158
+ },
159
+ "peaks": [
160
+ 0.19121313095092773,
161
+ 0.19117864966392517
162
+ ],
163
+ "verification": {
164
+ "independentTimelineBiquadMaxError": [
165
+ 3.0919909477233887e-07,
166
+ 1.9371509552001953e-07
167
+ ],
168
+ "tolerance": 5e-06,
169
+ "scrubbedSamples": 0
170
+ },
171
+ "listeningFocus": "Slow widening and pitch motion; mono fold coloration; rate/depth edit can click.",
172
+ "waveform": {
173
+ "file": "chorus-edits-waveform.png",
174
+ "sha256": "9c2ab4f7b0ffc1d195b24c78ff02d992bac62e65935301317bd15d5224dcea24",
175
+ "bucketSamples": 128,
176
+ "amplitudeRange": [
177
+ -0.5,
178
+ 0.5
179
+ ]
180
+ }
181
+ },
182
+ {
183
+ "id": "rhythmic-delay-base",
184
+ "setting": {
185
+ "config": {
186
+ "maxDelaySeconds": 2,
187
+ "tone": "lowpass",
188
+ "stereoPhaseCycles": 0.5
189
+ },
190
+ "parameters": {
191
+ "timeLeftSeconds": 0.375,
192
+ "timeRightSeconds": 0.5,
193
+ "sync": true,
194
+ "bpm": 120,
195
+ "beatsLeft": 0.75,
196
+ "beatsRight": 1,
197
+ "feedback": 0.48,
198
+ "cutoffHz": 3200,
199
+ "mix": 0.35,
200
+ "rateHz": 0,
201
+ "depthSeconds": 0,
202
+ "bypass": false,
203
+ "reset": false
204
+ }
205
+ },
206
+ "events": [],
207
+ "source": "Identical mono L/R; four 0.5s plucks at t=0/0.5/1/1.5; 0.3*exp(-local/0.065)*(sin(2pi*110*local)+0.25*sin(2pi*330*local)); stops at 2s.",
208
+ "input": {
209
+ "file": "rhythmic-delay-base-input.wav",
210
+ "sha256": "b58394aba9656fd2949937c71881d6e9a45add8c41cb8247a726389aa3e2982c"
211
+ },
212
+ "raw": {
213
+ "file": "rhythmic-delay-base-raw.wav",
214
+ "sha256": "3421ba3682892b307e7f426cdbf512ae9f47fd578ff954f6117465a2dc0c9fc9"
215
+ },
216
+ "peaks": [
217
+ 0.18629704415798187,
218
+ 0.32658955454826355
219
+ ],
220
+ "verification": {
221
+ "independentTimelineBiquadMaxError": [
222
+ 1.4901161193847656e-08,
223
+ 2.9802322387695312e-08
224
+ ],
225
+ "tolerance": 5e-06,
226
+ "scrubbedSamples": 0
227
+ },
228
+ "listeningFocus": "Dotted-eighth/quarter spacing and darkening repeats; abrupt tempo edit can jump/click; bypass hides but preserves tail.",
229
+ "waveform": {
230
+ "file": "rhythmic-delay-base-waveform.png",
231
+ "sha256": "5e420ffe22efd3dfc57955286ae9d66bf838672f3b7a2f6a33d123c30fe2c467",
232
+ "bucketSamples": 128,
233
+ "amplitudeRange": [
234
+ -0.5,
235
+ 0.5
236
+ ]
237
+ }
238
+ },
239
+ {
240
+ "id": "rhythmic-delay-edits",
241
+ "setting": {
242
+ "config": {
243
+ "maxDelaySeconds": 2,
244
+ "tone": "lowpass",
245
+ "stereoPhaseCycles": 0.5
246
+ },
247
+ "parameters": {
248
+ "timeLeftSeconds": 0.375,
249
+ "timeRightSeconds": 0.5,
250
+ "sync": true,
251
+ "bpm": 120,
252
+ "beatsLeft": 0.75,
253
+ "beatsRight": 1,
254
+ "feedback": 0.48,
255
+ "cutoffHz": 3200,
256
+ "mix": 0.35,
257
+ "rateHz": 0,
258
+ "depthSeconds": 0,
259
+ "bypass": false,
260
+ "reset": false
261
+ }
262
+ },
263
+ "events": [
264
+ {
265
+ "sample": 72000,
266
+ "values": {
267
+ "bpm": 180,
268
+ "feedback": 0.65
269
+ }
270
+ },
271
+ {
272
+ "sample": 144000,
273
+ "values": {
274
+ "bypass": true
275
+ }
276
+ },
277
+ {
278
+ "sample": 168000,
279
+ "values": {
280
+ "bypass": false
281
+ }
282
+ },
283
+ {
284
+ "sample": 216000,
285
+ "values": {
286
+ "reset": true
287
+ }
288
+ },
289
+ {
290
+ "sample": 216001,
291
+ "values": {
292
+ "reset": false
293
+ }
294
+ }
295
+ ],
296
+ "source": "Identical mono L/R; four 0.5s plucks at t=0/0.5/1/1.5; 0.3*exp(-local/0.065)*(sin(2pi*110*local)+0.25*sin(2pi*330*local)); stops at 2s.",
297
+ "input": {
298
+ "file": "rhythmic-delay-edits-input.wav",
299
+ "sha256": "b58394aba9656fd2949937c71881d6e9a45add8c41cb8247a726389aa3e2982c"
300
+ },
301
+ "raw": {
302
+ "file": "rhythmic-delay-edits-raw.wav",
303
+ "sha256": "a8e395239a030f920735f61c770b63e6cd40eaba2541bd604235763c74e7ae78"
304
+ },
305
+ "peaks": [
306
+ 0.1877381056547165,
307
+ 0.30593806505203247
308
+ ],
309
+ "verification": {
310
+ "independentTimelineBiquadMaxError": [
311
+ 2.9802322387695312e-08,
312
+ 2.9802322387695312e-08
313
+ ],
314
+ "tolerance": 5e-06,
315
+ "scrubbedSamples": 0
316
+ },
317
+ "listeningFocus": "Dotted-eighth/quarter spacing and darkening repeats; abrupt tempo edit can jump/click; bypass hides but preserves tail.",
318
+ "waveform": {
319
+ "file": "rhythmic-delay-edits-waveform.png",
320
+ "sha256": "72bbeef4e0453e180968e6a520a197fd0d26454768e50d739f5ff7c4da311a33",
321
+ "bucketSamples": 128,
322
+ "amplitudeRange": [
323
+ -0.5,
324
+ 0.5
325
+ ]
326
+ }
327
+ }
328
+ ],
329
+ "plotter": {
330
+ "matplotlib": "3.10.8",
331
+ "numpy": "2.3.5"
332
+ }
333
+ }