@denaudio/den 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/LICENSE-APACHE +202 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +107 -2
  5. package/dist/biquad-eq.d.ts +11 -0
  6. package/dist/biquad-eq.js +35 -0
  7. package/dist/catalog-filter-math.d.ts +4 -0
  8. package/dist/catalog-filter-math.js +30 -0
  9. package/dist/character-filter.d.ts +13 -0
  10. package/dist/character-filter.js +53 -0
  11. package/dist/convolution-spectrum.d.ts +25 -0
  12. package/dist/convolution-spectrum.js +82 -0
  13. package/dist/convolution.d.ts +17 -0
  14. package/dist/convolution.js +87 -0
  15. package/dist/crossover.d.ts +11 -0
  16. package/dist/crossover.js +17 -0
  17. package/dist/curve-shaper.d.ts +26 -0
  18. package/dist/curve-shaper.js +73 -0
  19. package/dist/delay-fx.d.ts +37 -0
  20. package/dist/delay-fx.js +84 -0
  21. package/dist/delay-readhead.d.ts +19 -0
  22. package/dist/delay-readhead.js +54 -0
  23. package/dist/delay-settings.d.ts +55 -0
  24. package/dist/delay-settings.js +20 -0
  25. package/dist/drive.d.ts +24 -0
  26. package/dist/drive.js +116 -0
  27. package/dist/dual-head-delay.d.ts +28 -0
  28. package/dist/dual-head-delay.js +77 -0
  29. package/dist/dynamics.d.ts +61 -0
  30. package/dist/dynamics.js +130 -0
  31. package/dist/envelope.d.ts +34 -0
  32. package/dist/envelope.js +62 -0
  33. package/dist/filter.d.ts +9 -0
  34. package/dist/filter.js +34 -0
  35. package/dist/filterbank-vocoder.d.ts +35 -0
  36. package/dist/filterbank-vocoder.js +52 -0
  37. package/dist/formant-bank.d.ts +15 -0
  38. package/dist/formant-bank.js +28 -0
  39. package/dist/freeze-reverb.d.ts +35 -0
  40. package/dist/freeze-reverb.js +61 -0
  41. package/dist/frequency-shifter.d.ts +24 -0
  42. package/dist/frequency-shifter.js +57 -0
  43. package/dist/gate.d.ts +2 -0
  44. package/dist/gate.js +12 -0
  45. package/dist/index.d.ts +7 -0
  46. package/dist/index.js +12 -0
  47. package/dist/instrument-example.d.ts +12 -0
  48. package/dist/instrument-example.js +29 -0
  49. package/dist/instrument-settings.d.ts +95 -0
  50. package/dist/instrument-settings.js +36 -0
  51. package/dist/instrument.d.ts +42 -0
  52. package/dist/instrument.js +166 -0
  53. package/dist/lfo.d.ts +19 -0
  54. package/dist/lfo.js +55 -0
  55. package/dist/live-buffer.d.ts +39 -0
  56. package/dist/live-buffer.js +52 -0
  57. package/dist/live-granular.d.ts +41 -0
  58. package/dist/live-granular.js +64 -0
  59. package/dist/lookahead-limiter.d.ts +31 -0
  60. package/dist/lookahead-limiter.js +67 -0
  61. package/dist/loop-crossfade.d.ts +28 -0
  62. package/dist/loop-crossfade.js +72 -0
  63. package/dist/modulation-fx.d.ts +47 -0
  64. package/dist/modulation-fx.js +58 -0
  65. package/dist/modulation.d.ts +132 -0
  66. package/dist/modulation.js +228 -0
  67. package/dist/mpe-expression.d.ts +36 -0
  68. package/dist/mpe-expression.js +73 -0
  69. package/dist/multiband-dynamics.d.ts +50 -0
  70. package/dist/multiband-dynamics.js +60 -0
  71. package/dist/musical-controls.d.ts +76 -0
  72. package/dist/musical-controls.js +125 -0
  73. package/dist/musical-examples.d.ts +21 -0
  74. package/dist/musical-examples.js +142 -0
  75. package/dist/musical-materials.d.ts +110 -0
  76. package/dist/musical-materials.js +56 -0
  77. package/dist/oscillator.d.ts +10 -0
  78. package/dist/oscillator.js +35 -0
  79. package/dist/oversampled-drive.d.ts +19 -0
  80. package/dist/oversampled-drive.js +99 -0
  81. package/dist/performance.d.ts +61 -0
  82. package/dist/performance.js +241 -0
  83. package/dist/pitch-quantizer.d.ts +26 -0
  84. package/dist/pitch-quantizer.js +88 -0
  85. package/dist/prepared-convolution.d.ts +26 -0
  86. package/dist/prepared-convolution.js +104 -0
  87. package/dist/resident-recorder.d.ts +28 -0
  88. package/dist/resident-recorder.js +68 -0
  89. package/dist/resident-time-stretch.d.ts +46 -0
  90. package/dist/resident-time-stretch.js +136 -0
  91. package/dist/resonator.d.ts +32 -0
  92. package/dist/resonator.js +63 -0
  93. package/dist/reverb.d.ts +27 -0
  94. package/dist/reverb.js +52 -0
  95. package/dist/sample.d.ts +113 -0
  96. package/dist/sample.js +212 -0
  97. package/dist/source.d.ts +58 -0
  98. package/dist/source.js +113 -0
  99. package/dist/spatial-chains.d.ts +50 -0
  100. package/dist/spatial-chains.js +76 -0
  101. package/dist/spectral-fft.d.ts +27 -0
  102. package/dist/spectral-fft.js +84 -0
  103. package/dist/spectral-freeze.d.ts +24 -0
  104. package/dist/spectral-freeze.js +74 -0
  105. package/dist/spectral-gate.d.ts +23 -0
  106. package/dist/spectral-gate.js +58 -0
  107. package/dist/spectral-stft.d.ts +14 -0
  108. package/dist/spectral-stft.js +70 -0
  109. package/dist/spectral-texture.d.ts +46 -0
  110. package/dist/spectral-texture.js +141 -0
  111. package/dist/spectral.d.ts +18 -0
  112. package/dist/spectral.js +10 -0
  113. package/dist/state-variable-filter.d.ts +17 -0
  114. package/dist/state-variable-filter.js +25 -0
  115. package/dist/stereo-delay.d.ts +45 -0
  116. package/dist/stereo-delay.js +70 -0
  117. package/dist/virtual-analog.d.ts +26 -0
  118. package/dist/virtual-analog.js +66 -0
  119. package/dist/voice-policy.d.ts +37 -0
  120. package/dist/voice-policy.js +245 -0
  121. package/dist/wavetable-bands.d.ts +37 -0
  122. package/dist/wavetable-bands.js +145 -0
  123. package/dist/wavetable-read.d.ts +6 -0
  124. package/dist/wavetable-read.js +11 -0
  125. package/dist/wavetable.d.ts +29 -0
  126. package/dist/wavetable.js +49 -0
  127. package/dist/windowed-pitch-shift.d.ts +34 -0
  128. package/dist/windowed-pitch-shift.js +51 -0
  129. package/docs/catalog-audition.md +76 -0
  130. package/docs/catalog-filters.md +125 -0
  131. package/docs/catalog-modulation-fx.md +220 -0
  132. package/docs/catalog-status.md +433 -0
  133. package/docs/character-filter.md +139 -0
  134. package/docs/contracts.md +141 -0
  135. package/docs/control-modulation.md +194 -0
  136. package/docs/convolution.md +50 -0
  137. package/docs/curve-shaper.md +117 -0
  138. package/docs/delay-fx-capture-findings.json +134 -0
  139. package/docs/delay-fx.md +202 -0
  140. package/docs/delay-readhead.md +121 -0
  141. package/docs/delay-settings-candidate.json +333 -0
  142. package/docs/delay-settings.md +111 -0
  143. package/docs/dependency-findings.md +58 -0
  144. package/docs/deployment-status.md +37 -0
  145. package/docs/drive.md +152 -0
  146. package/docs/dual-head-delay.md +131 -0
  147. package/docs/dynamics.md +180 -0
  148. package/docs/envelope-integer-staging.md +64 -0
  149. package/docs/filter-materialization-results.json +71 -0
  150. package/docs/filter-materialization.md +135 -0
  151. package/docs/filter-oscillator.md +126 -0
  152. package/docs/filterbank-vocoder-entry.md +58 -0
  153. package/docs/freeze-reverb.md +179 -0
  154. package/docs/frequency-shifter.md +133 -0
  155. package/docs/frontend-audition.md +22 -0
  156. package/docs/gain-schedule-verification.md +53 -0
  157. package/docs/initial-acceptance.md +79 -0
  158. package/docs/instrument-performance.md +191 -0
  159. package/docs/instrument-settings.md +130 -0
  160. package/docs/instrument.md +204 -0
  161. package/docs/integration-candidate.md +114 -0
  162. package/docs/live-buffer.md +163 -0
  163. package/docs/live-granular.md +179 -0
  164. package/docs/lookahead-multiband.md +183 -0
  165. package/docs/loop-crossfade.md +99 -0
  166. package/docs/mpe-expression.md +65 -0
  167. package/docs/musical-clock-seek-boundary.md +31 -0
  168. package/docs/musical-controls-contract.md +144 -0
  169. package/docs/musical-examples.md +255 -0
  170. package/docs/oversampled-drive.md +153 -0
  171. package/docs/performance.md +53 -0
  172. package/docs/pitch-quantizer.md +55 -0
  173. package/docs/prepared-convolution.md +197 -0
  174. package/docs/realtime-audition.md +63 -0
  175. package/docs/resident-recorder.md +151 -0
  176. package/docs/resident-time-stretch.md +196 -0
  177. package/docs/sample.md +191 -0
  178. package/docs/sound-audition-headroom.md +103 -0
  179. package/docs/sources.md +81 -0
  180. package/docs/spatial-chains-entry.md +251 -0
  181. package/docs/spectral-freeze-entry.md +132 -0
  182. package/docs/spectral-gate-entry.md +134 -0
  183. package/docs/spectral-large-candidate.md +81 -0
  184. package/docs/spectral-texture-entry.md +178 -0
  185. package/docs/spectral.md +127 -0
  186. package/docs/virtual-analog.md +77 -0
  187. package/docs/voice-policy.md +38 -0
  188. package/docs/wavetable-bands.md +138 -0
  189. package/docs/wavetable.md +101 -0
  190. package/docs/windowed-pitch-shift.md +112 -0
  191. package/package.json +269 -4
