Skip to content

Commit

Permalink
Update.
Browse files Browse the repository at this point in the history
  • Loading branch information
SamiPerttu committed Oct 13, 2024
1 parent e72837c commit 6eb07ab
Show file tree
Hide file tree
Showing 14 changed files with 365 additions and 147 deletions.
8 changes: 8 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
1 change: 0 additions & 1 deletion FUTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
94 changes: 67 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -509,7 +500,8 @@ net.pipe_output(sine_id);
```

The overhead of `Net` is the overhead of calling into `Box<dyn AudioUnit>`
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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -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::<U, _, _>(f)`| `f` | `U * f` | Branch into `U` nodes from indexed generator `f`. |
| `branchf::<U, _, _>(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::<U, _, _>(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::<U, _, _>(f)` | `f` | `f` | Bus together `U` nodes from indexed generator `f`, e.g., `\| i \| mul(i as f32 + 1.0) >> sine()`. |
| `busf::<U, _, _>(f)` | `f` | `f` | Bus together `U` nodes from fractional generator `f`. |
| `busi::<U, _, _>(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. |
Expand Down Expand Up @@ -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`. |
Expand Down Expand Up @@ -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::<U, _, _>(f)` | `f` | `f` | Chain `U` nodes from indexed generator `f`. |
| `pipef::<U, _, _>(f)` | `f` | `f` | Chain `U` nodes from fractional generator `f`. |
| `pipei::<U, _, _>(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`. |
Expand All @@ -1224,20 +1265,19 @@ 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. |
| `split::<U>()` | 1 | `U` | Split signal into `U` channels. |
| `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::<U, _, _>(f)` | `U * f` | `U * f` | Stack `U` nodes from indexed generator `f`. |
| `stackf::<U, _, _>(f)` | `U * f` | `U * f` | Stack `U` nodes from fractional generator `f`, e.g., `\| x \| delay(xerp(0.1, 0.2, x))`. |
| `stacki::<U, _, _>(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::<U, _, _>(f)` | `U * f` | `f` | Sum `U` nodes from indexed generator `f`. |
| `sumf::<U, _, _>(f)` | `U * f` | `f` | Sum `U` nodes from fractional generator `f`, e.g., `\| x \| delay(xerp(0.1, 0.2, x))`. |
| `sumi::<U, _, _>(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`. |
Expand Down
4 changes: 2 additions & 2 deletions examples/beep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
10 changes: 8 additions & 2 deletions examples/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ enum Waveform {
Noise,
PolySaw,
PolySquare,
PolyPulse,
}

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

Expand Down Expand Up @@ -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())),
Expand Down Expand Up @@ -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,
Expand Down
19 changes: 19 additions & 0 deletions src/combinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -211,6 +212,10 @@ impl<X: AudioNode> An<X> {
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)
}
Expand Down Expand Up @@ -246,6 +251,20 @@ impl<X: AudioNode> An<X> {
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<X> Neg for An<X>
Expand Down
Loading

0 comments on commit 6eb07ab

Please sign in to comment.