@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,34 @@
1
+ import { type Node } from '@unworklet/core';
2
+ export interface WindowedPitchShiftConfig {
3
+ /** Match the enclosing processor's ctx.sampleRate; integer 8000..192000. */
4
+ sampleRate: number;
5
+ /** Fixed delay sweep width, even integer 32..16384; default 2048.
6
+ * A head's cycle lasts windowSamples / abs(1-ratio) output samples.
7
+ * This is not a fixed-duration grain or an STFT frame. */
8
+ windowSamples?: number;
9
+ }
10
+ export interface WindowedPitchShiftControls {
11
+ /** Input-time speed [0.5,2]. Invalid values retain the last valid ratio,
12
+ * or 1 on reset. Finite endpoints are accepted without epsilon. */
13
+ ratio: Node<'f32'>;
14
+ /** Level retrigger: use phase zero now, preserving history. Can click.
15
+ * Held retrigger pins the current read to the initial midpoint delay. */
16
+ retrigger: Node<'bool'>;
17
+ /** Priority over retrigger. Silence/discard this sample, invalidate history,
18
+ * and hold phase zero. A valid ratio is still accepted during reset. */
19
+ reset: Node<'bool'>;
20
+ }
21
+ /** Bounded mono two-window moving-readhead pitch effect. Call once per sample.
22
+ * Finite f32 input required. Complementary triangular weights and linear reads
23
+ * are convex: no implicit gain, feedback, dry/wet mix or bypass. Initially,
24
+ * ratio 1 is a (1+windowSamples/2)-sample delay; unity after modulation may comb.
25
+ * Read ages are [1,windowSamples+1]; no single latency for shifting audio.
26
+ * Linear interpolation aliases, and windows can color/cancel/modulate audio.
27
+ * No formant preservation, general time stretching or realtime guarantee.
28
+ */
29
+ export declare const windowedPitchShift: import("@unworklet/core").SubgraphDecl<[config: WindowedPitchShiftConfig], {
30
+ tick(input: Node<"f32">, c: WindowedPitchShiftControls): {
31
+ output: Node<"f32">;
32
+ ratioRejected: Node<"bool">;
33
+ };
34
+ }>;
@@ -0,0 +1,51 @@
1
+ import { defineSubgraph, f32, f64, i32, select, state } from '@unworklet/core';
2
+ const SCALE = 2 ** 128;
3
+ /** Bounded mono two-window moving-readhead pitch effect. Call once per sample.
4
+ * Finite f32 input required. Complementary triangular weights and linear reads
5
+ * are convex: no implicit gain, feedback, dry/wet mix or bypass. Initially,
6
+ * ratio 1 is a (1+windowSamples/2)-sample delay; unity after modulation may comb.
7
+ * Read ages are [1,windowSamples+1]; no single latency for shifting audio.
8
+ * Linear interpolation aliases, and windows can color/cancel/modulate audio.
9
+ * No formant preservation, general time stretching or realtime guarantee.
10
+ */
11
+ export const windowedPitchShift = defineSubgraph((config) => {
12
+ const { sampleRate, windowSamples = 2048 } = config;
13
+ if (!Number.isInteger(sampleRate) || sampleRate < 8000 || sampleRate > 192000 ||
14
+ !Number.isInteger(windowSamples) || windowSamples < 32 || windowSamples > 16384 || windowSamples % 2 !== 0) {
15
+ throw new RangeError('windowedPitchShift requires integer sampleRate [8000,192000] and even windowSamples [32,16384]');
16
+ }
17
+ const size = windowSamples + 3;
18
+ // Exact binary scaling avoids native tiny-state scrubbing, retaining all
19
+ // finite f32 inputs without threatening f64 overflow at either endpoint.
20
+ const history = state.buffer.f64({ size }).expose({ name: 'historyScaled', snapshot: 'persistent' });
21
+ const cursor = state.i32(0).named('cursor'), valid = state.i32(0).named('valid');
22
+ const phase = state.f64(0).named('phaseScaled'), ratio = state.f64(1).named('ratio');
23
+ const wet = state.f64(0).expose({ name: 'wetScaled', snapshot: 'transient' });
24
+ return {
25
+ tick(input, c) {
26
+ const accepted = c.ratio.gte(0.5).and(c.ratio.lte(2));
27
+ ratio.write(select(accepted, f64(c.ratio), select(c.reset, f64(1), ratio.read())));
28
+ phase.write(select(c.reset.or(c.retrigger), f64(0), phase.read()));
29
+ const p = phase.read().div(SCALE), other = select(p.gte(0.5), p.sub(0.5), p.add(0.5));
30
+ const weight = f64(1).sub(p.mul(2).sub(1).abs());
31
+ const write = select(c.reset, i32(0), cursor.read()), available = select(c.reset, i32(0), valid.read());
32
+ const at = (position) => {
33
+ const delay = position.mul(windowSamples).add(1), whole = i32(delay.floor()), fraction = delay.sub(f64(whole));
34
+ const relative = write.sub(whole), recentIndex = select(relative.lt(0), relative.add(size), relative);
35
+ const olderIndex = select(recentIndex.eq(0), i32(size - 1), recentIndex.sub(1));
36
+ const recent = select(available.gte(whole), history.read(recentIndex), f64(0));
37
+ const older = select(available.gt(whole), history.read(olderIndex), f64(0));
38
+ return recent.mul(f64(1).sub(fraction)).add(older.mul(fraction));
39
+ };
40
+ // Materialize before advancing phase or overwriting the ring. Using the
41
+ // exact complement guarantees unit DC gain at every crossfade position.
42
+ wet.write(at(p).mul(weight).add(at(other).mul(f64(1).sub(weight))));
43
+ history.write(write, f64(select(c.reset, f32(0), input)).mul(SCALE));
44
+ cursor.write(select(c.reset, i32(0), write.add(1).mod(size)));
45
+ valid.write(select(c.reset, i32(0), available.add(1).min(size)));
46
+ const next = p.add(f64(1).sub(ratio.read()).div(windowSamples));
47
+ phase.write(select(c.reset, f64(0), next.sub(next.floor()).mul(SCALE)));
48
+ return { output: f32(wet.read().div(SCALE)), ratioRejected: accepted.not() };
49
+ },
50
+ };
51
+ });
@@ -0,0 +1,76 @@
1
+ # Catalog A/B audition
2
+
3
+ This is a historical **test-only** browser fixture. It is no longer included in
4
+ `npm run build:site` or publicly deployed. `tests/site-fixture-build.mjs` stages
5
+ the original pages in a temporary directory so the existing browser and
6
+ navigation assertions remain executable. Its three isolated consumers install
7
+ identical packed den bytes with locked dependencies. The original DSP settings
8
+ are unchanged. `npm run build:catalog` remains a standalone development fixture.
9
+
10
+ ## Four candidate materials
11
+
12
+ The page imports the already reviewed `/musical-examples` processors, complete
13
+ A/B parameter maps and original asset builders without rewriting or normalizing
14
+ them. See [musical examples](musical-examples.md) for independent three-rate
15
+ pitch, envelope, tail, headroom, repeat and snapshot evidence.
16
+
17
+ - Glass dyad: two independently held 220/330 Hz wavetable voices, original
18
+ three-frame cycle bank, soft-glass and brighter-harmonic maps.
19
+ - FM/modal hit: fixed 220 Hz FM voice with three resonant modes. Gate rise
20
+ strikes once; a new strike clears the previous tail.
21
+ - Grain cloud: two seeded grains over original 4096-frame PCM, forward/reverse
22
+ maps. Release stops new grains and lets current windows finish.
23
+ - Shaped echo: soft ADAA, Q0.5 low-pass and stereo repeats. This page supplies a
24
+ native 220 Hz sine at amplitude 0.25 with a 10 ms gain time constant. This
25
+ explicit audition source is separate from the effect's unchanged settings.
26
+
27
+ The visible master ranges from 0 to 1 and defaults to **1.00×**. There is no
28
+ hidden additional attenuation, normalization, limiter or per-example leveling.
29
+ Start with low device volume. Stereo waveforms use a fixed −1..+1 scale; the
30
+ visible peak is an observed analyser window, not proof of all-frame headroom or
31
+ all-device deadline clearance. The browser is deliberately restricted to 48 kHz.
32
+
33
+ ## Interaction and cleanup
34
+
35
+ No AudioContext is constructed on page load. Start creates a fresh context and
36
+ native node with all gates off. Asset-backed candidates send the original PCM
37
+ through the existing native event, then inspect native snapshot length and
38
+ buffer-head values before enabling the play controls. The displayed SHA256
39
+ identifies the complete submitted asset; the receipt check itself checks loaded
40
+ length and the inspected head, not a full buffer hash. Snapshot acknowledgement
41
+ has a 10-second bound and is cancelled immediately by Stop.
42
+
43
+ Select materials and A/B maps while stopped. Pointer capture supports separate
44
+ fingers. Global A/S/Space shortcuts apply outside interactive controls. Focus a
45
+ play button and hold Enter/Space to play it; assistive/virtual click activation
46
+ holds for 180 ms and releases. Focused Stop, Clear, Release, sliders and links
47
+ keep their native keyboard behavior. Blur releases held intents.
48
+
49
+ Release drops gates and leaves tails. Clear drops gates and source excitation,
50
+ holds the native reset for 40 ms, and leaves the context ready. Stop releases,
51
+ fades the master for 20 ms and disposes/closes audio. Visibility loss and
52
+ navigation close immediately. Generation/cancellation checks keep late resume,
53
+ compilation or preload completion from reviving an old session. No microphone,
54
+ external MIDI, loader, parameter, snapshot or routing framework is added.
55
+
56
+ ## Provenance and verification boundary
57
+
58
+ The build records packed-package integrity plus the Git commit and dirty state
59
+ when the checkout is available. A Vercel build without Git may use the documented
60
+ `VERCEL_GIT_COMMIT_SHA` system value; otherwise it explicitly says unavailable.
61
+ No project access or environment settings are changed, and no other environment
62
+ values are exposed. Browser assets display CANDIDATE and Runtime NOT_CLEARED.
63
+
64
+ `tests/catalog-audition.test.mjs` serves the actual combined test-fixture bytes. The
65
+ hosted browser gate must exercise all eight A/B selections, native asset receipt,
66
+ actual stereo unity/half-gain residuals, finite/headroom windows, release/tail,
67
+ clear/stop and route preservation. The independent lifecycle helper observes
68
+ native contexts/worklets and introduces resume/WASM/snapshot barriers; it checks
69
+ cancellation, retry, keyboard/touch behavior, fatal errors and navigation. Mobile
70
+ and desktop screenshots and a source/package manifest are retained. The earlier
71
+ offline tests remain the full-frame numerical/headroom/continuation evidence.
72
+
73
+ Local compilation/staging is not a browser pass. Hosted exact-head CI and review
74
+ must pass before integration. No new human listening approval or golden
75
+ promotion is inferred from automated checks. Historical intermittent runtime
76
+ failures remain open.
@@ -0,0 +1,125 @@
1
+ # Catalog filters: clean responses, EQ, formants and crossover
2
+
3
+ These are additive CANDIDATE modules. The initial `filter`, instrument, sound
4
+ settings and Delay FX are unchanged. No framework, dependency upgrade, dynamics
5
+ processing, automatic normalization or approved golden is added.
6
+
7
+ ## Public entries and contract
8
+
9
+ All modules are unworklet `defineSubgraph` declarations. Instantiate with immutable
10
+ `sampleRate`, use `Node<f32>` signals/controls and `Node<bool>` reset, and call each
11
+ instance exactly once per sample. Each instance owns independent fixed histories.
12
+ Construction accepts finite rates 8000–192000 Hz; numeric fixtures exercise
13
+ 44100/48000/96000 Hz. Browser worklet compilation remains 48000 Hz only.
14
+
15
+ - `@denaudio/den/state-variable-filter`: `stateVariableFilter({sampleRate})`.
16
+ `tick(input, cutoffHz, q, reset)` returns simultaneous `lowpass`, `bandpass`,
17
+ `highpass`, `notch`, `allpass`. Cutoff clamps to [20,min(20000,.45*sampleRate)] Hz,
18
+ Q to [.5,10]. Bandpass is normalized to unity at its center; low/high resonance
19
+ can exceed unity. This is a linear trapezoidal SVF, not a nonlinear ladder or
20
+ character model. It uses one two-integrator history for all five responses.
21
+ - `@denaudio/den/biquad-eq`: `biquadEq({sampleRate,mode})`, with construction mode
22
+ `peaking`, `lowShelf` or `highShelf`. `tick(input,cutoffHz,q,gainDb,reset)` uses
23
+ the same cutoff/Q limits and gain [-24,24] dB. Shelves have fixed monotonic
24
+ slope S=1; their Q argument is intentionally unused. The peaking filter's gain
25
+ is the gain at center; shelf gain at center is half the requested dB, with full
26
+ shelf gain at DC or Nyquist. No hidden makeup gain is used.
27
+ - `@denaudio/den/formant-bank`: `formantBank({sampleRate,bands})`, with 1–8 fixed
28
+ `{frequencyHz,q,gain}` bands. Construction validates frequency within the cutoff
29
+ range, Q [.5,10], finite gain [-1,1]. `tick(input,frequencyRatio,resonanceScale,reset)`
30
+ applies linear frequency/Q factors, each clamped to [.25,4], then each filter's
31
+ final cutoff/Q bounds. Parallel unity-peak bandpasses are summed with their
32
+ specified gains. There is no normalization, vowel-recognition claim, speech
33
+ synthesizer, or implicit voice. Negative gain is an explicit phase inversion.
34
+ - `@denaudio/den/crossover`: `crossover({sampleRate})`.
35
+ `tick(input,cutoffHz,reset)` returns `{low,high}` from fourth-order
36
+ Linkwitz–Riley branches: two cascaded second-order Butterworth responses each.
37
+ A shared first-stage SVF supplies LP/HP, followed by separate LP/HP stages.
38
+ At fixed cutoff, both outputs are -6.0206 dB at crossover and their positive-
39
+ polarity sum has unity magnitude. The sum is an allpass: it is not zero phase,
40
+ original PCM, a linear-phase FIR, or latency compensation.
41
+
42
+ Inputs and controls must be finite. These blocks do not clamp or sanitize audio,
43
+ apply bypass, or guarantee arbitrary modulation is stable. The tests cover
44
+ specified per-sample sweeps; fixed-parameter transfer-function guarantees must
45
+ not be transferred to arbitrary coefficient jumps. Host callers choose their
46
+ musical control trajectories and explicit dry/wet/bypass path. Reset clears
47
+ history before the current input, so it may produce a waveform discontinuity.
48
+ There is no click-free-reset claim. When input stops, IIR tails decay normally.
49
+
50
+ ## Numeric and state choices
51
+
52
+ Internal audio histories are f64. unworklet 0.4.1 flushes scalar state writes with
53
+ magnitude below 1e-30, so these new modules store audio histories scaled by exact
54
+ `2**128` and divide by the same power on read. This preserves subnormal f32 signal
55
+ history without extra scale rounding. The scaled full finite f32 range remains
56
+ well within f64. Bounded nonzero coefficient scratch values are overwritten
57
+ before every use. No audio intermediate is cached merely to shorten a graph.
58
+ The initial filter's existing state/schema is not changed or migrated.
59
+
60
+ The half-angle tangent uses the existing reviewed bounded polynomial formulation.
61
+ EQ amplitude uses a degree-16 exponential polynomial on the explicitly bounded
62
+ ±24 dB domain. Both SVF and EQ use a TPT two-integrator realization; EQ mixes its band/low
63
+ responses to match the RBJ static formulas. Peaking uses damping 1/(Q*A),
64
+ low shelf scales the tuning by 1/sqrt(A), and high shelf by sqrt(A), where
65
+ A=10^(gainDb/40). Independent impulse oracles use trigonometric RBJ coefficients
66
+ and direct form I. Snapshots use the existing same-processor/schema/rate contract.
67
+
68
+ ## Verification and provenance
69
+
70
+ `tests/catalog-filters.spec.ts` has 49 cases covering:
71
+
72
+ - all five SVF response impulses, Q/cutoff endpoints and comparison with the
73
+ unchanged initial low-pass at three rates (absolute sample error <2e-6)
74
+ - three EQ modes across gain/cutoff endpoints; unity at 0 dB, center/shelf gains;
75
+ independent impulse error <1e-5
76
+ - LR4 cascade references, half-amplitude/in-phase split at crossover, sampled
77
+ frequency magnitude-flat sum, and an explicit rejection of identity-PCM claims
78
+ - independent weighted formant-band sums and center shifts
79
+ - reset, uninterrupted same-schema snapshot continuation, lower clamps,
80
+ controlled per-sample modulation, finite output and zero scrubbed samples
81
+ - tiny/subnormal histories at 1e-35 and 1e-39, measured against independent
82
+ references after scaling for inspection
83
+ - invalid construction rate/band count/frequency/Q/gain rejection
84
+ - adversarial 10/100/1000 Hz cutoff sweeps at Q=10 and +24 dB across all three
85
+ EQ modes/rates; zero scrubs and a fixed conservative peak ceiling reject
86
+ runaway state without implying a universal headroom limit
87
+
88
+ The isolated packed consumer exercises all four public subpaths, strict NodeNext
89
+ checking, actual five-output worklet construction, three-rate offline impulse
90
+ renders, repeated PCM identity and independent references. Its per-run artifacts
91
+ link source/package/lock hashes, complete controls, source impulse, rates, raw
92
+ planar f32/WAV, fixed ±1 static waveforms, measured peaks/errors and build/WASM
93
+ hashes. The browser stage checks real 48 kHz processing, reset and native snapshot
94
+ restoration; it must pass on hosted exact-head CI before integration. Local
95
+ browser availability failures remain failed/unverified stages, never a pass.
96
+ No generated impulse WAV is human-approved musical audio or a golden baseline.
97
+
98
+ ## Formula sources and boundaries
99
+
100
+ The multimode SVF follows the linear trapezoidal circuit solution described in
101
+ [Andrew Simper's SVF note](https://www.cytomic.com/files/dsp/SvfLinearTrapOptimised2.pdf).
102
+ Cytomic's [technical-paper page](https://cytomic.com/technical-papers/) states its
103
+ shared algorithmic knowledge is public domain. EQ response formulas are checked
104
+ against the [W3C/RBJ Audio EQ Cookbook](https://www.w3.org/TR/audio-eq-cookbook/).
105
+ This is an original implementation from mathematical formulas, not bundled
106
+ third-party source. The crossover is the explicit cascade construction above;
107
+ formants are an explicit parallel bank, not a borrowed commercial preset.
108
+
109
+ Nonlinear character filters, comb resonators, dynamic multiband processing and
110
+ vowel presets are separate catalog work. No universal real-time capacity,
111
+ all-device stability, transparent changing-cutoff reconstruction, or human
112
+ listening approval is established by these tests.
113
+
114
+ ## Rejected implementation retained in history
115
+
116
+ The rejected `tests/fixtures/eq-before-tpt.ts` (from the first local catalog
117
+ commit `4235a0a`) retains the time-varying transposed direct-form II EQ.
118
+ Independent review found peaking-state amplification to peak 23939.65 at
119
+ 44.1 kHz with a normalized 0.1 sine, Q=10, +24 dB and 100 Hz logarithmic cutoff
120
+ modulation; 1 kHz modulation overflowed/scrubbed. Static impulse agreement did
121
+ not detect this. That realization is rejected and preserved as a non-shipping counterexample
122
+ fixture. A regression confirms it scrubs under the same input that the TPT
123
+ replacement handles with zero scrubs. The reproductions are now mandatory regressions;
124
+ no threshold was raised to accept the rejected behavior. This correction changes
125
+ only the unmerged new catalog EQ, not the existing initial candidate's DSP.
@@ -0,0 +1,220 @@
1
+ # Modulation, stereo delay and algorithmic reverb candidates
2
+
3
+ Status: **CANDIDATE**. These are composable DSP modules, not human-approved sounds,
4
+ finished room/plate emulations, or a browser real-time performance guarantee.
5
+ The catalog source is [sections 8 and 10](https://www.notion.so/3ef449b4e81d817db1acf1fff48779b3).
6
+
7
+ Public entries:
8
+
9
+ - `@denaudio/den/modulation-fx`: `flanger`, `phaser` and their config/control types
10
+ - `@denaudio/den/stereo-delay`: `pingPongDelay`, `multiTapDelay`, `StereoDelayConfig`,
11
+ `PingPongControls`, `DelayMixControls`, `DelayTap`, `MultiTapDelayConfig`
12
+ - `@denaudio/den/reverb`: `algorithmicReverb`, `AlgorithmicReverbConfig`, `ReverbControls`
13
+
14
+ All use existing `defineSubgraph` / `instantiate` and state/snapshot mechanisms.
15
+ No loader, routing, serialization, parameter, or test framework is added.
16
+ Keep unworklet at 0.4.1. The root entry does not re-export these candidate modules.
17
+
18
+ ## Shared entry contract
19
+
20
+ Instantiate during graph construction with `sampleRate: ctx.sampleRate`; rates are
21
+ integer Hz in [8000, 192000]. Call each instance's `tick` exactly once per sample.
22
+ Audio and continuous controls are `Node<'f32'>`, reset/bypass are `Node<'bool'>`.
23
+ Inputs and controls must be finite. Numerical tests use nominal audio within
24
+ [-1, 1]; extreme near-overflow f32 input is not a supported operating level.
25
+ There is no output limiter and wet/feedback paths can exceed unity.
26
+
27
+ `mix` is linear dry/wet in [0, 1], clamped at the endpoints. A zero mix continues
28
+ feeding/advancing the effect. Bypass emits unity dry, stops new input to the wet
29
+ network, and continues its existing tail. Re-enabling it reveals that continuing
30
+ tail. Bypass does not save CPU. Reset clears effective history **before** the
31
+ current input is processed. Holding reset continually clears history. Reset and
32
+ bypass together suppress new wet input; reset alone accepts the current input.
33
+
34
+ No implicit parameter smoothing or click-free bypass is claimed. Abrupt controls
35
+ can create discontinuities. The caller owns musical modulation and smoothing.
36
+ Persistent histories use the existing snapshot path; tests cover exact same-rate,
37
+ same-schema continuation. Cross-rate snapshots and changed construction schemas
38
+ are not asserted compatible. unworklet scalar writes flush values below 1e-30;
39
+ very small tails therefore differ from ideal infinite-precision equations.
40
+
41
+ ## Flanger
42
+
43
+ `flanger(config).tick(left, right, controls)` is stereo-in/stereo-out and returns
44
+ `left`, `right`, `timingRejected`, `modulationClipped`.
45
+
46
+ Construction controls:
47
+
48
+ - `maxDelaySeconds`: fixed per-channel capacity, one sample through 0.02 seconds;
49
+ default 0.02
50
+ - `stereoPhaseCycles`: finite relative right LFO phase; default 0.5
51
+
52
+ Sample controls: `delaySeconds`, `depthSeconds`, `rateHz`, `feedback`, `mix`,
53
+ `bypass`, `reset`. Base time must lie in [one sample, capacity]. Invalid time emits
54
+ dry, stops new wet input, and preserves the prior valid internal time (or minimum
55
+ time on reset), reporting `timingRejected`. Depth and rate inherit existing delay
56
+ bounds: depth [0, 0.05] seconds, rate [0, 20] Hz; the resulting moving head is clipped
57
+ to the declared short capacity and reports `modulationClipped`.
58
+ Feedback is positive, clamped to [0, 0.95].
59
+
60
+ This intentionally reuses `delayFx`, `delayReadhead` and the existing LFO. Flat
61
+ feedback and short delay create the comb response: with a static integer delay D,
62
+ wet transfer is z^-D / (1 - feedback*z^-D). At zero feedback and 50% mix, the first
63
+ notch is at sampleRate/(2D). Tests check complex frequency response, not merely a
64
+ renamed chorus preset. Wet latency is the selected delay; dry has no added delay.
65
+ Linear interpolation attenuates high frequencies and moving delay produces
66
+ Doppler pitch changes. There is no through-zero or negative-feedback mode.
67
+
68
+ ## Phaser
69
+
70
+ `phaser(config).tick(input, controls)` returns one mono sample. Compose independent
71
+ instances, or use different modulation trajectories, for stereo.
72
+
73
+ - `stages`: fixed 2, 4, 6 or 8 first-order allpasses; default 4
74
+ - `frequencyHz`: common per-stage -90 degree frequency, clamped to
75
+ [20, 0.45*sampleRate]
76
+ - `feedback`: signed previous-sample wet feedback, clamped to [-0.95, 0.95]
77
+ - `mix`, `bypass`, `reset`: shared behavior above
78
+
79
+ With a = (tan(pi*f/fs)-1)/(tan(pi*f/fs)+1), a static stage has transfer
80
+ (a + z^-1)/(1 + a*z^-1). Stages are normalized lattice sections:
81
+ y = a*x + b*s; next_s = b*x - a*s; b = sqrt(1-a*a). The scattering matrix is
82
+ orthogonal, so even changing a every sample cannot create energy in that isolated
83
+ section. The bounded sine/cosine polynomial uses f64 arithmetic, followed by f64
84
+ coefficient, signal, history and feedback staging. This does not promise absence
85
+ of modulation sidebands, perceptual artifacts, or numerical error.
86
+
87
+ Static wet magnitude is unity; dry/wet cancellation produces the phaser notches.
88
+ At 100% wet it is an allpass phase effect, not an EQ notch bank. There is direct
89
+ feedthrough, no lookahead, and exactly one sample in the explicit feedback path.
90
+ A normalized realization is deliberate: a time-varying direct-form allpass does
91
+ not inherit the same per-sample energy argument automatically.
92
+
93
+ ## Ping-pong delay
94
+
95
+ `pingPongDelay(config).tick(left, right, controls)` returns stereo samples and
96
+ `timingRejected`. `maxDelaySeconds` is fixed, one sample through 8 seconds;
97
+ default 2. `timeSeconds` sets both channel delays and has the same invalid-request
98
+ policy as the flanger base time. It is a moving, linearly interpolated head;
99
+ there is no dual-head transition or internal tempo conversion.
100
+
101
+ Feedback is signed and clamped to [-0.95, 0.95]. Both channels are read before
102
+ either channel is written. The left wet output feeds the right input and vice
103
+ versa. A left-only impulse appears on the left after D samples, right after 2D,
104
+ left after 3D, with amplitudes 1, g, g². No extra sample enters this circulation.
105
+ A duplicated mono input stays symmetric. A feedback filter is not part of this
106
+ candidate; the existing `delayFx` provides filtered independent-channel repeats.
107
+
108
+ ## Multitap delay
109
+
110
+ `multiTapDelay(config).tick(monoInput, controls)` returns `left`, `right`.
111
+ Construction fixes 1–8 taps and the common `maxDelaySeconds` capacity (default 2,
112
+ up to 8 seconds). Each tap has `delaySeconds`, `gainLeft`, `gainRight`. Times must
113
+ fit [one sample, capacity]; signed gains must be finite and in [-1e6, 1e6].
114
+
115
+ All gains share divisor max(1, sum(abs(left gains)), sum(abs(right gains))). This
116
+ preserves signs, each channel's relative tap levels, and stereo ratios; it reduces
117
+ absolute level when either sum exceeds unity. There is no per-tap auto gain or
118
+ energy-based normalization. The post-normalization L1 sum in either channel is
119
+ at most one, giving the mixed-tap feedback path a clear amplitude bound.
120
+
121
+ Each tap reuses one independent `delayReadhead`, read once and written once per
122
+ sample. This deliberately spends N fixed-capacity buffers instead of changing the
123
+ existing readhead's single-read contract. Feedback is the average of the two wet
124
+ channels, multiplied by signed gain [-0.95, 0.95], then added to mono input before
125
+ all tap writes. Opposite-polarity stereo taps can cancel in this feedback sum.
126
+ Dry mono input is copied to both channels. There is no runtime tap insertion,
127
+ reordering or time control; rebuild for a different rhythmic pattern.
128
+
129
+ ## Small algorithmic reverb
130
+
131
+ `algorithmicReverb(config).tick(left, right, { mix, bypass, reset })` returns
132
+ stereo samples. The network has four fixed delays and a Hadamard/2 orthogonal
133
+ feedback matrix. Reference delays are 29.7, 37.1, 41.1 and 43.7 ms, scaled and
134
+ rounded to sample counts at construction.
135
+
136
+ - `roomScale`: fixed [0.5, 2], default 1
137
+ - `decaySeconds`: fixed low-frequency target RT60 [0.1, 10], default 1.5
138
+ - `dampingHz`: fixed matched-z one-pole corner [20, 0.45*sampleRate], default
139
+ min(6000, 0.45*sampleRate)
140
+
141
+ Each delay's loop gain is 10^(-3*delaySamples/(sampleRate*decaySeconds)). The damping
142
+ pole exp(-2*pi*dampingHz/sampleRate) makes a convex one-pole lowpass, so high-band
143
+ decay is shorter than the low-frequency target. Room, decay and damping are
144
+ construction settings, **not runtime modulation controls**. Compute rate-relative
145
+ settings from `ctx.sampleRate`, including during unworklet's eager 48 kHz capture.
146
+
147
+ Input stereo is injected through two orthonormal Hadamard columns and wet stereo
148
+ uses two other orthonormal projections. Hadamard diffusion occurs at every return;
149
+ there are no extra input diffusers or hand-tuned early-reflection paths. Sparse
150
+ initial reflections and metallic modes are possible. Increasing echo density and
151
+ correct low-band decay do not establish a pleasant room/hall/plate sound.
152
+
153
+ There is no predelay or processing lookahead. The first wet arrival is approximately
154
+ 29.7 ms*roomScale; dry is immediate. This is an acoustic network delay, not a host
155
+ latency-compensation value. Four buffers use the existing readhead capacity rule,
156
+ ceil(sampleRate*maxDelaySeconds)+2; floating-point ceiling can add a guard slot.
157
+ Passing their times through the existing f32 seconds API introduces at most a small
158
+ fractional-sample interpolation difference; the independent reference includes it.
159
+ This implementation is damped, not a lossless network or freeze effect.
160
+
161
+ No convolution, IR handling, FFT, shimmer, frequency shifter or preset engine is
162
+ included. There is no claim of matching a commercial reverb or a physical room.
163
+
164
+ ## Verification and reproducibility
165
+
166
+ Focused suites are `tests/catalog-modulation-fx.spec.ts` and
167
+ `tests/catalog-reverb.spec.ts`, at 44.1, 48 and 96 kHz. They check independent
168
+ complex comb/allpass responses, DC/Nyquist and cutoff extremes, lattice
169
+ fast-modulation energy, signed ping-pong timing against a non-ring timeline,
170
+ normalization/polarity/steady-state tap levels, reset/bypass/mix, fresh silence,
171
+ exact same-schema continuation, stereo and mono behavior, FDN impulse/matrix
172
+ references, bounded energy, echo-density growth and band-decay fits.
173
+
174
+ The FDN 0.6 s decay fixture requires a low-band fitted RT60 between 0.45 and 0.75 s
175
+ and high-band RT60 below 65% of low-band RT60. This explicitly allows finite-window
176
+ and modal estimation differences; no test threshold is adjusted from an observed
177
+ result. A separate 43.7-second offline fixture exercises maximum 10-second decay,
178
+ sustained stereo excitation and a long tail. Every render requires finite outputs
179
+ and zero reported scrubbed samples.
180
+
181
+ `tests/catalog-modulation-packed.test.mjs` installs the actual tarball into a fresh
182
+ consumer, checks public-entry types, renders WASM at all three rates, builds a Vite
183
+ worklet bundle, and records CANDIDATE WAV files plus package/source hashes under
184
+ `artifacts/catalog-modulation`. Building a worklet bundle is not browser execution.
185
+ Exact-head CI/browser verification belongs to the integration gate. These tests do
186
+ not measure hardware audio, scheduler deadlines, maximum voice count, or universal
187
+ runtime performance. Listen to candidate material before musical approval.
188
+
189
+ ## Algorithm sources
190
+
191
+ - Julius O. Smith, [delay-line interpolation](https://www.dsprelated.com/freebooks/pasp/Delay_Line_Signal_Interpolation.html)
192
+ and [allpass sections](https://www.dsprelated.com/freebooks/filters/Allpass_Filter_Sections.html):
193
+ background for the interpolation and static allpass transfer properties.
194
+ - Sebastian J. Schlecht and Emanuel A. P. Habets,
195
+ [On Lossless Feedback Delay Networks](https://arxiv.org/abs/1606.07729): matrix
196
+ losslessness background. This candidate adds fixed losses and independent tests;
197
+ the paper is not evidence of this implementation's musical or runtime quality.
198
+
199
+ Implementation is original TypeScript composition of den/unworklet primitives;
200
+ no external DSP source code or audio assets were copied.
201
+
202
+
203
+ ## Review clarifications and boundary correction
204
+
205
+ `decaySeconds` sets each FDN line's nominal loop-gain loss, not an exact measured
206
+ RT60 or a hard 10-second tail ceiling. Damping suppresses high frequencies, but
207
+ very low damping also adds loop-filter group delay and can lengthen low-band
208
+ decay. Independent review at 48 kHz, roomScale 0.5, decaySeconds 10 and damping 20 Hz
209
+ measured asymptotic stereo-energy RT60 of 14.8826 seconds (fit 5–15 seconds of a
210
+ 43.69-second impulse render, zero scrubs/nonfinite values). Independent
211
+ matrix-pole analysis predicted 14.9275 seconds. This stable setting is retained
212
+ with the correct interpretation, not described as a measured 10-second maximum.
213
+
214
+ The catalog integration also corrects the shared read-head's one-sample
215
+ construction boundary. At rates such as 8001 Hz, `sampleRate*(1/sampleRate)` can
216
+ round below 1. Construction now checks the seconds value against the exact
217
+ computed reciprocal first, then floors the already-valid product at one sample.
218
+ The immediately smaller representable seconds value still rejects. Regression
219
+ coverage checks new wrappers plus old/new audio, complete state and WASM parity
220
+ for the original 44.1/48/96 kHz paths. No broader capacity rounding is introduced.