@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,45 @@
1
+ import { type Node } from '@unworklet/core';
2
+ export interface StereoDelayConfig {
3
+ sampleRate: number;
4
+ /** Fixed per-head capacity in seconds. Default 2; at most 8. */
5
+ maxDelaySeconds?: number;
6
+ }
7
+ export interface DelayMixControls {
8
+ /** Signed feedback, clamped to [-0.95, 0.95]. */
9
+ feedback: Node<'f32'>;
10
+ /** Linear dry/wet, clamped to [0, 1]. */
11
+ mix: Node<'f32'>;
12
+ bypass: Node<'bool'>;
13
+ reset: Node<'bool'>;
14
+ }
15
+ export interface PingPongControls extends DelayMixControls {
16
+ /** Both channel delays. Invalid time requests emit dry and reject new input. */
17
+ timeSeconds: Node<'f32'>;
18
+ }
19
+ /** Stereo cross-feedback delay. Both heads are read before either current write. */
20
+ export declare const pingPongDelay: import("@unworklet/core").SubgraphDecl<[config: StereoDelayConfig], {
21
+ tick(inputLeft: Node<"f32">, inputRight: Node<"f32">, c: PingPongControls): {
22
+ left: Node<"f32">;
23
+ right: Node<"f32">;
24
+ timingRejected: Node<"bool">;
25
+ };
26
+ }>;
27
+ export interface DelayTap {
28
+ /** Fixed delay, within the declared capacity. */
29
+ delaySeconds: number;
30
+ gainLeft: number;
31
+ gainRight: number;
32
+ }
33
+ export interface MultiTapDelayConfig extends StereoDelayConfig {
34
+ /** One to eight independent fixed-capacity heads. */
35
+ taps: readonly DelayTap[];
36
+ }
37
+ /** Mono-in, stereo-out tapped delay. Gains share a construction-time divisor
38
+ * max(1, sum(abs(left)), sum(abs(right))), preserving signs and stereo ratios.
39
+ */
40
+ export declare const multiTapDelay: import("@unworklet/core").SubgraphDecl<[config: MultiTapDelayConfig], {
41
+ tick(input: Node<"f32">, c: DelayMixControls): {
42
+ left: Node<"f32">;
43
+ right: Node<"f32">;
44
+ };
45
+ }>;
@@ -0,0 +1,70 @@
1
+ import { defineSubgraph, f32, f64, instantiate, select, state } from '@unworklet/core';
2
+ import { delayReadhead } from './delay-readhead.js';
3
+ function validate(config) {
4
+ const { sampleRate, maxDelaySeconds = 2 } = config;
5
+ if (!Number.isInteger(sampleRate) || sampleRate < 8000 || sampleRate > 192000 ||
6
+ !Number.isFinite(maxDelaySeconds) || maxDelaySeconds < 1 / sampleRate || maxDelaySeconds > 8) {
7
+ throw new RangeError('stereo delay requires integer sample rate [8000,192000] and capacity [one sample,8 seconds]');
8
+ }
9
+ return { sampleRate, maxDelaySeconds };
10
+ }
11
+ /** Stereo cross-feedback delay. Both heads are read before either current write. */
12
+ export const pingPongDelay = defineSubgraph((config) => {
13
+ const validated = validate(config), minimum = Math.fround(1 / config.sampleRate);
14
+ const left = instantiate(delayReadhead, validated, { name: 'left' });
15
+ const right = instantiate(delayReadhead, validated, { name: 'right' });
16
+ const acceptedTime = state.f32(minimum).named('acceptedTime');
17
+ const wetLeft = state.f32(0).expose({ name: 'wetLeft', snapshot: 'transient' });
18
+ const wetRight = state.f32(0).expose({ name: 'wetRight', snapshot: 'transient' });
19
+ return {
20
+ tick(inputLeft, inputRight, c) {
21
+ const valid = c.timeSeconds.gte(minimum).and(c.timeSeconds.lte(Math.fround(validated.maxDelaySeconds)));
22
+ acceptedTime.write(select(valid, c.timeSeconds, select(c.reset, f32(minimum), acceptedTime.read())));
23
+ const l = left.read(acceptedTime.read(), c.reset), r = right.read(acceptedTime.read(), c.reset);
24
+ wetLeft.write(l.output);
25
+ wetRight.write(r.output);
26
+ const dryOnly = c.bypass.or(valid.not()), gain = c.feedback.clamp(-0.95, 0.95);
27
+ l.write(select(dryOnly, f32(0), inputLeft).add(wetRight.read().mul(gain)));
28
+ r.write(select(dryOnly, f32(0), inputRight).add(wetLeft.read().mul(gain)));
29
+ const mix = c.mix.clamp(0, 1);
30
+ return {
31
+ left: select(dryOnly, inputLeft, inputLeft.mul(f32(1).sub(mix)).add(wetLeft.read().mul(mix))),
32
+ right: select(dryOnly, inputRight, inputRight.mul(f32(1).sub(mix)).add(wetRight.read().mul(mix))),
33
+ timingRejected: valid.not(),
34
+ };
35
+ },
36
+ };
37
+ });
38
+ /** Mono-in, stereo-out tapped delay. Gains share a construction-time divisor
39
+ * max(1, sum(abs(left)), sum(abs(right))), preserving signs and stereo ratios.
40
+ */
41
+ export const multiTapDelay = defineSubgraph((config) => {
42
+ const validated = validate(config);
43
+ if (!Array.isArray(config.taps) || config.taps.length < 1 || config.taps.length > 8 || config.taps.some(t => !Number.isFinite(t.delaySeconds) || t.delaySeconds < 1 / config.sampleRate || t.delaySeconds > validated.maxDelaySeconds ||
44
+ !Number.isFinite(t.gainLeft) || !Number.isFinite(t.gainRight) || Math.abs(t.gainLeft) > 1e6 || Math.abs(t.gainRight) > 1e6)) {
45
+ throw new RangeError('multiTapDelay needs 1–8 finite taps within capacity and gains within [-1e6,1e6]');
46
+ }
47
+ const divisor = Math.max(1, config.taps.reduce((s, t) => s + Math.abs(t.gainLeft), 0), config.taps.reduce((s, t) => s + Math.abs(t.gainRight), 0));
48
+ const taps = config.taps.map((t, i) => ({ ...t, gainLeft: t.gainLeft / divisor, gainRight: t.gainRight / divisor,
49
+ delay: instantiate(delayReadhead, validated, { name: `tap${i}` }) }));
50
+ const wetLeft = state.f64(0).expose({ name: 'wetLeft', snapshot: 'transient' });
51
+ const wetRight = state.f64(0).expose({ name: 'wetRight', snapshot: 'transient' });
52
+ const feed = state.f32(0).expose({ name: 'feed', snapshot: 'transient' });
53
+ return {
54
+ tick(input, c) {
55
+ const heads = taps.map(t => t.delay.read(f32(t.delaySeconds), c.reset));
56
+ // All output reads precede all writes. Each delay instance is consumed once.
57
+ wetLeft.write(heads.reduce((sum, h, i) => sum.add(f64(h.output).mul(taps[i].gainLeft)), f64(0)));
58
+ wetRight.write(heads.reduce((sum, h, i) => sum.add(f64(h.output).mul(taps[i].gainRight)), f64(0)));
59
+ const left = wetLeft.read(), right = wetRight.read();
60
+ feed.write(f32(f64(select(c.bypass, f32(0), input)).add(left.add(right).mul(0.5).mul(f64(c.feedback).clamp(-0.95, 0.95)))));
61
+ for (const h of heads)
62
+ h.write(feed.read());
63
+ const mix = f64(c.mix).clamp(0, 1), dry = f64(input).mul(f64(1).sub(mix));
64
+ return {
65
+ left: select(c.bypass, input, f32(dry.add(left.mul(mix)))),
66
+ right: select(c.bypass, input, f32(dry.add(right.mul(mix)))),
67
+ };
68
+ },
69
+ };
70
+ });
@@ -0,0 +1,26 @@
1
+ import { type Node } from '@unworklet/core';
2
+ export interface VirtualAnalogConfig {
3
+ sampleRate: number;
4
+ waveform: 'pulse' | 'triangle';
5
+ /** Initial/reset phase in cycles, [0,1). */
6
+ phaseCycles?: number;
7
+ }
8
+ export interface VirtualAnalogControls {
9
+ /** Hz, clipped to [0,.45*sampleRate]; NaN becomes zero. */
10
+ frequencyHz: Node<'f32'>;
11
+ /** Pulse high fraction [0,1], NaN becomes .5; ignored by triangle. */
12
+ duty: Node<'f32'>;
13
+ /** Level-sensitive: emit initial phase, then advance, on every true sample. */
14
+ reset: Node<'bool'>;
15
+ }
16
+ /** Alias-reduced pulse and triangle. Polynomial smoothing is not a brickwall filter. */
17
+ export declare const virtualAnalogSource: import("@unworklet/core").SubgraphDecl<[config: VirtualAnalogConfig], {
18
+ tick(c: VirtualAnalogControls): Node<"f32">;
19
+ }>;
20
+ export interface SeededNoiseConfig {
21
+ seed?: number;
22
+ }
23
+ /** Park–Miller 48271 white-noise candidate; deterministic, not cryptographic. */
24
+ export declare const seededNoise: import("@unworklet/core").SubgraphDecl<[config?: SeededNoiseConfig | undefined], {
25
+ tick(reset: Node<"bool">): Node<"f32">;
26
+ }>;
@@ -0,0 +1,66 @@
1
+ import { defineSubgraph, f32, f64, i32, select, state } from '@unworklet/core';
2
+ function bounded(value, low, high, fallback) {
3
+ return select(value.eq(value), f64(value), f64(fallback)).clamp(low, high);
4
+ }
5
+ /** Alias-reduced pulse and triangle. Polynomial smoothing is not a brickwall filter. */
6
+ export const virtualAnalogSource = defineSubgraph((config) => {
7
+ const { sampleRate, waveform, phaseCycles = 0 } = config;
8
+ if (!Number.isFinite(sampleRate) || sampleRate < 8000 || sampleRate > 192000)
9
+ throw new RangeError('VA sampleRate must be in [8000,192000] Hz');
10
+ if (waveform !== 'pulse' && waveform !== 'triangle')
11
+ throw new RangeError('unknown VA waveform');
12
+ if (!Number.isFinite(phaseCycles) || phaseCycles < 0 || phaseCycles >= 1)
13
+ throw new RangeError('phaseCycles must be in [0,1)');
14
+ // Exact power-of-two scaling avoids the native |state|<1e-30 flush,
15
+ // including f64 phaseCycles=Number.MIN_VALUE and smallest f32 Hz.
16
+ // Phase is wrapped below1 before scaling, with >16x finite headroom.
17
+ const scale = 2 ** 1020;
18
+ const phase = state.f64(phaseCycles * scale).named('scaledPhase');
19
+ return { tick(c) {
20
+ const step = bounded(c.frequencyHz, 0, .45 * sampleRate, 0).div(sampleRate);
21
+ const p = select(c.reset, f64(phaseCycles), phase.read().div(scale));
22
+ // Both select branches evaluate: the zero-Hz denominator must remain safe.
23
+ const width = select(step.gt(0), step, f64(1));
24
+ let output;
25
+ if (waveform === 'pulse') {
26
+ const duty = bounded(c.duty, 0, 1, .5);
27
+ // Rising-step residual for the unit-area triangular kernel [-1,1].
28
+ const blep = (q) => {
29
+ const after = f64(1).sub(q.div(width)).max(0);
30
+ const before = f64(1).sub(f64(1).sub(q).div(width)).max(0);
31
+ return before.mul(before).sub(after.mul(after));
32
+ };
33
+ const relative = p.sub(duty), falling = relative.sub(relative.floor());
34
+ const raw = select(p.lt(duty), f64(1), f64(-1));
35
+ output = raw.add(select(step.gt(0), blep(p).sub(blep(falling)), f64(0)));
36
+ }
37
+ else {
38
+ const rampResidual = (distance) => {
39
+ const t = f64(1).sub(distance.div(width)).max(0);
40
+ return t.mul(t).mul(t).div(6);
41
+ };
42
+ const raw = f64(1).sub(p.sub(.5).abs().mul(4));
43
+ // Slope jumps +8 at phase0 and -8 at phase.5. Support <.5 cycle,
44
+ // so only phase0 needs the periodic image at phase1.
45
+ const correction = rampResidual(p).add(rampResidual(f64(1).sub(p)))
46
+ .sub(rampResidual(p.sub(.5).abs())).mul(step).mul(8);
47
+ output = raw.add(select(step.gt(0), correction, f64(0)));
48
+ }
49
+ phase.write(p.add(step).frac().mul(scale));
50
+ return f32(output);
51
+ } };
52
+ });
53
+ /** Park–Miller 48271 white-noise candidate; deterministic, not cryptographic. */
54
+ export const seededNoise = defineSubgraph((config = {}) => {
55
+ const { seed = 1 } = config;
56
+ if (!Number.isInteger(seed) || seed < 1 || seed > 2147483646)
57
+ throw new RangeError('noise seed must be an integer in [1,2147483646]');
58
+ const random = state.i32(seed).named('random');
59
+ return { tick(reset) {
60
+ const current = select(reset, i32(seed), random.read());
61
+ // Exact integer product in f64; i32 multiplication would overflow.
62
+ const next = i32(f64(current).mul(48271).mod(2147483647));
63
+ random.write(next);
64
+ return f32(f64(next).mul(2 / 2147483647).sub(1));
65
+ } };
66
+ });
@@ -0,0 +1,37 @@
1
+ import { type MidiInputHandle, type Node } from '@unworklet/core';
2
+ /** Fixed construction capacities, not product-wide polyphony limits. */
3
+ export interface VoicePolicyConfig {
4
+ mode: 'mono' | 'poly';
5
+ /** Poly default 16; mono must use one slot. Maximum 32 limits graph expansion. */
6
+ capacity?: number;
7
+ /** Held note-on identities (including stolen notes). Default 128, maximum 256. */
8
+ heldCapacity?: number;
9
+ /** Mono only: preserve gate without a retrigger when another held note takes priority. */
10
+ legato?: boolean;
11
+ }
12
+ /** Voice allocation only. MIDI transport, graph storage, and node control remain unworklet's. */
13
+ export declare const voicePolicy: import("@unworklet/core").SubgraphDecl<[config: VoicePolicyConfig], {
14
+ capacity: number;
15
+ heldCapacity: number;
16
+ noteOn: (key: Node<"i32">, ch: Node<"i32">, vel: Node<"i32">) => void;
17
+ noteOff: (key: Node<"i32">, ch: Node<"i32">, when?: Node<"bool">) => void;
18
+ reset: (when?: Node<"bool">) => void;
19
+ allNotesOff: (ch: Node<"i32">) => void;
20
+ allSoundOff: (ch: Node<"i32">) => void;
21
+ overflowed: () => Node<"bool">;
22
+ /** Call once in declaration scope with an existing unworklet MIDI input. */
23
+ bindMidi(input: MidiInputHandle): void;
24
+ voices: {
25
+ read(): {
26
+ active: Node<"bool">;
27
+ gate: Node<"bool">;
28
+ note: Node<"i32">;
29
+ channel: Node<"i32">;
30
+ velocity: Node<"f32">;
31
+ };
32
+ /** One latched start/retrigger; read once per sample before invoking the envelope. */
33
+ takeRetrigger(): Node<"bool">;
34
+ /** Envelope completion may free only a released voice, never a newly held one. */
35
+ releaseFinished(done: Node<"bool">): void;
36
+ }[];
37
+ }>;
@@ -0,0 +1,245 @@
1
+ import { bool, defineSubgraph, f32, i32, select, state } from '@unworklet/core';
2
+ function capacity(value, maximum, name) {
3
+ if (!Number.isInteger(value) || value < 1 || value > maximum) {
4
+ throw new RangeError(`${name} must be an integer in [1, ${maximum}]`);
5
+ }
6
+ return value;
7
+ }
8
+ /** Voice allocation only. MIDI transport, graph storage, and node control remain unworklet's. */
9
+ export const voicePolicy = defineSubgraph((config) => {
10
+ if (config.mode !== 'mono' && config.mode !== 'poly')
11
+ throw new TypeError('mode must be mono or poly');
12
+ const size = capacity(config.capacity ?? (config.mode === 'mono' ? 1 : 16), 32, 'capacity');
13
+ const heldSize = capacity(config.heldCapacity ?? Math.max(128, size), 256, 'heldCapacity');
14
+ if (config.mode === 'mono' && size !== 1)
15
+ throw new RangeError('mono capacity must be 1');
16
+ if (heldSize < size)
17
+ throw new RangeError('heldCapacity must be at least capacity');
18
+ if (config.legato !== undefined && typeof config.legato !== 'boolean')
19
+ throw new TypeError('legato must be boolean');
20
+ const legato = config.legato ?? true;
21
+ const ints = (name, length) => state.buffer.i32({ size: length }).expose({ name, snapshot: 'transient' });
22
+ const note = ints('note', size);
23
+ const channel = ints('channel', size);
24
+ const velocity = state.buffer.f32({ size }).expose({ name: 'velocity', snapshot: 'transient' });
25
+ const age = ints('age', size); // zero means free, otherwise dense allocation order 1..size
26
+ const releaseAge = ints('releaseAge', size); // zero means held, otherwise release order
27
+ const owner = ints('owner', size); // held token 1..heldSize; zero means no key owns the release tail
28
+ const retrigger = ints('retrigger', size);
29
+ const heldNote = ints('heldNote', heldSize + 1);
30
+ const heldChannel = ints('heldChannel', heldSize + 1);
31
+ const heldVelocity = ints('heldVelocity', heldSize + 1);
32
+ // Intrusive held-key order: token 0 is a sentinel; valid tokens are 1..heldSize.
33
+ const occupied = ints('occupied', heldSize + 1);
34
+ const previous = ints('previous', heldSize + 1);
35
+ const next = ints('next', heldSize + 1);
36
+ const head = state.i32(0).expose({ name: 'head', snapshot: 'transient' });
37
+ const tail = state.i32(0).expose({ name: 'tail', snapshot: 'transient' });
38
+ // Materialize loop accumulators: unworklet 0.4.1 analysis recursively expands
39
+ // expression DAGs, so nested selects otherwise grow exponentially.
40
+ const scratch = ints('scratch', 4);
41
+ const overflow = state.bool(false).expose({ name: 'overflow', snapshot: 'transient' });
42
+ const validKey = (key, ch) => key.gte(0).and(key.lte(127)).and(ch.gte(0)).and(ch.lte(15));
43
+ const releaseCount = () => {
44
+ let count = i32(0);
45
+ for (let n = 0; n < size; n++)
46
+ count = count.add(select(releaseAge.read(n).gt(0), i32(1), i32(0)));
47
+ return count;
48
+ };
49
+ function releaseVoice(index, when) {
50
+ const shouldRelease = when.and(age.read(index).gt(0)).and(owner.read(index).gt(0));
51
+ const nextOrder = releaseCount().add(1);
52
+ owner.write(index, select(shouldRelease, i32(0), owner.read(index)));
53
+ releaseAge.write(index, select(shouldRelease, nextOrder, releaseAge.read(index)));
54
+ }
55
+ function freeVoice(index, when) {
56
+ const oldAge = age.read(index);
57
+ const oldRelease = releaseAge.read(index);
58
+ const enabled = when.and(oldAge.gt(0));
59
+ for (let n = 0; n < size; n++) {
60
+ const a = age.read(n);
61
+ const r = releaseAge.read(n);
62
+ age.write(n, select(enabled.and(a.gt(oldAge)), a.sub(1), a));
63
+ releaseAge.write(n, select(enabled.and(oldRelease.gt(0)).and(r.gt(oldRelease)), r.sub(1), r));
64
+ }
65
+ age.write(index, select(enabled, i32(0), age.read(index)));
66
+ releaseAge.write(index, select(enabled, i32(0), releaseAge.read(index)));
67
+ owner.write(index, select(enabled, i32(0), owner.read(index)));
68
+ note.write(index, select(enabled, i32(0), note.read(index)));
69
+ channel.write(index, select(enabled, i32(0), channel.read(index)));
70
+ velocity.write(index, select(enabled, f32(0), velocity.read(index)));
71
+ retrigger.write(index, select(enabled, i32(0), retrigger.read(index)));
72
+ }
73
+ function activate(index, token, when, trigger) {
74
+ const oldAge = age.read(index);
75
+ const oldRelease = releaseAge.read(index);
76
+ let count = i32(0);
77
+ for (let n = 0; n < size; n++)
78
+ count = count.add(select(age.read(n).gt(0), i32(1), i32(0)));
79
+ for (let n = 0; n < size; n++) {
80
+ const a = age.read(n);
81
+ const r = releaseAge.read(n);
82
+ age.write(n, select(when.and(oldAge.gt(0)).and(a.gt(oldAge)), a.sub(1), a));
83
+ releaseAge.write(n, select(when.and(oldRelease.gt(0)).and(r.gt(oldRelease)), r.sub(1), r));
84
+ }
85
+ age.write(index, select(when, select(oldAge.gt(0), count, count.add(1)), age.read(index)));
86
+ releaseAge.write(index, select(when, i32(0), releaseAge.read(index)));
87
+ owner.write(index, select(when, token, owner.read(index)));
88
+ note.write(index, select(when, heldNote.read(token), note.read(index)));
89
+ channel.write(index, select(when, heldChannel.read(token), channel.read(index)));
90
+ velocity.write(index, select(when, f32(heldVelocity.read(token)).div(127), velocity.read(index)));
91
+ retrigger.write(index, select(when.and(trigger), i32(1), retrigger.read(index)));
92
+ }
93
+ function monoPriority(when) {
94
+ const selected = tail.read();
95
+ const newest = selected;
96
+ const changed = owner.read(0).eq(selected).not();
97
+ const trigger = bool(!legato).or(owner.read(0).eq(0));
98
+ activate(i32(0), selected, when.and(newest.gt(0)).and(changed), trigger);
99
+ releaseVoice(0, when.and(newest.eq(0)));
100
+ }
101
+ function removeHeld(token, when) {
102
+ const before = previous.read(token);
103
+ const after = next.read(token);
104
+ next.write(before, select(when.and(before.gt(0)), after, next.read(before)));
105
+ previous.write(after, select(when.and(after.gt(0)), before, previous.read(after)));
106
+ head.write(select(when.and(head.read().eq(token)), after, head.read()));
107
+ tail.write(select(when.and(tail.read().eq(token)), before, tail.read()));
108
+ occupied.write(token, select(when, i32(0), occupied.read(token)));
109
+ previous.write(token, select(when, i32(0), previous.read(token)));
110
+ next.write(token, select(when, i32(0), next.read(token)));
111
+ heldNote.write(token, select(when, i32(0), heldNote.read(token)));
112
+ heldChannel.write(token, select(when, i32(0), heldChannel.read(token)));
113
+ heldVelocity.write(token, select(when, i32(0), heldVelocity.read(token)));
114
+ }
115
+ function noteOff(key, ch, when = bool(true)) {
116
+ scratch.write(0, 0);
117
+ scratch.write(1, head.read());
118
+ for (let n = 0; n < heldSize; n++) {
119
+ const token = scratch.read(0);
120
+ const cursor = scratch.read(1);
121
+ const match = token.eq(0).and(cursor.gt(0)).and(heldNote.read(cursor).eq(key)).and(heldChannel.read(cursor).eq(ch));
122
+ scratch.write(0, select(match, cursor, token));
123
+ scratch.write(1, next.read(cursor));
124
+ }
125
+ const token = scratch.read(0);
126
+ const found = when.and(validKey(key, ch)).and(token.gt(0));
127
+ removeHeld(token, found);
128
+ if (config.mode === 'mono')
129
+ monoPriority(found);
130
+ else
131
+ for (let n = 0; n < size; n++)
132
+ releaseVoice(n, found.and(owner.read(n).eq(token)));
133
+ }
134
+ function noteOn(key, ch, vel) {
135
+ noteOff(key, ch, vel.eq(0));
136
+ const enabled = validKey(key, ch).and(vel.gt(0)).and(vel.lte(127));
137
+ let token = i32(0);
138
+ let found = bool(false);
139
+ for (let n = heldSize; n >= 1; n--) {
140
+ const free = occupied.read(n).eq(0);
141
+ token = select(free, i32(n), token);
142
+ found = found.or(free);
143
+ }
144
+ const accept = enabled.and(found);
145
+ overflow.write(overflow.read().or(enabled.and(found.not())));
146
+ const last = tail.read();
147
+ next.write(last, select(accept.and(last.gt(0)), token, next.read(last)));
148
+ previous.write(token, select(accept, last, previous.read(token)));
149
+ next.write(token, select(accept, i32(0), next.read(token)));
150
+ head.write(select(accept.and(last.eq(0)), token, head.read()));
151
+ tail.write(select(accept, token, tail.read()));
152
+ occupied.write(token, select(accept, i32(1), occupied.read(token)));
153
+ heldNote.write(token, select(accept, key, heldNote.read(token)));
154
+ heldChannel.write(token, select(accept, ch, heldChannel.read(token)));
155
+ heldVelocity.write(token, select(accept, vel, heldVelocity.read(token)));
156
+ if (config.mode === 'mono')
157
+ monoPriority(accept);
158
+ else {
159
+ scratch.write(2, 2 * size + 1);
160
+ scratch.write(3, 0);
161
+ for (let n = 0; n < size; n++) {
162
+ const a = age.read(n);
163
+ const r = releaseAge.read(n);
164
+ const score = select(a.eq(0), i32(0), select(r.gt(0), r, a.add(size)));
165
+ const best = scratch.read(2);
166
+ const chosen = scratch.read(3);
167
+ const better = score.lt(best); // ascending slots break ties deterministically
168
+ scratch.write(3, select(better, i32(n), chosen));
169
+ scratch.write(2, select(better, score, best));
170
+ }
171
+ activate(scratch.read(3), token, accept, bool(true));
172
+ }
173
+ }
174
+ function channelOff(ch, immediate, when = bool(true)) {
175
+ const enabled = when.and(ch.gte(0)).and(ch.lte(15));
176
+ // Remove matching identities through bounded, constant-time list unlinking.
177
+ for (let n = 1; n <= heldSize; n++) {
178
+ removeHeld(i32(n), enabled.and(occupied.read(n).gt(0)).and(heldChannel.read(n).eq(ch)));
179
+ }
180
+ if (immediate)
181
+ for (let n = 0; n < size; n++)
182
+ freeVoice(n, enabled.and(channel.read(n).eq(ch)));
183
+ if (config.mode === 'mono')
184
+ monoPriority(enabled);
185
+ else if (!immediate)
186
+ for (let n = 0; n < size; n++)
187
+ releaseVoice(n, enabled.and(channel.read(n).eq(ch)));
188
+ }
189
+ function reset(when = bool(true)) {
190
+ for (let n = 0; n < size; n++)
191
+ freeVoice(n, when);
192
+ for (let n = 0; n <= heldSize; n++) {
193
+ occupied.write(n, select(when, i32(0), occupied.read(n)));
194
+ previous.write(n, select(when, i32(0), previous.read(n)));
195
+ next.write(n, select(when, i32(0), next.read(n)));
196
+ heldNote.write(n, select(when, i32(0), heldNote.read(n)));
197
+ heldChannel.write(n, select(when, i32(0), heldChannel.read(n)));
198
+ heldVelocity.write(n, select(when, i32(0), heldVelocity.read(n)));
199
+ }
200
+ head.write(select(when, i32(0), head.read()));
201
+ tail.write(select(when, i32(0), tail.read()));
202
+ overflow.write(select(when, false, overflow.read()));
203
+ }
204
+ return {
205
+ capacity: size,
206
+ heldCapacity: heldSize,
207
+ noteOn,
208
+ noteOff,
209
+ reset,
210
+ allNotesOff: (ch) => channelOff(ch, false),
211
+ allSoundOff: (ch) => channelOff(ch, true),
212
+ overflowed: () => overflow.read(),
213
+ /** Call once in declaration scope with an existing unworklet MIDI input. */
214
+ bindMidi(input) {
215
+ input.onEvent('noteOn', e => noteOn(e.note, e.channel, e.velocity));
216
+ input.onEvent('noteOff', e => noteOff(e.note, e.channel));
217
+ input.onEvent('cc', e => {
218
+ channelOff(e.channel, false, e.controller.eq(123));
219
+ channelOff(e.channel, true, e.controller.eq(120));
220
+ });
221
+ },
222
+ voices: Array.from({ length: size }, (_, index) => ({
223
+ read() {
224
+ const active = age.read(index).gt(0);
225
+ return {
226
+ active,
227
+ gate: owner.read(index).gt(0),
228
+ note: select(active, note.read(index), i32(-1)),
229
+ channel: select(active, channel.read(index), i32(-1)),
230
+ velocity: velocity.read(index),
231
+ };
232
+ },
233
+ /** One latched start/retrigger; read once per sample before invoking the envelope. */
234
+ takeRetrigger() {
235
+ const value = retrigger.read(index).gt(0);
236
+ retrigger.write(index, 0);
237
+ return value;
238
+ },
239
+ /** Envelope completion may free only a released voice, never a newly held one. */
240
+ releaseFinished(done) {
241
+ freeVoice(index, done.and(owner.read(index).eq(0)));
242
+ },
243
+ })),
244
+ };
245
+ });
@@ -0,0 +1,37 @@
1
+ import type { WavetableConfig, WavetableControls } from './wavetable.js';
2
+ export interface PreparedWavetableBands {
3
+ /** Band-major, then frame-major complete cycles, without duplicated endpoints. */
4
+ data: Float32Array;
5
+ frameLength: number;
6
+ frameCount: number;
7
+ /** Descending retained positive harmonics; DC is preserved in every band. */
8
+ harmonicLimits: readonly number[];
9
+ }
10
+ export interface WavetableBandsOptions {
11
+ /** Original finite mono cycles. Equal power-of-two lengths 16..4096; 1..16 frames.
12
+ * All derived bands together must fit 65536 samples. Input is never changed. */
13
+ frames: readonly Float32Array[];
14
+ }
15
+ /** Layout is the fixed schedule produced by prepareWavetableBands. */
16
+ export interface BandedWavetableConfig extends WavetableConfig {
17
+ /** Power of two, 16..4096 samples per cycle. */
18
+ frameLength: number;
19
+ /** 1..16 morph frames; frameLength * frameCount * derived band count must
20
+ * fit both the shared resident capacity and the 65536-sample native limit. */
21
+ frameCount: number;
22
+ }
23
+ /** Prepare harmonic-truncated pitch bands outside the audio callback. No per-band
24
+ * normalization, resampling, loader, or provenance/license assumption is added.
25
+ * Reject nonfinite input or reconstruction that overflows float32. DC, phase and
26
+ * retained harmonic gain are preserved within FFT/float32 roundoff. */
27
+ export declare function prepareWavetableBands({ frames }: WavetableBandsOptions): PreparedWavetableBands;
28
+ /** Four periodic cycle interpolations: frame morph in neighboring pitch bands.
29
+ * Fetches eight integer resident reads so tiny local fractions survive offsets.
30
+ * Static harmonic truncation is not a brickwall reconstruction filter and does
31
+ * not remove interpolation images or bandwidth from modulation/reset/asset edits. */
32
+ export declare const bandedWavetableSource: import("@unworklet/core").SubgraphDecl<[config: BandedWavetableConfig], {
33
+ tick(c: WavetableControls): {
34
+ output: import("@unworklet/core").Node<"f32">;
35
+ missing: import("@unworklet/core").Node<"bool">;
36
+ };
37
+ }>;