@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
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Resident take recorder candidate
|
|
2
|
+
|
|
3
|
+
Status: **CANDIDATE / NOT_CLEARED**. Catalog §3 describes sample/live-buffer
|
|
4
|
+
sources, fixed-capacity storage, and read/write competition. This module adds a
|
|
5
|
+
bounded native take writer that composes with the existing sample readers.
|
|
6
|
+
It is not a microphone permission layer, recorder application, rolling history,
|
|
7
|
+
overdub looper, streaming engine, or realtime/listening approval.
|
|
8
|
+
|
|
9
|
+
## Entry and scope
|
|
10
|
+
|
|
11
|
+
Import `residentTakeRecorder`, `ResidentTakeRecorderConfig`,
|
|
12
|
+
`ResidentTakeRecorderControls` and `ResidentTakeRecorder` from
|
|
13
|
+
`@denaudio/den/resident-recorder`. Instantiate once inside a native processor:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
const take = instantiate(residentTakeRecorder,
|
|
17
|
+
{ capacity: 65536, sampleRate: ctx.sampleRate }, { name: 'take' });
|
|
18
|
+
const player = instantiate(samplePlayer,
|
|
19
|
+
{ sampleRate: ctx.sampleRate, sample: take.sample, loop: true },
|
|
20
|
+
{ name: 'player' });
|
|
21
|
+
// In forSample, call the writer once, then the readers, in the intended order.
|
|
22
|
+
const status = take.tick({ input, record, reset });
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Capacity is an integer in 1–65,536 mono source frames, immutable after graph
|
|
26
|
+
construction. `sampleRate` is the incoming processor rate, finite 8–192 kHz.
|
|
27
|
+
The maximum take lasts about 1.486 s at 44.1 kHz, 1.365 s at 48 kHz, or 0.683 s
|
|
28
|
+
at 96 kHz. Changing the configured rate does not resample input. There is no
|
|
29
|
+
hidden monitor output: route the original input separately if monitoring is
|
|
30
|
+
wanted. All recording and reading is graph-native unworklet 0.4.1.
|
|
31
|
+
|
|
32
|
+
## Sample-accurate write contract
|
|
33
|
+
|
|
34
|
+
- Initially length and revision are zero, and reads are missing/silent.
|
|
35
|
+
- `record:true` appends exactly one frame if length is below capacity.
|
|
36
|
+
`record:false` pauses without changing any exposed PCM or length.
|
|
37
|
+
- At capacity, recording stops without wrap or overwrite. Dropping and raising
|
|
38
|
+
record does not start another take. Reset is needed to start at frame zero.
|
|
39
|
+
- `reset:true` has priority, sets length and loaded-prefix boundary to zero and
|
|
40
|
+
suppresses recording. It increments the wrapping i32 revision on every held
|
|
41
|
+
reset tick. A still-high record resumes on the first non-reset tick.
|
|
42
|
+
- `tick` returns `{written,full,length}` for this tick after the operation.
|
|
43
|
+
`written` includes recording a silent or sanitized frame. `full` is true on
|
|
44
|
+
the tick that writes the final frame; no extra input is accepted thereafter.
|
|
45
|
+
- Call `tick` exactly once per incoming sample. Native statement order controls
|
|
46
|
+
visibility: a read before `tick` sees the prior take, and a read after sees
|
|
47
|
+
the completed append/reset. PCM is stored before length is advanced; no
|
|
48
|
+
partially initialized frame becomes exposed. There is no inter-thread reader
|
|
49
|
+
or concurrent host-write contract.
|
|
50
|
+
|
|
51
|
+
There is no circular overwrite, so frame zero stays frame zero until reset/load.
|
|
52
|
+
The valid frame age can be computed from the current length; paused time does
|
|
53
|
+
not add frames or silence. This is a growing finite take, not a continuously
|
|
54
|
+
advancing live history with a moving read/write safety margin.
|
|
55
|
+
|
|
56
|
+
## Existing sample reader compatibility
|
|
57
|
+
|
|
58
|
+
`take.sample` implements the unchanged `ResidentSample` interface. It can be
|
|
59
|
+
passed directly to `samplePlayer`, `multisamplePlayer` and `granularSource`.
|
|
60
|
+
The direct `read` has the existing bounded linear interpolation, slice, looping,
|
|
61
|
+
NaN-position and nonfinite-tap policies; the inaccessible sentinel cannot be
|
|
62
|
+
selected by a reader. Interpolation spans a loaded-prefix/recorded-suffix
|
|
63
|
+
boundary normally.
|
|
64
|
+
|
|
65
|
+
`sample.load(data)` is the existing native Float32Array event-boundary operation:
|
|
66
|
+
it replaces the take, truncates to capacity, sets length to that truncated
|
|
67
|
+
length, and increments revision. Empty data logically unloads. Subsequent
|
|
68
|
+
recording appends after the loaded prefix. If load and record occur in the same
|
|
69
|
+
quantum, native message dispatch installs the prefix before the audio ticks;
|
|
70
|
+
reset at the first tick can still empty it. No decoding, file/device I/O, new
|
|
71
|
+
transport, acknowledgement or serializer is added. Follow the input budgeting,
|
|
72
|
+
preload and callback-copy limitations in [sample.md](sample.md).
|
|
73
|
+
|
|
74
|
+
Appending does not change revision. Existing readers observe the growing length
|
|
75
|
+
on each call/tick rather than a frozen initial bound. In particular, loop period
|
|
76
|
+
grows, and grains whose positions move within the newly valid region can read
|
|
77
|
+
it. A one-shot that already became inactive does not revive just because length
|
|
78
|
+
grows. For stable loop/grain boundaries, stop recording before playback.
|
|
79
|
+
Reset/load changes revision and invalidates existing playback/grains under
|
|
80
|
+
their normal contract; a held player gate is not a fresh trigger. Trigger/gate
|
|
81
|
+
and read/write order remain explicit responsibilities of the composition.
|
|
82
|
+
|
|
83
|
+
## Amplitude and bounded storage
|
|
84
|
+
|
|
85
|
+
Core 0.4.1 scalar buffer stores flush magnitudes below 1e-30. Direct f32 recording
|
|
86
|
+
would therefore discard some representable signal values. This candidate uses:
|
|
87
|
+
|
|
88
|
+
1. `loadedPCM`: C f32 frames, populated only by native event `copyFrom`.
|
|
89
|
+
2. `recordedPCM`: C+1 f64 frames, recording `f64(input) * 2^128`.
|
|
90
|
+
3. `loadedPrefixLength`: the native i32 boundary selecting the loaded prefix or
|
|
91
|
+
recorded suffix. Reset makes it zero; load makes it the new take length.
|
|
92
|
+
|
|
93
|
+
Exact power-of-two scaling keeps every finite nonzero f32 input above the native
|
|
94
|
+
store floor and below f64 overflow. Readers divide by the same factor. It is a
|
|
95
|
+
storage encoding, not audio gain/normalization: decoded finite values retain
|
|
96
|
+
their amplitude, including maximum f32 and the smallest f32 subnormal. Signed
|
|
97
|
+
zero records as positive zero. NaN and ±Infinity record as zero, without relying
|
|
98
|
+
on the output scrubber. Event-loaded invalid values are independently sanitized
|
|
99
|
+
at read taps, matching the original resident sample.
|
|
100
|
+
|
|
101
|
+
The extra recorded frame is an unreachable sentinel. Paused/full/reset ticks
|
|
102
|
+
write only zero there. Native `select` evaluates both branches, so using a
|
|
103
|
+
sentinel avoids accidentally rewriting any exposed sample. Both buffers are
|
|
104
|
+
persistent; buffer storage is exactly `12*C + 8` bytes, 786,440 bytes at maximum
|
|
105
|
+
capacity, plus three i32 persistent states, transient read position, normal
|
|
106
|
+
runtime/IO overhead, and any caller-created ingress ring. Both read branches
|
|
107
|
+
are evaluated; the split costs two bounded memory reads per interpolation tap.
|
|
108
|
+
|
|
109
|
+
Reset and short/empty replacement are **logical invalidation, not secure
|
|
110
|
+
erasure**. Unreachable loaded/recorded tails remain in snapshots until later
|
|
111
|
+
overwritten. No growing allocation or clearing pass occurs per reset. Native
|
|
112
|
+
snapshots include both buffers, length, loaded-prefix boundary and revision;
|
|
113
|
+
the transient read-position cache is excluded. Exact continuation is promised
|
|
114
|
+
only for the same graph, configuration and sample rate. It is not a portable
|
|
115
|
+
audio-file format or an arbitrary live-restore guarantee.
|
|
116
|
+
|
|
117
|
+
## Evidence and limits
|
|
118
|
+
|
|
119
|
+
`tests/resident-recorder.spec.ts` compares every output sample against an
|
|
120
|
+
independent scalar oracle at 44.1/48/96 kHz. Coverage includes pause/full/held
|
|
121
|
+
reset, pre/post-write reads, prefix-to-suffix interpolation, short/empty/oversize
|
|
122
|
+
replacement, capacity one/65,536, f32 extremes/subnormals/nonfinite input,
|
|
123
|
+
inaccessible sentinel and unchanged loaded bytes, growing player/granular
|
|
124
|
+
composition, wrapping revision and exact snapshots.
|
|
125
|
+
|
|
126
|
+
`tests/resident-recorder-packed.test.mjs` installs the real tarball into an
|
|
127
|
+
isolated locked consumer, checks public TypeScript, renders native DSP against
|
|
128
|
+
the independent oracle, verifies same-schema snapshot continuation, and builds
|
|
129
|
+
the actual Vite worklet/WASM at 48 kHz. Manifests retain exact source, tarball,
|
|
130
|
+
consumer-lock, rendered audio and build hashes. The consumer audio is original
|
|
131
|
+
synthetic signal, **CANDIDATE**, without normalization or a limiter.
|
|
132
|
+
|
|
133
|
+
The separate `tests/capture-freeze-browser.test.mjs` requires actual 48 kHz
|
|
134
|
+
worklet recording, deterministic native capture limits, pause/full-stop,
|
|
135
|
+
existing-player readback, prefix/suffix interpolation and native PCM/history
|
|
136
|
+
restoration. It uses synthetic graph input; no microphone or device is opened.
|
|
137
|
+
This browser stage must pass on the exact final head before merge.
|
|
138
|
+
|
|
139
|
+
Offline multirate success, worklet build and actual browser execution are separate
|
|
140
|
+
evidence. There is no device capture, listening approval, realtime deadline,
|
|
141
|
+
seamless take transition, antialiasing, long-form recording or streaming claim.
|
|
142
|
+
Core's non-48-kHz browser loader restriction remains unchanged.
|
|
143
|
+
|
|
144
|
+
Live core 0.4.1 restoration copies DSP state before restoring host AudioParam
|
|
145
|
+
values. Old record/reset/player controls can therefore act on restored state
|
|
146
|
+
before the saved controls arrive. The browser fixture first renders the saved
|
|
147
|
+
record, play, input, limit, position and reset controls, then restores the take.
|
|
148
|
+
The earlier already-full-buffer pass masked the write hazard and is not general
|
|
149
|
+
atomic-restore evidence. Browser playback checks are functional PCM/readback
|
|
150
|
+
checks; phase-aligned quantum continuation is separately tested offline. See the
|
|
151
|
+
[observed frozen-tail restore boundary](freeze-reverb.md#live-browser-restore-ordering).
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Bounded resident WSOLA time stretching
|
|
2
|
+
|
|
3
|
+
Status: **CANDIDATE**, catalog §11. `residentTimeStretch` from
|
|
4
|
+
`@denaudio/den/resident-time-stretch` changes resident mono duration independently
|
|
5
|
+
of grain-local pitch. It is a waveform-similarity overlap-add (WSOLA) variant with
|
|
6
|
+
fixed sparse comparison points. This is not a renamed two-head live pitch delay,
|
|
7
|
+
a phase vocoder, formant preservation or a transparent general-purpose stretcher.
|
|
8
|
+
|
|
9
|
+
## Entry, native ownership and cost
|
|
10
|
+
|
|
11
|
+
Instantiate with `sampleRate: ctx.sampleRate` (integer 8000..192000), an existing
|
|
12
|
+
`ResidentSample`, `hopSamples: 128 | 256` (default 256) and
|
|
13
|
+
`searchFrames: 0 | 8 | 16 | 32 | 64 | 128` (default 64). H is the synthesis hop in
|
|
14
|
+
output frames; the two overlapping source grains each span 2 H output frames.
|
|
15
|
+
S is the search radius in source frames. Neither scales automatically with rate.
|
|
16
|
+
At 48 kHz the default window spans 10.67 ms and search±64 spans±1.33 ms; these are short
|
|
17
|
+
creative windows, especially for bass, speech and complex/polyphonic material.
|
|
18
|
+
|
|
19
|
+
The asset remains bounded to 1..65536 mono f32 source frames with a fixed
|
|
20
|
+
integer source rate 8000..192000 Hz, using the existing native resident ingress. See [sample.md](sample.md)
|
|
21
|
+
for delivery, truncation, revision, RAM and callback-copy caveats. Decode and
|
|
22
|
+
validate PCM outside audio processing, preload with audible playback gated off,
|
|
23
|
+
and establish the delivery boundary before starting. There is no new loader,
|
|
24
|
+
streaming path, JS-rendered audio, external WASM or unworklet modification.
|
|
25
|
+
|
|
26
|
+
Call `tick(controls, everyNSamples)` once per sample inside stride-1 native
|
|
27
|
+
`forSample((i, everyNSamples) => ...)`. This module allocates 64 f64 transient
|
|
28
|
+
comparison values (512 bytes), fixed scalar state and no length-dependent output
|
|
29
|
+
buffer. Resident PCM and ingress allocation are shared, not copied per instance.
|
|
30
|
+
Sixteen maximum ingress slots reserve 4 MiB, plus 256 KiB resident PCM.
|
|
31
|
+
|
|
32
|
+
The graph uses bounded native counted loops, not expanded code per PCM frame or
|
|
33
|
+
search candidate. Every 128 output samples it evaluates 64 reference reads and
|
|
34
|
+
(2 S+1)×64 candidate comparisons; each comparison uses zero-padded linear sampling.
|
|
35
|
+
H256 commits only alternate evaluations. This eager 128 scheduling, including
|
|
36
|
+
when stopped, is deliberate: unworklet 0.4.1's native hop counters are not snapshot
|
|
37
|
+
state, whereas the module's persistent H clock is. Muting/gate-off does not remove
|
|
38
|
+
search cost. S0 still evaluates the zero-offset baseline. Maximum S128 means
|
|
39
|
+
16,448 comparisons per 128-sample quantum; there is no average-cost CPU-saving claim.
|
|
40
|
+
|
|
41
|
+
## Controls, launch and transitions
|
|
42
|
+
|
|
43
|
+
Controls are `{ gate, trigger, reset, durationScale, pitchRatio }`.
|
|
44
|
+
|
|
45
|
+
- Gate rise or true trigger requests a launch at the next global H boundary,
|
|
46
|
+
with 0..H−1 samples of request waiting. Controls latch at actual launch, not at
|
|
47
|
+
the request sample. Gate must remain high. A held trigger restarts every hop.
|
|
48
|
+
- `durationScale` and `pitchRatio` must each be finite f32 in [0.5,2]. Both latch
|
|
49
|
+
for the whole run; edits alone do nothing. Invalid controls reject that launch,
|
|
50
|
+
clear its request and leave any already-running playback intact. There is no
|
|
51
|
+
implicit clamp, smoothing or fallback transposition.
|
|
52
|
+
- A launch resets source/output time to 0. Its first H-sample grain is direct,
|
|
53
|
+
without an artificial fade-in. Later boundaries use overlap. Launch/retrigger
|
|
54
|
+
can click. The resident is read with zero padding on both ends; no loop or
|
|
55
|
+
reverse mode is supplied.
|
|
56
|
+
- Gate-off cancels immediately without release/tail. Reset cancels and silences
|
|
57
|
+
its current sample and clears pending launch, EOF and rejection. Held reset
|
|
58
|
+
stays silent; a still-high gate requests a new launch after release.
|
|
59
|
+
- Reset does not rephase the global hop. A resident revision immediately cancels
|
|
60
|
+
playback and pending launch. A gate already held high does not silently start
|
|
61
|
+
the replacement, although a new trigger in the same sample can request it.
|
|
62
|
+
Empty/unloaded assets are missing/silent.
|
|
63
|
+
|
|
64
|
+
## Exact time, search and sample law
|
|
65
|
+
|
|
66
|
+
Let L be the loaded source length, R the output rate, Rs the fixed source rate,
|
|
67
|
+
D the accepted durationScale and P the accepted pitchRatio. A launch has exactly
|
|
68
|
+
T=ceil(L×R/Rs×D) active output samples, n=0..T−1. The logical source position is
|
|
69
|
+
n×L/T. Thus requested duration rounds upward by less than one output frame, and
|
|
70
|
+
pitch never changes T. The largest permitted T is 3,145,728 output frames.
|
|
71
|
+
Fractional resident source rates are rejected by this module, without changing the residentSample API.
|
|
72
|
+
|
|
73
|
+
Duration is an exact rational ceil for integer rates and the accepted f32 D.
|
|
74
|
+
The implementation splits L×R×(D×2^24) into exact small integer products and
|
|
75
|
+
compares their integer part/remainder to a nearby quotient, avoiding a pre-rounded
|
|
76
|
+
rate quotient or an epsilon adjustment. The independent scalar oracle uses
|
|
77
|
+
BigInt rational arithmetic. The regression L=147 / Rs=44100 / R=48000 has exactly
|
|
78
|
+
80/160/320 samples at D=0.5/1/2; the original pre-rounded-rate calculation wrongly
|
|
79
|
+
produced 81/161/321 and is preserved in the initial review evidence.
|
|
80
|
+
|
|
81
|
+
Grain-local sampling advances q=P×Rs/R source frames per output sample. At each
|
|
82
|
+
later synthesis hop the new nominal grain start is n×L/T, independent of P. The
|
|
83
|
+
previous grain continues from its selected start plus Hq. Alignment compares that
|
|
84
|
+
previous tail with candidate starts nominal+d, where integer d is 0,±1,…,±S.
|
|
85
|
+
Each candidate's score is the sum of squared differences at 64 positions
|
|
86
|
+
j×H/64, j=0..63, using the same q. Baseline d=0 is first, then +1,−1,+2,−2,…;
|
|
87
|
+
strictly smaller cost wins, so exact ties prefer the smallest absolute offset,
|
|
88
|
+
then the positive offset. S0 is ordinary overlap-add without alignment.
|
|
89
|
+
|
|
90
|
+
At local output index k=0..H−1, output is
|
|
91
|
+
(1−k/H)×previousTail[k] + (k/H)×newHead[k]. First-hop output is newHead[k]. Each
|
|
92
|
+
endpoint outside [0,L) is zero, including fractional intervals [−1,0) and [L−1,L).
|
|
93
|
+
Indices are explicitly clamped before resident access; padding is not endpoint
|
|
94
|
+
hold, accidental index wrap or stale PCM reuse. EOF outputs silence exactly from
|
|
95
|
+
n=T onward, with no post-EOF overlap tail. A late grain may encounter padding
|
|
96
|
+
before EOF, so active duration does not imply nonzero PCM at every active sample.
|
|
97
|
+
For the 8192-frame, 1/128-cycles/source-frame cosine fixture at D=2/P=2, a late
|
|
98
|
+
256-frame region peaks near 0.309 instead of the 0.5 interior amplitude. This is
|
|
99
|
+
retained edge attenuation, not corrected by hidden normalization.
|
|
100
|
+
|
|
101
|
+
Interpolation and overlap weights are convex. Peak is bounded by the largest
|
|
102
|
+
absolute finite resident PCM value (within floating rounding), and supported
|
|
103
|
+
constant interiors retain unity gain. There is no loudness/peak normalization,
|
|
104
|
+
limiter, clipping or implicit dry mix. Nonfinite PCM taps become zero through the
|
|
105
|
+
resident contract. Internal exact binary scaling by 2^192 avoids the native tiny
|
|
106
|
+
scalar-store scrub during sample/error accumulation. Even the 64-term score for
|
|
107
|
+
opposite finite f32 extrema is below 2^649, safely finite in f64; this scaling does
|
|
108
|
+
not alter audio gain or relative candidate scores.
|
|
109
|
+
|
|
110
|
+
Return values are `{ output, active, position, ended, pending, rejected, missing,
|
|
111
|
+
selectedOffset }`. `active` refers to the current output sample, including zeros.
|
|
112
|
+
`position` is the current logical n×L/T, held at L after EOF; it is not either
|
|
113
|
+
aligned readhead's position. `ended` becomes true on the first EOF sample and
|
|
114
|
+
stays true until reset/replacement/new launch. Early gate-off does not set EOF.
|
|
115
|
+
`rejected` retains the latest launch result until another attempted launch/reset.
|
|
116
|
+
`selectedOffset` is the last committed hop's integer d. Rejected and selectedOffset
|
|
117
|
+
are diagnostics, not proof that any output was audible.
|
|
118
|
+
|
|
119
|
+
All launch/clock/time/grain/EOF and resident state use native persistent snapshots;
|
|
120
|
+
comparison, score and output scratch are transient. Restoration is bit-identical
|
|
121
|
+
only for the same graph, configuration, resident metadata and rate, at the native
|
|
122
|
+
quantum boundaries. In particular, H256 halfway-hop and pending launches must
|
|
123
|
+
continue exactly after a 128-aligned snapshot. No serializer or migration layer is
|
|
124
|
+
introduced.
|
|
125
|
+
|
|
126
|
+
## Audible and acceptance limits
|
|
127
|
+
|
|
128
|
+
Linear interpolation is not bandlimited. Pitch-up aliases original source
|
|
129
|
+
frequencies above R/(2 P) Hz, or R/(2 P Rs) cycles per source frame.
|
|
130
|
+
Interpolation images/attenuation remain below that limit. All pitch/formants move
|
|
131
|
+
together, and input frequencies above the destination Nyquist can alias even at
|
|
132
|
+
P1 when source/output rates differ.
|
|
133
|
+
|
|
134
|
+
Sparse waveform matching can choose an unsuitable periodic alignment. Its 64
|
|
135
|
+
comparison points do not represent every output sample. Limited S cannot align
|
|
136
|
+
long periods or all components of a polyphonic signal. Linear crossfades can
|
|
137
|
+
cancel, amplitude-modulate and color tones, especially without alignment; grains
|
|
138
|
+
can duplicate, omit or smear transients. Short input and both padded edges receive
|
|
139
|
+
only partial windows, and EOF may truncate a window/transient abruptly. No
|
|
140
|
+
speech/consonant preservation, stereo coherence or artifact-free claim follows.
|
|
141
|
+
Use a separate envelope/crossfade if a click-free articulation is required.
|
|
142
|
+
|
|
143
|
+
The algorithm is an original bounded implementation of the waveform-alignment
|
|
144
|
+
idea described in Driedger and Müller, [A Review of Time-Scale Modification of
|
|
145
|
+
Music Signals](https://www.audiolabs-erlangen.de/content/resources/MIR/00_PCD_AudioLabs/2016_DriedgerMueller_TSMOverview_AppliedSciences_ePrint.pdf),
|
|
146
|
+
2016, §4. Their descriptions explain the method/tradeoffs, not this implementation's
|
|
147
|
+
quality or performance. No third-party implementation was copied.
|
|
148
|
+
|
|
149
|
+
Verification covers independent scalar source-time/search results and independent
|
|
150
|
+
analytic duration/carrier checks at 44.1/48/96 kHz, arbitrary-PCM unity, mismatched
|
|
151
|
+
source/output rates, short and zero-padded boundaries, invalid controls/nonfinite
|
|
152
|
+
PCM, finite extremes/subnormals, launch/reload/reset/EOF and same-schema exact
|
|
153
|
+
snapshot continuation. Packed public import/types, actual native ingress/render,
|
|
154
|
+
fixed memory, graph/WASM cost and candidate audio are separate evidence. Every
|
|
155
|
+
render is CANDIDATE; no approved golden is replaced. There is no human-listening,
|
|
156
|
+
browser/hardware realtime deadline, host integration or full catalog acceptance.
|
|
157
|
+
|
|
158
|
+
Initial max-size feasibility (65536 frames/H256/S128, local Node 24) measured
|
|
159
|
+
296,328 graph bytes, 50,051 WASM bytes, 4,521,984 native bytes and 640 ms compile.
|
|
160
|
+
Twenty unloaded quanta observed 43.3 ms cold and 2.42 ms warm maximum. These include
|
|
161
|
+
an eagerly evaluated search even though the resident was unloaded; they do not
|
|
162
|
+
prove loaded/device performance. The cold quantum greatly exceeds the tested audio
|
|
163
|
+
budget, and 2.42 ms already exceeds the 1.33 ms budget at 96 kHz. Later packed diagnostics
|
|
164
|
+
must be read alongside this retained observation, not substituted for a guarantee.
|
|
165
|
+
|
|
166
|
+
Independent review retained a dominant-frequency counterexample: a 997.13 Hz
|
|
167
|
+
source at 32000 Hz, output 44100 Hz, L=16384, H128/S128 and D=1.75/P=1.25 produces
|
|
168
|
+
an interior Hann-window spectral peak near 1248.1625 Hz instead of the nominal
|
|
169
|
+
1246.4125 Hz, a +1.75 Hz deviation. Sparse, integer-offset waveform alignment can
|
|
170
|
+
shift the dominant output frequency as well as color its amplitude. The separate
|
|
171
|
+
437.3 Hz test's <=0.75 Hz result describes only its measured rate/control matrix;
|
|
172
|
+
it is not a universal tuning tolerance. P specifies the exact local grain read
|
|
173
|
+
slope, not an artifact-free global spectral-peak guarantee.
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
Further independent measurements strengthen these limits. The same 997.13 Hz
|
|
177
|
+
source/control case at 96000 Hz output has a dominant peak near 1249.4125 Hz,
|
|
178
|
+
+3 Hz from the nominal 1246.4125 Hz. The spectral peak is not guaranteed to track
|
|
179
|
+
P to a universal tuning tolerance.
|
|
180
|
+
|
|
181
|
+
For the 48000 Hz, L=8192, period-128 cosine at D=2/P=2 and H256/S64, the intended
|
|
182
|
+
750 Hz Hann-window carrier amplitude over output frames [15360,16128) is about
|
|
183
|
+
0.00145034, versus about 0.499999997 in the interior. The last nonzero output is
|
|
184
|
+
frame 16159 although the active duration ends at 16384. This is padded-edge
|
|
185
|
+
cancellation/distortion and loss of intended-carrier energy, not merely a
|
|
186
|
+
uniform gain reduction. The earlier late-region peak near 0.309 therefore must
|
|
187
|
+
not be read as preserved pitch/tone quality at the edge. No gain compensation
|
|
188
|
+
is applied. Exact logical duration and local read slope do not guarantee
|
|
189
|
+
transparent duration/pitch output, preserved tail content or completed
|
|
190
|
+
catalog time-stretch quality.
|
package/docs/sample.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Resident sample, multisample and granular candidates
|
|
2
|
+
|
|
3
|
+
Status: **CANDIDATE**. These are bounded DSP building blocks for catalog §3.
|
|
4
|
+
No listening approval, bandlimited claim, browser/host delivery proof, streaming,
|
|
5
|
+
asset loader, hardware latency or real-time deadline guarantee is implied.
|
|
6
|
+
|
|
7
|
+
## Public entry and ownership
|
|
8
|
+
|
|
9
|
+
Import `residentSample`, `samplePlayer`, `multisamplePlayer`, `granularSource`
|
|
10
|
+
and their configuration/control types from `@denaudio/den/sample`. Create named
|
|
11
|
+
subgraph instances with native `instantiate`. One resident sample can be shared
|
|
12
|
+
by multiple readers; player/grain state is private to each instance.
|
|
13
|
+
|
|
14
|
+
A resident is **mono**, has a fixed capacity of 1–65,536 f32 frames and a fixed
|
|
15
|
+
source rate in [8,000,192,000] Hz. Its current loaded length is independent of
|
|
16
|
+
capacity. Each player's `sampleRate` is its processor's `ctx.sampleRate`.
|
|
17
|
+
Decoding, channel selection/downmixing and finite-PCM validation belong in the
|
|
18
|
+
caller, outside audio processing. Channel/rate changes require a newly configured
|
|
19
|
+
resident; there is no embedded file format or mutable sample-rate metadata.
|
|
20
|
+
|
|
21
|
+
Use the existing native ingress, for example:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
const sample = instantiate(residentSample,
|
|
25
|
+
{ capacity: 65536, sourceSampleRate: 48000 }, { name: 'sample' });
|
|
26
|
+
const load = event<{ data: Float32Array }>({
|
|
27
|
+
from: 'main', name: 'load', capacity: CAPACITY_16,
|
|
28
|
+
payloadCapacity: sample.capacity * Float32Array.BYTES_PER_ELEMENT,
|
|
29
|
+
});
|
|
30
|
+
load.onReceive(({ data }) => sample.load(data));
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Validate bytes/frames on the sending side. Send at most the declared capacity,
|
|
34
|
+
and handle native transport overflow/rejection. In the verified native offline
|
|
35
|
+
path, a typed-array payload larger than the per-slot budget is silently truncated
|
|
36
|
+
before `load` sees it; `length()` then reflects that truncated input. Do not rely
|
|
37
|
+
on transport rejection to validate a full asset. The DSP `load` itself truncates
|
|
38
|
+
an input longer than the resident to capacity; an empty input unloads. A shorter
|
|
39
|
+
replacement leaves unreachable bytes in the fixed buffer but all readers clamp
|
|
40
|
+
to the new length, so old data cannot leak into playback. The unused tail remains in native snapshots
|
|
41
|
+
until overwritten; replacement/unload is logical invalidation, **not secure erasure**.
|
|
42
|
+
|
|
43
|
+
**Loading is not callback-free.** `Buffer.copyFrom` is a bounded native
|
|
44
|
+
`memory.copy` in the process message-dispatch phase, before that quantum's audio
|
|
45
|
+
samples. Prepare/decode outside the callback, preload with playback gated off,
|
|
46
|
+
and start audible playback only after the delivery boundary is established by
|
|
47
|
+
the host. There is no new loader or acknowledgement protocol here. The
|
|
48
|
+
postMessage fallback can allocate on the audio thread; offline tests do not
|
|
49
|
+
establish live transport safety.
|
|
50
|
+
|
|
51
|
+
Specify `CAPACITY_16` explicitly. Core 0.4.1 allocates the payload budget once per
|
|
52
|
+
ring slot: at the maximum resident size, 16 × 65,536 × 4 = 4 MiB for the payload
|
|
53
|
+
ring, plus 256 KiB PCM and small ring/state overhead. Multiple ingress/resident
|
|
54
|
+
pairs multiply this budget. Sixteen distinct maximum-capacity residents, each
|
|
55
|
+
with its own 16-slot ingress, reserve 64 MiB payload + 4 MiB PCM before ring/state
|
|
56
|
+
overhead; that bank is materially more expensive than 16 zones sharing one resident
|
|
57
|
+
(4 MiB payload + 256 KiB PCM). Simultaneously queued full loads also copy 4 MiB
|
|
58
|
+
into the bank at a boundary and are unsuitable for an unmeasured audible reload.
|
|
59
|
+
The native default ring has 256 slots and would use
|
|
60
|
+
64 MiB payload space for that same capacity. Capacity is not an allocation-free
|
|
61
|
+
loading claim or a recommended live reload size.
|
|
62
|
+
|
|
63
|
+
PCM, loaded length and revision are persistent native snapshot state. The revision
|
|
64
|
+
increments as a wrapping i32 on every load, including empty input. Readers compare
|
|
65
|
+
for inequality, not ordering, so signed/unsigned wrap preserves invalidation.
|
|
66
|
+
The bounded 16-message ring cannot wrap the whole 32-bit revision between adjacent
|
|
67
|
+
audio ticks. Snapshots promise exact continuation only for the **same graph,
|
|
68
|
+
configuration, resident metadata and rate**; they are not a portable sample bank
|
|
69
|
+
format or cross-configuration migration contract.
|
|
70
|
+
|
|
71
|
+
## Playback and slices
|
|
72
|
+
|
|
73
|
+
`samplePlayer({ sampleRate, sample, startFrame?, endFrame?, loop?, releaseFrames? })`
|
|
74
|
+
accepts `tick({ gate, trigger, reset, rate })` and returns
|
|
75
|
+
`{ output, active, position, missing }`.
|
|
76
|
+
|
|
77
|
+
- Slice boundaries are construction-time frame indices `[startFrame,endFrame)`.
|
|
78
|
+
The end intersects the loaded length; an empty intersection is missing/silent.
|
|
79
|
+
- `rate` is a signed ratio clamped to [-16,16]. The increment is
|
|
80
|
+
`rate * sourceSampleRate / sampleRate`. Zero holds; negative starts at `end-1`.
|
|
81
|
+
Rate changes while playing change direction/speed without repositioning.
|
|
82
|
+
- Gate-rise or a true trigger while gate is high starts/restarts. Trigger is
|
|
83
|
+
level-sensitive, so holding it repeats the initial sample. Reset has priority;
|
|
84
|
+
held reset is silent, then a still-high gate restarts on the next tick.
|
|
85
|
+
- Linear interpolation explicitly wraps the second tap at the loop boundary.
|
|
86
|
+
One-shot interpolation holds the final frame until the phase exits `[start,end)`.
|
|
87
|
+
No index relies on unworklet's automatic wrap. Length 0/1 is defined and bounded.
|
|
88
|
+
- Gate-off applies a linear `releaseFrames` ramp; the first off sample subtracts
|
|
89
|
+
`1/releaseFrames`, so release 1 stops immediately. Default 0 stops immediately.
|
|
90
|
+
Natural one-shot exhaustion stops independently. Use an envelope for a richer
|
|
91
|
+
articulation law. `active` describes the current emitted sample, including a
|
|
92
|
+
zero PCM value; it does not predict whether the next sample is active.
|
|
93
|
+
- Replacement invalidates the current playback. A gate held high does not
|
|
94
|
+
silently restart a new asset; a fresh gate-rise/trigger can start it. A load and
|
|
95
|
+
fresh trigger in the same quantum use the newly copied asset.
|
|
96
|
+
- Nonfinite rates use bounded fallback/saturation; NaN means zero speed.
|
|
97
|
+
Nonfinite PCM taps become zero individually, without poisoning reader state.
|
|
98
|
+
Fractional position/control state uses exact binary scaling internally to avoid
|
|
99
|
+
the native scalar-store flush below 1e-30; finite extreme PCM is not reduced to
|
|
100
|
+
a ±1-only assumption. Tiny/subnormal controls are tested against full finite PCM.
|
|
101
|
+
- Public resident `read(position,start,end,loop)` also intersects end with loaded
|
|
102
|
+
length. Non-loop out-of-range positions hold the nearest valid endpoint; it is
|
|
103
|
+
not a zero-padded reader. Direct frame positions saturate to the signed i32 index range, with NaN
|
|
104
|
+
selecting start; start/end are i32 nodes. The player/granular methods maintain
|
|
105
|
+
their own tighter phase/control bounds. Player/grain activity masks enforce their own silence.
|
|
106
|
+
|
|
107
|
+
Loop interpolation smooths fractional lookup but **does not make a discontinuous
|
|
108
|
+
loop seamless**. For an explicit tail/head overlap, use the separate
|
|
109
|
+
[crossfaded loop candidate](loop-crossfade.md), whose effective period is shortened
|
|
110
|
+
from `L` to `L-F`. `samplePlayer` itself is unchanged. Automatic zero-crossing search,
|
|
111
|
+
time-stretching, bandlimited rate conversion and seamless dynamic asset replacement
|
|
112
|
+
are not provided. A high-frequency sine played at double rate demonstrably aliases.
|
|
113
|
+
|
|
114
|
+
## Multisample mapping
|
|
115
|
+
|
|
116
|
+
`multisamplePlayer({ sampleRate, zones, releaseFrames? })` accepts
|
|
117
|
+
`tick({ key, velocity, gate, trigger, reset, rate })` and returns
|
|
118
|
+
`{ output, active, zoneIndex, missing }`.
|
|
119
|
+
|
|
120
|
+
There are 1–16 fixed zones. Each specifies `sample`, integer `keyLow/keyHigh`,
|
|
121
|
+
normalized `velocityLow/velocityHigh`, integer `rootKey`, optional `tuneCents`
|
|
122
|
+
(±2,400), `gain` (±4), and optional slice/loop configuration. All endpoints are
|
|
123
|
+
inclusive; the first matching zone wins overlaps. Inputs clamp to MIDI 0–127 and
|
|
124
|
+
velocity 0–1; fractional keys are supported for transposition inside a zone.
|
|
125
|
+
|
|
126
|
+
Zone, key and velocity latch at note-on. Root-key tuning multiplies the rate by
|
|
127
|
+
`2 ** ((key-rootKey)/12 + tuneCents/1200)`, then the player limits the final signed
|
|
128
|
+
ratio to ±16. Output gain includes latched velocity × zone gain. No match returns
|
|
129
|
+
zone -1/missing/silence. A matching but unloaded sample remains the selected missing
|
|
130
|
+
zone; there is no hidden fallback. A new note resets other zone players, so this is
|
|
131
|
+
a **monophonic selector**, not a polyphonic allocator or release-tail mixer.
|
|
132
|
+
Clipping/limiting is not implicit; zone gain can exceed unity. Preserve f32
|
|
133
|
+
headroom when applying gain: extreme finite PCM multiplied by a gain above one
|
|
134
|
+
can overflow the output type; the raw-resident finite-range proof is not an
|
|
135
|
+
unbounded-gain guarantee.
|
|
136
|
+
|
|
137
|
+
## Granular source
|
|
138
|
+
|
|
139
|
+
`granularSource({ sampleRate, sample, maxGrains, seed?, loop? })` accepts
|
|
140
|
+
`tick({ gate, reset, positionFrames, jitterFrames, rate, durationSeconds, densityHz })`
|
|
141
|
+
and returns `{ output, activeGrains, onset, dropped, missing }`.
|
|
142
|
+
|
|
143
|
+
- Fixed pool of 1–32 grains. First free slot wins; a full pool drops the onset
|
|
144
|
+
instead of stealing a grain or allocating memory. Density is limited to
|
|
145
|
+
0–2,000 Hz. Gate-rise creates an initial grain even at zero density.
|
|
146
|
+
- Position and uniform signed jitter are in source frames. Jittered start clamps
|
|
147
|
+
into the loaded asset. Signed pitch rate and source/host conversion match playback.
|
|
148
|
+
Position, rate and duration latch at onset; later controls affect new grains.
|
|
149
|
+
- Duration rounds to the nearest output frame, bounded to 3 frames–2 seconds.
|
|
150
|
+
The triangular window is `max(0,1-abs(2*age/(duration-1)-1))`, exactly zero at
|
|
151
|
+
both endpoints. This is not a Hann window or a constant-overlap-add guarantee.
|
|
152
|
+
- `seed` is an integer 1–2,147,483,646. Park–Miller 48,271 modulo 2,147,483,647
|
|
153
|
+
advances for every scheduled onset, including a dropped onset. Reset restores
|
|
154
|
+
the seed; snapshot restore preserves it and every grain. Repeatability is for
|
|
155
|
+
identical controls/rate/configuration. The oscillator/noise PRNG is not reused.
|
|
156
|
+
- Scheduling phase is f64. Nominal onset times can round by one output sample at
|
|
157
|
+
nonintegral periods; this candidate is not a host musical clock or event output.
|
|
158
|
+
- `loop:true` wraps each grain within the whole resident. `loop:false` masks
|
|
159
|
+
reads outside the asset to zero. Gate-off stops scheduling while existing
|
|
160
|
+
windows finish; reset or asset replacement cancels all existing grains.
|
|
161
|
+
- Sum is divided by configured `maxGrains`, not active count. This gives a simple
|
|
162
|
+
overlap bound for finite bounded PCM and avoids changing gain as grains appear,
|
|
163
|
+
but sparse pools are correspondingly quieter. It is not loudness normalization.
|
|
164
|
+
|
|
165
|
+
Bounded [take recording](resident-recorder.md), [rolling history](live-buffer.md),
|
|
166
|
+
[live grains with explicit write-age/expiry rules](live-granular.md), and
|
|
167
|
+
[loop crossfades](loop-crossfade.md) are separate public candidates. The live
|
|
168
|
+
history is not a ResidentSample and must not be cast into this reader. Stereo
|
|
169
|
+
grains, broader spectral sources and host streaming remain outside this contract.
|
|
170
|
+
|
|
171
|
+
## Evidence and acceptance
|
|
172
|
+
|
|
173
|
+
The native-ingress probe and `tests/sample.spec.ts` use synthetic original
|
|
174
|
+
impulse/ramp/sine PCM, independent numeric references, 44.1/48/96 kHz playback,
|
|
175
|
+
missing/short/empty replacement, finite controls, exact snapshot continuation,
|
|
176
|
+
zone boundaries, seeded grain onset/window/drop and explicit alias evidence.
|
|
177
|
+
Maximum resident/ring allocation and maximum pool/zone checks are separate from
|
|
178
|
+
a hardware performance claim. Packed-consumer evidence is produced by
|
|
179
|
+
`tests/sample-packed.test.mjs`; its manifests and audio are always CANDIDATE.
|
|
180
|
+
No existing approved audio baseline is replaced.
|
|
181
|
+
|
|
182
|
+
Maximum capacity is a functional offline bound, not a recommended live preset.
|
|
183
|
+
`makeProcessor({ zoneCount: 1, grainCount: 4 })` in the isolated consumer is a
|
|
184
|
+
modest composition for separate browser acceptance; its graph holds the fixture
|
|
185
|
+
clock/gates until native loaded length is nonzero, so delayed delivery cannot
|
|
186
|
+
consume the audition before PCM arrives. The maximum fixture uses
|
|
187
|
+
16 zones and 32 grains. Per-module 1/4/32-grain and 1/16-zone measurements are
|
|
188
|
+
reported separately. The public compile driver cannot inject load messages, so
|
|
189
|
+
these timing diagnostics explicitly start unloaded and must not be presented as
|
|
190
|
+
audible-pool/device performance. The actual loaded offline renders separately
|
|
191
|
+
prove maximum grain activity, PCM, onset/drop and continuation behavior.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Integrated sound audition: numerical headroom
|
|
2
|
+
|
|
3
|
+
Status: **CANDIDATE**. Offline numerical checks do not clear the real-time gate,
|
|
4
|
+
constitute human listening approval, or authorize production deployment.
|
|
5
|
+
|
|
6
|
+
## Scope and reproduction
|
|
7
|
+
|
|
8
|
+
`tests/integration-consumer/sound-matrix-render.mjs` runs in the isolated consumer
|
|
9
|
+
installed from `npm pack`, using public `@denaudio/den/instrument` and
|
|
10
|
+
`@denaudio/den/delay-fx` imports. `tests/integration.test.mjs` invokes it after the
|
|
11
|
+
existing diagnostic renderer, which remains unchanged. No package DSP, public
|
|
12
|
+
sound settings, gains, or normalization are altered by this audition.
|
|
13
|
+
|
|
14
|
+
At 48 kHz it renders all four instruments through diagnostic delay, Chorus, and
|
|
15
|
+
Rhythmic Delay. Each source runs through the FX defaults, dry mix, and maximum
|
|
16
|
+
wet mix with feedback 0.5. The ten source traces produce 90 stereo FX rows:
|
|
17
|
+
|
|
18
|
+
- Every displayed note at MIDI velocity 127, followed by note-off, retrigger
|
|
19
|
+
during release, and verified exact source silence after the final release.
|
|
20
|
+
- The main hold button for every instrument, including each polyphonic chord.
|
|
21
|
+
- Overlapping notes and last-held-note return for the mono instruments.
|
|
22
|
+
- Four coherent, same-pitch voices at **every displayed pitch** for Diagnostic
|
|
23
|
+
and Pad, followed by release. This is stronger than the UI's velocity 100.
|
|
24
|
+
- Pad holds exceed its 0.8 s attack + 1.2 s decay + one complete 8.333... s LFO
|
|
25
|
+
period. This applies separately to the chord and every coherent pitch.
|
|
26
|
+
- Live FX history between register steps and six additional seconds after each
|
|
27
|
+
complete trace, exposing accumulated echoes and release tails.
|
|
28
|
+
|
|
29
|
+
The traces contain 205.848 seconds of source audio. The initial standalone run
|
|
30
|
+
completed in about 93 seconds on the cloud executor. This is test duration, not
|
|
31
|
+
real-time performance evidence.
|
|
32
|
+
|
|
33
|
+
The script writes `integration-sound-matrix.json`, including all settings, MIDI
|
|
34
|
+
events, time windows, source/output peak and RMS, output headroom, release/end-tail
|
|
35
|
+
measurements, scrub/nonfinite/clipping counts, reference errors, and bounds. It
|
|
36
|
+
also writes twelve `sound-matrix-{instrument}-{effect}.wav` files: main-button
|
|
37
|
+
holds at each FX's defaults, unnormalized at Master 1, with SHA-256 hashes.
|
|
38
|
+
These files are listening **candidates**, never expected audio or golden files.
|
|
39
|
+
|
|
40
|
+
## Independent checks and bound
|
|
41
|
+
|
|
42
|
+
Each FX sample is compared with an independent unbounded-timeline delay oracle.
|
|
43
|
+
The oracle uses a direct-form bilinear biquad rather than the production
|
|
44
|
+
circular buffers and state-variable-filter recurrence, and evaluates the
|
|
45
|
+
Chorus modulation and stereo phase separately. Maximum sample error is limited
|
|
46
|
+
to `6e-6`; the observed worst error was `2.1909363568e-6`.
|
|
47
|
+
|
|
48
|
+
Every render must have zero scrubbed samples, zero nonfinite output, and zero
|
|
49
|
+
samples at or above full scale. Dry output must be bit-identical to its source.
|
|
50
|
+
Deliberately clipped, nonfinite, and above-ceiling fixtures verify that the
|
|
51
|
+
numerical guards reject invalid material.
|
|
52
|
+
|
|
53
|
+
The test first enforces explicit per-instrument **source regression ceilings**:
|
|
54
|
+
|
|
55
|
+
| Instrument | Source ceiling | Rationale |
|
|
56
|
+
| --- | ---: | --- |
|
|
57
|
+
| Diagnostic | 0.20001 | Four 0.05-gain voices, small numerical margin |
|
|
58
|
+
| Bass | 0.24 | Twice the 0.12 raw gain allows resonant/transient overshoot |
|
|
59
|
+
| Percussion | 0.16001 | One 0.16-gain voice, small numerical margin |
|
|
60
|
+
| Pad | 0.18001 | Four 0.045-gain voices, small numerical margin |
|
|
61
|
+
|
|
62
|
+
These are independently chosen numerical guards, not a theorem about every
|
|
63
|
+
possible MIDI schedule or every time-varying instrument filter. Bass actually
|
|
64
|
+
reaches 0.12384, demonstrating why raw gain alone is not a sufficient bound.
|
|
65
|
+
|
|
66
|
+
For a recorded source peak M, mix m, and feedback f, the FX bound is:
|
|
67
|
+
|
|
68
|
+
`M * ((1 - m) + m / (1 - f)) * Master + 1e-6`
|
|
69
|
+
|
|
70
|
+
This bound is independent of the rendered FX peak. All three feedback paths
|
|
71
|
+
are flat or fixed Q=0.5 low-pass; their nonnegative impulse responses have unit
|
|
72
|
+
L1 norm at the candidate cutoffs. Interpolated delay, including Chorus's moving
|
|
73
|
+
readheads, cannot increase the supremum. Therefore f <= 0.5 permits at most a
|
|
74
|
+
factor of two. The oracle asserts valid timing and unclipped modulation.
|
|
75
|
+
|
|
76
|
+
With **Master range 0–1, default 1**, the largest fixed source ceiling yields
|
|
77
|
+
an FX ceiling below 0.480001. Every output is also required to stay below the
|
|
78
|
+
separate **0.5 ceiling**, preserving at least 6.02 dB to full scale for tested
|
|
79
|
+
material. This explicitly replaces reliance on the prior diagnostic-only 0.8
|
|
80
|
+
limit. No limiter or normalization is inserted.
|
|
81
|
+
|
|
82
|
+
## Measured maxima
|
|
83
|
+
|
|
84
|
+
Maximum absolute sample across all scenarios and controls at Master 1:
|
|
85
|
+
|
|
86
|
+
| Instrument | Raw source | Diagnostic delay | Chorus | Rhythmic Delay |
|
|
87
|
+
| --- | ---: | ---: | ---: | ---: |
|
|
88
|
+
| Diagnostic | 0.187219307 | 0.276219070 | 0.368202865 | 0.326295972 |
|
|
89
|
+
| Bass | 0.123841561 | 0.163044527 | 0.205227017 | 0.176587388 |
|
|
90
|
+
| Percussion | 0.158914849 | 0.213010132 | 0.240000606 | 0.219704628 |
|
|
91
|
+
| Pad | 0.168394074 | 0.208122611 | 0.312585205 | 0.228740290 |
|
|
92
|
+
|
|
93
|
+
All 90 rows passed. The worst output, **0.368202865**, is Diagnostic → Chorus,
|
|
94
|
+
four coherent voices across the displayed register, wet mix 1 and feedback 0.5.
|
|
95
|
+
It leaves **8.678 dB** of measured headroom. Every render had zero scrubbed,
|
|
96
|
+
nonfinite, or clipped samples.
|
|
97
|
+
|
|
98
|
+
Master 2 would not clip these particular recorded samples, but would exceed the
|
|
99
|
+
chosen 0.5 regression ceiling and surrender 6.02 dB of output margin. The
|
|
100
|
+
integrated candidate therefore keeps Master 0–1 without changing public sound
|
|
101
|
+
parameters. The matrix does not establish a universal arbitrary-MIDI safety
|
|
102
|
+
proof, real-time capacity, device playback behavior, or physical listening
|
|
103
|
+
approval; those remain separate checks.
|