@denaudio/den 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +8 -3
- package/dist/convolution.d.ts +2 -0
- package/dist/convolution.js +14 -8
- package/dist/prepared-convolution.d.ts +5 -1
- package/dist/prepared-convolution.js +13 -8
- package/dist/resident-time-stretch.d.ts +2 -0
- package/dist/resident-time-stretch.js +6 -5
- package/dist/spatial-chains.d.ts +2 -0
- package/dist/spatial-chains.js +2 -2
- package/dist/spectral-fft.d.ts +7 -1
- package/dist/spectral-fft.js +19 -9
- package/dist/spectral-freeze.d.ts +2 -0
- package/dist/spectral-gate.d.ts +2 -0
- package/dist/spectral-stft.d.ts +1 -0
- package/dist/spectral-stft.js +7 -6
- package/dist/spectral-texture.d.ts +4 -0
- package/dist/spectral.d.ts +4 -2
- package/dist/spectral.js +2 -2
- package/docs/catalog-modulation-fx.md +1 -1
- package/docs/catalog-status.md +3 -1
- package/docs/contracts.md +10 -0
- package/docs/convolution.md +7 -2
- package/docs/dependency-findings.md +50 -53
- package/docs/prepared-convolution.md +22 -13
- package/docs/resident-time-stretch.md +12 -6
- package/docs/spatial-chains-entry.md +5 -4
- package/docs/spectral-freeze-entry.md +11 -9
- package/docs/spectral-gate-entry.md +12 -8
- package/docs/spectral-large-candidate.md +7 -0
- package/docs/spectral-texture-entry.md +14 -10
- package/docs/spectral.md +26 -17
- package/docs/voice-policy.md +2 -2
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,52 @@ and migration instructions first. Unreleased entries do not imply npm availabili
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## 0.2.0 — 2026-11-12
|
|
9
|
+
|
|
10
|
+
- **Breaking:** requires `@unworklet/core` **0.6.0**. Update the compiler, offline
|
|
11
|
+
renderer, Vite plugin and test helpers to 0.6.0 together. Node 24 is unchanged.
|
|
12
|
+
- **Breaking:** pass `ctx.renderQuantumSize` to spectral framing, convolution,
|
|
13
|
+
spatial-chain and resident time-stretch construction configs. Their dispatch
|
|
14
|
+
periods now preserve saved hop phase at the actual quantum boundary. Rebuild
|
|
15
|
+
processors for the target context; old or cross-target snapshots are not a
|
|
16
|
+
supported migration path.
|
|
17
|
+
- FFT and spectral work-buffer loops use `forIndex` instead of assuming that an
|
|
18
|
+
audio loop always runs 128 samples. Transform and partition sizes retain their
|
|
19
|
+
existing sample-based meaning.
|
|
20
|
+
- The playground uses the actual AudioContext sample rate and render quantum
|
|
21
|
+
(128 on browsers without `renderQuantumSize`) for worker compilation, metadata
|
|
22
|
+
and schema identity. It no longer forces or requires 48 kHz.
|
|
23
|
+
- The spectral-freeze example waits for input before filling its capture history,
|
|
24
|
+
so delayed source setup cannot freeze an empty startup buffer.
|
|
25
|
+
- Add multi-rate and multi-quantum spectral regression coverage, including
|
|
26
|
+
continuation after snapshots at non-128 boundaries. Dependency regression
|
|
27
|
+
checks now exercise the upstream fixes included in 0.6.0.
|
|
28
|
+
- Audio remains CANDIDATE. This compatibility update does not grant human
|
|
29
|
+
listening approval or clear the existing real-time capacity limitations.
|
|
30
|
+
|
|
31
|
+
### Migrating construction configs
|
|
32
|
+
|
|
33
|
+
Add `renderQuantumSize: ctx.renderQuantumSize` inside the `defineProcessor(ctx =>
|
|
34
|
+
...)` callback when constructing `stftIdentity`, `spectralGate`, `spectralFreeze`,
|
|
35
|
+
`spectralBlur`, `spectralCrossSynthesis`, `partitionedConvolution`,
|
|
36
|
+
`preparedConvolution`, `residentTimeStretch`, `hybridReverb` or
|
|
37
|
+
`feedforwardPitchedReverb`. For example:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// Inside the existing defineProcessor(ctx => { ... }) callback:
|
|
41
|
+
const stft = instantiate(stftIdentity, {
|
|
42
|
+
size: 256, hopSize: 128, renderQuantumSize: ctx.renderQuantumSize,
|
|
43
|
+
}, { name: 'stft' });
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Host-side `prepareConvolutionSpectrum` and its `PreparedConvolutionConfig` keep
|
|
47
|
+
their existing shape; the DSP constructor uses `PreparedConvolutionRuntimeConfig`.
|
|
48
|
+
Frame lengths, partition lengths, latency in samples and control sampling times
|
|
49
|
+
retain their meaning. Native scheduler counters are not snapshot state, so
|
|
50
|
+
dispatch uses a divisor of both hop and quantum; smaller or non-dividing quanta
|
|
51
|
+
can perform more FFT/search work. This is functional support, not a new
|
|
52
|
+
real-time performance guarantee.
|
|
53
|
+
|
|
8
54
|
## 0.1.0 — 2026-10-09
|
|
9
55
|
|
|
10
56
|
- First public npm release of `@denaudio/den`, with 54 ESM module entry points,
|
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ builds the English TypeScript playground at `/`. Its Monaco editor resolves real
|
|
|
32
32
|
package declarations for completion, hover, signatures and diagnostics. Every
|
|
33
33
|
public import has an editable audio example. Run compiles in a cancellable
|
|
34
34
|
worker, creates a native node, and exposes its AudioParams; Stop disposes it.
|
|
35
|
-
See the [source format and host contract](https://github.com/yuichkun/den/blob/
|
|
35
|
+
See the [source format and host contract](https://github.com/yuichkun/den/blob/main/playground/README.md).
|
|
36
36
|
|
|
37
37
|
The earlier public website was deleted before this replacement. Old
|
|
38
38
|
`catalog.html`, `audition.html`, `diagnostics.html` and `playground.html` routes
|
|
@@ -46,7 +46,7 @@ See the [acceptance record](docs/initial-acceptance.md).
|
|
|
46
46
|
|
|
47
47
|
## Public package boundary
|
|
48
48
|
|
|
49
|
-
The ESM-only `@denaudio/den` requires **@unworklet/core 0.
|
|
49
|
+
The ESM-only `@denaudio/den` requires **@unworklet/core 0.6.0** as a peer. All
|
|
50
50
|
exercised compiler, renderer, plugin and test dependencies are locked. Import
|
|
51
51
|
editable construction functions/subgraphs through these public subpaths:
|
|
52
52
|
|
|
@@ -66,7 +66,12 @@ export default createInstrument(bassConfig);
|
|
|
66
66
|
Use the existing `@unworklet/unplugin` Vite plugin and `createNode`; den adds no
|
|
67
67
|
loader, parameter, MIDI, routing, snapshot or serialization framework. Settings
|
|
68
68
|
are plain engine construction options and complete native parameter maps.
|
|
69
|
-
|
|
69
|
+
Compilation follows the AudioContext's sample rate and render quantum. Spectral
|
|
70
|
+
framing, both convolution modules, spatial chains and resident time stretch
|
|
71
|
+
require the processor's `ctx.renderQuantumSize` in their construction config so
|
|
72
|
+
snapshot restoration preserves hop timing even at short quantum boundaries. See the
|
|
73
|
+
[0.2.0 migration notes](CHANGELOG.md#020--2026-11-12).
|
|
74
|
+
Snapshots require the same processor/schema/sample rate/render quantum. See the
|
|
70
75
|
[instrument contract](docs/instrument.md), [sound settings](docs/instrument-settings.md),
|
|
71
76
|
[Delay FX contract](docs/delay-fx.md), [effect settings](docs/delay-settings.md),
|
|
72
77
|
[shared contracts](docs/contracts.md), and [dependency limitations](docs/dependency-findings.md).
|
package/dist/convolution.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type EveryNSamples, type Node } from '@unworklet/core';
|
|
2
2
|
export interface PartitionedConvolutionConfig {
|
|
3
|
+
/** Pass ctx.renderQuantumSize; aligns native dispatch with snapshot boundaries. */
|
|
4
|
+
renderQuantumSize: number;
|
|
3
5
|
/** Construction-fixed power of two in [4,32]. FFT size is twice this value. */
|
|
4
6
|
blockSize: number;
|
|
5
7
|
/** Fixed IR, length 1..4*blockSize. Every tap in [-1,1], total absolute gain <=4. */
|
package/dist/convolution.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineSubgraph, f32, f64, i32, instantiate, select, state } from '@unworklet/core';
|
|
2
|
-
import { spectralFft } from './spectral-fft.js';
|
|
2
|
+
import { spectralDispatchPeriod, spectralFft } from './spectral-fft.js';
|
|
3
3
|
/** Bounded uniform-partition convolution candidate, exactly blockSize latency.
|
|
4
4
|
* Call once per sample in stride-1 forSample with its native everyNSamples.
|
|
5
5
|
* Input must be finite normalized audio [-1,1]. No IR loader/swap or dry path.
|
|
@@ -13,6 +13,7 @@ export const partitionedConvolution = defineSubgraph((config) => {
|
|
|
13
13
|
config.impulse.reduce((sum, tap) => sum + Math.abs(tap), 0) > 4) {
|
|
14
14
|
throw new RangeError('partitionedConvolution requires power-of-two blockSize [4,32], 1..4*blockSize finite IR taps in [-1,1], and IR absolute sum <=4');
|
|
15
15
|
}
|
|
16
|
+
const dispatchPeriod = spectralDispatchPeriod(block, config.renderQuantumSize);
|
|
16
17
|
const impulse = [...config.impulse], size = 2 * block, partitions = Math.ceil(impulse.length / block);
|
|
17
18
|
// Construction-only O(P*N*B) coefficient preparation. No host FFT runtime,
|
|
18
19
|
// asset read, callback allocation or coefficient update on the audio thread.
|
|
@@ -43,11 +44,15 @@ export const partitionedConvolution = defineSubgraph((config) => {
|
|
|
43
44
|
validSamples.write(select(reset, i32(0), validSamples.read()));
|
|
44
45
|
validBlocks.write(select(reset, i32(0), validBlocks.read()));
|
|
45
46
|
pendingReset.write(pendingReset.read().or(reset));
|
|
46
|
-
|
|
47
|
+
// A quantum need not divide blockSize. Persistent cursor phase keeps
|
|
48
|
+
// frame commits exact when native scheduler counters restart on restore.
|
|
49
|
+
everyNSamples(dispatchPeriod, () => {
|
|
50
|
+
const frameDue = cursor.read().mod(block).eq(0);
|
|
47
51
|
const transformed = fft.transform(n => select(n.lt(block).and(validSamples.read().gte(i32(block).sub(n))), history.read(cursor.read().add(n).mod(block)), f64(0)), () => f64(0), false);
|
|
48
52
|
for (let k = 0; k < size; k++) {
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
const index = spectrumCursor.read().mul(size).add(k);
|
|
54
|
+
previousReal.write(index, select(frameDue, transformed.real(k), previousReal.read(index)));
|
|
55
|
+
previousImag.write(index, select(frameDue, transformed.imag(k), previousImag.read(index)));
|
|
51
56
|
}
|
|
52
57
|
for (let k = 0; k < size; k++) {
|
|
53
58
|
let real = f64(0), imag = f64(0);
|
|
@@ -64,11 +69,12 @@ export const partitionedConvolution = defineSubgraph((config) => {
|
|
|
64
69
|
const frame = fft.transform(n => mixedReal.read(n), n => mixedImag.read(n), true);
|
|
65
70
|
for (let n = 0; n < size; n++) {
|
|
66
71
|
const index = cursor.read().add(n).mod(size);
|
|
67
|
-
|
|
72
|
+
const prior = overlap.read(index);
|
|
73
|
+
overlap.write(index, select(frameDue, select(pendingReset.read(), f64(0), prior).add(frame.real(n)), prior));
|
|
68
74
|
}
|
|
69
|
-
spectrumCursor.write(spectrumCursor.read().add(1).mod(partitions));
|
|
70
|
-
validBlocks.write(validBlocks.read().add(1).min(partitions - 1));
|
|
71
|
-
pendingReset.write(
|
|
75
|
+
spectrumCursor.write(select(frameDue, spectrumCursor.read().add(1).mod(partitions), spectrumCursor.read()));
|
|
76
|
+
validBlocks.write(select(frameDue, validBlocks.read().add(1).min(partitions - 1), validBlocks.read()));
|
|
77
|
+
pendingReset.write(pendingReset.read().and(frameDue.not()));
|
|
72
78
|
});
|
|
73
79
|
const output = select(reset.or(pendingReset.read()), f32(0), f32(overlap.read(cursor.read()).div(scale)));
|
|
74
80
|
overlap.write(cursor.read(), f64(0));
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { type EveryNSamples, type MessageGraphPayload, type Node } from '@unworklet/core';
|
|
2
2
|
import { type PreparedConvolutionConfig, type PreparedConvolutionPacket } from './convolution-spectrum.js';
|
|
3
3
|
export { prepareConvolutionSpectrum, type PreparedConvolutionConfig, type PreparedConvolutionPacket } from './convolution-spectrum.js';
|
|
4
|
+
export interface PreparedConvolutionRuntimeConfig extends PreparedConvolutionConfig {
|
|
5
|
+
/** Pass ctx.renderQuantumSize; aligns native dispatch with snapshot boundaries. */
|
|
6
|
+
renderQuantumSize: number;
|
|
7
|
+
}
|
|
4
8
|
/** Fixed B128/P64 prepared-spectrum mono convolution candidate.
|
|
5
9
|
* Correctness/gain bounds require an unchanged packet from host preparation.
|
|
6
10
|
* Caller owns native CAPACITY_16 ingress with payloadCapacity 131072 bytes.
|
|
7
11
|
* Copy/scan runs in native dispatch, so preload with audible playback gated off.
|
|
8
12
|
*/
|
|
9
|
-
export declare const preparedConvolution: import("@unworklet/core").SubgraphDecl<[config:
|
|
13
|
+
export declare const preparedConvolution: import("@unworklet/core").SubgraphDecl<[config: PreparedConvolutionRuntimeConfig], {
|
|
10
14
|
/** Module-specific native event payload; this is not a host loader/ack. */
|
|
11
15
|
load(packet: MessageGraphPayload<PreparedConvolutionPacket>): void;
|
|
12
16
|
/** Once per sample, inside stride-1 forSample with its native scheduler.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineSubgraph, f32, f64, i32, instantiate, select, state } from '@unworklet/core';
|
|
2
|
-
import { spectralFft, spectralForEach } from './spectral-fft.js';
|
|
2
|
+
import { spectralDispatchPeriod, spectralFft, spectralForEach } from './spectral-fft.js';
|
|
3
3
|
import { PREPARED_BLOCK as B, PREPARED_FFT_SIZE as N, PREPARED_PARTITIONS as P, PREPARED_VALUES as VALUES, validatePreparedConvolutionConfig } from './convolution-spectrum.js';
|
|
4
4
|
export { prepareConvolutionSpectrum } from './convolution-spectrum.js';
|
|
5
5
|
/** Fixed B128/P64 prepared-spectrum mono convolution candidate.
|
|
@@ -9,6 +9,7 @@ export { prepareConvolutionSpectrum } from './convolution-spectrum.js';
|
|
|
9
9
|
*/
|
|
10
10
|
export const preparedConvolution = defineSubgraph((config) => {
|
|
11
11
|
validatePreparedConvolutionConfig(config);
|
|
12
|
+
const dispatchPeriod = spectralDispatchPeriod(B, config.renderQuantumSize);
|
|
12
13
|
const coefficients = state.buffer.f32({ size: VALUES }).expose({ name: 'coefficients', snapshot: 'persistent' });
|
|
13
14
|
const history = state.buffer.f64({ size: B }).expose({ name: 'historyScaled', snapshot: 'persistent' });
|
|
14
15
|
const previous = state.buffer.f64({ size: VALUES }).expose({ name: 'previousSpectraScaled', snapshot: 'persistent' });
|
|
@@ -55,12 +56,15 @@ export const preparedConvolution = defineSubgraph((config) => {
|
|
|
55
56
|
validSamples.write(select(invalidate, i32(0), validSamples.read()));
|
|
56
57
|
validBlocks.write(select(invalidate, i32(0), validBlocks.read()));
|
|
57
58
|
pendingReset.write(pendingReset.read().or(invalidate));
|
|
58
|
-
|
|
59
|
+
// Keep B-sample frame phase in the persistent cursor. A fresh native
|
|
60
|
+
// scheduler must also align after snapshots taken inside a B-sample hop.
|
|
61
|
+
everyNSamples(dispatchPeriod, () => {
|
|
62
|
+
const frameDue = cursor.read().mod(B).eq(0);
|
|
59
63
|
const frame = fft.transform(n => select(n.lt(B).and(validSamples.read().gte(i32(B).sub(n))), history.read(cursor.read().add(n).mod(B)), f64(0)), () => f64(0), false);
|
|
60
64
|
spectralForEach(N, k => {
|
|
61
65
|
const index = spectrumCursor.read().mul(N).add(k).mul(2);
|
|
62
|
-
previous.write(index, frame.real(k));
|
|
63
|
-
previous.write(index.add(1), frame.imag(k));
|
|
66
|
+
previous.write(index, select(frameDue, frame.real(k), previous.read(index)));
|
|
67
|
+
previous.write(index.add(1), select(frameDue, frame.imag(k), previous.read(index.add(1))));
|
|
64
68
|
});
|
|
65
69
|
spectralForEach(N, k => {
|
|
66
70
|
sumReal.write(0);
|
|
@@ -82,11 +86,12 @@ export const preparedConvolution = defineSubgraph((config) => {
|
|
|
82
86
|
const result = fft.transform(n => mixedReal.read(n), n => mixedImag.read(n), true);
|
|
83
87
|
spectralForEach(N, n => {
|
|
84
88
|
const index = cursor.read().add(n).mod(N);
|
|
85
|
-
|
|
89
|
+
const prior = overlap.read(index);
|
|
90
|
+
overlap.write(index, select(frameDue, select(pendingReset.read(), f64(0), prior).add(result.real(n)), prior));
|
|
86
91
|
});
|
|
87
|
-
spectrumCursor.write(spectrumCursor.read().add(1).mod(P));
|
|
88
|
-
validBlocks.write(validBlocks.read().add(1).min(P - 1));
|
|
89
|
-
pendingReset.write(
|
|
92
|
+
spectrumCursor.write(select(frameDue, spectrumCursor.read().add(1).mod(P), spectrumCursor.read()));
|
|
93
|
+
validBlocks.write(select(frameDue, validBlocks.read().add(1).min(P - 1), validBlocks.read()));
|
|
94
|
+
pendingReset.write(pendingReset.read().and(frameDue.not()));
|
|
90
95
|
});
|
|
91
96
|
const output = select(invalidate.or(pendingReset.read()), f32(0), f32(overlap.read(cursor.read()).div(scale)));
|
|
92
97
|
overlap.write(cursor.read(), f64(0));
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type EveryNSamples, type Node } from '@unworklet/core';
|
|
2
2
|
import type { ResidentSample } from './sample.js';
|
|
3
3
|
export interface ResidentTimeStretchConfig {
|
|
4
|
+
/** Pass ctx.renderQuantumSize; aligns native dispatch with snapshot boundaries. */
|
|
5
|
+
renderQuantumSize: number;
|
|
4
6
|
/** Match ctx.sampleRate; integer 8000..192000. */
|
|
5
7
|
sampleRate: number;
|
|
6
8
|
/** Resident sourceSampleRate must also be an integer in [8000,192000]. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineSubgraph, f32, f64, i32, select, state } from '@unworklet/core';
|
|
2
|
-
import { spectralForEach } from './spectral-fft.js';
|
|
2
|
+
import { spectralDispatchPeriod, spectralForEach } from './spectral-fft.js';
|
|
3
3
|
// Exact binary internal scaling protects tiny waveform differences from the
|
|
4
4
|
// native 1e-30 scalar-store scrub. Even opposite finite f32 extrema produce
|
|
5
5
|
// a 64-term squared score below 2**649, far inside finite f64 range.
|
|
@@ -17,6 +17,7 @@ export const residentTimeStretch = defineSubgraph((config) => {
|
|
|
17
17
|
![128, 256].includes(H) || ![0, 8, 16, 32, 64, 128].includes(S)) {
|
|
18
18
|
throw new RangeError('residentTimeStretch requires integer sampleRate/sourceSampleRate [8000,192000], hopSamples 128/256 and searchFrames 0/8/16/32/64/128');
|
|
19
19
|
}
|
|
20
|
+
const dispatchPeriod = spectralDispatchPeriod(H, config.renderQuantumSize);
|
|
20
21
|
const active = state.bool(false).named('active'), pending = state.bool(false).named('pending');
|
|
21
22
|
const ended = state.bool(false).named('ended'), rejected = state.bool(false).named('rejected');
|
|
22
23
|
const previousGate = state.bool(false).named('previousGate'), revision = state.i32(-1).named('revision');
|
|
@@ -70,10 +71,10 @@ export const residentTimeStretch = defineSubgraph((config) => {
|
|
|
70
71
|
sourceLength.write(select(clear, i32(0), sourceLength.read()));
|
|
71
72
|
const request = c.trigger.or(c.gate.and(previousGate.read().not())).and(c.gate).and(c.reset.not()).and(missing.not());
|
|
72
73
|
pending.write(pending.read().or(request));
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
// carries H
|
|
76
|
-
everyNSamples(
|
|
74
|
+
// Native scheduler counters are not snapshotted. This period divides H
|
|
75
|
+
// and the actual quantum, so every block-atomic restore realigns dispatch.
|
|
76
|
+
// The persistent clock carries H phase; off-phase searches do not commit.
|
|
77
|
+
everyNSamples(dispatchPeriod, () => {
|
|
77
78
|
const due = clock.read().eq(0), attempt = due.and(pending.read()).and(c.gate).and(c.reset.not());
|
|
78
79
|
const valid = c.durationScale.gte(.5).and(c.durationScale.lte(2)).and(c.pitchRatio.gte(.5)).and(c.pitchRatio.lte(2));
|
|
79
80
|
launching.write(attempt.and(valid).and(missing.not()));
|
package/dist/spatial-chains.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type EveryNSamples, type Node } from '@unworklet/core';
|
|
2
2
|
export interface SpatialChainConfig {
|
|
3
|
+
/** Pass ctx.renderQuantumSize for the early convolution snapshot phase. */
|
|
4
|
+
renderQuantumSize: number;
|
|
3
5
|
/** Match the enclosing ctx.sampleRate; integer 8000..192000. */
|
|
4
6
|
sampleRate: number;
|
|
5
7
|
}
|
package/dist/spatial-chains.js
CHANGED
|
@@ -5,12 +5,12 @@ import { algorithmicReverb } from './reverb.js';
|
|
|
5
5
|
import { windowedPitchShift } from './windowed-pitch-shift.js';
|
|
6
6
|
// Construction-only composition. Each existing module retains its own state,
|
|
7
7
|
// scheduler and native snapshot contract; no new runtime routing is introduced.
|
|
8
|
-
function spatialPaths({ sampleRate }) {
|
|
8
|
+
function spatialPaths({ sampleRate, renderQuantumSize }) {
|
|
9
9
|
if (!Number.isInteger(sampleRate) || sampleRate < 8000 || sampleRate > 192000) {
|
|
10
10
|
throw new RangeError('spatial chains require integer sampleRate [8000,192000]');
|
|
11
11
|
}
|
|
12
12
|
const color = instantiate(partitionedConvolution, {
|
|
13
|
-
blockSize: 8, impulse: [.625, 0, 0, .25, 0, 0, 0, -.125],
|
|
13
|
+
renderQuantumSize, blockSize: 8, impulse: [.625, 0, 0, .25, 0, 0, 0, -.125],
|
|
14
14
|
}, { name: 'earlyColor' });
|
|
15
15
|
const reflections = instantiate(multiTapDelay, {
|
|
16
16
|
sampleRate, maxDelaySeconds: .023,
|
package/dist/spectral-fft.d.ts
CHANGED
|
@@ -3,12 +3,18 @@ import { type Node } from '@unworklet/core';
|
|
|
3
3
|
* No audio/parameter indexing or sample-clock advancement belongs in this body.
|
|
4
4
|
*/
|
|
5
5
|
export declare function spectralForEach(size: number, body: (index: Node<'i32'>) => void): void;
|
|
6
|
+
/** Internal sample dispatch period. Native scheduler phase is not snapshotted.
|
|
7
|
+
* A divisor of both the hop and quantum realigns a fresh scheduler at every
|
|
8
|
+
* block-atomic restore; persistent module cursors decide when to commit frames.
|
|
9
|
+
* Keep the historical 128-sample maximum dispatch period at ordinary targets.
|
|
10
|
+
*/
|
|
11
|
+
export declare function spectralDispatchPeriod(hopSize: number, renderQuantumSize: number): number;
|
|
6
12
|
/** Internal construction-fixed FFT. Not a separate runtime or public package API. */
|
|
7
13
|
export declare const spectralFft: import("@unworklet/core").SubgraphDecl<[config: {
|
|
8
14
|
size: number;
|
|
9
15
|
}], {
|
|
10
16
|
/** JS functions/loops run during graph capture. Butterfly iteration uses the
|
|
11
|
-
* public fixed-
|
|
17
|
+
* public fixed-count native forIndex loop over work-buffer indices, nested
|
|
12
18
|
* inside the caller's hop body. It performs no audio I/O or audio-time update.
|
|
13
19
|
* Inputs must not alias these work buffers.
|
|
14
20
|
* Result aliases scratch and must be consumed before the next transform.
|
package/dist/spectral-fft.js
CHANGED
|
@@ -1,20 +1,30 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defineSubgraph, f64, forIndex, i32, select, state } from '@unworklet/core';
|
|
2
2
|
/** Internal fixed-capacity buffer iteration, using only public native loops.
|
|
3
3
|
* No audio/parameter indexing or sample-clock advancement belongs in this body.
|
|
4
4
|
*/
|
|
5
5
|
export function spectralForEach(size, body) {
|
|
6
|
-
if (
|
|
7
|
-
throw new RangeError('spectral buffer iteration requires
|
|
6
|
+
if (size < 4 || size > 1024 || !Number.isInteger(Math.log2(size))) {
|
|
7
|
+
throw new RangeError('spectral buffer iteration requires power-of-two size [4,1024]');
|
|
8
8
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
forIndex(size, body);
|
|
10
|
+
}
|
|
11
|
+
/** Internal sample dispatch period. Native scheduler phase is not snapshotted.
|
|
12
|
+
* A divisor of both the hop and quantum realigns a fresh scheduler at every
|
|
13
|
+
* block-atomic restore; persistent module cursors decide when to commit frames.
|
|
14
|
+
* Keep the historical 128-sample maximum dispatch period at ordinary targets.
|
|
15
|
+
*/
|
|
16
|
+
export function spectralDispatchPeriod(hopSize, renderQuantumSize) {
|
|
17
|
+
if (!Number.isInteger(renderQuantumSize) || renderQuantumSize < 1) {
|
|
18
|
+
throw new RangeError('spectral renderQuantumSize must be the positive integer ctx.renderQuantumSize');
|
|
19
|
+
}
|
|
20
|
+
let divisor = Math.min(hopSize, 128), remaining = renderQuantumSize;
|
|
21
|
+
while (remaining !== 0)
|
|
22
|
+
[divisor, remaining] = [remaining, divisor % remaining];
|
|
23
|
+
return divisor;
|
|
12
24
|
}
|
|
13
25
|
/** Internal construction-fixed FFT. Not a separate runtime or public package API. */
|
|
14
26
|
export const spectralFft = defineSubgraph((config) => {
|
|
15
27
|
const { size } = config;
|
|
16
|
-
if (SAMPLES_PER_BLOCK !== 128)
|
|
17
|
-
throw new RangeError('spectral FFT requires the public unworklet fixed-128 loop contract');
|
|
18
28
|
if (!Number.isInteger(size) || size < 8 || size > 1024 || !Number.isInteger(Math.log2(size))) {
|
|
19
29
|
throw new RangeError('spectral FFT size must be a power of two in [8,1024]');
|
|
20
30
|
}
|
|
@@ -33,7 +43,7 @@ export const spectralFft = defineSubgraph((config) => {
|
|
|
33
43
|
};
|
|
34
44
|
return {
|
|
35
45
|
/** JS functions/loops run during graph capture. Butterfly iteration uses the
|
|
36
|
-
* public fixed-
|
|
46
|
+
* public fixed-count native forIndex loop over work-buffer indices, nested
|
|
37
47
|
* inside the caller's hop body. It performs no audio I/O or audio-time update.
|
|
38
48
|
* Inputs must not alias these work buffers.
|
|
39
49
|
* Result aliases scratch and must be consumed before the next transform.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type EveryNSamples, type Node } from '@unworklet/core';
|
|
2
2
|
export interface SpectralFreezeConfig {
|
|
3
|
+
/** Pass ctx.renderQuantumSize; aligns native dispatch with snapshot boundaries. */
|
|
4
|
+
renderQuantumSize: number;
|
|
3
5
|
/** Construction-fixed power of two in [8,1024]. Reference alignment is size samples. */
|
|
4
6
|
size: number;
|
|
5
7
|
/** Exactly size/2 or size/4. Rebuild to change. */
|
package/dist/spectral-gate.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type EveryNSamples, type Node } from '@unworklet/core';
|
|
2
2
|
export interface SpectralGateConfig {
|
|
3
|
+
/** Pass ctx.renderQuantumSize; aligns native dispatch with snapshot boundaries. */
|
|
4
|
+
renderQuantumSize: number;
|
|
3
5
|
/** Construction-fixed power of two in [8,1024]. Reference alignment is size samples. */
|
|
4
6
|
size: number;
|
|
5
7
|
/** Exactly size/2 or size/4. Rebuild to change. */
|
package/dist/spectral-stft.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export interface StftSpectrum {
|
|
|
8
8
|
export declare function createStftIdentity(config: {
|
|
9
9
|
size: number;
|
|
10
10
|
hopSize: number;
|
|
11
|
+
renderQuantumSize: number;
|
|
11
12
|
}, maximumSize: number, processSpectrum?: (spectrum: StftSpectrum) => StftSpectrum): {
|
|
12
13
|
tick(input: Node<"f32">, reset: Node<"bool">, everyNSamples: EveryNSamples): Node<"f32">;
|
|
13
14
|
};
|
package/dist/spectral-stft.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { f32, f64, i32, instantiate, select, state } from '@unworklet/core';
|
|
2
|
-
import { spectralFft, spectralForEach } from './spectral-fft.js';
|
|
2
|
+
import { spectralDispatchPeriod, spectralFft, spectralForEach } from './spectral-fft.js';
|
|
3
3
|
/** Internal construction only. Public wrappers choose their reviewed capacity. */
|
|
4
4
|
export function createStftIdentity(config, maximumSize, processSpectrum) {
|
|
5
5
|
const { size, hopSize } = config;
|
|
@@ -7,6 +7,7 @@ export function createStftIdentity(config, maximumSize, processSpectrum) {
|
|
|
7
7
|
(hopSize !== size / 2 && hopSize !== size / 4)) {
|
|
8
8
|
throw new RangeError(`stftIdentity requires power-of-two size [8,${maximumSize}] and hopSize size/2 or size/4`);
|
|
9
9
|
}
|
|
10
|
+
const dispatchPeriod = spectralDispatchPeriod(hopSize, config.renderQuantumSize);
|
|
10
11
|
const fft = instantiate(spectralFft, { size }, { name: 'fft' });
|
|
11
12
|
// Public 0.4.1 flushes buffer writes below 1e-30. Exact power-of-two
|
|
12
13
|
// scaling retains finite f32 subnormals without adding an audio threshold.
|
|
@@ -28,12 +29,12 @@ export function createStftIdentity(config, maximumSize, processSpectrum) {
|
|
|
28
29
|
tick(input, reset, everyNSamples) {
|
|
29
30
|
valid.write(select(reset, i32(0), valid.read()));
|
|
30
31
|
pendingReset.write(pendingReset.read().or(reset));
|
|
31
|
-
// Native
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
32
|
+
// Native scheduler counters are not snapshot state. The dispatch period
|
|
33
|
+
// divides both the hop and actual quantum, so a fresh scheduler aligns
|
|
34
|
+
// after any block-atomic restore. The persistent ring cursor carries H
|
|
35
|
+
// phase; off-phase FFT results are computed eagerly but never committed.
|
|
35
36
|
// No CPU-saving claim is attached to the select-based commit condition.
|
|
36
|
-
everyNSamples(
|
|
37
|
+
everyNSamples(dispatchPeriod, () => {
|
|
37
38
|
const frameDue = cursor.read().mod(hopSize).eq(0);
|
|
38
39
|
// sin(pi*n/N) equals the nonnegative periodic sqrt-Hann window.
|
|
39
40
|
// Reanchor per frame; one native buffer loop replaces N code copies.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type EveryNSamples, type Node } from '@unworklet/core';
|
|
2
2
|
export interface SpectralBlurConfig {
|
|
3
|
+
/** Pass ctx.renderQuantumSize; aligns native dispatch with snapshot boundaries. */
|
|
4
|
+
renderQuantumSize: number;
|
|
3
5
|
/** Construction-fixed power of two in [8,256]; live reference delay is size. */
|
|
4
6
|
size: number;
|
|
5
7
|
/** Exactly size/2 or size/4. */
|
|
@@ -8,6 +10,8 @@ export interface SpectralBlurConfig {
|
|
|
8
10
|
radius: number;
|
|
9
11
|
}
|
|
10
12
|
export interface SpectralCrossSynthesisConfig {
|
|
13
|
+
/** Pass ctx.renderQuantumSize; aligns native dispatch with snapshot boundaries. */
|
|
14
|
+
renderQuantumSize: number;
|
|
11
15
|
/** Construction-fixed power of two in [8,256]; live reference delay is size. */
|
|
12
16
|
size: number;
|
|
13
17
|
/** Exactly size/2 or size/4. */
|
package/dist/spectral.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export interface StftIdentityConfig {
|
|
2
|
+
/** Pass ctx.renderQuantumSize; aligns native dispatch with snapshot boundaries. */
|
|
3
|
+
renderQuantumSize: number;
|
|
2
4
|
/** Construction-fixed power of two in [8,1024]. Latency is size samples. */
|
|
3
5
|
size: number;
|
|
4
6
|
/** Exactly size/2 or size/4. Rebuild to change. */
|
|
@@ -7,8 +9,8 @@ export interface StftIdentityConfig {
|
|
|
7
9
|
/** Graph-native FFT/IFFT identity candidate, periodic sqrt-Hann WOLA.
|
|
8
10
|
* Call exactly once inside stride-1 forSample, supplying its everyNSamples.
|
|
9
11
|
* Finite f32 input required. Reset silences immediately and discards that sample.
|
|
10
|
-
* Reset does not rephase frames.
|
|
11
|
-
*
|
|
12
|
+
* Reset does not rephase frames. FFT dispatch divides hop, quantum and 128;
|
|
13
|
+
* the persistent cursor controls frame commits and snapshot phase.
|
|
12
14
|
* No realtime guarantee.
|
|
13
15
|
*/
|
|
14
16
|
export declare const stftIdentity: import("@unworklet/core").SubgraphDecl<[config: StftIdentityConfig], {
|
package/dist/spectral.js
CHANGED
|
@@ -3,8 +3,8 @@ import { createStftIdentity } from './spectral-stft.js';
|
|
|
3
3
|
/** Graph-native FFT/IFFT identity candidate, periodic sqrt-Hann WOLA.
|
|
4
4
|
* Call exactly once inside stride-1 forSample, supplying its everyNSamples.
|
|
5
5
|
* Finite f32 input required. Reset silences immediately and discards that sample.
|
|
6
|
-
* Reset does not rephase frames.
|
|
7
|
-
*
|
|
6
|
+
* Reset does not rephase frames. FFT dispatch divides hop, quantum and 128;
|
|
7
|
+
* the persistent cursor controls frame commits and snapshot phase.
|
|
8
8
|
* No realtime guarantee.
|
|
9
9
|
*/
|
|
10
10
|
export const stftIdentity = defineSubgraph((config) => createStftIdentity(config, 1024));
|
|
@@ -13,7 +13,7 @@ Public entries:
|
|
|
13
13
|
|
|
14
14
|
All use existing `defineSubgraph` / `instantiate` and state/snapshot mechanisms.
|
|
15
15
|
No loader, routing, serialization, parameter, or test framework is added.
|
|
16
|
-
|
|
16
|
+
Use the tested unworklet 0.6.0 dependency. The root entry does not re-export these candidate modules.
|
|
17
17
|
|
|
18
18
|
## Shared entry contract
|
|
19
19
|
|
package/docs/catalog-status.md
CHANGED
|
@@ -59,7 +59,9 @@ Contracts, controls and limitations are detailed in [filters](catalog-filters.md
|
|
|
59
59
|
[filter-bank vocoder](filterbank-vocoder-entry.md),
|
|
60
60
|
and [musical examples](musical-examples.md).
|
|
61
61
|
All remain editable unworklet subgraphs. No parameter, MIDI, state, routing,
|
|
62
|
-
loader, asset or test framework is introduced.
|
|
62
|
+
loader, asset or test framework is introduced. The current dependency is unworklet
|
|
63
|
+
0.6.0; older measurements below retain their original version and scope. See the
|
|
64
|
+
[0.2.0 migration notes](../CHANGELOG.md#020--2026-11-12). The only initial-DSP source exception is
|
|
63
65
|
a narrowly tested one-sample read-head construction-boundary correction; existing
|
|
64
66
|
common-rate audio/state/WASM parity is required.
|
|
65
67
|
|
package/docs/contracts.md
CHANGED
|
@@ -7,6 +7,16 @@ implemented initial package/site boundary are linked from [the README](../README
|
|
|
7
7
|
and [the acceptance record](initial-acceptance.md). Later reviewed module-specific
|
|
8
8
|
contracts supersede the provisional choices below.
|
|
9
9
|
|
|
10
|
+
The current package uses unworklet **0.6.0**. Its entry gate renders in real
|
|
11
|
+
Chromium at 44.1/48/96 kHz, and snapshots include current AudioParam values even
|
|
12
|
+
before rendering or while suspended. The 0.4.1 rate restriction and suspended-edit
|
|
13
|
+
workaround below are historical. Incompatible slot types are now skipped instead
|
|
14
|
+
of reinterpreted; live restore applies saved AudioParams before persistent state.
|
|
15
|
+
See [dependency findings](dependency-findings.md) for the checked regressions and
|
|
16
|
+
remaining constraints. Snapshots still require the same processor/schema, sample
|
|
17
|
+
rate and render quantum; this upgrade does not imply general automation rollback,
|
|
18
|
+
cross-target migration, transient-state clearing or human audio approval.
|
|
19
|
+
|
|
10
20
|
## Authority and provisional choices
|
|
11
21
|
|
|
12
22
|
The approved initial-scope specification says that individual DSP methods and
|
package/docs/convolution.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Bounded uniform convolution candidate
|
|
2
2
|
|
|
3
|
-
`partitionedConvolution({ blockSize, impulse }).tick(input, reset,
|
|
3
|
+
`partitionedConvolution({ blockSize, impulse, renderQuantumSize: ctx.renderQuantumSize }).tick(input, reset,
|
|
4
4
|
everyNSamples)` uses the existing stride-1 `forSample` scheduler. This is a small
|
|
5
5
|
uniform-partition frequency-domain convolution, not a direct FIR relabeled as
|
|
6
6
|
FFT processing. It is CANDIDATE and has no human or realtime acceptance.
|
|
@@ -9,6 +9,11 @@ not a practical convolution-reverb or complete catalog §10 capability.
|
|
|
9
9
|
|
|
10
10
|
## Contract
|
|
11
11
|
|
|
12
|
+
- Pass `ctx.renderQuantumSize` from the enclosing processor. Dispatch runs every
|
|
13
|
+
`gcd(B, renderQuantumSize)` samples, and the persistent cursor commits only at
|
|
14
|
+
true B-sample boundaries. This preserves block-atomic snapshot phase even when
|
|
15
|
+
the quantum does not divide B. Off-phase work is eager; small or coprime
|
|
16
|
+
quanta increase cost and do not gain realtime clearance.
|
|
12
17
|
- B is a construction-fixed power of two in 4..32; N=2B. The original/fixed IR has
|
|
13
18
|
1..4B taps, each finite in [-1,1], and sum of absolute tap values <=4. Input is
|
|
14
19
|
finite normalized audio [-1,1]; output is not clipped or gain-normalized.
|
|
@@ -30,7 +35,7 @@ not a practical convolution-reverb or complete catalog §10 capability.
|
|
|
30
35
|
are double constants. No treatment of arbitrary nonfinite inputs is promised.
|
|
31
36
|
- Persistent history, overlap, spectra and indices use native full snapshots.
|
|
32
37
|
FFT/mixed-spectrum scratch is transient and rewritten before use. Same graph
|
|
33
|
-
and rate are required. Changing IR/configuration requires a new graph; no
|
|
38
|
+
and rate/quantum are required. Changing IR/configuration requires a new graph; no
|
|
34
39
|
migration, IR swap, crossfade, stereo adapter or long-room support is implied.
|
|
35
40
|
|
|
36
41
|
## Required evidence
|
|
@@ -1,61 +1,58 @@
|
|
|
1
|
-
# unworklet
|
|
1
|
+
# unworklet compatibility findings
|
|
2
2
|
|
|
3
|
-
The
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
The current checks use registry-installed **0.6.0** packages and the checked-in
|
|
4
|
+
locks. The initial 0.4.1 findings are retained below as historical context; fixes
|
|
5
|
+
are checked against their correct behavior rather than preserving upstream bugs.
|
|
6
|
+
No unworklet source or package internals are patched.
|
|
6
7
|
|
|
7
|
-
| Issue |
|
|
8
|
+
| Issue | 0.4.1 observation | 0.6.0 regression check and remaining constraint |
|
|
8
9
|
| --- | --- | --- |
|
|
9
|
-
| [#78](https://github.com/yuichkun/unworklet/issues/78) |
|
|
10
|
-
| [#79](https://github.com/yuichkun/unworklet/issues/79) | Restoring f32(1) into same-name i32
|
|
11
|
-
| [#96](https://github.com/yuichkun/unworklet/issues/96) | duration=896/48000
|
|
12
|
-
| [#98](https://github.com/yuichkun/unworklet/issues/98) | Unity DC
|
|
13
|
-
| [#104](https://github.com/yuichkun/unworklet/issues/104) |
|
|
14
|
-
| [#106](https://github.com/yuichkun/unworklet/issues/106) |
|
|
15
|
-
|
|
16
|
-
The
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
and
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
recompile at context rate; `lang/browser.compileSource(source)` likewise has no
|
|
49
|
-
rate argument in 0.4.1. No metadata bypass, custom runtime/loader, or upstream edit
|
|
50
|
-
is introduced. The rejection probes document this limitation; non-48-kHz browser
|
|
51
|
-
support is not an acceptance requirement and does not block den's next lanes.
|
|
10
|
+
| [#78](https://github.com/yuichkun/unworklet/issues/78) | A pre-render snapshot stored gain 0 instead of initial 0.5; suspended edits stored the last rendered value. | Real Chromium snapshots retain initial 0.5 and a suspended edit to 0.25. Rendering verifies 0.5 → 0.25 → restored 0.5 at 44.1/48/96 kHz. |
|
|
11
|
+
| [#79](https://github.com/yuichkun/unworklet/issues/79) | Restoring f32(1) into a same-name i32 slot yielded the reinterpreted bits 1065353216. | The incompatible slot is skipped and keeps its initial i32 value 7. den promises identical-schema/rate continuation; deliberate type changes still need a reviewed migration. |
|
|
12
|
+
| [#96](https://github.com/yuichkun/unworklet/issues/96) | duration=896/48000 executed 8 blocks instead of 7. | Exact 896- and 256-sample durations execute 7 and 2 default quanta. |
|
|
13
|
+
| [#98](https://github.com/yuichkun/unworklet/issues/98) | Unity DC measured 6.021 dB. | Unity DC measures 0 dB; the helper rejects the doubled-gain expectation. Direct sample comparisons remain independent checks. |
|
|
14
|
+
| [#104](https://github.com/yuichkun/unworklet/issues/104) | A default config `{name:'default'}` swallowed `{name:'x'}`, producing 7 instead of 1. | **Still reproduced.** Required config plus a separate explicit instance name renders 1; den continues to use this form. |
|
|
15
|
+
| [#106](https://github.com/yuichkun/unworklet/issues/106) | A lang type import under TS 5.9.3 ESNext failed with Volar TS2416. | Both ES2023 and ESNext pass strict checking with `skipLibCheck:false`. The consumer keeps ES2023 without a declaration shim. |
|
|
16
|
+
|
|
17
|
+
The 0.5.0 and 0.6.0 [upstream changelog](https://github.com/yuichkun/unworklet/blob/v0.6.0/CHANGELOG.md)
|
|
18
|
+
describes the snapshot, duration, gain, declaration and compilation changes.
|
|
19
|
+
`tests/dependency.spec.ts` and the packed browser entry test independently check
|
|
20
|
+
the paths above. `expectAudioMatches` accepts an independently calculated
|
|
21
|
+
one-sample memory and rejects an intentionally incorrect answer. No audio golden
|
|
22
|
+
is regenerated or approved by these tests.
|
|
23
|
+
|
|
24
|
+
Reviewed but not independently reproduced here: #95 (snapshot matcher name reuse)
|
|
25
|
+
and #97 (integer WAV golden scale). This gate uses float WAV output and direct
|
|
26
|
+
sample assertions. Build/HMR reuse (#100/#101/#102), arbitrary schema migrations,
|
|
27
|
+
SIMD, DevTools and other APIs remain outside this fixture's coverage.
|
|
28
|
+
|
|
29
|
+
## Browser sample rate and render quantum
|
|
30
|
+
|
|
31
|
+
The packed entry consumer renders at **44100, 48000 and 96000 Hz** through the
|
|
32
|
+
public `?worklet` import and `createNode`. At non-default rates, unworklet 0.6.0
|
|
33
|
+
compiles in its generated Web Worker for the actual AudioContext. The test checks
|
|
34
|
+
real output, live gain changes and snapshot restoration at each rate, and records
|
|
35
|
+
the actual rate and quantum in the browser evidence. Offline sample-rate coverage
|
|
36
|
+
is recorded separately; it is not substituted for browser evidence.
|
|
37
|
+
|
|
38
|
+
A processor compiled directly, including the playground's compiler worker, must
|
|
39
|
+
receive `{ sampleRate: context.sampleRate, renderQuantumSize:
|
|
40
|
+
context.renderQuantumSize ?? 128 }`. Its runtime metadata and schema hash must
|
|
41
|
+
come from that compile result. The playground browser gate separately checks
|
|
42
|
+
44.1/48/96 kHz with 32/128/256-sample quanta. Fixed-length DSP buffer traversal uses
|
|
43
|
+
`forIndex`; FFT lengths and sample-based hop periods are not render quanta.
|
|
44
|
+
See the [spectral contract](spectral.md) for snapshot dispatch behavior.
|
|
45
|
+
|
|
46
|
+
These tests establish the listed contexts and operations, not arbitrary device
|
|
47
|
+
performance or realtime deadline clearance. Cross-rate snapshot continuation and
|
|
48
|
+
human sound approval remain outside this gate.
|
|
52
49
|
|
|
53
50
|
## Tooling security
|
|
54
51
|
|
|
55
|
-
Vite
|
|
52
|
+
Vite remains pinned to **7.3.6** in the package and isolated consumers. The former
|
|
56
53
|
7.3.1 pin was affected by
|
|
57
54
|
[GHSA-p9ff-h696-f583](https://github.com/vitejs/vite/security/advisories/GHSA-p9ff-h696-f583)
|
|
58
|
-
and other advisories
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
and other reported advisories. The gate serves a production preview on
|
|
56
|
+
`127.0.0.1`; it does not expose a development server to the network. Locks retain
|
|
57
|
+
the patched esbuild dependency. Root and consumer audit results are checked after
|
|
58
|
+
installation.
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
# Prepared-spectrum convolution candidate
|
|
2
2
|
|
|
3
3
|
`@denaudio/den/prepared-convolution` exports `preparedConvolution`, the host-only
|
|
4
|
-
`prepareConvolutionSpectrum`, and `PreparedConvolutionConfig
|
|
5
|
-
`PreparedConvolutionPacket`.
|
|
4
|
+
`prepareConvolutionSpectrum`, and the types `PreparedConvolutionConfig`,
|
|
5
|
+
`PreparedConvolutionRuntimeConfig` and `PreparedConvolutionPacket`. The original
|
|
6
|
+
configuration type remains the host spectrum shape; the runtime configuration
|
|
7
|
+
extends it with the required render quantum. This is an offline-validated **CANDIDATE**, without
|
|
6
8
|
browser, realtime-capacity or human-audition clearance.
|
|
7
9
|
|
|
8
10
|
## Fixed boundary
|
|
9
11
|
|
|
10
|
-
- Mono, block size B=128, FFT N=256, P=64 partitions.
|
|
11
|
-
`{ blockSize: 128, partitions: 64 }
|
|
12
|
+
- Mono, block size B=128, FFT N=256, P=64 partitions. Host preparation accepts
|
|
13
|
+
`{ blockSize: 128, partitions: 64 }`. DSP instantiation additionally requires
|
|
14
|
+
`renderQuantumSize: ctx.renderQuantumSize`; unsupported configurations throw.
|
|
15
|
+
The host spectrum and packet format do not depend on the quantum.
|
|
12
16
|
- Original IR: 1..8192 finite real taps, each in [-1,1], with absolute sum <=4.
|
|
13
17
|
No normalization, truncation, sample-rate conversion or output limiter.
|
|
14
18
|
- Audio input must be finite and normalized to [-1,1]. Output can exceed unity.
|
|
@@ -54,7 +58,8 @@ import {
|
|
|
54
58
|
} from '@denaudio/den/prepared-convolution';
|
|
55
59
|
|
|
56
60
|
const convolver = instantiate(preparedConvolution,
|
|
57
|
-
{ blockSize: 128, partitions: 64
|
|
61
|
+
{ blockSize: 128, partitions: 64, renderQuantumSize: ctx.renderQuantumSize },
|
|
62
|
+
{ name: 'convolver' });
|
|
58
63
|
const ir = event<PreparedConvolutionPacket>({
|
|
59
64
|
from: 'main', name: 'ir', capacity: CAPACITY_16, payloadCapacity: 131072,
|
|
60
65
|
});
|
|
@@ -109,8 +114,8 @@ practical test budget, not a proven bound for every implementation/environment.
|
|
|
109
114
|
## Load, reset, state and scheduling
|
|
110
115
|
|
|
111
116
|
`tick(input, reset, everyNSamples)` returns `{ output, loaded, rejected,
|
|
112
|
-
revision }`. Call it once per sample inside stride-1 native forSample.
|
|
113
|
-
|
|
117
|
+
revision }`. Call it once per sample inside stride-1 native forSample.
|
|
118
|
+
Its fixed buffer loops use `forIndex` independently of the audio quantum.
|
|
114
119
|
|
|
115
120
|
- Valid full packet: loaded=true, rejected=false. A shorter IR still carries
|
|
116
121
|
the complete padded 32768-value table, so previous coefficients are replaced.
|
|
@@ -128,12 +133,16 @@ implementation requires the core 0.4.1 128-sample quantum.
|
|
|
128
133
|
- Missing/unloaded/rejected assets are silent. Invalidation is logical, not
|
|
129
134
|
secure erasure; bytes may remain in buffers/snapshots until overwritten.
|
|
130
135
|
|
|
131
|
-
FFT+64-partition MAC+IFFT+OLA run eagerly
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
136
|
+
FFT+64-partition MAC+IFFT+OLA run eagerly every `gcd(128, renderQuantumSize)`
|
|
137
|
+
samples, including while unloaded. Only the persistent cursor's true B-sample
|
|
138
|
+
boundaries commit spectral history, overlap, counters and pending reset. The
|
|
139
|
+
native scheduler realigns after any block-atomic snapshot, even inside a hop.
|
|
140
|
+
Indices are explicit; fixed `forIndex` loops avoid graph expansion by P*N.
|
|
141
|
+
At quantum 32 the eager workload is four times that at 128; coprime quanta can
|
|
142
|
+
increase it by 128 times. These targets do not imply realtime capacity.
|
|
143
|
+
Named persistent cursor, spectral cursor, valid counts, coefficients/status/
|
|
144
|
+
revision, histories and overlap fully describe continuation.
|
|
145
|
+
Same-graph/config/rate/quantum native snapshots continue bit-identically. Pending queued
|
|
137
146
|
messages, cross-configuration migration and live-swap crossfades are outside
|
|
138
147
|
this contract. Transient FFT/mix buffers are overwritten before use.
|
|
139
148
|
|
|
@@ -8,7 +8,8 @@ a phase vocoder, formant preservation or a transparent general-purpose stretcher
|
|
|
8
8
|
|
|
9
9
|
## Entry, native ownership and cost
|
|
10
10
|
|
|
11
|
-
Instantiate with `sampleRate: ctx.sampleRate` (integer 8000..192000),
|
|
11
|
+
Instantiate with `sampleRate: ctx.sampleRate` (integer 8000..192000),
|
|
12
|
+
`renderQuantumSize: ctx.renderQuantumSize` (positive integer), an existing
|
|
12
13
|
`ResidentSample`, `hopSamples: 128 | 256` (default 256) and
|
|
13
14
|
`searchFrames: 0 | 8 | 16 | 32 | 64 | 128` (default 64). H is the synthesis hop in
|
|
14
15
|
output frames; the two overlapping source grains each span 2 H output frames.
|
|
@@ -30,13 +31,16 @@ buffer. Resident PCM and ingress allocation are shared, not copied per instance.
|
|
|
30
31
|
Sixteen maximum ingress slots reserve 4 MiB, plus 256 KiB resident PCM.
|
|
31
32
|
|
|
32
33
|
The graph uses bounded native counted loops, not expanded code per PCM frame or
|
|
33
|
-
search candidate. Every 128 output samples it
|
|
34
|
+
search candidate. Every `gcd(H, 128, renderQuantumSize)` output samples it
|
|
35
|
+
evaluates 64 reference reads and
|
|
34
36
|
(2 S+1)×64 candidate comparisons; each comparison uses zero-padded linear sampling.
|
|
35
|
-
|
|
36
|
-
when stopped, is deliberate: unworklet
|
|
37
|
+
Only true H-sample boundaries commit evaluations. This eager scheduling,
|
|
38
|
+
including when stopped, is deliberate: unworklet's native hop counters are not snapshot
|
|
37
39
|
state, whereas the module's persistent H clock is. Muting/gate-off does not remove
|
|
38
40
|
search cost. S0 still evaluates the zero-offset baseline. Maximum S128 means
|
|
39
|
-
16,448 comparisons per
|
|
41
|
+
16,448 comparisons per dispatch. Quantum 32 causes four times the work per
|
|
42
|
+
sample of quantum 128; a quantum coprime to 128 can increase it by 128 times.
|
|
43
|
+
There is no average-cost CPU-saving claim or realtime clearance.
|
|
40
44
|
|
|
41
45
|
## Controls, launch and transitions
|
|
42
46
|
|
|
@@ -120,7 +124,9 @@ All launch/clock/time/grain/EOF and resident state use native persistent snapsho
|
|
|
120
124
|
comparison, score and output scratch are transient. Restoration is bit-identical
|
|
121
125
|
only for the same graph, configuration, resident metadata and rate, at the native
|
|
122
126
|
quantum boundaries. In particular, H256 halfway-hop and pending launches must
|
|
123
|
-
continue exactly after
|
|
127
|
+
continue exactly after any native block-aligned snapshot at the same rate and
|
|
128
|
+
quantum. The persistent clock retains hop phase while the native dispatch
|
|
129
|
+
period divides the actual quantum. No serializer or migration layer is
|
|
124
130
|
introduced.
|
|
125
131
|
|
|
126
132
|
## Audible and acceptance limits
|
|
@@ -22,12 +22,13 @@ catalog categories. Existing frozen-tail support remains separate.
|
|
|
22
22
|
|
|
23
23
|
One new subpath, `@denaudio/den/spatial-chains`; no root-entry re-export:
|
|
24
24
|
|
|
25
|
-
- `hybridReverb({ sampleRate }).tick(input, controls, everyNSamples)` returns
|
|
25
|
+
- `hybridReverb({ sampleRate, renderQuantumSize }).tick(input, controls, everyNSamples)` returns
|
|
26
26
|
`{ left, right }`.
|
|
27
|
-
- `feedforwardPitchedReverb({ sampleRate, windowSamples? }).tick(input,
|
|
27
|
+
- `feedforwardPitchedReverb({ sampleRate, renderQuantumSize, windowSamples? }).tick(input,
|
|
28
28
|
controls, everyNSamples)` returns `{ left, right, ratioRejected }`.
|
|
29
29
|
- `SpatialChainConfig`: integer `sampleRate` in 8000..192000, taken from the
|
|
30
|
-
enclosing `ctx.sampleRate
|
|
30
|
+
enclosing `ctx.sampleRate`, and positive integer `renderQuantumSize` from
|
|
31
|
+
`ctx.renderQuantumSize` for the early convolution's snapshot-safe dispatch.
|
|
31
32
|
- `FeedforwardPitchedReverbConfig`: same rate plus even integer `windowSamples`
|
|
32
33
|
in 32..16384, default 2048. This is the pitch delay excursion, not a grain or
|
|
33
34
|
FFT-frame duration.
|
|
@@ -147,7 +148,7 @@ advances the pitched entry's readers. Retrigger resets both pitch phases before
|
|
|
147
148
|
their current read, retains history and can click. No smoothing or universally
|
|
148
149
|
click-free automation, reset, retrigger or bypass is claimed.
|
|
149
150
|
|
|
150
|
-
Use only native same-rate, same-schema snapshots. Every inherited persistent
|
|
151
|
+
Use only native same-rate, same-quantum, same-schema snapshots. Every inherited persistent
|
|
151
152
|
history/cursor/valid count, convolution scheduling state, FDN damping state and
|
|
152
153
|
pitch ratio/phase persists; transient scratch must be rewritten before use.
|
|
153
154
|
Restore requires the same configuration and coordinated input/control timeline.
|
|
@@ -8,7 +8,7 @@ sustain, phase-vocoder or phase-locking claim.
|
|
|
8
8
|
|
|
9
9
|
## Public contract
|
|
10
10
|
|
|
11
|
-
`spectralFreeze({ size, hopSize }).tick(input, freeze, reset, everyNSamples)`
|
|
11
|
+
`spectralFreeze({ size, hopSize, renderQuantumSize: ctx.renderQuantumSize }).tick(input, freeze, reset, everyNSamples)`
|
|
12
12
|
|
|
13
13
|
- Instantiate once with an explicit subgraph name. Call exactly once per sample
|
|
14
14
|
inside stride-1 `forSample`, supplying its existing `everyNSamples`.
|
|
@@ -79,15 +79,17 @@ Persistent state contains the existing history/overlap/cursor/reset state, two
|
|
|
79
79
|
(N/2+1)-element f64 capture planes, the hold flag, held phase and a private
|
|
80
80
|
modulo-H sample clock. The latter mirrors the reviewed framing cursor%H; neither
|
|
81
81
|
clock changes phase on reset. Frame controls and capture decisions are transient.
|
|
82
|
-
Full snapshots continue exactly for the identical graph, configuration
|
|
82
|
+
Full snapshots continue exactly for the identical graph, configuration, rate
|
|
83
|
+
and quantum.
|
|
83
84
|
Tests cover inactive, frozen, post-reset and release/rearm states at every
|
|
84
|
-
|
|
85
|
-
promise is added.
|
|
86
|
-
|
|
87
|
-
The shared STFT dispatch
|
|
88
|
-
hook and IFFT
|
|
89
|
-
|
|
90
|
-
|
|
85
|
+
native block-aligned snapshot offset class, including non-128 boundaries.
|
|
86
|
+
No cross-rate, cross-quantum or cross-configuration migration promise is added.
|
|
87
|
+
|
|
88
|
+
The shared STFT dispatch runs every `gcd(H, 128, renderQuantumSize)` samples.
|
|
89
|
+
The FFT, hook and IFFT execute eagerly, but only true t=mH frames capture,
|
|
90
|
+
advance the held phase or commit overlap. These extra hook calls do not change
|
|
91
|
+
persistent capture/hold phase. No CPU saving is implied by a select; smaller or
|
|
92
|
+
coprime quanta increase workload as documented in the [spectral contract](spectral.md).
|
|
91
93
|
|
|
92
94
|
## Headroom derivation
|
|
93
95
|
|
|
@@ -7,7 +7,7 @@ This is one bounded spectral effect built on the independently reviewed
|
|
|
7
7
|
|
|
8
8
|
## API and meaning
|
|
9
9
|
|
|
10
|
-
- `spectralGate({ size, hopSize }).tick(input, threshold, floor, reset, everyNSamples)`
|
|
10
|
+
- `spectralGate({ size, hopSize, renderQuantumSize: ctx.renderQuantumSize }).tick(input, threshold, floor, reset, everyNSamples)`
|
|
11
11
|
- Construction-fixed power-of-two N=8..1024 and H=N/2 or N/4.
|
|
12
12
|
- Input: finite normalized f32 audio in [-1,1]. Controls: f32. Threshold is clamped to [0,max-f32], floor to [0,1];
|
|
13
13
|
infinities saturate, NaN threshold becomes 0 and NaN floor becomes 1 (all-pass).
|
|
@@ -29,9 +29,11 @@ This is one bounded spectral effect built on the independently reviewed
|
|
|
29
29
|
## Framing, state and headroom
|
|
30
30
|
|
|
31
31
|
Retain the original periodic sqrt-Hann analysis/synthesis windows, normalization
|
|
32
|
-
2H/N, reset discard semantics
|
|
33
|
-
|
|
34
|
-
commits frames.
|
|
32
|
+
2H/N, reset discard semantics and persistent cursor. Dispatch now follows
|
|
33
|
+
`gcd(H, 128, renderQuantumSize)` to align every native snapshot boundary; only
|
|
34
|
+
cursor%H==0 commits frames. Off-phase FFT/IFFT work is eager. See the shared
|
|
35
|
+
[spectral contract](spectral.md) for increased costs at small or coprime quanta;
|
|
36
|
+
there is no H-rate CPU-saving claim.
|
|
35
37
|
|
|
36
38
|
The WOLA alignment/reference delay is N samples; a processed transient can spread
|
|
37
39
|
across its synthesis frame [t,t+N), so its first nonzero sample need not occur at
|
|
@@ -43,7 +45,7 @@ old history/overlap and leaves absolute frame phase unchanged.
|
|
|
43
45
|
|
|
44
46
|
Only the existing history, overlap, cursor, valid history and pending reset need
|
|
45
47
|
persistent storage. Frame controls and spectrum work are transient and overwritten
|
|
46
|
-
before use. Identical graph/rate full-snapshot continuation remains in scope.
|
|
48
|
+
before use. Identical graph/rate/quantum full-snapshot continuation remains in scope.
|
|
47
49
|
|
|
48
50
|
Attenuating bins can increase individual time-domain peaks. For normalized input,
|
|
49
51
|
an ideal contraction bound per frame and the overlapping windows give the
|
|
@@ -52,10 +54,12 @@ not a limiter or loudness promise; measured peaks accompany candidate evidence.
|
|
|
52
54
|
|
|
53
55
|
## Implementation boundary and verification
|
|
54
56
|
|
|
55
|
-
|
|
57
|
+
The original gate entry added a narrow optional internal construction-time
|
|
58
|
+
spectrum-copy hook to the
|
|
56
59
|
existing STFT function. The identity branch must produce the same graph, schema,
|
|
57
|
-
PCM and full snapshots as its frozen predecessor.
|
|
58
|
-
|
|
60
|
+
PCM and full snapshots as its frozen predecessor. The 0.2.0 compatibility
|
|
61
|
+
migration separately replaces fixed-quantum FFT loops and extends snapshot-safe
|
|
62
|
+
dispatch; its numerical and continuation assertions preserve this gate behavior. The public gate wrapper is separate; shared
|
|
59
63
|
exports, CI and site belong to integration.
|
|
60
64
|
|
|
61
65
|
Required tests: independent direct DFT/bin-mask/inverse-DFT/time-indexed WOLA;
|
|
@@ -22,6 +22,13 @@ for windowed framing, snapshots, practical concurrency or realtime acceptance.
|
|
|
22
22
|
|
|
23
23
|
## Persistent frame phase
|
|
24
24
|
|
|
25
|
+
The measurements and dispatch description below record the original 0.4.1
|
|
26
|
+
candidate at quantum 128. Since den 0.2.0, callers pass `ctx.renderQuantumSize`
|
|
27
|
+
and dispatch is `gcd(H, 128, renderQuantumSize)`; fixed FFT loops use `forIndex`.
|
|
28
|
+
The [current spectral contract](spectral.md) describes snapshot behavior and
|
|
29
|
+
increased work for smaller or coprime quanta. Historical timings below are not
|
|
30
|
+
measurements of those targets.
|
|
31
|
+
|
|
25
32
|
Public unworklet 0.4.1 has no condition-driven statement block. Its select is
|
|
26
33
|
eager; everyNSamples/byN bounds are construction-fixed. Native everyNSamples
|
|
27
34
|
counters are omitted from snapshots. Simply changing a hop to256 or512 would
|
|
@@ -7,6 +7,9 @@ locking, human sound approval, or real-time clearance.
|
|
|
7
7
|
|
|
8
8
|
## Shared framing
|
|
9
9
|
|
|
10
|
+
- Pass `renderQuantumSize: ctx.renderQuantumSize` in both module configurations.
|
|
11
|
+
Dispatch follows the shared [spectral contract](spectral.md), preserving
|
|
12
|
+
block-atomic snapshot phase at the actual render quantum.
|
|
10
13
|
- Construction-fixed power-of-two N=8..256, H=N/2 or N/4. Rebuild to change.
|
|
11
14
|
- Instantiate with an explicit subgraph name and call exactly once per sample
|
|
12
15
|
inside stride-1 `forSample`, passing its native `everyNSamples`.
|
|
@@ -16,11 +19,11 @@ locking, human sound approval, or real-time clearance.
|
|
|
16
19
|
synthesis, forward unnormalized DFT, inverse 1/N, WOLA scale 2H/N, internal
|
|
17
20
|
2^128 scaling, and live reference delay N.
|
|
18
21
|
- A committed frame at t=mH consumes [t-N,t), excludes current input, and samples
|
|
19
|
-
controls at t. Missing startup/reset history is zero padded.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
controls at t. Missing startup/reset history is zero padded. Dispatch occurs
|
|
23
|
+
every `gcd(H, 128, renderQuantumSize)` samples; off-phase work is eager and
|
|
24
|
+
commits no overlap. At the original quantum 128, H<=128 needs no off-phase
|
|
25
|
+
calls. Smaller or coprime quanta add work; the N1024 single-input evidence is
|
|
26
|
+
not inherited by this two-framer composition. N<=256 retains its initial bound.
|
|
24
27
|
- Clamp amount to [0,1]; NaN becomes 0, infinities clamp. Interpolation is linear
|
|
25
28
|
in bin magnitude, not dB, power, audio time, or equal-power gain. It replaces
|
|
26
29
|
newly committed frames; previous overlap remains until it finishes within N.
|
|
@@ -31,7 +34,7 @@ locking, human sound approval, or real-time clearance.
|
|
|
31
34
|
|
|
32
35
|
## Frequency-magnitude blur
|
|
33
36
|
|
|
34
|
-
`spectralBlur({ size, hopSize, radius }).tick(input, amount, reset, everyNSamples)`
|
|
37
|
+
`spectralBlur({ size, hopSize, radius, renderQuantumSize }).tick(input, amount, reset, everyNSamples)`
|
|
35
38
|
|
|
36
39
|
Radius is a fixed integer 1..min(8,N/2-1). Let X[k] be the full two-sided complex
|
|
37
40
|
DFT and A[k]=|X[k]|. Define a circular triangular blur on the raw, two-sided DFT
|
|
@@ -76,7 +79,7 @@ not a time-memory blur or a freeze.
|
|
|
76
79
|
|
|
77
80
|
## Gain-bounded cross-synthesis
|
|
78
81
|
|
|
79
|
-
`spectralCrossSynthesis({ size, hopSize, maxGain }).tick(carrier, modulator, amount, reset, everyNSamples)`
|
|
82
|
+
`spectralCrossSynthesis({ size, hopSize, maxGain, renderQuantumSize }).tick(carrier, modulator, amount, reset, everyNSamples)`
|
|
80
83
|
|
|
81
84
|
maxGain is a finite construction-fixed number in [1,16]. Let C[k], M[k] be the
|
|
82
85
|
synchronized carrier/modulator frames, A[k]=|C[k]|, and B[k]=|M[k]|:
|
|
@@ -124,11 +127,12 @@ per instance. Magnitudes, FFT buffers, windows and sampled controls are transien
|
|
|
124
127
|
and are fully recomputed before use at the next frame. No cross-frame blur
|
|
125
128
|
memory or private snapshot format is introduced.
|
|
126
129
|
|
|
127
|
-
Only full native same-graph/configuration/sample-rate snapshots are supported.
|
|
130
|
+
Only full native same-graph/configuration/sample-rate/quantum snapshots are supported.
|
|
128
131
|
Restoring only one member of a cross-synthesis pair is outside the contract.
|
|
129
132
|
Tests must prove exact continuation across every legal H class at native
|
|
130
|
-
|
|
131
|
-
reset. No cross-configuration/schema/rate
|
|
133
|
+
block boundaries, including non-128-aligned boundaries, active processing,
|
|
134
|
+
pending reset, and held reset. No cross-configuration/schema/rate/quantum
|
|
135
|
+
migration is promised.
|
|
132
136
|
|
|
133
137
|
## Ideal headroom and validation
|
|
134
138
|
|
package/docs/spectral.md
CHANGED
|
@@ -7,23 +7,26 @@ realtime clearance is implied.
|
|
|
7
7
|
|
|
8
8
|
## Entry contract
|
|
9
9
|
|
|
10
|
-
`stftIdentity({ size, hopSize }).tick(input, reset, everyNSamples)` is an ordinary
|
|
10
|
+
`stftIdentity({ size, hopSize, renderQuantumSize: ctx.renderQuantumSize }).tick(input, reset, everyNSamples)` is an ordinary
|
|
11
11
|
unworklet subgraph. Instantiate it once with an explicit name. Call tick exactly
|
|
12
12
|
once per sample from stride-1 `forSample((i, everyNSamples) => ...)`. The last
|
|
13
13
|
argument is unworklet's existing conditional scheduler, not a den scheduler or a
|
|
14
14
|
runtime JavaScript callback. `select` is never used to skip the FFT workload.
|
|
15
15
|
|
|
16
|
+
- Pass the enclosing processor's `ctx.renderQuantumSize` in the configuration.
|
|
17
|
+
It is a positive integer, matching unworklet's compile target, not the FFT size
|
|
18
|
+
or hop. All STFT-based modules use this value for snapshot-safe dispatch.
|
|
16
19
|
- Supported N is a power of two in 8..1024. H is N/2 or N/4. These are
|
|
17
20
|
offline-candidate bounds, not a performance or concurrency recommendation.
|
|
18
21
|
- The internal, non-public complex radix-2 kernel has natural-order real/imaginary
|
|
19
22
|
planes, forward exponent -i2πkn/N, inverse exponent +i2πkn/N and inverse scale 1/N.
|
|
20
23
|
All intermediate work/history use fixed f64 buffers; I/O is f32. The stream
|
|
21
24
|
uses an exact 2^128 scale internally to avoid 0.4.1's 1e-30 buffer-write
|
|
22
|
-
flush discarding quiet f32 inputs. Stage
|
|
23
|
-
graph construction. Within each stage, the public
|
|
24
|
-
|
|
25
|
-
updates to sample time. The
|
|
26
|
-
|
|
25
|
+
flush discarding quiet f32 inputs. Stage JavaScript loops execute during
|
|
26
|
+
graph construction. Within each stage, the public `forIndex(count)` loop
|
|
27
|
+
visits exactly the work-buffer capacity, independently of the render quantum,
|
|
28
|
+
without audio/parameter reads or updates to sample time. The outer `forSample`
|
|
29
|
+
loop alone follows the audio quantum. Twiddle step constants are calculated at build
|
|
27
30
|
time, then a bounded f64 recurrence reanchors to (1,0) at every butterfly group.
|
|
28
31
|
- A frame at sample t=mH consumes samples [t-N,t), with absent history set to
|
|
29
32
|
zero. It is added to output samples [t,t+N). Output therefore has exactly N
|
|
@@ -41,14 +44,17 @@ runtime JavaScript callback. `select` is never used to skip the FFT workload.
|
|
|
41
44
|
or new storage/snapshot system is supplied.
|
|
42
45
|
- Native persistent history/overlap/cursor/valid/reset state is saved. FFT and
|
|
43
46
|
spectral scratch are transient and fully overwritten at each scheduled frame.
|
|
44
|
-
Only identical graph/rate full-snapshot continuation is in scope;
|
|
45
|
-
and cross-rate continuation are not promised.
|
|
46
|
-
Native
|
|
47
|
-
|
|
48
|
-
boundary
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
Only identical graph/rate/quantum full-snapshot continuation is in scope;
|
|
48
|
+
migration and cross-rate or cross-quantum continuation are not promised.
|
|
49
|
+
Native hop counters are not included in snapshots. Dispatch therefore occurs
|
|
50
|
+
every `gcd(H, 128, renderQuantumSize)` samples, a divisor of both the hop and
|
|
51
|
+
every block-atomic restore boundary. The persistent cursor modulo H selects
|
|
52
|
+
frame commits. Off-phase FFT results are computed eagerly and discarded.
|
|
53
|
+
At quantum 128 this retains the former dispatch period. For H>=128, quantum
|
|
54
|
+
32 evaluates four times as many FFT pairs per sample; a quantum coprime to
|
|
55
|
+
128 dispatches every sample, up to 128 times the old workload. Correct output
|
|
56
|
+
and snapshot phase do not imply realtime feasibility. See
|
|
57
|
+
[large-frame evidence](spectral-large-candidate.md) for historical costs.
|
|
52
58
|
|
|
53
59
|
## Verification required before integration
|
|
54
60
|
|
|
@@ -56,13 +62,16 @@ Independent direct O(N²) DFT covers complex impulse, DC, Nyquist, positive and
|
|
|
56
62
|
negative bins, off-bin tone, seeded inputs, signs, natural ordering, conjugate
|
|
57
63
|
symmetry and inverse normalization. Independent time-indexed DFT/WOLA covers
|
|
58
64
|
startup, every hop phase, held/arbitrary resets, block boundaries and three
|
|
59
|
-
offline rates (44.1/48/96 kHz).
|
|
60
|
-
|
|
65
|
+
offline rates (44.1/48/96 kHz). The rate/quantum matrix includes quantum
|
|
66
|
+
32/128/256, independent numerical oracles and exact quantum-128 PCM comparisons.
|
|
67
|
+
Exact same-schema snapshots include non-128-aligned boundaries at quantum 32
|
|
68
|
+
and non-power-of-two quantum 48. Reset discard is checked separately. Larger construction sizes require measured capture/compile
|
|
61
69
|
cost, WASM size, memory and quantum-burst timing before becoming public bounds.
|
|
62
70
|
|
|
63
71
|
Node driver measurements describe that environment only, including whether copies
|
|
64
72
|
were included. They do not certify browser deadlines, concurrency or a device
|
|
65
|
-
budget. The
|
|
73
|
+
budget. The playground now compiles for its actual AudioContext sample rate and
|
|
74
|
+
render quantum using unworklet 0.6.0; this does not grant realtime clearance.
|
|
66
75
|
|
|
67
76
|
## Initial local evidence
|
|
68
77
|
|
package/docs/voice-policy.md
CHANGED
|
@@ -31,8 +31,8 @@ All voice and held-key storage uses unworklet transient state. Snapshots exclude
|
|
|
31
31
|
|
|
32
32
|
## Verification
|
|
33
33
|
|
|
34
|
-
The numerical fixture renders voice state through diagnostic outputs using published unworklet 0.
|
|
34
|
+
The numerical fixture renders voice state through diagnostic outputs using published unworklet 0.6.0. It checks FIFO duplicates, stolen-note releases, release reuse, mono priority/legato/retrigger, channel controls, reset, independent instances, bounded overflow and repeated reuse. These outputs are diagnostics, not audition audio or approved goldens. Offline 44.1/48/96 kHz tests are separate from the [browser entry coverage](dependency-findings.md#browser-sample-rate-and-render-quantum).
|
|
35
35
|
|
|
36
|
-
Upstream
|
|
36
|
+
Upstream [#99](https://github.com/yuichkun/unworklet/issues/99) was fixed in unworklet 0.5.0: `expectMidiBalance` treats velocity-zero note-on as note termination. The 0.6.0 regression accepts a positive-velocity note-on followed by velocity-zero note-on, and rejects both a hanging note and an additional unmatched note-off. Den retains its independent numerical gate/identity assertions. The legacy half-sample duration margin remains in this fixture; exact-duration behavior is checked separately in the [dependency regressions](dependency-findings.md).
|
|
37
37
|
|
|
38
38
|
Capacity probes also exposed recursive expression expansion in unworklet's analysis of nested selects. Transient scratch accumulators keep the traversal and selection expressions shallow. Before this change the default-capacity test was stopped after more than one minute of CPU work; afterward the full numerical suite including capacity probes ran in seconds. A 64-voice/256-held diagnostic graph failed WebAssembly compilation with `local count too large`; the supported construction ceiling is therefore 32 voices, tested with 256 held identities. Larger combined instrument graphs still require their own compile and runtime budget checks. The current release-completion path performs rank maintenance across slots; passing these fixtures does not establish a real-time CPU budget for a complete instrument.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@denaudio/den",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Composable DSP modules and instrument/delay engines for unworklet",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT OR Apache-2.0",
|
|
@@ -251,14 +251,14 @@
|
|
|
251
251
|
"test:release": "node --test tests/release.test.mjs tests/release-package.test.mjs"
|
|
252
252
|
},
|
|
253
253
|
"peerDependencies": {
|
|
254
|
-
"@unworklet/core": "0.
|
|
254
|
+
"@unworklet/core": "0.6.0"
|
|
255
255
|
},
|
|
256
256
|
"devDependencies": {
|
|
257
|
-
"@unworklet/core": "0.
|
|
258
|
-
"@unworklet/lang": "0.
|
|
259
|
-
"@unworklet/offline": "0.
|
|
260
|
-
"@unworklet/test": "0.
|
|
261
|
-
"@unworklet/unplugin": "0.
|
|
257
|
+
"@unworklet/core": "0.6.0",
|
|
258
|
+
"@unworklet/lang": "0.6.0",
|
|
259
|
+
"@unworklet/offline": "0.6.0",
|
|
260
|
+
"@unworklet/test": "0.6.0",
|
|
261
|
+
"@unworklet/unplugin": "0.6.0",
|
|
262
262
|
"playwright": "1.60.0",
|
|
263
263
|
"typescript": "5.9.3",
|
|
264
264
|
"vite": "7.3.6",
|