@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,104 @@
1
+ import { defineSubgraph, f32, f64, i32, instantiate, select, state } from '@unworklet/core';
2
+ import { spectralDispatchPeriod, spectralFft, spectralForEach } from './spectral-fft.js';
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
+ export { prepareConvolutionSpectrum } from './convolution-spectrum.js';
5
+ /** Fixed B128/P64 prepared-spectrum mono convolution candidate.
6
+ * Correctness/gain bounds require an unchanged packet from host preparation.
7
+ * Caller owns native CAPACITY_16 ingress with payloadCapacity 131072 bytes.
8
+ * Copy/scan runs in native dispatch, so preload with audible playback gated off.
9
+ */
10
+ export const preparedConvolution = defineSubgraph((config) => {
11
+ validatePreparedConvolutionConfig(config);
12
+ const dispatchPeriod = spectralDispatchPeriod(B, config.renderQuantumSize);
13
+ const coefficients = state.buffer.f32({ size: VALUES }).expose({ name: 'coefficients', snapshot: 'persistent' });
14
+ const history = state.buffer.f64({ size: B }).expose({ name: 'historyScaled', snapshot: 'persistent' });
15
+ const previous = state.buffer.f64({ size: VALUES }).expose({ name: 'previousSpectraScaled', snapshot: 'persistent' });
16
+ const mixedReal = state.buffer.f64({ size: N }).expose({ name: 'mixedRealScaled', snapshot: 'transient' });
17
+ const mixedImag = state.buffer.f64({ size: N }).expose({ name: 'mixedImagScaled', snapshot: 'transient' });
18
+ const overlap = state.buffer.f64({ size: N }).expose({ name: 'overlapScaled', snapshot: 'persistent' });
19
+ const cursor = state.i32(0).named('cursor'), spectrumCursor = state.i32(0).named('spectrumCursor');
20
+ const validSamples = state.i32(0).named('validSamples'), validBlocks = state.i32(0).named('validBlocks');
21
+ const pendingReset = state.bool(true).named('pendingReset');
22
+ const loaded = state.bool(false).named('loaded'), rejected = state.bool(false).named('rejected');
23
+ const revision = state.i32(0).named('revision');
24
+ const malformed = state.bool(false).expose({ name: 'malformed', snapshot: 'transient' });
25
+ const sumReal = state.f64(0).expose({ name: 'sumRealScaled', snapshot: 'transient' });
26
+ const sumImag = state.f64(0).expose({ name: 'sumImagScaled', snapshot: 'transient' });
27
+ const fft = instantiate(spectralFft, { size: N }, { name: 'fft' });
28
+ const scale = 2 ** 128;
29
+ return {
30
+ /** Module-specific native event payload; this is not a host loader/ack. */
31
+ load(packet) {
32
+ coefficients.copyFrom(packet.spectrum);
33
+ const length = packet.spectrum.length;
34
+ const header = packet.formatVersion.eq(1).and(packet.blockSize.eq(B)).and(packet.partitions.eq(P));
35
+ const complete = header.and(packet.impulseFrames.gte(1)).and(packet.impulseFrames.lte(B * P)).and(packet.impulseFrames.floor().eq(packet.impulseFrames))
36
+ .and(packet.declaredValues.eq(VALUES)).and(length.eq(VALUES));
37
+ const unload = header.and(packet.impulseFrames.eq(0)).and(packet.declaredValues.eq(0)).and(length.eq(0));
38
+ malformed.write(false);
39
+ spectralForEach(P, p => spectralForEach(N, k => {
40
+ const index = p.mul(N).add(k).mul(2), re = coefficients.read(index), im = coefficients.read(index.add(1));
41
+ malformed.write(malformed.read().or(re.eq(re).not()).or(im.eq(im).not()).or(re.abs().gt(4)).or(im.abs().gt(4)));
42
+ }));
43
+ loaded.write(complete.and(malformed.read().not()));
44
+ rejected.write(loaded.read().not().and(unload.not()));
45
+ revision.write(revision.read().add(1));
46
+ validSamples.write(0);
47
+ validBlocks.write(0);
48
+ pendingReset.write(true);
49
+ },
50
+ /** Once per sample, inside stride-1 forSample with its native scheduler.
51
+ * Finite normalized input[-1,1]. Reset discards its sample and keeps the IR.
52
+ * Reload clears prior tail but accepts the next sample after its handler.
53
+ */
54
+ tick(input, reset, everyNSamples) {
55
+ const invalidate = reset.or(loaded.read().not());
56
+ validSamples.write(select(invalidate, i32(0), validSamples.read()));
57
+ validBlocks.write(select(invalidate, i32(0), validBlocks.read()));
58
+ pendingReset.write(pendingReset.read().or(invalidate));
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);
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);
64
+ spectralForEach(N, k => {
65
+ const index = spectrumCursor.read().mul(N).add(k).mul(2);
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))));
68
+ });
69
+ spectralForEach(N, k => {
70
+ sumReal.write(0);
71
+ sumImag.write(0);
72
+ spectralForEach(P, p => {
73
+ const x = spectrumCursor.read().add(P).sub(p).mod(P).mul(N).add(k).mul(2);
74
+ const h = p.mul(N).add(k).mul(2);
75
+ const xr = select(validBlocks.read().gte(p), previous.read(x), f64(0));
76
+ const xi = select(validBlocks.read().gte(p), previous.read(x.add(1)), f64(0));
77
+ // Mask malformed/unloaded coefficient bytes before multiplication.
78
+ const hr = f64(select(loaded.read(), coefficients.read(h), f32(0)));
79
+ const hi = f64(select(loaded.read(), coefficients.read(h.add(1)), f32(0)));
80
+ sumReal.write(sumReal.read().add(xr.mul(hr).sub(xi.mul(hi))));
81
+ sumImag.write(sumImag.read().add(xr.mul(hi).add(xi.mul(hr))));
82
+ });
83
+ mixedReal.write(k, sumReal.read());
84
+ mixedImag.write(k, sumImag.read());
85
+ });
86
+ const result = fft.transform(n => mixedReal.read(n), n => mixedImag.read(n), true);
87
+ spectralForEach(N, n => {
88
+ const index = cursor.read().add(n).mod(N);
89
+ const prior = overlap.read(index);
90
+ overlap.write(index, select(frameDue, select(pendingReset.read(), f64(0), prior).add(result.real(n)), prior));
91
+ });
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()));
95
+ });
96
+ const output = select(invalidate.or(pendingReset.read()), f32(0), f32(overlap.read(cursor.read()).div(scale)));
97
+ overlap.write(cursor.read(), f64(0));
98
+ history.write(cursor.read().mod(B), f64(select(invalidate, f32(0), input)).mul(scale));
99
+ cursor.write(cursor.read().add(1).mod(N));
100
+ validSamples.write(select(invalidate, i32(0), validSamples.read().add(1).min(B)));
101
+ return { output, loaded: loaded.read(), rejected: rejected.read(), revision: revision.read() };
102
+ },
103
+ };
104
+ });
@@ -0,0 +1,28 @@
1
+ import { type Node } from '@unworklet/core';
2
+ import type { ResidentSample } from './sample.js';
3
+ export interface ResidentTakeRecorderConfig {
4
+ /** Mono take capacity, 1..65,536 frames. Separate fixed load/record buffers. */
5
+ capacity: number;
6
+ /** Rate of the incoming signal/processor, 8,000..192,000 Hz. No resampling. */
7
+ sampleRate: number;
8
+ }
9
+ export interface ResidentTakeRecorderControls {
10
+ input: Node<'f32'>;
11
+ /** Append one frame per tick; false pauses. Capacity stops recording without wrapping. */
12
+ record: Node<'bool'>;
13
+ /** Level-sensitive logical clear, with priority over record. Not secure erasure. */
14
+ reset: Node<'bool'>;
15
+ }
16
+ export interface ResidentTakeRecorder {
17
+ /** Existing resident reader/load contract. Load replaces the take; later recording appends. */
18
+ readonly sample: ResidentSample;
19
+ tick(controls: ResidentTakeRecorderControls): {
20
+ written: Node<'bool'>;
21
+ full: Node<'bool'>;
22
+ length: Node<'i32'>;
23
+ };
24
+ }
25
+ /** Native finite take/pause/resume writer for catalog §3. Exact binary scaling
26
+ * retains finite f32 input below core's scalar-store floor. No overdub, circular
27
+ * overwrite, gain, device input, transport, or realtime guarantee. */
28
+ export declare const residentTakeRecorder: import("@unworklet/core").SubgraphDecl<[config: ResidentTakeRecorderConfig], ResidentTakeRecorder>;
@@ -0,0 +1,68 @@
1
+ import { defineSubgraph, f32, f64, i32, select, state } from '@unworklet/core';
2
+ const READ_POSITION_SCALE = 2 ** 192;
3
+ const PCM_SCALE = 2 ** 128;
4
+ const F32_MAX = 3.4028234663852886e38;
5
+ /** Native finite take/pause/resume writer for catalog §3. Exact binary scaling
6
+ * retains finite f32 input below core's scalar-store floor. No overdub, circular
7
+ * overwrite, gain, device input, transport, or realtime guarantee. */
8
+ export const residentTakeRecorder = defineSubgraph((config) => {
9
+ const { capacity, sampleRate } = config;
10
+ if (!Number.isInteger(capacity) || capacity < 1 || capacity > 65536)
11
+ throw new RangeError('take capacity must be an integer in [1,65536]');
12
+ if (!Number.isFinite(sampleRate) || sampleRate < 8000 || sampleRate > 192000)
13
+ throw new RangeError('sampleRate must be in [8000,192000] Hz');
14
+ // Event ingress keeps its original f32 bytes; appended PCM is scaled into
15
+ // f64 so even the smallest nonzero f32 survives core's 1e-30 store floor.
16
+ // One boundary chooses the loaded prefix or recorded suffix. No conversion
17
+ // loop/copy or additional runtime is required when replacing a take.
18
+ const loadedPCM = state.buffer.f32({ size: capacity }).expose({ name: 'loadedPCM', snapshot: 'persistent' });
19
+ const recordedPCM = state.buffer.f64({ size: capacity + 1 }).expose({ name: 'recordedPCM', snapshot: 'persistent' });
20
+ const length = state.i32(0).named('length'), revision = state.i32(0).named('revision');
21
+ const loadedPrefixLength = state.i32(0).named('loadedPrefixLength');
22
+ const readPosition = state.f64(0).expose({ name: 'readPosition', snapshot: 'transient' });
23
+ const sample = {
24
+ capacity, sourceSampleRate: sampleRate,
25
+ load(data) {
26
+ loadedPCM.copyFrom(data);
27
+ length.write(data.length.clamp(0, capacity));
28
+ loadedPrefixLength.write(data.length.clamp(0, capacity));
29
+ revision.write(revision.read().add(1));
30
+ },
31
+ length() { return length.read(); },
32
+ revision() { return revision.read(); },
33
+ // Match residentSample's published interpolation, clipping and invalid-tap
34
+ // policy without changing the existing module or consumer interfaces.
35
+ read(position, start, end, loop) {
36
+ const lo = start.clamp(0, capacity - 1), hi = end.clamp(0, capacity).min(length.read());
37
+ const valid = hi.gt(lo);
38
+ const safe = select(position.eq(position), position, f64(lo)).clamp(-2147483648, 2147483647);
39
+ const span = f64(hi.sub(lo).max(1)), relative = safe.sub(f64(lo));
40
+ const wrapped = relative.sub(relative.div(span).floor().mul(span)).add(f64(lo));
41
+ readPosition.write((loop ? wrapped : safe.clamp(f64(lo), f64(hi.sub(1).max(lo)))).mul(READ_POSITION_SCALE));
42
+ const p = readPosition.read().div(READ_POSITION_SCALE);
43
+ const whole = i32(p.floor()).clamp(0, capacity - 1), fraction = p.sub(f64(whole));
44
+ const next = loop ? select(whole.add(1).gte(hi), lo, whole.add(1)) : whole.add(1).min(hi.sub(1).max(lo));
45
+ const second = next.clamp(0, capacity - 1), prefix = loadedPrefixLength.read();
46
+ const rawA = select(whole.lt(prefix), f64(loadedPCM.read(whole)), recordedPCM.read(whole).div(PCM_SCALE));
47
+ const rawB = select(second.lt(prefix), f64(loadedPCM.read(second)), recordedPCM.read(second).div(PCM_SCALE));
48
+ const a = select(rawA.eq(rawA).and(rawA.abs().lte(F32_MAX)), rawA, f64(0));
49
+ const b = select(rawB.eq(rawB).and(rawB.abs().lte(F32_MAX)), rawB, f64(0));
50
+ return select(valid, f32(a.add(b.sub(a).mul(fraction))), f32(0));
51
+ },
52
+ };
53
+ return { sample, tick(c) {
54
+ const before = length.read();
55
+ const written = c.record.and(c.reset.not()).and(before.lt(capacity));
56
+ const index = select(written, before, i32(capacity));
57
+ const finite = c.input.eq(c.input).and(c.input.abs().lte(F32_MAX));
58
+ // select evaluates both branches. Inactive writes touch only a private
59
+ // sentinel, never existing exposed PCM. Signed zero stores as positive zero.
60
+ recordedPCM.write(index, select(written.and(finite), f64(c.input).mul(PCM_SCALE), f64(0)));
61
+ // Commit bytes before making the additional frame visible to a later reader.
62
+ length.write(select(c.reset, i32(0), before.add(select(written, i32(1), i32(0)))));
63
+ loadedPrefixLength.write(select(c.reset, i32(0), loadedPrefixLength.read()));
64
+ revision.write(revision.read().add(select(c.reset, i32(1), i32(0))));
65
+ const current = length.read();
66
+ return { written, full: current.eq(capacity), length: current };
67
+ } };
68
+ });
@@ -0,0 +1,46 @@
1
+ import { type EveryNSamples, type Node } from '@unworklet/core';
2
+ import type { ResidentSample } from './sample.js';
3
+ export interface ResidentTimeStretchConfig {
4
+ /** Pass ctx.renderQuantumSize; aligns native dispatch with snapshot boundaries. */
5
+ renderQuantumSize: number;
6
+ /** Match ctx.sampleRate; integer 8000..192000. */
7
+ sampleRate: number;
8
+ /** Resident sourceSampleRate must also be an integer in [8000,192000]. */
9
+ sample: ResidentSample;
10
+ /** Fixed synthesis hop. Window length is twice this value; default 256. */
11
+ hopSamples?: 128 | 256;
12
+ /** Integer source-frame offsets searched, independently of source rate.
13
+ * Zero disables alignment. Default 64; compare 64 points per candidate. */
14
+ searchFrames?: 0 | 8 | 16 | 32 | 64 | 128;
15
+ }
16
+ export interface ResidentTimeStretchControls {
17
+ gate: Node<'bool'>;
18
+ /** Level-sensitive request. Starts at the next global hop boundary while
19
+ * gate stays high. Held trigger restarts each hop; it can click. */
20
+ trigger: Node<'bool'>;
21
+ /** Silences/cancels now. Does not rephase the global hop clock. */
22
+ reset: Node<'bool'>;
23
+ /** Output duration / original duration, [0.5,2], latched at actual launch.
24
+ * Invalid values reject that launch. Changes need a fresh trigger. */
25
+ durationScale: Node<'f32'>;
26
+ /** Grain-local pitch multiplier, [0.5,2], latched at actual launch. */
27
+ pitchRatio: Node<'f32'>;
28
+ }
29
+ /** Fixed-capacity mono resident WSOLA variant. Native 64-point squared-error
30
+ * alignment and complementary linear overlap; no normalization or clipping.
31
+ * True independent duration/pitch controls, with bounded search/transient and
32
+ * interpolation/alias limitations. Call once in stride-1 forSample, passing
33
+ * that loop's everyNSamples. Native snapshots continue only the same schema.
34
+ */
35
+ export declare const residentTimeStretch: import("@unworklet/core").SubgraphDecl<[config: ResidentTimeStretchConfig], {
36
+ tick(c: ResidentTimeStretchControls, everyNSamples: EveryNSamples): {
37
+ output: Node<"f32">;
38
+ active: Node<"bool">;
39
+ position: Node<"f32">;
40
+ ended: Node<"bool">;
41
+ pending: Node<"bool">;
42
+ rejected: Node<"bool">;
43
+ missing: Node<"bool">;
44
+ selectedOffset: Node<"i32">;
45
+ };
46
+ }>;
@@ -0,0 +1,136 @@
1
+ import { defineSubgraph, f32, f64, i32, select, state } from '@unworklet/core';
2
+ import { spectralDispatchPeriod, 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 dispatchPeriod = spectralDispatchPeriod(H, config.renderQuantumSize);
21
+ const active = state.bool(false).named('active'), pending = state.bool(false).named('pending');
22
+ const ended = state.bool(false).named('ended'), rejected = state.bool(false).named('rejected');
23
+ const previousGate = state.bool(false).named('previousGate'), revision = state.i32(-1).named('revision');
24
+ const clock = state.i32(0).named('clock'), age = state.i32(0).named('age'), total = state.i32(0).named('total');
25
+ const sourceLength = state.i32(0).named('sourceLength'), pitchStep = state.f64(1).named('pitchStep');
26
+ const current = state.f64(0).named('currentStart'), previous = state.f64(0).named('previousStart');
27
+ const first = state.bool(true).named('first'), offset = state.i32(0).named('selectedOffset');
28
+ const reference = state.buffer.f64({ size: 64 }).expose({ name: 'referenceScaled', snapshot: 'transient' });
29
+ const score = state.f64(0).expose({ name: 'scoreScaled', snapshot: 'transient' });
30
+ const bestScore = state.f64(0).expose({ name: 'bestScoreScaled', snapshot: 'transient' });
31
+ const bestOffset = state.i32(0).expose({ name: 'bestOffset', snapshot: 'transient' });
32
+ const launching = state.bool(false).expose({ name: 'launching', snapshot: 'transient' });
33
+ const oldValue = state.f64(0).expose({ name: 'oldValueScaled', snapshot: 'transient' });
34
+ const wet = state.f64(0).expose({ name: 'wetScaled', snapshot: 'transient' });
35
+ // Each PCM endpoint outside [0,length) is zero, including fractional reads
36
+ // straddling either boundary. Clamp indices BEFORE calling the shared reader.
37
+ const readScaled = (position) => {
38
+ const whole = position.floor(), fraction = position.sub(whole), end = sample.length();
39
+ const a = f64(sample.read(whole.clamp(0, f64(end.sub(1).max(0))), i32(0), end, false));
40
+ oldValue.write(select(whole.gte(0).and(whole.lt(f64(end))), a, f64(0)).mul(SCALE));
41
+ const next = whole.add(1);
42
+ const b = f64(sample.read(next.clamp(0, f64(end.sub(1).max(0))), i32(0), end, false));
43
+ 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));
44
+ };
45
+ // Exact rational ceil(L * R * D / Rs) for integer rates and finite f32 D.
46
+ // A=L*R<2**34 and K=D*2**24<=2**25. Split the >53-bit A*K product:
47
+ // hi*K<2**43 and lo*K<2**41 are exact integers. Numerator / 2**16 is
48
+ // represented by its integer part and a 16-bit remainder. The nearest
49
+ // approximate integer q is either floor(exact) or ceil(exact), since the
50
+ // absolute arithmetic error is far below 0.5. Exact comparison with
51
+ // q*Rs*256 then returns the true ceil without an epsilon adjustment.
52
+ const durationFrames = (duration) => {
53
+ const a = f64(sample.length()).mul(sampleRate), k = f64(duration).mul(2 ** 24);
54
+ const hi = a.div(65536).floor(), loProduct = a.sub(hi.mul(65536)).mul(k);
55
+ const integerPart = hi.mul(k).add(loProduct.div(65536).floor()), remainder = loProduct.mod(65536);
56
+ const q = a.mul(f64(duration)).div(sample.sourceSampleRate).add(.5).floor();
57
+ const target = q.mul(sample.sourceSampleRate).mul(256);
58
+ const greater = integerPart.gt(target).or(integerPart.eq(target).and(remainder.gt(0)));
59
+ return i32(q.add(select(greater, f64(1), f64(0))));
60
+ };
61
+ return {
62
+ tick(c, everyNSamples) {
63
+ const missing = sample.length().eq(0), changed = sample.revision().eq(revision.read()).not();
64
+ const clear = c.reset.or(changed).or(missing);
65
+ active.write(active.read().and(clear.not()).and(c.gate));
66
+ pending.write(pending.read().and(clear.not()).and(c.gate));
67
+ ended.write(ended.read().and(clear.not()));
68
+ rejected.write(rejected.read().and(c.reset.not()));
69
+ age.write(select(clear, i32(0), age.read()));
70
+ total.write(select(clear, i32(0), total.read()));
71
+ sourceLength.write(select(clear, i32(0), sourceLength.read()));
72
+ const request = c.trigger.or(c.gate.and(previousGate.read().not())).and(c.gate).and(c.reset.not()).and(missing.not());
73
+ pending.write(pending.read().or(request));
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, () => {
78
+ const due = clock.read().eq(0), attempt = due.and(pending.read()).and(c.gate).and(c.reset.not());
79
+ const valid = c.durationScale.gte(.5).and(c.durationScale.lte(2)).and(c.pitchRatio.gte(.5)).and(c.pitchRatio.lte(2));
80
+ launching.write(attempt.and(valid).and(missing.not()));
81
+ const launch = launching.read();
82
+ pending.write(pending.read().and(attempt.not()));
83
+ rejected.write(select(attempt, valid.not(), rejected.read()));
84
+ age.write(select(launch, i32(0), age.read()));
85
+ total.write(select(launch, durationFrames(select(valid, c.durationScale, f32(1))), total.read()));
86
+ sourceLength.write(select(launch, sample.length(), sourceLength.read()));
87
+ pitchStep.write(select(launch, f64(c.pitchRatio).mul(sample.sourceSampleRate / sampleRate), pitchStep.read()));
88
+ active.write(active.read().or(launch));
89
+ ended.write(ended.read().and(launch.not()));
90
+ const nominal = f64(age.read()).mul(f64(sourceLength.read())).div(f64(total.read().max(1)));
91
+ const priorTail = current.read().add(pitchStep.read().mul(H));
92
+ spectralForEach(64, j => reference.write(j, readScaled(priorTail.add(f64(j).mul(H / 64).mul(pitchStep.read())))));
93
+ const compare = (candidate) => {
94
+ score.write(0);
95
+ spectralForEach(64, j => {
96
+ const value = readScaled(nominal.add(f64(candidate)).add(f64(j).mul(H / 64).mul(pitchStep.read())));
97
+ const difference = value.sub(reference.read(j));
98
+ score.write(score.read().add(difference.mul(difference)));
99
+ });
100
+ };
101
+ compare(i32(0));
102
+ bestScore.write(score.read());
103
+ bestOffset.write(0);
104
+ if (S > 0)
105
+ spectralForEach(S * 2, j => {
106
+ const distance = j.div(2).add(1), candidate = select(j.mod(2).eq(0), distance, distance.mul(-1));
107
+ compare(candidate);
108
+ const improve = score.read().lt(bestScore.read());
109
+ bestOffset.write(select(improve, candidate, bestOffset.read()));
110
+ bestScore.write(select(improve, score.read(), bestScore.read()));
111
+ });
112
+ const commit = due.and(active.read());
113
+ previous.write(select(commit, select(launch, f64(0), priorTail), previous.read()));
114
+ current.write(select(commit, select(launch, f64(0), nominal.add(f64(bestOffset.read()))), current.read()));
115
+ offset.write(select(commit, select(launch, i32(0), bestOffset.read()), offset.read()));
116
+ first.write(select(commit, launch, first.read()));
117
+ });
118
+ const exhausted = active.read().and(age.read().gte(total.read()));
119
+ ended.write(ended.read().or(exhausted));
120
+ active.write(active.read().and(exhausted.not()));
121
+ const playing = active.read();
122
+ const local = f64(clock.read()).mul(pitchStep.read()), blend = f64(clock.read()).div(H);
123
+ // Capture the first read because resident.read uses a shared transient.
124
+ wet.write(readScaled(previous.read().add(local)));
125
+ const incoming = readScaled(current.read().add(local));
126
+ const mixed = select(first.read(), incoming, wet.read().mul(f64(1).sub(blend)).add(incoming.mul(blend)));
127
+ const output = select(playing, f32(mixed.div(SCALE)), f32(0));
128
+ const position = f32(f64(age.read().min(total.read())).mul(f64(sourceLength.read())).div(f64(total.read().max(1))));
129
+ age.write(select(playing, age.read().add(1), age.read()));
130
+ clock.write(clock.read().add(1).mod(H));
131
+ previousGate.write(c.gate.and(c.reset.not()));
132
+ revision.write(sample.revision());
133
+ return { output, active: playing, position, ended: ended.read(), pending: pending.read(), rejected: rejected.read(), missing, selectedOffset: offset.read() };
134
+ },
135
+ };
136
+ });
@@ -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
+ });