use std::sync::{Arc, LazyLock};
use rand::{rngs::SmallRng, Rng, SeedableRng};
use assume::assume;
use strum::EnumCount;
use crate::{utils::buffer::InterleavedBufferMut, Error};
#[derive(
Clone, Copy, Debug, PartialEq, Eq, strum::EnumString, strum::Display, strum::VariantNames,
)]
#[repr(u8)]
pub enum GrainPlaybackDirection {
Forward,
Backward,
Random,
}
#[derive(
Clone, Copy, Debug, PartialEq, Eq, strum::EnumString, strum::Display, strum::VariantNames,
)]
#[repr(u8)]
pub enum GrainOverlapMode {
Cloud,
Sequential,
}
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
strum::EnumString,
strum::Display,
strum::VariantNames,
strum::EnumCount,
)]
#[repr(u8)]
pub enum GrainWindowMode {
Hann = 0,
Blackman = 1,
Triangle = 2,
Tukey = 3,
Trapezoid = 4,
Exponential = 5,
RampUp = 6,
RampDown = 7,
}
impl GrainWindowMode {
pub fn sequential_crossfade_point(&self) -> f32 {
match self {
GrainWindowMode::Hann
| GrainWindowMode::Blackman
| GrainWindowMode::Triangle
| GrainWindowMode::Tukey => 0.5,
GrainWindowMode::Trapezoid => 0.9,
GrainWindowMode::Exponential | GrainWindowMode::RampUp | GrainWindowMode::RampDown => {
0.8
}
}
}
}
pub(crate) struct GrainWindow<const N: usize> {
luts: [[f32; N]; GrainWindowMode::COUNT],
}
impl<const N: usize> GrainWindow<N> {
const _VERIFY_N: () = assert!(
N.is_power_of_two(),
"Grain window size must be a pow2 value"
);
const MASK: usize = N - 1;
pub fn new() -> Self {
let mut luts = [[0.0; N]; GrainWindowMode::COUNT];
#[allow(clippy::needless_range_loop)]
for i in 0..N {
let phase = i as f32 / N as f32;
luts[GrainWindowMode::Hann as usize][i] =
0.5 * (1.0 - (2.0 * std::f32::consts::PI * phase).cos());
let pi_phase = std::f32::consts::PI * phase;
luts[GrainWindowMode::Blackman as usize][i] =
0.42 - 0.5 * (2.0 * pi_phase).cos() + 0.08 * (4.0 * pi_phase).cos();
luts[GrainWindowMode::Triangle as usize][i] = if phase < 0.5 {
2.0 * phase
} else {
2.0 * (1.0 - phase)
};
let alpha = 0.5;
let width = alpha / 2.0;
luts[GrainWindowMode::Tukey as usize][i] = if phase < width {
let u = phase / width;
0.5 * (1.0 - (std::f32::consts::PI * u).cos())
} else if phase > 1.0 - width {
let u = (1.0 - phase) / width;
0.5 * (1.0 - (std::f32::consts::PI * u).cos())
} else {
1.0
};
let ramp_width = 0.1;
luts[GrainWindowMode::Trapezoid as usize][i] = if phase < ramp_width {
phase / ramp_width
} else if phase > 1.0 - ramp_width {
(1.0 - phase) / ramp_width
} else {
1.0
};
let decay_rate = 6.0;
let center_dist = (phase - 0.5).abs();
luts[GrainWindowMode::Exponential as usize][i] = (-decay_rate * center_dist).exp();
luts[GrainWindowMode::RampUp as usize][i] = if phase < 0.9 {
phase / 0.9
} else {
let u = (phase - 0.9) / 0.1;
0.5 * (1.0 + (std::f32::consts::PI * u).cos())
};
luts[GrainWindowMode::RampDown as usize][i] = if phase < 0.1 {
let u = phase / 0.1;
0.5 * (1.0 - (std::f32::consts::PI * u).cos())
} else {
1.0 - ((phase - 0.1) / 0.9)
};
}
Self { luts }
}
#[inline]
pub fn sample(&self, mode: GrainWindowMode, phase: f64) -> f32 {
debug_assert!((0.0..=1.0).contains(&phase));
let index_float = phase * (N - 1) as f64;
let index = (index_float as usize) & Self::MASK;
let fraction = index_float.fract() as f32;
let next_index = (index + 1) & Self::MASK;
let lut = &self.luts[mode as usize];
if index < N - 1 {
lut[index] * (1.0 - fraction) + lut[next_index] * fraction
} else {
lut[N - 1]
}
}
}
static GRAIN_WINDOW_LUT: LazyLock<GrainWindow<2048>> = LazyLock::new(GrainWindow::new);
pub(crate) struct GranularParameterModulation<'a> {
pub size: &'a [f32],
pub density: &'a [f32],
pub variation: &'a [f32],
pub spray: &'a [f32],
pub pan_spread: &'a [f32],
pub position: &'a [f32],
pub speed: &'a [f32],
}
#[derive(Clone, Debug)]
pub struct GranularParameters {
pub overlap_mode: GrainOverlapMode,
pub window: GrainWindowMode,
pub size: f32,
pub density: f32,
pub variation: f32,
pub spray: f32,
pub pan_spread: f32,
pub playback_direction: GrainPlaybackDirection,
pub position: f32,
pub step: f32,
}
impl Default for GranularParameters {
fn default() -> Self {
Self {
overlap_mode: GrainOverlapMode::Cloud,
window: GrainWindowMode::Triangle,
size: 100.0,
density: 10.0,
spray: 0.0,
variation: 0.0,
pan_spread: 0.0,
playback_direction: GrainPlaybackDirection::Forward,
position: 0.5,
step: 0.0,
}
}
}
impl GranularParameters {
pub fn new() -> Self {
Self::default()
}
pub fn validate(&self) -> Result<(), Error> {
if self.size < 1.0 || self.size > 1000.0 {
return Err(Error::ParameterError(
"Grain size must be between 1 and 1000 ms".to_string(),
));
}
if self.density < 1.0 || self.density > 100.0 {
return Err(Error::ParameterError(
"Grain density must be between 1.0 and 100.0 Hz".to_string(),
));
}
if self.spray < 0.0 || self.spray > 1.0 {
return Err(Error::ParameterError(
"Grain spray must be between 0.0 and 1.0".to_string(),
));
}
if self.variation < 0.0 || self.variation > 1.0 {
return Err(Error::ParameterError(
"Grain variation must be between 0.0 and 1.0".to_string(),
));
}
if self.pan_spread < 0.0 || self.pan_spread > 1.0 {
return Err(Error::ParameterError(
"Grain pan spread must be between 0.0 and 1.0".to_string(),
));
}
if self.position < 0.0 || self.position > 1.0 {
return Err(Error::ParameterError(
"Position must be between 0.0 and 1.0".to_string(),
));
}
if self.step < -4.0 || self.step > 4.0 {
return Err(Error::ParameterError(
"Step must be between -4.0 and 4.0".to_string(),
));
}
Ok(())
}
}
pub(crate) struct GrainPool<const POOL_SIZE: usize> {
overlap_mode: GrainOverlapMode,
grain_pool: [Grain; POOL_SIZE],
active_grain_indices: Vec<usize>,
primary_grain_index: Option<usize>,
sample_buffer: Arc<Box<[f32]>>,
sample_loop_range: Option<(f32, f32)>,
playing_loop_range: bool,
trigger_new_grains: bool,
trigger_phase: f32,
speed: f64,
volume: f32,
panning: f32,
playhead: f32,
sample_rate: u32,
rng: SmallRng,
}
impl<const POOL_SIZE: usize> GrainPool<POOL_SIZE> {
const ENVELOPE_THRESHOLD: f32 = 0.001;
pub fn new(
sample_rate: u32,
sample_buffer: Arc<Box<[f32]>>,
sample_loop_range: Option<(f32, f32)>,
) -> Self {
debug_assert!(
!sample_buffer.is_empty(),
"Need a valid, non empty sample buffer"
);
debug_assert!(
sample_loop_range
.is_none_or(|l| (0.0..=1.0).contains(&l.0) && (0.0..=1.0).contains(&l.1)),
"Invalid loop points (should be relative positions), but are: {:?}",
sample_loop_range
);
let overlap_mode = GrainOverlapMode::Cloud;
let grain_pool = [Grain::new(); POOL_SIZE];
let active_grain_indices = Vec::with_capacity(POOL_SIZE);
let primary_grain_index = None;
let playing_loop_range = false;
let trigger_phase = 0.0;
let trigger_new_grains = true;
let speed = 1.0;
let volume = 1.0;
let panning = 0.0;
let playhead = 0.0;
let rng = SmallRng::from_os_rng();
Self {
overlap_mode,
grain_pool,
active_grain_indices,
primary_grain_index,
sample_buffer,
sample_loop_range,
playing_loop_range,
trigger_new_grains,
trigger_phase,
speed,
volume,
panning,
playhead,
sample_rate,
rng,
}
}
#[inline]
fn fold_into_loop_range(position: f64, loop_start: f64, loop_end: f64) -> f64 {
let loop_len = loop_end - loop_start;
if loop_len > 0.0 {
loop_start + (position - loop_start).rem_euclid(loop_len)
} else {
loop_start
}
}
pub fn is_exhausted(&self) -> bool {
!self.trigger_new_grains && self.active_grain_indices.is_empty()
}
pub fn playback_position(&self, parameters: &GranularParameters, position_mod: f32) -> f32 {
let mut base_position = if parameters.step == 0.0 {
parameters.position
} else {
self.playhead
};
if position_mod != 0.0 {
base_position += position_mod;
}
if self.playing_loop_range {
if let Some((loop_start, loop_end)) = self.sample_loop_range {
base_position = Self::fold_into_loop_range(
base_position as f64,
loop_start as f64,
loop_end as f64,
) as f32;
}
}
base_position.rem_euclid(1.0)
}
pub fn start(
&mut self,
parameters: &GranularParameters,
speed: f64,
volume: f32,
panning: f32,
) {
self.trigger_new_grains = true;
self.trigger_phase = 1.0;
self.speed = speed;
self.volume = volume;
self.panning = panning;
self.playhead = parameters.position;
self.playing_loop_range = false;
}
pub fn stop(&mut self) {
self.trigger_new_grains = false;
}
pub fn reset(&mut self) {
self.active_grain_indices.clear();
for grain in &mut self.grain_pool {
grain.deactivate();
}
self.trigger_new_grains = true;
self.primary_grain_index = None;
}
pub fn set_speed(&mut self, speed: f64) {
self.speed = speed;
}
pub fn set_volume(&mut self, volume: f32) {
self.volume = volume;
}
pub fn set_panning(&mut self, panning: f32) {
self.panning = panning;
}
pub fn set_loop_range(&mut self, loop_range: Option<(f32, f32)>) {
self.sample_loop_range = loop_range;
}
#[inline]
#[allow(clippy::too_many_arguments)]
fn try_trigger_grain(
&mut self,
parameters: &GranularParameters,
size_mod: f32,
density_mod: f32,
variation_mod: f32,
spray_mod: f32,
pan_spread_mod: f32,
position_mod: f32,
) -> bool {
if self.overlap_mode != parameters.overlap_mode {
self.overlap_mode = parameters.overlap_mode;
self.primary_grain_index = None;
}
if self.overlap_mode == GrainOverlapMode::Sequential {
if let Some(primary_index) = self.primary_grain_index {
let primary_grain = &self.grain_pool[primary_index];
if primary_grain.is_active() {
let grain_progress = primary_grain.window_phase();
let crossfade_point = parameters.window.sequential_crossfade_point();
if grain_progress < crossfade_point as f64 {
return false;
}
}
}
}
if !self.trigger_new_grains || !self.update_trigger_phase(parameters, density_mod) {
return false;
}
let spray_variation = if !self.sample_buffer.is_empty() {
let file_duration = self.sample_buffer.len() as f64 / self.sample_rate as f64;
let modulated_spray = (parameters.spray + spray_mod).clamp(0.0, 1.0);
let spray_seconds = modulated_spray as f64 * 2.0 * (self.rng.random::<f64>() - 0.5);
spray_seconds / file_duration
} else {
0.0
};
let mut grain_position =
self.playback_position(parameters, position_mod) as f64 + spray_variation;
if self.playing_loop_range {
if let Some((loop_start, loop_end)) = self.sample_loop_range {
grain_position =
Self::fold_into_loop_range(grain_position, loop_start as f64, loop_end as f64);
}
}
grain_position = grain_position.rem_euclid(1.0);
let activated_index = self.activate_new_grain(
parameters,
size_mod,
variation_mod,
pan_spread_mod,
grain_position,
);
if self.overlap_mode == GrainOverlapMode::Sequential {
if let Some(index) = activated_index {
self.primary_grain_index = Some(index);
}
}
activated_index.is_some()
}
#[inline]
fn advance_playhead(&mut self, buffer_frame_count: usize, step: f32, speed_mod: f32) {
let speed_mult = 1.0 + speed_mod;
let modulated_step = step * speed_mult;
let position_increment = modulated_step / buffer_frame_count as f32;
self.playhead += position_increment;
if let Some((loop_start, loop_end)) = self.sample_loop_range {
if self.playing_loop_range {
self.playhead = Self::fold_into_loop_range(
self.playhead as f64,
loop_start as f64,
loop_end as f64,
) as f32;
} else if self.playhead >= loop_start && self.playhead < loop_end {
self.playing_loop_range = true;
} else {
if self.playhead >= 1.0 {
self.playhead -= 1.0;
} else if self.playhead < 0.0 {
self.playhead += 1.0;
}
}
} else if self.playhead >= 1.0 {
self.playhead -= 1.0;
} else if self.playhead < 0.0 {
self.playhead += 1.0;
}
}
pub fn process(
&mut self,
mut output: &mut [f32],
channel_count: usize,
parameters: &GranularParameters,
modulation: &GranularParameterModulation,
) -> usize {
let grain_window = &*GRAIN_WINDOW_LUT;
let sample_frame_count = self.sample_buffer.len();
let move_playhead = parameters.step != 0.0 && sample_frame_count > 0;
match channel_count {
1 => {
for (frame_index, frame) in output.as_frames_mut::<1>().iter_mut().enumerate() {
self.try_trigger_grain(
parameters,
modulation.size[frame_index],
modulation.density[frame_index],
modulation.variation[frame_index],
modulation.spray[frame_index],
modulation.pan_spread[frame_index],
modulation.position[frame_index],
);
if move_playhead {
self.advance_playhead(
sample_frame_count,
parameters.step,
modulation.speed[frame_index],
);
}
for &grain_index in &self.active_grain_indices {
let grain = &mut self.grain_pool[grain_index];
if !grain.is_active() {
continue;
}
let grain_output = grain.process(grain_window);
if grain_output.envelope > Self::ENVELOPE_THRESHOLD {
let sample = self.sample_at_position(grain_output.position);
frame[0] += sample * grain_output.envelope;
}
}
}
}
2 => {
for (frame_index, frame) in output.as_frames_mut::<2>().iter_mut().enumerate() {
self.try_trigger_grain(
parameters,
modulation.size[frame_index],
modulation.density[frame_index],
modulation.variation[frame_index],
modulation.spray[frame_index],
modulation.pan_spread[frame_index],
modulation.position[frame_index],
);
if move_playhead {
self.advance_playhead(
sample_frame_count,
parameters.step,
modulation.speed[frame_index],
);
}
for &grain_index in &self.active_grain_indices {
let grain = &mut self.grain_pool[grain_index];
if grain.is_active() {
let grain_output = grain.process(grain_window);
if grain_output.envelope > Self::ENVELOPE_THRESHOLD {
let sample = self.sample_at_position(grain_output.position);
let windowed_sample = sample * grain_output.envelope;
let left_gain = (1.0 - grain_output.panning) * 0.5;
let right_gain = (1.0 + grain_output.panning) * 0.5;
frame[0] += windowed_sample * left_gain;
frame[1] += windowed_sample * right_gain;
}
}
}
}
}
_ => {
for (frame_index, frame) in output.frames_mut(channel_count).enumerate() {
self.try_trigger_grain(
parameters,
modulation.size[frame_index],
modulation.density[frame_index],
modulation.variation[frame_index],
modulation.spray[frame_index],
modulation.pan_spread[frame_index],
modulation.position[frame_index],
);
if move_playhead {
self.advance_playhead(
sample_frame_count,
parameters.step,
modulation.speed[frame_index],
);
}
let mut stereo_out = [0.0; 2];
for &grain_index in &self.active_grain_indices {
let grain = &mut self.grain_pool[grain_index];
if !grain.is_active() {
continue;
}
let grain_output = grain.process(grain_window);
if grain_output.envelope > Self::ENVELOPE_THRESHOLD {
let sample = self.sample_at_position(grain_output.position);
let windowed_sample = sample * grain_output.envelope;
let left_gain = (1.0 - grain_output.panning) * 0.5;
let right_gain = (1.0 + grain_output.panning) * 0.5;
stereo_out[0] += windowed_sample * left_gain;
stereo_out[1] += windowed_sample * right_gain;
}
}
for (channel, sample) in frame.enumerate() {
if channel < 2 {
*sample += stereo_out[channel];
}
}
}
}
}
self.active_grain_indices
.retain(|&index| self.grain_pool[index].is_active());
output.len()
}
fn update_trigger_phase(
&mut self,
granular_params: &GranularParameters,
density_mod: f32,
) -> bool {
if self.overlap_mode == GrainOverlapMode::Sequential {
return true;
}
let density_mult = 1.0 + density_mod;
let density = (granular_params.density * density_mult).clamp(1.0, 100.0);
let trigger_increment = density / self.sample_rate as f32;
self.trigger_phase += trigger_increment;
if self.trigger_phase >= 1.0 {
self.trigger_phase -= 1.0;
return true;
}
false
}
fn activate_new_grain(
&mut self,
parameters: &GranularParameters,
size_mod: f32,
variation_mod: f32,
pan_spread_mod: f32,
position: f64,
) -> Option<usize> {
if let Some(index) = self.grain_pool.iter().position(|g| !g.is_active()) {
let grain = &mut self.grain_pool[index];
let window_mode = parameters.window;
let speed = self.speed;
let variation = (parameters.variation + variation_mod).clamp(0.0, 1.0);
let volume_scale = 1.0 - (variation * self.rng.random::<f32>());
let volume = self.volume * volume_scale;
let random_semitones = variation as f64 * (self.rng.random::<f64>() - 0.5);
let speed = if random_semitones != 0.0 {
speed * 2.0_f64.powf(random_semitones / 12.0)
} else {
speed
};
let min_scale = 1.0 - (0.75 * variation);
let max_scale = 1.0 + (2.0 * variation);
let size_scale = min_scale + (max_scale - min_scale) * self.rng.random::<f32>();
let size_mult = 1.0 + size_mod;
let grain_size_ms = (parameters.size * size_mult).clamp(1.0, 1000.0);
let grain_size =
((grain_size_ms * size_scale * self.sample_rate as f32 / 1000.0) as usize).max(2);
let modulated_pan_spread = (parameters.pan_spread + pan_spread_mod).clamp(0.0, 1.0);
let panning_spread = modulated_pan_spread * (self.rng.random::<f32>() * 2.0 - 1.0);
let panning = (self.panning + panning_spread).clamp(-1.0, 1.0);
let pitch_variation_semitones =
variation * (self.rng.random::<f32>() * 2.0 - 1.0) * 0.5;
let pitch_variation_mult = 2.0_f64.powf(pitch_variation_semitones as f64 / 12.0);
let varied_speed = speed * pitch_variation_mult;
let file_length_frames = self.sample_buffer.len();
let reverse = match parameters.playback_direction {
GrainPlaybackDirection::Forward => false,
GrainPlaybackDirection::Backward => true,
GrainPlaybackDirection::Random => self.rng.random::<bool>(),
};
let loop_range = if self.playing_loop_range {
self.sample_loop_range
.map(|(start, end)| (start as f64, end as f64))
} else {
None
};
grain.activate(
window_mode,
position,
varied_speed,
volume,
panning,
grain_size,
file_length_frames,
reverse,
loop_range,
);
if let Some(position) = self.active_grain_indices.iter().position(|&v| v == index) {
self.active_grain_indices.remove(position);
}
self.active_grain_indices.push(index);
Some(index)
} else {
None
}
}
#[inline]
fn sample_at_position(&self, normalized_pos: f32) -> f32 {
let len = self.sample_buffer.len();
assume!(unsafe: len > 0, "Buffer len is asserted in constructor");
let max_index = len - 1;
let float_index = normalized_pos * max_index as f32;
let index = (float_index as usize).min(max_index);
let fraction = float_index - (index as f32);
let i1 = index;
let i2 = if i1 < max_index { i1 + 1 } else { 0 };
let i0 = if i1 > 0 { i1 - 1 } else { max_index };
let i3 = if i2 < max_index { i2 + 1 } else { 0 };
assume!(unsafe: i0 < len);
let y0 = self.sample_buffer[i0];
assume!(unsafe: i1 < len);
let y1 = self.sample_buffer[i1];
assume!(unsafe: i2 < len);
let y2 = self.sample_buffer[i2];
assume!(unsafe: i3 < len);
let y3 = self.sample_buffer[i3];
let a = -0.5 * y0 + 1.5 * y1 - 1.5 * y2 + 0.5 * y3;
let b = y0 - 2.5 * y1 + 2.0 * y2 - 0.5 * y3;
let c = -0.5 * y0 + 0.5 * y2;
let d = y1;
a * fraction * fraction * fraction + b * fraction * fraction + c * fraction + d
}
}
#[derive(Debug, Copy, Clone)]
struct GrainOutput {
envelope: f32,
panning: f32,
position: f32,
}
#[derive(Debug, Clone, Copy)]
struct Grain {
active: bool,
volume: f32,
panning: f32,
position: f64,
increment: f64,
samples_remaining: usize,
window_phase: f64,
window_increment: f64,
window_mode: GrainWindowMode,
loop_range: Option<(f64, f64)>,
}
impl Default for Grain {
fn default() -> Self {
Self::new()
}
}
impl Grain {
pub const fn new() -> Self {
Self {
active: false,
position: 0.0,
volume: 1.0,
panning: 0.0,
increment: 0.0,
samples_remaining: 0,
window_phase: 0.0,
window_increment: 0.0,
window_mode: GrainWindowMode::Triangle,
loop_range: None,
}
}
#[inline]
pub fn is_active(&self) -> bool {
self.active
}
#[inline]
pub fn window_phase(&self) -> f64 {
self.window_phase
}
#[allow(clippy::too_many_arguments)]
pub fn activate(
&mut self,
window_mode: GrainWindowMode,
position: f64,
speed: f64,
volume: f32,
panning: f32,
grain_size_samples: usize,
file_length_frames: usize,
reverse: bool,
loop_range: Option<(f64, f64)>,
) {
self.active = true;
self.window_mode = window_mode;
self.position = position.clamp(0.0, 1.0);
self.volume = volume.clamp(0.0, 100.0);
self.panning = panning.clamp(-1.0, 1.0);
self.samples_remaining = grain_size_samples;
self.loop_range = loop_range;
let base_increment = if file_length_frames > 0 {
speed / file_length_frames as f64
} else {
0.0
};
self.increment = base_increment * if reverse { -1.0 } else { 1.0 };
self.window_phase = 0.0;
if grain_size_samples > 0 {
self.window_increment = 1.0 / grain_size_samples as f64;
} else {
self.window_increment = 0.0;
}
}
#[allow(dead_code)]
pub fn deactivate(&mut self) {
self.active = false;
self.samples_remaining = 0;
}
pub fn process(&mut self, grain_window: &GrainWindow<2048>) -> GrainOutput {
#[cfg(not(test))]
debug_assert!(self.active, "Should only process active grains");
let envelope_value = grain_window.sample(self.window_mode, self.window_phase);
let position = self.position as f32;
self.position += self.increment;
self.window_phase += self.window_increment;
self.samples_remaining = self.samples_remaining.saturating_sub(1);
if let Some((loop_start, loop_end)) = self.loop_range {
let loop_len = loop_end - loop_start;
if loop_len > 0.0 {
self.position = loop_start + (self.position - loop_start).rem_euclid(loop_len);
}
} else if self.position < 0.0 {
self.position += 1.0;
} else if self.position > 1.0 {
self.position -= 1.0;
}
if self.samples_remaining == 0 {
self.active = false;
}
let envelope = envelope_value * self.volume;
let panning = self.panning;
GrainOutput {
envelope,
panning,
position,
}
}
}