@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,163 @@
|
|
|
1
|
+
# Rolling live sample buffer candidate
|
|
2
|
+
|
|
3
|
+
Status: **CANDIDATE / NOT_CLEARED**. Catalog §3 calls out fixed-capacity live
|
|
4
|
+
history, its age and read/write competition. This module provides that native
|
|
5
|
+
storage/read contract. It adds no microphone/device capture, host transport,
|
|
6
|
+
streaming, overdub, sample loader, grain scheduler, normalization or limiter.
|
|
7
|
+
Audio and builds produced by its tests are not listening-approved goldens.
|
|
8
|
+
|
|
9
|
+
## Entry and coordinates
|
|
10
|
+
|
|
11
|
+
Import `liveSampleBuffer`, `LiveSampleBufferConfig`, `LiveSampleBufferControls`,
|
|
12
|
+
`LiveSampleBufferRead`, `LiveSampleBufferStatus` and `LiveSampleBuffer` from
|
|
13
|
+
`@denaudio/den/live-buffer`. Instantiate once in a native processor:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
const history = instantiate(liveSampleBuffer,
|
|
17
|
+
{ capacity: 65536, sampleRate: ctx.sampleRate }, { name: 'history' });
|
|
18
|
+
// Within forSample, choose the order deliberately:
|
|
19
|
+
const previous = history.readAge(f64(0));
|
|
20
|
+
const status = history.tick({ input, record, reset });
|
|
21
|
+
const current = history.readAge(f64(0));
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Capacity is an immutable integer in 1–65,536 mono frames. `sampleRate` is the
|
|
25
|
+
incoming processor rate in [8,000,192,000] Hz, without resampling. At capacity
|
|
26
|
+
65,536, continuous recording retains approximately 1.486 s at 44.1 kHz, 1.365 s
|
|
27
|
+
at 48 kHz or 0.683 s at 96 kHz. Rate conversion, stereo pairing and routing are
|
|
28
|
+
the caller's composition. There is no automatic input-monitor output.
|
|
29
|
+
|
|
30
|
+
`readAge(ageFrames: Node<'f64'>)` returns `{output,available}`. Ages count
|
|
31
|
+
**accepted writes**, not elapsed processor frames or wall-clock time:
|
|
32
|
+
|
|
33
|
+
- Age 0 is the newest accepted write at this call's statement position; age 1
|
|
34
|
+
is the write immediately before it. Fractional age interpolates toward older
|
|
35
|
+
writes. For history [10,20,30] in chronological order, age .25 returns 27.5.
|
|
36
|
+
- A finite age in inclusive [0,length−1] is available. At an integer age, the
|
|
37
|
+
exact recorded frame is returned. At the oldest valid integer, the older tap
|
|
38
|
+
is held at that frame. Capacity one therefore supports only age 0.
|
|
39
|
+
- Negative ages, NaN, infinities, unfilled ages and ages beyond retained history
|
|
40
|
+
return zero with `available:false`. No endpoint clamping, zero-padded blend,
|
|
41
|
+
extrapolation or wrap from oldest to newest is applied to unavailable ages.
|
|
42
|
+
- The circular storage address wraps, but the age coordinate does not. An age
|
|
43
|
+
just above length−1 is unavailable even if stale bytes exist at that address.
|
|
44
|
+
Fractional reads never interpolate into overwritten or unfilled history.
|
|
45
|
+
- Pausing freezes all ages and bytes. It does not insert silent frames. A fixed
|
|
46
|
+
age is a moving delay tap while recording, then a frozen sample while paused.
|
|
47
|
+
After another write, a surviving sample's age increases by one; at capacity,
|
|
48
|
+
the former oldest sample expires.
|
|
49
|
+
|
|
50
|
+
This interface intentionally is **not `ResidentSample`**. It has no `load`,
|
|
51
|
+
chronological `read`, or `sourceSampleRate` member, and must not be passed to
|
|
52
|
+
existing `samplePlayer`, `multisamplePlayer` or `granularSource` through a type
|
|
53
|
+
cast. Those readers interpret stable asset positions; rotating chronological
|
|
54
|
+
indexes would silently change an active grain's meaning. Stable captured takes
|
|
55
|
+
remain the separate `residentTakeRecorder` contract. The separate
|
|
56
|
+
[`liveGranularSource`](live-granular.md) reader explicitly tracks accepted-write
|
|
57
|
+
motion and expiry, with writer-before-reader order and a shared reset.
|
|
58
|
+
|
|
59
|
+
## Writer, ordering and revision
|
|
60
|
+
|
|
61
|
+
Call `tick({input,record,reset})` exactly once per incoming sample:
|
|
62
|
+
|
|
63
|
+
- Initially length, head and revision are zero; every read is unavailable.
|
|
64
|
+
- When record is true and reset false, accept one input frame. Before capacity,
|
|
65
|
+
length grows by one. At capacity, length stays fixed and the oldest frame is
|
|
66
|
+
overwritten. A false record pauses without modifying exposed history.
|
|
67
|
+
- Reset is level-sensitive and wins over record. It sets head and length to
|
|
68
|
+
zero and accepts no input. A held reset stays empty. Still-high record resumes
|
|
69
|
+
on the first non-reset tick, beginning a new history at frame zero.
|
|
70
|
+
- The returned `{written,full,length}` describes the operation's result.
|
|
71
|
+
`written` includes accepting zero or sanitizing invalid input to zero; `full`
|
|
72
|
+
becomes true on the write that first fills capacity and remains true through
|
|
73
|
+
later overwrites or pauses until reset.
|
|
74
|
+
- Native statement order is the visibility boundary. A read before `tick` sees
|
|
75
|
+
the previous history, including before reset; a read afterward sees the
|
|
76
|
+
complete write or empty reset. PCM is written before head and length advance.
|
|
77
|
+
Calling several readers between writes is supported. This is not a concurrent
|
|
78
|
+
host writer or cross-thread consistency contract.
|
|
79
|
+
- `length()` returns the current retained count. `revision()` is a wrapping i32
|
|
80
|
+
mutation serial incremented once for each accepted write or reset tick.
|
|
81
|
+
Pause leaves it unchanged. Reset+record increments once, not twice. Compare
|
|
82
|
+
revisions for equality/inequality, never numeric ordering; 2^32 mutations can
|
|
83
|
+
revisit the same value. Revision is neither an absolute timestamp nor a stable
|
|
84
|
+
frame identity, and no compatibility with resident asset revisions is implied.
|
|
85
|
+
|
|
86
|
+
Read-before-write age 0 is the last accepted frame, not the current input.
|
|
87
|
+
Read-after-write age 0 is current input when accepted, including at ring wrap.
|
|
88
|
+
This order matters for zero-delay and feedback composition. No implicit delay,
|
|
89
|
+
feedback loop, read/write safety margin or click-free transition is supplied.
|
|
90
|
+
|
|
91
|
+
## Amplitude, storage and snapshots
|
|
92
|
+
|
|
93
|
+
All finite f32 input is supported, including values beyond ±1, full f32 extrema
|
|
94
|
+
and subnormals. NaN and ±Infinity are recorded as zero, without depending on the
|
|
95
|
+
output scrubber. Input signed zero records as positive zero. Linear reads use
|
|
96
|
+
f64 intermediate arithmetic, so opposite full-range taps do not overflow their
|
|
97
|
+
difference. Interpolation is not anti-alias filtering; rapid age changes can
|
|
98
|
+
click or alias, and downstream gain or feedback can overflow independently.
|
|
99
|
+
|
|
100
|
+
Core 0.4.1 scalar stores flush magnitudes below 1e-30. The native persistent
|
|
101
|
+
buffer therefore stores `f64(input) * 2^128`, decoding by the same exact binary
|
|
102
|
+
factor. This is a storage encoding, not signal gain or normalization. Every
|
|
103
|
+
finite nonzero f32 is above the store floor and far below f64 overflow when
|
|
104
|
+
encoded. The transient age cache is likewise scaled by 2^192 to retain tiny
|
|
105
|
+
fractions that can make a representable f32 difference against full-range PCM.
|
|
106
|
+
|
|
107
|
+
Storage is one fixed C+1 f64 buffer: exactly `8*(C+1)` bytes, or 524,296 bytes at
|
|
108
|
+
capacity 65,536, plus three persistent i32 states (head/length/revision), a
|
|
109
|
+
transient f64 read cache and normal runtime/IO overhead. The extra cell is an
|
|
110
|
+
unreachable sentinel. Paused/reset ticks write zero only there because native
|
|
111
|
+
`select` evaluates both branches. Processing allocates no growing history or
|
|
112
|
+
new per-sample buffers; writes and each read use fixed bounded work.
|
|
113
|
+
|
|
114
|
+
Reset is **logical invalidation, not secure erasure**. Old history remains in
|
|
115
|
+
snapshot bytes until overwritten but cannot be read through `readAge`. Snapshot
|
|
116
|
+
state includes the buffer, head, length and revision; the read cache is excluded.
|
|
117
|
+
Exact continuation applies only to the same graph, configuration and sample
|
|
118
|
+
rate with the same subsequent input/control sequence. No new serializer or
|
|
119
|
+
cross-configuration migration is provided. Core 0.4.1 live state restore and
|
|
120
|
+
host AudioParam restore are not atomic; this module does not fix that boundary.
|
|
121
|
+
Establish safe host controls before restoring a live writing graph. Offline
|
|
122
|
+
continuation evidence is distinct from an arbitrary live-control restore claim.
|
|
123
|
+
|
|
124
|
+
## Verification and limits
|
|
125
|
+
|
|
126
|
+
`tests/live-buffer.spec.ts` compares all pre/post-read, availability and writer
|
|
127
|
+
status samples to an independent chronological-array reference at 44.1/48/96
|
|
128
|
+
kHz. It covers capacity one/two/17, every physical wrap position, fractional ages
|
|
129
|
+
adjacent to newest/oldest retained samples, invalid/unfilled/expired reads,
|
|
130
|
+
pause, held reset, tiny/full-range PCM, tiny f64 ages, revision wrap, snapshots
|
|
131
|
+
while partial/full/paused/reset, stale-byte exclusion, and maximum fixed storage.
|
|
132
|
+
The reference does not use a circular buffer or production addressing helpers.
|
|
133
|
+
|
|
134
|
+
`tests/live-buffer-packed.test.mjs` installs the actual tarball into a locked
|
|
135
|
+
isolated consumer, checks the public types, renders all-sample references and
|
|
136
|
+
same-schema continuation at three rates, then builds actual Vite worklet/WASM
|
|
137
|
+
assets. Its 65,536-frame memory diagnostic actively records through two wraps,
|
|
138
|
+
checks the oldest-age output and confirms constant native memory. Source,
|
|
139
|
+
tarball, lock, audio and build hashes are retained with failure phases.
|
|
140
|
+
|
|
141
|
+
These are bounded offline/build checks, not browser execution, device capture,
|
|
142
|
+
real-time deadline or listening approval. There is no streaming, long-form
|
|
143
|
+
recording, live granular playback, overdub or fixed-latency host integration
|
|
144
|
+
claim. Audio candidates preserve raw gain; no normalization or limiter masks
|
|
145
|
+
their output.
|
|
146
|
+
|
|
147
|
+
## Small actual-browser composition
|
|
148
|
+
|
|
149
|
+
The separate `tests/live-buffer-browser.test.mjs` requires an actual 48 kHz
|
|
150
|
+
worklet. A native fixture counter gates exactly 3, then 20, accepted writes into
|
|
151
|
+
capacity 8, independently of host timer delays. This counter is test composition,
|
|
152
|
+
not a capacity stop in the rolling-buffer API. An exact binary ramp exposes both
|
|
153
|
+
wraps, every retained sample, fractional/expired reads, pause and reset/resume.
|
|
154
|
+
Native accumulated pre/post-read sums verify statement order during the brief
|
|
155
|
+
writing intervals rather than inspecting only a later silent/paused window.
|
|
156
|
+
|
|
157
|
+
For in-place state restoration, the fixture saves a full wrapped positive
|
|
158
|
+
history and replaces it with a short negative one. It first renders recording
|
|
159
|
+
off, then renders all saved controls and proves the negative mutation remains.
|
|
160
|
+
Restore must recover the original PCM, length, cursor-dependent continuation,
|
|
161
|
+
revision and accumulated sums; every retained age is read back. No microphone,
|
|
162
|
+
host transport, arbitrary live-control atomicity or hardware continuity claim
|
|
163
|
+
is involved. Three-rate native chronological references precede browser execution.
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Live history granular reader candidate
|
|
2
|
+
|
|
3
|
+
Status: **CANDIDATE / NOT_CLEARED**. This bounded catalog §3 reader consumes the
|
|
4
|
+
native rolling history from `@denaudio/den/live-buffer`. It supplies a fixed grain
|
|
5
|
+
pool and windows, without an input device, loader, density scheduler, random
|
|
6
|
+
jitter, host transport or streaming contract. Existing resident sample readers
|
|
7
|
+
remain separate; do not cast rolling history to `ResidentSample`.
|
|
8
|
+
|
|
9
|
+
## Public composition and statement order
|
|
10
|
+
|
|
11
|
+
Import `liveGranularSource`, `LiveGranularConfig`, `LiveGranularControls`,
|
|
12
|
+
`LiveGranularResult` and `LiveGranularSource` from `@denaudio/den/live-granular`.
|
|
13
|
+
Construction accepts `{history: LiveSampleBuffer, maxGrains: number}`. `maxGrains`
|
|
14
|
+
is an integer in 1–8; the history has fixed capacity 1–65,536 and a finite processor
|
|
15
|
+
sample rate in [8,000,192,000] Hz. The reader adds no PCM buffer. Instantiate once:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
const history = instantiate(liveSampleBuffer,
|
|
19
|
+
{ capacity: 65536, sampleRate: ctx.sampleRate }, { name: 'history' });
|
|
20
|
+
const grains = instantiate(liveGranularSource,
|
|
21
|
+
{ history, maxGrains: 8 }, { name: 'grains' });
|
|
22
|
+
// Within every stride-1 forSample iteration:
|
|
23
|
+
const status = history.tick({ input, record, reset });
|
|
24
|
+
const result = grains.tick({
|
|
25
|
+
written: status.written, reset, trigger,
|
|
26
|
+
ageFrames, rate, durationSeconds,
|
|
27
|
+
});
|
|
28
|
+
output.write(result.output);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Call the writer exactly once, then each reader exactly once per sample. Pass the
|
|
32
|
+
writer's actual `status.written` and the **same reset signal**. Several readers can
|
|
33
|
+
share that result; no intervening write/reset is allowed. `record` is not a
|
|
34
|
+
substitute for `written`: reset suppresses a write, while an accepted invalid
|
|
35
|
+
input still writes sanitized zero. The history sample rate must match the
|
|
36
|
+
processor. No source-rate conversion is performed. Skipping calls, mismatching
|
|
37
|
+
reset, or supplying a fabricated write status is unsupported.
|
|
38
|
+
|
|
39
|
+
## Launch controls and pool policy
|
|
40
|
+
|
|
41
|
+
`trigger` is level sensitive: each true non-reset sample requests one new grain.
|
|
42
|
+
A held trigger requests every sample. No gate, automatic onset, density clock or
|
|
43
|
+
queue is implied. The following f32 controls are latched only on successful launch:
|
|
44
|
+
|
|
45
|
+
- `ageFrames`: finite accepted-write age in [0,capacity−1], also currently within
|
|
46
|
+
[0,history.length()−1]. Zero means the newest frame after this sample's write.
|
|
47
|
+
- `rate`: finite signed history frames per output sample in [−16,16]. Positive
|
|
48
|
+
moves toward newer source material; negative moves toward older material.
|
|
49
|
+
- `durationSeconds`: finite [0,2]. Duration is
|
|
50
|
+
`clamp(floor(durationSeconds * history.sampleRate + .5),3,round(2*sampleRate))`
|
|
51
|
+
output samples. Zero is valid and produces a three-frame grain; negative,
|
|
52
|
+
NaN and infinite values are rejected.
|
|
53
|
+
|
|
54
|
+
Invalid controls or an unavailable onset set `rejected:true`. They do not clamp
|
|
55
|
+
to a source endpoint, steal a slot, or alter current grains. Internal eager
|
|
56
|
+
branches use safe operands before integer conversion, including on rejected
|
|
57
|
+
NaN/infinite requests. Valid requests use the first free slot in stable index
|
|
58
|
+
order. A full pool sets `dropped:true`; no stealing, restart or delayed retry
|
|
59
|
+
occurs. `launched` means allocation succeeded. `dropped` and `rejected` are mutually
|
|
60
|
+
exclusive; all three are false without a request or during reset.
|
|
61
|
+
|
|
62
|
+
An existing unavailable grain is terminated before allocation, so its slot is
|
|
63
|
+
available for a new grain on that same sample. Changing age/rate/duration while a
|
|
64
|
+
grain runs only affects later requests. There is no implicit output gain control.
|
|
65
|
+
|
|
66
|
+
## Accepted writes, movement and expiry
|
|
67
|
+
|
|
68
|
+
Let `a[n]` be a grain's read age at processor sample n. At launch it is exactly
|
|
69
|
+
`ageFrames`, measured **after the current write**, without adding that write again.
|
|
70
|
+
For a surviving grain on the next sample:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
a[n+1] = a[n] + ((written[n+1] ? 1 : 0) - latchedRate)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The increment is formed before addition. Continuous writing at rate 1 therefore
|
|
77
|
+
preserves even a tiny representable onset fraction; computing `(tinyAge+1)-1`
|
|
78
|
+
would lose it. At rate 0, the grain stays at the same source identity, whose age
|
|
79
|
+
increases with accepted writes until overwritten. Under continuous writing, rate
|
|
80
|
+
1 holds a fixed delay age. Pausing freezes history, **not** the grain clock or
|
|
81
|
+
source movement: a positive-rate grain moves toward the newest retained edge and
|
|
82
|
+
may expire. Resuming recording cannot revive a terminated grain.
|
|
83
|
+
|
|
84
|
+
Every read must be in the inclusive interval [0,currentLength−1]. The shared
|
|
85
|
+
reader interpolates toward older frames and holds the oldest valid integer tap.
|
|
86
|
+
There is no implicit newest/oldest safety margin, extrapolation, zero-padded blend
|
|
87
|
+
or wrap of the age coordinate. An out-of-range read immediately terminates that
|
|
88
|
+
grain and increments `expiredGrains`; it does not clamp or resume later. A valid
|
|
89
|
+
launch at age 0 while paused, with positive rate, can have only its silent onset
|
|
90
|
+
and expire next sample. A valid launch does not promise a full audible window.
|
|
91
|
+
|
|
92
|
+
Natural completion wins on the sample after the final window endpoint: a grain
|
|
93
|
+
whose duration ended is already inactive and is not reported as expired. Reset
|
|
94
|
+
has highest priority, immediately silences and clears all grain state, and zeros
|
|
95
|
+
all result flags/counts. It must also reset the shared writer. A held reset keeps
|
|
96
|
+
the graph empty; a still-high trigger requests again on the first non-reset sample.
|
|
97
|
+
|
|
98
|
+
## Windows, gain and finite precision
|
|
99
|
+
|
|
100
|
+
A grain of N output samples has indices k=0…N−1 and triangular weight
|
|
101
|
+
`max(0,1-abs(2*k/(N-1)-1))`. Both endpoints are exactly zero. Odd lengths reach 1;
|
|
102
|
+
even lengths have a smaller peak. `activeGrains` counts valid reads on the current
|
|
103
|
+
sample, including the zero-weight endpoints and successful launches.
|
|
104
|
+
|
|
105
|
+
Each linearly interpolated f32 history read is weighted and summed in f64. The sum
|
|
106
|
+
is divided by **fixed maxGrains**, then converted once to f32. This is fixed pool
|
|
107
|
+
headroom, not active-count normalization, content normalization or a limiter. A
|
|
108
|
+
single grain in an eight-slot pool is attenuated by eight. Magnitude stays within
|
|
109
|
+
the largest active read magnitude, subject to rounding. Finite PCM above unity,
|
|
110
|
+
f32 extrema and subnormals remain supported; the writer sanitizes NaN/infinite
|
|
111
|
+
input to zero. No clipping masks the candidate audio.
|
|
112
|
+
|
|
113
|
+
Cursor and latched-rate stores use exact binary scaling by 2^192 to avoid core
|
|
114
|
+
0.4.1's 1e−30 scalar-store floor. The scale is storage encoding, not gain. It does
|
|
115
|
+
not increase f64 precision: movement smaller than a large cursor's representable
|
|
116
|
+
spacing may round away. Linear interpolation is not anti-alias filtering. Window
|
|
117
|
+
cancellation, read-edge expiry, pauses and reset can change sound or truncate
|
|
118
|
+
transients. This is not pitch-perfect, time-transparent or streaming-continuity
|
|
119
|
+
processing, and there is no real-time/device/listening clearance.
|
|
120
|
+
|
|
121
|
+
## State and verification
|
|
122
|
+
|
|
123
|
+
Each slot has five persistent scalars: active bool, window index i32, duration
|
|
124
|
+
i32, scaled read age f64 and scaled rate f64. The first-free allocation cache is
|
|
125
|
+
transient. The history retains its own fixed buffer/head/length/revision and
|
|
126
|
+
transient read cache. Work and storage are bounded by capacity and maxGrains;
|
|
127
|
+
processing creates no growing history or per-sample buffers.
|
|
128
|
+
|
|
129
|
+
Snapshots must include the writer **and every reader sharing it** together.
|
|
130
|
+
Continuation is exact only for the same graph/configuration/sample rate and
|
|
131
|
+
identical subsequent inputs/controls. Partial-graph restore is unsupported.
|
|
132
|
+
Native live-state restore and AudioParam restore are non-atomic; establish safe
|
|
133
|
+
host controls before restoring. The writer's reset is logical invalidation, not
|
|
134
|
+
secure PCM erasure. These constraints are unchanged by this reader.
|
|
135
|
+
|
|
136
|
+
`tests/live-granular.spec.ts` uses an independent chronological array plus
|
|
137
|
+
absolute rational source positions. It avoids both the ring addressing and the
|
|
138
|
+
production age recurrence; exact rational positions retain tiny fractions while
|
|
139
|
+
accepted-write and playback movements cancel. Evidence covers 44.1/48/96 kHz,
|
|
140
|
+
variable accepted writes, paused/resumed recording, first-free saturation,
|
|
141
|
+
same-sample expiry/relaunch, zero endpoint counts, natural completion, latching,
|
|
142
|
+
invalid eager operands, wrap/read margins, full finite/tiny PCM, snapshots,
|
|
143
|
+
capacity one and maximum capacity with eight slots. The maximum fixture remains
|
|
144
|
+
active across ring wrap and records through two wraps.
|
|
145
|
+
|
|
146
|
+
`tests/live-granular-packed.test.mjs` packages the actual public subpath, installs
|
|
147
|
+
an isolated consumer with pinned core 0.4.1, checks public types, renders the
|
|
148
|
+
independent three-rate reference and same-schema continuation, and builds actual
|
|
149
|
+
Vite worklet/WASM assets. Source/tarball/lock/audio/build hashes and failure phases
|
|
150
|
+
are retained. The runner defaults to a fresh `npm ci` consumer. An explicitly
|
|
151
|
+
approved local-only `DEN_LIVE_GRANULAR_REUSE_LOCKED_CONSUMER` path instead links
|
|
152
|
+
unchanged matching locked dependencies and extracts only den from the real new
|
|
153
|
+
tarball; it rejects this mode in CI, archives and verifies the original package,
|
|
154
|
+
tarball and lock, and records reuse honestly in provenance. These are offline/build candidates, not human-approved goldens or
|
|
155
|
+
browser/device/real-time evidence. A separately verified browser composition may
|
|
156
|
+
add bounded browser evidence without changing that limit.
|
|
157
|
+
|
|
158
|
+
## Small actual-browser composition
|
|
159
|
+
|
|
160
|
+
`tests/live-granular-browser.test.mjs` uses a muted 48 kHz graph with one grain
|
|
161
|
+
and a 64-frame history. Native counters bound the writer and a three-sample
|
|
162
|
+
request burst, so a delayed host callback cannot change the expected fill or
|
|
163
|
+
drop counts. A paused original ramp supplies a known constant source identity.
|
|
164
|
+
Every observed output sample is compared to its independent triangle-window
|
|
165
|
+
value using a native frame/launch counter, rather than fitting output RMS.
|
|
166
|
+
Changing onset controls while that grain runs must preserve its latched motion
|
|
167
|
+
and duration.
|
|
168
|
+
|
|
169
|
+
The fixture saves a noninitial positive grain, clears the shared graph, and
|
|
170
|
+
creates a different negative history/grain. It renders recording and triggering
|
|
171
|
+
off with every saved control before restoring the entire graph; the negative
|
|
172
|
+
mutation must still be audible in the muted observation path until restore.
|
|
173
|
+
The recovered positive history and window phase must match the sample oracle.
|
|
174
|
+
Resumed writes then overwrite the original source identity and must permanently
|
|
175
|
+
expire the grain. Empty-history rejection and held reset remain separate checks.
|
|
176
|
+
An independent chronological/source-identity oracle exercises the composition
|
|
177
|
+
at three rates before the actual browser gate. This is bounded native behavior,
|
|
178
|
+
not device capture, scheduling infrastructure, runtime clearance or an acoustic
|
|
179
|
+
listening judgment.
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# Lookahead limiter and three-band dynamics candidates
|
|
2
|
+
|
|
3
|
+
Status: **CANDIDATE**. These additive modules extend catalog section 9 with a
|
|
4
|
+
bounded sample-peak lookahead limiter and a phase-compensated three-band
|
|
5
|
+
composition. They reuse unworklet 0.4.1 and the existing reviewed peak follower,
|
|
6
|
+
LR4 crossover and dynamics. Generated audio is not a golden or human-approved
|
|
7
|
+
sound. Offline correctness is not browser or hardware real-time clearance.
|
|
8
|
+
|
|
9
|
+
## Measured real-time boundary
|
|
10
|
+
|
|
11
|
+
The first clean packed proof (`a2f2b0e`, 2026-10-05) measured the full
|
|
12
|
+
17-output three-band fixture at 44.1/48/96 kHz. It exceeded the local 128-frame
|
|
13
|
+
quantum budget in **28/512, 66/512 and 221/512 blocks**, respectively. P99 times
|
|
14
|
+
were 4.59/5.01/4.40 ms. At 96 kHz its median was 1.277 ms against a 1.333 ms
|
|
15
|
+
quantum. **This multiband graph is not real-time cleared.** The maximum-2048-sample
|
|
16
|
+
limiter had no budget exceedances in the same local diagnostic, which also does
|
|
17
|
+
not establish browser or device clearance.
|
|
18
|
+
|
|
19
|
+
These are local Node-driver wall times after 128 warm-up blocks, repeating the
|
|
20
|
+
first 128-frame input/control block, excluding audio I/O copies and browser
|
|
21
|
+
scheduling. They are useful negative evidence, not a universal capacity limit or
|
|
22
|
+
an acceptance benchmark. Stage materialization reduced the separate mixed-mode
|
|
23
|
+
comparison fixture from 6,752,682 to 153,584 WASM bytes while preserving all
|
|
24
|
+
17 output channels byte-for-byte at three rates. That compile-size improvement
|
|
25
|
+
does not remove the measured runtime boundary.
|
|
26
|
+
|
|
27
|
+
## Entry contracts
|
|
28
|
+
|
|
29
|
+
- `@denaudio/den/lookahead-limiter`: `lookaheadLimiter`,
|
|
30
|
+
`LookaheadLimiterConfig`, `LookaheadLimiterControls`.
|
|
31
|
+
- `@denaudio/den/multiband-dynamics`: `multibandDynamics`,
|
|
32
|
+
`MultibandDynamicsConfig`, `MultibandDynamicsControls`,
|
|
33
|
+
`MultibandDynamicsBandConfig`, `MultibandDynamicsBandControls`.
|
|
34
|
+
|
|
35
|
+
Instantiate with stable unique names and call each instance exactly once per
|
|
36
|
+
sample. `sampleRate` is an integer from 8000 through 192000 Hz and must equal the
|
|
37
|
+
enclosing processor's compile rate. All runtime numeric inputs are native f32
|
|
38
|
+
Nodes and reset is a native boolean Node. There is no alternate runtime,
|
|
39
|
+
parameter, snapshot or routing framework. State restoration requires the same
|
|
40
|
+
schema, construction configuration and rate.
|
|
41
|
+
|
|
42
|
+
## Limiter: exact window and fixed latency
|
|
43
|
+
|
|
44
|
+
Configuration is `{sampleRate, lookaheadSamples}`. The delay is a required integer
|
|
45
|
+
in **0..2048**; it cannot be changed at runtime. Latency is exactly that many
|
|
46
|
+
samples, exposed as construction-time `latencySamples` and `latencySeconds`.
|
|
47
|
+
The capacity is a resource boundary, not a claim that every setting is real-time
|
|
48
|
+
safe. At 44.1/48/96 kHz, the maximum is about 46.44/42.67/21.33 milliseconds.
|
|
49
|
+
|
|
50
|
+
`tick(left, right, {ceilingDb, release, reset})` returns limited `left`/`right`,
|
|
51
|
+
latency-matched `dryLeft`/`dryRight`, and `windowPeak`, `envelope`, `gain` and
|
|
52
|
+
`ceiling` diagnostics. There is no implicit makeup, mix or bypass. Dry mixing or
|
|
53
|
+
another processor after the limiter can violate the ceiling.
|
|
54
|
+
|
|
55
|
+
Let N be the delay and p[n] be the larger absolute current L/R input. On sample n,
|
|
56
|
+
the detector target is the **inclusive maximum of p[n-N] through p[n]**, bounded
|
|
57
|
+
by the latest reset. The output program sample is input[n-N]. Processing order:
|
|
58
|
+
|
|
59
|
+
1. If reset, invalidate all preceding audio and detector history.
|
|
60
|
+
2. Insert current L/R and their peak in the ring and max tree.
|
|
61
|
+
3. Recompute the tree path and obtain the inclusive window maximum.
|
|
62
|
+
4. Update the existing peak follower with zero attack and requested release.
|
|
63
|
+
5. Apply the current ceiling/envelope ratio to the N-sample-old program.
|
|
64
|
+
6. Advance the ring cursor.
|
|
65
|
+
|
|
66
|
+
Attack is immediate. Lookahead conservatively attenuates audio up to N samples
|
|
67
|
+
before a transient emerges; it does not interpolate a gain ramp. The follower
|
|
68
|
+
release is the reviewed one-pole amplitude detector recurrence, not linear-gain
|
|
69
|
+
or dB-gain release: `c=1-exp(-1/max(1,release*sampleRate))` for positive release,
|
|
70
|
+
otherwise c=1. The envelope never falls below the current window maximum.
|
|
71
|
+
Release is clamped to 0..30 seconds, with nonfinite values falling back to zero.
|
|
72
|
+
Abrupt ceiling/release edits can cause discontinuities and pumping is possible.
|
|
73
|
+
|
|
74
|
+
`ceilingDb` is clamped to -120..0 dBFS, with nonfinite values falling back to zero.
|
|
75
|
+
Its f32 exponential amplitude is returned as `ceiling`. The exact discrete
|
|
76
|
+
contract is `abs(output[channel][n]) <= ceiling[n]`, not an infinite-precision dB
|
|
77
|
+
identity. A downward ceiling edit constrains **the output sample at that moment**,
|
|
78
|
+
including previously buffered audio; ceilings are not delayed with the program.
|
|
79
|
+
The gain is `min(1,ceiling/envelope)`, or one for a zero envelope. Multiplication
|
|
80
|
+
stays binary64 until final output conversion, so maximum finite f32 audio at a
|
|
81
|
+
-120 dB ceiling is not multiplied by a quantized subnormal f32 gain. Returned
|
|
82
|
+
`gain` is a rounded f32 diagnostic; it may underflow or have poor relative
|
|
83
|
+
precision at extreme inputs and must not substitute for the actual audio path.
|
|
84
|
+
The final output clamp guards floating-point ceiling rounding.
|
|
85
|
+
|
|
86
|
+
All finite f32 program inputs, including subnormals and above-full-scale values,
|
|
87
|
+
are preserved on the dry path. As in native unworklet state, negative zero is
|
|
88
|
+
normalized to positive zero. Nonfinite program samples become zero. This
|
|
89
|
+
limiter measures samples, not intersample reconstruction peaks. It performs no
|
|
90
|
+
oversampling, true-peak estimation or certification.
|
|
91
|
+
|
|
92
|
+
### Reset and memory
|
|
93
|
+
|
|
94
|
+
Reset invalidates history before the current input. An N>0 limiter then emits N
|
|
95
|
+
silent samples while filling; N=0 processes the current sample immediately with
|
|
96
|
+
fresh detector history. Holding reset high thus keeps N>0 output silent, whereas
|
|
97
|
+
N=0 continues limiting each current sample independently. Reset is not a
|
|
98
|
+
click-free bypass operation. Dry and wet always share the same delay/reset rule.
|
|
99
|
+
|
|
100
|
+
There are N+1 stereo ring positions and a power-of-two max tree. Every sample
|
|
101
|
+
updates only one leaf and `ceil(log2(N+1))` ancestors. No data-dependent search,
|
|
102
|
+
allocation, epoch counter or reset-time full-buffer scan occurs. At maximum N,
|
|
103
|
+
three binary64 buffers occupy 98,320 bytes plus a fixed small scalar history.
|
|
104
|
+
All audio/peak buffer values use exact power-of-two scaling, preserving f32
|
|
105
|
+
subnormals through unworklet's less-than-1e-30 state-write flush.
|
|
106
|
+
|
|
107
|
+
The reset-validity invariant is an initially empty prefix of leaves. During
|
|
108
|
+
sequential refill, an included sibling subtree starts before the valid-prefix
|
|
109
|
+
end and has already been rebuilt since reset; a sibling starting at or beyond
|
|
110
|
+
that end is read as zero. The prefix expands by one leaf per sample. After full
|
|
111
|
+
refill, all real leaves remain valid through wraps, while padding leaves are
|
|
112
|
+
always excluded. This also holds at N=0, non-power-of-two window lengths,
|
|
113
|
+
first wrap, repeated resets and repeated wraps. No stale pre-reset peak can
|
|
114
|
+
reappear. Rings, tree, cursor, prefix count and follower state are persistent in
|
|
115
|
+
native snapshots.
|
|
116
|
+
|
|
117
|
+
## Three-band phase-compensated dynamics
|
|
118
|
+
|
|
119
|
+
Configuration is `{sampleRate, bands:[lowConfig,midConfig,highConfig]}`. Each band
|
|
120
|
+
configuration has the existing `DynamicsConfig` mode, operation and optional
|
|
121
|
+
hysteresis, excluding sampleRate. Controls are `{lowCutoffHz,highCutoffHz,bands,
|
|
122
|
+
reset}`; each of the three control records is `DynamicsControls` without reset.
|
|
123
|
+
Each band uses its own filtered L/R program as a max-absolute linked stereo
|
|
124
|
+
sidechain. There is no external sidechain or lookahead in this composition.
|
|
125
|
+
|
|
126
|
+
Finite audio is passed without input clipping; nonfinite samples become zero.
|
|
127
|
+
The **caller headroom precondition for the tested boundary is |L|,|R| <=16**.
|
|
128
|
+
This is not an arbitrary-full-f32-input guarantee. Recombination and transients
|
|
129
|
+
can exceed input or full-scale peaks even when all dynamics gains are at most
|
|
130
|
+
one. This processor has no output ceiling. Apply the separate limiter afterward
|
|
131
|
+
if a discrete sample ceiling is needed.
|
|
132
|
+
|
|
133
|
+
Cutoffs use finite fallbacks of 200 and 2000 Hz respectively, clamp individually
|
|
134
|
+
to `[20,min(20000,0.45*sampleRate)]`, then sort ascending. Reversed controls are
|
|
135
|
+
therefore equivalent to sorted controls. Equal cutoffs are permitted; the middle
|
|
136
|
+
branch does not vanish merely because the cutoffs coincide. Each edit takes
|
|
137
|
+
effect on the current sample without clearing filter or dynamics history.
|
|
138
|
+
|
|
139
|
+
At fixed cutoffs, writing Lf/Hf for the existing LR4 low/high transfers and
|
|
140
|
+
Af=Lf+Hf for their positive-polarity allpass sum, the bands are:
|
|
141
|
+
|
|
142
|
+
- Low: `Llow * Ahigh` (the extra high-crossover sum compensates its phase).
|
|
143
|
+
- Middle: `Hlow * Lhigh`.
|
|
144
|
+
- High: `Hlow * Hhigh`.
|
|
145
|
+
|
|
146
|
+
At unity dynamics gains, the complete transfer is **Alow * Ahigh**, of unit
|
|
147
|
+
magnitude with frequency-dependent phase. The low-band compensation is essential;
|
|
148
|
+
naively splitting only the high branch a second time does not reconstruct this
|
|
149
|
+
transfer. No explicit integer sample delay is added, but IIR phase/group delay
|
|
150
|
+
is frequency dependent. This is not identity PCM or a linear-phase crossover.
|
|
151
|
+
Static transfer guarantees do not imply flat magnitude under moving cutoffs.
|
|
152
|
+
|
|
153
|
+
The results contain `left`, `right`, phase-matched `dryLeft`/`dryRight`, three
|
|
154
|
+
existing dynamics result records `low`/`mid`/`high`, and effective sorted cutoff
|
|
155
|
+
Nodes. The dry outputs are the unprocessed **allpass recombination**, not original
|
|
156
|
+
PCM. Dry/wet mixing can use this phase-matched reference, but dynamically unequal
|
|
157
|
+
band gains still change phase and magnitude. No implicit output normalization is
|
|
158
|
+
performed. All six crossover instances and three dynamics instances reset before
|
|
159
|
+
the current sample; their native state is independent and snapshot-persistent. Two fixed five-element
|
|
160
|
+
scaled binary64 scratch buffers materialize existing f32 crossover stage
|
|
161
|
+
boundaries once instead of re-expanding them at each consumer. They add 80 bytes
|
|
162
|
+
and are transient: every element is overwritten before use on each sample, so
|
|
163
|
+
they need no saved history. Exact scaling preserves nonzero f32 values and the
|
|
164
|
+
original stage rounding; native signed-zero normalization still applies.
|
|
165
|
+
|
|
166
|
+
## Verification scope
|
|
167
|
+
|
|
168
|
+
The focused tests use an independent naive timeline maximum for the limiter,
|
|
169
|
+
independent exponential recurrence for its detector, RBJ direct-form-I filter
|
|
170
|
+
references for static bands, and complex transfer products for recombination,
|
|
171
|
+
including DC. They cover 44.1/48/96 kHz, capacity endpoints, first-fill and repeated
|
|
172
|
+
wraps, reset and held reset, current-output ceiling automation, malformed inputs,
|
|
173
|
+
full-range and subnormal limiter audio, per-band gain controls, cutoff sorting
|
|
174
|
+
and equality, same-schema snapshot continuation, finite caller headroom and zero
|
|
175
|
+
scrub diagnostics. A maximum-capacity compile/memory check is bounded evidence,
|
|
176
|
+
not a deadline assertion.
|
|
177
|
+
|
|
178
|
+
The isolated consumer installs the actual packed public exports, strict-checks
|
|
179
|
+
TypeScript and renders the same candidate surface with independent numerical
|
|
180
|
+
assertions. Its manifests record exact source/package hashes, rates, controls,
|
|
181
|
+
raw candidate audio and native diagnostics. Browser, external-device and
|
|
182
|
+
maximum-capacity real-time support are not established by offline evidence.
|
|
183
|
+
Independent review and exact-head aggregate CI remain required before merge.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Crossfaded resident loop candidate
|
|
2
|
+
|
|
3
|
+
Status: **CANDIDATE**. This additive reader uses the existing resident PCM ingress,
|
|
4
|
+
native state and linear interpolation. It does not change `samplePlayer`.
|
|
5
|
+
No listening approval, seamless arbitrary-content claim, antialiasing, streaming,
|
|
6
|
+
loader, time-stretching or browser/realtime deadline proof is implied.
|
|
7
|
+
|
|
8
|
+
## Entry and effective period
|
|
9
|
+
|
|
10
|
+
Import `crossfadedLoopPlayer` and `CrossfadedLoopConfig` from
|
|
11
|
+
`@denaudio/den/loop-crossfade`; import `residentSample` and the shared
|
|
12
|
+
`SamplePlayerControls` from `@denaudio/den/sample`.
|
|
13
|
+
|
|
14
|
+
Construction accepts `{ sampleRate, sample, startFrame = 0,
|
|
15
|
+
endFrame = sample.capacity, crossfadeFrames, releaseFrames = 0 }`.
|
|
16
|
+
The fixed slice is `[startFrame,endFrame)`; `crossfadeFrames` is an integer in
|
|
17
|
+
`[0,floor((endFrame-startFrame)/2)]`. Zero explicitly disables the overlap.
|
|
18
|
+
Resident and host rates follow the existing [8,000,192,000] Hz contract.
|
|
19
|
+
`releaseFrames` is an integer in `[0,ceil(sampleRate*10)]` output samples.
|
|
20
|
+
|
|
21
|
+
For the currently loaded asset define:
|
|
22
|
+
|
|
23
|
+
- `S = startFrame`, `E = min(endFrame, sample.length())`, `L = max(0,E-S)`
|
|
24
|
+
- `F = min(crossfadeFrames,floor(L/2))`
|
|
25
|
+
- `P = L-F`, the **effective loop period in source frames**
|
|
26
|
+
|
|
27
|
+
The playback phase `q` traverses `[0,P)`. The primary read position is
|
|
28
|
+
`x = S+F+q`. In the last `F` source frames of that cycle (`q >= P-F`),
|
|
29
|
+
read the tail at `x`, read the head at `S+q-(P-F)`, and linearly blend
|
|
30
|
+
`(1-w)*tail + w*head`, where `w = (q-(P-F))/F`. Outside the overlap,
|
|
31
|
+
read only the primary position. Overlap taps use nonwrapping resident linear
|
|
32
|
+
interpolation, which holds the last endpoint between `E-1` and `E`.
|
|
33
|
+
At the phase seam the head approaches `S+F`, the next primary start.
|
|
34
|
+
When `F=0`, use ordinary wrapping linear interpolation over the whole slice.
|
|
35
|
+
|
|
36
|
+
This makes the repeated cycle `L-F` source frames long, rather than `L`.
|
|
37
|
+
For fixed nonzero rate, the cycle lasts `P / abs(rate*sourceRate/hostRate)`
|
|
38
|
+
output frames (not necessarily an integer). At unit rate with equal rates,
|
|
39
|
+
an eight-frame slice with two overlap frames repeats every six samples.
|
|
40
|
+
The first `F` source frames appear only as the incoming overlap head;
|
|
41
|
+
forward triggering starts at `S+F`, not at the asset attack at `S`.
|
|
42
|
+
Do not use this reader when an unchanged loop duration or dry attack is required.
|
|
43
|
+
|
|
44
|
+
The law is unity-sum linear, not equal-power. It bounds the output peak by the
|
|
45
|
+
largest finite source tap before release gain. Correlated equal signals do not
|
|
46
|
+
receive a +3 dB bump. Different phases can cancel or comb; mismatched transients
|
|
47
|
+
can soften, repeat or smear. Even though the continuous interpolated overlap
|
|
48
|
+
meets at its endpoints, one-sample overlaps or large playback increments can
|
|
49
|
+
still produce large adjacent output steps. This is not a universal click-remover.
|
|
50
|
+
|
|
51
|
+
## Timeline, controls and lifecycle
|
|
52
|
+
|
|
53
|
+
`tick({ gate, trigger, reset, rate })` returns
|
|
54
|
+
`{ output, active, position, phase, periodFrames, crossfadeFrames, missing }`.
|
|
55
|
+
`phase` and `position` describe the current sample before advancement.
|
|
56
|
+
`periodFrames` and returned `crossfadeFrames` report effective `P` and `F`.
|
|
57
|
+
|
|
58
|
+
- `rate` is signed, clamped to [-16,16], with NaN treated as zero and infinities
|
|
59
|
+
saturated. Advance by `rate * sample.sourceSampleRate / sampleRate`.
|
|
60
|
+
Zero holds the current phase. A change in direction changes only advancement.
|
|
61
|
+
- Gate-rise, or a true trigger while gate is high, starts/restarts. Forward/zero
|
|
62
|
+
starts at `q=0`; negative starts at `q=P-1` (primary position `E-1`). Reverse
|
|
63
|
+
traverses the **same blended periodic waveform backwards**, including its
|
|
64
|
+
overlap. It does not choose a different, direction-dependent fade waveform.
|
|
65
|
+
Holding trigger repeats that initial sample.
|
|
66
|
+
- Reset wins over trigger/gate, is silent, and resets phase to zero and gate
|
|
67
|
+
memory to low. Releasing reset with gate still high starts again.
|
|
68
|
+
- Gate-off release matches `samplePlayer`: the first off sample subtracts
|
|
69
|
+
`1/releaseFrames`; zero or one stops immediately. Release never exceeds one.
|
|
70
|
+
`active` describes the current emitted sample, even when the PCM value is zero.
|
|
71
|
+
Inactive playback freezes its phase; inactive output is zero.
|
|
72
|
+
- Each resident load, including an empty or identical replacement, invalidates
|
|
73
|
+
playback and resets its phase. Held gate does not restart the new asset.
|
|
74
|
+
A fresh gate-rise/trigger on that tick can start the newly loaded data.
|
|
75
|
+
- A shorter replacement clips both heads and reduces `F` and `P`. One available
|
|
76
|
+
frame gives `F=0`, `P=1` and a constant sample. Empty slice gives missing true,
|
|
77
|
+
silence, and both reported lengths zero. No stale PCM tail is read.
|
|
78
|
+
- `position` is the primary source position, not a unique representation of both
|
|
79
|
+
blended taps. While missing it reports `S`; phase is zero.
|
|
80
|
+
|
|
81
|
+
Use at most one `tick` per player instance per output sample; different named
|
|
82
|
+
players can share one resident and their returned values may be consumed after
|
|
83
|
+
other shared-resident reads. All playing state and resident PCM remain native
|
|
84
|
+
snapshot state. Source interpolation splits local integer/fractional coordinates before adding
|
|
85
|
+
slice/head offsets, preserving meaningful tiny-phase contributions against large
|
|
86
|
+
finite PCM. Phase accumulation uses f64 arithmetic; increments below the f64
|
|
87
|
+
resolution at the current phase can still be lost. Public phase/position
|
|
88
|
+
diagnostics are f32 and are not a higher-precision transport.
|
|
89
|
+
Same-graph/configuration/metadata/rate snapshots resume exactly;
|
|
90
|
+
there is no serializer, asset migration or cross-rate snapshot contract.
|
|
91
|
+
See [resident ingress and replacement constraints](sample.md) before loading.
|
|
92
|
+
|
|
93
|
+
## Verification
|
|
94
|
+
|
|
95
|
+
Focused and isolated packed-package checks cover independent scalar timelines,
|
|
96
|
+
source/host conversion at 44.1/48/96 kHz, forward/reverse/fractional and large rates,
|
|
97
|
+
overlap endpoints, short and missing slices, headroom/cancellation/transients,
|
|
98
|
+
reset/release/retrigger, replacement, deferred shared reads and exact snapshots.
|
|
99
|
+
Generated WAV files are CANDIDATE evidence, never approved goldens.
|