diff --git a/CHANGES.md b/CHANGES.md index 827d6ad..186e9b1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,13 @@ ## Changes +### Version 0.17 (Next Version) + +- `Wave32/64`: `silence` is now `zero`. +- New opcode `impulse`. +- Optimization of reverb delay times in the example `optimize`. +- New opcodes `node64` and `node32` for converting an `AudioUnit` into an `AudioNode`. +- New reverb opcode `reverb2_stereo`. + ### Version 0.16 - `AudioNode` now requires `Send` and `Sync`. diff --git a/Cargo.toml b/Cargo.toml index a5a28e3..43f1520 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ duplicate = "1.0.0" dyn-clone = "1.0.16" symphonia = { version = "0.5.3", optional = true, features = ["all"] } thingbuf = "0.1.4" -funutd = "0.12.1" +funutd = "0.14.0" [features] default = ["files"] @@ -31,14 +31,15 @@ files = ["dep:symphonia"] [dev-dependencies] cpal = "0.15.2" -anyhow = "1.0.77" +anyhow = "1.0.79" plotters = "0.3.5" criterion = "0.5.1" midi-msg = "0.5.0" midir = "0.9.1" read_input = "0.8.6" assert_no_alloc = "1.1.2" -eframe = "0.24.1" +eframe = "0.25.0" +rayon = "1.8.0" [[bench]] name = "benchmark" @@ -84,6 +85,10 @@ path = "examples/network.rs" name = "keys" path = "examples/keys.rs" +[[example]] +name = "optimize" +path = "examples/optimize.rs" + [package.metadata.docs.rs] all-features = true rustc-args = ["--cfg", "docsrs"] diff --git a/README.md b/README.md index 12e7b4a..ced73b4 100644 --- a/README.md +++ b/README.md @@ -561,8 +561,9 @@ Due to nonlinearity, we do not attempt to calculate frequency responses for thes ### Frequency Domain Resynthesis -Filtering and other effects can be done in the frequency domain as well -with the `resynth` opcode. +Filtering and other effects can be done in the +[frequency domain](https://en.wikipedia.org/wiki/Frequency_domain) +as well with the `resynth` opcode. The resynthesizer [Fourier transforms](https://en.wikipedia.org/wiki/Discrete-time_Fourier_transform) @@ -633,6 +634,13 @@ Later we can set the amplitude from anywhere: amp.set_value(0.5); ``` +A useful pattern is piping a shared variable through a `follow` filter +to smooth parameter changes, here with a 0.1 second response time: + +```rust +let amp_controlled = noise() * (var(&) >> follow(0.1)); +``` + The `timer` opcode maintains stream time in a shared variable. The timer node has no inputs or outputs and can be joined to any node by stacking. @@ -888,6 +896,7 @@ The type parameters in the table refer to the hacker preludes. | `highshelf_q(q, gain)` | 2 (audio, frequency) | 1 | High shelf filter (2nd order) with Q `q` and amplitude gain `gain`. | | `hold(v)` | 2 (signal, frequency) | 1 | Sample-and-hold component with hold time variability `v` in 0...1. | | `hold_hz(f, v)` | 1 | 1 | Sample-and-hold component at `f` Hz with hold time variability `v` in 0...1. | +| `impulse::()` | - | `U` | `U`-channel impulse; on each channel the first sample is one, the rest are zeros. | `join::()` | `U` | 1 | Average together `U` channels. Inverse of `split`. | | `lfo(f)` | - | `f` | Time-varying control `f` with scalar or tuple output, e.g., `\|t\| exp(-t)`. Synonymous with `envelope`. | | `lfo2(f)` | 1 (x) | `f` | Time-varying, input dependent control `f` with scalar or tuple output, e.g., `\|t, x\| exp(-t * x)`. Synonymous with `envelope2`. | @@ -925,6 +934,8 @@ The type parameters in the table refer to the hacker preludes. | `multitap::(min_delay, max_delay)` | `N + 1` (audio, delay...) | 1 | Tapped delay line with cubic interpolation. Number of taps is `N`. | | `multitick::()` | `U` | `U` | Multichannel single sample delay. | | `multizero::()` | - | `U` | Multichannel zero signal. | +| `node32::(unit)` | `I` | `O` | Convert an `AudioUnit32` into an `AudioNode` with `I` inputs and `O` outputs. | +| `node64::(unit)` | `I` | `O` | Convert an `AudioUnit64` into an `AudioNode` with `I` inputs and `O` outputs. | | `noise()` | - | 1 | [White noise](https://en.wikipedia.org/wiki/White_noise) source. Synonymous with `white`. | | `notch()` | 3 (audio, frequency, Q) | 1 | Notch filter (2nd order). | | `notch_hz(f, q)` | 1 | 1 | Notch filter (2nd order) centered at `f` Hz with Q `q`. | @@ -940,7 +951,7 @@ The type parameters in the table refer to the hacker preludes. | `peak_q(q)` | 2 (audio, frequency) | 1 | Peaking filter (2nd order) with Q `q`. | | `phaser(fb, f)` | 1 | 1 | Phaser effect with feedback amount `fb` and modulation function `f`, e.g., `\|t\| sin_hz(0.1, t) * 0.5 + 0.5`. | | `pink()` | - | 1 | [Pink noise](https://en.wikipedia.org/wiki/Pink_noise) source. | -| `pinkpass()` | 1 | 1 | Pinking filter (3 dB/octave). | +| `pinkpass()` | 1 | 1 | Pinking filter (3 dB/octave lowpass). | | `pipe::(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. | @@ -949,7 +960,8 @@ The type parameters in the table refer to the hacker preludes. | `resonator()` | 3 (audio, frequency, bandwidth) | 1 | Constant-gain bandpass resonator (2nd order). | | `resonator_hz(f, bw)` | 1 | 1 | Constant-gain bandpass resonator (2nd order) with center frequency `f` Hz and bandwidth `bw` Hz. | | `resynth::(w, f)` | `I` | `O` | Frequency domain resynthesis with window length `w` and processing function `f`. | -| `reverb_stereo(r, t)` | 2 | 2 | Stereo reverb with room size `r` meters (10 is average) and reverberation time `t` seconds. | +| `reverb_stereo(r, t)` | 2 | 2 | Stereo reverb (32-channel [FDN](https://ccrma.stanford.edu/~jos/pasp/Feedback_Delay_Networks_FDN.html)) with room size `r` meters (10 is average) and reverberation time `t` seconds. | +| `reverb2_stereo(r, t)` | 2 | 2 | Another stereo reverb (8-channel and 16-channel [FDN](https://ccrma.stanford.edu/~jos/pasp/Feedback_Delay_Networks_FDN.html)s in series) with room size `r` meters (10 is average) and reverberation time `t` seconds. | | `reverse::()` | `N` | `N` | Reverse channel order, e.g., swap left and right channels. | | `rossler()` | 1 (frequency) | 1 | [Rössler dynamical system](https://en.wikipedia.org/wiki/R%C3%B6ssler_attractor) oscillator. | | `saw()` | 1 (frequency) | 1 | Bandlimited saw wave oscillator. | diff --git a/benches/benchmark.rs b/benches/benchmark.rs index 94a24e1..1b81026 100644 --- a/benches/benchmark.rs +++ b/benches/benchmark.rs @@ -57,12 +57,7 @@ fn reverb_bench(_dummy: usize) -> Wave32 { Wave32::render( 44100.0, 1.0, - &mut (noise() - >> split() - >> fdn::(stack::(|i| { - delay(0.005 + 0.002 * i as f32) >> fir((0.22, 0.44, 0.22)) - })) - >> join()), + &mut ((noise() | noise()) >> reverb_stereo(10.0, 1.0)), ) } diff --git a/examples/grain.rs b/examples/grain.rs index bcd4ecf..cae9b32 100644 --- a/examples/grain.rs +++ b/examples/grain.rs @@ -173,7 +173,7 @@ where let mut dna = Dna::new(36); let mut c = Net64::wrap(gen_granular(2, &scale, 2.4, 30, &mut dna)); - for parameter in dna.parameters().iter() { + for parameter in dna.parameter_vector().iter() { println!("{}: {}", parameter.name(), parameter.value()); } diff --git a/examples/grain2.rs b/examples/grain2.rs index 978b0c1..15b53ab 100644 --- a/examples/grain2.rs +++ b/examples/grain2.rs @@ -38,13 +38,13 @@ where let mut dna = Dna::new(10); let mut c = gen_granular(1, &scale, 2.0, 30, &mut dna); - for parameter in dna.parameters().iter() { + for parameter in dna.parameter_vector().iter() { println!("{}: {}", parameter.name(), parameter.value()); } let mut dna2 = Dna::new(7); let mut fx = gen_effect(&mut dna2); - for parameter in dna2.parameters().iter() { + for parameter in dna2.parameter_vector().iter() { println!("{}: {}", parameter.name(), parameter.value()); } diff --git a/examples/keys.rs b/examples/keys.rs index ee8e6a2..c8fb425 100644 --- a/examples/keys.rs +++ b/examples/keys.rs @@ -45,6 +45,8 @@ struct State { waveform: Waveform, /// Selected filter. filter: Filter, + /// Vibrato amount in 0...1. + vibrato_amount: f64, /// Chorus amount. chorus_amount: Shared, /// Reverb amount. @@ -128,7 +130,7 @@ where let chorus_amount = shared(1.0); let mut net = Net64::wrap(Box::new(sequencer_backend)); - let (reverb, reverb_backend) = Slot64::new(Box::new(reverb_stereo(room_size, reverb_time))); + let (reverb, reverb_backend) = Slot64::new(Box::new(reverb2_stereo(room_size, reverb_time))); net = net >> pan(0.0); // Smooth chorus and reverb amounts to prevent discontinuities. net = net @@ -160,7 +162,7 @@ where )?; stream.play()?; - let viewport = ViewportBuilder::default().with_min_inner_size(vec2(360.0, 420.0)); + let viewport = ViewportBuilder::default().with_min_inner_size(vec2(360.0, 480.0)); let options = eframe::NativeOptions { viewport, @@ -174,6 +176,7 @@ where net, waveform: Waveform::Saw, filter: Filter::None, + vibrato_amount: 0.7, chorus_amount, reverb_amount, room_size, @@ -237,6 +240,13 @@ impl eframe::App for State { ui.separator(); ui.end_row(); + ui.label("Vibrato Amount"); + let mut vibrato = self.vibrato_amount * 100.0; + ui.add(egui::Slider::new(&mut vibrato, 0.0..=100.0).suffix("%")); + self.vibrato_amount = vibrato * 0.01; + ui.separator(); + ui.end_row(); + ui.label("Filter"); ui.horizontal(|ui| { ui.selectable_value(&mut self.filter, Filter::None, "None"); @@ -262,14 +272,14 @@ impl eframe::App for State { let mut reverb_time = self.reverb_time; let mut room_size = self.room_size; ui.label("Reverb Time"); - ui.add(egui::Slider::new(&mut reverb_time, 1.0..=5.0).suffix("s")); + ui.add(egui::Slider::new(&mut reverb_time, 1.0..=10.0).suffix("s")); ui.label("Reverb Room Size"); - ui.add(egui::Slider::new(&mut room_size, 3.0..=30.0).suffix("m")); + ui.add(egui::Slider::new(&mut room_size, 3.0..=100.0).suffix("m")); if self.room_size != room_size || self.reverb_time != reverb_time { self.reverb.set( Fade::Smooth, 0.5, - Box::new(reverb_stereo(room_size, reverb_time)), + Box::new(reverb2_stereo(room_size, reverb_time)), ); self.room_size = room_size; self.reverb_time = reverb_time; @@ -325,26 +335,29 @@ impl eframe::App for State { } } if ctx.input(|c| c.key_down(KEYS[i])) && self.id[i].is_none() { - let pitch = midi_hz(40.0 + i as f64); + let pitch_hz = midi_hz(40.0 + i as f64); + let v = self.vibrato_amount * 0.003; + let pitch = lfo(move |t| { + pitch_hz * xerp11(1.0 / (1.0 + v), 1.0 + v, sin_hz(6.0, t) + sin_hz(6.1, t)) + }); let waveform = match self.waveform { - Waveform::Sine => Net64::wrap(Box::new(sine_hz(pitch) * 0.1)), - Waveform::Saw => Net64::wrap(Box::new(saw_hz(pitch) * 0.5)), - Waveform::Square => Net64::wrap(Box::new(square_hz(pitch) * 0.5)), - Waveform::Triangle => Net64::wrap(Box::new(triangle_hz(pitch) * 0.5)), - Waveform::Organ => Net64::wrap(Box::new(organ_hz(pitch) * 0.5)), - Waveform::Hammond => Net64::wrap(Box::new(hammond_hz(pitch) * 0.5)), + Waveform::Sine => Net64::wrap(Box::new(pitch * 2.0 >> sine() * 0.1)), + Waveform::Saw => Net64::wrap(Box::new(pitch >> saw() * 0.5)), + Waveform::Square => Net64::wrap(Box::new(pitch >> square() * 0.5)), + Waveform::Triangle => Net64::wrap(Box::new(pitch >> triangle() * 0.5)), + Waveform::Organ => Net64::wrap(Box::new(pitch >> organ() * 0.5)), + Waveform::Hammond => Net64::wrap(Box::new(pitch >> hammond() * 0.5)), Waveform::Pulse => Net64::wrap(Box::new( - lfo(move |t| (pitch, lerp11(0.01, 0.99, sin_hz(0.1, t)))) + (pitch | lfo(move |t| lerp11(0.01, 0.99, sin_hz(0.1, t)))) >> pulse() * 0.5, )), Waveform::Pluck => { - Net64::wrap(Box::new(zero() >> pluck(pitch, 0.5, 0.5) * 0.5)) + Net64::wrap(Box::new(zero() >> pluck(pitch_hz, 0.5, 0.5) * 0.5)) } Waveform::Noise => Net64::wrap(Box::new( (noise() - | lfo(move |t| { - (pitch, funutd::math::lerp(100.0, 10.0, clamp01(t * 5.0))) - })) + | pitch * 4.0 + | lfo(move |t| funutd::math::lerp(100.0, 20.0, clamp01(t * 5.0)))) >> !resonator() >> resonator() >> shape(fundsp::shape::Shape::AdaptiveTanh(0.01, 0.1)), diff --git a/examples/optimize.rs b/examples/optimize.rs new file mode 100644 index 0000000..2ee3782 --- /dev/null +++ b/examples/optimize.rs @@ -0,0 +1,82 @@ +//! Optimize a stereo reverb. Please run me in release mode! + +use fundsp::hacker32::*; +use fundsp::reverb::*; +use funutd::dna::*; +use funutd::*; +use rayon::prelude::*; + +/// Evaluate reverb quality from its genotype. +fn evaluate_reverb(dna: &mut Dna) -> f32 { + let reverb = generate_reverb(dna); + // Prevent cases where two lines have the same length. + let mut repeat_fitness = 0.0; + for i in 0..dna.parameters() { + let i_time = dna.parameter(i).value_f32().unwrap(); + for j in i + 1..dna.parameters() { + let j_time = dna.parameter(j).value_f32().unwrap(); + if round(44100.0 * i_time) == round(44100.0 * j_time) { + repeat_fitness -= 100.0; + } + } + } + repeat_fitness + reverb_fitness(reverb) +} + +fn main() { + let mut rng = Rnd::from_time(); + + let mut dna = Dna::new(rng.u64()); + let mut fitness = evaluate_reverb(&mut dna); + let mut rounds = 0; + + loop { + rounds += 1; + + let mut seeds = vec![]; + for _ in 0..360 { + seeds.push((rng.u64(), rng.f32() * rng.f32())); + } + let mutateds: Vec<(Dna, f32)> = seeds + .par_iter() + .map(|(seed, p)| { + let mutation_p = xerp(1.0 / 30.0, 1.5, *p).min(1.0); + let mut mutated = Dna::mutate(&dna, *seed, mutation_p); + let mutated_fitness = evaluate_reverb(&mut mutated); + (mutated, mutated_fitness) + }) + .collect(); + + let mut improved = false; + for (mut mutated, mutated_fitness) in mutateds { + if mutated_fitness > fitness { + fitness = mutated_fitness; + std::mem::swap(&mut dna, &mut mutated); + improved = true; + } + } + + if improved || rounds % 1000 == 0 { + println!( + "Rounds {} (Candidates {}) Fitness {}", + rounds, + rounds * 360, + fitness + ); + } + if improved { + let mut delays = Vec::new(); + for i in 0..dna.parameters() { + let delay = dna.parameter(i).value_f32().unwrap(); + println!( + "{}: {} ({})", + dna.parameter(i).name(), + delay, + round(delay * 44100.0) as i32 + ); + delays.push(delay); + } + println!("{:?}", delays); + } + } +} diff --git a/src/audionode.rs b/src/audionode.rs index bdd962e..f263ada 100644 --- a/src/audionode.rs +++ b/src/audionode.rs @@ -2784,3 +2784,59 @@ impl, T: Float> AudioNode for Reverse { Routing::Reverse.propagate(input, N::USIZE) } } + +/// `N`-channel impulse. First sample on each channel is one, the rest are zero. +#[derive(Default, Clone)] +pub struct Impulse { + _marker: PhantomData, + value: T, +} + +impl, T: Float> Impulse { + pub fn new() -> Self { + Self { + _marker: PhantomData, + value: T::one(), + } + } +} + +impl, T: Float> AudioNode for Impulse { + const ID: u64 = 81; + type Sample = T; + type Inputs = U0; + type Outputs = N; + type Setting = (); + + fn reset(&mut self) { + self.value = T::one(); + } + + #[inline] + fn tick( + &mut self, + _input: &Frame, + ) -> Frame { + let output = Frame::splat(self.value); + self.value = T::zero(); + output + } + fn process( + &mut self, + size: usize, + _input: &[&[Self::Sample]], + output: &mut [&mut [Self::Sample]], + ) { + if size == 0 { + return; + } + for i in 0..N::USIZE { + output[i][0] = self.value; + output[i][1..size].fill(T::zero()); + } + self.value = T::zero(); + } + fn route(&mut self, input: &SignalFrame, _frequency: f64) -> SignalFrame { + Routing::Generator(0.0).propagate(input, N::USIZE) + } +} diff --git a/src/audiounit.rs b/src/audiounit.rs index 409865f..a7ddf6f 100644 --- a/src/audiounit.rs +++ b/src/audiounit.rs @@ -11,6 +11,7 @@ use dyn_clone::DynClone; use num_complex::Complex64; use rsor::Slice; use std::fmt::Write; +use std::marker::PhantomData; /// An audio processor with an object safe interface. /// Once constructed, it has a fixed number of inputs and outputs. @@ -643,3 +644,83 @@ impl AudioUnit48 for BlockRateAdapter48 { self.unit.allocate(); } } + +/// Converts an AudioUnit into an AudioNode. +#[duplicate_item( + f48 Node48 AudioUnit48; + [ f64 ] [ Node64 ] [ AudioUnit64 ]; + [ f32 ] [ Node32 ] [ AudioUnit32 ]; +)] +#[derive(Clone)] +pub struct Node48, O: Size> { + _marker: PhantomData<(I, O)>, + unit: Box, +} + +#[duplicate_item( + f48 Node48 AudioUnit48; + [ f64 ] [ Node64 ] [ AudioUnit64 ]; + [ f32 ] [ Node32 ] [ AudioUnit32 ]; +)] +impl, O: Size> Node48 { + pub fn new(unit: Box) -> Self { + assert!(I::USIZE == unit.inputs()); + assert!(O::USIZE == unit.outputs()); + Self { + _marker: PhantomData, + unit, + } + } +} + +#[duplicate_item( + f48 Node48 AudioUnit48; + [ f64 ] [ Node64 ] [ AudioUnit64 ]; + [ f32 ] [ Node32 ] [ AudioUnit32 ]; +)] +impl, O: Size> AudioNode for Node48 { + const ID: u64 = 82; + type Sample = f48; + type Inputs = I; + type Outputs = O; + type Setting = (); + + fn set_sample_rate(&mut self, sample_rate: f64) { + self.unit.set_sample_rate(sample_rate); + } + + fn reset(&mut self) { + self.unit.reset(); + } + + #[inline] + fn tick( + &mut self, + input: &Frame, + ) -> Frame { + let mut output = Frame::default(); + self.unit.tick(input, &mut output); + output + } + + fn process( + &mut self, + size: usize, + input: &[&[Self::Sample]], + output: &mut [&mut [Self::Sample]], + ) { + self.unit.process(size, input, output); + } + + fn ping(&mut self, probe: bool, hash: AttoHash) -> AttoHash { + self.unit.ping(probe, hash) + } + + fn route(&mut self, input: &SignalFrame, frequency: f64) -> SignalFrame { + self.unit.route(input, frequency) + } + + fn allocate(&mut self) { + self.unit.allocate(); + } +} diff --git a/src/feedback.rs b/src/feedback.rs index aa074bc..2a22d4b 100644 --- a/src/feedback.rs +++ b/src/feedback.rs @@ -44,32 +44,7 @@ impl, T: Float> FrameUnop for FrameHadamard { } h *= 2; } - // Normalization for up to 256 channels. - if N::USIZE >= 256 { - return output * Frame::splat(T::from_f64(1.0 / 16.0)); - } - if N::USIZE >= 128 { - return output * Frame::splat(T::from_f64(1.0 / (SQRT_2 * 8.0))); - } - if N::USIZE >= 64 { - return output * Frame::splat(T::from_f64(1.0 / 8.0)); - } - if N::USIZE >= 32 { - return output * Frame::splat(T::from_f64(1.0 / (SQRT_2 * 4.0))); - } - if N::USIZE >= 16 { - return output * Frame::splat(T::from_f64(1.0 / 4.0)); - } - if N::USIZE >= 8 { - return output * Frame::splat(T::from_f64(1.0 / (SQRT_2 * 2.0))); - } - if N::USIZE >= 4 { - return output * Frame::splat(T::from_f64(1.0 / 2.0)); - } - if N::USIZE >= 2 { - return output * Frame::splat(T::from_f64(1.0 / SQRT_2)); - } - output + output * Frame::splat(T::from_f64(1.0 / sqrt(N::I32 as f64))) } // Not implemented. // TODO: Hadamard is a special op because of interchannel dependencies. @@ -315,7 +290,7 @@ pub struct Feedback48 { )] impl Feedback48 { /// Create new feedback unit with integrated feedback `delay` in seconds. - /// The delay amount is rounded up to the nearest sample. + /// The delay amount is rounded to the nearest sample. /// The minimum delay is one sample, which may also be accomplished by setting `delay` to zero. /// The feedback unit mixes back delayed output of contained unit `x` to its input. pub fn new(delay: f48, x: Box) -> Self { @@ -364,7 +339,7 @@ impl AudioUnit48 for Feedback48 { if self.sample_rate != sample_rate as f48 { self.sample_rate = sample_rate as f48; self.x.set_sample_rate(sample_rate); - self.samples = ceil(self.delay * sample_rate as f48).max(1.0) as usize; + self.samples = round(self.delay * sample_rate as f48).max(1.0) as usize; let feedback_samples = self.samples.next_power_of_two(); self.mask = feedback_samples - 1; for feedback in self.feedback.iter_mut() { diff --git a/src/gen.rs b/src/gen.rs index fd89d84..eeed2d5 100644 --- a/src/gen.rs +++ b/src/gen.rs @@ -1,4 +1,4 @@ -//! Sound generators. +//! Sound generators using the Dna system. use funutd::dna::*; diff --git a/src/hacker.rs b/src/hacker.rs index e0c918c..6a3cd01 100644 --- a/src/hacker.rs +++ b/src/hacker.rs @@ -1498,6 +1498,33 @@ pub fn reverb_stereo( super::prelude::reverb_stereo::(room_size, time) } +/// Stereo reverb (8-channel and 16-channel FDNs in series). +/// `room_size` is in meters. An average room size is 10 meters. +/// `time` is approximate reverberation time to -60 dB in seconds. +/// - Input 0: left signal +/// - Input 1: right signal +/// - Output 0: reverberated left signal +/// - Output 1: reverberated right signal +pub fn reverb2_stereo( + room_size: f64, + time: f64, +) -> An> { + super::prelude::reverb2_stereo::(room_size, time) +} + +/// Create a stereo reverb unit, given delay times (in seconds) for the 24 delay lines +/// and reverberation `time` (in seconds). +/// - Input 0: left signal +/// - Input 1: right signal +/// - Output 0: reverberated left signal +/// - Output 1: reverberated right signal +pub fn reverb2_stereo_delays( + delays: &[f64], + time: f64, +) -> An> { + super::prelude::reverb2_stereo_delays::(delays, time) +} + /// Saw-like discrete summation formula oscillator. /// - Input 0: frequency in Hz /// - Input 1: roughness in 0...1 is the attenuation of successive partials. @@ -2204,3 +2231,25 @@ where { An(Resynth::new(window_length, processing)) } + +/// `N`-channel impulse. The first sample on each channel is one and the rest are zero. +/// - Output(s): impulse. +pub fn impulse>() -> An> { + An(Impulse::new()) +} + +/// Convert an `AudioUnit` into an `AudioNode`. +/// The number of input channels (`I`) and output channels (`O`) must be specified +/// and must match the provided `AudioUnit`. +/// - Input(s): `I` inputs of `unit`. +/// - Output(s): `O` outputs of `unit`. +/// +/// ### Example: Type Erase An AudioNode +/// ``` +/// use fundsp::hacker::*; +/// let node = noise() >> pinkpass(); +/// let erased: An> = node64(Box::new(node)); +/// ``` +pub fn node64, O: Size>(unit: Box) -> An> { + An(Node64::new(unit)) +} diff --git a/src/hacker32.rs b/src/hacker32.rs index 6c6af2d..edd3981 100644 --- a/src/hacker32.rs +++ b/src/hacker32.rs @@ -1498,6 +1498,33 @@ pub fn reverb_stereo( super::prelude::reverb_stereo::(room_size, time) } +/// Stereo reverb (8-channel and 16-channel FDNs in series). +/// `room_size` is in meters. An average room size is 10 meters. +/// `time` is approximate reverberation time to -60 dB in seconds. +/// - Input 0: left signal +/// - Input 1: right signal +/// - Output 0: reverberated left signal +/// - Output 1: reverberated right signal +pub fn reverb2_stereo( + room_size: f64, + time: f64, +) -> An> { + super::prelude::reverb2_stereo::(room_size, time) +} + +/// Create a stereo reverb unit, given delay times (in seconds) for the 24 delay lines +/// and reverberation `time` (in seconds). +/// - Input 0: left signal +/// - Input 1: right signal +/// - Output 0: reverberated left signal +/// - Output 1: reverberated right signal +pub fn reverb2_stereo_delays( + delays: &[f64], + time: f64, +) -> An> { + super::prelude::reverb2_stereo_delays::(delays, time) +} + /// Saw-like discrete summation formula oscillator. /// - Input 0: frequency in Hz /// - Input 1: roughness in 0...1 is the attenuation of successive partials. @@ -2206,3 +2233,25 @@ where { An(Resynth::new(window_length, processing)) } + +/// `N`-channel impulse. The first sample on each channel is one and the rest are zero. +/// - Output(s): impulse. +pub fn impulse>() -> An> { + An(Impulse::new()) +} + +/// Convert an `AudioUnit` into an `AudioNode`. +/// The number of input channels (`I`) and output channels (`O`) must be specified +/// and must match the provided `AudioUnit`. +/// - Input(s): `I` inputs of `unit`. +/// - Output(s): `O` outputs of `unit`. +/// +/// ### Example: Type Erase An AudioNode +/// ``` +/// use fundsp::hacker32::*; +/// let node = noise() >> pinkpass(); +/// let erased: An> = node32(Box::new(node)); +/// ``` +pub fn node32, O: Size>(unit: Box) -> An> { + An(Node32::new(unit)) +} diff --git a/src/lib.rs b/src/lib.rs index 4600074..f54f97c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -280,6 +280,7 @@ pub mod realnet; pub mod realseq; pub mod resample; pub mod resynth; +pub mod reverb; pub mod rez; pub mod sequencer; pub mod setting; diff --git a/src/math.rs b/src/math.rs index a3ca6ce..51cbb0e 100644 --- a/src/math.rs +++ b/src/math.rs @@ -231,13 +231,13 @@ pub fn delerp11(a: T, b: T, x: T) -> T { (x - a) / (b - a) * T::new(2) - T::new(1) } -/// Exponential interpolation. `a`, `b` > 0. +/// Exponential interpolation in `a`...`b` with `t` in 0...1. `a`, `b` > 0. #[inline] pub fn xerp + Real, T>(a: U, b: U, t: T) -> U { exp(lerp(log(a), log(b), t)) } -/// Exponential interpolation with `t` in -1...1. `a`, `b` > 0. +/// Exponential interpolation in `a`...`b` with `t` in 0...1. `a`, `b` > 0. #[inline] pub fn xerp11 + Real, T: Num>(a: U, b: U, t: T) -> U { exp(lerp( @@ -303,7 +303,7 @@ pub fn amp_db(gain: T) -> T { } /// A-weighted response function. -/// Returns equal loudness amplitude response at `f` Hz. +/// Returns equal loudness amplitude response of the human ear at `f` Hz. /// Normalized to 1.0 at 1 kHz. /// /// ### Example @@ -325,7 +325,7 @@ pub fn a_weight(f: T) -> T { /// M-weighted response function normalized to 1 kHz. /// M-weighting is an unofficial name for /// the frequency response curve of the ITU-R 468 noise weighting standard. -/// Returns equal loudness amplitude response at `f` Hz. +/// Returns equal loudness amplitude response of the human ear at `f` Hz. /// Normalized to 1.0 at 1 kHz. /// /// ### Example diff --git a/src/prelude.rs b/src/prelude.rs index a13462f..bd46ea7 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1582,7 +1582,7 @@ where An(MultiJoin::::new()) } -/// Stereo reverb. +/// Stereo reverb (32-channel FDN). /// `room_size` is in meters. An average room size is 10 meters. /// `time` is approximate reverberation time to -60 dB in seconds. /// - Input 0: left signal @@ -1625,13 +1625,94 @@ where let reverb = fdn::(line); // Multiplex stereo into 32 channels, reverberate, then average them back. - multisplit::() >> reverb >> multijoin::() + //multisplit::() >> reverb >> multijoin::() - // This version pans the channels linearly (the above version pans them hard left or right). - //multisplit::() - // >> reverb - // >> sumf::(|x| pan(lerp(T::new(-1), T::new(1), convert(x)))) - // * dc((T::from_f64(1.0 / 16.0), T::from_f64(1.0 / 16.0))) + // This version pans the channels with an S shape (the above version pans them hard left or right). + multisplit::() + >> reverb + >> sumf::(|x| pan(lerp(T::new(-1), T::new(1), convert(smooth3(x))))) + * dc((T::from_f64(1.0 / 16.0), T::from_f64(1.0 / 16.0))) +} + +/// Stereo reverb (8-channel and 16-channel FDNs in series). +/// `room_size` is in meters. An average room size is 10 meters. +/// `time` is approximate reverberation time to -60 dB in seconds. +/// - Input 0: left signal +/// - Input 1: right signal +/// - Output 0: reverberated left signal +/// - Output 1: reverberated right signal +pub fn reverb2_stereo( + room_size: f64, + time: f64, +) -> An> { + // Optimized delay times from `optimize.rs` example. Fitness = 773.69745. + let mut delays = [ + 0.068582244, + 0.02007867, + 0.026451133, + 0.028128913, + 0.023684707, + 0.02030506, + 0.06849203, + 0.02046375, + 0.06855943, + 0.02119047, + 0.02082747, + 0.022844134, + 0.026518257, + 0.027085062, + 0.028968025, + 0.02377437, + 0.020056667, + 0.020010974, + 0.023298478, + 0.020509405, + 0.022709418, + 0.022346418, + 0.030714132, + 0.024976023, + ]; + for delay in delays.iter_mut() { + *delay *= room_size / 10.0; + } + reverb2_stereo_delays::(&delays, time) +} + +/// Create a stereo reverb unit, given delay times (in seconds) for the 24 delay lines +/// and reverberation `time` (in seconds). +/// - Input 0: left signal +/// - Input 1: right signal +/// - Output 0: reverberated left signal +/// - Output 1: reverberated right signal +pub fn reverb2_stereo_delays( + delays: &[f64], + time: f64, +) -> An> +where + T: Real, +{ + assert!(delays.len() == 24); + let room_size = delays.iter().sum::() / 24.0 / 0.03 * 10.0; + let a = T::from_f64(pow(db_amp(-60.0), 0.03 * room_size / 10.0 / time)); + + let line1 = stack::(|i| { + delay::(delays[i as usize]) >> fir((a / T::new(4), a / T::new(2), a / T::new(4))) + }); + + let line2 = stack::(|i| { + delay::(delays[8 + i as usize]) >> fir((a / T::new(4), a / T::new(2), a / T::new(4))) + }); + + let fdn1 = fdn(line1); + let fdn2 = fdn(line2); + + multisplit::() + >> fdn1 + >> multijoin::() + >> multisplit::() + >> fdn2 + >> sumf::(|x| pan(lerp(T::new(-1), T::new(1), convert(smooth3(x))))) + * dc((T::from_f64(1.0 / 8.0), T::from_f64(1.0 / 8.0))) } /// Saw-like discrete summation formula oscillator. @@ -2710,3 +2791,9 @@ where { An(Resynth::new(window_length, processing)) } + +/// `N`-channel impulse. The first sample on each channel is one and the rest are zero. +/// - Output(s): impulse. +pub fn impulse, T: Float>() -> An> { + An(Impulse::new()) +} diff --git a/src/reverb.rs b/src/reverb.rs new file mode 100644 index 0000000..fe8020c --- /dev/null +++ b/src/reverb.rs @@ -0,0 +1,65 @@ +//! Reverberation related code. + +use super::hacker32::*; +use funutd::dna::*; +use realfft::*; + +/// Generate a reverb unit. +pub fn generate_reverb( + dna: &mut Dna, +) -> An> { + let mut times = Vec::new(); + for i in 0..24 { + let name = format!("Delay {}", i); + times.push(dna.f32_in(&name, 0.020, 0.070) as f64); + } + reverb2_stereo_delays(×, 3.0) +} + +/// Attempt to measure the quality of a stereo reverb unit. +pub fn reverb_fitness(reverb: An>) -> f32 { + let mut response = Wave32::render(44100.0, 65536.0 / 44100.0, &mut (impulse() >> reverb)); + + // Pad the response with zeros to prevent circular convolution artifacts. + response.resize(response.length() * 2); + + let mut fitness = 0.0; + + let mut planner = RealFftPlanner::::new(); + let r2c = planner.plan_fft_forward(response.length()); + let c2r = planner.plan_fft_inverse(response.length()); + let mut spectrum = r2c.make_output_vec(); + + for channel in 0..=1 { + let mut data = response.channel(channel).clone(); + r2c.process(&mut data, &mut spectrum).unwrap(); + for x in spectrum.iter_mut() { + *x = Complex32::new(x.norm_sqr(), 0.0); + } + c2r.process(&mut spectrum, &mut data).unwrap(); + let z = if data[0] > 0.0 { 1.0 / data[0] } else { 0.0 }; + //println!("data 0 = {:?}", &data[0..100]); + // Now `data[i] * z` is a normalized autocorrelation ranging in -1...1 for a lag of `i` samples. + + // Minimize autocorrelation. + // Weight the frequencies by the noise response curve of the human ear. + let auto_weight = 1.0; + for i in 1..4410 * 4 { + fitness -= m_weight(44100.0 / i as f32) * abs(data[i] * z) * auto_weight; + } + + // Maximize echo density. + let echo_weight = 1_000_000.0; + for i in 1..response.length() { + // It is necessary to weight the initial buildup heavily to make it smooth. + let weight = 1.0 / squared(i as f32); + let r = response.at(channel, i); + let threshold = 1.0e-9; + if abs(r) >= threshold { + fitness += weight * echo_weight; + } + } + } + + fitness +} diff --git a/src/signal.rs b/src/signal.rs index 6f57be1..c2b214f 100644 --- a/src/signal.rs +++ b/src/signal.rs @@ -129,7 +129,7 @@ pub fn copy_signal_frame(source: &SignalFrame, i: usize, n: usize) -> SignalFram /// functionality. #[derive(Clone)] pub enum Routing { - /// Conservative routing: every input influences every output nonlinearly with extra latency. + /// Conservative routing: every input influences every output nonlinearly with extra latency in samples. Arbitrary(f64), /// Split or multisplit semantics. Split, @@ -137,6 +137,8 @@ pub enum Routing { Join, /// Reverse channel order semantics. Equal number of inputs and outputs. Reverse, + /// Generator with latency in samples. + Generator(f64), } impl Routing { @@ -182,6 +184,11 @@ impl Routing { output[i] = input[input.len() - 1 - i]; } } + Routing::Generator(latency) => { + for i in 0..outputs { + output[i] = Signal::Latency(*latency); + } + } } output } diff --git a/src/wave.rs b/src/wave.rs index 4af5061..303c88f 100644 --- a/src/wave.rs +++ b/src/wave.rs @@ -144,15 +144,16 @@ impl Wave48 { } } - /// Create an all-zeros wave with the given `duration` in seconds and number of `channels`. + /// Create an all-zeros wave with the given `duration` in seconds + /// (rounded to the nearest sample) and number of `channels`. /// /// ### Example /// ``` /// use fundsp::hacker32::*; - /// let wave = Wave32::silence(1, 44100.0, 1.0); + /// let wave = Wave32::zero(1, 44100.0, 1.0); /// assert!(wave.duration() == 1.0 && wave.amplitude() == 0.0); /// ``` - pub fn silence(channels: usize, sample_rate: f64, duration: f64) -> Self { + pub fn zero(channels: usize, sample_rate: f64, duration: f64) -> Self { let length = round(duration * sample_rate) as usize; assert!(channels > 0 || length == 0); let mut vec = Vec::with_capacity(channels); @@ -503,7 +504,7 @@ impl Wave48 { let duration = duration_samples as f64 / sample_rate; if latency_samples > 0 { let latency_wave = Self::render(sample_rate, duration + latency_duration, node); - let mut wave = Self::silence(node.outputs(), sample_rate, duration); + let mut wave = Self::zero(node.outputs(), sample_rate, duration); for channel in 0..wave.channels() { for i in 0..duration_samples { wave.set(channel, i, latency_wave.at(channel, i + latency_samples)); @@ -608,7 +609,7 @@ impl Wave48 { let duration = duration_samples as f64 / self.sample_rate(); if latency_samples > 0 { let latency_wave = self.filter(duration + latency_duration, node); - let mut wave = Self::silence(node.outputs(), self.sample_rate(), duration); + let mut wave = Self::zero(node.outputs(), self.sample_rate(), duration); for channel in 0..wave.channels() { for i in 0..duration_samples { wave.set(channel, i, latency_wave.at(channel, i + latency_samples)); diff --git a/tests/basic.rs b/tests/basic.rs index bb0b3b8..e3f5355 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -257,6 +257,7 @@ fn test_basic() { check_wave(net); check_wave((noise() | envelope(|t| spline_noise(1, t * 10.0))) >> panner()); + check_wave(impulse::()); // Wave filtering, tick vs. process rendering, node reseting. let input = Wave64::render(44100.0, 1.0, &mut (noise() | noise()));