@denaudio/den 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- package/LICENSE-APACHE +202 -0
- package/LICENSE-MIT +21 -0
- package/README.md +107 -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 +17 -0
- package/dist/convolution.js +87 -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 +26 -0
- package/dist/prepared-convolution.js +104 -0
- package/dist/resident-recorder.d.ts +28 -0
- package/dist/resident-recorder.js +68 -0
- package/dist/resident-time-stretch.d.ts +46 -0
- package/dist/resident-time-stretch.js +136 -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 +50 -0
- package/dist/spatial-chains.js +76 -0
- package/dist/spectral-fft.d.ts +27 -0
- package/dist/spectral-fft.js +84 -0
- package/dist/spectral-freeze.d.ts +24 -0
- package/dist/spectral-freeze.js +74 -0
- package/dist/spectral-gate.d.ts +23 -0
- package/dist/spectral-gate.js +58 -0
- package/dist/spectral-stft.d.ts +14 -0
- package/dist/spectral-stft.js +70 -0
- package/dist/spectral-texture.d.ts +46 -0
- package/dist/spectral-texture.js +141 -0
- package/dist/spectral.d.ts +18 -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 +433 -0
- package/docs/character-filter.md +139 -0
- package/docs/contracts.md +141 -0
- package/docs/control-modulation.md +194 -0
- package/docs/convolution.md +50 -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 +58 -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 +197 -0
- package/docs/realtime-audition.md +63 -0
- package/docs/resident-recorder.md +151 -0
- package/docs/resident-time-stretch.md +196 -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 +251 -0
- package/docs/spectral-freeze-entry.md +132 -0
- package/docs/spectral-gate-entry.md +134 -0
- package/docs/spectral-large-candidate.md +81 -0
- package/docs/spectral-texture-entry.md +178 -0
- package/docs/spectral.md +127 -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
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { defineSubgraph, f32, f64, i32, select, state } from '@unworklet/core';
|
|
2
|
+
import { readWavetableCycle } from './wavetable-read.js';
|
|
3
|
+
function layout(length, count) {
|
|
4
|
+
if (!Number.isInteger(length) || length < 16 || length > 4096 || !Number.isInteger(Math.log2(length)) ||
|
|
5
|
+
!Number.isInteger(count) || count < 1 || count > 16)
|
|
6
|
+
throw new RangeError('banded wavetable requires power-of-two frameLength 16..4096 and frameCount 1..16');
|
|
7
|
+
const harmonics = [];
|
|
8
|
+
for (let h = length / 2 - 1; h >= 1; h = Math.floor(h / 2))
|
|
9
|
+
harmonics.push(h);
|
|
10
|
+
const required = length * count * harmonics.length;
|
|
11
|
+
if (required > 65536)
|
|
12
|
+
throw new RangeError('all wavetable bands must fit 65536 samples');
|
|
13
|
+
return { harmonics, required };
|
|
14
|
+
}
|
|
15
|
+
// Host-only radix-2 FFT. This runs before native ingress, never in audio processing.
|
|
16
|
+
function transform(real, imag, inverse) {
|
|
17
|
+
const size = real.length;
|
|
18
|
+
for (let n = 1, j = 0; n < size; n++) {
|
|
19
|
+
let bit = size >> 1;
|
|
20
|
+
for (; j & bit; bit >>= 1)
|
|
21
|
+
j ^= bit;
|
|
22
|
+
j ^= bit;
|
|
23
|
+
if (n < j) {
|
|
24
|
+
const r = real[n], im = imag[n];
|
|
25
|
+
real[n] = real[j];
|
|
26
|
+
imag[n] = imag[j];
|
|
27
|
+
real[j] = r;
|
|
28
|
+
imag[j] = im;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
for (let width = 2; width <= size; width *= 2) {
|
|
32
|
+
for (let base = 0; base < size; base += width)
|
|
33
|
+
for (let k = 0; k < width / 2; k++) {
|
|
34
|
+
const angle = (inverse ? 2 : -2) * Math.PI * k / width, c = Math.cos(angle), s = Math.sin(angle);
|
|
35
|
+
const a = base + k, b = a + width / 2;
|
|
36
|
+
const tr = c * real[b] - s * imag[b], ti = s * real[b] + c * imag[b];
|
|
37
|
+
real[b] = real[a] - tr;
|
|
38
|
+
imag[b] = imag[a] - ti;
|
|
39
|
+
real[a] += tr;
|
|
40
|
+
imag[a] += ti;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (inverse)
|
|
44
|
+
for (let n = 0; n < size; n++) {
|
|
45
|
+
real[n] /= size;
|
|
46
|
+
imag[n] /= size;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Prepare harmonic-truncated pitch bands outside the audio callback. No per-band
|
|
50
|
+
* normalization, resampling, loader, or provenance/license assumption is added.
|
|
51
|
+
* Reject nonfinite input or reconstruction that overflows float32. DC, phase and
|
|
52
|
+
* retained harmonic gain are preserved within FFT/float32 roundoff. */
|
|
53
|
+
export function prepareWavetableBands({ frames }) {
|
|
54
|
+
const frameCount = frames.length, frameLength = frames[0]?.length ?? 0;
|
|
55
|
+
const { harmonics, required } = layout(frameLength, frameCount);
|
|
56
|
+
for (const frame of frames) {
|
|
57
|
+
if (!(frame instanceof Float32Array) || frame.length !== frameLength || frame.some(x => !Number.isFinite(x))) {
|
|
58
|
+
throw new RangeError('wavetable frames must be equal-length finite Float32Arrays');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const data = new Float32Array(required);
|
|
62
|
+
const real = new Float64Array(frameLength), imag = new Float64Array(frameLength);
|
|
63
|
+
const r = new Float64Array(frameLength), im = new Float64Array(frameLength);
|
|
64
|
+
for (let frame = 0; frame < frameCount; frame++) {
|
|
65
|
+
real.set(frames[frame]);
|
|
66
|
+
imag.fill(0);
|
|
67
|
+
transform(real, imag, false);
|
|
68
|
+
for (let band = 0; band < harmonics.length; band++) {
|
|
69
|
+
r.fill(0);
|
|
70
|
+
im.fill(0);
|
|
71
|
+
const limit = harmonics[band];
|
|
72
|
+
r[0] = real[0];
|
|
73
|
+
// Explicit conjugate pairs guarantee a real table despite FFT roundoff.
|
|
74
|
+
for (let h = 1; h <= limit; h++) {
|
|
75
|
+
r[h] = (real[h] + real[frameLength - h]) / 2;
|
|
76
|
+
im[h] = (imag[h] - imag[frameLength - h]) / 2;
|
|
77
|
+
r[frameLength - h] = r[h];
|
|
78
|
+
im[frameLength - h] = -im[h];
|
|
79
|
+
}
|
|
80
|
+
transform(r, im, true);
|
|
81
|
+
const offset = (band * frameCount + frame) * frameLength;
|
|
82
|
+
for (let n = 0; n < frameLength; n++) {
|
|
83
|
+
const value = Math.fround(r[n]);
|
|
84
|
+
if (!Number.isFinite(value))
|
|
85
|
+
throw new RangeError('prepared wavetable exceeds finite float32 PCM range');
|
|
86
|
+
data[offset + n] = value;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { data, frameLength, frameCount, harmonicLimits: Object.freeze(harmonics) };
|
|
91
|
+
}
|
|
92
|
+
function bounded(value, high) {
|
|
93
|
+
return select(value.eq(value), f64(value), f64(0)).clamp(0, high);
|
|
94
|
+
}
|
|
95
|
+
/** Four periodic cycle interpolations: frame morph in neighboring pitch bands.
|
|
96
|
+
* Fetches eight integer resident reads so tiny local fractions survive offsets.
|
|
97
|
+
* Static harmonic truncation is not a brickwall reconstruction filter and does
|
|
98
|
+
* not remove interpolation images or bandwidth from modulation/reset/asset edits. */
|
|
99
|
+
export const bandedWavetableSource = defineSubgraph((config) => {
|
|
100
|
+
const { sampleRate, sample, frameLength, frameCount, phaseCycles = 0 } = config;
|
|
101
|
+
const { harmonics, required } = layout(frameLength, frameCount);
|
|
102
|
+
if (!Number.isFinite(sampleRate) || sampleRate < 8000 || sampleRate > 192000)
|
|
103
|
+
throw new RangeError('banded wavetable sampleRate must be in [8000,192000] Hz');
|
|
104
|
+
if (!Number.isInteger(sample.capacity) || sample.capacity < required || sample.capacity > 65536)
|
|
105
|
+
throw new RangeError('all wavetable bands must fit resident capacity');
|
|
106
|
+
if (!Number.isFinite(phaseCycles) || phaseCycles < 0 || phaseCycles >= 1)
|
|
107
|
+
throw new RangeError('phaseCycles must be in [0,1)');
|
|
108
|
+
const phaseScale = 2 ** 1020, controlScale = 2 ** 128;
|
|
109
|
+
const phase = state.f64(phaseCycles * phaseScale).named('scaledPhase'), revision = state.i32(-1).named('revision');
|
|
110
|
+
const current = state.f64(phaseCycles * phaseScale).named('scaledCurrentPhase');
|
|
111
|
+
const scan = state.f64(0).named('scaledFrame'), pitch = state.f64(0).named('scaledFrequency'), band = state.f64(0).named('scaledBand');
|
|
112
|
+
const frameLo = state.i32(0).named('frameLo'), frameHi = state.i32(0).named('frameHi');
|
|
113
|
+
const bandLo = state.i32(0).named('bandLo'), bandHi = state.i32(0).named('bandHi');
|
|
114
|
+
const starts = Array.from({ length: 4 }, (_, i) => state.i32(0).named(`start${i}`));
|
|
115
|
+
return { tick(c) {
|
|
116
|
+
current.write(select(c.reset.or(sample.revision().eq(revision.read()).not()), f64(phaseCycles * phaseScale), phase.read()));
|
|
117
|
+
scan.write(bounded(c.frame, frameCount - 1).mul(controlScale));
|
|
118
|
+
pitch.write(bounded(c.frequencyHz, .45 * sampleRate).mul(controlScale));
|
|
119
|
+
const frequency = pitch.read().div(controlScale), frame = scan.read().div(controlScale), p = current.read().div(phaseScale);
|
|
120
|
+
// Nonoverlapping transitions: the bright H band has zero weight by .45*sr/H.
|
|
121
|
+
let position = f64(0);
|
|
122
|
+
for (const h of harmonics.slice(0, -1)) {
|
|
123
|
+
const start = .225 * sampleRate / h;
|
|
124
|
+
position = position.add(frequency.sub(start).div(start).clamp(0, 1));
|
|
125
|
+
}
|
|
126
|
+
band.write(position.mul(controlScale));
|
|
127
|
+
const b = band.read().div(controlScale), frameMix = frame.sub(frame.floor()), bandMix = b.sub(b.floor());
|
|
128
|
+
frameLo.write(i32(frame.floor()));
|
|
129
|
+
frameHi.write(frameLo.read().add(1).min(frameCount - 1));
|
|
130
|
+
bandLo.write(i32(b.floor()));
|
|
131
|
+
bandHi.write(bandLo.read().add(1).min(harmonics.length - 1));
|
|
132
|
+
starts[0].write(bandLo.read().mul(frameCount).add(frameLo.read()).mul(frameLength));
|
|
133
|
+
starts[1].write(bandLo.read().mul(frameCount).add(frameHi.read()).mul(frameLength));
|
|
134
|
+
starts[2].write(bandHi.read().mul(frameCount).add(frameLo.read()).mul(frameLength));
|
|
135
|
+
starts[3].write(bandHi.read().mul(frameCount).add(frameHi.read()).mul(frameLength));
|
|
136
|
+
const values = starts.map(start => readWavetableCycle(sample, p, start.read(), frameLength));
|
|
137
|
+
const a = f64(values[0]).mul(f64(1).sub(frameMix)).add(f64(values[1]).mul(frameMix));
|
|
138
|
+
const z = f64(values[2]).mul(f64(1).sub(frameMix)).add(f64(values[3]).mul(frameMix));
|
|
139
|
+
const missing = sample.length().lt(required);
|
|
140
|
+
const output = select(missing, f32(0), f32(a.mul(f64(1).sub(bandMix)).add(z.mul(bandMix))));
|
|
141
|
+
phase.write(p.add(frequency.div(sampleRate)).frac().mul(phaseScale));
|
|
142
|
+
revision.write(sample.revision());
|
|
143
|
+
return { output, missing };
|
|
144
|
+
} };
|
|
145
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type Node } from '@unworklet/core';
|
|
2
|
+
import type { ResidentSample } from './sample.js';
|
|
3
|
+
/** Construction helper, not a resident/runtime API. Keep the cycle-local fraction
|
|
4
|
+
* separate from the absolute integer offset so tiny phase contributions survive
|
|
5
|
+
* at nonzero frames/bands even against the full finite float32 PCM range. */
|
|
6
|
+
export declare function readWavetableCycle(sample: ResidentSample, phase: Node<'f64'>, start: Node<'i32'>, length: number): Node<'f32'>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { f32, f64, i32 } from '@unworklet/core';
|
|
2
|
+
/** Construction helper, not a resident/runtime API. Keep the cycle-local fraction
|
|
3
|
+
* separate from the absolute integer offset so tiny phase contributions survive
|
|
4
|
+
* at nonzero frames/bands even against the full finite float32 PCM range. */
|
|
5
|
+
export function readWavetableCycle(sample, phase, start, length) {
|
|
6
|
+
const position = phase.mul(length), whole = i32(position.floor()), fraction = position.sub(f64(whole));
|
|
7
|
+
const a = sample.read(f64(start.add(whole)), start, start.add(length), false);
|
|
8
|
+
const b = sample.read(f64(start.add(whole.add(1).mod(length))), start, start.add(length), false);
|
|
9
|
+
// Convex weighting avoids cancellation in a+(b-a)*fraction near the endpoint.
|
|
10
|
+
return f32(f64(a).mul(f64(1).sub(fraction)).add(f64(b).mul(fraction)));
|
|
11
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type Node } from '@unworklet/core';
|
|
2
|
+
import type { ResidentSample } from './sample.js';
|
|
3
|
+
export interface WavetableConfig {
|
|
4
|
+
sampleRate: number;
|
|
5
|
+
/** Shared mono PCM. Concatenate complete cycles, without duplicate end samples. */
|
|
6
|
+
sample: ResidentSample;
|
|
7
|
+
/** Samples per cycle, integer4..4096. */
|
|
8
|
+
frameLength: number;
|
|
9
|
+
/** Fixed number of morph frames, integer1..16; total must fit resident capacity. */
|
|
10
|
+
frameCount: number;
|
|
11
|
+
/** Initial/reset phase in cycles, [0,1). */
|
|
12
|
+
phaseCycles?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface WavetableControls {
|
|
15
|
+
/** Hz clipped to [0,.45*sampleRate], NaN becomes0. */
|
|
16
|
+
frequencyHz: Node<'f32'>;
|
|
17
|
+
/** Fractional frame index, clipped to [0,frameCount−1], NaN becomes0. */
|
|
18
|
+
frame: Node<'f32'>;
|
|
19
|
+
/** Level-sensitive phase reset. A resident revision also restarts phase. */
|
|
20
|
+
reset: Node<'bool'>;
|
|
21
|
+
}
|
|
22
|
+
/** Two periodic linear cycle interpolations and frame morph. No automatic bandlimit. */
|
|
23
|
+
export declare const wavetableSource: import("@unworklet/core").SubgraphDecl<[config: WavetableConfig], {
|
|
24
|
+
tick(c: WavetableControls): {
|
|
25
|
+
output: Node<"f32">;
|
|
26
|
+
missing: Node<"bool">;
|
|
27
|
+
};
|
|
28
|
+
}>;
|
|
29
|
+
export { bandedWavetableSource, prepareWavetableBands, type BandedWavetableConfig, type PreparedWavetableBands, type WavetableBandsOptions } from './wavetable-bands.js';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { defineSubgraph, f32, f64, i32, select, state } from '@unworklet/core';
|
|
2
|
+
import { readWavetableCycle } from './wavetable-read.js';
|
|
3
|
+
function integer(value, low, high, name) {
|
|
4
|
+
if (!Number.isInteger(value) || value < low || value > high)
|
|
5
|
+
throw new RangeError(`${name} must be an integer in [${low},${high}]`);
|
|
6
|
+
}
|
|
7
|
+
function bounded(value, high) {
|
|
8
|
+
return select(value.eq(value), f64(value), f64(0)).clamp(0, high);
|
|
9
|
+
}
|
|
10
|
+
/** Two periodic linear cycle interpolations and frame morph. No automatic bandlimit. */
|
|
11
|
+
export const wavetableSource = defineSubgraph((config) => {
|
|
12
|
+
const { sampleRate, sample, frameLength, frameCount, phaseCycles = 0 } = config;
|
|
13
|
+
if (!Number.isFinite(sampleRate) || sampleRate < 8000 || sampleRate > 192000)
|
|
14
|
+
throw new RangeError('wavetable sampleRate must be in [8000,192000] Hz');
|
|
15
|
+
integer(sample.capacity, 4, 65536, 'resident capacity');
|
|
16
|
+
integer(frameLength, 4, 4096, 'frameLength');
|
|
17
|
+
integer(frameCount, 1, 16, 'frameCount');
|
|
18
|
+
const required = frameLength * frameCount;
|
|
19
|
+
if (required > sample.capacity)
|
|
20
|
+
throw new RangeError('wavetable frames must fit resident capacity');
|
|
21
|
+
if (!Number.isFinite(phaseCycles) || phaseCycles < 0 || phaseCycles >= 1)
|
|
22
|
+
throw new RangeError('phaseCycles must be in [0,1)');
|
|
23
|
+
// Phase accepts f64 configuration, including Number.MIN_VALUE: scaling
|
|
24
|
+
// by 2^1020 keeps even that value above the native 1e-30 flush. Phase
|
|
25
|
+
// is always <1 before scaling, leaving >16x finite headroom. Morph is
|
|
26
|
+
// f32 input and only needs 2^128; neither scaled value touches PCM.
|
|
27
|
+
const scale = 2 ** 1020, frameScale = 2 ** 128;
|
|
28
|
+
const phase = state.f64(phaseCycles * scale).named('scaledPhase'), revision = state.i32(-1).named('revision');
|
|
29
|
+
// Native materialization bounds capture size even when resident.read expands
|
|
30
|
+
// its four wrap/validation paths. These are ordinary same-schema snapshot slots.
|
|
31
|
+
const current = state.f64(phaseCycles * scale).named('scaledCurrentPhase'), scan = state.f64(0).named('scaledFrame');
|
|
32
|
+
const lower = state.i32(0).named('lowerStart'), upper = state.i32(0).named('upperStart');
|
|
33
|
+
return { tick(c) {
|
|
34
|
+
const changed = sample.revision().eq(revision.read()).not();
|
|
35
|
+
current.write(select(c.reset.or(changed), f64(phaseCycles * scale), phase.read()));
|
|
36
|
+
scan.write(bounded(c.frame, frameCount - 1).mul(frameScale));
|
|
37
|
+
const frame = scan.read().div(frameScale), p = current.read().div(scale);
|
|
38
|
+
lower.write(i32(frame.floor()).mul(frameLength));
|
|
39
|
+
upper.write(i32(frame.floor()).add(1).min(frameCount - 1).mul(frameLength));
|
|
40
|
+
const a = readWavetableCycle(sample, p, lower.read(), frameLength);
|
|
41
|
+
const b = readWavetableCycle(sample, p, upper.read(), frameLength);
|
|
42
|
+
const missing = sample.length().lt(required), mix = frame.sub(frame.floor());
|
|
43
|
+
const output = select(missing, f32(0), f32(f64(a).mul(f64(1).sub(mix)).add(f64(b).mul(mix))));
|
|
44
|
+
phase.write(p.add(bounded(c.frequencyHz, .45 * sampleRate).div(sampleRate)).frac().mul(scale));
|
|
45
|
+
revision.write(sample.revision());
|
|
46
|
+
return { output, missing };
|
|
47
|
+
} };
|
|
48
|
+
});
|
|
49
|
+
export { bandedWavetableSource, prepareWavetableBands } from './wavetable-bands.js';
|
|
@@ -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.
|