@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,132 @@
1
+ import { type MidiOutputHandle, type Node } from '@unworklet/core';
2
+ export type ClockMode = 'free' | 'tempo';
3
+ export interface MusicalClockConfig {
4
+ sampleRate: number;
5
+ mode: ClockMode;
6
+ /** Fixed cycle length, 1..64 steps. */
7
+ steps: number;
8
+ /** Tempo mode only: power of two in [1/64,64], default 1. */
9
+ stepsPerBeat?: number;
10
+ }
11
+ export interface MusicalClockControls {
12
+ /** Free mode: steps/second. Tempo mode: BPM. Clamped to [0,1000]. */
13
+ rate: Node<'f32'>;
14
+ /** Hold at step/phase zero with no tick. First reset-low sample ticks. */
15
+ reset: Node<'bool'>;
16
+ /** Rising edge seeks; held high does not pin or retrigger. Reset consumes it. */
17
+ seek: Node<'bool'>;
18
+ /** Steps, clamped to +/-1048576 then wrapped into the fixed cycle. */
19
+ position: Node<'f32'>;
20
+ }
21
+ export interface MusicalClockOutput {
22
+ step: Node<'f32'>;
23
+ /** Fraction of the current step, in [0,1). */
24
+ phase: Node<'f32'>;
25
+ /** Initial sample, reset release, seek edge, or a step boundary. */
26
+ tick: Node<'bool'>;
27
+ }
28
+ export interface SequenceStep {
29
+ value: number;
30
+ gate: number;
31
+ }
32
+ export interface StepSequenceConfig extends Omit<MusicalClockConfig, 'steps'> {
33
+ /** Fixed 1..64 steps; values [-1,1], gate fractions [0,1]. */
34
+ steps: readonly SequenceStep[];
35
+ }
36
+ export interface StepSequenceOutput extends MusicalClockOutput {
37
+ value: Node<'f32'>;
38
+ gate: Node<'bool'>;
39
+ }
40
+ export type MsegCurve = 'linear' | 'easeIn' | 'easeOut' | 'smoothstep';
41
+ export interface MsegSegment {
42
+ seconds: number;
43
+ target: number;
44
+ curve: MsegCurve;
45
+ }
46
+ export interface MsegConfig {
47
+ sampleRate: number;
48
+ initial: number;
49
+ /** Fixed 1..16 segments; durations [0,30] s, targets [-1,1]. */
50
+ segments: readonly MsegSegment[];
51
+ /** Whole envelope repeats; the final target need not match initial. */
52
+ loop?: boolean;
53
+ }
54
+ export interface MsegControls {
55
+ trigger: Node<'bool'>;
56
+ reset: Node<'bool'>;
57
+ }
58
+ export interface MsegOutput {
59
+ value: Node<'f32'>;
60
+ done: Node<'bool'>;
61
+ segment: Node<'f32'>;
62
+ }
63
+ export interface SampleAndHoldConfig {
64
+ initial: number;
65
+ }
66
+ export interface RandomModulatorConfig extends SampleAndHoldConfig {
67
+ seed: number;
68
+ }
69
+ export interface RandomModulatorControls {
70
+ /** Level-sensitive sample request: advances once on every high sample. */
71
+ trigger: Node<'bool'>;
72
+ reset: Node<'bool'>;
73
+ /** rho in rho*previous+(1-rho)*uniform; clamped [0,1]. */
74
+ correlation: Node<'f32'>;
75
+ }
76
+ /** Sample clock, no transport or event scheduler. Call exactly once per sample. */
77
+ export declare const musicalClock: import("@unworklet/core").SubgraphDecl<[config: MusicalClockConfig], {
78
+ tick(c: MusicalClockControls): MusicalClockOutput;
79
+ }>;
80
+ /** Fixed step values and gate fractions; no MIDI generation or smoothing. */
81
+ export declare const stepSequence: import("@unworklet/core").SubgraphDecl<[config: StepSequenceConfig], {
82
+ tick(c: MusicalClockControls): StepSequenceOutput;
83
+ }>;
84
+ /**
85
+ * Fixed piecewise envelope. Rising trigger emits the first duration step;
86
+ * held trigger does not retrigger. Reset consumes trigger and emits initial.
87
+ * Zero-length segments collapse at their boundary in list order. Loop jumps
88
+ * from last target to the first segment's first step on the next sample.
89
+ */
90
+ export declare const mseg: import("@unworklet/core").SubgraphDecl<[config: MsegConfig], {
91
+ tick(c: MsegControls): MsegOutput;
92
+ }>;
93
+ /** Level-sensitive sampling; full finite f32 range. Native state maps -0 to +0. */
94
+ export declare const sampleAndHold: import("@unworklet/core").SubgraphDecl<[config: SampleAndHoldConfig], {
95
+ tick(input: Node<"f32">, trigger: Node<"bool">, reset: Node<"bool">): Node<"f32">;
96
+ }>;
97
+ /**
98
+ * Park-Miller seeded S&H / bounded correlated random. Each high trigger advances
99
+ * the PRNG, including correlation=1. Reset wins, restores seed/initial, and
100
+ * never advances. This is a convex random recurrence, not normalized Gaussian
101
+ * noise or a specified spectral color. All dynamic inputs must be finite.
102
+ */
103
+ export declare const randomModulator: import("@unworklet/core").SubgraphDecl<[config: RandomModulatorConfig], {
104
+ tick(c: RandomModulatorControls): Node<"f32">;
105
+ }>;
106
+ export interface ArpeggiatorNote {
107
+ note: number;
108
+ velocity: number;
109
+ gate: number;
110
+ }
111
+ export interface FixedArpeggiatorConfig extends Omit<MusicalClockConfig, 'steps'> {
112
+ channel: number;
113
+ /** Fixed order, 1..64 notes. gate=0 is a rest; no incoming chord capture. */
114
+ notes: readonly ArpeggiatorNote[];
115
+ }
116
+ export interface FixedArpeggiatorControls extends MusicalClockControls {
117
+ /** Native unsigned 32-bit absolute timestamp bits, carried by an i32 node. */
118
+ atSample: Node<'i32'>;
119
+ }
120
+ export interface FixedArpeggiatorOutput extends MusicalClockOutput {
121
+ note: Node<'f32'>;
122
+ gate: Node<'bool'>;
123
+ }
124
+ /**
125
+ * Bounded fixed-note arp using a caller-owned native MIDI output. At most one
126
+ * off then one on per sample. Output capacity >=256 plus per-quantum draining
127
+ * is required for the conservative burst bound, not guaranteed host delivery.
128
+ * Rate zero holds a sounding note; reset before stopping/disconnecting.
129
+ */
130
+ export declare const fixedArpeggiator: import("@unworklet/core").SubgraphDecl<[config: FixedArpeggiatorConfig], {
131
+ tick(output: MidiOutputHandle, c: FixedArpeggiatorControls): FixedArpeggiatorOutput;
132
+ }>;
@@ -0,0 +1,228 @@
1
+ import { bool, defineSubgraph, f32, f64, i32, instantiate, select, state } from '@unworklet/core';
2
+ function checkRate(rate) {
3
+ if (!Number.isInteger(rate) || rate < 8000 || rate > 192000)
4
+ throw new RangeError('modulation sampleRate must be an integer in [8000,192000]');
5
+ }
6
+ function boundedNumber(value, lo, hi, name) {
7
+ if (!Number.isFinite(value) || value < lo || value > hi)
8
+ throw new RangeError(`${name} must be finite in [${lo},${hi}]`);
9
+ }
10
+ function checkClock(config) {
11
+ checkRate(config.sampleRate);
12
+ if (config.mode !== 'free' && config.mode !== 'tempo')
13
+ throw new RangeError('clock mode must be free or tempo');
14
+ if (!Number.isInteger(config.steps) || config.steps < 1 || config.steps > 64)
15
+ throw new RangeError('clock steps must be an integer in [1,64]');
16
+ const division = config.stepsPerBeat ?? 1;
17
+ if (!Number.isFinite(division) || division < 1 / 64 || division > 64 || !Number.isInteger(Math.log2(division))) {
18
+ throw new RangeError('stepsPerBeat must be a power of two in [1/64,64]');
19
+ }
20
+ if (config.mode === 'free' && division !== 1)
21
+ throw new RangeError('stepsPerBeat applies only to tempo mode');
22
+ }
23
+ // Core 0.4.1 flushes tiny scalar-state writes. Exact power-of-two scaling
24
+ // preserves every finite f32 control, including subnormals, without a new
25
+ // amplitude threshold. Even the largest finite f32 input remains finite here.
26
+ const SCALE = 2 ** 128;
27
+ const PHASE_MAX = 1 - 2 ** -24;
28
+ const clockCore = defineSubgraph((config) => {
29
+ checkClock(config);
30
+ const threshold = config.sampleRate * (config.mode === 'tempo' ? 60 : 1);
31
+ const division = config.mode === 'tempo' ? config.stepsPerBeat ?? 1 : 1;
32
+ const lastPosition = config.steps - 2 ** (Math.ceil(Math.log2(config.steps)) - 53);
33
+ const position = state.f64(0).named('positionScaled');
34
+ const error = state.f64(0).named('errorScaled');
35
+ const step = state.i32(0).named('step');
36
+ const pending = state.bool(true).named('pending');
37
+ const previousSeek = state.bool(false).named('previousSeek');
38
+ // Explicit materialization keeps capture/compile work bounded for composed
39
+ // consumers, rather than expanding shared arithmetic recursively.
40
+ const current = state.f64(0).named('currentScaled');
41
+ const increment = state.f64(0).named('incrementScaled');
42
+ const sum = state.f64(0).named('sumScaled');
43
+ const nextError = state.f64(0).named('nextErrorScaled');
44
+ return {
45
+ tick(c) {
46
+ const seek = c.seek.and(previousSeek.read().not()).and(c.reset.not());
47
+ const request = f64(c.position.clamp(-1048576, 1048576));
48
+ const wrapped = request.sub(request.div(config.steps).floor().mul(config.steps)).min(lastPosition);
49
+ const targetStep = wrapped.floor();
50
+ const oldStep = select(c.reset, i32(0), select(seek, i32(targetStep), step.read()));
51
+ current.write(select(c.reset, f64(0), select(seek, wrapped.sub(targetStep).mul(threshold * SCALE), position.read())));
52
+ const compensation = select(c.reset.or(seek), f64(0), error.read());
53
+ // A positive compensation at an exactly rounded threshold means the
54
+ // true position is still below the boundary. Preserve that distinction
55
+ // for gate=1 and crossing decisions (no epsilon or early sample tick).
56
+ const phase = current.read().sub(compensation).div(threshold * SCALE).clamp(0, 1 - 2 ** -53);
57
+ const pulse = c.reset.not().and(seek.or(pending.read()));
58
+ // Accumulate frequency units, not rounded samples-per-step or repeated
59
+ // fractional periods. Compensated summation preserves fractional tempos.
60
+ increment.write(f64(c.rate.clamp(0, 1000)).mul(division * SCALE).sub(compensation));
61
+ sum.write(current.read().add(increment.read()));
62
+ nextError.write(sum.read().sub(current.read()).sub(increment.read()));
63
+ const crossed = sum.read().gt(threshold * SCALE).or(sum.read().eq(threshold * SCALE).and(nextError.read().lte(0)));
64
+ const next = sum.read().sub(select(crossed, f64(threshold * SCALE), f64(0)));
65
+ const nextStep = oldStep.add(select(crossed, i32(1), i32(0)));
66
+ error.write(select(c.reset, f64(0), nextError.read()));
67
+ position.write(select(c.reset, f64(0), next));
68
+ step.write(select(c.reset, i32(0), select(nextStep.gte(config.steps), i32(0), nextStep)));
69
+ pending.write(c.reset.or(crossed));
70
+ previousSeek.write(c.seek);
71
+ return { step: f32(oldStep), phase: f32(phase).clamp(0, PHASE_MAX), phase64: phase, tick: pulse };
72
+ },
73
+ };
74
+ });
75
+ /** Sample clock, no transport or event scheduler. Call exactly once per sample. */
76
+ export const musicalClock = defineSubgraph((config) => {
77
+ const clock = instantiate(clockCore, config, { name: 'clock' });
78
+ return { tick(c) {
79
+ const value = clock.tick(c);
80
+ return { step: value.step, phase: value.phase, tick: value.tick };
81
+ } };
82
+ });
83
+ /** Fixed step values and gate fractions; no MIDI generation or smoothing. */
84
+ export const stepSequence = defineSubgraph((config) => {
85
+ if (!Array.isArray(config.steps) || config.steps.length < 1 || config.steps.length > 64)
86
+ throw new RangeError('sequence requires 1..64 steps');
87
+ const steps = config.steps.map(step => {
88
+ boundedNumber(step.value, -1, 1, 'step value');
89
+ boundedNumber(step.gate, 0, 1, 'step gate');
90
+ return { value: step.value, gate: step.gate };
91
+ });
92
+ const clock = instantiate(clockCore, { ...config, steps: steps.length }, { name: 'clock' });
93
+ return { tick(c) {
94
+ const t = clock.tick(c);
95
+ let value = f32(steps[0].value), gate = f64(steps[0].gate);
96
+ for (let n = 1; n < steps.length; n++) {
97
+ value = select(t.step.eq(n), f32(steps[n].value), value);
98
+ gate = select(t.step.eq(n), f64(steps[n].gate), gate);
99
+ }
100
+ return { value, step: t.step, phase: t.phase, tick: t.tick,
101
+ gate: c.reset.not().and(t.phase64.lt(gate)) };
102
+ } };
103
+ });
104
+ /**
105
+ * Fixed piecewise envelope. Rising trigger emits the first duration step;
106
+ * held trigger does not retrigger. Reset consumes trigger and emits initial.
107
+ * Zero-length segments collapse at their boundary in list order. Loop jumps
108
+ * from last target to the first segment's first step on the next sample.
109
+ */
110
+ export const mseg = defineSubgraph((config) => {
111
+ checkRate(config.sampleRate);
112
+ boundedNumber(config.initial, -1, 1, 'MSEG initial');
113
+ if (!Array.isArray(config.segments) || config.segments.length < 1 || config.segments.length > 16)
114
+ throw new RangeError('MSEG requires 1..16 segments');
115
+ if (config.loop !== undefined && typeof config.loop !== 'boolean')
116
+ throw new RangeError('MSEG loop must be boolean');
117
+ let total = 0, startValue = config.initial;
118
+ const segments = config.segments.map(segment => {
119
+ boundedNumber(segment.seconds, 0, 30, 'MSEG seconds');
120
+ boundedNumber(segment.target, -1, 1, 'MSEG target');
121
+ if (!['linear', 'easeIn', 'easeOut', 'smoothstep'].includes(segment.curve))
122
+ throw new RangeError('unknown MSEG curve');
123
+ const frames = Math.round(segment.seconds * config.sampleRate);
124
+ const result = { ...segment, start: total, frames, startValue };
125
+ total += frames;
126
+ startValue = segment.target;
127
+ return result;
128
+ });
129
+ if (config.loop && total === 0)
130
+ throw new RangeError('looped MSEG needs at least one duration sample');
131
+ const frame = state.i32(0).named('frame');
132
+ const started = state.bool(false).named('started');
133
+ const previousTrigger = state.bool(false).named('previousTrigger');
134
+ return { tick(c) {
135
+ const trigger = c.trigger.and(previousTrigger.read().not()).and(c.reset.not());
136
+ const active = c.reset.not().and(started.read().or(trigger));
137
+ const advance = config.loop ? select(frame.read().gte(total), i32(1), frame.read().add(1)) : frame.read().add(1).min(total);
138
+ frame.write(select(active, select(trigger, i32(Math.min(1, total)), advance), i32(0)));
139
+ const elapsed = frame.read();
140
+ let value = f64(config.initial), index = i32(0);
141
+ for (let n = 0; n < segments.length; n++) {
142
+ const s = segments[n];
143
+ const t = f64(elapsed.sub(s.start)).div(Math.max(1, s.frames)).clamp(0, 1);
144
+ const curve = s.curve === 'easeIn' ? t.mul(t) : s.curve === 'easeOut' ? t.mul(f64(2).sub(t)) : s.curve === 'smoothstep' ? t.mul(t).mul(f64(3).sub(t.mul(2))) : t;
145
+ const applicable = active.and(s.frames === 0 ? elapsed.gte(s.start) : elapsed.gt(s.start));
146
+ const next = s.frames === 0 ? f64(s.target) : f64(s.startValue).add(curve.mul(s.target - s.startValue));
147
+ value = select(applicable, next, value);
148
+ index = select(applicable, i32(n), index);
149
+ }
150
+ started.write(active);
151
+ previousTrigger.write(c.trigger);
152
+ return { value: f32(value).clamp(-1, 1), segment: f32(index), done: active.not().or(bool(!config.loop).and(elapsed.gte(total))) };
153
+ } };
154
+ });
155
+ /** Level-sensitive sampling; full finite f32 range. Native state maps -0 to +0. */
156
+ export const sampleAndHold = defineSubgraph((config) => {
157
+ boundedNumber(config.initial, -1, 1, 'sampleAndHold initial');
158
+ const held = state.f64(config.initial * SCALE).named('heldScaled');
159
+ return { tick(input, trigger, reset) {
160
+ held.write(select(reset, f64(config.initial * SCALE), select(trigger, f64(input).mul(SCALE), held.read())));
161
+ return f32(held.read().div(SCALE));
162
+ } };
163
+ });
164
+ /**
165
+ * Park-Miller seeded S&H / bounded correlated random. Each high trigger advances
166
+ * the PRNG, including correlation=1. Reset wins, restores seed/initial, and
167
+ * never advances. This is a convex random recurrence, not normalized Gaussian
168
+ * noise or a specified spectral color. All dynamic inputs must be finite.
169
+ */
170
+ export const randomModulator = defineSubgraph((config) => {
171
+ boundedNumber(config.initial, -1, 1, 'random initial');
172
+ if (!Number.isInteger(config.seed) || config.seed < 1 || config.seed > 2147483646)
173
+ throw new RangeError('random seed must be an integer in [1,2147483646]');
174
+ const seed = state.f64(config.seed).named('seed');
175
+ const value = state.f64(config.initial * SCALE).named('valueScaled');
176
+ return { tick(c) {
177
+ // Product <= 3.61e13 < 2^53; all integer arithmetic is exactly represented.
178
+ const product = seed.read().mul(16807);
179
+ const nextSeed = product.sub(product.div(2147483647).floor().mul(2147483647));
180
+ seed.write(select(c.reset, f64(config.seed), select(c.trigger, nextSeed, seed.read())));
181
+ const uniform = seed.read().sub(1).div(2147483645).mul(2).sub(1);
182
+ const rho = f64(c.correlation.clamp(0, 1));
183
+ const next = value.read().mul(rho).add(uniform.mul(f64(1).sub(rho)).mul(SCALE));
184
+ value.write(select(c.reset, f64(config.initial * SCALE), select(c.trigger, next, value.read())));
185
+ return f32(value.read().div(SCALE)).clamp(-1, 1);
186
+ } };
187
+ });
188
+ /**
189
+ * Bounded fixed-note arp using a caller-owned native MIDI output. At most one
190
+ * off then one on per sample. Output capacity >=256 plus per-quantum draining
191
+ * is required for the conservative burst bound, not guaranteed host delivery.
192
+ * Rate zero holds a sounding note; reset before stopping/disconnecting.
193
+ */
194
+ export const fixedArpeggiator = defineSubgraph((config) => {
195
+ if (!Number.isInteger(config.channel) || config.channel < 0 || config.channel > 15)
196
+ throw new RangeError('arp channel must be an integer in [0,15]');
197
+ if (!Array.isArray(config.notes) || config.notes.length < 1 || config.notes.length > 64)
198
+ throw new RangeError('arp requires 1..64 notes');
199
+ const notes = config.notes.map(note => {
200
+ if (!Number.isInteger(note.note) || note.note < 0 || note.note > 127)
201
+ throw new RangeError('arp note must be an integer in [0,127]');
202
+ if (!Number.isInteger(note.velocity) || note.velocity < 1 || note.velocity > 127)
203
+ throw new RangeError('arp velocity must be an integer in [1,127]');
204
+ boundedNumber(note.gate, 0, 1, 'arp gate');
205
+ return { ...note };
206
+ });
207
+ const clock = instantiate(clockCore, { ...config, steps: notes.length }, { name: 'clock' });
208
+ const active = state.bool(false).named('active');
209
+ const activeNote = state.i32(notes[0].note).named('activeNote');
210
+ return { tick(output, c) {
211
+ const t = clock.tick(c);
212
+ let note = i32(notes[0].note), velocity = i32(notes[0].velocity), fraction = f64(notes[0].gate);
213
+ for (let n = 1; n < notes.length; n++) {
214
+ note = select(t.step.eq(n), i32(notes[n].note), note);
215
+ velocity = select(t.step.eq(n), i32(notes[n].velocity), velocity);
216
+ fraction = select(t.step.eq(n), f64(notes[n].gate), fraction);
217
+ }
218
+ const gate = c.reset.not().and(t.phase64.lt(fraction));
219
+ const previous = active.read();
220
+ const off = previous.and(gate.not().or(t.tick));
221
+ const on = gate.and(previous.not().or(t.tick));
222
+ output.emitIf(off, { type: 'noteOff', channel: config.channel, note: activeNote.read(), velocity: 0, atSample: c.atSample });
223
+ output.emitIf(on, { type: 'noteOn', channel: config.channel, note, velocity, atSample: c.atSample });
224
+ activeNote.write(select(on, note, activeNote.read()));
225
+ active.write(gate);
226
+ return { note: f32(note), gate, step: t.step, phase: t.phase, tick: t.tick };
227
+ } };
228
+ });
@@ -0,0 +1,36 @@
1
+ import { type MidiInputHandle, type Node } from '@unworklet/core';
2
+ export interface MpeExpressionConfig {
3
+ /** Fixed lower zone: native channel 0 is master, 1..memberChannels are members. Default 15. */
4
+ memberChannels?: number;
5
+ /** Symmetric semitone range, finite [0,96]. Default 48. No RPN negotiation. */
6
+ memberBendRange?: number;
7
+ /** Symmetric semitone range, finite [0,96]. Default 2. */
8
+ masterBendRange?: number;
9
+ }
10
+ /** Pass the existing allocator voice's active/channel fields without remapping. */
11
+ export interface MpeExpressionVoice {
12
+ active: Node<'bool'>;
13
+ channel: Node<'i32'>;
14
+ }
15
+ /** Lower-zone expression routing only: no note allocator or pedal propagation.
16
+ * Bind once to the same native MIDI input as the existing allocation policy.
17
+ * See docs/mpe-expression.md for channel reuse, reset and unsupported features.
18
+ */
19
+ export declare const mpeExpression: import("@unworklet/core").SubgraphDecl<[config: MpeExpressionConfig], {
20
+ memberChannels: number;
21
+ memberBendRange: number;
22
+ masterBendRange: number;
23
+ reset: (when?: Node<"bool">) => void;
24
+ bindMidi(input: MidiInputHandle): void;
25
+ read(voice: MpeExpressionVoice): {
26
+ inZone: Node<"bool">;
27
+ memberBend: Node<"f32">;
28
+ masterBend: Node<"f32">;
29
+ /** Already includes both bend ranges. Do not add performancePolicy.bend again. */
30
+ bendSemitones: Node<"f32">;
31
+ memberPressure: Node<"f32">;
32
+ masterPressure: Node<"f32">;
33
+ memberTimbre: Node<"f32">;
34
+ masterTimbre: Node<"f32">;
35
+ };
36
+ }>;
@@ -0,0 +1,73 @@
1
+ import { bool, defineSubgraph, f32, f64, i32, select, state } from '@unworklet/core';
2
+ /** Lower-zone expression routing only: no note allocator or pedal propagation.
3
+ * Bind once to the same native MIDI input as the existing allocation policy.
4
+ * See docs/mpe-expression.md for channel reuse, reset and unsupported features.
5
+ */
6
+ export const mpeExpression = defineSubgraph((config) => {
7
+ const members = config.memberChannels ?? 15;
8
+ const memberRange = config.memberBendRange ?? 48;
9
+ const masterRange = config.masterBendRange ?? 2;
10
+ if (!Number.isInteger(members) || members < 1 || members > 15) {
11
+ throw new RangeError('mpeExpression memberChannels must be an integer in [1,15]');
12
+ }
13
+ for (const [name, value] of [['memberBendRange', memberRange], ['masterBendRange', masterRange]]) {
14
+ if (!Number.isFinite(value) || value < 0 || value > 96)
15
+ throw new RangeError(`mpeExpression ${name} must be finite in [0,96]`);
16
+ }
17
+ const ints = (name) => state.buffer.i32({ size: members + 1 }).expose({ name, snapshot: 'transient' });
18
+ // Store centered integer MIDI values, so fresh-instance transient defaults
19
+ // represent bend 8192, pressure 0 and CC74 64. In-place restore leaves
20
+ // transient state unchanged; reset() is the explicit clearing operation.
21
+ const bend = ints('bend'), pressure = ints('pressure'), timbre = ints('timbre');
22
+ const validChannel = (channel) => channel.gte(0).and(channel.lte(members));
23
+ const valid7 = (value) => value.gte(0).and(value.lte(127));
24
+ const normalizedBend = (channel) => {
25
+ const value = bend.read(channel);
26
+ return f64(value).div(select(value.lt(0), f64(8192), f64(8191)));
27
+ };
28
+ function reset(when = bool(true)) {
29
+ for (let channel = 0; channel <= members; channel++) {
30
+ bend.write(channel, select(when, i32(0), bend.read(channel)));
31
+ pressure.write(channel, select(when, i32(0), pressure.read(channel)));
32
+ timbre.write(channel, select(when, i32(0), timbre.read(channel)));
33
+ }
34
+ }
35
+ return {
36
+ memberChannels: members, memberBendRange: memberRange, masterBendRange: masterRange,
37
+ reset,
38
+ bindMidi(input) {
39
+ input.onEvent('pitchBend', e => {
40
+ const channel = e.channel.clamp(0, members);
41
+ const valid = validChannel(e.channel).and(e.value.gte(0)).and(e.value.lte(16383));
42
+ bend.write(channel, select(valid, e.value.sub(8192), bend.read(channel)));
43
+ });
44
+ input.onEvent('channelPressure', e => {
45
+ const channel = e.channel.clamp(0, members);
46
+ pressure.write(channel, select(validChannel(e.channel).and(valid7(e.pressure)), e.pressure, pressure.read(channel)));
47
+ });
48
+ input.onEvent('cc', e => {
49
+ const channel = e.channel.clamp(0, members), valid = validChannel(e.channel).and(valid7(e.value));
50
+ const clear = valid.and(e.controller.eq(121));
51
+ bend.write(channel, select(clear, i32(0), bend.read(channel)));
52
+ pressure.write(channel, select(clear, i32(0), pressure.read(channel)));
53
+ timbre.write(channel, select(clear, i32(0), select(valid.and(e.controller.eq(74)), e.value.sub(64), timbre.read(channel))));
54
+ });
55
+ },
56
+ read(voice) {
57
+ const inZone = voice.active.and(voice.channel.gte(1)).and(voice.channel.lte(members));
58
+ const channel = voice.channel.clamp(1, members);
59
+ const member = normalizedBend(channel), master = normalizedBend(i32(0));
60
+ const lane = (value) => select(inZone, value, f32(0));
61
+ return {
62
+ inZone,
63
+ memberBend: lane(f32(member)), masterBend: lane(f32(master)),
64
+ /** Already includes both bend ranges. Do not add performancePolicy.bend again. */
65
+ bendSemitones: lane(f32(member.mul(memberRange).add(master.mul(masterRange)))),
66
+ memberPressure: lane(f32(pressure.read(channel)).div(127)),
67
+ masterPressure: lane(f32(pressure.read(0)).div(127)),
68
+ memberTimbre: lane(f32(timbre.read(channel).add(64)).div(127)),
69
+ masterTimbre: lane(f32(timbre.read(0).add(64)).div(127)),
70
+ };
71
+ },
72
+ };
73
+ });
@@ -0,0 +1,50 @@
1
+ import { type Node } from '@unworklet/core';
2
+ import { type DynamicsConfig, type DynamicsControls } from './dynamics.js';
3
+ export type MultibandDynamicsBandConfig = Omit<DynamicsConfig, 'sampleRate'>;
4
+ export interface MultibandDynamicsConfig {
5
+ sampleRate: number;
6
+ /** Fixed low/mid/high operations and detector modes. */
7
+ bands: readonly [MultibandDynamicsBandConfig, MultibandDynamicsBandConfig, MultibandDynamicsBandConfig];
8
+ }
9
+ export type MultibandDynamicsBandControls = Omit<DynamicsControls, 'reset'>;
10
+ export interface MultibandDynamicsControls {
11
+ /** Finite fallbacks 200/2000 Hz, individually clamped and then sorted. Equality is permitted. */
12
+ lowCutoffHz: Node<'f32'>;
13
+ highCutoffHz: Node<'f32'>;
14
+ bands: readonly [MultibandDynamicsBandControls, MultibandDynamicsBandControls, MultibandDynamicsBandControls];
15
+ reset: Node<'bool'>;
16
+ }
17
+ /** Three phase-compensated LR4 bands with independent linked-stereo dynamics. */
18
+ export declare const multibandDynamics: import("@unworklet/core").SubgraphDecl<[config: MultibandDynamicsConfig], {
19
+ /** No explicit sample delay. IIR phase/group delay is frequency dependent. */
20
+ tick(left: Node<"f32">, right: Node<"f32">, controls: MultibandDynamicsControls): {
21
+ left: Node<"f32">;
22
+ right: Node<"f32">;
23
+ /** Phase-matched unprocessed recombination; not the original dry PCM. */
24
+ dryLeft: Node<"f32">;
25
+ dryRight: Node<"f32">;
26
+ low: {
27
+ left: Node<"f32">;
28
+ right: Node<"f32">;
29
+ envelope: Node<"f32">;
30
+ gain: Node<"f32">;
31
+ gainDb: Node<"f32">;
32
+ };
33
+ mid: {
34
+ left: Node<"f32">;
35
+ right: Node<"f32">;
36
+ envelope: Node<"f32">;
37
+ gain: Node<"f32">;
38
+ gainDb: Node<"f32">;
39
+ };
40
+ high: {
41
+ left: Node<"f32">;
42
+ right: Node<"f32">;
43
+ envelope: Node<"f32">;
44
+ gain: Node<"f32">;
45
+ gainDb: Node<"f32">;
46
+ };
47
+ lowCutoffHz: Node<"f32">;
48
+ highCutoffHz: Node<"f32">;
49
+ };
50
+ }>;
@@ -0,0 +1,60 @@
1
+ import { defineSubgraph, f32, f64, instantiate, select, state } from '@unworklet/core';
2
+ import { crossover } from './crossover.js';
3
+ import { dynamics } from './dynamics.js';
4
+ function finite(input, fallback = 0) {
5
+ return select(input.eq(input).and(input.abs().lte(3.4028234663852886e38)), input, f32(fallback));
6
+ }
7
+ const sum = (a, b, c) => f32(f64(a).add(f64(b)).add(c ? f64(c) : f64(0)));
8
+ /** Three phase-compensated LR4 bands with independent linked-stereo dynamics. */
9
+ export const multibandDynamics = defineSubgraph((config) => {
10
+ const { sampleRate } = config;
11
+ if (!Number.isInteger(sampleRate) || sampleRate < 8000 || sampleRate > 192000) {
12
+ throw new RangeError('multibandDynamics sampleRate must be an integer in [8000,192000]');
13
+ }
14
+ if (!Array.isArray(config.bands) || config.bands.length !== 3)
15
+ throw new RangeError('multibandDynamics requires exactly three bands');
16
+ const maximum = Math.min(20000, 0.45 * sampleRate);
17
+ const splits = ['left', 'right'].map(channel => ({
18
+ low: instantiate(crossover, { sampleRate }, { name: `${channel}LowSplit` }),
19
+ high: instantiate(crossover, { sampleRate }, { name: `${channel}HighSplit` }),
20
+ compensate: instantiate(crossover, { sampleRate }, { name: `${channel}LowCompensation` }),
21
+ }));
22
+ // Materialize the existing f32 stage boundaries once. Without these native
23
+ // scratch buffers, 0.4.1 re-expands returned expressions at every consumer.
24
+ // Exact binary64 scaling prevents the native <1e-30 buffer-store flush from
25
+ // changing tiny audio; each slot is overwritten before read on every tick.
26
+ const scale = 2 ** 128;
27
+ const scratch = ['left', 'right'].map(channel => state.buffer.f64({ size: 5 })
28
+ .expose({ name: `${channel}StageScratchScaled`, snapshot: 'transient' }));
29
+ const processors = config.bands.map((band, i) => instantiate(dynamics, { ...band, sampleRate }, { name: `band${i}` }));
30
+ return {
31
+ /** No explicit sample delay. IIR phase/group delay is frequency dependent. */
32
+ tick(left, right, controls) {
33
+ const a = finite(controls.lowCutoffHz, 200).clamp(20, maximum);
34
+ const b = finite(controls.highCutoffHz, 2000).clamp(20, maximum);
35
+ const lowCutoffHz = a.min(b), highCutoffHz = a.max(b);
36
+ const bands = [left, right].map((input, ch) => {
37
+ const first = splits[ch].low.tick(finite(input), lowCutoffHz, controls.reset);
38
+ const store = (index, value) => scratch[ch].write(index, f64(value).mul(scale));
39
+ const read = (index) => f32(scratch[ch].read(index).div(scale));
40
+ store(0, first.low);
41
+ store(1, first.high);
42
+ const upper = splits[ch].high.tick(read(1), highCutoffHz, controls.reset);
43
+ const compensation = splits[ch].compensate.tick(read(0), highCutoffHz, controls.reset);
44
+ store(2, sum(compensation.low, compensation.high));
45
+ store(3, upper.low);
46
+ store(4, upper.high);
47
+ return [read(2), read(3), read(4)];
48
+ });
49
+ const processed = processors.map((unit, i) => unit.tick(bands[0][i], bands[1][i], bands[0][i], bands[1][i], { ...controls.bands[i], reset: controls.reset }));
50
+ return {
51
+ left: sum(processed[0].left, processed[1].left, processed[2].left),
52
+ right: sum(processed[0].right, processed[1].right, processed[2].right),
53
+ /** Phase-matched unprocessed recombination; not the original dry PCM. */
54
+ dryLeft: sum(...bands[0]), dryRight: sum(...bands[1]),
55
+ low: processed[0], mid: processed[1], high: processed[2],
56
+ lowCutoffHz, highCutoffHz,
57
+ };
58
+ },
59
+ };
60
+ });
@@ -0,0 +1,76 @@
1
+ import { type Node } from '@unworklet/core';
2
+ export interface CurvedAdsrConfig {
3
+ sampleRate: number;
4
+ }
5
+ export interface CurvedAdsrControls {
6
+ gate: Node<'bool'>;
7
+ /** Level-sensitive; ignored while gate is false. */
8
+ retrigger: Node<'bool'>;
9
+ /** Wins over all other controls and consumes the gate level. */
10
+ reset: Node<'bool'>;
11
+ /** Seconds, clamped [0,30], rounded to the nearest sample (half up). */
12
+ attack: Node<'f32'>;
13
+ decay: Node<'f32'>;
14
+ sustain: Node<'f32'>;
15
+ release: Node<'f32'>;
16
+ /** Latched segment bends [-1,1]: -1 ease-in, 0 linear, +1 ease-out. */
17
+ attackBend: Node<'f32'>;
18
+ decayBend: Node<'f32'>;
19
+ releaseBend: Node<'f32'>;
20
+ }
21
+ export interface CurvedAdsrOutput {
22
+ level: Node<'f32'>;
23
+ done: Node<'bool'>;
24
+ }
25
+ /**
26
+ * Segment-relative curved ADSR. Tick exactly once per sample; finite controls.
27
+ * C(t,b)=t+b*t*(1-t), with exact endpoints. Segment settings latch on entry;
28
+ * decay latches at the preceding positive attack endpoint. Sustain follows its
29
+ * current input in the sustain stage. Retrigger/note-off start at the last
30
+ * emitted f32 level. Zero attack collapses into decay on the same sample.
31
+ * Reset > gate-off > retrigger. Reset consumes held gate. No smoothing/tail
32
+ * threshold; zero sustain remains active. Bend zero is linear, not a promise of
33
+ * byte identity with the legacy iterative envelope. See the entry contract.
34
+ */
35
+ export declare const curvedAdsr: import("@unworklet/core").SubgraphDecl<[config: CurvedAdsrConfig], {
36
+ tick(c: CurvedAdsrControls): CurvedAdsrOutput;
37
+ }>;
38
+ export type MusicalLfoWaveform = 'sine' | 'triangle' | 'saw' | 'square';
39
+ export interface MusicalLfoConfig {
40
+ sampleRate: number;
41
+ mode: 'free' | 'tempo';
42
+ waveform: MusicalLfoWaveform;
43
+ /** Tempo mode only, power of two in [1/64,64], default 1. */
44
+ beatsPerCycle?: number;
45
+ }
46
+ export interface MusicalLfoControls {
47
+ /** Free: Hz [0,20]. Tempo: BPM [0,1000]. */
48
+ rate: Node<'f32'>;
49
+ /** Hold the base clock at zero; offset still applies. Consumes seek level. */
50
+ reset: Node<'bool'>;
51
+ /** Rising-edge seek, including while held. Reset wins. */
52
+ seek: Node<'bool'>;
53
+ /** Seek position in cycles, clamped +/-1048576 then wrapped. */
54
+ position: Node<'f32'>;
55
+ /** Live cycles offset, clamped +/-1048576. May deliberately jump. */
56
+ phaseOffset: Node<'f32'>;
57
+ /** Stops base-phase integration; seek/reset and live offset still apply. */
58
+ hold: Node<'bool'>;
59
+ }
60
+ export interface MusicalLfoOutput {
61
+ value: Node<'f32'>;
62
+ /** Canonical f32 waveform phase [0,1), capped at 1-2^-24. */
63
+ phase: Node<'f32'>;
64
+ }
65
+ /**
66
+ * Fixed-waveform LFO composed from musicalClock. Tick once per sample with finite
67
+ * inputs. Output precedes integration. Held reset pins base zero; release emits
68
+ * zero base before resuming. Hold/rate zero preserve base phase. Tempo follows
69
+ * BPM/(60*beatsPerCycle) exactly within the clock's numerical contract, including
70
+ * up to 1066 2/3 Hz. Public f32 clock-phase quantization is intentional, not the
71
+ * legacy sine LFO's internal-f64 phase guarantee. Waveforms are non-bandlimited;
72
+ * phase/offset/reset changes can jump. No smoothing/depth/routing/transport.
73
+ */
74
+ export declare const musicalLfo: import("@unworklet/core").SubgraphDecl<[config: MusicalLfoConfig], {
75
+ tick(c: MusicalLfoControls): MusicalLfoOutput;
76
+ }>;