@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 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/3042e73435986fa2804f5811604e784d93470c1f/playground/README.md).
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.4.1** as a peer. All
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
- Snapshots require the same processor/schema/sample rate. See the
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).
@@ -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. */
@@ -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
- everyNSamples(block, () => {
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
- previousReal.write(spectrumCursor.read().mul(size).add(k), transformed.real(k));
50
- previousImag.write(spectrumCursor.read().mul(size).add(k), transformed.imag(k));
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
- overlap.write(index, select(pendingReset.read(), f64(0), overlap.read(index)).add(frame.real(n)));
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(false);
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: PreparedConvolutionConfig], {
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
- everyNSamples(B, () => {
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
- overlap.write(index, select(pendingReset.read(), f64(0), overlap.read(index)).add(result.real(n)));
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(false);
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
- // 0.4.1 does not snapshot native everyNSamples counters. A 128-sample
74
- // scheduler realigns at every allowed snapshot boundary; persistent clock
75
- // carries H=256 phase. Search is eager every 128, even when not committing.
76
- everyNSamples(128, () => {
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()));
@@ -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
  }
@@ -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,
@@ -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-128 native forSample loop over work-buffer indices, nested
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.
@@ -1,20 +1,30 @@
1
- import { SAMPLES_PER_BLOCK, defineSubgraph, f64, forSample, i32, select, state } from '@unworklet/core';
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 (SAMPLES_PER_BLOCK !== 128 || size < 4 || size > 1024 || !Number.isInteger(Math.log2(size))) {
7
- throw new RangeError('spectral buffer iteration requires fixed-128 loops and power-of-two size [4,1024]');
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
- const stride = Math.max(1, 128 / size);
10
- for (let chunk = 0; chunk < Math.max(1, size / 128); chunk++)
11
- forSample.byN(stride, inner => body(inner.div(stride).add(chunk * 128)));
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-128 native forSample loop over work-buffer indices, nested
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. */
@@ -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. */
@@ -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
  };
@@ -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 hop counters are not snapshot state in 0.4.1. Dispatch at a
32
- // period dividing the 128-sample snapshot boundary; the named persistent
33
- // ring cursor carries the actual H phase. For H>128 this eagerly computes
34
- // an FFT pair every 128 samples, committing only at the true frame phase.
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(Math.min(hopSize, 128), () => {
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. */
@@ -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. For hops above128, FFT work still runs every
11
- * 128 samples; the persistent cursor controls frame commits and snapshot phase.
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. For hops above128, FFT work still runs every
7
- * 128 samples; the persistent cursor controls frame commits and snapshot phase.
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
- Keep unworklet at 0.4.1. The root entry does not re-export these candidate modules.
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
 
@@ -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. Dependencies stay at unworklet 0.4.1. The only initial-DSP source exception is
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
@@ -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 0.4.1 findings
1
+ # unworklet compatibility findings
2
2
 
3
- The upstream reports #68–106 were read as reports, not assumed verified. These
4
- checks use registry-installed 0.4.1 packages and the checked-in dependency locks.
5
- No unworklet repository or dependency source was modified.
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 | Independently observed here | Smallest den-local constraint |
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) | Real Chromium: gain initialized 0.5 but pre-render snapshot stores 0; editing to 0.25 while suspended still stores last-rendered 0.5. | Save after stable parameter values have rendered. Browser test verifies 0.5 → 0.25 → restored 0.5 in actual output. Suspended preset editing remains unsupported. |
10
- | [#79](https://github.com/yuichkun/unworklet/issues/79) | Restoring f32(1) into same-name i32 state yields 1065353216. | Gate accepts identical schema/rate only. Do not expose incompatible saved presets without reviewed migration tests. No den serializer added. |
11
- | [#96](https://github.com/yuichkun/unworklet/issues/96) | duration=896/48000 executes 8 blocks instead of 7. | Fixed gate durations 128/sampleRate and 256/sampleRate are verified by lengths/state; do not infer arbitrary durations are safe. |
12
- | [#98](https://github.com/yuichkun/unworklet/issues/98) | Unity DC fails the frequency-gain helper with 6.021 dB. | Direct independent sample comparisons; no FFT is needed for this gate. |
13
- | [#104](https://github.com/yuichkun/unworklet/issues/104) | Default config `{name:'default'}` swallows `{name:'x'}`, output 7 rather than 1. | Required config parameter plus explicit separate instance name renders 1. Used by gate and consumer. |
14
- | [#106](https://github.com/yuichkun/unworklet/issues/106) | Pure lang type import under TS 5.9.3 ESNext fails with Volar TS2416; ES2023 has zero diagnostics. | ES2023, strict=true, skipLibCheck=false. No type shim or blanket declaration skipping. |
15
-
16
- The pinned dependency reproduction assertions intentionally capture known wrong
17
- behavior. On dependency upgrade, investigate changes and replace them with correct
18
- regression expectations; do not preserve upstream bugs as product requirements.
19
-
20
- `expectAudioMatches` is exercised against an independently calculated one-sample
21
- memory and against an intentionally incorrect answer. The helper passes the former
22
- and rejects the latter. No audio snapshot matcher or golden update is used.
23
-
24
- Reviewed but **not reproduced** here: #95 (snapshot matcher name reuse), #97
25
- (integer WAV golden scale). The gate uses no approved golden; its WAVs use the
26
- existing float encoder, and numerical assertions inspect raw samples. Before future
27
- golden adoption, test these helper paths rather than assuming approval machinery.
28
- #100/#101/#102 concern build/HMR reuse; the gate uses a new build process and a new
29
- AudioContext, and does not claim HMR/repeated in-process build correctness.
30
- `.uwk` lowering, SIMD, MIDI dispatch, DevTools, arbitrary schema migration, and the
31
- other reported APIs are outside this fixture; their reports are not cleared by
32
- this PR. Later lanes must inspect and reproduce the bugs relevant to their APIs.
33
-
34
- ## Browser sample-rate scope (known, non-blocking)
35
-
36
- The supported entry consumer creates a **48000 Hz** AudioContext. Offline render
37
- coverage at 44100/48000/96000 is separate and does not prove browser support at
38
- those rates. The manifest names `offlineSampleRates`, `browserRenderedSampleRates`,
39
- and per-context `browserCoverage` explicitly.
40
-
41
- Real Chromium contexts at 44100 and 96000 are also probed: `createNode` refuses the
42
- plugin's 48000-Hz artifact, reporting the compiled and actual rates. This is the
43
- intentional safety guard associated with upstream
44
- [#22](https://github.com/yuichkun/unworklet/issues/22), not a den DSP failure. In
45
- published 0.4.1, unplugin calls `compile(processor)` without options and its public
46
- options expose only analysis-artifact and cross-origin-isolation flags. Core
47
- `compile` supports a rate for offline/custom bundling, but `createNode` does not
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 is pinned to **7.3.6** in both the package and isolated consumer. The earlier
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 returned by `npm audit`. The gate only serves a production
59
- preview on `127.0.0.1`, not a network-exposed development server; nevertheless the
60
- affected tooling has been replaced. Both lockfiles also resolve the patched
61
- esbuild dependency. Root and consumer audit results are checked after installation.
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`. This is an offline-validated **CANDIDATE**, without
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. Configuration must be exactly
11
- `{ blockSize: 128, partitions: 64 }`; unsupported configurations throw.
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 }, { name: 'convolver' });
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. This
113
- implementation requires the core 0.4.1 128-sample quantum.
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 once every 128 samples, including while
132
- unloaded. Indices are explicit; native counted loops avoid graph expansion by
133
- P*N. B equals quantum size, so the dispatch phase is identical at every native
134
- snapshot boundary. Named persistent cursor, spectral cursor, valid counts,
135
- coefficients/status/revision, histories and overlap fully describe continuation.
136
- Same-graph/config/rate native snapshots continue bit-identically. Pending queued
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), an existing
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 evaluates 64 reference reads and
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
- H256 commits only alternate evaluations. This eager 128 scheduling, including
36
- when stopped, is deliberate: unworklet 0.4.1's native hop counters are not snapshot
37
+ 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 128-sample quantum; there is no average-cost CPU-saving claim.
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 a 128-aligned snapshot. No serializer or migration layer is
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 and rate.
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
- 128-sample snapshot offset class. No cross-rate or cross-configuration migration
85
- promise is added.
86
-
87
- The shared STFT dispatch remains every min(H,128) samples. For H>128, the FFT,
88
- hook and IFFT still execute eagerly every128 samples; only true t=mH frames
89
- capture, advance the held phase or commit overlap. These extra hook calls do not
90
- change persistent capture/hold phase. No CPU saving is implied by a select.
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, persistent cursor, and every min(H,128) dispatch.
33
- For H>128 an FFT/IFFT pair still executes every128 samples; only cursor%H==0
34
- commits frames. No H-rate CPU-saving claim.
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
- Add a narrow optional internal construction-time spectrum-copy hook to the
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. Do not modify the FFT kernel,
58
- existing convolution or unworklet. The public gate wrapper is separate; shared
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. H<=128 divides
20
- the native 128-sample block; there are no eager noncommitting frame calls.
21
- The existing N1024 single-input clearance is not inherited: two full framers
22
- add cost and scheduling/state proof obligations. N<=256 bounds that initial
23
- composition and avoids H>128's eager noncommitting hooks entirely.
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
- 128-sample boundaries, including active processing, pending reset, and held
131
- reset. No cross-configuration/schema/rate migration is promised.
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/chunk JavaScript loops execute during
23
- graph construction. Within each stage, the public fixed-128 `forSample` loop
24
- iterates explicitly addressed work buffers, without audio/parameter reads or
25
- updates to sample time. The kernel checks `SAMPLES_PER_BLOCK===128`; other
26
- quantum sizes are unsupported. Twiddle step constants are calculated at build
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; migration
45
- and cross-rate continuation are not promised.
46
- Native 0.4.1 hop counters are not included in offline snapshots. Dispatch
47
- therefore occurs every min(H,128) samples, a divisor of the128-sample restore
48
- boundary; the named persistent cursor modulo H selects frame commits. For
49
- H>128 the FFT pair still executes every128 samples and off-phase results are
50
- discarded. This explicitly preserves snapshot phase rather than promising
51
- H-rate CPU savings. See [large-frame evidence](spectral-large-candidate.md).
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). Exact same-schema snapshots and reset discard are
60
- checked separately. Larger construction sizes require measured capture/compile
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 browser boundary remains unworklet 0.4.1's existing 48-kHz support.
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
 
@@ -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.4.1. 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 do not extend the browser gate's existing 48 kHz support claim.
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 issue #99 is independently reproduced: `expectMidiBalance` counts velocity-zero note-on as another held note. Den uses explicit numerical gate/identity assertions instead. The fixture stays inside the last render quantum to avoid the independently reproduced #96 duration rounding issue. No dependency changes or helper patches are made.
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.1.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.4.1"
254
+ "@unworklet/core": "0.6.0"
255
255
  },
256
256
  "devDependencies": {
257
- "@unworklet/core": "0.4.1",
258
- "@unworklet/lang": "0.4.1",
259
- "@unworklet/offline": "0.4.1",
260
- "@unworklet/test": "0.4.1",
261
- "@unworklet/unplugin": "0.4.1",
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",