@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,61 @@
1
+ import { type MidiInputHandle, type Node } from '@unworklet/core';
2
+ import { type VoicePolicyConfig } from './voice-policy.js';
3
+ export interface PitchGlideConfig {
4
+ sampleRate: number;
5
+ }
6
+ export interface PitchGlideControls {
7
+ /** MIDI-note semitones, clamped [-128,255]. */
8
+ target: Node<'f32'>;
9
+ /** [0,30] seconds; rounded to nearest sample and latched on target change. */
10
+ seconds: Node<'f32'>;
11
+ /** Level-sensitive: follow target immediately, consuming its current value. */
12
+ reset: Node<'bool'>;
13
+ /** Level-sensitive immediate target following, e.g. a nonlegato retrigger. */
14
+ snap: Node<'bool'>;
15
+ }
16
+ /** Constant-time linear glide in musical pitch. Call once per sample. */
17
+ export declare const pitchGlide: import("@unworklet/core").SubgraphDecl<[config: PitchGlideConfig], {
18
+ tick(c: PitchGlideControls): Node<"f32">;
19
+ }>;
20
+ export interface TuningControls {
21
+ note: Node<'f32'>;
22
+ transpose: Node<'f32'>;
23
+ cents: Node<'f32'>;
24
+ a4: Node<'f32'>;
25
+ }
26
+ /** Finite controls, bounded 12-TET conversion, one final frequency ceiling. */
27
+ export declare function tunedFrequency(c: TuningControls, maxFrequency: number): Node<"f32">;
28
+ export interface PerformancePolicyConfig extends VoicePolicyConfig {
29
+ /** Poly default/max 4; mono must be 1. */
30
+ capacity?: number;
31
+ /** Default/max 8; counts physically-held AND pedal-held identities. */
32
+ heldCapacity?: number;
33
+ }
34
+ /** Native MIDI expression + bounded sustain ledger over the unchanged allocator.
35
+ * MIDI remains quantum-boundary FIFO. All performance state is transient.
36
+ * See docs/performance.md for duplicate, overflow, CC and DSP-clear semantics.
37
+ */
38
+ export declare const performancePolicy: import("@unworklet/core").SubgraphDecl<[config: PerformancePolicyConfig], {
39
+ capacity: number;
40
+ heldCapacity: number;
41
+ reset: (when?: Node<"bool">) => void;
42
+ overflowed: () => Node<"bool">;
43
+ bindMidi(input: MidiInputHandle): void;
44
+ voices: {
45
+ read(): {
46
+ channelPressure: Node<"f32">;
47
+ polyPressure: Node<"f32">;
48
+ bend: Node<"f32">;
49
+ timbre: Node<"f32">;
50
+ sustain: Node<"bool">;
51
+ active: Node<"bool">;
52
+ gate: Node<"bool">;
53
+ note: Node<"i32">;
54
+ channel: Node<"i32">;
55
+ velocity: Node<"f32">;
56
+ };
57
+ takeRetrigger(): Node<"bool">;
58
+ takeReset(): Node<"bool">;
59
+ releaseFinished(done: Node<"bool">): void;
60
+ }[];
61
+ }>;
@@ -0,0 +1,241 @@
1
+ import { bool, defineSubgraph, f32, f64, i32, instantiate, select, state } from '@unworklet/core';
2
+ import { voicePolicy } from './voice-policy.js';
3
+ // Native scalar writes flush tiny values. Scale musical pitch state so every
4
+ // finite f32 target survives; this is storage encoding, not a new noise floor.
5
+ const PITCH_SCALE = 2 ** 128;
6
+ /** Constant-time linear glide in musical pitch. Call once per sample. */
7
+ export const pitchGlide = defineSubgraph((config) => {
8
+ if (!Number.isInteger(config.sampleRate) || config.sampleRate < 8000 || config.sampleRate > 192000) {
9
+ throw new RangeError('pitchGlide sampleRate must be an integer in [8000,192000]');
10
+ }
11
+ const live = state.bool(false).named('live');
12
+ const value = state.f64(0).named('valueScaled');
13
+ const target = state.f64(0).named('targetScaled');
14
+ const remaining = state.i32(0).named('remaining');
15
+ return { tick(c) {
16
+ const destination = f64(c.target.clamp(-128, 255)).mul(PITCH_SCALE);
17
+ const changed = destination.eq(target.read()).not();
18
+ const immediate = c.reset.or(c.snap).or(live.read().not());
19
+ const frames = i32(f64(c.seconds.clamp(0, 30)).mul(config.sampleRate).add(0.5).floor());
20
+ remaining.write(select(immediate, i32(0), select(changed, frames, remaining.read())));
21
+ const count = remaining.read();
22
+ const output = select(immediate.or(count.lte(1)), destination, value.read().add(destination.sub(value.read()).div(f64(count.max(1)))));
23
+ value.write(output);
24
+ target.write(destination);
25
+ remaining.write(count.sub(1).max(0));
26
+ live.write(true);
27
+ return f32(output.div(PITCH_SCALE));
28
+ } };
29
+ });
30
+ /** Finite controls, bounded 12-TET conversion, one final frequency ceiling. */
31
+ export function tunedFrequency(c, maxFrequency) {
32
+ if (!Number.isFinite(maxFrequency) || maxFrequency <= 0 || maxFrequency > 3.4028234663852886e38) {
33
+ throw new RangeError('maxFrequency must be positive and representable as finite f32');
34
+ }
35
+ const semitones = f64(c.note.clamp(-128, 255)).sub(69)
36
+ .add(f64(c.transpose.clamp(-48, 48))).add(f64(c.cents.clamp(-1200, 1200)).div(100));
37
+ // Both pow and exp use an approximate f32 bridge in core 0.4.1.
38
+ // |semitones| <= 257: exp(x/32), |x/32| < .465, degree 12,
39
+ // then five squarings. The bounded remainder stays below f32 rounding.
40
+ const x = semitones.mul(Math.LN2 / (12 * 32));
41
+ let factorial = 1;
42
+ const inverse = [1];
43
+ for (let n = 1; n <= 12; n++) {
44
+ factorial *= n;
45
+ inverse.push(1 / factorial);
46
+ }
47
+ let ratio = f64(inverse[12]);
48
+ for (let n = 11; n >= 0; n--)
49
+ ratio = ratio.mul(x).add(inverse[n]);
50
+ for (let n = 0; n < 5; n++)
51
+ ratio = ratio.mul(ratio);
52
+ return f32(f64(c.a4.clamp(220, 880)).mul(ratio).min(maxFrequency));
53
+ }
54
+ /** Native MIDI expression + bounded sustain ledger over the unchanged allocator.
55
+ * MIDI remains quantum-boundary FIFO. All performance state is transient.
56
+ * See docs/performance.md for duplicate, overflow, CC and DSP-clear semantics.
57
+ */
58
+ export const performancePolicy = defineSubgraph((config) => {
59
+ const size = config.capacity ?? (config.mode === 'mono' ? 1 : 4);
60
+ const heldSize = config.heldCapacity ?? 8;
61
+ if (!Number.isInteger(size) || size < 1 || size > 4)
62
+ throw new RangeError('performance capacity must be an integer in [1,4]');
63
+ if (!Number.isInteger(heldSize) || heldSize < size || heldSize > 8)
64
+ throw new RangeError('performance heldCapacity must be between capacity and 8');
65
+ const policy = instantiate(voicePolicy, { ...config, capacity: size, heldCapacity: heldSize }, { name: 'allocator' });
66
+ const ints = (name, length) => state.buffer.i32({ size: length }).expose({ name, snapshot: 'transient' });
67
+ const floats = (name, length) => state.buffer.f32({ size: length }).expose({ name, snapshot: 'transient' });
68
+ // Dense ages (1..heldSize), not an ever-increasing event counter.
69
+ const age = ints('heldAge', heldSize), key = ints('heldKey', heldSize), channel = ints('heldChannel', heldSize), down = ints('heldDown', heldSize);
70
+ const pedal = ints('pedal', 16), bend = floats('bend', 16), pressure = floats('channelPressure', 16), timbre = floats('timbre', 16);
71
+ const polyPressure = floats('polyPressure', size), trigger = ints('trigger', size), clear = ints('clear', size);
72
+ const oldKey = ints('oldKey', size), oldChannel = ints('oldChannel', size), scratch = ints('scratch', 3);
73
+ const previousIdentity = state.i32(-1).expose({ name: 'previousIdentity', snapshot: 'transient' });
74
+ const overflow = state.bool(false).expose({ name: 'overflow', snapshot: 'transient' });
75
+ const validChannel = (ch) => ch.gte(0).and(ch.lte(15));
76
+ const validKey = (note, ch) => validChannel(ch).and(note.gte(0)).and(note.lte(127));
77
+ const safeChannel = (ch) => ch.clamp(0, 15);
78
+ function newestIdentity() {
79
+ scratch.write(0, -1);
80
+ scratch.write(1, 0);
81
+ for (let n = 0; n < heldSize; n++) {
82
+ const newer = age.read(n).gt(scratch.read(1));
83
+ scratch.write(0, select(newer, i32(n), scratch.read(0)));
84
+ scratch.write(1, select(newer, age.read(n), scratch.read(1)));
85
+ }
86
+ }
87
+ function beforeChange() {
88
+ if (config.mode === 'mono') {
89
+ newestIdentity();
90
+ previousIdentity.write(scratch.read(0));
91
+ }
92
+ policy.voices.forEach((voice, n) => { const v = voice.read(); oldKey.write(n, v.note); oldChannel.write(n, v.channel); });
93
+ }
94
+ function afterChange() {
95
+ if (config.mode === 'mono')
96
+ newestIdentity();
97
+ const identityChanged = config.mode === 'mono' ? scratch.read(0).gte(0).and(scratch.read(0).eq(previousIdentity.read()).not()) : bool(false);
98
+ policy.voices.forEach((voice, n) => {
99
+ const v = voice.read(), retrigger = voice.takeRetrigger();
100
+ trigger.write(n, select(retrigger, i32(1), trigger.read(n)));
101
+ polyPressure.write(n, select(retrigger.or(identityChanged).or(v.active.not()).or(v.note.eq(oldKey.read(n)).not()).or(v.channel.eq(oldChannel.read(n)).not()), f32(0), polyPressure.read(n)));
102
+ });
103
+ }
104
+ function remove(index, when) {
105
+ const old = age.read(index);
106
+ for (let n = 0; n < heldSize; n++)
107
+ age.write(n, select(when.and(age.read(n).gt(old)), age.read(n).sub(1), age.read(n)));
108
+ age.write(index, select(when, i32(0), age.read(index)));
109
+ key.write(index, select(when, i32(0), key.read(index)));
110
+ channel.write(index, select(when, i32(0), channel.read(index)));
111
+ down.write(index, select(when, i32(0), down.read(index)));
112
+ }
113
+ function oldest(ch, note, physicallyDown, matchKey) {
114
+ scratch.write(0, 0);
115
+ scratch.write(1, heldSize + 1);
116
+ for (let n = 0; n < heldSize; n++) {
117
+ const candidate = age.read(n).gt(0).and(age.read(n).lt(scratch.read(1))).and(channel.read(n).eq(ch))
118
+ .and(physicallyDown ? down.read(n).gt(0) : down.read(n).eq(0)).and(matchKey ? key.read(n).eq(note) : bool(true));
119
+ scratch.write(0, select(candidate, i32(n), scratch.read(0)));
120
+ scratch.write(1, select(candidate, age.read(n), scratch.read(1)));
121
+ }
122
+ }
123
+ function noteOff(note, ch, when = bool(true)) {
124
+ oldest(ch, note, true, true);
125
+ const index = scratch.read(0), found = when.and(validKey(note, ch)).and(scratch.read(1).lte(heldSize));
126
+ const release = found.and(pedal.read(safeChannel(ch)).eq(0));
127
+ down.write(index, select(found, i32(0), down.read(index)));
128
+ policy.noteOff(select(release, note, i32(-1)), ch);
129
+ remove(index, release);
130
+ }
131
+ function flush(ch) {
132
+ for (let n = 0; n < heldSize; n++) {
133
+ oldest(ch, i32(0), false, false);
134
+ const index = scratch.read(0), found = validChannel(ch).and(scratch.read(1).lte(heldSize));
135
+ policy.noteOff(select(found, key.read(index), i32(-1)), ch);
136
+ remove(index, found);
137
+ }
138
+ }
139
+ function noteOn(note, ch, velocity) {
140
+ noteOff(note, ch, velocity.eq(0));
141
+ scratch.write(0, 0);
142
+ scratch.write(1, 0);
143
+ scratch.write(2, 0);
144
+ for (let n = heldSize - 1; n >= 0; n--) {
145
+ const free = age.read(n).eq(0);
146
+ scratch.write(0, select(free, i32(n), scratch.read(0)));
147
+ scratch.write(1, select(free, i32(1), scratch.read(1)));
148
+ scratch.write(2, scratch.read(2).add(select(free, i32(0), i32(1))));
149
+ }
150
+ const index = scratch.read(0), enabled = validKey(note, ch).and(velocity.gt(0)).and(velocity.lte(127));
151
+ const accept = enabled.and(scratch.read(1).gt(0));
152
+ overflow.write(overflow.read().or(enabled.and(accept.not())));
153
+ age.write(index, select(accept, scratch.read(2).add(1), age.read(index)));
154
+ key.write(index, select(accept, note, key.read(index)));
155
+ channel.write(index, select(accept, ch, channel.read(index)));
156
+ down.write(index, select(accept, i32(1), down.read(index)));
157
+ policy.noteOn(select(accept, note, i32(-1)), ch, velocity);
158
+ }
159
+ function allSoundOff(ch) {
160
+ policy.voices.forEach((voice, n) => {
161
+ const affected = voice.read().active.and(voice.read().channel.eq(ch));
162
+ clear.write(n, select(affected, i32(1), clear.read(n)));
163
+ trigger.write(n, select(affected, i32(0), trigger.read(n)));
164
+ });
165
+ policy.allSoundOff(ch);
166
+ for (let n = 0; n < heldSize; n++)
167
+ remove(i32(n), age.read(n).gt(0).and(channel.read(n).eq(ch)));
168
+ const safe = safeChannel(ch);
169
+ pedal.write(safe, select(validChannel(ch), i32(0), pedal.read(safe)));
170
+ }
171
+ function control(ch, controller, value) {
172
+ const safe = safeChannel(ch), valid = validChannel(ch).and(value.gte(0)).and(value.lte(127));
173
+ const sustain = valid.and(controller.eq(64)), resetControllers = valid.and(controller.eq(121));
174
+ const notesOff = valid.and(controller.eq(123)), soundOff = valid.and(controller.eq(120));
175
+ pedal.write(safe, select(resetControllers, i32(0), select(sustain, select(value.gte(64), i32(1), i32(0)), pedal.read(safe))));
176
+ bend.write(safe, select(resetControllers, f32(0), bend.read(safe)));
177
+ pressure.write(safe, select(resetControllers, f32(0), pressure.read(safe)));
178
+ timbre.write(safe, select(resetControllers, f32(0), select(valid.and(controller.eq(74)), f32(value).div(127), timbre.read(safe))));
179
+ policy.voices.forEach((voice, n) => polyPressure.write(n, select(resetControllers.and(voice.read().channel.eq(ch)), f32(0), polyPressure.read(n))));
180
+ for (let n = 0; n < heldSize; n++)
181
+ down.write(n, select(notesOff.and(channel.read(n).eq(ch)), i32(0), down.read(n)));
182
+ const release = resetControllers.or(sustain.and(value.lt(64))).or(notesOff.and(pedal.read(safe).eq(0)));
183
+ flush(select(release, ch, i32(-1)));
184
+ allSoundOff(select(soundOff, ch, i32(-1)));
185
+ }
186
+ function reset(when = bool(true)) {
187
+ policy.reset(when);
188
+ for (let n = 0; n < heldSize; n++) {
189
+ age.write(n, select(when, i32(0), age.read(n)));
190
+ key.write(n, select(when, i32(0), key.read(n)));
191
+ channel.write(n, select(when, i32(0), channel.read(n)));
192
+ down.write(n, select(when, i32(0), down.read(n)));
193
+ }
194
+ for (let ch = 0; ch < 16; ch++) {
195
+ pedal.write(ch, select(when, i32(0), pedal.read(ch)));
196
+ bend.write(ch, select(when, f32(0), bend.read(ch)));
197
+ pressure.write(ch, select(when, f32(0), pressure.read(ch)));
198
+ timbre.write(ch, select(when, f32(0), timbre.read(ch)));
199
+ }
200
+ for (let n = 0; n < size; n++) {
201
+ polyPressure.write(n, select(when, f32(0), polyPressure.read(n)));
202
+ trigger.write(n, select(when, i32(0), trigger.read(n)));
203
+ clear.write(n, select(when, i32(1), clear.read(n)));
204
+ }
205
+ overflow.write(select(when, false, overflow.read()));
206
+ }
207
+ return {
208
+ capacity: size, heldCapacity: heldSize, reset,
209
+ overflowed: () => overflow.read().or(policy.overflowed()),
210
+ bindMidi(input) {
211
+ input.onEvent('noteOn', e => { beforeChange(); noteOn(e.note, e.channel, e.velocity); afterChange(); });
212
+ input.onEvent('noteOff', e => { beforeChange(); noteOff(e.note, e.channel); afterChange(); });
213
+ input.onEvent('cc', e => { beforeChange(); control(e.channel, e.controller, e.value); afterChange(); });
214
+ input.onEvent('pitchBend', e => {
215
+ const ch = safeChannel(e.channel), value = f32(e.value).sub(8192);
216
+ bend.write(ch, select(validChannel(e.channel).and(e.value.gte(0)).and(e.value.lte(16383)), value.div(select(e.value.lt(8192), f32(8192), f32(8191))), bend.read(ch)));
217
+ });
218
+ input.onEvent('channelPressure', e => {
219
+ const ch = safeChannel(e.channel);
220
+ pressure.write(ch, select(validChannel(e.channel).and(e.pressure.gte(0)).and(e.pressure.lte(127)), f32(e.pressure).div(127), pressure.read(ch)));
221
+ });
222
+ input.onEvent('aftertouch', e => {
223
+ policy.voices.forEach((voice, n) => {
224
+ const v = voice.read(), match = v.active.and(v.channel.eq(e.channel)).and(v.note.eq(e.note)).and(e.pressure.gte(0)).and(e.pressure.lte(127));
225
+ polyPressure.write(n, select(match, f32(e.pressure).div(127), polyPressure.read(n)));
226
+ });
227
+ });
228
+ },
229
+ voices: policy.voices.map((voice, n) => ({
230
+ read() {
231
+ const v = voice.read(), ch = safeChannel(v.channel);
232
+ return { ...v, channelPressure: select(v.active, pressure.read(ch), f32(0)),
233
+ polyPressure: select(v.active, polyPressure.read(n), f32(0)), bend: select(v.active, bend.read(ch), f32(0)),
234
+ timbre: select(v.active, timbre.read(ch), f32(0)), sustain: v.active.and(pedal.read(ch).gt(0)) };
235
+ },
236
+ takeRetrigger() { const value = trigger.read(n).gt(0); trigger.write(n, 0); return value; },
237
+ takeReset() { const value = clear.read(n).gt(0); clear.write(n, 0); return value; },
238
+ releaseFinished(done) { voice.releaseFinished(done); },
239
+ })),
240
+ };
241
+ });
@@ -0,0 +1,26 @@
1
+ import { type Node } from '@unworklet/core';
2
+ export interface PitchQuantizerConfig {
3
+ /** 1..128 finite offsets in [0,12), in semitones. Copied, f32-rounded, sorted. */
4
+ pitchClasses: readonly number[];
5
+ /** Fixed Schmitt margin in semitones, f32-rounded in [0,12]. Default 0. */
6
+ hysteresis?: number;
7
+ }
8
+ export interface PitchQuantizerOutput {
9
+ /** Selected octave*12 + pitch class, rounded to f32. */
10
+ pitch: Node<'f32'>;
11
+ /** Index into the sorted, f32-rounded pitchClasses. */
12
+ degree: Node<'i32'>;
13
+ /** Octave multiplier; pitch=12*octave+pitchClasses[degree]. Not an octave label. */
14
+ octave: Node<'i32'>;
15
+ }
16
+ /**
17
+ * Audio-rate, octave-periodic nearest pitch, not audio pitch detection or MIDI
18
+ * output. Call once per sample. NaN becomes 0; other inputs clamp to +/-16384.
19
+ * f64 computational distance ties choose the lower pitch. A positive margin
20
+ * retains the previous degree inside its expanded adjacent-midpoint interval,
21
+ * including both endpoints. Reset (level-sensitive) and first call snap to
22
+ * nearest immediately. Zero margin always uses nearest, including tie policy.
23
+ */
24
+ export declare const pitchQuantizer: import("@unworklet/core").SubgraphDecl<[config: PitchQuantizerConfig], {
25
+ tick(input: Node<"f32">, reset: Node<"bool">): PitchQuantizerOutput;
26
+ }>;
@@ -0,0 +1,88 @@
1
+ import { bool, defineSubgraph, f32, f64, i32, select, state } from '@unworklet/core';
2
+ // Native 0.4.1 scalar state flushes tiny values. f32 controls/offsets scaled
3
+ // by this exact power of two remain well above that floor, including 2^-149.
4
+ const SCALE = 2 ** 128;
5
+ /**
6
+ * Audio-rate, octave-periodic nearest pitch, not audio pitch detection or MIDI
7
+ * output. Call once per sample. NaN becomes 0; other inputs clamp to +/-16384.
8
+ * f64 computational distance ties choose the lower pitch. A positive margin
9
+ * retains the previous degree inside its expanded adjacent-midpoint interval,
10
+ * including both endpoints. Reset (level-sensitive) and first call snap to
11
+ * nearest immediately. Zero margin always uses nearest, including tie policy.
12
+ */
13
+ export const pitchQuantizer = defineSubgraph((config) => {
14
+ if (!Array.isArray(config.pitchClasses) || config.pitchClasses.length < 1 || config.pitchClasses.length > 128) {
15
+ throw new RangeError('pitchClasses requires 1..128 offsets');
16
+ }
17
+ // Array.from visits sparse holes too; every supplied slot must be valid.
18
+ const degrees = Array.from(config.pitchClasses, value => {
19
+ if (!Number.isFinite(value) || value < 0 || value >= 12 || Math.fround(value) >= 12) {
20
+ throw new RangeError('pitch class must be finite in [0,12), including after f32 rounding');
21
+ }
22
+ return Math.fround(value) || 0;
23
+ }).sort((a, b) => a - b);
24
+ if (degrees.some((value, n) => n > 0 && value === degrees[n - 1])) {
25
+ throw new RangeError('pitch classes must be distinct after f32 rounding');
26
+ }
27
+ const requestedMargin = config.hysteresis ?? 0;
28
+ if (!Number.isFinite(requestedMargin) || requestedMargin < 0 || requestedMargin > 12) {
29
+ throw new RangeError('hysteresis must be finite in [0,12] semitones');
30
+ }
31
+ const margin = Math.fround(requestedMargin);
32
+ const degree = state.i32(0).named('degree');
33
+ const octave = state.i32(0).named('octave');
34
+ const initialized = state.bool(false).named('initialized');
35
+ const nearestDegree = state.i32(0).named('nearestDegree');
36
+ const nearestOctave = state.i32(0).named('nearestOctave');
37
+ const nearestPitch = state.f64(0).named('nearestPitchScaled');
38
+ const nearestDistance = state.f64(0).named('nearestDistanceScaled');
39
+ const candidateOctave = state.i32(0).named('candidateOctave');
40
+ const candidatePitch = state.f64(0).named('candidatePitchScaled');
41
+ const candidateDistance = state.f64(0).named('candidateDistanceScaled');
42
+ const takeCandidate = state.bool(false).named('takeCandidate');
43
+ const update = state.bool(false).named('update');
44
+ return { tick(input, reset) {
45
+ // Unlike min/max alone, the explicit NaN branch gives a portable policy.
46
+ const x = f64(select(input.eq(input), input, f32(0)).clamp(-16384, 16384));
47
+ nearestDistance.write(f64(32768 * SCALE));
48
+ nearestPitch.write(f64(32768 * SCALE));
49
+ for (let n = 0; n < degrees.length; n++) {
50
+ // Compare both bracketing octaves by their actual f64 distances.
51
+ // A midpoint shortcut is not equivalent near zero: e.g. +/-6 have
52
+ // equal rounded distances from an f32 subnormal, so lower must win.
53
+ candidateOctave.write(i32(x.sub(degrees[n]).div(12).floor()));
54
+ for (let adjacent = 0; adjacent < 2; adjacent++) {
55
+ if (adjacent > 0)
56
+ candidateOctave.write(candidateOctave.read().add(1));
57
+ candidatePitch.write(f64(candidateOctave.read()).mul(12).add(degrees[n]).mul(SCALE));
58
+ candidateDistance.write(x.mul(SCALE).sub(candidatePitch.read()).abs());
59
+ takeCandidate.write(candidateDistance.read().lt(nearestDistance.read()).or(candidateDistance.read().eq(nearestDistance.read()).and(candidatePitch.read().lt(nearestPitch.read()))));
60
+ nearestDegree.write(select(takeCandidate.read(), i32(n), nearestDegree.read()));
61
+ nearestOctave.write(select(takeCandidate.read(), candidateOctave.read(), nearestOctave.read()));
62
+ nearestPitch.write(select(takeCandidate.read(), candidatePitch.read(), nearestPitch.read()));
63
+ nearestDistance.write(select(takeCandidate.read(), candidateDistance.read(), nearestDistance.read()));
64
+ }
65
+ }
66
+ if (margin === 0) {
67
+ update.write(bool(true));
68
+ }
69
+ else {
70
+ let lower = f64(0), upper = f64(0);
71
+ for (let n = 0; n < degrees.length; n++) {
72
+ const previous = n === 0 ? degrees[degrees.length - 1] - 12 : degrees[n - 1];
73
+ const next = n === degrees.length - 1 ? degrees[0] + 12 : degrees[n + 1];
74
+ lower = select(degree.read().eq(n), f64((previous + degrees[n]) / 2 - margin), lower);
75
+ upper = select(degree.read().eq(n), f64((degrees[n] + next) / 2 + margin), upper);
76
+ }
77
+ const base = f64(octave.read()).mul(12);
78
+ update.write(reset.or(initialized.read().not()).or(x.lt(base.add(lower))).or(x.gt(base.add(upper))));
79
+ }
80
+ degree.write(select(update.read(), nearestDegree.read(), degree.read()));
81
+ octave.write(select(update.read(), nearestOctave.read(), octave.read()));
82
+ initialized.write(bool(true));
83
+ let selected = f64(degrees[0]);
84
+ for (let n = 1; n < degrees.length; n++)
85
+ selected = select(degree.read().eq(n), f64(degrees[n]), selected);
86
+ return { pitch: f32(f64(octave.read()).mul(12).add(selected)), degree: degree.read(), octave: octave.read() };
87
+ } };
88
+ });
@@ -0,0 +1,22 @@
1
+ import { type EveryNSamples, type MessageGraphPayload, type Node } from '@unworklet/core';
2
+ import { type PreparedConvolutionConfig, type PreparedConvolutionPacket } from './convolution-spectrum.js';
3
+ export { prepareConvolutionSpectrum, type PreparedConvolutionConfig, type PreparedConvolutionPacket } from './convolution-spectrum.js';
4
+ /** Fixed B128/P64 prepared-spectrum mono convolution candidate.
5
+ * Correctness/gain bounds require an unchanged packet from host preparation.
6
+ * Caller owns native CAPACITY_16 ingress with payloadCapacity 131072 bytes.
7
+ * Copy/scan runs in native dispatch, so preload with audible playback gated off.
8
+ */
9
+ export declare const preparedConvolution: import("@unworklet/core").SubgraphDecl<[config: PreparedConvolutionConfig], {
10
+ /** Module-specific native event payload; this is not a host loader/ack. */
11
+ load(packet: MessageGraphPayload<PreparedConvolutionPacket>): void;
12
+ /** Once per sample, inside stride-1 forSample with its native scheduler.
13
+ * Finite normalized input[-1,1]. Reset discards its sample and keeps the IR.
14
+ * Reload clears prior tail but accepts the next sample after its handler.
15
+ */
16
+ tick(input: Node<"f32">, reset: Node<"bool">, everyNSamples: EveryNSamples): {
17
+ output: Node<"f32">;
18
+ loaded: Node<"bool">;
19
+ rejected: Node<"bool">;
20
+ revision: Node<"i32">;
21
+ };
22
+ }>;
@@ -0,0 +1,99 @@
1
+ import { defineSubgraph, f32, f64, i32, instantiate, select, state } from '@unworklet/core';
2
+ import { 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 coefficients = state.buffer.f32({ size: VALUES }).expose({ name: 'coefficients', snapshot: 'persistent' });
13
+ const history = state.buffer.f64({ size: B }).expose({ name: 'historyScaled', snapshot: 'persistent' });
14
+ const previous = state.buffer.f64({ size: VALUES }).expose({ name: 'previousSpectraScaled', snapshot: 'persistent' });
15
+ const mixedReal = state.buffer.f64({ size: N }).expose({ name: 'mixedRealScaled', snapshot: 'transient' });
16
+ const mixedImag = state.buffer.f64({ size: N }).expose({ name: 'mixedImagScaled', snapshot: 'transient' });
17
+ const overlap = state.buffer.f64({ size: N }).expose({ name: 'overlapScaled', snapshot: 'persistent' });
18
+ const cursor = state.i32(0).named('cursor'), spectrumCursor = state.i32(0).named('spectrumCursor');
19
+ const validSamples = state.i32(0).named('validSamples'), validBlocks = state.i32(0).named('validBlocks');
20
+ const pendingReset = state.bool(true).named('pendingReset');
21
+ const loaded = state.bool(false).named('loaded'), rejected = state.bool(false).named('rejected');
22
+ const revision = state.i32(0).named('revision');
23
+ const malformed = state.bool(false).expose({ name: 'malformed', snapshot: 'transient' });
24
+ const sumReal = state.f64(0).expose({ name: 'sumRealScaled', snapshot: 'transient' });
25
+ const sumImag = state.f64(0).expose({ name: 'sumImagScaled', snapshot: 'transient' });
26
+ const fft = instantiate(spectralFft, { size: N }, { name: 'fft' });
27
+ const scale = 2 ** 128;
28
+ return {
29
+ /** Module-specific native event payload; this is not a host loader/ack. */
30
+ load(packet) {
31
+ coefficients.copyFrom(packet.spectrum);
32
+ const length = packet.spectrum.length;
33
+ const header = packet.formatVersion.eq(1).and(packet.blockSize.eq(B)).and(packet.partitions.eq(P));
34
+ const complete = header.and(packet.impulseFrames.gte(1)).and(packet.impulseFrames.lte(B * P)).and(packet.impulseFrames.floor().eq(packet.impulseFrames))
35
+ .and(packet.declaredValues.eq(VALUES)).and(length.eq(VALUES));
36
+ const unload = header.and(packet.impulseFrames.eq(0)).and(packet.declaredValues.eq(0)).and(length.eq(0));
37
+ malformed.write(false);
38
+ spectralForEach(P, p => spectralForEach(N, k => {
39
+ const index = p.mul(N).add(k).mul(2), re = coefficients.read(index), im = coefficients.read(index.add(1));
40
+ malformed.write(malformed.read().or(re.eq(re).not()).or(im.eq(im).not()).or(re.abs().gt(4)).or(im.abs().gt(4)));
41
+ }));
42
+ loaded.write(complete.and(malformed.read().not()));
43
+ rejected.write(loaded.read().not().and(unload.not()));
44
+ revision.write(revision.read().add(1));
45
+ validSamples.write(0);
46
+ validBlocks.write(0);
47
+ pendingReset.write(true);
48
+ },
49
+ /** Once per sample, inside stride-1 forSample with its native scheduler.
50
+ * Finite normalized input[-1,1]. Reset discards its sample and keeps the IR.
51
+ * Reload clears prior tail but accepts the next sample after its handler.
52
+ */
53
+ tick(input, reset, everyNSamples) {
54
+ const invalidate = reset.or(loaded.read().not());
55
+ validSamples.write(select(invalidate, i32(0), validSamples.read()));
56
+ validBlocks.write(select(invalidate, i32(0), validBlocks.read()));
57
+ pendingReset.write(pendingReset.read().or(invalidate));
58
+ everyNSamples(B, () => {
59
+ const frame = fft.transform(n => select(n.lt(B).and(validSamples.read().gte(i32(B).sub(n))), history.read(cursor.read().add(n).mod(B)), f64(0)), () => f64(0), false);
60
+ spectralForEach(N, k => {
61
+ const index = spectrumCursor.read().mul(N).add(k).mul(2);
62
+ previous.write(index, frame.real(k));
63
+ previous.write(index.add(1), frame.imag(k));
64
+ });
65
+ spectralForEach(N, k => {
66
+ sumReal.write(0);
67
+ sumImag.write(0);
68
+ spectralForEach(P, p => {
69
+ const x = spectrumCursor.read().add(P).sub(p).mod(P).mul(N).add(k).mul(2);
70
+ const h = p.mul(N).add(k).mul(2);
71
+ const xr = select(validBlocks.read().gte(p), previous.read(x), f64(0));
72
+ const xi = select(validBlocks.read().gte(p), previous.read(x.add(1)), f64(0));
73
+ // Mask malformed/unloaded coefficient bytes before multiplication.
74
+ const hr = f64(select(loaded.read(), coefficients.read(h), f32(0)));
75
+ const hi = f64(select(loaded.read(), coefficients.read(h.add(1)), f32(0)));
76
+ sumReal.write(sumReal.read().add(xr.mul(hr).sub(xi.mul(hi))));
77
+ sumImag.write(sumImag.read().add(xr.mul(hi).add(xi.mul(hr))));
78
+ });
79
+ mixedReal.write(k, sumReal.read());
80
+ mixedImag.write(k, sumImag.read());
81
+ });
82
+ const result = fft.transform(n => mixedReal.read(n), n => mixedImag.read(n), true);
83
+ spectralForEach(N, n => {
84
+ const index = cursor.read().add(n).mod(N);
85
+ overlap.write(index, select(pendingReset.read(), f64(0), overlap.read(index)).add(result.real(n)));
86
+ });
87
+ spectrumCursor.write(spectrumCursor.read().add(1).mod(P));
88
+ validBlocks.write(validBlocks.read().add(1).min(P - 1));
89
+ pendingReset.write(false);
90
+ });
91
+ const output = select(invalidate.or(pendingReset.read()), f32(0), f32(overlap.read(cursor.read()).div(scale)));
92
+ overlap.write(cursor.read(), f64(0));
93
+ history.write(cursor.read().mod(B), f64(select(invalidate, f32(0), input)).mul(scale));
94
+ cursor.write(cursor.read().add(1).mod(N));
95
+ validSamples.write(select(invalidate, i32(0), validSamples.read().add(1).min(B)));
96
+ return { output, loaded: loaded.read(), rejected: rejected.read(), revision: revision.read() };
97
+ },
98
+ };
99
+ });
@@ -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
+ });