From 3e820ccd3e54e9f7563b3d7a56451eba323c1f77 Mon Sep 17 00:00:00 2001 From: Luke Date: Wed, 29 Apr 2026 22:31:50 +0200 Subject: [PATCH 1/5] hadamard --- crates/src/nodes/audio/hadamard.rs | 78 ++++++++++++++++++++++++++++++ crates/src/registry.rs | 13 +++++ 2 files changed, 91 insertions(+) create mode 100644 crates/src/nodes/audio/hadamard.rs 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/registry.rs b/crates/src/registry.rs index 0795181..4de00d6 100644 --- a/crates/src/registry.rs +++ b/crates/src/registry.rs @@ -13,6 +13,7 @@ use crate::{ allpass::Allpass, delay::{DelayRead, DelayWrite}, external::ExternalInput, + hadamard::HadamardMixer, mixer::{MonoFanOut, TrackMixer}, onepole::OnePole, ops::{ApplyOpKind, mult_node_factory}, @@ -369,6 +370,18 @@ 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!( "external".into(), required = ["interface_name", "chans"], From 26fe70c88ef1edde4c91abe99938ab43a13d12f4 Mon Sep 17 00:00:00 2001 From: Luke Date: Wed, 29 Apr 2026 22:40:06 +0200 Subject: [PATCH 2/5] householder --- crates/src/nodes/audio/householder.rs | 46 +++++++++++++++++++++++++++ crates/src/nodes/audio/mod.rs | 2 ++ crates/src/registry.rs | 13 ++++++++ 3 files changed, 61 insertions(+) create mode 100644 crates/src/nodes/audio/householder.rs 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 4de00d6..baa9be8 100644 --- a/crates/src/registry.rs +++ b/crates/src/registry.rs @@ -14,6 +14,7 @@ use crate::{ delay::{DelayRead, DelayWrite}, external::ExternalInput, hadamard::HadamardMixer, + householder::HouseholderMixer, mixer::{MonoFanOut, TrackMixer}, onepole::OnePole, ops::{ApplyOpKind, mult_node_factory}, @@ -382,6 +383,18 @@ pub fn audio_registry_factory() -> NodeRegistry { 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"], From ca2e35c8390ff9cb4db8964b09e1e2be4a34134d Mon Sep 17 00:00:00 2001 From: Luke Date: Wed, 29 Apr 2026 22:40:35 +0200 Subject: [PATCH 3/5] version bump --- crates/Cargo.lock | 2 +- crates/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/Cargo.lock b/crates/Cargo.lock index 5c07aa5..f1a3154 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.24" dependencies = [ "approx", "arc-swap", diff --git a/crates/Cargo.toml b/crates/Cargo.toml index ce6f498..af57f76 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.24" edition = "2024" repository="https://github.com/legato-dsp/legato" license = "AGPL-3.0" From 73579bd17b6ad67bdae877bcf60ad954f78a8270 Mon Sep 17 00:00:00 2001 From: Luke Date: Thu, 30 Apr 2026 01:03:40 +0200 Subject: [PATCH 4/5] api tweak --- crates/src/builder.rs | 4 ---- 1 file changed, 4 deletions(-) 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 } From d4ba3843bc63af0b3ffe7741d66f62dbc8afb9d4 Mon Sep 17 00:00:00 2001 From: Luke Date: Thu, 30 Apr 2026 01:05:10 +0200 Subject: [PATCH 5/5] bump version --- crates/Cargo.lock | 2 +- crates/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/Cargo.lock b/crates/Cargo.lock index f1a3154..36d94d5 100644 --- a/crates/Cargo.lock +++ b/crates/Cargo.lock @@ -676,7 +676,7 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "legato" -version = "0.0.24" +version = "0.0.25" dependencies = [ "approx", "arc-swap", diff --git a/crates/Cargo.toml b/crates/Cargo.toml index af57f76..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.24" +version = "0.0.25" edition = "2024" repository="https://github.com/legato-dsp/legato" license = "AGPL-3.0"