@denaudio/den 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/LICENSE-APACHE +202 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +102 -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 +15 -0
  14. package/dist/convolution.js +81 -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 +22 -0
  86. package/dist/prepared-convolution.js +99 -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 +44 -0
  90. package/dist/resident-time-stretch.js +135 -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 +48 -0
  100. package/dist/spatial-chains.js +76 -0
  101. package/dist/spectral-fft.d.ts +21 -0
  102. package/dist/spectral-fft.js +74 -0
  103. package/dist/spectral-freeze.d.ts +22 -0
  104. package/dist/spectral-freeze.js +74 -0
  105. package/dist/spectral-gate.d.ts +21 -0
  106. package/dist/spectral-gate.js +58 -0
  107. package/dist/spectral-stft.d.ts +13 -0
  108. package/dist/spectral-stft.js +69 -0
  109. package/dist/spectral-texture.d.ts +42 -0
  110. package/dist/spectral-texture.js +141 -0
  111. package/dist/spectral.d.ts +16 -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 +431 -0
  133. package/docs/character-filter.md +139 -0
  134. package/docs/contracts.md +131 -0
  135. package/docs/control-modulation.md +194 -0
  136. package/docs/convolution.md +45 -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 +61 -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 +188 -0
  174. package/docs/realtime-audition.md +63 -0
  175. package/docs/resident-recorder.md +151 -0
  176. package/docs/resident-time-stretch.md +190 -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 +250 -0
  181. package/docs/spectral-freeze-entry.md +130 -0
  182. package/docs/spectral-gate-entry.md +130 -0
  183. package/docs/spectral-large-candidate.md +74 -0
  184. package/docs/spectral-texture-entry.md +174 -0
  185. package/docs/spectral.md +118 -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,44 @@
