elemaudio_rs/authoring/
el.rs

1use crate::core::{resolve, ElemNode};
2use crate::graph::{Node, Value};
3
4fn empty_props() -> Value {
5    Value::Object(Default::default())
6}
7
8/// Helper accepted by the variadic math fold helpers.
9///
10/// Arrays work for homogeneous inputs, while tuples are the ergonomic
11/// escape hatch for mixed `Node`/numeric arguments.
12pub trait IntoNodeList {
13    fn into_nodes(self) -> Vec<ElemNode>;
14}
15
16impl<T> IntoNodeList for [T; 1]
17where
18    T: Into<ElemNode>,
19{
20    fn into_nodes(self) -> Vec<ElemNode> {
21        self.into_iter().map(Into::into).collect()
22    }
23}
24
25impl<T> IntoNodeList for [T; 2]
26where
27    T: Into<ElemNode>,
28{
29    fn into_nodes(self) -> Vec<ElemNode> {
30        self.into_iter().map(Into::into).collect()
31    }
32}
33
34impl<T> IntoNodeList for [T; 3]
35where
36    T: Into<ElemNode>,
37{
38    fn into_nodes(self) -> Vec<ElemNode> {
39        self.into_iter().map(Into::into).collect()
40    }
41}
42
43impl<T> IntoNodeList for [T; 4]
44where
45    T: Into<ElemNode>,
46{
47    fn into_nodes(self) -> Vec<ElemNode> {
48        self.into_iter().map(Into::into).collect()
49    }
50}
51
52macro_rules! impl_tuple_nodes {
53    ($($ty:ident => $var:ident),+) => {
54        impl<$($ty),+> IntoNodeList for ($($ty,)+)
55        where
56            $($ty: Into<ElemNode>,)+
57        {
58            fn into_nodes(self) -> Vec<ElemNode> {
59                let ($($var,)+) = self;
60                vec![$($var.into(),)+]
61            }
62        }
63    };
64}
65
66impl_tuple_nodes!(A => a, B => b);
67impl_tuple_nodes!(A => a, B => b, C => c);
68impl_tuple_nodes!(A => a, B => b, C => c, D => d);
69impl_tuple_nodes!(A => a, B => b, C => c, D => d, E => e);
70impl_tuple_nodes!(A => a, B => b, C => c, D => d, E => e, F => f);
71impl_tuple_nodes!(A => a, B => b, C => c, D => d, E => e, F => f, G => g);
72impl_tuple_nodes!(A => a, B => b, C => c, D => d, E => e, F => f, G => g, H => h);
73
74fn fold_node(items: impl IntoNodeList, kind: &str) -> Node {
75    Node::new(
76        kind,
77        Value::Null,
78        items.into_nodes().into_iter().map(resolve).collect(),
79    )
80}
81
82fn node0(kind: &str) -> Node {
83    Node::new(kind, Value::Null, vec![])
84}
85
86fn node1(kind: &str, child: impl Into<ElemNode>) -> Node {
87    Node::new(kind, Value::Null, vec![resolve(child)])
88}
89
90fn node2(kind: &str, left: impl Into<ElemNode>, right: impl Into<ElemNode>) -> Node {
91    Node::new(kind, Value::Null, vec![resolve(left), resolve(right)])
92}
93
94fn node3(
95    kind: &str,
96    a: impl Into<ElemNode>,
97    b: impl Into<ElemNode>,
98    c: impl Into<ElemNode>,
99) -> Node {
100    Node::new(kind, Value::Null, vec![resolve(a), resolve(b), resolve(c)])
101}
102
103fn node6(
104    kind: &str,
105    a: impl Into<ElemNode>,
106    b: impl Into<ElemNode>,
107    c: impl Into<ElemNode>,
108    d: impl Into<ElemNode>,
109    e: impl Into<ElemNode>,
110    f: impl Into<ElemNode>,
111) -> Node {
112    Node::new(
113        kind,
114        Value::Null,
115        vec![
116            resolve(a),
117            resolve(b),
118            resolve(c),
119            resolve(d),
120            resolve(e),
121            resolve(f),
122        ],
123    )
124}
125
126fn node_props0(kind: &str, props: Value) -> Node {
127    Node::new(kind, props, vec![])
128}
129
130fn node_props1(kind: &str, props: Value, child: impl Into<ElemNode>) -> Node {
131    Node::new(kind, props, vec![resolve(child)])
132}
133
134fn node_props2(kind: &str, props: Value, a: impl Into<ElemNode>, b: impl Into<ElemNode>) -> Node {
135    Node::new(kind, props, vec![resolve(a), resolve(b)])
136}
137
138fn node_props3(
139    kind: &str,
140    props: Value,
141    a: impl Into<ElemNode>,
142    b: impl Into<ElemNode>,
143    c: impl Into<ElemNode>,
144) -> Node {
145    Node::new(kind, props, vec![resolve(a), resolve(b), resolve(c)])
146}
147
148fn node_props4(
149    kind: &str,
150    props: Value,
151    a: impl Into<ElemNode>,
152    b: impl Into<ElemNode>,
153    c: impl Into<ElemNode>,
154    d: impl Into<ElemNode>,
155) -> Node {
156    Node::new(
157        kind,
158        props,
159        vec![resolve(a), resolve(b), resolve(c), resolve(d)],
160    )
161}
162
163fn node_props_variadic<T>(kind: &str, props: Value, args: impl IntoIterator<Item = T>) -> Node
164where
165    T: Into<ElemNode>,
166{
167    Node::new(kind, props, args.into_iter().map(resolve).collect())
168}
169
170/// Constant signal node.
171pub fn constant(props: Value) -> Node {
172    Node::new("const", props, vec![])
173}
174
175/// Constant signal node with the Rust-friendly helper name
176/// (const is a protected keyword in rust)
177pub fn const_(value: f64) -> Node {
178    constant(serde_json::json!({ "value": value }))
179}
180
181/// Constant signal node with an author-supplied key.
182pub fn const_with_key(key: &str, value: f64) -> Node {
183    constant(serde_json::json!({ "key": key, "value": value }))
184}
185
186/// Alias for the upstream `const` helper.
187pub fn r#const(props: Value) -> Node {
188    constant(props)
189}
190
191/// Creates a custom node kind with explicit props and children.
192pub fn custom<T>(
193    kind: impl Into<String>,
194    props: Value,
195    children: impl IntoIterator<Item = T>,
196) -> Node
197where
198    T: Into<ElemNode>,
199{
200    Node::new(kind, props, children.into_iter().map(resolve).collect())
201}
202
203/// Sample-rate signal. Expects no children.
204pub fn sr() -> Node {
205    node0("sr")
206}
207/// Current time signal in seconds. Expects no children.
208pub fn time() -> Node {
209    node0("time")
210}
211/// el.counter(g)
212///
213/// Outputs a continuous count of elapsed samples. Expects one child, `g`,
214/// a pulse train alternating between `0` and `1`. When `g` is high, the
215/// counter will run. When `g` is low, the counter will reset and output `0`
216/// until `g` is high again.
217pub fn counter(g: impl Into<ElemNode>) -> Node {
218    node1("counter", g)
219}
220/// el.accum(xn, reset)
221///
222/// Outputs a continuous and running sum over the samples in the input signal
223/// `xn`. This value can grow very large, very quickly, so use with care.
224/// The second argument `reset` is a pulse train signal which resets the
225/// running sum to `0` on each rising edge.
226pub fn accum(xn: impl Into<ElemNode>, reset: impl Into<ElemNode>) -> Node {
227    node2("accum", xn, reset)
228}
229/// el.phasor(rate)
230///
231/// Outputs a ramp from `0` to `1` at the given rate. Expects one child
232/// signal providing the ramp rate in `hz`.
233///
234/// For the same signal with phase reset behavior, see `sphasor`.
235pub fn phasor(rate: impl Into<ElemNode>) -> Node {
236    node1("phasor", rate)
237}
238/// Synchronous phase accumulator. Expects 2 children: rate and sync/reset.
239pub fn syncphasor(left: impl Into<ElemNode>, right: impl Into<ElemNode>) -> Node {
240    node2("sphasor", left, right)
241}
242/// Alias for the upstream `sphasor` helper.
243pub fn sphasor(left: impl Into<ElemNode>, right: impl Into<ElemNode>) -> Node {
244    syncphasor(left, right)
245}
246/// el.latch(t, x)
247///
248/// A sample and hold node. Samples a new value from `x` on a rising edge
249/// of a pulse train `t`, then holds and emits that value until the next
250/// rising edge of `t`.
251///
252/// Expected children:
253/// 1. The control signal, `t`, a pulse train
254/// 2. The input signal to sample
255pub fn latch(t: impl Into<ElemNode>, x: impl Into<ElemNode>) -> Node {
256    node2("latch", t, x)
257}
258/// Peak-hold helper. `props` typically carries `hold` time metadata.
259/// Expects 2 children: input signal and hold/reset control.
260pub fn maxhold(props: Value, a: impl Into<ElemNode>, b: impl Into<ElemNode>) -> Node {
261    node_props2("maxhold", props, a, b)
262}
263/// One-shot helper. `props` typically carries an `arm` flag.
264/// Expects 1 child: the signal to pass once when armed.
265pub fn once(props: Value, child: impl Into<ElemNode>) -> Node {
266    node_props1("once", props, child)
267}
268/// el.rand(\[props\])
269///
270/// Generates a stream of random numbers uniformly distributed on the
271/// range `[0, 1]`.
272///
273/// Props:
274/// - `seed`: seeds the random number generator
275pub fn rand(props: Option<Value>) -> Node {
276    Node::new("rand", props.unwrap_or_else(empty_props), vec![])
277}
278
279/// el.metro(props)
280///
281/// Only available in the WASM-based renderers. You may extend the runtime
282/// in your own integration with a similar processor if you like, but it is
283/// not provided by default.
284///
285/// Emits a pulse train signal much like `el.train`, alternating from `0`
286/// to `1` at a given rate. Importantly, the `el.metro` node is used for
287/// synchronized train signals, and will emit an event through the core
288/// renderer's interface on each rising edge of its output signal.
289///
290/// Props:
291/// - `name`: identifies a metro node by name
292/// - `interval`: metronome period in milliseconds
293pub fn metro(props: Option<Value>) -> Node {
294    Node::new("metro", props.unwrap_or_else(empty_props), vec![])
295}
296/// Named input tap. `props` should carry the tap name metadata.
297pub fn tap_in(props: Value) -> Node {
298    node_props0("tapIn", props)
299}
300/// Named output tap. `props` should carry the tap name metadata.
301/// Expects 1 child: the signal to export.
302pub fn tap_out(props: Value, child: impl Into<ElemNode>) -> Node {
303    node_props1("tapOut", props, child)
304}
305/// Meter / monitor node. `props` should carry the meter name metadata.
306/// Expects 1 child: the signal to observe.
307pub fn meter(props: Value, child: impl Into<ElemNode>) -> Node {
308    node_props1("meter", props, child)
309}
310/// Snapshot node. `props` should carry snapshot metadata.
311/// Expects 2 children: the source signal and trigger/reset.
312pub fn snapshot(props: Value, a: impl Into<ElemNode>, b: impl Into<ElemNode>) -> Node {
313    node_props2("snapshot", props, a, b)
314}
315pub fn scope<T>(props: Value, args: impl IntoIterator<Item = T>) -> Node
316where
317    T: Into<ElemNode>,
318{
319    node_props_variadic("scope", props, args)
320}
321/// FFT analyzer node. `props` should carry analyzer metadata.
322/// Expects 1 child: the signal to analyze.
323pub fn fft(props: Value, child: impl Into<ElemNode>) -> Node {
324    node_props1("fft", props, child)
325}
326/// Capture node. `props` should carry capture metadata.
327/// Expects 2 children: the capture gate and the input signal.
328pub fn capture(props: Value, a: impl Into<ElemNode>, b: impl Into<ElemNode>) -> Node {
329    node_props2("capture", props, a, b)
330}
331/// Table lookup node. `props` should carry table metadata such as path.
332/// Expects 1 child: the lookup coordinate.
333pub fn table(props: Value, child: impl Into<ElemNode>) -> Node {
334    node_props1("table", props, child)
335}
336/// Convolution node. `props` should carry impulse-response metadata.
337/// Expects 1 child: the source signal.
338pub fn convolve(props: Value, child: impl Into<ElemNode>) -> Node {
339    node_props1("convolve", props, child)
340}
341/// Discrete sequence node. `props` should carry `seq` data and optional loop metadata.
342/// Expects 2 children: trigger and reset.
343pub fn seq(props: Value, trigger: impl Into<ElemNode>, reset: impl Into<ElemNode>) -> Node {
344    node_props2("seq", props, trigger, reset)
345}
346/// Discrete sequence node with the `seq2` variant semantics.
347/// `props` should carry `seq` data and optional loop metadata.
348/// Expects 2 children: trigger and reset.
349pub fn seq2(props: Value, trigger: impl Into<ElemNode>, reset: impl Into<ElemNode>) -> Node {
350    node_props2("seq2", props, trigger, reset)
351}
352/// Sample-accurate discrete sequence node.
353/// `props` should carry sparse `seq` entries and optional loop metadata.
354/// Expects 2 children: trigger and reset.
355pub fn sparseq(props: Value, trigger: impl Into<ElemNode>, reset: impl Into<ElemNode>) -> Node {
356    node_props2("sparseq", props, trigger, reset)
357}
358/// Sparse sequence helper variant. `props` should carry sparse `seq` entries.
359/// Expects 1 child: the time or trigger signal.
360pub fn sparseq2(props: Value, child: impl Into<ElemNode>) -> Node {
361    node_props1("sparseq2", props, child)
362}
363/// Sample sequence player. `props` should carry sample metadata such as path.
364/// Expects 1 child: the playback trigger or time signal.
365pub fn sampleseq(props: Value, child: impl Into<ElemNode>) -> Node {
366    node_props1("sampleseq", props, child)
367}
368/// Sample sequence player variant. `props` should carry sample metadata such as path.
369/// Expects 1 child: the playback trigger or time signal.
370pub fn sampleseq2(props: Value, child: impl Into<ElemNode>) -> Node {
371    node_props1("sampleseq2", props, child)
372}
373/// el.pole(p, x)
374///
375/// Implements a simple one-pole filter, also sometimes called a leaky
376/// integrator. Expects two children: the first is the pole position `p`,
377/// the second is the signal `x` to filter.
378pub fn pole(p: impl Into<ElemNode>, x: impl Into<ElemNode>) -> Node {
379    node2("pole", p, x)
380}
381/// el.env(atkPole, relPole, x)
382///
383/// A one-pole envelope follower with different attack and release times.
384/// This is similar to `el.pole(p, el.abs(x))` in implementation. Expects
385/// three children: attack pole, release pole, and the signal to monitor.
386pub fn env(
387    atk_pole: impl Into<ElemNode>,
388    rel_pole: impl Into<ElemNode>,
389    x: impl Into<ElemNode>,
390) -> Node {
391    node3("env", atk_pole, rel_pole, x)
392}
393/// Unit delay / z^-1 helper. Expects 1 child.
394pub fn z(child: impl Into<ElemNode>) -> Node {
395    node1("z", child)
396}
397/// Multi-tap delay. `props` should carry delay buffer metadata such as `size`.
398/// Expects 3 children: read, write, and feedback/control.
399pub fn delay(
400    props: Value,
401    a: impl Into<ElemNode>,
402    b: impl Into<ElemNode>,
403    c: impl Into<ElemNode>,
404) -> Node {
405    node_props3("delay", props, a, b, c)
406}
407/// Sample-accurate delay. `props` should carry buffer metadata such as `size`.
408/// Expects 1 child: the signal to delay.
409pub fn sdelay(props: Value, child: impl Into<ElemNode>) -> Node {
410    node_props1("sdelay", props, child)
411}
412/// Prewarp helper. Expects 1 child.
413pub fn prewarp(child: impl Into<ElemNode>) -> Node {
414    node1("prewarp", child)
415}
416/// One-pole modulation helper. `props` should carry mode metadata.
417/// Expects 2 children: cutoff and input signal.
418pub fn mm1p(props: Value, fc: impl Into<ElemNode>, x: impl Into<ElemNode>) -> Node {
419    node_props2("mm1p", props, fc, x)
420}
421/// State-variable filter. `props` should carry mode metadata.
422/// Expects 3 children: cutoff, resonance/Q, and input signal.
423pub fn svf(
424    props: Value,
425    fc: impl Into<ElemNode>,
426    q: impl Into<ElemNode>,
427    x: impl Into<ElemNode>,
428) -> Node {
429    node_props3("svf", props, fc, q, x)
430}
431/// State-variable shelving filter. `props` should carry mode metadata.
432/// Expects 4 children: cutoff, resonance/Q, gain, and input signal.
433pub fn svfshelf(
434    props: Value,
435    fc: impl Into<ElemNode>,
436    q: impl Into<ElemNode>,
437    gain: impl Into<ElemNode>,
438    x: impl Into<ElemNode>,
439) -> Node {
440    node_props4("svfshelf", props, fc, q, gain, x)
441}
442/// Direct-form biquad helper. Expects 6 children: b0, b1, b2, a1, a2, x.
443pub fn biquad(
444    b0: impl Into<ElemNode>,
445    b1: impl Into<ElemNode>,
446    b2: impl Into<ElemNode>,
447    a1: impl Into<ElemNode>,
448    a2: impl Into<ElemNode>,
449    x: impl Into<ElemNode>,
450) -> Node {
451    node6("biquad", b0, b1, b2, a1, a2, x)
452}
453
454/// Sine helper. Expects exactly 1 child.
455pub fn sin(x: impl Into<ElemNode>) -> Node {
456    node1("sin", x)
457}
458
459/// Cosine helper. Expects exactly 1 child.
460pub fn cos(x: impl Into<ElemNode>) -> Node {
461    node1("cos", x)
462}
463
464/// Tangent helper. Expects exactly 1 child.
465pub fn tan(x: impl Into<ElemNode>) -> Node {
466    node1("tan", x)
467}
468
469/// Hyperbolic tangent helper. Expects exactly 1 child.
470pub fn tanh(x: impl Into<ElemNode>) -> Node {
471    node1("tanh", x)
472}
473
474/// Inverse hyperbolic sine helper. Expects exactly 1 child.
475pub fn asinh(x: impl Into<ElemNode>) -> Node {
476    node1("asinh", x)
477}
478
479/// Natural logarithm helper. Expects exactly 1 child.
480pub fn ln(x: impl Into<ElemNode>) -> Node {
481    node1("ln", x)
482}
483
484/// Base-10 logarithm helper. Expects exactly 1 child.
485pub fn log(x: impl Into<ElemNode>) -> Node {
486    node1("log", x)
487}
488
489/// Base-2 logarithm helper. Expects exactly 1 child.
490pub fn log2(x: impl Into<ElemNode>) -> Node {
491    node1("log2", x)
492}
493
494/// Ceiling helper. Expects exactly 1 child.
495pub fn ceil(x: impl Into<ElemNode>) -> Node {
496    node1("ceil", x)
497}
498
499/// Floor helper. Expects exactly 1 child.
500pub fn floor(x: impl Into<ElemNode>) -> Node {
501    node1("floor", x)
502}
503
504/// Round helper. Expects exactly 1 child.
505pub fn round(x: impl Into<ElemNode>) -> Node {
506    node1("round", x)
507}
508
509/// Square root helper. Expects exactly 1 child.
510pub fn sqrt(x: impl Into<ElemNode>) -> Node {
511    node1("sqrt", x)
512}
513
514/// Exponential helper. Expects exactly 1 child.
515pub fn exp(x: impl Into<ElemNode>) -> Node {
516    node1("exp", x)
517}
518
519/// Absolute value helper. Expects exactly 1 child.
520pub fn abs(x: impl Into<ElemNode>) -> Node {
521    node1("abs", x)
522}
523
524/// Less-than helper. Expects exactly 2 children.
525pub fn le(left: impl Into<ElemNode>, right: impl Into<ElemNode>) -> Node {
526    node2("le", left, right)
527}
528
529/// Less-than-or-equal helper. Expects exactly 2 children.
530pub fn leq(left: impl Into<ElemNode>, right: impl Into<ElemNode>) -> Node {
531    node2("leq", left, right)
532}
533
534/// Greater-than helper. Expects exactly 2 children.
535pub fn ge(left: impl Into<ElemNode>, right: impl Into<ElemNode>) -> Node {
536    node2("ge", left, right)
537}
538
539/// Greater-than-or-equal helper. Expects exactly 2 children.
540pub fn geq(left: impl Into<ElemNode>, right: impl Into<ElemNode>) -> Node {
541    node2("geq", left, right)
542}
543
544/// Power helper. Expects exactly 2 children.
545pub fn pow(left: impl Into<ElemNode>, right: impl Into<ElemNode>) -> Node {
546    node2("pow", left, right)
547}
548
549/// Equality helper. Expects exactly 2 children.
550pub fn eq(left: impl Into<ElemNode>, right: impl Into<ElemNode>) -> Node {
551    node2("eq", left, right)
552}
553
554/// Logical `and` helper. Expects exactly 2 children.
555pub fn and(left: impl Into<ElemNode>, right: impl Into<ElemNode>) -> Node {
556    node2("and", left, right)
557}
558
559/// Logical `or` helper. Expects exactly 2 children.
560pub fn or(left: impl Into<ElemNode>, right: impl Into<ElemNode>) -> Node {
561    node2("or", left, right)
562}
563
564/// Addition helper. Expects one or more children.
565pub fn add(items: impl IntoNodeList) -> Node {
566    fold_node(items, "add")
567}
568
569/// Subtraction helper. Expects one or more children.
570pub fn sub(items: impl IntoNodeList) -> Node {
571    fold_node(items, "sub")
572}
573
574/// Multiplication helper. Expects one or more children.
575pub fn mul(items: impl IntoNodeList) -> Node {
576    fold_node(items, "mul")
577}
578
579/// Division helper. Expects one or more children.
580pub fn div(items: impl IntoNodeList) -> Node {
581    fold_node(items, "div")
582}
583
584/// Modulo helper. Expects exactly 2 children.
585pub fn r#mod(left: impl Into<ElemNode>, right: impl Into<ElemNode>) -> Node {
586    node2("mod", left, right)
587}
588
589/// Minimum helper. Expects exactly 2 children.
590pub fn min(left: impl Into<ElemNode>, right: impl Into<ElemNode>) -> Node {
591    node2("min", left, right)
592}
593
594/// Maximum helper. Expects exactly 2 children.
595pub fn max(left: impl Into<ElemNode>, right: impl Into<ElemNode>) -> Node {
596    node2("max", left, right)
597}
598
599/// Alias for the upstream `in` helper.
600pub fn identity(props: Value, x: Option<Node>) -> Node {
601    match x {
602        Some(x) => Node::new("in", props, vec![x]),
603        None => Node::new("in", props, vec![]),
604    }
605}
606
607/// Rust-friendly alias for `in`.
608pub fn r#in(props: Value, x: Option<Node>) -> Node {
609    identity(props, x)
610}
611
612/// Band-limited cycle oscillator helper. Expects 1 child: frequency or rate.
613pub fn cycle(rate: impl Into<ElemNode>) -> Node {
614    sin(mul([const_(2.0 * std::f64::consts::PI), phasor(rate)]))
615}
616
617/// Pulse train helper. Expects 1 child: frequency or rate.
618pub fn train(rate: impl Into<ElemNode>) -> Node {
619    le(phasor(rate), const_(0.5))
620}
621
622/// Saw oscillator helper. Expects 1 child: frequency or rate.
623pub fn saw(rate: impl Into<ElemNode>) -> Node {
624    sub([mul([const_(2.0), phasor(rate)]), const_(1.0)])
625}
626
627/// Square oscillator helper. Expects 1 child: frequency or rate.
628pub fn square(rate: impl Into<ElemNode>) -> Node {
629    sub([mul([const_(2.0), train(rate)]), const_(1.0)])
630}
631
632/// Triangle oscillator helper. Expects 1 child: frequency or rate.
633pub fn triangle(rate: impl Into<ElemNode>) -> Node {
634    mul([const_(2.0), sub([const_(0.5), abs(saw(rate))])])
635}
636
637/// Band-limited polyBLEP saw oscillator. Expects 1 child: frequency or rate.
638pub fn blepsaw(rate: impl Into<ElemNode>) -> Node {
639    node1("blepsaw", rate)
640}
641
642/// Band-limited polyBLEP square oscillator. Expects 1 child: frequency or rate.
643pub fn blepsquare(rate: impl Into<ElemNode>) -> Node {
644    node1("blepsquare", rate)
645}
646
647/// Band-limited polyBLEP triangle oscillator. Expects 1 child: frequency or rate.
648pub fn bleptriangle(rate: impl Into<ElemNode>) -> Node {
649    node1("bleptriangle", rate)
650}
651
652/// White noise helper. Optional `props` are forwarded unchanged.
653pub fn noise(props: Option<Value>) -> Node {
654    sub([mul([const_(2.0), rand(props)]), const_(1.0)])
655}
656
657/// Pink noise helper. Optional `props` are forwarded unchanged.
658pub fn pinknoise(props: Option<Value>) -> Node {
659    pink(noise(props))
660}
661
662/// Milliseconds to samples. Expects 1 child: a time value in milliseconds.
663pub fn ms2samps(t: impl Into<ElemNode>) -> Node {
664    let t = resolve(t);
665    mul([sr(), div([t, const_(1000.0)])])
666}
667
668/// Time constant to pole. Expects 1 child: a time constant in seconds.
669pub fn tau2pole(t: impl Into<ElemNode>) -> Node {
670    let t = resolve(t);
671    exp(div([const_(-1.0), mul([t, sr()])]))
672}
673
674/// Decibels to gain. Expects 1 child: a decibel value.
675pub fn db2gain(db: impl Into<ElemNode>) -> Node {
676    let db = resolve(db);
677    pow(const_(10.0), mul([db, const_(1.0 / 20.0)]))
678}
679
680/// Gain to decibels. Expects 1 child: a linear gain value.
681pub fn gain2db(gain: impl Into<ElemNode>) -> Node {
682    let gain = resolve(gain);
683    select(
684        ge(gain.clone(), const_(0.0)),
685        max(const_(-120.0), mul([const_(20.0), log(gain)])),
686        const_(-120.0),
687    )
688}
689
690/// Linear select helper. Expects 3 children: gate, a, and b.
691pub fn select(g: impl Into<ElemNode>, a: impl Into<ElemNode>, b: impl Into<ElemNode>) -> Node {
692    let g = resolve(g);
693    let a = resolve(a);
694    let b = resolve(b);
695    add([mul([g.clone(), a]), mul([sub([const_(1.0), g]), b])])
696}
697
698/// Hann window helper. Expects 1 child: the phase or normalized position.
699pub fn hann(t: impl Into<ElemNode>) -> Node {
700    let t = resolve(t);
701    mul([
702        const_(0.5),
703        sub([
704            const_(1.0),
705            cos(mul([const_(2.0 * std::f64::consts::PI), t])),
706        ]),
707    ])
708}
709
710/// One-pole smoothing. Expects 2 children: pole coefficient and input signal.
711pub fn smooth(p: impl Into<ElemNode>, x: impl Into<ElemNode>) -> Node {
712    let p = resolve(p);
713    let x = resolve(x);
714    pole(p.clone(), mul([sub([const_(1.0), p]), x]))
715}
716
717/// 20ms smoothing helper. Expects 1 child: the input signal.
718pub fn sm(x: impl Into<ElemNode>) -> Node {
719    smooth(tau2pole(const_(0.02)), x)
720}
721
722/// Simple one-zero filter. Expects 3 children: b0, b1, and input signal.
723pub fn zero(b0: impl Into<ElemNode>, b1: impl Into<ElemNode>, x: impl Into<ElemNode>) -> Node {
724    let b0 = resolve(b0);
725    let b1 = resolve(b1);
726    let x = resolve(x);
727    sub([mul([b0, x.clone()]), mul([b1, z(x)])])
728}
729
730/// DC blocking filter. Expects 1 child: the input signal.
731pub fn dcblock(x: impl Into<ElemNode>) -> Node {
732    let x = resolve(x);
733    pole(const_(0.995), zero(const_(1.0), const_(1.0), x))
734}
735
736/// Direct form 1 helper. Expects 4 children: b0, b1, a1, and input signal.
737pub fn df11(
738    b0: impl Into<ElemNode>,
739    b1: impl Into<ElemNode>,
740    a1: impl Into<ElemNode>,
741    x: impl Into<ElemNode>,
742) -> Node {
743    let b0 = resolve(b0);
744    let b1 = resolve(b1);
745    let a1 = resolve(a1);
746    let x = resolve(x);
747    pole(a1, zero(b0, b1, x))
748}
749
750/// Lowpass filter. `props` should carry mode metadata.
751/// Expects 3 children: cutoff, resonance/Q, and input signal.
752pub fn lowpass(fc: impl Into<ElemNode>, q: impl Into<ElemNode>, x: impl Into<ElemNode>) -> Node {
753    let fc = resolve(fc);
754    let q = resolve(q);
755    let x = resolve(x);
756    svf(serde_json::json!({ "mode": "lowpass" }), fc, q, x)
757}
758
759/// Highpass filter. `props` should carry mode metadata.
760/// Expects 3 children: cutoff, resonance/Q, and input signal.
761pub fn highpass(fc: impl Into<ElemNode>, q: impl Into<ElemNode>, x: impl Into<ElemNode>) -> Node {
762    let fc = resolve(fc);
763    let q = resolve(q);
764    let x = resolve(x);
765    svf(serde_json::json!({ "mode": "highpass" }), fc, q, x)
766}
767
768/// Bandpass filter. `props` should carry mode metadata.
769/// Expects 3 children: cutoff, resonance/Q, and input signal.
770pub fn bandpass(fc: impl Into<ElemNode>, q: impl Into<ElemNode>, x: impl Into<ElemNode>) -> Node {
771    let fc = resolve(fc);
772    let q = resolve(q);
773    let x = resolve(x);
774    svf(serde_json::json!({ "mode": "bandpass" }), fc, q, x)
775}
776
777/// Notch filter. `props` should carry mode metadata.
778/// Expects 3 children: cutoff, resonance/Q, and input signal.
779pub fn notch(fc: impl Into<ElemNode>, q: impl Into<ElemNode>, x: impl Into<ElemNode>) -> Node {
780    let fc = resolve(fc);
781    let q = resolve(q);
782    let x = resolve(x);
783    svf(serde_json::json!({ "mode": "notch" }), fc, q, x)
784}
785
786/// Allpass filter. `props` should carry mode metadata.
787/// Expects 3 children: cutoff, resonance/Q, and input signal.
788pub fn allpass(fc: impl Into<ElemNode>, q: impl Into<ElemNode>, x: impl Into<ElemNode>) -> Node {
789    let fc = resolve(fc);
790    let q = resolve(q);
791    let x = resolve(x);
792    svf(serde_json::json!({ "mode": "allpass" }), fc, q, x)
793}
794
795/// Peak EQ filter. `props` should carry mode metadata.
796/// Expects 4 children: cutoff, resonance/Q, gain in dB, and input signal.
797pub fn peak(
798    fc: impl Into<ElemNode>,
799    q: impl Into<ElemNode>,
800    gain_decibels: impl Into<ElemNode>,
801    x: impl Into<ElemNode>,
802) -> Node {
803    let fc = resolve(fc);
804    let q = resolve(q);
805    let gain_decibels = resolve(gain_decibels);
806    let x = resolve(x);
807    svfshelf(
808        serde_json::json!({ "mode": "peak" }),
809        fc,
810        q,
811        gain_decibels,
812        x,
813    )
814}
815
816/// Low shelf filter. `props` should carry mode metadata.
817/// Expects 4 children: cutoff, resonance/Q, gain in dB, and input signal.
818pub fn lowshelf(
819    fc: impl Into<ElemNode>,
820    q: impl Into<ElemNode>,
821    gain_decibels: impl Into<ElemNode>,
822    x: impl Into<ElemNode>,
823) -> Node {
824    let fc = resolve(fc);
825    let q = resolve(q);
826    let gain_decibels = resolve(gain_decibels);
827    let x = resolve(x);
828    svfshelf(
829        serde_json::json!({ "mode": "lowshelf" }),
830        fc,
831        q,
832        gain_decibels,
833        x,
834    )
835}
836
837/// High shelf filter. `props` should carry mode metadata.
838/// Expects 4 children: cutoff, resonance/Q, gain in dB, and input signal.
839pub fn highshelf(
840    fc: impl Into<ElemNode>,
841    q: impl Into<ElemNode>,
842    gain_decibels: impl Into<ElemNode>,
843    x: impl Into<ElemNode>,
844) -> Node {
845    let fc = resolve(fc);
846    let q = resolve(q);
847    let gain_decibels = resolve(gain_decibels);
848    let x = resolve(x);
849    svfshelf(
850        serde_json::json!({ "mode": "highshelf" }),
851        fc,
852        q,
853        gain_decibels,
854        x,
855    )
856}
857
858/// Pinking filter. Expects 1 child: the source signal.
859pub fn pink(x: impl Into<ElemNode>) -> Node {
860    let x = resolve(x);
861    let clip = |lower: Node, upper: Node, x: Node| min(upper, max(lower, x));
862
863    clip(
864        const_(-1.0),
865        const_(1.0),
866        mul([
867            db2gain(const_(-30.0)),
868            add([
869                pole(const_(0.99765), mul([x.clone(), const_(0.099046)])),
870                pole(const_(0.963), mul([x.clone(), const_(0.2965164)])),
871                pole(const_(0.57), mul([x.clone(), const_(1.0526913)])),
872                mul([const_(0.1848), x]),
873            ]),
874        ]),
875    )
876}
877
878/// el.adsr(a, d, s, r, g)
879///
880/// An exponential ADSR envelope generator, triggered by the gate signal
881/// `g`. When the gate is high (`1`), this generates the ADS phase. When
882/// the gate is low, the R phase.
883///
884/// Expected children:
885/// - Attack time in seconds (number or signal)
886/// - Decay time in seconds (number or signal)
887/// - Sustain amplitude between `0` and `1` (number or signal)
888/// - Release time in seconds (number or signal)
889/// - Gate signal; a pulse train alternating between `0` and `1`
890pub fn adsr(
891    a: impl Into<ElemNode>,
892    d: impl Into<ElemNode>,
893    s: impl Into<ElemNode>,
894    r: impl Into<ElemNode>,
895    g: impl Into<ElemNode>,
896) -> Node {
897    let attack_sec = resolve(a);
898    let decay_sec = resolve(d);
899    let sustain = resolve(s);
900    let release_sec = resolve(r);
901    let gate = resolve(g);
902    let atk_samps = mul([attack_sec.clone(), sr()]);
903    let atk_gate = le(counter(gate.clone()), atk_samps);
904    let target_value = select(
905        gate.clone(),
906        select(atk_gate.clone(), const_(1.0), sustain.clone()),
907        const_(0.0),
908    );
909    let t60 = max(
910        const_(0.0001),
911        select(
912            gate.clone(),
913            select(atk_gate, attack_sec, decay_sec),
914            release_sec,
915        ),
916    );
917    let p = tau2pole(div([t60, const_(6.91)]));
918
919    smooth(p, target_value)
920}
921
922/// Compressor.
923/// Expects 6 children: attack in ms, release in ms, threshold, ratio, sidechain, and input.
924pub fn compress(
925    attack_ms: Node,
926    release_ms: Node,
927    threshold: Node,
928    ratio: Node,
929    sidechain: Node,
930    xn: Node,
931) -> Node {
932    let env = env(
933        tau2pole(mul([const_(0.001), attack_ms])),
934        tau2pole(mul([const_(0.001), release_ms])),
935        sidechain,
936    );
937
938    let env_decibels = gain2db(env);
939    let adjusted_ratio = sub([const_(1.0), div([const_(1.0), ratio])]);
940    let gain = mul([adjusted_ratio, sub([threshold, env_decibels])]);
941    let clean_gain = min(const_(0.0), gain);
942    let compressed_gain = db2gain(clean_gain);
943
944    mul([xn, compressed_gain])
945}
946
947/// Soft-knee compressor.
948/// Expects 7 children: attack in ms, release in ms, threshold, ratio, knee width, sidechain, and input.
949pub fn skcompress(
950    attack_ms: Node,
951    release_ms: Node,
952    threshold: Node,
953    ratio: Node,
954    knee_width: Node,
955    sidechain: Node,
956    xn: Node,
957) -> Node {
958    let env = env(
959        tau2pole(mul([const_(0.001), attack_ms])),
960        tau2pole(mul([const_(0.001), release_ms])),
961        sidechain,
962    );
963
964    let env_decibels = gain2db(env);
965    let lower_knee_bound = sub([threshold.clone(), div([knee_width.clone(), const_(2.0)])]);
966    let upper_knee_bound = add([threshold.clone(), div([knee_width.clone(), const_(2.0)])]);
967    let is_in_soft_knee_range = and(
968        geq(env_decibels.clone(), lower_knee_bound.clone()),
969        leq(env_decibels.clone(), upper_knee_bound.clone()),
970    );
971    let adjusted_ratio = sub([const_(1.0), div([const_(1.0), ratio])]);
972    let gain = select(
973        is_in_soft_knee_range,
974        mul([
975            div([adjusted_ratio.clone(), const_(2.0)]),
976            mul([
977                div([
978                    sub([env_decibels.clone(), lower_knee_bound.clone()]),
979                    knee_width.clone(),
980                ]),
981                sub([lower_knee_bound.clone(), env_decibels.clone()]),
982            ]),
983        ]),
984        mul([adjusted_ratio, sub([threshold, env_decibels])]),
985    );
986    let clean_gain = min(const_(0.0), gain);
987    let compressed_gain = db2gain(clean_gain);
988
989    mul([xn, compressed_gain])
990}
991
992/// Loads a sample from the virtual file system and triggers its playback
993/// on the rising edge of an incoming pulse train. Expects a props arg and
994/// then two children: first the pulse train to trigger playback, and
995/// second a signal which continuously directs the sample's playback rate.
996pub fn sample(props: Value, trigger: impl Into<ElemNode>, rate: impl Into<ElemNode>) -> Node {
997    Node::new("sample", props, vec![resolve(trigger), resolve(rate)])
998}