From 6eb07abd1671d804e45d996f50f2a35ffab64e5f Mon Sep 17 00:00:00 2001 From: Sami Perttu Date: Sun, 13 Oct 2024 19:35:59 +0300 Subject: [PATCH] Update. --- CHANGES.md | 8 +++ FUTURE.md | 1 - README.md | 94 ++++++++++++++++++++++---------- examples/beep.rs | 4 +- examples/keys.rs | 10 +++- src/combinator.rs | 19 +++++++ src/hacker.rs | 50 ++++++++--------- src/hacker32.rs | 50 ++++++++--------- src/math.rs | 16 +++--- src/oscillator.rs | 128 +++++++++++++++++++++++++++++++++++++++++--- src/prelude.rs | 72 ++++++++++++------------- src/setting.rs | 9 ++++ src/wavetable.rs | 35 ++++++++---- tests/test_basic.rs | 16 ++++-- 14 files changed, 365 insertions(+), 147 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 5aafe3f..6187209 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,13 @@ ## Changes +### Version 0.21 (Next Version) + +- New PolyBLEP oscillator opcodes `poly_saw`, `poly_saw_hz`, + `poly_square`, `poly_square_hz`, `poly_pulse` and `poly_pulse_hz`. +- Noise functions now accept seed as `u64` instead of `i64`. +- `sine_phase`, `ramp_phase` and `ramp_hz_phase` opcodes were removed: + there is a new builder notation for setting the initial phase, for example, `sine().phase(0.0)`. + ### Version 0.20 - `Net::chain` is more robust now. diff --git a/FUTURE.md b/FUTURE.md index 30514b1..95ee5a9 100644 --- a/FUTURE.md +++ b/FUTURE.md @@ -5,7 +5,6 @@ This is a list of feature ideas for the future. - What is the best approach to making `Granular` real-time safe. - `AudioUnit` versions of `oversample` and `resample` that accept an inner `AudioUnit`. - Compressor without lookahead. -- Adaptive normalizer without lookahead. - Exponential follower (`follow` is linear). - More physical models. Karplus-Strong exists already; figure out if it could be improved somehow. - Dynamic bypass wrapper that bypasses a node when input and output levels drop low enough. diff --git a/README.md b/README.md index 5969dde..81e7a1d 100644 --- a/README.md +++ b/README.md @@ -271,18 +271,9 @@ The aims of the environments are: - Minimize the number of characters needed to type to express an idiom. - Keep the syntax clean so that a subset of the hacker environment can be parsed straightforwardly as a high-level DSL for quick prototyping. -- Make the syntax usable even to people with no prior exposure to programming. - -### Deterministic Pseudorandom Phase - -FunDSP uses a deterministic pseudorandom phase system for audio generators. -Generator phases are seeded from network structure and node location. + This has been done in the [lapis](https://github.com/tomara-x/lapis) project. -Thus, two identical networks sound identical separately but different when combined. -This means that `noise() | noise()` is a stereo noise source, for example. - -Pseudorandom phase is an attempt to decorrelate different channels of audio. -It is also used to pick sample points for envelopes, contributing to a "warmer" sound. +- Make the syntax usable even to people with no prior exposure to programming. ## Operators @@ -509,7 +500,8 @@ net.pipe_output(sine_id); ``` The overhead of `Net` is the overhead of calling into `Box` -objects. Beyond that, the dynamic versions are roughly as efficient +objects. Static graphs can get optimized more effectively as well by the compiler. +Beyond that, the dynamic versions are roughly as efficient as the static ones. The graph syntax is also available for combining `Net` instances. @@ -559,6 +551,11 @@ net.commit(); Using dynamic networks incurs some overhead so it is an especially good idea to use block processing, which neutralizes it effectively. +It is possible to create cycles with `Net` methods. +If that happens an error is raised, which can be checked with `Net::error`. +If the cycle is removed, the error is cleared. +All nodes get processed anyway. + ### Sequencer The `Sequencer` component mixes together generator nodes dynamically. @@ -604,12 +601,56 @@ Some signals found flowing in audio networks. | Modality | Preferred Units/Range | Notes | | -------------- | ---------------------- | ------------------------------------------ | | frequency | Hz | | -| phase | 0...1 | The wavetable oscillator uses this range. | +| phase | 0...1 | All oscillators use this range. | | time | s | | | audio data | -1...1 | Inner processing may use any range that is convenient. However, only special output formats can store audio data outside this range. | | stereo pan | -1...1 (left to right) | For ergonomy, consider clamping any pan input to this range. | +| pulse width | 0...1 | | | control amount | 0...1 | If there is no natural interpretation of the parameter. | +## Deterministic Pseudorandom Phase + +FunDSP uses a deterministic pseudorandom phase system for audio generators. +Generator phases are seeded from network structure and node location. + +Thus, two identical networks sound identical separately but different when combined. +This means that `noise() | noise()` is a stereo noise source, for example. + +Pseudorandom phase is an attempt to decorrelate different channels of audio. +It is also used to pick sample points for envelopes, contributing to a "warmer" sound. + +## Oscillators + +To override pseudorandom phase in an oscillator with an initial phase of your own (in 0...1), +use the `phase` builder method. For example, in `let mut A = sine_hz(220.0).phase(0.0)` +the (220 Hz) sine wave starts from zero phase (where the value is zero, too). + +To change the initial phase later, use the `phase` setting, for example, +`A.set(Setting::phase(0.5))`. The setting takes effect during the next reset. + +The following table lists the oscillator opcodes. + +| Opcode | Type | Waveform | +| -------------- | ---------------------- | ------------------------------------------ | +| `dsf_saw` | DSF | saw-like +| `dsf_square` | DSF | square-like +| `hammond` | wavetable | Hammond-y waveform, which emphasizes first three partials | +| `organ` | wavetable | organ-y waveform, which emphasizes octave partials | +| `poly_pulse` | PolyBLEP | pulse | +| `poly_saw` | PolyBLEP | saw | +| `poly_square` | PolyBLEP | square | +| `pulse` | wavetable | pulse | +| `saw` | wavetable | saw | +| `sine` | sine | sine | +| `soft_saw` | wavetable | soft saw, which falls off like a triangle wave | +| `square` | wavetable | square | +| `triangle` | wavetable | triangle | + +The wavetable oscillator is bandlimited with pristine quality. +However, unlike the other types it allocates memory in the form of static wavetables. +The DSF oscillator has similar quality but is somewhat expensive to evaluate. +The PolyBLEP oscillator is a fast approximation with fair quality. + ## Working With Waves FunDSP includes a multichannel wave abstraction called `Wave`. @@ -1078,11 +1119,11 @@ The type parameters in the table refer to the hacker preludes. | `biquad(a1, a2, b0, b1, b2)` | 1 | 1 | Arbitrary [biquad filter](https://en.wikipedia.org/wiki/Digital_biquad_filter) with coefficients in normalized form. | | `brown()` | - | 1 | [Brown](https://en.wikipedia.org/wiki/Brownian_noise) noise. | | `branch(x, y)` | `x = y` | `x + y` | Branch into `x` and `y`. Identical with `x ^ y`. | -| `branchi::(f)`| `f` | `U * f` | Branch into `U` nodes from indexed generator `f`. | | `branchf::(f)`| `f` | `U * f` | Branch into `U` nodes from fractional generator `f`, e.g., `\| x \| resonator_hz(xerp(20.0, 20_000.0, x), xerp(5.0, 5_000.0, x))`. | +| `branchi::(f)`| `f` | `U * f` | Branch into `U` nodes from indexed generator `f`. | | `bus(x, y)` | `x = y` | `x = y` | Bus `x` and `y`. Identical with `x & y`. | -| `busi::(f)` | `f` | `f` | Bus together `U` nodes from indexed generator `f`, e.g., `\| i \| mul(i as f32 + 1.0) >> sine()`. | | `busf::(f)` | `f` | `f` | Bus together `U` nodes from fractional generator `f`. | +| `busi::(f)` | `f` | `f` | Bus together `U` nodes from indexed generator `f`, e.g., `\| i \| mul(i as f32 + 1.0) >> sine()`. | | `butterpass()` | 2 (audio, frequency) | 1 | Butterworth lowpass filter (2nd order). | | `butterpass_hz(f)` | 1 | 1 | Butterworth lowpass filter (2nd order) with cutoff frequency `f` Hz. | | `chorus(seed, sep, var, mod)` | 1 | 1 | Chorus effect with LFO seed `seed`, voice separation `sep` seconds, delay variation `var` seconds and LFO modulation frequency `mod` Hz. | @@ -1118,7 +1159,7 @@ The type parameters in the table refer to the hacker preludes. | `feedback(x)` | `x` | `x` | Enclose (single sample) feedback circuit `x` (with equal number of inputs and outputs). | | `feedback2(x, y)` | `x`, `y`| `x`, `y`| Enclose (single sample) feedback circuit `x` (with equal number of inputs and outputs) with extra feedback loop processing `y`. The feedforward path does not include `y`. | | `fir(weights)` | 1 | 1 | FIR filter with the specified weights, for example, `fir((0.5, 0.5))`. | -| `fir3(gain)` | 1 | 1 | Symmetric 3-point FIR calculated from desired `gain` at the Nyquist frequency. | +| `fir3(gain)` | 1 | 1 | Symmetric 3-point FIR calculated from desired amplitude `gain` at the Nyquist frequency (a monotonic lowpass when `gain` < 1). | | `flanger(fb, min_d, max_d, f)`| 1| 1 | Flanger effect with feedback amount `fb`, minimum delay `min_d` seconds, maximum delay `max_d` seconds and delay function `f`, e.g., `\|t\| lerp11(0.01, 0.02, sin_hz(0.1, t))`. | | `fhighpass(shape)` | 3 (audio, frequency, Q) | 1 | Feedback biquad highpass (2nd order) with feedback `shape`, for example, `Softsign(1.0)`. | | `fhighpass_hz(shape, f, q)` | 1 | 1 | Feedback biquad highpass (2nd order) with feedback `shape`, center `f` Hz and Q `q`. | @@ -1195,19 +1236,19 @@ The type parameters in the table refer to the hacker preludes. | `pink()` | - | 1 | [Pink noise](https://en.wikipedia.org/wiki/Pink_noise) source. | | `pinkpass()` | 1 | 1 | Pinking filter (3 dB/octave lowpass). | | `pipe(x, y)` | `x` | `y` | Pipe `x` to `y`. Identical with `x >> y`. | -| `pipei::(f)` | `f` | `f` | Chain `U` nodes from indexed generator `f`. | | `pipef::(f)` | `f` | `f` | Chain `U` nodes from fractional generator `f`. | +| `pipei::(f)` | `f` | `f` | Chain `U` nodes from indexed generator `f`. | | `pluck(f, gain, damping)` | 1 (excitation) | 1 | [Karplus-Strong](https://en.wikipedia.org/wiki/Karplus%E2%80%93Strong_string_synthesis) plucked string oscillator with frequency `f` Hz, `gain` per second (`gain` <= 1) and high frequency `damping` in 0...1. | -| `poly_saw()` | 1 (frequency) | 1 | Bandlimited saw wave oscillator. | -| `poly_saw_hz(f)` | - | 1 | Bandlimited saw wave oscillator with frequency `f` Hz. | -| `poly_square()` | 1 (frequency) | 1 | Bandlimited square wave oscillator. | -| `poly_square_hz(f)` | - | 1 | Bandlimited square wave oscillator with frequency `f` Hz. | +| `poly_pulse()` | 2 (frequency, pulse width) | 1 | Somewhat bandlimited pulse wave oscillator. | +| `poly_pulse_hz(f, w)` | - | 1 | Somewhat bandlimited pulse wave oscillator with frequency `f` Hz and pulse width `w` in 0...1. | +| `poly_saw()` | 1 (frequency) | 1 | Somewhat bandlimited saw wave oscillator. | +| `poly_saw_hz(f)` | - | 1 | Somewhat bandlimited saw wave oscillator with frequency `f` Hz. | +| `poly_square()` | 1 (frequency) | 1 | Somewhat bandlimited square wave oscillator. | +| `poly_square_hz(f)` | - | 1 | Somewhat bandlimited square wave oscillator with frequency `f` Hz. | | `product(x, y)` | `x + y` | `x = y` | Multiply nodes `x` and `y`. Same as `x * y`. | -| `pulse()` | 2 (frequency, duty cycle) | 1 | Bandlimited pulse wave with duty cycle in 0...1. | +| `pulse()` | 2 (frequency, pulse width) | 1 | Bandlimited pulse wave with pulse width in 0...1. | | `ramp()` | 1 (frequency) | 1 | Non-bandlimited ramp (sawtooth) wave in 0...1. | | `ramp_hz(f)` | 0 | 1 | Non-bandlimited ramp (sawtooth) wave in 0...1 with frequency `f` Hz. | -| `ramp_phase(phase)` | 1 (frequency) | 1 | Non-bandlimited ramp (sawtooth) wave in 0...1 with initial `phase` in 0...1. | -| `ramp_hz_phase(f, phase)` | 0 | 1 | Non-bandlimited ramp (sawtooth) wave in 0...1 with frequency `f` Hz and initial `phase` in 0...1. | | `resample(node)` | 1 (speed) | `node` | Resample generator `node` using cubic interpolation at speed obtained from the input, where 1 is the original speed. | | `resonator()` | 3 (audio, frequency, Q) | 1 | Constant-gain bandpass resonator (2nd order). | | `resonator_hz(f, q)` | 1 | 1 | Constant-gain bandpass resonator (2nd order) with center frequency `f` Hz and Q `q`. | @@ -1224,7 +1265,6 @@ The type parameters in the table refer to the hacker preludes. | `shape_fn(f)` | 1 | 1 | Shape signal with waveshaper function `f`, e.g., `tanh`. | | `sine()` | 1 (frequency) | 1 | Sine oscillator. | | `sine_hz(f)` | - | 1 | Sine oscillator at `f` Hz. | -| `sine_phase(p)` | 1 (frequency) | 1 | Sine oscillator with initial phase `p` in 0...1. | | `sink()` | 1 | - | Consume signal. | | `soft_saw()` | 1 (frequency) | 1 | Bandlimited soft saw wave oscillator. | | `soft_saw_hz(f)` | - | 1 | Bandlimited soft saw wave oscillator at `f` Hz. | @@ -1232,12 +1272,12 @@ The type parameters in the table refer to the hacker preludes. | `square()` | 1 (frequency) | 1 | Bandlimited square wave oscillator. | | `square_hz(f)` | - | 1 | Bandlimited square wave oscillator at frequency `f` Hz. | | `stack(x, y)` | `x + y` | `x + y` | Stack `x` and `y`. Identical with `x \| y`. | -| `stacki::(f)` | `U * f` | `U * f` | Stack `U` nodes from indexed generator `f`. | | `stackf::(f)` | `U * f` | `U * f` | Stack `U` nodes from fractional generator `f`, e.g., `\| x \| delay(xerp(0.1, 0.2, x))`. | +| `stacki::(f)` | `U * f` | `U * f` | Stack `U` nodes from indexed generator `f`. | | `sub(x)` | `x` | `x` | Subtract constant `x` from signal. | | `sum(x, y)` | `x + y` | `x = y` | Add nodes `x` and `y`. Same as `x + y`. | -| `sumi::(f)` | `U * f` | `f` | Sum `U` nodes from indexed generator `f`. | | `sumf::(f)` | `U * f` | `f` | Sum `U` nodes from fractional generator `f`, e.g., `\| x \| delay(xerp(0.1, 0.2, x))`. | +| `sumi::(f)` | `U * f` | `f` | Sum `U` nodes from indexed generator `f`. | | `tap(min_delay, max_delay)` | 2 (audio, delay) | 1 | Tapped delay line with cubic interpolation. All times are in seconds. | | `tap_linear(min_delay, max_delay)` | 2 (audio, delay) | 1 | Tapped delay line with linear interpolation. All times are in seconds. | | `thru(x)` | `x` | `x` inputs | Pass through missing outputs. Same as `!x`. | diff --git a/examples/beep.rs b/examples/beep.rs index e35e5cd..324abd7 100644 --- a/examples/beep.rs +++ b/examples/beep.rs @@ -45,8 +45,8 @@ where // Pulse wave. //let c = lfo(|t| { // let pitch = 220.0; - // let duty = lerp11(0.01, 0.99, sin_hz(0.05, t)); - // (pitch, duty) + // let width = lerp11(0.01, 0.99, sin_hz(0.05, t)); + // (pitch, width) //}) >> pulse(); //let c = zero() >> pluck(220.0, 0.8, 0.8); diff --git a/examples/keys.rs b/examples/keys.rs index 982ae5e..af70901 100644 --- a/examples/keys.rs +++ b/examples/keys.rs @@ -22,6 +22,7 @@ enum Waveform { Noise, PolySaw, PolySquare, + PolyPulse, } #[derive(Debug, PartialEq)] @@ -206,7 +207,7 @@ where net, waveform: Waveform::Saw, filter: Filter::None, - vibrato_amount: 0.5, + vibrato_amount: 0.25, chorus_amount, reverb_amount, room_size: room_size as f64, @@ -272,6 +273,7 @@ impl eframe::App for State { ui.selectable_value(&mut self.waveform, Waveform::Noise, "Noise"); ui.selectable_value(&mut self.waveform, Waveform::PolySaw, "PolySaw"); ui.selectable_value(&mut self.waveform, Waveform::PolySquare, "PolySquare"); + ui.selectable_value(&mut self.waveform, Waveform::PolyPulse, "PolyPulse"); }); ui.separator(); @@ -485,6 +487,10 @@ impl eframe::App for State { )), Waveform::PolySaw => Net::wrap(Box::new(pitch >> poly_saw() * 0.06)), Waveform::PolySquare => Net::wrap(Box::new(pitch >> poly_square() * 0.06)), + Waveform::PolyPulse => Net::wrap(Box::new( + (pitch | lfo(move |t| lerp11(0.01, 0.99, sin_hz(0.1, t)))) + >> poly_pulse() * 0.06, + )), }; let filter = match self.filter { Filter::None => Net::wrap(Box::new(pass())), @@ -516,7 +522,7 @@ impl eframe::App for State { >> fresonator(Softsign(1.01)), )), }; - let mut note = Box::new(waveform >> filter); + let mut note = Box::new(waveform >> filter >> dcblock()); // Give the note its own random seed. note.ping(false, AttoHash::new(self.rnd.u64())); // Insert new note. We set the end time to infinity initially, diff --git a/src/combinator.rs b/src/combinator.rs index 72837e5..ee91acd 100644 --- a/src/combinator.rs +++ b/src/combinator.rs @@ -6,6 +6,7 @@ use super::audionode::*; use super::buffer::*; use super::math::*; +use super::setting::*; use super::signal::*; use super::*; use core::ops::{Add, BitAnd, BitOr, BitXor, Mul, Neg, Shr, Sub}; @@ -211,6 +212,10 @@ impl An { self.0.process(size, input, output); } #[inline] + pub fn set(&mut self, setting: Setting) { + self.0.set(setting); + } + #[inline] pub fn route(&mut self, input: &SignalFrame, frequency: f64) -> SignalFrame { self.0.route(input, frequency) } @@ -246,6 +251,20 @@ impl An { pub fn filter_stereo(&mut self, x: f32, y: f32) -> (f32, f32) { self.0.filter_stereo(x, y) } + + /// This builder method sets oscillator initial phase in 0...1, + /// overriding pseudorandom phase. + /// + /// ### Example (Square Wave At 110 Hz With Initial Phase 0.5) + /// ``` + /// use fundsp::hacker::*; + /// let oscillator = square_hz(110.0).phase(0.5); + /// ``` + pub fn phase(mut self, phase: f32) -> Self { + self.set(Setting::phase(phase)); + self.reset(); + self + } } impl Neg for An diff --git a/src/hacker.rs b/src/hacker.rs index ce1ed3b..f988035 100644 --- a/src/hacker.rs +++ b/src/hacker.rs @@ -347,13 +347,6 @@ pub fn sine_hz(f: f32) -> An, Sine>> { constant(f) >> sine() } -/// Sine oscillator with initial `phase` in 0...1. -/// - Input 0: frequency (Hz) -/// - Output 0: sine wave -pub fn sine_phase(phase: f32) -> An> { - An(Sine::with_phase(phase)) -} - /// Ramp generator with output in 0...1. Not bandlimited. /// - Input 0: repetition frequency (Hz) /// - Output 0: ramp phase in 0...1 @@ -367,19 +360,6 @@ pub fn ramp_hz(f: f32) -> An, Ramp>> { constant(f) >> ramp() } -/// Ramp generator with output in 0...1, starting from initial phase `phase` in 0...1. -/// - Input 0: repetition frequency (Hz) -/// - Output 0: ramp phase in 0...1 -pub fn ramp_phase(phase: f32) -> An> { - An(Ramp::with_phase(phase)) -} - -/// Ramp generator with output in 0...1 at fixed frequency `f` Hz. -/// - Output 0: ramp phase in 0...1 -pub fn ramp_hz_phase(f: f32, phase: f32) -> An, Ramp>> { - constant(f) >> ramp_phase(phase) -} - /// Rossler dynamical system oscillator. /// - Input 0: frequency. The Rossler oscillator exhibits peaks at multiples of this frequency. /// - Output 0: system output @@ -2194,7 +2174,7 @@ pub fn bandrez_q(q: f32) -> An, Constant>, Rez An { super::prelude::pulse() @@ -2656,28 +2636,48 @@ pub fn fresonator_hz( super::prelude::fresonator_hz(shape, center as f64, q as f64) } -/// PolyBLEP saw wave oscillator. A fast bandlimited saw wave algorithm. +/// PolyBLEP saw wave oscillator. +/// A fast, fairly bandlimited saw wave algorithm. /// - Input 0: frequency (Hz) /// - Output 0: saw wave pub fn poly_saw() -> An> { An(PolySaw::new()) } -/// PolyBLEP saw wave oscillator at `f` Hz. A fast bandlimited saw wave algorithm. +/// PolyBLEP saw wave oscillator at `f` Hz. +/// A fast, fairly bandlimited saw wave algorithm. /// - Output 0: saw wave pub fn poly_saw_hz(f: f32) -> An, PolySaw>> { dc(f) >> poly_saw() } -/// PolyBLEP square wave oscillator. A fast bandlimited square wave algorithm. +/// PolyBLEP square wave oscillator. +/// A fast, fairly bandlimited square wave algorithm. /// - Input 0: frequency (Hz) /// - Output 0: square wave pub fn poly_square() -> An> { An(PolySquare::new()) } -/// PolyBLEP square wave oscillator at `f` Hz. A fast bandlimited square wave algorithm. +/// PolyBLEP square wave oscillator at `f` Hz. +/// A fast, fairly bandlimited square wave algorithm. /// - Output 0: square wave pub fn poly_square_hz(f: f32) -> An, PolySquare>> { dc(f) >> poly_square() } + +/// PolyBLEP pulse wave oscillator. +/// A fast, fairly bandlimited pulse wave algorithm. +/// - Input 0: frequency (Hz) +/// - Input 1: pulse width in 0...1 +/// - Output 0: pulse wave +pub fn poly_pulse() -> An> { + An(PolyPulse::new()) +} + +/// PolyBLEP pulse wave oscillator at `f` Hz with pulse `width` in 0...1. +/// A fast, fairly bandlimited pulse wave algorithm. +/// - Output 0: pulse wave +pub fn poly_pulse_hz(f: f32, width: f32) -> An, PolyPulse>> { + dc((f, width)) >> poly_pulse() +} diff --git a/src/hacker32.rs b/src/hacker32.rs index c80905d..4b8b273 100644 --- a/src/hacker32.rs +++ b/src/hacker32.rs @@ -347,13 +347,6 @@ pub fn sine_hz(f: f32) -> An, Sine>> { constant(f) >> sine() } -/// Sine oscillator with initial `phase` in 0...1. -/// - Input 0: frequency (Hz) -/// - Output 0: sine wave -pub fn sine_phase(phase: f32) -> An> { - An(Sine::with_phase(phase)) -} - /// Ramp generator with output in 0...1. Not bandlimited. /// - Input 0: repetition frequency (Hz) /// - Output 0: ramp phase in 0...1 @@ -367,19 +360,6 @@ pub fn ramp_hz(f: f32) -> An, Ramp>> { constant(f) >> ramp() } -/// Ramp generator with output in 0...1, starting from initial phase `phase` in 0...1. -/// - Input 0: repetition frequency (Hz) -/// - Output 0: ramp phase in 0...1 -pub fn ramp_phase(phase: f32) -> An> { - An(Ramp::with_phase(phase)) -} - -/// Ramp generator with output in 0...1 at fixed frequency `f` Hz. -/// - Output 0: ramp phase in 0...1 -pub fn ramp_hz_phase(f: f32, phase: f32) -> An, Ramp>> { - constant(f) >> ramp_phase(phase) -} - /// Rossler dynamical system oscillator. /// - Input 0: frequency. The Rossler oscillator exhibits peaks at multiples of this frequency. /// - Output 0: system output @@ -2194,7 +2174,7 @@ pub fn bandrez_q(q: f32) -> An, Constant>, Rez An { super::prelude::pulse() @@ -2656,28 +2636,48 @@ pub fn fresonator_hz( super::prelude::fresonator_hz(shape, center, q) } -/// PolyBLEP saw wave oscillator. A fast bandlimited saw wave algorithm. +/// PolyBLEP saw wave oscillator. +/// A fast, fairly bandlimited saw wave algorithm. /// - Input 0: frequency (Hz) /// - Output 0: saw wave pub fn poly_saw() -> An> { An(PolySaw::new()) } -/// PolyBLEP saw wave oscillator at `f` Hz. A fast bandlimited saw wave algorithm. +/// PolyBLEP saw wave oscillator at `f` Hz. +/// A fast, fairly bandlimited saw wave algorithm. /// - Output 0: saw wave pub fn poly_saw_hz(f: f32) -> An, PolySaw>> { dc(f) >> poly_saw() } -/// PolyBLEP square wave oscillator. A fast bandlimited square wave algorithm. +/// PolyBLEP square wave oscillator. +/// A fast, fairly bandlimited square wave algorithm. /// - Input 0: frequency (Hz) /// - Output 0: square wave pub fn poly_square() -> An> { An(PolySquare::new()) } -/// PolyBLEP square wave oscillator at `f` Hz. A fast bandlimited square wave algorithm. +/// PolyBLEP square wave oscillator at `f` Hz. +/// A fast, fairly bandlimited square wave algorithm. /// - Output 0: square wave pub fn poly_square_hz(f: f32) -> An, PolySquare>> { dc(f) >> poly_square() } + +/// PolyBLEP pulse wave oscillator. +/// A fast, fairly bandlimited pulse wave algorithm. +/// - Input 0: frequency (Hz) +/// - Input 1: pulse width in 0...1 +/// - Output 0: pulse wave +pub fn poly_pulse() -> An> { + An(PolyPulse::new()) +} + +/// PolyBLEP pulse wave oscillator at `f` Hz with pulse `width` in 0...1. +/// A fast, fairly bandlimited pulse wave algorithm. +/// - Output 0: pulse wave +pub fn poly_pulse_hz(f: f32, width: f32) -> An, PolyPulse>> { + dc((f, width)) >> poly_pulse() +} diff --git a/src/math.rs b/src/math.rs index df80e84..e1cb47e 100644 --- a/src/math.rs +++ b/src/math.rs @@ -669,13 +669,13 @@ where /// Each integer cell is an interpolation segment. /// Easing function `ease` (for example, `smooth3`) can be asymmetric: /// `(r, f)` employs `r` for rising and `f` for falling segments. -pub fn ease_noise(ease: impl SegmentInterpolator, seed: i64, x: T) -> T { +pub fn ease_noise(ease: impl SegmentInterpolator, seed: u64, x: T) -> T { let fx = floor(x); let dx = x - fx; let ix = fx.to_i64(); - fn get_point(seed: i64, i: i64) -> T { - AttoHash::new(seed as u64).hash(i as u64).hash11() + fn get_point(seed: u64, i: i64) -> T { + AttoHash::new(seed).hash(i as u64).hash11() } let y1 = get_point(seed, ix); @@ -713,13 +713,13 @@ pub fn spline_noise(seed: u64, x: T) -> T { /// Sums octaves (`octaves` > 0) of spline noise. /// The lowest frequency of the noise is 1, with each successive octave doubling in frequency. /// Roughness (`roughness` > 0) is the multiplicative weighting of successive octaves. For example, 0.5. -pub fn fractal_noise(seed: i64, octaves: i64, roughness: T, x: T) -> T { +pub fn fractal_noise(seed: u64, octaves: i64, roughness: T, x: T) -> T { assert!(octaves > 0); let mut octave_weight = T::one(); let mut total_weight = T::zero(); let mut frequency = T::one(); let mut result = T::zero(); - let mut rnd = funutd::Rnd::from_u64(seed as u64); + let mut rnd = funutd::Rnd::from_u64(seed); for _octave in 0..octaves { // Employ a pseudorandom offset for each octave. let octave_x = x * frequency + T::from_f32(rnd.f32()); @@ -737,7 +737,7 @@ pub fn fractal_noise(seed: i64, octaves: i64, roughness: T, x: T) -> T /// Roughness (`roughness` > 0) is the multiplicative weighting of successive octaves. For example, 0.5. pub fn fractal_ease_noise( ease: impl SegmentInterpolator, - seed: i64, + seed: u64, octaves: i64, roughness: T, x: T, @@ -747,11 +747,11 @@ pub fn fractal_ease_noise( let mut total_weight = T::zero(); let mut frequency = T::one(); let mut result = T::zero(); - let mut rnd = funutd::Rnd::from_u64(seed as u64); + let mut rnd = funutd::Rnd::from_u64(seed); for _octave in 0..octaves { // Employ a pseudorandom offset for each octave. let octave_x = x * frequency + T::from_f32(rnd.f32()); - result += octave_weight * ease_noise(ease.clone(), rnd.i64(), octave_x); + result += octave_weight * ease_noise(ease.clone(), rnd.u64(), octave_x); total_weight += octave_weight; octave_weight *= roughness; frequency *= T::new(2); diff --git a/src/oscillator.rs b/src/oscillator.rs index cf67d06..2c9f78a 100644 --- a/src/oscillator.rs +++ b/src/oscillator.rs @@ -85,6 +85,12 @@ impl AudioNode for Sine { self.process_remainder(size, input, output); } + fn set(&mut self, setting: Setting) { + if let Parameter::Phase(phase) = setting.parameter() { + self.initial_phase = Some(F::from_f32(*phase)); + } + } + fn set_hash(&mut self, hash: u64) { self.hash = hash; self.reset(); @@ -117,6 +123,7 @@ pub struct Dsf> { harmonic_spacing: f32, sample_duration: f32, hash: u64, + initial_phase: Option, _marker: PhantomData, } @@ -128,6 +135,7 @@ impl> Dsf { harmonic_spacing, sample_duration: 0.0, hash: 0, + initial_phase: None, _marker: PhantomData, }; node.reset(); @@ -155,7 +163,10 @@ impl> AudioNode for Dsf { type Outputs = typenum::U1; fn reset(&mut self) { - self.phase = rnd1(self.hash) as f32; + self.phase = match self.initial_phase { + Some(phase) => phase, + None => convert(rnd1(self.hash)), + }; } fn set_sample_rate(&mut self, sample_rate: f64) { @@ -179,8 +190,10 @@ impl> AudioNode for Dsf { } fn set(&mut self, setting: Setting) { - if let Parameter::Roughness(roughness) = setting.parameter() { - self.set_roughness(*roughness); + match setting.parameter() { + Parameter::Roughness(roughness) => self.set_roughness(*roughness), + Parameter::Phase(phase) => self.initial_phase = Some(*phase), + _ => (), } } @@ -478,6 +491,12 @@ impl AudioNode for Ramp { [phase].into() } + fn set(&mut self, setting: Setting) { + if let Parameter::Phase(phase) = setting.parameter() { + self.initial_phase = Some(F::from_f32(*phase)); + } + } + fn set_hash(&mut self, hash: u64) { self.hash = hash; self.reset(); @@ -489,6 +508,7 @@ impl AudioNode for Ramp { } /// PolyBLEP function with phase `t` in 0...1 and phase increment `dt`. +#[inline] fn polyblep(t: F, dt: F) -> F { if t < dt { let z = t / dt; @@ -501,7 +521,8 @@ fn polyblep(t: F, dt: F) -> F { } } -/// PolyBLEP saw oscillator. A fast bandlimited algorithm for a saw wave. +/// PolyBLEP saw oscillator. +/// A fast, fairly bandlimited algorithm for a saw wave. /// - Input 0: frequency (Hz). /// - Output 0: saw waveform in -1...1. #[derive(Default, Clone)] @@ -560,6 +581,12 @@ impl AudioNode for PolySaw { [value.to_f32()].into() } + fn set(&mut self, setting: Setting) { + if let Parameter::Phase(phase) = setting.parameter() { + self.initial_phase = Some(F::from_f32(*phase)); + } + } + fn set_hash(&mut self, hash: u64) { self.hash = hash; self.reset(); @@ -570,9 +597,10 @@ impl AudioNode for PolySaw { } } -/// PolyBLEP square oscillator. A fast bandlimited algorithm for a square wave. +/// PolyBLEP square oscillator. +/// A fast, fairly bandlimited algorithm for a square wave. /// - Input 0: frequency (Hz). -/// - Output 0: saw waveform in -1...1. +/// - Output 0: square waveform in -1...1. #[derive(Default, Clone)] pub struct PolySquare { phase: F, @@ -630,11 +658,97 @@ impl AudioNode for PolySquare { } else { -F::one() }; - let half = phase + F::from_f32(0.5); + let half = phase - F::from_f32(0.5); let value = square + polyblep(phase, delta) - polyblep(half - half.floor(), delta); [value.to_f32()].into() } + fn set(&mut self, setting: Setting) { + if let Parameter::Phase(phase) = setting.parameter() { + self.initial_phase = Some(F::from_f32(*phase)); + } + } + + fn set_hash(&mut self, hash: u64) { + self.hash = hash; + self.reset(); + } + + fn route(&mut self, input: &SignalFrame, _frequency: f64) -> SignalFrame { + super::signal::Routing::Arbitrary(0.0).route(input, self.outputs()) + } +} + +/// PolyBLEP pulse oscillator. +/// A fast, fairly bandlimited algorithm for a pulse wave. +/// - Input 0: frequency (Hz). +/// - Input 1: pulse width in 0...1. +/// - Output 0: pulse waveform in -1...1. +#[derive(Default, Clone)] +pub struct PolyPulse { + phase: F, + sample_duration: F, + hash: u64, + initial_phase: Option, +} + +impl PolyPulse { + /// Create oscillator. + pub fn new() -> Self { + let mut osc = Self::default(); + osc.reset(); + osc.set_sample_rate(DEFAULT_SR); + osc + } + /// Create oscillator with initial phase in 0...1. + pub fn with_phase(initial_phase: f32) -> Self { + let mut osc = Self { + phase: F::zero(), + sample_duration: F::zero(), + hash: 0, + initial_phase: Some(F::from_f32(initial_phase)), + }; + osc.reset(); + osc.set_sample_rate(DEFAULT_SR); + osc + } +} + +impl AudioNode for PolyPulse { + const ID: u64 = 97; + type Inputs = typenum::U2; + type Outputs = typenum::U1; + + fn reset(&mut self) { + self.phase = match self.initial_phase { + Some(phase) => phase, + None => convert(rnd1(self.hash)), + }; + } + + fn set_sample_rate(&mut self, sample_rate: f64) { + self.sample_duration = convert(1.0 / sample_rate); + } + + #[inline] + fn tick(&mut self, input: &Frame) -> Frame { + let phase = self.phase; + let delta = F::from_f32(input[0]) * self.sample_duration; + let width = F::from_f32(input[1]); + self.phase += delta; + self.phase -= self.phase.floor(); + let square = if phase < width { F::one() } else { -F::one() }; + let half = phase - width; + let value = square + polyblep(phase, delta) - polyblep(half - half.floor(), delta); + [value.to_f32()].into() + } + + fn set(&mut self, setting: Setting) { + if let Parameter::Phase(phase) = setting.parameter() { + self.initial_phase = Some(F::from_f32(*phase)); + } + } + fn set_hash(&mut self, hash: u64) { self.hash = hash; self.reset(); diff --git a/src/prelude.rs b/src/prelude.rs index 300d537..952fd30 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -347,13 +347,6 @@ pub fn sine_hz(f: f32) -> An, Sine>> { constant(f) >> sine() } -/// Sine oscillator with initial `phase` in 0...1. -/// - Input 0: frequency (Hz) -/// - Output 0: sine wave -pub fn sine_phase(phase: f32) -> An> { - An(Sine::with_phase(phase)) -} - /// Ramp generator with output in 0...1. Not bandlimited. /// - Input 0: repetition frequency (Hz) /// - Output 0: ramp phase in 0...1 @@ -367,19 +360,6 @@ pub fn ramp_hz(f: f32) -> An, Ramp>> { constant(f) >> ramp() } -/// Ramp generator with output in 0...1, starting from initial phase `phase` in 0...1. -/// - Input 0: repetition frequency (Hz) -/// - Output 0: ramp phase in 0...1 -pub fn ramp_phase(phase: f32) -> An> { - An(Ramp::with_phase(phase)) -} - -/// Ramp generator with output in 0...1 at fixed frequency `f` Hz. -/// - Output 0: ramp phase in 0...1 -pub fn ramp_hz_phase(f: f32, phase: f32) -> An, Ramp>> { - constant(f) >> ramp_phase(phase) -} - /// Rossler dynamical system oscillator. /// - Input 0: frequency. The Rossler oscillator exhibits peaks at multiples of this frequency. /// - Output 0: system output @@ -2602,7 +2582,7 @@ pub fn bandrez_q(q: F) -> An, Constant>, R /// Pulse wave oscillator. /// - Input 0: frequency in Hz -/// - Input 1: pulse duty cycle in 0...1 +/// - Input 1: pulse width in 0...1 /// - Output 0: pulse wave pub fn pulse() -> An { An(PulseWave::new()) @@ -2655,9 +2635,9 @@ pub fn wavech_at( /// Mono chorus, 5 voices. For stereo, stack two of these using different seed values. /// `seed`: LFO seed. -/// `separation`: base voice separation in seconds (for example, 0.015). -/// `variation`: delay variation in seconds (for example, 0.005). -/// `mod_frequency`: delay modulation frequency (for example, 0.2). +/// `separation`: base voice separation in seconds (for example, 0.0). +/// `variation`: delay variation in seconds (for example, 0.02). +/// `mod_frequency`: delay modulation frequency (for example, 0.3). /// - Input 0: audio. /// - Output 0: chorused audio, including original signal. /// @@ -2674,30 +2654,30 @@ pub fn chorus( ) -> An> { (pass() & (pass() - | lfo(move |t| { + | An(Envelope::new(0.01, move |t| { ( lerp11( separation, separation + variation, - spline_noise(seed, t * mod_frequency), + fractal_noise(seed, 8, 0.45, t * mod_frequency), ), lerp11( separation * 2.0, separation * 2.0 + variation, - spline_noise(hash1(seed), t * (mod_frequency + 0.02)), + fractal_noise(hash1(seed), 8, 0.45, t * (mod_frequency + 0.02)), ), lerp11( separation * 3.0, separation * 3.0 + variation, - spline_noise(hash2(seed), t * (mod_frequency + 0.04)), + fractal_noise(hash2(seed), 8, 0.45, t * (mod_frequency + 0.04)), ), lerp11( separation * 4.0, separation * 4.0 + variation, - spline_noise(hash1(seed ^ 0xfedcba), t * (mod_frequency + 0.06)), + fractal_noise(hash1(seed ^ 0xfedcba), 8, 0.45, t * (mod_frequency + 0.06)), ), ) - })) + }))) >> multitap::(separation, separation * 4.0 + variation)) * dc(0.2) } @@ -2724,7 +2704,7 @@ pub fn flanger f32 + Clone + Send + Sync>( pass() & feedback2( (pass() | lfo(delay_f)) >> tap(minimum_delay, maximum_delay), - shape(Tanh(feedback_amount.to_f32())), + shape(Tanh(feedback_amount)), ) } @@ -2746,7 +2726,7 @@ pub fn phaser f32 + Clone + Send + Sync>( pass() & feedback( (pass() | lfo(move |t| lerp(2.0, 20.0, clamp01(phase_f(t))))) - >> pipei::(|_i| (pass() | add(0.05)) >> !allpole::()) + >> pipei::(|_i| add((0.0, 0.1)) >> !allpole::()) >> (mul(feedback_amount) | sink()), ) } @@ -3104,28 +3084,48 @@ pub fn fresonator_hz( An(filter) } -/// PolyBLEP saw wave oscillator. A fast bandlimited saw wave algorithm. +/// PolyBLEP saw wave oscillator. +/// A fast, fairly bandlimited saw wave algorithm. /// - Input 0: frequency (Hz) /// - Output 0: saw wave pub fn poly_saw() -> An> { An(PolySaw::new()) } -/// PolyBLEP saw wave oscillator at `f` Hz. A fast bandlimited saw wave algorithm. +/// PolyBLEP saw wave oscillator at `f` Hz. +/// A fast, fairly bandlimited saw wave algorithm. /// - Output 0: saw wave pub fn poly_saw_hz(f: f32) -> An, PolySaw>> { dc(f) >> poly_saw() } -/// PolyBLEP square wave oscillator. A fast bandlimited square wave algorithm. +/// PolyBLEP square wave oscillator. +/// A fast, fairly bandlimited square wave algorithm. /// - Input 0: frequency (Hz) /// - Output 0: square wave pub fn poly_square() -> An> { An(PolySquare::new()) } -/// PolyBLEP square wave oscillator at `f` Hz. A fast bandlimited square wave algorithm. +/// PolyBLEP square wave oscillator at `f` Hz. +/// A fast, fairly bandlimited square wave algorithm. /// - Output 0: square wave pub fn poly_square_hz(f: f32) -> An, PolySquare>> { dc(f) >> poly_square() } + +/// PolyBLEP pulse wave oscillator. +/// A fast, fairly bandlimited pulse wave algorithm. +/// - Input 0: frequency (Hz) +/// - Input 1: pulse width in 0...1 +/// - Output 0: pulse wave +pub fn poly_pulse() -> An> { + An(PolyPulse::new()) +} + +/// PolyBLEP pulse wave oscillator at `f` Hz with pulse `width` in 0...1. +/// A fast, fairly bandlimited pulse wave algorithm. +/// - Output 0: pulse wave +pub fn poly_pulse_hz(f: f32, width: f32) -> An, PolyPulse>> { + dc((f, width)) >> poly_pulse() +} diff --git a/src/setting.rs b/src/setting.rs index 91ae1f0..51731bc 100644 --- a/src/setting.rs +++ b/src/setting.rs @@ -45,6 +45,8 @@ pub enum Parameter { Pan(f32), /// Set attack and release times in seconds. AttackRelease(f32, f32), + /// Oscillator initial phase in 0...1. + Phase(f32), } /// Address specifies location to apply setting in a graph. @@ -150,6 +152,13 @@ impl Setting { address: ArrayVec::new(), } } + /// Create setting for oscillator initial phase in 0...1. + pub fn phase(phase: f32) -> Self { + Self { + parameter: Parameter::Phase(phase), + address: ArrayVec::new(), + } + } /// Add indexed address to setting. pub fn index(mut self, index: usize) -> Self { self.address.push(Address::Index(index)); diff --git a/src/wavetable.rs b/src/wavetable.rs index 12afc50..6d29df0 100644 --- a/src/wavetable.rs +++ b/src/wavetable.rs @@ -5,6 +5,7 @@ use super::buffer::*; use super::combinator::*; use super::math::*; use super::prelude::pass; +use super::setting::*; use super::signal::*; use super::typenum::*; use super::*; @@ -89,8 +90,8 @@ impl Wavetable { /// Create new wavetable. `min_pitch` and `max_pitch` are the minimum /// and maximum base frequencies in Hz (for example, 20.0 and 20_000.0). /// `tables_per_octave` is the number of wavetables per octave - /// (for example, 4.0). `phase(i)` is the phase of the `i`th partial. - /// `amplitude(p, i)` is the amplitude of the `i`th partial with base frequency `p`. + /// (for example, 4.0). `phase(i)` is the phase of the `i`th partial in 0...1. + /// `amplitude(p, i)` is the (relative) amplitude of the `i`th partial with base frequency `p`. pub fn new( min_pitch: f64, max_pitch: f64, @@ -258,10 +259,11 @@ where N: Size, { table: Arc, - /// Phase in 0...1. + /// Current phase in 0...1. phase: f32, - /// Initial phase in 0...1, seeded via pseudorandom phase system. - initial_phase: f32, + hash: u64, + /// Optional initial phase in 0...1 that overrides pseudorandom phase. + initial_phase: Option, /// Previously used transposition table. table_hint: usize, sample_rate: f32, @@ -277,7 +279,8 @@ where WaveSynth { table, phase: 0.0, - initial_phase: 0.0, + hash: 0, + initial_phase: None, table_hint: 0, sample_rate: DEFAULT_SR as f32, sample_duration: 1.0 / DEFAULT_SR as f32, @@ -295,7 +298,10 @@ where type Outputs = N; fn reset(&mut self) { - self.phase = self.initial_phase; + self.phase = match self.initial_phase { + Some(phase) => phase, + None => convert(rnd1(self.hash)), + }; } fn set_sample_rate(&mut self, sample_rate: f64) { @@ -304,8 +310,8 @@ where } fn set_hash(&mut self, hash: u64) { - self.initial_phase = super::math::rnd1(hash) as f32; - self.phase = self.initial_phase; + self.hash = hash; + self.reset(); } #[inline] @@ -349,6 +355,12 @@ where self.process_remainder(size, input, output); } + fn set(&mut self, setting: Setting) { + if let Parameter::Phase(phase) = setting.parameter() { + self.initial_phase = Some(*phase); + } + } + fn route(&mut self, input: &SignalFrame, _frequency: f64) -> SignalFrame { Routing::Arbitrary(0.0).route(input, self.outputs()) } @@ -429,7 +441,7 @@ impl AudioNode for PhaseSynth { /// Pulse wave oscillator. /// - Input 0: frequency in Hz -/// - Input 1: pulse duty cycle in 0...1 +/// - Input 1: pulse width in 0...1 /// - Output 0: pulse wave #[derive(Clone)] pub struct PulseWave { @@ -472,6 +484,9 @@ impl AudioNode for PulseWave { fn process(&mut self, size: usize, input: &BufferRef, output: &mut BufferMut) { self.pulse.process(size, input, output); } + fn set(&mut self, setting: Setting) { + self.pulse.left_mut().left_mut().left_mut().set(setting); + } fn route(&mut self, input: &SignalFrame, frequency: f64) -> SignalFrame { self.pulse.route(input, frequency) } diff --git a/tests/test_basic.rs b/tests/test_basic.rs index 8a11b60..2f2013b 100644 --- a/tests/test_basic.rs +++ b/tests/test_basic.rs @@ -181,7 +181,9 @@ fn test_basic() { ); check_wave(dc((110.0, 220.0)) >> multipass() >> -stackf::(|f| (f - 0.5) * sine())); check_wave( - dc((110.0, 220.0, 440.0, 880.0)) >> multipass() >> (sink() | -sine() | sink() | sine()), + dc((110.0, 220.0, 440.0, 880.0)) + >> multipass() + >> (sink() | -sine().phase(0.0) | sink() | sine()), ); check_wave(dc((110.0, 220.0)) >> declick_s(0.1) + pass() >> (saw() ^ dsf_square_r(0.9))); check_wave( @@ -199,12 +201,14 @@ fn test_basic() { | ((mls() | dc(880.0)) >> !lowshelf_q(1.0, 0.5) >> highshelf_q(2.0, 2.0)), ); check_wave( - (noise() | dc(440.0)) >> pipei::(|_| !lowpass_q(1.0)) >> highpass_q(1.0) + (square_hz(110.0).phase(0.25) | dc(440.0)) + >> pipei::(|_| !lowpass_q(1.0)) + >> highpass_q(1.0) | ((mls() | dc(880.0)) >> !bandpass_q(1.0) >> notch_q(2.0)), ); check_wave( dc((440.0, 880.0)) >> multisplit::() >> sumi::(|_| saw() * 0.2) - | noise(), + | saw_hz(220.0).phase(0.5), ); check_wave( dc((440.0, 880.0)) >> multisplit::() >> multijoin::() >> (sine() | sine()), @@ -226,7 +230,7 @@ fn test_basic() { noise() >> fresonator_hz(Atan(0.5), 500.0, 50.0) | noise() >> fhighpass_hz(Softsign(0.2), 2000.0, 2.0), ); - check_wave(dc(440.0) >> ramp() | dc(-220.0) >> ramp_phase(0.0)); + check_wave(dc(440.0) >> ramp() | ramp_hz(-220.0).phase(0.5)); check_wave_big(Box::new(dc((110.0, 0.5)) >> pulse() * 0.2 >> delay(0.1))); check_wave_big(Box::new(envelope(|t| exp(-t * 10.0)))); @@ -297,6 +301,10 @@ fn test_basic() { check_wave((noise() | envelope(|t| spline_noise(1, t * 10.0))) >> panner()); check_wave(impulse::()); check_wave(poly_saw_hz(440.0) | poly_square_hz(4400.0)); + check_wave(poly_saw_hz(550.0).phase(0.75) | poly_square_hz(5500.0).phase(0.5)); + check_wave( + dc((660.0, 0.1)) >> poly_pulse().phase(0.75) | poly_pulse_hz(6600.0, 0.9).phase(0.9), + ); let dc42 = Net::wrap(Box::new(dc(42.))); let dcs = dc42.clone() | dc42;