Skip to content

Commit

Permalink
Update.
Browse files Browse the repository at this point in the history
  • Loading branch information
SamiPerttu committed Jul 3, 2024
1 parent 3fdeb05 commit e16287b
Show file tree
Hide file tree
Showing 10 changed files with 216 additions and 15 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
Cargo.lock
.vscode
/.cargo
examples/input/target
examples/input/Cargo.lock
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ generic-array = "0.14.7"
numeric-array = { version = "0.5.2", default-features = false }
dyn-clone = "1.0.17"
libm = "0.2.8"
wide = "0.7.24"
wide = { version = "0.7.25", default-features = false }
num-complex = { version = "0.4.6", default-features = false, features = ["libm"] }
tinyvec = { version = "1.6.0", features = ["alloc"] }
hashbrown = "0.14.5"
Expand Down
9 changes: 7 additions & 2 deletions FUTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@ This is a list of feature ideas for the future.
- 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.
- Improve basic effects implemented in graph notation such as `reverb` (e.g., early reflections), `chorus`, `flanger` and `phaser`.
- Improve basic effects implemented in graph notation such as `chorus`, `flanger` and `phaser`.
- More sound generators in the `gen` module.
- Improve or replace the drum sounds in the library.
- 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.
- Fading nodes in and out when replacing a node in `Net`.
- Make a more flexible node replacement method for `Net` where the number of inputs and outputs could be changed.
- Text-to-speech engine.
- Resampler with sinc interpolation.
- Denormal mitigation on `x86` targets. This would mean enabling the flush-denormals-to-zero flag in feedback loops.
- Ring buffer component for transporting audio data from another thread.
44 changes: 36 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Add `fundsp` to your `Cargo.toml` as a dependency.

```rust
[dependencies]
fundsp = "0.18.0"
fundsp = "0.18.1"
```

The `files` feature is enabled by default. It adds support for
Expand All @@ -66,7 +66,7 @@ Audio file reading and writing is not available in `no_std`.

```rust
[dependencies]
fundsp = { version = "0.18.0", default-features = false }
fundsp = { version = "0.18.1", default-features = false }
```

## Graph Notation
Expand Down Expand Up @@ -309,7 +309,8 @@ An alternative to some operators are functions available in the preludes.
Some of them have multiple combination versions;
these work only for multiples of the same type of node, with statically (at compile time) set number of nodes.

The nodes are allocated inline in all functions.
The nodes are allocated inline in all functions, as are any
inner buffers needed for block processing.

| Operator Form | Function Form | Multiple Combination Forms |
| ------------- | --------------- | -------------------------- |
Expand All @@ -327,6 +328,12 @@ The nodes are allocated inline in all functions.

![](operators.png "FunDSP Graph Operators")

Each cyan dot in the diagram above can contain an arbitrary number of channels, including zero.

In the `AudioNode` system the number of channels is determined statically, at compile time,
while in the `AudioUnit` system (using `Net`) the number of channels can be
decided at runtime.

### Broadcasting

Arithmetic operators are applied to outputs channelwise.
Expand Down Expand Up @@ -499,6 +506,10 @@ net.pipe(dc_id, sine_id);
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
as the static ones.

The graph syntax is also available for combining `Net` instances.
Connectivity checks are then deferred to runtime.

Expand Down Expand Up @@ -539,8 +550,8 @@ net = net >> peak_hz(1000.0, 1.0);
net.commit();
```

Using dynamic networks incurs overhead so it is an especially good idea
to use block processing to amortize the overhead.
Using dynamic networks incurs some overhead so it is an especially good idea
to use block processing, which amortizes it effectively.

### Sequencer

Expand All @@ -561,10 +572,11 @@ can be set. The sequencer returns an `ID` that can be used for later edits to th

```rust
// Add a new event with start time 1.0 seconds and end time 2.0 seconds.
// Fade-in time is 0.1 seconds, while the fade-out time is 0.2 seconds.
// This returns an `EventId`.
let id1 = sequencer.push(1.0, 2.0, Fade::Smooth, 0.1, 0.1, Box::new(noise() | noise()));
let id1 = sequencer.push(1.0, 2.0, Fade::Smooth, 0.1, 0.2, Box::new(noise() | noise()));
// Add a new event that starts immediately and plays indefinitely.
let id2 = sequencer.push_relative(0.0, f64::INFINITY, Fade::Smooth, 0.1, 0.1, Box::new(pink() | pink()));
let id2 = sequencer.push_relative(0.0, f64::INFINITY, Fade::Smooth, 0.1, 0.2, Box::new(pink() | pink()));
```

For use as a dynamic mixer, the sequencer can be split into a frontend and a backend.
Expand All @@ -575,7 +587,7 @@ The frontend is for adding and editing events, and the real-time safe backend re
let mut backend = sequencer.backend();
// Now we can insert the backend into, for example, a `Net`.
// Later we can use the frontend to create events and make edits to them; the end time and fade-out time
// can be changed. Here we start fading out the event immediately.
// can be changed. Here we start fading out the event immediately with an envelope duration of 0.1 seconds.
sequencer.edit_relative(id2, 0.0, 0.1);
```