1
+ import { type EveryNSamples, type Node } from '@unworklet/core';
2
+ import type { ResidentSample } from './sample.js';
3
+ export interface ResidentTimeStretchConfig {
4
+ /** Match ctx.sampleRate; integer 8000..192000. */
5
+ sampleRate: number;
6
+ /** Resident sourceSampleRate must also be an integer in [8000,192000]. */
7
+ sample: ResidentSample;
8
+ /** Fixed synthesis hop. Window length is twice this value; default 256. */
9
+ hopSamples?: 128 | 256;
10
+ /** Integer source-frame offsets searched, independently of source rate.
11
+ * Zero disables alignment. Default 64; compare 64 points per candidate. */
12
+ searchFrames?: 0 | 8 | 16 | 32 | 64 | 128;
13
+ }
14
+ export interface ResidentTimeStretchControls {
15
+ gate: Node<'bool'>;
16
+ /** Level-sensitive request. Starts at the next global hop boundary while
17
+ * gate stays high. Held trigger restarts each hop; it can click. */
18
+ trigger: Node<'bool'>;
19
+ /** Silences/cancels now. Does not rephase the global hop clock. */
20
+ reset: Node<'bool'>;
21
+ /** Output duration / original duration, [0.5,2], latched at actual launch.
22
+ * Invalid values reject that launch. Changes need a fresh trigger. */
23
+ durationScale: Node<'f32'>;
24
+ /** Grain-local pitch multiplier, [0.5,2], latched at actual launch. */
25
+ pitchRatio: Node<'f32'>;
26
+ }
27
+ /** Fixed-capacity mono resident WSOLA variant. Native 64-point squared-error
28
+ * alignment and complementary linear overlap; no normalization or clipping.
29
+ * True independent duration/pitch controls, with bounded search/transient and
30
+ * interpolation/alias limitations. Call once in stride-1 forSample, passing
31
+ * that loop's everyNSamples. Native snapshots continue only the same schema.
32
+ */
33
+ export declare const residentTimeStretch: import("@unworklet/core").SubgraphDecl<[config: ResidentTimeStretchConfig], {
34
+ tick(c: ResidentTimeStretchControls, everyNSamples: EveryNSamples): {
35
+ output: Node<"f32">;
36
+ active: Node<"bool">;
37
+ position: Node<"f32">;
38
+ ended: Node<"bool">;
39
+ pending: Node<"bool">;
40
+ rejected: Node<"bool">;
41
+ missing: Node<"bool">;
42
+ selectedOffset: Node<"i32">;
43
+ };
44
+ }>;
@@ -0,0 +1,135 @@
1
+ import { defineSubgraph, f32, f64, i32, select, state } from '@unworklet/core';
2
+ import { spectralForEach } from './spectral-fft.js';
3
+ // Exact binary internal scaling protects tiny waveform differences from the
4
+ // native 1e-30 scalar-store scrub. Even opposite finite f32 extrema produce
5
+ // a 64-term squared score below 2**649, far inside finite f64 range.
6
+ const SCALE = 2 ** 192;
7
+ /** Fixed-capacity mono resident WSOLA variant. Native 64-point squared-error
8
+ * alignment and complementary linear overlap; no normalization or clipping.
9
+ * True independent duration/pitch controls, with bounded search/transient and
10
+ * interpolation/alias limitations. Call once in stride-1 forSample, passing
11
+ * that loop's everyNSamples. Native snapshots continue only the same schema.
12
+ */
13
+ export const residentTimeStretch = defineSubgraph((config) => {
14
+ const { sample, sampleRate, hopSamples: H = 256, searchFrames: S = 64 } = config;
15
+ if (!Number.isInteger(sampleRate) || sampleRate < 8000 || sampleRate > 192000 ||
16
+ !Number.isInteger(sample.sourceSampleRate) || sample.sourceSampleRate < 8000 || sample.sourceSampleRate > 192000 ||
17
+ ![128, 256].includes(H) || ![0, 8, 16, 32, 64, 128].includes(S)) {
18
+ throw new RangeError('residentTimeStretch requires integer sampleRate/sourceSampleRate [8000,192000], hopSamples 128/256 and searchFrames 0/8/16/32/64/128');
19
+ }
20
+ const active = state.bool(false).named('active'), pending = state.bool(false).named('pending');
21
+ const ended = state.bool(false).named('ended'), rejected = state.bool(false).named('rejected');
22
+ const previousGate = state.bool(false).named('previousGate'), revision = state.i32(-1).named('revision');
23
+ const clock = state.i32(0).named('clock'), age = state.i32(0).named('age'), total = state.i32(0).named('total');
24
+ const sourceLength = state.i32(0).named('sourceLength'), pitchStep = state.f64(1).named('pitchStep');
25
+ const current = state.f64(0).named('currentStart'), previous = state.f64(0).named('previousStart');
26
+ const first = state.bool(true).named('first'), offset = state.i32(0).named('selectedOffset');
27
+ const reference = state.buffer.f64({ size: 64 }).expose({ name: 'referenceScaled', snapshot: 'transient' });
28
+ const score = state.f64(0).expose({ name: 'scoreScaled', snapshot: 'transient' });
29
+ const bestScore = state.f64(0).expose({ name: 'bestScoreScaled', snapshot: 'transient' });
30
+ const bestOffset = state.i32(0).expose({ name: 'bestOffset', snapshot: 'transient' });
31
+ const launching = state.bool(false).expose({ name: 'launching', snapshot: 'transient' });
32
+ const oldValue = state.f64(0).expose({ name: 'oldValueScaled', snapshot: 'transient' });
33
+ const wet = state.f64(0).expose({ name: 'wetScaled', snapshot: 'transient' });
34
+ // Each PCM endpoint outside [0,length) is zero, including fractional reads
35
+ // straddling either boundary. Clamp indices BEFORE calling the shared reader.
36
+ const readScaled = (position) => {
37
+ const whole = position.floor(), fraction = position.sub(whole), end = sample.length();
38
+ const a = f64(sample.read(whole.clamp(0, f64(end.sub(1).max(0))), i32(0), end, false));
39
+ oldValue.write(select(whole.gte(0).and(whole.lt(f64(end))), a, f64(0)).mul(SCALE));
40
+ const next = whole.add(1);
41
+ const b = f64(sample.read(next.clamp(0, f64(end.sub(1).max(0))), i32(0), end, false));
42
+ return oldValue.read().mul(f64(1).sub(fraction)).add(select(next.gte(0).and(next.lt(f64(end))), b, f64(0)).mul(SCALE).mul(fraction));
43
+ };
44
+ // Exact rational ceil(L * R * D / Rs) for integer rates and finite f32 D.
45
+ // A=L*R<2**34 and K=D*2**24<=2**25. Split the >53-bit A*K product:
46
+ // hi*K<2**43 and lo*K<2**41 are exact integers. Numerator / 2**16 is
47
+ // represented by its integer part and a 16-bit remainder. The nearest
48
+ // approximate integer q is either floor(exact) or ceil(exact), since the
49
+ // absolute arithmetic error is far below 0.5. Exact comparison with
50
+ // q*Rs*256 then returns the true ceil without an epsilon adjustment.
51
+ const durationFrames = (duration) => {
52
+ const a = f64(sample.length()).mul(sampleRate), k = f64(duration).mul(2 ** 24);
53
+ const hi = a.div(65536).floor(), loProduct = a.sub(hi.mul(65536)).mul(k);
54
+ const integerPart = hi.mul(k).add(loProduct.div(65536).floor()), remainder = loProduct.mod(65536);
55
+ const q = a.mul(f64(duration)).div(sample.sourceSampleRate).add(.5).floor();
56
+ const target = q.mul(sample.sourceSampleRate).mul(256);
57
+ const greater = integerPart.gt(target).or(integerPart.eq(target).and(remainder.gt(0)));
58
+ return i32(q.add(select(greater, f64(1), f64(0))));
59
+ };
60
+ return {
61
+ tick(c, everyNSamples) {
62
+ const missing = sample.length().eq(0), changed = sample.revision().eq(revision.read()).not();
63
+ const clear = c.reset.or(changed).or(missing);
64
+ active.write(active.read().and(clear.not()).and(c.gate));
65
+ pending.write(pending.read().and(clear.not()).and(c.gate));
66
+ ended.write(ended.read().and(clear.not()));
67
+ rejected.write(rejected.read().and(c.reset.not()));
68
+ age.write(select(clear, i32(0), age.read()));
69
+ total.write(select(clear, i32(0), total.read()));
70
+ sourceLength.write(select(clear, i32(0), sourceLength.read()));
71
+ const request = c.trigger.or(c.gate.and(previousGate.read().not())).and(c.gate).and(c.reset.not()).and(missing.not());
72
+ 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, () => {
77
+ const due = clock.read().eq(0), attempt = due.and(pending.read()).and(c.gate).and(c.reset.not());
78
+ const valid = c.durationScale.gte(.5).and(c.durationScale.lte(2)).and(c.pitchRatio.gte(.5)).and(c.pitchRatio.lte(2));
79
+ launching.write(attempt.and(valid).and(missing.not()));
80
+ const launch = launching.read();
81
+ pending.write(pending.read().and(attempt.not()));
82
+ rejected.write(select(attempt, valid.not(), rejected.read()));
83
+ age.write(select(launch, i32(0), age.read()));
84
+ total.write(select(launch, durationFrames(select(valid, c.durationScale, f32(1))), total.read()));
85
+ sourceLength.write(select(launch, sample.length(), sourceLength.read()));
86
+ pitchStep.write(select(launch, f64(c.pitchRatio).mul(sample.sourceSampleRate / sampleRate), pitchStep.read()));
87
+ active.write(active.read().or(launch));
88
+ ended.write(ended.read().and(launch.not()));
89
+ const nominal = f64(age.read()).mul(f64(sourceLength.read())).div(f64(total.read().max(1)));
90
+ const priorTail = current.read().add(pitchStep.read().mul(H));
91
+ spectralForEach(64, j => reference.write(j, readScaled(priorTail.add(f64(j).mul(H / 64).mul(pitchStep.read())))));
92
+ const compare = (candidate) => {
93
+ score.write(0);
94
+ spectralForEach(64, j => {
95
+ const value = readScaled(nominal.add(f64(candidate)).add(f64(j).mul(H / 64).mul(pitchStep.read())));
96
+ const difference = value.sub(reference.read(j));
97
+ score.write(score.read().add(difference.mul(difference)));
98
+ });
99
+ };
100
+ compare(i32(0));
101
+ bestScore.write(score.read());
102
+ bestOffset.write(0);
103
+ if (S > 0)
104
+ spectralForEach(S * 2, j => {
105
+ const distance = j.div(2).add(1), candidate = select(j.mod(2).eq(0), distance, distance.mul(-1));
106
+ compare(candidate);
107
+ const improve = score.read().lt(bestScore.read());
108
+ bestOffset.write(select(improve, candidate, bestOffset.read()));
109
+ bestScore.write(select(improve, score.read(), bestScore.read()));
110
+ });
111
+ const commit = due.and(active.read());
112
+ previous.write(select(commit, select(launch, f64(0), priorTail), previous.read()));
113
+ current.write(select(commit, select(launch, f64(0), nominal.add(f64(bestOffset.read()))), current.read()));
114
+ offset.write(select(commit, select(launch, i32(0), bestOffset.read()), offset.read()));
115
+ first.write(select(commit, launch, first.read()));
116
+ });
117
+ const exhausted = active.read().and(age.read().gte(total.read()));
118
+ ended.write(ended.read().or(exhausted));
119
+ active.write(active.read().and(exhausted.not()));
120
+ const playing = active.read();
121
+ const local = f64(clock.read()).mul(pitchStep.read()), blend = f64(clock.read()).div(H);
122
+ // Capture the first read because resident.read uses a shared transient.
123
+ wet.write(readScaled(previous.read().add(local)));
124
+ const incoming = readScaled(current.read().add(local));
125
+ const mixed = select(first.read(), incoming, wet.read().mul(f64(1).sub(blend)).add(incoming.mul(blend)));
126
+ const output = select(playing, f32(mixed.div(SCALE)), f32(0));
127
+ const position = f32(f64(age.read().min(total.read())).mul(f64(sourceLength.read())).div(f64(total.read().max(1))));
128
+ age.write(select(playing, age.read().add(1), age.read()));
129
+ clock.write(clock.read().add(1).mod(H));
130
+ previousGate.write(c.gate.and(c.reset.not()));
131
+ revision.write(sample.revision());
132
+ return { output, active: playing, position, ended: ended.read(), pending: pending.read(), rejected: rejected.read(), missing, selectedOffset: offset.read() };
133
+ },
134
+ };
135
+ });
@@ -0,0 +1,32 @@
1
+ import { type Node } from '@unworklet/core';
2
+ export interface ResonatorMode {
3
+ frequencyHz: number;
4
+ decaySeconds: number;
5
+ gain: number;
6
+ }
7
+ export interface ModalResonatorConfig {
8
+ sampleRate: number;
9
+ modes: readonly ResonatorMode[];
10
+ }
11
+ /** Fixed stable two-pole modes. Decay is amplitude T60 in seconds. */
12
+ export declare const modalResonator: import("@unworklet/core").SubgraphDecl<[config: ModalResonatorConfig], {
13
+ /** Excitation clamps to [-1,1]. Reset clears each mode before current excitation. */
14
+ tick(input: Node<"f32">, reset: Node<"bool">): Node<"f32">;
15
+ }>;
16
+ export interface TunedCombConfig {
17
+ sampleRate: number;
18
+ minFrequencyHz: number;
19
+ }
20
+ export interface TunedCombControls {
21
+ input: Node<'f32'>;
22
+ frequencyHz: Node<'f32'>;
23
+ feedback: Node<'f32'>;
24
+ /** One-pole memory coefficient in [0,1]; 0 bypasses damping, 1 freezes it. */
25
+ damping: Node<'f32'>;
26
+ reset: Node<'bool'>;
27
+ }
28
+ /** Fractional moving-delay resonator. Interpolation and damping change tuning/decay. */
29
+ export declare const tunedComb: import("@unworklet/core").SubgraphDecl<[config: TunedCombConfig], {
30
+ /** Normalize excitation by (1-|feedback|) before storage; return the raw tap. */
31
+ tick(c: TunedCombControls): Node<"f32">;
32
+ }>;
@@ -0,0 +1,63 @@
1
+ import { defineSubgraph, f32, f64, instantiate, select, state } from '@unworklet/core';
2
+ import { delayReadhead } from './delay-readhead.js';
3
+ function maximumFrequency(sampleRate) {
4
+ if (!Number.isFinite(sampleRate) || sampleRate < 8000 || sampleRate > 192000)
5
+ throw new RangeError('resonator sampleRate must be in [8000,192000] Hz');
6
+ return 0.45 * sampleRate;
7
+ }
8
+ /** Fixed stable two-pole modes. Decay is amplitude T60 in seconds. */
9
+ export const modalResonator = defineSubgraph((config) => {
10
+ const maximum = maximumFrequency(config.sampleRate);
11
+ if (config.modes.length < 1 || config.modes.length > 16)
12
+ throw new RangeError('modalResonator requires 1–16 modes');
13
+ const scale = 2 ** 128; // Keep valid f32 subnormal tails above upstream scalar-state flush.
14
+ const modes = config.modes.map((mode, i) => {
15
+ if (!Number.isFinite(mode.frequencyHz) || mode.frequencyHz < 20 || mode.frequencyHz > maximum ||
16
+ !Number.isFinite(mode.decaySeconds) || mode.decaySeconds < 0.005 || mode.decaySeconds > 30 ||
17
+ !Number.isFinite(mode.gain) || Math.abs(mode.gain) > 16)
18
+ throw new RangeError('invalid mode frequency, T60 or gain');
19
+ const omega = 2 * Math.PI * mode.frequencyHz / config.sampleRate;
20
+ const radius = Math.exp(-Math.LN10 * 3 / (mode.decaySeconds * config.sampleRate));
21
+ return { gain: mode.gain, a1: 2 * radius * Math.cos(omega), a2: -radius * radius, b: Math.sin(omega),
22
+ first: state.f64(0).named(`mode${i}First`), second: state.f64(0).named(`mode${i}Second`) };
23
+ });
24
+ const normalization = modes.reduce((sum, mode) => sum + Math.abs(mode.gain), 0) || 1;
25
+ return {
26
+ /** Excitation clamps to [-1,1]. Reset clears each mode before current excitation. */
27
+ tick(input, reset) {
28
+ let sum = f64(0);
29
+ for (const mode of modes) {
30
+ const first = select(reset, f64(0), mode.first.read().div(scale));
31
+ const second = select(reset, f64(0), mode.second.read().div(scale));
32
+ const next = f64(input).clamp(-1, 1).mul(mode.b).add(first.mul(mode.a1)).add(second.mul(mode.a2));
33
+ mode.second.write(first.mul(scale));
34
+ mode.first.write(next.mul(scale));
35
+ sum = sum.add(next.mul(mode.gain / normalization));
36
+ }
37
+ return f32(sum);
38
+ },
39
+ };
40
+ });
41
+ /** Fractional moving-delay resonator. Interpolation and damping change tuning/decay. */
42
+ export const tunedComb = defineSubgraph((config) => {
43
+ const maximum = maximumFrequency(config.sampleRate);
44
+ if (!Number.isFinite(config.minFrequencyHz) || config.minFrequencyHz < 20 || config.minFrequencyHz > maximum) {
45
+ throw new RangeError('comb minFrequencyHz must be in [20,.45*sampleRate]');
46
+ }
47
+ const line = instantiate(delayReadhead, { sampleRate: config.sampleRate, maxDelaySeconds: 1 / config.minFrequencyHz }, { name: 'line' });
48
+ const history = state.f64(0).named('dampingHistory');
49
+ const scale = 2 ** 128;
50
+ return {
51
+ /** Normalize excitation by (1-|feedback|) before storage; return the raw tap. */
52
+ tick(c) {
53
+ const frequency = f64(c.frequencyHz).clamp(config.minFrequencyHz, maximum);
54
+ const feedback = f64(c.feedback).clamp(-0.999, 0.999), damping = f64(c.damping).clamp(0, 1);
55
+ const tap = line.read(f32(f64(1).div(frequency)), c.reset);
56
+ const previous = select(c.reset, f64(0), history.read().div(scale));
57
+ const filtered = f64(tap.output).mul(f64(1).sub(damping)).add(previous.mul(damping));
58
+ history.write(filtered.mul(scale));
59
+ tap.write(f32(f64(c.input).mul(f64(1).sub(feedback.abs())).add(filtered.mul(feedback))));
60
+ return tap.output;
61
+ },
62
+ };
63
+ });
@@ -0,0 +1,27 @@
1
+ import { type Node } from '@unworklet/core';
2
+ export interface AlgorithmicReverbConfig {
3
+ sampleRate: number;
4
+ /** Fixed delay-time scale, [0.5, 2]. Default 1. Rebuild to change. */
5
+ roomScale?: number;
6
+ /** Nominal loop-gain loss time in seconds, [0.1, 10]. Default 1.5.
7
+ * Not an exact RT60 or hard tail-duration ceiling: very low damping adds
8
+ * loop group delay and can lengthen low-band decay. Rebuild to change. */
9
+ decaySeconds?: number;
10
+ /** Fixed one-pole loop lowpass corner, [20, 0.45*sampleRate]. Default min(6000, 0.45*sampleRate). */
11
+ dampingHz?: number;
12
+ }
13
+ export interface ReverbControls {
14
+ mix: Node<'f32'>;
15
+ /** Dry output, no new input; existing tail continues internally. */
16
+ bypass: Node<'bool'>;
17
+ reset: Node<'bool'>;
18
+ }
19
+ /** Four-line Hadamard FDN candidate. Fixed capacities and coefficients, no FFT.
20
+ * Finite inputs required. No predelay; the shortest network delay precedes wet audio.
21
+ */
22
+ export declare const algorithmicReverb: import("@unworklet/core").SubgraphDecl<[config: AlgorithmicReverbConfig], {
23
+ tick(inputLeft: Node<"f32">, inputRight: Node<"f32">, c: ReverbControls): {
24
+ left: Node<"f32">;
25
+ right: Node<"f32">;
26
+ };
27
+ }>;
package/dist/reverb.js ADDED
@@ -0,0 +1,52 @@
1
+ import { defineSubgraph, f32, f64, instantiate, select, state } from '@unworklet/core';
2
+ import { delayReadhead } from './delay-readhead.js';
3
+ /** Four-line Hadamard FDN candidate. Fixed capacities and coefficients, no FFT.
4
+ * Finite inputs required. No predelay; the shortest network delay precedes wet audio.
5
+ */
6
+ export const algorithmicReverb = defineSubgraph((config) => {
7
+ const { sampleRate, roomScale = 1, decaySeconds = 1.5 } = config;
8
+ const dampingHz = config.dampingHz ?? Math.min(6000, 0.45 * sampleRate);
9
+ if (!Number.isInteger(sampleRate) || sampleRate < 8000 || sampleRate > 192000 ||
10
+ !Number.isFinite(roomScale) || roomScale < 0.5 || roomScale > 2 ||
11
+ !Number.isFinite(decaySeconds) || decaySeconds < 0.1 || decaySeconds > 10 ||
12
+ !Number.isFinite(dampingHz) || dampingHz < 20 || dampingHz > 0.45 * sampleRate) {
13
+ throw new RangeError('algorithmicReverb requires sample rate [8000,192000], roomScale [0.5,2], decaySeconds [0.1,10], dampingHz [20,0.45*sampleRate]');
14
+ }
15
+ // Incommensurate reference lengths (seconds), rounded to fixed sample counts.
16
+ const lengths = [0.0297, 0.0371, 0.0411, 0.0437].map(seconds => Math.round(seconds * roomScale * sampleRate));
17
+ const lines = lengths.map((length, i) => ({
18
+ delay: instantiate(delayReadhead, { sampleRate, maxDelaySeconds: length / sampleRate }, { name: `line${i}` }),
19
+ seconds: length / sampleRate,
20
+ gain: 10 ** (-3 * length / (sampleRate * decaySeconds)),
21
+ damping: state.f64(0).named(`damping${i}`),
22
+ wet: state.f64(0).expose({ name: `wet${i}`, snapshot: 'transient' }),
23
+ }));
24
+ // One-pole matched-z lowpass: convex combination, unity DC, no added resonance.
25
+ const pole = Math.exp(-2 * Math.PI * dampingHz / sampleRate);
26
+ // H/2 is orthogonal. These literal rows also keep generated work fixed.
27
+ const matrix = [[1, 1, 1, 1], [1, -1, 1, -1], [1, 1, -1, -1], [1, -1, -1, 1]];
28
+ return {
29
+ tick(inputLeft, inputRight, c) {
30
+ const heads = lines.map(line => line.delay.read(f32(line.seconds), c.reset));
31
+ for (let i = 0; i < lines.length; i++) {
32
+ const line = lines[i];
33
+ line.wet.write(f64(heads[i].output));
34
+ line.damping.write(select(c.reset, f64(0), line.damping.read()).mul(pole).add(line.wet.read().mul(1 - pole)));
35
+ }
36
+ const damped = lines.map(line => line.damping.read().mul(line.gain));
37
+ const left = f64(select(c.bypass, f32(0), inputLeft)), right = f64(select(c.bypass, f32(0), inputRight));
38
+ for (let i = 0; i < lines.length; i++) {
39
+ const injection = left.add(right.mul(matrix[1][i])).mul(0.5);
40
+ const feedback = damped.reduce((sum, x, j) => sum.add(x.mul(matrix[i][j] * 0.5)), f64(0));
41
+ heads[i].write(f32(injection.add(feedback)));
42
+ }
43
+ const wetLeft = lines.reduce((sum, line, i) => sum.add(line.wet.read().mul(matrix[2][i] * 0.5)), f64(0));
44
+ const wetRight = lines.reduce((sum, line, i) => sum.add(line.wet.read().mul(matrix[3][i] * 0.5)), f64(0));
45
+ const mix = f64(c.mix).clamp(0, 1);
46
+ return {
47
+ left: select(c.bypass, inputLeft, f32(f64(inputLeft).mul(f64(1).sub(mix)).add(wetLeft.mul(mix)))),
48
+ right: select(c.bypass, inputRight, f32(f64(inputRight).mul(f64(1).sub(mix)).add(wetRight.mul(mix)))),
49
+ };
50
+ },
51
+ };
52
+ });
@@ -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
+ }>;