@@ -0,0 +1,113 @@
1
+ import { type Node, type TypedArrayFieldRef } from '@unworklet/core';
2
+ export interface ResidentSampleConfig {
3
+ /** Fixed mono PCM capacity, 1..65,536 frames; decoding/file I/O run outside the processor. */
4
+ capacity: number;
5
+ sourceSampleRate: number;
6
+ }
7
+ export interface ResidentSample {
8
+ readonly capacity: number;
9
+ readonly sourceSampleRate: number;
10
+ /** Call in event<{data:Float32Array}>.onReceive. Copy runs at the block boundary.
11
+ * Prepare/decode outside the callback and load before audible use. Oversize input
12
+ * truncates to capacity; empty input unloads. Each load invalidates current playback. */
13
+ load(data: TypedArrayFieldRef<'f32'>): void;
14
+ length(): Node<'i32'>;
15
+ revision(): Node<'i32'>;
16
+ /** Linear interpolation; end is exclusive and clipped to loaded length.
17
+ * NaN position uses start; other positions saturate to the signed i32 frame range. */
18
+ read(position: Node<'f64'>, start: Node<'i32'>, end: Node<'i32'>, loop: boolean): Node<'f32'>;
19
+ }
20
+ /** A fixed-capacity mono sample, initially missing. No asset loader or streaming. */
21
+ export declare const residentSample: import("@unworklet/core").SubgraphDecl<[config: ResidentSampleConfig], ResidentSample>;
22
+ export interface SamplePlayerConfig {
23
+ sampleRate: number;
24
+ sample: ResidentSample;
25
+ /** Fixed slice [startFrame,endFrame), intersected with the current asset length. */
26
+ startFrame?: number;
27
+ endFrame?: number;
28
+ loop?: boolean;
29
+ /** Linear gate-off ramp in output samples; zero stops immediately. */
30
+ releaseFrames?: number;
31
+ }
32
+ export interface SamplePlayerControls {
33
+ gate: Node<'bool'>;
34
+ /** Level-sensitive restart; a held trigger repeats the first sample. */
35
+ trigger: Node<'bool'>;
36
+ /** Priority over gate/trigger; held reset is silent. */
37
+ reset: Node<'bool'>;
38
+ /** Signed speed, [-16,16]; source/host sample-rate conversion is automatic. */
39
+ rate: Node<'f32'>;
40
+ }
41
+ /** Mono, one voice, one-shot or loop; nearest endpoints with linear interpolation. */
42
+ export declare const samplePlayer: import("@unworklet/core").SubgraphDecl<[config: SamplePlayerConfig], {
43
+ tick(c: SamplePlayerControls): {
44
+ output: Node<"f32">;
45
+ active: Node<"bool">;
46
+ position: Node<"f32">;
47
+ missing: Node<"bool">;
48
+ };
49
+ }>;
50
+ export interface SampleZone {
51
+ sample: ResidentSample;
52
+ keyLow: number;
53
+ keyHigh: number;
54
+ /** Inclusive normalized velocity endpoints. First matching zone wins overlaps. */
55
+ velocityLow: number;
56
+ velocityHigh: number;
57
+ rootKey: number;
58
+ tuneCents?: number;
59
+ gain?: number;
60
+ startFrame?: number;
61
+ endFrame?: number;
62
+ loop?: boolean;
63
+ }
64
+ export interface MultisampleConfig {
65
+ sampleRate: number;
66
+ zones: readonly SampleZone[];
67
+ releaseFrames?: number;
68
+ }
69
+ export interface MultisampleControls {
70
+ key: Node<'f32'>;
71
+ velocity: Node<'f32'>;
72
+ gate: Node<'bool'>;
73
+ trigger: Node<'bool'>;
74
+ reset: Node<'bool'>;
75
+ /** Additional signed ratio, multiplied after root-key transposition; final ratio clips to ±16. */
76
+ rate: Node<'f32'>;
77
+ }
78
+ /** Monophonic key/velocity selection; zone/key/velocity latch on note-on. */
79
+ export declare const multisamplePlayer: import("@unworklet/core").SubgraphDecl<[config: MultisampleConfig], {
80
+ tick(c: MultisampleControls): {
81
+ output: Node<"f32">;
82
+ active: Node<"bool">;
83
+ zoneIndex: Node<"i32">;
84
+ missing: Node<"bool">;
85
+ };
86
+ }>;
87
+ export interface GranularConfig {
88
+ sampleRate: number;
89
+ sample: ResidentSample;
90
+ maxGrains: number;
91
+ seed?: number;
92
+ loop?: boolean;
93
+ }
94
+ export interface GranularControls {
95
+ gate: Node<'bool'>;
96
+ reset: Node<'bool'>;
97
+ positionFrames: Node<'f32'>;
98
+ jitterFrames: Node<'f32'>;
99
+ rate: Node<'f32'>;
100
+ durationSeconds: Node<'f32'>;
101
+ /** 0..2000 onsets/second. Gate-rise creates the first grain even at zero density. */
102
+ densityHz: Node<'f32'>;
103
+ }
104
+ /** Fixed grain pool, triangular endpoint-zero windows; first free slot, drop on full. */
105
+ export declare const granularSource: import("@unworklet/core").SubgraphDecl<[config: GranularConfig], {
106
+ tick(c: GranularControls): {
107
+ output: Node<"f32">;
108
+ activeGrains: Node<"i32">;
109
+ onset: Node<"bool">;
110
+ dropped: Node<"bool">;
111
+ missing: Node<"bool">;
112
+ };
113
+ }>;
package/dist/sample.js ADDED
@@ -0,0 +1,212 @@
1
+ import { bool, defineSubgraph, f32, f64, i32, instantiate, select, state } from '@unworklet/core';
2
+ // Native floating scalar stores flush magnitudes below 1e-30. Exact binary
3
+ // scaling preserves meaningful subnormal control/position contributions.
4
+ const CONTROL_SCALE = 2 ** 128;
5
+ const READ_POSITION_SCALE = 2 ** 192;
6
+ function sampleRate(rate) {
7
+ if (!Number.isFinite(rate) || rate < 8000 || rate > 192000)
8
+ throw new RangeError('sample rate must be in [8000,192000] Hz');
9
+ }
10
+ function integer(value, minimum, maximum, name) {
11
+ if (!Number.isInteger(value) || value < minimum || value > maximum)
12
+ throw new RangeError(`${name} must be an integer in [${minimum},${maximum}]`);
13
+ }
14
+ function bounded(value, minimum, maximum, fallback = 0) {
15
+ return select(value.eq(value), f64(value), f64(fallback)).clamp(minimum, maximum);
16
+ }
17
+ function wrap(position, start, end) {
18
+ const length = f64(end.sub(start).max(1));
19
+ const relative = position.sub(f64(start));
20
+ return relative.sub(relative.div(length).floor().mul(length)).add(f64(start));
21
+ }
22
+ /** A fixed-capacity mono sample, initially missing. No asset loader or streaming. */
23
+ export const residentSample = defineSubgraph((config) => {
24
+ integer(config.capacity, 1, 65536, 'sample capacity');
25
+ sampleRate(config.sourceSampleRate);
26
+ const pcm = state.buffer.f32({ size: config.capacity }).expose({ name: 'pcm', snapshot: 'persistent' });
27
+ const length = state.i32(0).named('length'), revision = state.i32(0).named('revision');
28
+ // A native transient scalar captures normalization once per read, including
29
+ // when several independent readers share this resident in the same sample.
30
+ const readPosition = state.f64(0).expose({ name: 'readPosition', snapshot: 'transient' });
31
+ return {
32
+ capacity: config.capacity, sourceSampleRate: config.sourceSampleRate,
33
+ load(data) { pcm.copyFrom(data); length.write(data.length.clamp(0, config.capacity)); revision.write(revision.read().add(1)); },
34
+ length() { return length.read(); }, revision() { return revision.read(); },
35
+ read(position, start, end, loop) {
36
+ const lo = start.clamp(0, config.capacity - 1), hi = end.clamp(0, config.capacity).min(length.read());
37
+ const valid = hi.gt(lo);
38
+ const safePosition = select(position.eq(position), position, f64(lo)).clamp(-2147483648, 2147483647);
39
+ readPosition.write((loop ? wrap(safePosition, lo, hi) : safePosition.clamp(f64(lo), f64(hi.sub(1).max(lo)))).mul(READ_POSITION_SCALE));
40
+ const p = readPosition.read().div(READ_POSITION_SCALE);
41
+ const whole = i32(p.floor()).clamp(0, config.capacity - 1), fraction = p.sub(f64(whole));
42
+ const next = loop ? select(whole.add(1).gte(hi), lo, whole.add(1)) : whole.add(1).min(hi.sub(1).max(lo));
43
+ const rawA = f64(pcm.read(whole)), rawB = f64(pcm.read(next.clamp(0, config.capacity - 1)));
44
+ // Invalid PCM taps are silent, without poisoning adjacent finite samples/state.
45
+ const a = select(rawA.eq(rawA).and(rawA.abs().lte(3.4028234663852886e38)), rawA, f64(0));
46
+ const b = select(rawB.eq(rawB).and(rawB.abs().lte(3.4028234663852886e38)), rawB, f64(0));
47
+ return select(valid, f32(a.add(b.sub(a).mul(fraction))), f32(0));
48
+ },
49
+ };
50
+ });
51
+ /** Mono, one voice, one-shot or loop; nearest endpoints with linear interpolation. */
52
+ export const samplePlayer = defineSubgraph((config) => {
53
+ sampleRate(config.sampleRate);
54
+ const { sample, startFrame = 0, endFrame = sample.capacity, loop = false, releaseFrames = 0 } = config;
55
+ integer(startFrame, 0, sample.capacity - 1, 'startFrame');
56
+ integer(endFrame, startFrame + 1, sample.capacity, 'endFrame');
57
+ integer(releaseFrames, 0, Math.ceil(config.sampleRate * 10), 'releaseFrames');
58
+ const phase = state.f64(startFrame * CONTROL_SCALE).named('phase'), active = state.bool(false).named('active');
59
+ const previousGate = state.bool(false).named('previousGate'), revision = state.i32(-1).named('revision');
60
+ const remaining = state.i32(0).named('releaseRemaining');
61
+ return {
62
+ tick(c) {
63
+ const end = sample.length().min(endFrame), start = i32(startFrame), available = end.gt(start);
64
+ const speed = bounded(c.rate, -16, 16).mul(sample.sourceSampleRate / config.sampleRate);
65
+ const changed = sample.revision().eq(revision.read()).not();
66
+ const trigger = c.trigger.or(c.gate.and(previousGate.read().not())).and(c.gate).and(c.reset.not()).and(available);
67
+ const alive = select(trigger, bool(true), active.read().and(changed.not())).and(c.reset.not()).and(available);
68
+ const initial = select(speed.lt(0), f64(end.sub(1)), f64(start));
69
+ phase.write(select(trigger, initial, phase.read().div(CONTROL_SCALE)).mul(CONTROL_SCALE));
70
+ const position = phase.read().div(CONTROL_SCALE);
71
+ const releaseLeft = select(trigger, i32(releaseFrames), select(c.gate, remaining.read(), remaining.read().sub(1).max(0)));
72
+ const level = releaseFrames > 0 ? f64(releaseLeft).div(releaseFrames) : select(c.gate, f64(1), f64(0));
73
+ active.write(alive.and(level.gt(0)));
74
+ const playing = active.read();
75
+ const output = select(playing, f32(f64(sample.read(position, start, end, loop)).mul(level)), f32(0));
76
+ const advanced = position.add(speed);
77
+ phase.write(select(c.reset, f64(startFrame), loop ? wrap(advanced, start, end) : advanced.clamp(f64(start).sub(1), f64(end))).mul(CONTROL_SCALE));
78
+ active.write(playing.and(loop ? bool(true) : advanced.gte(f64(start)).and(advanced.lt(f64(end)))));
79
+ remaining.write(select(playing, releaseLeft, i32(0)));
80
+ previousGate.write(c.gate.and(c.reset.not()));
81
+ revision.write(sample.revision());
82
+ return { output, active: playing, position: f32(position), missing: available.not() };
83
+ },
84
+ };
85
+ });
86
+ // Core 0.4.1 pow uses a shared f32 approximation even for f64 operands.
87
+ // Reduce the bounded pitch exponent to [0,1), evaluate exp(x*ln2) in f64,
88
+ // and scale by exact powers of two. This avoids pitch drift from that bridge.
89
+ function transposeRatio(exponent) {
90
+ const whole = exponent.floor(), x = exponent.sub(whole).mul(Math.LN2);
91
+ let factorial = 1;
92
+ const coefficients = [1];
93
+ for (let n = 1; n <= 12; n++) {
94
+ factorial *= n;
95
+ coefficients.push(1 / factorial);
96
+ }
97
+ let polynomial = f64(coefficients[12]);
98
+ for (let n = 11; n >= 0; n--)
99
+ polynomial = polynomial.mul(x).add(coefficients[n]);
100
+ const index = i32(whole.add(13));
101
+ let scale = f64(2 ** -13);
102
+ for (let bit = 0; bit < 5; bit++)
103
+ scale = scale.mul(select(index.div(2 ** bit).mod(2).eq(1), f64(2 ** (2 ** bit)), f64(1)));
104
+ return polynomial.mul(scale);
105
+ }
106
+ /** Monophonic key/velocity selection; zone/key/velocity latch on note-on. */
107
+ export const multisamplePlayer = defineSubgraph((config) => {
108
+ sampleRate(config.sampleRate);
109
+ integer(config.zones.length, 1, 16, 'zone count');
110
+ const zones = config.zones.map((zone, index) => {
111
+ integer(zone.keyLow, 0, 127, 'keyLow');
112
+ integer(zone.keyHigh, zone.keyLow, 127, 'keyHigh');
113
+ integer(zone.rootKey, 0, 127, 'rootKey');
114
+ const { velocityLow: low, velocityHigh: high, tuneCents = 0, gain = 1 } = zone;
115
+ if (!Number.isFinite(low) || !Number.isFinite(high) || low < 0 || high > 1 || high < low || !Number.isFinite(tuneCents) || Math.abs(tuneCents) > 2400 || !Number.isFinite(gain) || Math.abs(gain) > 4)
116
+ throw new RangeError('invalid velocity zone, tuning or gain');
117
+ return { ...zone, tuneCents, gain, transposition: state.f64(1).named(`zone${index}Transposition`), player: instantiate(samplePlayer, { ...zone, sampleRate: config.sampleRate, releaseFrames: config.releaseFrames }, { name: `zone${index}` }) };
118
+ });
119
+ const selected = state.i32(-1).named('selected'), lastGate = state.bool(false).named('previousGate');
120
+ const keyState = state.f64(60 * CONTROL_SCALE).named('key'), velocityState = state.f64(0).named('velocity');
121
+ const noteOn = state.bool(false).named('noteOn');
122
+ return { tick(c) {
123
+ const key = bounded(c.key, 0, 127, 60), velocity = bounded(c.velocity, 0, 1);
124
+ noteOn.write(c.trigger.or(c.gate.and(lastGate.read().not())).and(c.gate).and(c.reset.not()));
125
+ const start = noteOn.read();
126
+ let match = i32(-1);
127
+ for (let index = zones.length - 1; index >= 0; index--) {
128
+ const z = zones[index];
129
+ match = select(key.gte(z.keyLow).and(key.lte(z.keyHigh)).and(velocity.gte(z.velocityLow)).and(velocity.lte(z.velocityHigh)), i32(index), match);
130
+ }
131
+ selected.write(select(c.reset, i32(-1), select(start, match, selected.read())));
132
+ const current = selected.read();
133
+ keyState.write(select(start, key.mul(CONTROL_SCALE), keyState.read()));
134
+ velocityState.write(select(start, velocity.mul(CONTROL_SCALE), velocityState.read()));
135
+ // Native state reads materialize latched controls before the unrolled bank.
136
+ const note = keyState.read().div(CONTROL_SCALE), level = velocityState.read().div(CONTROL_SCALE);
137
+ let sum = f64(0), active = bool(false), missing = current.lt(0);
138
+ zones.forEach((z, index) => {
139
+ const chosen = current.eq(index);
140
+ // Materialize the latched ratio once. Repeating the polynomial graph inside
141
+ // every readhead expression causes excessive capture/compiler expansion.
142
+ z.transposition.write(select(start, transposeRatio(note.sub(z.rootKey).div(12).add(z.tuneCents / 1200)), z.transposition.read()));
143
+ const ratio = z.transposition.read().mul(bounded(c.rate, -16, 16));
144
+ const result = z.player.tick({ gate: c.gate.and(chosen), trigger: start.and(chosen), reset: c.reset.or(start.and(chosen.not())), rate: f32(ratio) });
145
+ sum = sum.add(f64(result.output).mul(level).mul(z.gain));
146
+ active = active.or(result.active);
147
+ missing = missing.or(chosen.and(result.missing));
148
+ });
149
+ lastGate.write(c.gate.and(c.reset.not()));
150
+ return { output: f32(sum), active, zoneIndex: current, missing };
151
+ } };
152
+ });
153
+ /** Fixed grain pool, triangular endpoint-zero windows; first free slot, drop on full. */
154
+ export const granularSource = defineSubgraph((config) => {
155
+ sampleRate(config.sampleRate);
156
+ integer(config.maxGrains, 1, 32, 'maxGrains');
157
+ const { sample, maxGrains, seed = 1, loop = true } = config;
158
+ integer(seed, 1, 2147483646, 'grain seed');
159
+ const random = state.i32(seed).named('random'), clock = state.f64(0).named('clock');
160
+ const allocatedState = state.bool(false).named('allocated'), onsetState = state.bool(false).named('onset');
161
+ const onsetPosition = state.f64(0).named('onsetPosition'), onsetSpeed = state.f64(0).named('onsetSpeed');
162
+ const onsetDuration = state.i32(3).named('onsetDuration');
163
+ const lastGate = state.bool(false).named('previousGate'), revision = state.i32(-1).named('revision');
164
+ const grains = Array.from({ length: maxGrains }, (_, index) => ({
165
+ position: state.f64(0).named(`grain${index}Position`), age: state.i32(0).named(`grain${index}Age`),
166
+ duration: state.i32(3).named(`grain${index}Duration`), speed: state.f64(0).named(`grain${index}Speed`),
167
+ active: state.bool(false).named(`grain${index}Active`),
168
+ }));
169
+ return { tick(c) {
170
+ const changed = sample.revision().eq(revision.read()).not(), clear = c.reset.or(changed);
171
+ const rising = c.gate.and(lastGate.read().not());
172
+ const phase = select(clear.or(rising), f64(0), clock.read().div(CONTROL_SCALE));
173
+ onsetState.write(c.gate.and(c.reset.not()).and(sample.length().gt(0)).and(rising.or(phase.gte(1))));
174
+ const onset = onsetState.read();
175
+ const rng = select(c.reset, i32(seed), random.read());
176
+ const nextRandom = i32(f64(rng).mul(48271).mod(2147483647));
177
+ random.write(select(onset, nextRandom, rng));
178
+ const jitter = f64(random.read()).div(2147483647).mul(2).sub(1).mul(bounded(c.jitterFrames, 0, sample.capacity - 1));
179
+ onsetPosition.write(bounded(c.positionFrames, 0, sample.capacity - 1).add(jitter).clamp(0, f64(sample.length().sub(1).max(0))).mul(CONTROL_SCALE));
180
+ onsetDuration.write(i32(bounded(c.durationSeconds, 0, 2).mul(config.sampleRate).add(0.5).floor()).clamp(3, Math.round(config.sampleRate * 2)));
181
+ onsetSpeed.write(bounded(c.rate, -16, 16).mul(sample.sourceSampleRate / config.sampleRate).mul(CONTROL_SCALE));
182
+ const center = onsetPosition.read().div(CONTROL_SCALE), duration = onsetDuration.read(), speed = onsetSpeed.read().div(CONTROL_SCALE);
183
+ allocatedState.write(false);
184
+ let sum = f64(0), count = i32(0);
185
+ for (const grain of grains) {
186
+ const existing = grain.active.read().and(grain.age.read().lt(grain.duration.read())).and(clear.not());
187
+ // Keep the first-free scan linear in graph size as well as runtime work.
188
+ const allocated = allocatedState.read();
189
+ const launch = onset.and(allocated.not()).and(existing.not());
190
+ allocatedState.write(allocated.or(launch));
191
+ grain.active.write(existing.or(launch));
192
+ grain.age.write(select(launch, i32(0), grain.age.read()));
193
+ grain.duration.write(select(launch, duration, grain.duration.read()));
194
+ grain.position.write(select(launch, center.mul(CONTROL_SCALE), grain.position.read()));
195
+ grain.speed.write(select(launch, speed.mul(CONTROL_SCALE), grain.speed.read()));
196
+ const active = grain.active.read(), age = grain.age.read(), length = grain.duration.read();
197
+ const position = grain.position.read().div(CONTROL_SCALE), step = grain.speed.read().div(CONTROL_SCALE);
198
+ const window = f64(1).sub(f64(age).div(f64(length.sub(1))).mul(2).sub(1).abs()).max(0);
199
+ const inBounds = position.gte(0).and(position.lt(f64(sample.length())));
200
+ const value = sample.read(position, i32(0), sample.length(), loop);
201
+ sum = sum.add(select(active.and(loop ? bool(true) : inBounds), f64(value).mul(window), f64(0)));
202
+ count = count.add(select(active, i32(1), i32(0)));
203
+ grain.position.write((loop ? wrap(position.add(step), i32(0), sample.length()) : position.add(step).clamp(-sample.capacity * 48, sample.capacity * 48)).mul(CONTROL_SCALE));
204
+ grain.age.write(select(active, age.add(1), i32(0)));
205
+ grain.active.write(active.and(age.add(1).lt(length)));
206
+ }
207
+ clock.write(select(c.gate.and(c.reset.not()), phase.sub(phase.floor()).add(bounded(c.densityHz, 0, 2000).div(config.sampleRate)), f64(0)).mul(CONTROL_SCALE));
208
+ lastGate.write(c.gate.and(c.reset.not()));
209
+ revision.write(sample.revision());
210
+ return { output: f32(sum.div(maxGrains)), activeGrains: count, onset, dropped: onset.and(allocatedState.read().not()), missing: sample.length().eq(0) };
211
+ } };
212
+ });
@@ -0,0 +1,58 @@
1
+ import { type Node } from '@unworklet/core';
2
+ export interface SourceConfig {
3
+ sampleRate: number;
4
+ }
5
+ export interface PhaseModulationControls {
6
+ carrierHz: Node<'f32'>;
7
+ modulatorHz: Node<'f32'>;
8
+ /** Signed phase deviation, in radians, clamped to [-8π,8π]. */
9
+ depthRadians: Node<'f32'>;
10
+ /** Previous carrier output (one sample old) times this value, in radians. */
11
+ feedbackRadians: Node<'f32'>;
12
+ reset: Node<'bool'>;
13
+ }
14
+ export interface FrequencyModulationControls {
15
+ carrierHz: Node<'f32'>;
16
+ modulatorHz: Node<'f32'>;
17
+ /** Signed instantaneous frequency deviation in Hz, not a PM index. */
18
+ deviationHz: Node<'f32'>;
19
+ /** Previous carrier output (one sample old) times this value, in Hz. */
20
+ feedbackHz: Node<'f32'>;
21
+ reset: Node<'bool'>;
22
+ }
23
+ /** Two-operator PM with a sine modulator and one-sample carrier feedback. */
24
+ export declare const phaseModulation: import("@unworklet/core").SubgraphDecl<[config: SourceConfig], {
25
+ /** Current phase is emitted before advance. Held reset emits zero every sample. */
26
+ tick(c: PhaseModulationControls): Node<"f32">;
27
+ }>;
28
+ /** Two-operator linear FM. Negative instantaneous frequency clips to zero. */
29
+ export declare const frequencyModulation: import("@unworklet/core").SubgraphDecl<[config: SourceConfig], {
30
+ tick(c: FrequencyModulationControls): Node<"f32">;
31
+ }>;
32
+ export interface AdditivePartial {
33
+ ratio: number;
34
+ gain: number;
35
+ }
36
+ export interface AdditiveSourceConfig extends SourceConfig {
37
+ partials: readonly AdditivePartial[];
38
+ }
39
+ /** Fixed sine partial bank. Gains are normalized by their absolute sum. */
40
+ export declare const additiveSource: import("@unworklet/core").SubgraphDecl<[config: AdditiveSourceConfig], {
41
+ /** Partials above .45*sampleRate are muted; crossing the limit is not smoothed. */
42
+ tick(frequencyHz: Node<"f32">, reset: Node<"bool">): Node<"f32">;
43
+ }>;
44
+ export interface UnisonVoice {
45
+ detuneCents: number;
46
+ pan: number;
47
+ }
48
+ export interface UnisonSourceConfig extends SourceConfig {
49
+ waveform: 'sine' | 'saw';
50
+ voices: readonly UnisonVoice[];
51
+ }
52
+ /** Fixed detune bank. Linear pan divided by voice count; L+R is the mono average. */
53
+ export declare const unisonSource: import("@unworklet/core").SubgraphDecl<[config: UnisonSourceConfig], {
54
+ tick(frequencyHz: Node<"f32">, reset: Node<"bool">): {
55
+ left: Node<"f32">;
56
+ right: Node<"f32">;
57
+ };
58
+ }>;
package/dist/source.js ADDED
@@ -0,0 +1,113 @@
1
+ import { defineSubgraph, f32, f64, instantiate, select, state } from '@unworklet/core';
2
+ import { oscillator } from './oscillator.js';
3
+ function maximumFrequency(sampleRate) {
4
+ if (!Number.isFinite(sampleRate) || sampleRate < 8000 || sampleRate > 192000) {
5
+ throw new RangeError('source sampleRate must be finite and in [8000,192000] Hz');
6
+ }
7
+ return 0.45 * sampleRate;
8
+ }
9
+ // Same degree-13 sine construction as oscillator, with a wrapped phase offset.
10
+ // Pure graph arithmetic; no runtime JS, table asset, or additional state system.
11
+ function sineCycles(cycles) {
12
+ const p = cycles.sub(cycles.floor());
13
+ const folded = select(p.lt(0.25), p, select(p.lt(0.75), f64(0.5).sub(p), p.sub(1)));
14
+ const angle = folded.mul(2 * Math.PI), z = angle.mul(angle);
15
+ const polynomial = f64(1 / 6227020800).mul(z).sub(1 / 39916800).mul(z).add(1 / 362880)
16
+ .mul(z).sub(1 / 5040).mul(z).add(1 / 120).mul(z).sub(1 / 6).mul(z).add(1);
17
+ return f32(angle.mul(polynomial));
18
+ }
19
+ /** Two-operator PM with a sine modulator and one-sample carrier feedback. */
20
+ export const phaseModulation = defineSubgraph((config) => {
21
+ const maximum = maximumFrequency(config.sampleRate);
22
+ const modulator = instantiate(oscillator, { ...config, waveform: 'sine' }, { name: 'modulator' });
23
+ const phase = state.f64(0).named('carrierPhase');
24
+ const previous = state.f32(0).named('previousCarrier');
25
+ return {
26
+ /** Current phase is emitted before advance. Held reset emits zero every sample. */
27
+ tick(c) {
28
+ const p = select(c.reset, f64(0), phase.read());
29
+ const prior = select(c.reset, f32(0), previous.read());
30
+ const mod = modulator.tick(c.modulatorHz, c.reset);
31
+ const offset = f64(c.depthRadians).clamp(-8 * Math.PI, 8 * Math.PI).mul(f64(mod))
32
+ .add(f64(c.feedbackRadians).clamp(-Math.PI, Math.PI).mul(f64(prior))).div(2 * Math.PI);
33
+ const output = sineCycles(p.add(offset));
34
+ phase.write(p.add(f64(c.carrierHz).clamp(0, maximum).div(config.sampleRate)).frac());
35
+ previous.write(output);
36
+ return output;
37
+ },
38
+ };
39
+ });
40
+ /** Two-operator linear FM. Negative instantaneous frequency clips to zero. */
41
+ export const frequencyModulation = defineSubgraph((config) => {
42
+ const maximum = maximumFrequency(config.sampleRate);
43
+ const modulator = instantiate(oscillator, { ...config, waveform: 'sine' }, { name: 'modulator' });
44
+ const phase = state.f64(0).named('carrierPhase');
45
+ const previous = state.f32(0).named('previousCarrier');
46
+ return {
47
+ tick(c) {
48
+ const p = select(c.reset, f64(0), phase.read());
49
+ const prior = select(c.reset, f32(0), previous.read());
50
+ const mod = modulator.tick(c.modulatorHz, c.reset);
51
+ const instantaneous = f64(c.carrierHz).clamp(0, maximum)
52
+ .add(f64(c.deviationHz).clamp(-maximum, maximum).mul(f64(mod)))
53
+ .add(f64(c.feedbackHz).clamp(-maximum, maximum).mul(f64(prior))).clamp(0, maximum);
54
+ const output = sineCycles(p);
55
+ phase.write(p.add(instantaneous.div(config.sampleRate)).frac());
56
+ previous.write(output);
57
+ return output;
58
+ },
59
+ };
60
+ });
61
+ /** Fixed sine partial bank. Gains are normalized by their absolute sum. */
62
+ export const additiveSource = defineSubgraph((config) => {
63
+ const maximum = maximumFrequency(config.sampleRate);
64
+ if (config.partials.length < 1 || config.partials.length > 32)
65
+ throw new RangeError('additiveSource requires 1–32 partials');
66
+ const partials = config.partials.map((p, i) => {
67
+ if (!Number.isFinite(p.ratio) || p.ratio <= 0 || p.ratio > 128 || !Number.isFinite(p.gain) || Math.abs(p.gain) > 16) {
68
+ throw new RangeError('partial ratio must be in (0,128] and gain in [-16,16]');
69
+ }
70
+ return { ...p, source: instantiate(oscillator, { sampleRate: config.sampleRate, waveform: 'sine' }, { name: `partial${i}` }) };
71
+ });
72
+ const normalization = partials.reduce((sum, p) => sum + Math.abs(p.gain), 0) || 1;
73
+ return {
74
+ /** Partials above .45*sampleRate are muted; crossing the limit is not smoothed. */
75
+ tick(frequencyHz, reset) {
76
+ let sum = f64(0);
77
+ for (const p of partials) {
78
+ const frequency = f64(frequencyHz).clamp(0, maximum).mul(p.ratio);
79
+ const sample = p.source.tick(f32(frequency), reset);
80
+ sum = sum.add(f64(select(frequency.lte(maximum), sample, f32(0))).mul(p.gain / normalization));
81
+ }
82
+ return f32(sum);
83
+ },
84
+ };
85
+ });
86
+ /** Fixed detune bank. Linear pan divided by voice count; L+R is the mono average. */
87
+ export const unisonSource = defineSubgraph((config) => {
88
+ const maximum = maximumFrequency(config.sampleRate);
89
+ if (config.voices.length < 1 || config.voices.length > 8)
90
+ throw new RangeError('unisonSource requires 1–8 voices');
91
+ if (config.waveform !== 'sine' && config.waveform !== 'saw')
92
+ throw new RangeError('unknown unison waveform');
93
+ const voices = config.voices.map((v, i) => {
94
+ if (!Number.isFinite(v.detuneCents) || Math.abs(v.detuneCents) > 1200 || !Number.isFinite(v.pan) || Math.abs(v.pan) > 1) {
95
+ throw new RangeError('unison detune must be in [-1200,1200] cents and pan in [-1,1]');
96
+ }
97
+ return { ratio: 2 ** (v.detuneCents / 1200), left: (1 - v.pan) / (2 * config.voices.length),
98
+ right: (1 + v.pan) / (2 * config.voices.length),
99
+ source: instantiate(oscillator, { sampleRate: config.sampleRate, waveform: config.waveform }, { name: `voice${i}` }) };
100
+ });
101
+ return {
102
+ tick(frequencyHz, reset) {
103
+ let left = f64(0), right = f64(0);
104
+ for (const voice of voices) {
105
+ const frequency = f64(frequencyHz).clamp(0, maximum).mul(voice.ratio).clamp(0, maximum);
106
+ const sample = f64(voice.source.tick(f32(frequency), reset));
107
+ left = left.add(sample.mul(voice.left));
108
+ right = right.add(sample.mul(voice.right));
109
+ }
110
+ return { left: f32(left), right: f32(right) };
111
+ },
112
+ };
113
+ });
@@ -0,0 +1,50 @@
1
+ import { type EveryNSamples, type Node } from '@unworklet/core';
2
+ export interface SpatialChainConfig {
3
+ /** Pass ctx.renderQuantumSize for the early convolution snapshot phase. */
4
+ renderQuantumSize: number;
5
+ /** Match the enclosing ctx.sampleRate; integer 8000..192000. */
6
+ sampleRate: number;
7
+ }
8
+ export interface SpatialChainControls {
9
+ /** Finite linear dry/wet, clamped to [0,1]. Zero still feeds the wet paths. */
10
+ mix: Node<'f32'>;
11
+ /** Unity dry, no new wet excitation; all existing tails keep processing. */
12
+ bypass: Node<'bool'>;
13
+ /** Highest priority: silence, discard current input and invalidate history. */
14
+ reset: Node<'bool'>;
15
+ }
16
+ export interface FeedforwardPitchedReverbConfig extends SpatialChainConfig {
17
+ /** Fixed pitch delay excursion: even integer 32..16384, default 2048. */
18
+ windowSamples?: number;
19
+ }
20
+ export interface FeedforwardPitchedReverbControls extends SpatialChainControls {
21
+ /** Reader speed [.5,2]. Invalid values retain the prior ratio, or 1 on reset. */
22
+ ratio: Node<'f32'>;
23
+ /** Finite unpitched/pitched late blend, clamped [0,1]. Readers always advance. */
24
+ pitchMix: Node<'f32'>;
25
+ /** Rephase the current pitch read while retaining history. Can click. */
26
+ retrigger: Node<'bool'>;
27
+ }
28
+ /** CANDIDATE mono-in/stereo-out short FIR + three early taps || damped FDN.
29
+ * Finite normalized input [-1,1]; no limiter. Dry is immediate, early has B8
30
+ * latency plus 7/13/23ms taps, and late begins near 29.7ms with an IIR tail.
31
+ * Call once per sample in stride-1 forSample with its native everyNSamples.
32
+ */
33
+ export declare const hybridReverb: import("@unworklet/core").SubgraphDecl<[config: SpatialChainConfig], {
34
+ tick(input: Node<"f32">, c: SpatialChainControls, everyNSamples: EveryNSamples): {
35
+ left: Node<"f32">;
36
+ right: Node<"f32">;
37
+ };
38
+ }>;
39
+ /** Same hybrid space with pitch applied only to a feedforward copy of its FDN
40
+ * output. No pitched signal enters the FDN or early path. This is
41
+ * not regenerative shimmer. Pitch adds variable 1..W+1-sample read ages, with
42
+ * interpolation aliasing/window coloration; parallel blending can cancel.
43
+ */
44
+ export declare const feedforwardPitchedReverb: import("@unworklet/core").SubgraphDecl<[config: FeedforwardPitchedReverbConfig], {
45
+ tick(input: Node<"f32">, c: FeedforwardPitchedReverbControls, everyNSamples: EveryNSamples): {
46
+ left: Node<"f32">;
47
+ right: Node<"f32">;
48
+ ratioRejected: Node<"bool">;
49
+ };
50
+ }>;
@@ -0,0 +1,76 @@
1
+ import { bool, defineSubgraph, f32, f64, instantiate, select } from '@unworklet/core';
2
+ import { partitionedConvolution } from './convolution.js';
3
+ import { multiTapDelay } from './stereo-delay.js';
4
+ import { algorithmicReverb } from './reverb.js';
5
+ import { windowedPitchShift } from './windowed-pitch-shift.js';
6
+ // Construction-only composition. Each existing module retains its own state,
7
+ // scheduler and native snapshot contract; no new runtime routing is introduced.
8
+ function spatialPaths({ sampleRate, renderQuantumSize }) {
9
+ if (!Number.isInteger(sampleRate) || sampleRate < 8000 || sampleRate > 192000) {
10
+ throw new RangeError('spatial chains require integer sampleRate [8000,192000]');
11
+ }
12
+ const color = instantiate(partitionedConvolution, {
13
+ renderQuantumSize, blockSize: 8, impulse: [.625, 0, 0, .25, 0, 0, 0, -.125],
14
+ }, { name: 'earlyColor' });
15
+ const reflections = instantiate(multiTapDelay, {
16
+ sampleRate, maxDelaySeconds: .023,
17
+ taps: [
18
+ { delaySeconds: .007, gainLeft: .5, gainRight: .25 },
19
+ { delaySeconds: .013, gainLeft: .25, gainRight: -.25 },
20
+ { delaySeconds: .023, gainLeft: -.125, gainRight: .375 },
21
+ ],
22
+ }, { name: 'earlyTaps' });
23
+ const late = instantiate(algorithmicReverb, {
24
+ sampleRate, roomScale: 1, decaySeconds: .6, dampingHz: Math.min(3500, .4 * sampleRate),
25
+ }, { name: 'late' });
26
+ return {
27
+ tick(input, c, everyNSamples) {
28
+ const excitation = select(c.reset.or(c.bypass), f32(0), input);
29
+ const early = reflections.tick(color.tick(excitation, c.reset, everyNSamples), {
30
+ feedback: f32(0), mix: f32(1), bypass: bool(false), reset: c.reset,
31
+ });
32
+ const tail = late.tick(excitation, f32(0), { mix: f32(1), bypass: bool(false), reset: c.reset });
33
+ return { early, tail };
34
+ },
35
+ };
36
+ }
37
+ function blend(input, early, late, c) {
38
+ const mix = f64(c.mix).clamp(0, 1), wet = f64(early).add(late).mul(.5);
39
+ return select(c.reset, f32(0), select(c.bypass, input, f32(f64(input).mul(f64(1).sub(mix)).add(wet.mul(mix)))));
40
+ }
41
+ /** CANDIDATE mono-in/stereo-out short FIR + three early taps || damped FDN.
42
+ * Finite normalized input [-1,1]; no limiter. Dry is immediate, early has B8
43
+ * latency plus 7/13/23ms taps, and late begins near 29.7ms with an IIR tail.
44
+ * Call once per sample in stride-1 forSample with its native everyNSamples.
45
+ */
46
+ export const hybridReverb = defineSubgraph((config) => {
47
+ const paths = spatialPaths(config);
48
+ return {
49
+ tick(input, c, everyNSamples) {
50
+ const { early, tail } = paths.tick(input, c, everyNSamples);
51
+ return { left: blend(input, early.left, f64(tail.left), c), right: blend(input, early.right, f64(tail.right), c) };
52
+ },
53
+ };
54
+ });
55
+ /** Same hybrid space with pitch applied only to a feedforward copy of its FDN
56
+ * output. No pitched signal enters the FDN or early path. This is
57
+ * not regenerative shimmer. Pitch adds variable 1..W+1-sample read ages, with
58
+ * interpolation aliasing/window coloration; parallel blending can cancel.
59
+ */
60
+ export const feedforwardPitchedReverb = defineSubgraph((config) => {
61
+ const paths = spatialPaths(config);
62
+ const left = instantiate(windowedPitchShift, config, { name: 'pitchLeft' });
63
+ const right = instantiate(windowedPitchShift, config, { name: 'pitchRight' });
64
+ return {
65
+ tick(input, c, everyNSamples) {
66
+ const { early, tail } = paths.tick(input, c, everyNSamples);
67
+ const l = left.tick(tail.left, c), r = right.tick(tail.right, c);
68
+ const mix = f64(c.pitchMix).clamp(0, 1), dry = f64(1).sub(mix);
69
+ return {
70
+ left: blend(input, early.left, f64(tail.left).mul(dry).add(f64(l.output).mul(mix)), c),
71
+ right: blend(input, early.right, f64(tail.right).mul(dry).add(f64(r.output).mul(mix)), c),
72
+ ratioRejected: l.ratioRejected,
73
+ };
74
+ },
75
+ };
76
+ });
@@ -0,0 +1,27 @@
1
+ import { type Node } from '@unworklet/core';
2
+ /** Internal fixed-capacity buffer iteration, using only public native loops.
3
+ * No audio/parameter indexing or sample-clock advancement belongs in this body.
4
+ */
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;
12
+ /** Internal construction-fixed FFT. Not a separate runtime or public package API. */
13
+ export declare const spectralFft: import("@unworklet/core").SubgraphDecl<[config: {
14
+ size: number;
15
+ }], {
16
+ /** JS functions/loops run during graph capture. Butterfly iteration uses the
17
+ * public fixed-count native forIndex loop over work-buffer indices, nested
18
+ * inside the caller's hop body. It performs no audio I/O or audio-time update.
19
+ * Inputs must not alias these work buffers.
20
+ * Result aliases scratch and must be consumed before the next transform.
21
+ * Forward: sum x[n] exp(-i*2*pi*k*n/N). Inverse: positive sign and 1/N.
22
+ */
23
+ transform(real: (index: Node<"i32">) => Node<"f64">, imag: (index: Node<"i32">) => Node<"f64">, inverse: boolean): {
24
+ real: (index: number | Node<"i32">) => Node<"f64">;
25
+ imag: (index: number | Node<"i32">) => Node<"f64">;
26
+ };
27
+ }>;