1use aether_core::{node::DspNode, param::ParamBlock, BUFFER_SIZE, MAX_INPUTS};
16
17const MAX_GRAIN_SAMPLES: usize = 48_000 / 2; const MAX_GRAINS: usize = 64;
19const INPUT_BUF_SIZE: usize = 48_000 * 4; struct Grain {
22 active: bool,
23 pos: f64, speed: f64, age: usize, duration: usize, amplitude: f32,
28}
29
30impl Grain {
31 fn new() -> Self {
32 Self { active: false, pos: 0.0, speed: 1.0, age: 0, duration: 1024, amplitude: 0.0 }
33 }
34
35 #[inline(always)]
36 fn envelope(&self) -> f32 {
37 let t = self.age as f32 / self.duration as f32;
39 let hann = 0.5 * (1.0 - (std::f32::consts::TAU * t).cos());
40 hann * self.amplitude
41 }
42
43 #[inline(always)]
44 fn next_sample(&mut self, input_buf: &[f32]) -> f32 {
45 if !self.active { return 0.0; }
46 let env = self.envelope();
47 let idx = self.pos as usize % input_buf.len();
48 let frac = (self.pos - self.pos.floor()) as f32;
49 let s0 = input_buf[idx];
50 let s1 = input_buf[(idx + 1) % input_buf.len()];
51 let sample = s0 + (s1 - s0) * frac;
52 self.pos += self.speed;
53 self.age += 1;
54 if self.age >= self.duration { self.active = false; }
55 sample * env
56 }
57}
58
59pub struct Granular {
60 grains: [Grain; MAX_GRAINS],
61 input_buf: Box<[f32; INPUT_BUF_SIZE]>,
62 write_pos: usize,
63 samples_since_last_grain: usize,
64 rng: u32,
65}
66
67impl Granular {
68 pub fn new() -> Self {
69 Self {
70 grains: std::array::from_fn(|_| Grain::new()),
71 input_buf: Box::new([0.0; INPUT_BUF_SIZE]),
72 write_pos: 0,
73 samples_since_last_grain: 0,
74 rng: 0xDEAD_BEEF,
75 }
76 }
77
78 fn rand_f32(&mut self) -> f32 {
79 self.rng ^= self.rng << 13;
80 self.rng ^= self.rng >> 17;
81 self.rng ^= self.rng << 5;
82 self.rng as f32 / u32::MAX as f32
83 }
84
85 fn spawn_grain(&mut self, grain_size_ms: f32, pitch_scatter: f32, position: f32, pos_scatter: f32, sr: f32) {
86 let duration = ((grain_size_ms / 1000.0) * sr) as usize;
87 let duration = duration.clamp(64, MAX_GRAIN_SAMPLES);
88
89 let slot = self.grains.iter().position(|g| !g.active);
91 let slot = match slot { Some(s) => s, None => return };
92
93 let pos_center = (position + (self.rand_f32() - 0.5) * pos_scatter).clamp(0.0, 1.0);
95 let buf_pos = (pos_center * INPUT_BUF_SIZE as f32) as usize;
96
97 let semitone_offset = (self.rand_f32() - 0.5) * 2.0 * pitch_scatter;
99 let speed = 2.0f64.powf(semitone_offset as f64 / 12.0);
100
101 self.grains[slot] = Grain {
102 active: true,
103 pos: buf_pos as f64,
104 speed,
105 age: 0,
106 duration,
107 amplitude: 0.7 + self.rand_f32() * 0.3,
108 };
109 }
110}
111
112impl Default for Granular {
113 fn default() -> Self { Self::new() }
114}
115
116impl DspNode for Granular {
117 fn process(
118 &mut self,
119 inputs: &[Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS],
120 output: &mut [f32; BUFFER_SIZE],
121 params: &mut ParamBlock,
122 sample_rate: f32,
123 ) {
124 let silence = [0.0f32; BUFFER_SIZE];
125 let input = inputs[0].unwrap_or(&silence);
126
127 for (i, out) in output.iter_mut().enumerate() {
128 let grain_size = params.get(0).current.clamp(10.0, 500.0);
129 let density = params.get(1).current.clamp(1.0, 50.0);
130 let pitch_scat = params.get(2).current.clamp(0.0, 2.0);
131 let position = params.get(3).current.clamp(0.0, 1.0);
132 let pos_scat = params.get(4).current.clamp(0.0, 1.0);
133 let wet = params.get(5).current.clamp(0.0, 1.0);
134
135 self.input_buf[self.write_pos] = input[i];
137 self.write_pos = (self.write_pos + 1) % INPUT_BUF_SIZE;
138
139 let samples_per_grain = (sample_rate / density) as usize;
141 self.samples_since_last_grain += 1;
142 if self.samples_since_last_grain >= samples_per_grain {
143 self.samples_since_last_grain = 0;
144 self.spawn_grain(grain_size, pitch_scat, position, pos_scat, sample_rate);
145 }
146
147 let mut wet_signal = 0.0f32;
149 for grain in self.grains.iter_mut() {
150 wet_signal += grain.next_sample(&*self.input_buf);
151 }
152
153 *out = input[i] * (1.0 - wet) + wet_signal * wet;
154 params.tick_all();
155 }
156 }
157
158 fn type_name(&self) -> &'static str { "Granular" }
159}