use aether_core::{node::DspNode, param::ParamBlock, BUFFER_SIZE, MAX_INPUTS};
const MAX_GRAIN_SAMPLES: usize = 48_000 / 2; const MAX_GRAINS: usize = 64;
const INPUT_BUF_SIZE: usize = 48_000 * 4;
struct Grain {
active: bool,
pos: f64, speed: f64, age: usize, duration: usize, amplitude: f32,
}
impl Grain {
fn new() -> Self {
Self { active: false, pos: 0.0, speed: 1.0, age: 0, duration: 1024, amplitude: 0.0 }
}
#[inline(always)]
fn envelope(&self) -> f32 {
let t = self.age as f32 / self.duration as f32;
let hann = 0.5 * (1.0 - (std::f32::consts::TAU * t).cos());
hann * self.amplitude
}
#[inline(always)]
fn next_sample(&mut self, input_buf: &[f32]) -> f32 {
if !self.active { return 0.0; }
let env = self.envelope();
let idx = self.pos as usize % input_buf.len();
let frac = (self.pos - self.pos.floor()) as f32;
let s0 = input_buf[idx];
let s1 = input_buf[(idx + 1) % input_buf.len()];
let sample = s0 + (s1 - s0) * frac;
self.pos += self.speed;
self.age += 1;
if self.age >= self.duration { self.active = false; }
sample * env
}
}
pub struct Granular {
grains: [Grain; MAX_GRAINS],
input_buf: Box<[f32; INPUT_BUF_SIZE]>,
write_pos: usize,
samples_since_last_grain: usize,
rng: u32,
}
impl Granular {
pub fn new() -> Self {
Self {
grains: std::array::from_fn(|_| Grain::new()),
input_buf: Box::new([0.0; INPUT_BUF_SIZE]),
write_pos: 0,
samples_since_last_grain: 0,
rng: 0xDEAD_BEEF,
}
}
fn rand_f32(&mut self) -> f32 {
self.rng ^= self.rng << 13;
self.rng ^= self.rng >> 17;
self.rng ^= self.rng << 5;
self.rng as f32 / u32::MAX as f32
}
fn spawn_grain(&mut self, grain_size_ms: f32, pitch_scatter: f32, position: f32, pos_scatter: f32, sr: f32) {
let duration = ((grain_size_ms / 1000.0) * sr) as usize;
let duration = duration.clamp(64, MAX_GRAIN_SAMPLES);
let slot = self.grains.iter().position(|g| !g.active);
let slot = match slot { Some(s) => s, None => return };
let pos_center = (position + (self.rand_f32() - 0.5) * pos_scatter).clamp(0.0, 1.0);
let buf_pos = (pos_center * INPUT_BUF_SIZE as f32) as usize;
let semitone_offset = (self.rand_f32() - 0.5) * 2.0 * pitch_scatter;
let speed = 2.0f64.powf(semitone_offset as f64 / 12.0);
self.grains[slot] = Grain {
active: true,
pos: buf_pos as f64,
speed,
age: 0,
duration,
amplitude: 0.7 + self.rand_f32() * 0.3,
};
}
}
impl Default for Granular {
fn default() -> Self { Self::new() }
}
impl DspNode for Granular {
fn process(
&mut self,
inputs: &[Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS],
output: &mut [f32; BUFFER_SIZE],
params: &mut ParamBlock,
sample_rate: f32,
) {
let silence = [0.0f32; BUFFER_SIZE];
let input = inputs[0].unwrap_or(&silence);
for (i, out) in output.iter_mut().enumerate() {
let grain_size = params.get(0).current.clamp(10.0, 500.0);
let density = params.get(1).current.clamp(1.0, 50.0);
let pitch_scat = params.get(2).current.clamp(0.0, 2.0);
let position = params.get(3).current.clamp(0.0, 1.0);
let pos_scat = params.get(4).current.clamp(0.0, 1.0);
let wet = params.get(5).current.clamp(0.0, 1.0);
self.input_buf[self.write_pos] = input[i];
self.write_pos = (self.write_pos + 1) % INPUT_BUF_SIZE;
let samples_per_grain = (sample_rate / density) as usize;
self.samples_since_last_grain += 1;
if self.samples_since_last_grain >= samples_per_grain {
self.samples_since_last_grain = 0;
self.spawn_grain(grain_size, pitch_scat, position, pos_scat, sample_rate);
}
let mut wet_signal = 0.0f32;
for grain in self.grains.iter_mut() {
wet_signal += grain.next_sample(&*self.input_buf);
}
*out = input[i] * (1.0 - wet) + wet_signal * wet;
params.tick_all();
}
}
fn type_name(&self) -> &'static str { "Granular" }
}