Skip to content

Commit

Permalink
PolyBLEP oscillators.
Browse files Browse the repository at this point in the history
  • Loading branch information
SamiPerttu committed Oct 8, 2024
1 parent 34d9171 commit e72837c
Show file tree
Hide file tree
Showing 8 changed files with 250 additions and 3 deletions.
1 change: 0 additions & 1 deletion FUTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -1196,6 +1198,10 @@ The type parameters in the table refer to the hacker preludes.
| `pipei::<U, _, _>(f)` | `f` | `f` | Chain `U` nodes from indexed generator `f`. |
| `pipef::<U, _, _>(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. |
Expand Down
8 changes: 7 additions & 1 deletion examples/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ enum Waveform {
Pulse,
Pluck,
Noise,
PolySaw,
PolySquare,
}

#[derive(Debug, PartialEq)]
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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())),
Expand Down
26 changes: 26 additions & 0 deletions src/hacker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2655,3 +2655,29 @@ pub fn fresonator_hz<S: Shape>(
) -> An<FixedFbBiquad<f64, ResonatorBiquad<f64>, 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<PolySaw<f64>> {
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<Pipe<Constant<U1>, PolySaw<f64>>> {
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<PolySquare<f64>> {
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<Pipe<Constant<U1>, PolySquare<f64>>> {
dc(f) >> poly_square()
}
26 changes: 26 additions & 0 deletions src/hacker32.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2655,3 +2655,29 @@ pub fn fresonator_hz<S: Shape>(
) -> An<FixedFbBiquad<f32, ResonatorBiquad<f32>, 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<PolySaw<f32>> {
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<Pipe<Constant<U1>, PolySaw<f32>>> {
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<PolySquare<f32>> {
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<Pipe<Constant<U1>, PolySquare<f32>>> {
dc(f) >> poly_square()
}
159 changes: 158 additions & 1 deletion src/oscillator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,7 @@ impl<F: Float> Ramp<F> {
}

impl<F: Float> AudioNode for Ramp<F> {
const ID: u64 = 21;
const ID: u64 = 94;
type Inputs = typenum::U1;
type Outputs = typenum::U1;

Expand Down Expand Up @@ -487,3 +487,160 @@ impl<F: Float> AudioNode for Ramp<F> {
super::signal::Routing::Arbitrary(0.0).route(input, self.outputs())
}
}

/// PolyBLEP function with phase `t` in 0...1 and phase increment `dt`.
fn polyblep<F: Float>(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<F: Float> {
phase: F,
sample_duration: F,
hash: u64,
initial_phase: Option<F>,
}

impl<F: Float> PolySaw<F> {
/// 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<F: Float> AudioNode for PolySaw<F> {
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<f32, Self::Inputs>) -> Frame<f32, Self::Outputs> {
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<F: Float> {
phase: F,
sample_duration: F,
hash: u64,
initial_phase: Option<F>,
}

impl<F: Float> PolySquare<F> {
/// 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<F: Float> AudioNode for PolySquare<F> {
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<f32, Self::Inputs>) -> Frame<f32, Self::Outputs> {
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())
}
}
26 changes: 26 additions & 0 deletions src/prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3103,3 +3103,29 @@ pub fn fresonator_hz<F: Real, S: Shape>(
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<F: Float>() -> An<PolySaw<F>> {
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: Float>(f: f32) -> An<Pipe<Constant<U1>, PolySaw<F>>> {
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<F: Float>() -> An<PolySquare<F>> {
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: Float>(f: f32) -> An<Pipe<Constant<U1>, PolySquare<F>>> {
dc(f) >> poly_square()
}
1 change: 1 addition & 0 deletions tests/test_basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ fn test_basic() {

check_wave((noise() | envelope(|t| spline_noise(1, t * 10.0))) >> panner());
check_wave(impulse::<U2>());
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;
Expand Down

0 comments on commit e72837c

Please sign in to comment.