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