diff --git a/crates/Cargo.lock b/crates/Cargo.lock index 5c07aa5..36d94d5 100644 --- a/crates/Cargo.lock +++ b/crates/Cargo.lock @@ -676,7 +676,7 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "legato" -version = "0.0.23" +version = "0.0.25" dependencies = [ "approx", "arc-swap", diff --git a/crates/Cargo.toml b/crates/Cargo.toml index ce6f498..e22f860 100644 --- a/crates/Cargo.toml +++ b/crates/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "legato" description = "Legato is a WIP audiograph and DSL for quickly developing audio applications" -version = "0.0.23" +version = "0.0.25" edition = "2024" repository="https://github.com/legato-dsp/legato" license = "AGPL-3.0" diff --git a/crates/src/builder.rs b/crates/src/builder.rs index e4d8876..bdd7770 100644 --- a/crates/src/builder.rs +++ b/crates/src/builder.rs @@ -684,16 +684,12 @@ pub struct ResourceBuilderView<'a> { impl<'a> ResourceBuilderView<'a> { pub fn add_delay_line(&mut self, name: &str, capacity: usize) -> DelayLineKey { - // Check and see if this key already has an entry let new_key = self.resource_builder.add_delay_line(capacity); - if let Some(values) = self.delay_keys.get_mut(name) { values.push(new_key); } else { - let new_key = self.resource_builder.add_delay_line(capacity); self.delay_keys.insert(name.into(), vec![new_key]); } - new_key } diff --git a/crates/src/nodes/audio/hadamard.rs b/crates/src/nodes/audio/hadamard.rs new file mode 100644 index 0000000..75d657a --- /dev/null +++ b/crates/src/nodes/audio/hadamard.rs @@ -0,0 +1,78 @@ +use crate::{ + context::AudioContext, + node::{Inputs, Node}, + ports::{PortBuilder, Ports}, +}; + +/// The Hadamard mixer applies a fast Walsh-Hadamard +/// transform (FWHT). +/// +/// https://en.wikipedia.org/wiki/Fast_Walsh%E2%80%93Hadamard_transform +/// +/// These mixers are generally good at creating more +/// density in FDN. +/// +/// `chans` must be a power of two or it will panic! +#[derive(Clone)] +pub struct HadamardMixer { + ports: Ports, + chans: usize, + vertical_slice: Box<[f32]>, +} + +impl HadamardMixer { + pub fn new(chans: usize) -> Self { + assert!(chans.is_power_of_two()); + Self { + ports: PortBuilder::default() + .audio_in(chans) + .audio_out(chans) + .build(), + chans, + vertical_slice: vec![0.0; chans].into(), // could maybe be an enum and on the stack? + } + } + + /// Update the FWHT in place + /// + /// see: https://en.wikipedia.org/wiki/Fast_Walsh%E2%80%93Hadamard_transform + fn fht(a: &mut [f32]) { + let n = a.len(); + let mut h = 1; + while h < n { + let mut i = 0; + while i < n { + for j in i..i + h { + let x = a[j]; + let y = a[j + h]; + a[j] = x + y; + a[j + h] = x - y; + } + i += h * 2; + } + h *= 2; + } + // Normalize + let norm = 1.0 / (n as f32).sqrt(); + a.iter_mut().for_each(|x| *x *= norm); + } +} + +impl Node for HadamardMixer { + fn process(&mut self, ctx: &mut AudioContext, inputs: &Inputs, outputs: &mut [&mut [f32]]) { + let block_size = ctx.get_config().block_size; + + for i in 0..block_size { + for c in 0..self.chans { + self.vertical_slice[c] = inputs.get(c).and_then(|x| *x).map_or(0.0, |buf| buf[i]); + } + Self::fht(&mut self.vertical_slice); // apply transform + for c in 0..self.chans { + outputs[c][i] = self.vertical_slice[c]; + } + } + } + fn ports(&self) -> &Ports { + &self.ports + } +} diff --git a/crates/src/nodes/audio/householder.rs b/crates/src/nodes/audio/householder.rs new file mode 100644 index 0000000..c8f7cc8 --- /dev/null +++ b/crates/src/nodes/audio/householder.rs @@ -0,0 +1,46 @@ +use crate::{ + context::AudioContext, + node::{Inputs, Node}, + ports::{PortBuilder, Ports}, +}; + +/// As suggested in https://signalsmith-audio.co.uk/writing/2021/lets-write-a-reverb/ +/// +/// Allegedly a bit lower density than saw a hadamard mixer +#[derive(Clone)] +pub struct HouseholderMixer { + chans: usize, + ports: Ports, +} + +impl HouseholderMixer { + pub fn new(chans: usize) -> Self { + Self { + chans, + ports: PortBuilder::default() + .audio_in(chans) + .audio_out(chans) + .build(), + } + } +} + +impl Node for HouseholderMixer { + fn process(&mut self, _ctx: &mut AudioContext, inputs: &Inputs, outputs: &mut [&mut [f32]]) { + let block_size = outputs[0].len(); + let multiplier = 2.0 / self.chans as f32; + + for i in 0..block_size { + let sum: f32 = (0..self.chans) + .map(|c| inputs.get(c).and_then(|x| *x).map_or(0.0, |buf| buf[i])) + .sum(); + for c in 0..self.chans { + let x = inputs.get(c).and_then(|x| *x).map_or(0.0, |buf| buf[i]); + outputs[c][i] = x - multiplier * sum; + } + } + } + fn ports(&self) -> &Ports { + &self.ports + } +} diff --git a/crates/src/nodes/audio/mod.rs b/crates/src/nodes/audio/mod.rs index 3c082c9..3f4145a 100644 --- a/crates/src/nodes/audio/mod.rs +++ b/crates/src/nodes/audio/mod.rs @@ -3,6 +3,8 @@ pub mod allpass; pub mod delay; pub mod external; pub mod fir; +pub mod hadamard; +pub mod householder; pub mod mixer; pub mod onepole; pub mod ops; diff --git a/crates/src/registry.rs b/crates/src/registry.rs index 0795181..baa9be8 100644 --- a/crates/src/registry.rs +++ b/crates/src/registry.rs @@ -13,6 +13,8 @@ use crate::{ allpass::Allpass, delay::{DelayRead, DelayWrite}, external::ExternalInput, + hadamard::HadamardMixer, + householder::HouseholderMixer, mixer::{MonoFanOut, TrackMixer}, onepole::OnePole, ops::{ApplyOpKind, mult_node_factory}, @@ -369,6 +371,30 @@ pub fn audio_registry_factory() -> NodeRegistry { Ok(Box::new(node)) } ), + node_spec!( + "hadamard".into(), + required = ["chans"], + optional = [], + build = |_, p| { + let chans = p + .get_usize("chans") + .expect("Must provide chans to audio_input"); + + Ok(Box::new(HadamardMixer::new(chans))) + } + ), + node_spec!( + "householder".into(), + required = ["chans"], + optional = [], + build = |_, p| { + let chans = p + .get_usize("chans") + .expect("Must provide chans to audio_input"); + + Ok(Box::new(HouseholderMixer::new(chans))) + } + ), node_spec!( "external".into(), required = ["interface_name", "chans"],