diff --git a/FUTURE.md b/FUTURE.md index 4b886ad..30514b1 100644 --- a/FUTURE.md +++ b/FUTURE.md @@ -14,7 +14,6 @@ This is a list of feature ideas for the future. - Improve or replace the drum sounds in the `sound` module. - Real-time safe sound server that uses `cpal`. It could have a static set of read/write channels for rendering audio, including hardware channels. - Conversion of graphs into a graphical form. Format associative operator chains appropriately. -- Interpreter for simple FunDSP expressions. - Expand `README.md` into a book. - Time stretching / pitch shifting algorithm. - FFT convolution engine and HRTF support. diff --git a/README.md b/README.md index dcb08be..5969dde 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,8 @@ To discuss FunDSP and other topics, come hang out with us at the [bevy_fundsp](https://github.com/harudagondi/bevy_fundsp) integrates FunDSP into the [Bevy](https://bevyengine.org/) game engine. +[lapis](https://github.com/tomara-x/lapis) is an interpreter for FunDSP expressions. + [midi_fundsp](https://github.com/gjf2a/midi_fundsp) enables the easy creation of live synthesizer software using FunDSP for synthesis. @@ -1196,6 +1198,10 @@ The type parameters in the table refer to the hacker preludes. | `pipei::(f)` | `f` | `f` | Chain `U` nodes from indexed generator `f`. | | `pipef::(f)` | `f` | `f` | Chain `U` nodes from fractional 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. | | `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. | | `ramp()` | 1 (frequency) | 1 | Non-bandlimited ramp (sawtooth) wave in 0...1. | diff --git a/examples/keys.rs b/examples/keys.rs index 21ff304..982ae5e 100644 --- a/examples/keys.rs +++ b/examples/keys.rs @@ -20,6 +20,8 @@ enum Waveform { Pulse, Pluck, Noise, + PolySaw, + PolySquare, } #[derive(Debug, PartialEq)] @@ -262,12 +264,14 @@ impl eframe::App for State { ui.selectable_value(&mut self.waveform, Waveform::Square, "Square"); ui.selectable_value(&mut self.waveform, Waveform::Triangle, "Triangle"); ui.selectable_value(&mut self.waveform, Waveform::Organ, "Organ"); + ui.selectable_value(&mut self.waveform, Waveform::Hammond, "Hammond"); }); ui.horizontal(|ui| { - ui.selectable_value(&mut self.waveform, Waveform::Hammond, "Hammond"); ui.selectable_value(&mut self.waveform, Waveform::Pulse, "Pulse"); ui.selectable_value(&mut self.waveform, Waveform::Pluck, "Pluck"); 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.separator(); @@ -479,6 +483,8 @@ impl eframe::App for State { >> resonator() >> shape(Adaptive::new(0.1, Atan(0.05))) * 0.5, )), + Waveform::PolySaw => Net::wrap(Box::new(pitch >> poly_saw() * 0.06)), + Waveform::PolySquare => Net::wrap(Box::new(pitch >> poly_square() * 0.06)), }; let filter = match self.filter { Filter::None => Net::wrap(Box::new(pass())), diff --git a/src/hacker.rs b/src/hacker.rs index bb29716..ce1ed3b 100644 --- a/src/hacker.rs +++ b/src/hacker.rs @@ -2655,3 +2655,29 @@ pub fn fresonator_hz( ) -> An, S>> { super::prelude::fresonator_hz(shape, center as f64, q as f64) } + +/// PolyBLEP saw wave oscillator. A fast 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. +/// - 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. +/// - 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. +/// - Output 0: square wave +pub fn poly_square_hz(f: f32) -> An, PolySquare>> { + dc(f) >> poly_square() +} diff --git a/src/hacker32.rs b/src/hacker32.rs index d211388..c80905d 100644 --- a/src/hacker32.rs +++ b/src/hacker32.rs @@ -2655,3 +2655,29 @@ pub fn fresonator_hz( ) -> An, S>> { super::prelude::fresonator_hz(shape, center, q) } + +/// PolyBLEP saw wave oscillator. A fast 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. +/// - 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. +/// - 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. +/// - Output 0: square wave +pub fn poly_square_hz(f: f32) -> An, PolySquare>> { + dc(f) >> poly_square() +} diff --git a/src/oscillator.rs b/src/oscillator.rs index ce8605a..cf67d06 100644 --- a/src/oscillator.rs +++ b/src/oscillator.rs @@ -455,7 +455,7 @@ impl Ramp { } impl AudioNode for Ramp { - const ID: u64 = 21; + const ID: u64 = 94; type Inputs = typenum::U1; type Outputs = typenum::U1; @@ -487,3 +487,160 @@ impl AudioNode for Ramp { super::signal::Routing::Arbitrary(0.0).route(input, self.outputs()) } } + +/// PolyBLEP function with phase `t` in 0...1 and phase increment `dt`. +fn polyblep(t: F, dt: F) -> F { + if t < dt { + let z = t / dt; + z + z - z * z - F::one() + } else if t > F::one() - dt { + let z = (t - F::one()) / dt; + z + z + z * z + F::one() + } else { + F::zero() + } +} + +/// PolyBLEP saw oscillator. A fast bandlimited algorithm for a saw wave. +/// - Input 0: frequency (Hz). +/// - Output 0: saw waveform in -1...1. +#[derive(Default, Clone)] +pub struct PolySaw { + phase: F, + sample_duration: F, + hash: u64, + initial_phase: Option, +} + +impl PolySaw { + /// 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 PolySaw { + const ID: u64 = 95; + type Inputs = typenum::U1; + 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; + self.phase += delta; + self.phase -= self.phase.floor(); + let value = F::new(2) * phase - F::one() - polyblep(phase, delta); + [value.to_f32()].into() + } + + 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 square oscillator. A fast bandlimited algorithm for a square wave. +/// - Input 0: frequency (Hz). +/// - Output 0: saw waveform in -1...1. +#[derive(Default, Clone)] +pub struct PolySquare { + phase: F, + sample_duration: F, + hash: u64, + initial_phase: Option, +} + +impl PolySquare { + /// 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 PolySquare { + const ID: u64 = 96; + type Inputs = typenum::U1; + 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; + self.phase += delta; + self.phase -= self.phase.floor(); + let square = if phase < F::from_f32(0.5) { + F::one() + } else { + -F::one() + }; + 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_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()) + } +} diff --git a/src/prelude.rs b/src/prelude.rs index 47d0fdb..300d537 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -3103,3 +3103,29 @@ pub fn fresonator_hz( filter.set_center_q(center, q); An(filter) } + +/// PolyBLEP saw wave oscillator. A fast 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. +/// - 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. +/// - 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. +/// - Output 0: square wave +pub fn poly_square_hz(f: f32) -> An, PolySquare>> { + dc(f) >> poly_square() +} diff --git a/tests/test_basic.rs b/tests/test_basic.rs index 67128e1..8a11b60 100644 --- a/tests/test_basic.rs +++ b/tests/test_basic.rs @@ -296,6 +296,7 @@ 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)); let dc42 = Net::wrap(Box::new(dc(42.))); let dcs = dc42.clone() | dc42;