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