Expand Down Expand Up @@ -1365,6 +1377,22 @@ These math functions have the shape of an easing function.

---

For example, if we wish to interpolate a transition, then interpolation, de-interpolation and
re-interpolation combined with easing functions is handy. This example defines
an inaudible tone suppression function for pure tones, falling smoothly from one at 20 kHz to zero at the default Nyquist
frequency of 22.05 kHz.

```rust
use fundsp::hacker::*;
fn pure_tone_amp(hz: f32) -> f32 { lerp(1.0, 0.0, smooth5(clamp01(delerp(20_000.0, 22_050.0, hz)))) }
```

The above is an example of re-interpolation, which is de-interpolation followed with interpolation. First we recover, using `delerp` and clamping, an interpolation value in 0...1 (this is de-interpolation). Then we interpolate that value smoothly using `lerp` with the `smooth5` ease, in the desired final range of 1 to 0.

For exponential interpolation and de-interpolation, such as might be often used to deal with amplitudes or frequencies, use the `xerp` and `dexerp` functions.

---

### Noise Functions

![](fractal_noise.png "fractal_noise example")
Expand Down
8 changes: 4 additions & 4 deletions examples/beep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ fn main() {

fn run<T>(device: &cpal::Device, config: &cpal::StreamConfig) -> Result<(), anyhow::Error>
where
T: SizedSample + FromSample<f64>,
T: SizedSample + FromSample<f32>,
{
let sample_rate = config.sample_rate.0 as f64;
let channels = config.channels as usize;
Expand Down Expand Up @@ -128,12 +128,12 @@ where

fn write_data<T>(output: &mut [T], channels: usize, next_sample: &mut dyn FnMut() -> (f32, f32))
where
T: SizedSample + FromSample<f64>,
T: SizedSample + FromSample<f32>,
{
for frame in output.chunks_mut(channels) {
let sample = next_sample();
let left = T::from_sample(sample.0 as f64);
let right: T = T::from_sample(sample.1 as f64);
let left = T::from_sample(sample.0);
let right: T = T::from_sample(sample.1);

for (channel, sample) in frame.iter_mut().enumerate() {
if channel & 1 == 0 {
Expand Down
11 changes: 11 additions & 0 deletions examples/input/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "input-example"
version = "0.1.0"
edition = "2021"
publish = false

[dependencies]
fundsp = { path = "../.." }
crossbeam-channel = "0.5.13"
cpal = "0.15.3"

151 changes: 151 additions & 0 deletions examples/input/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
//! Process (stereo) input and play the result (in stereo).
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::{FromSample, SizedSample};
use fundsp::hacker32::*;

use crossbeam_channel::{bounded, Receiver, Sender};

#[derive(Clone)]
pub struct InputNode {
receiver: Receiver<(f32, f32)>,
}

impl InputNode {
pub fn new(receiver: Receiver<(f32, f32)>) -> Self {
InputNode { receiver }
}
}

impl AudioNode for InputNode {
const ID: u64 = 87;
type Inputs = U0;
type Outputs = U2;

#[inline]
fn tick(&mut self, _input: &Frame<f32, Self::Inputs>) -> Frame<f32, Self::Outputs> {
let (left, right) = self.receiver.try_recv().unwrap_or((0.0, 0.0));
[left, right].into()
}
}

fn main() {
// Sender / receiver for left and right channels (stereo mic).
let (sender, receiver) = bounded(4096);

let host = cpal::default_host();
// Start input.
let in_device = host.default_input_device().unwrap();
let in_config = in_device.default_input_config().unwrap();
match in_config.sample_format() {
cpal::SampleFormat::F32 => run_in::<f32>(&in_device, &in_config.into(), sender),
cpal::SampleFormat::I16 => run_in::<i16>(&in_device, &in_config.into(), sender),
cpal::SampleFormat::U16 => run_in::<u16>(&in_device, &in_config.into(), sender),
format => eprintln!("Unsupported sample format: {}", format),
}
// Start output.
let out_device = host.default_output_device().unwrap();
let out_config = out_device.default_output_config().unwrap();
match out_config.sample_format() {
cpal::SampleFormat::F32 => run_out::<f32>(&out_device, &out_config.into(), receiver),
cpal::SampleFormat::I16 => run_out::<i16>(&out_device, &out_config.into(), receiver),
cpal::SampleFormat::U16 => run_out::<u16>(&out_device, &out_config.into(), receiver),
format => eprintln!("Unsupported sample format: {}", format),
}
println!("Processing stereo input to stereo output.");
loop {
std::thread::sleep(std::time::Duration::from_secs(1));
}
}

fn run_in<T>(device: &cpal::Device, config: &cpal::StreamConfig, sender: Sender<(f32, f32)>)
where
T: SizedSample,
f32: FromSample<T>,
{
let channels = config.channels as usize;
let err_fn = |err| eprintln!("an error occurred on stream: {}", err);
let stream = device.build_input_stream(
config,
move |data: &[T], _: &cpal::InputCallbackInfo| read_data(data, channels, sender.clone()),
err_fn,
None,
);
if let Ok(stream) = stream {
if let Ok(()) = stream.play() {
std::mem::forget(stream);
}
}
println!("Input stream built.");
}

fn read_data<T>(input: &[T], channels: usize, sender: Sender<(f32, f32)>)
where
T: SizedSample,
f32: FromSample<T>,
{
for frame in input.chunks(channels) {
let mut left = 0.0;
let mut right = 0.0;
for (channel, sample) in frame.iter().enumerate() {
if channel & 1 == 0 {
left = sample.to_sample::<f32>();
} else {
right = sample.to_sample::<f32>();
}
}
if let Ok(()) = sender.try_send((left, right)) {}
}
}

fn run_out<T>(device: &cpal::Device, config: &cpal::StreamConfig, receiver: Receiver<(f32, f32)>)
where
T: SizedSample + FromSample<f32>,
{
let channels = config.channels as usize;

let input = An(InputNode::new(receiver));
let reverb = reverb2_stereo(20.0, 3.0, 1.0, 1.0, highshelf_hz(1000.0, 1.0, db_amp(-1.0)));
let chorus = chorus(0, 0.0, 0.02, 0.3) | chorus(1, 0.0, 0.02, 0.3);
// Here is the final input-to-output processing chain.
let graph = input >> chorus >> (0.8 * reverb & 0.2 * multipass());
let mut graph = BlockRateAdapter::new(Box::new(graph));
graph.set_sample_rate(config.sample_rate.0 as f64);

let mut next_value = move || graph.get_stereo();

let err_fn = |err| eprintln!("An error occurred on stream: {}", err);
let stream = device.build_output_stream(
config,
move |data: &mut [T], _: &cpal::OutputCallbackInfo| {
write_data(data, channels, &mut next_value)
},
err_fn,
None,
);
if let Ok(stream) = stream {
if let Ok(()) = stream.play() {
std::mem::forget(stream);
}
}
println!("Output stream built.");
}

fn write_data<T>(output: &mut [T], channels: usize, next_sample: &mut dyn FnMut() -> (f32, f32))
where
T: SizedSample + FromSample<f32>,
{
for frame in output.chunks_mut(channels) {
let sample = next_sample();
let left = T::from_sample(sample.0);
let right = T::from_sample(sample.1);

for (channel, sample) in frame.iter_mut().enumerate() {
if channel & 1 == 0 {
*sample = left;
} else {
*sample = right;
}
}
}
}
2 changes: 2 additions & 0 deletions src/delay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ where
for tap_i in 1..N::USIZE + 1 {
let tap =
clamp(self.min_delay, self.max_delay, convert(input[tap_i])) * self.sample_rate;
// Safety: the value has been clamped.
let tap_floor = unsafe { f32::to_int_unchecked::<usize>(tap.to_f32()) };
let tap_i1 = self.i + (self.buffer.len() - tap_floor);
let tap_i0 = (tap_i1 + 1) & mask;
Expand Down Expand Up @@ -407,6 +408,7 @@ where
for tap_i in 1..N::USIZE + 1 {
let tap =
clamp(self.min_delay, self.max_delay, convert(input[tap_i])) * self.sample_rate;
// Safety: the value has been clamped.
let tap_floor = unsafe { f32::to_int_unchecked::<usize>(tap.to_f32()) };
let tap_i1 = self.i + (self.buffer.len() - tap_floor);
let tap_i2 = (tap_i1.wrapping_sub(1)) & mask;
Expand Down
1 change: 1 addition & 0 deletions src/feedback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ impl<N: Size<f32>> FrameUnop<N> for FrameHadamard<N> {
let y = output[j + h];
output[j] = x + y;
output[j + h] = x - y;
// Note. This unsafe version is not any faster.
//let x = unsafe { *output.get_unchecked(j) };
//let y = unsafe { *output.get_unchecked(j + h) };
//unsafe { *output.get_unchecked_mut(j) = x + y };
Expand Down
1 change: 1 addition & 0 deletions src/wavetable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ impl Wavetable {
pub fn at(&self, i: usize, phase: f32) -> f32 {
let table: &Vec<f32> = &self.table[i].1;
let p = table.len() as f32 * phase;
// Safety: we know phase is in 0...1.
let i1 = unsafe { f32::to_int_unchecked::<usize>(p) };
let w = p - i1 as f32;
let mask = table.len() - 1;
Expand Down

0 comments on commit e16287b

Please sign in to comment.