Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
4 changes: 0 additions & 4 deletions crates/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
78 changes: 78 additions & 0 deletions crates/src/nodes/audio/hadamard.rs
Original file line number Diff line number Diff line change
@@ -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
}
}
46 changes: 46 additions & 0 deletions crates/src/nodes/audio/householder.rs
Original file line number Diff line number Diff line change
@@ -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
}
}
2 changes: 2 additions & 0 deletions crates/src/nodes/audio/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
26 changes: 26 additions & 0 deletions crates/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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"],
Expand Down
Loading