1use std::sync::{Arc, LazyLock};
4
5use rand::{rngs::SmallRng, Rng, SeedableRng};
6
7use assume::assume;
8use strum::EnumCount;
9
10use crate::{utils::buffer::InterleavedBufferMut, Error};
11
12#[derive(
16 Clone, Copy, Debug, PartialEq, Eq, strum::EnumString, strum::Display, strum::VariantNames,
17)]
18#[repr(u8)]
19pub enum GrainPlaybackDirection {
20 Forward,
22 Backward,
24 Random,
26}
27
28#[derive(
32 Clone, Copy, Debug, PartialEq, Eq, strum::EnumString, strum::Display, strum::VariantNames,
33)]
34#[repr(u8)]
35pub enum GrainOverlapMode {
36 Cloud,
40 Sequential,
45}
46
47#[derive(
50 Clone,
51 Copy,
52 Debug,
53 PartialEq,
54 Eq,
55 strum::EnumString,
56 strum::Display,
57 strum::VariantNames,
58 strum::EnumCount,
59)]
60#[repr(u8)]
61pub enum GrainWindowMode {
62 Hann = 0,
63 Blackman = 1,
64 Triangle = 2,
65 Tukey = 3,
66 Trapezoid = 4,
67 Exponential = 5,
68 RampUp = 6,
69 RampDown = 7,
70}
71
72impl GrainWindowMode {
73 pub fn sequential_crossfade_point(&self) -> f32 {
79 match self {
80 GrainWindowMode::Hann
82 | GrainWindowMode::Blackman
83 | GrainWindowMode::Triangle
84 | GrainWindowMode::Tukey => 0.5,
85
86 GrainWindowMode::Trapezoid => 0.9,
88
89 GrainWindowMode::Exponential | GrainWindowMode::RampUp | GrainWindowMode::RampDown => {
91 0.8
92 }
93 }
94 }
95}
96
97pub(crate) struct GrainWindow<const N: usize> {
100 luts: [[f32; N]; GrainWindowMode::COUNT],
101}
102
103impl<const N: usize> GrainWindow<N> {
104 const _VERIFY_N: () = assert!(
106 N.is_power_of_two(),
107 "Grain window size must be a pow2 value"
108 );
109 const MASK: usize = N - 1;
110
111 pub fn new() -> Self {
113 let mut luts = [[0.0; N]; GrainWindowMode::COUNT];
114
115 #[allow(clippy::needless_range_loop)]
116 for i in 0..N {
117 let phase = i as f32 / N as f32; luts[GrainWindowMode::Hann as usize][i] =
123 0.5 * (1.0 - (2.0 * std::f32::consts::PI * phase).cos());
124
125 let pi_phase = std::f32::consts::PI * phase;
129 luts[GrainWindowMode::Blackman as usize][i] =
130 0.42 - 0.5 * (2.0 * pi_phase).cos() + 0.08 * (4.0 * pi_phase).cos();
131
132 luts[GrainWindowMode::Triangle as usize][i] = if phase < 0.5 {
134 2.0 * phase
135 } else {
136 2.0 * (1.0 - phase)
137 };
138
139 let alpha = 0.5;
143 let width = alpha / 2.0;
144 luts[GrainWindowMode::Tukey as usize][i] = if phase < width {
145 let u = phase / width;
146 0.5 * (1.0 - (std::f32::consts::PI * u).cos())
147 } else if phase > 1.0 - width {
148 let u = (1.0 - phase) / width;
149 0.5 * (1.0 - (std::f32::consts::PI * u).cos())
150 } else {
151 1.0
152 };
153
154 let ramp_width = 0.1;
157 luts[GrainWindowMode::Trapezoid as usize][i] = if phase < ramp_width {
158 phase / ramp_width
159 } else if phase > 1.0 - ramp_width {
160 (1.0 - phase) / ramp_width
161 } else {
162 1.0
163 };
164
165 let decay_rate = 6.0;
169 let center_dist = (phase - 0.5).abs();
170 luts[GrainWindowMode::Exponential as usize][i] = (-decay_rate * center_dist).exp();
171
172 luts[GrainWindowMode::RampUp as usize][i] = if phase < 0.9 {
175 phase / 0.9
177 } else {
178 let u = (phase - 0.9) / 0.1;
180 0.5 * (1.0 + (std::f32::consts::PI * u).cos())
181 };
182
183 luts[GrainWindowMode::RampDown as usize][i] = if phase < 0.1 {
186 let u = phase / 0.1;
188 0.5 * (1.0 - (std::f32::consts::PI * u).cos())
189 } else {
190 1.0 - ((phase - 0.1) / 0.9)
192 };
193 }
194
195 Self { luts }
196 }
197
198 #[inline]
201 pub fn sample(&self, mode: GrainWindowMode, phase: f64) -> f32 {
202 debug_assert!((0.0..=1.0).contains(&phase));
203
204 let index_float = phase * (N - 1) as f64;
205 let index = (index_float as usize) & Self::MASK;
206 let fraction = index_float.fract() as f32;
207 let next_index = (index + 1) & Self::MASK;
208
209 let lut = &self.luts[mode as usize];
210 if index < N - 1 {
211 lut[index] * (1.0 - fraction) + lut[next_index] * fraction
212 } else {
213 lut[N - 1]
214 }
215 }
216}
217
218static GRAIN_WINDOW_LUT: LazyLock<GrainWindow<2048>> = LazyLock::new(GrainWindow::new);
222
223pub(crate) struct GranularParameterModulation<'a> {
228 pub size: &'a [f32],
229 pub density: &'a [f32],
230 pub variation: &'a [f32],
231 pub spray: &'a [f32],
232 pub pan_spread: &'a [f32],
233 pub position: &'a [f32],
234 pub speed: &'a [f32],
235}
236
237#[derive(Clone, Debug)]
241pub struct GranularParameters {
242 pub overlap_mode: GrainOverlapMode,
244 pub window: GrainWindowMode,
246 pub size: f32,
248 pub density: f32,
251 pub variation: f32,
254 pub spray: f32,
257 pub pan_spread: f32,
260 pub playback_direction: GrainPlaybackDirection,
262 pub position: f32,
264 pub step: f32,
266}
267
268impl Default for GranularParameters {
269 fn default() -> Self {
270 Self {
271 overlap_mode: GrainOverlapMode::Cloud,
272 window: GrainWindowMode::Triangle,
273 size: 100.0,
274 density: 10.0,
275 spray: 0.0,
276 variation: 0.0,
277 pan_spread: 0.0,
278 playback_direction: GrainPlaybackDirection::Forward,
279 position: 0.5,
280 step: 0.0,
281 }
282 }
283}
284
285impl GranularParameters {
286 pub fn new() -> Self {
287 Self::default()
288 }
289
290 pub fn validate(&self) -> Result<(), Error> {
292 if self.size < 1.0 || self.size > 1000.0 {
293 return Err(Error::ParameterError(
294 "Grain size must be between 1 and 1000 ms".to_string(),
295 ));
296 }
297
298 if self.density < 1.0 || self.density > 100.0 {
299 return Err(Error::ParameterError(
300 "Grain density must be between 1.0 and 100.0 Hz".to_string(),
301 ));
302 }
303
304 if self.spray < 0.0 || self.spray > 1.0 {
305 return Err(Error::ParameterError(
306 "Grain spray must be between 0.0 and 1.0".to_string(),
307 ));
308 }
309
310 if self.variation < 0.0 || self.variation > 1.0 {
311 return Err(Error::ParameterError(
312 "Grain variation must be between 0.0 and 1.0".to_string(),
313 ));
314 }
315
316 if self.pan_spread < 0.0 || self.pan_spread > 1.0 {
317 return Err(Error::ParameterError(
318 "Grain pan spread must be between 0.0 and 1.0".to_string(),
319 ));
320 }
321
322 if self.position < 0.0 || self.position > 1.0 {
323 return Err(Error::ParameterError(
324 "Position must be between 0.0 and 1.0".to_string(),
325 ));
326 }
327
328 if self.step < -4.0 || self.step > 4.0 {
329 return Err(Error::ParameterError(
330 "Step must be between -4.0 and 4.0".to_string(),
331 ));
332 }
333
334 Ok(())
335 }
336}
337
338pub(crate) struct GrainPool<const POOL_SIZE: usize> {
346 overlap_mode: GrainOverlapMode,
348 grain_pool: [Grain; POOL_SIZE],
350 active_grain_indices: Vec<usize>,
352 primary_grain_index: Option<usize>,
354 sample_buffer: Arc<Box<[f32]>>,
356 sample_loop_range: Option<(f32, f32)>,
358 playing_loop_range: bool,
360 trigger_new_grains: bool,
362 trigger_phase: f32,
365 speed: f64,
367 volume: f32,
369 panning: f32,
371 playhead: f32,
373 sample_rate: u32,
375 rng: SmallRng,
377}
378
379impl<const POOL_SIZE: usize> GrainPool<POOL_SIZE> {
380 const ENVELOPE_THRESHOLD: f32 = 0.001; pub fn new(
385 sample_rate: u32,
386 sample_buffer: Arc<Box<[f32]>>,
387 sample_loop_range: Option<(f32, f32)>,
388 ) -> Self {
389 debug_assert!(
390 !sample_buffer.is_empty(),
391 "Need a valid, non empty sample buffer"
392 );
393 debug_assert!(
394 sample_loop_range
395 .is_none_or(|l| (0.0..=1.0).contains(&l.0) && (0.0..=1.0).contains(&l.1)),
396 "Invalid loop points (should be relative positions), but are: {:?}",
397 sample_loop_range
398 );
399 let overlap_mode = GrainOverlapMode::Cloud;
400 let grain_pool = [Grain::new(); POOL_SIZE];
401 let active_grain_indices = Vec::with_capacity(POOL_SIZE);
402 let primary_grain_index = None;
403 let playing_loop_range = false;
404 let trigger_phase = 0.0;
405 let trigger_new_grains = true;
406 let speed = 1.0;
407 let volume = 1.0;
408 let panning = 0.0;
409 let playhead = 0.0;
410 let rng = SmallRng::from_os_rng();
411
412 Self {
413 overlap_mode,
414 grain_pool,
415 active_grain_indices,
416 primary_grain_index,
417 sample_buffer,
418 sample_loop_range,
419 playing_loop_range,
420 trigger_new_grains,
421 trigger_phase,
422 speed,
423 volume,
424 panning,
425 playhead,
426 sample_rate,
427 rng,
428 }
429 }
430
431 #[inline]
433 fn fold_into_loop_range(position: f64, loop_start: f64, loop_end: f64) -> f64 {
434 let loop_len = loop_end - loop_start;
435 if loop_len > 0.0 {
436 loop_start + (position - loop_start).rem_euclid(loop_len)
437 } else {
438 loop_start
439 }
440 }
441
442 pub fn is_exhausted(&self) -> bool {
443 !self.trigger_new_grains && self.active_grain_indices.is_empty()
444 }
445
446 pub fn playback_position(&self, parameters: &GranularParameters, position_mod: f32) -> f32 {
447 let mut base_position = if parameters.step == 0.0 {
449 parameters.position
450 } else {
451 self.playhead
452 };
453
454 if position_mod != 0.0 {
456 base_position += position_mod;
457 }
458
459 if self.playing_loop_range {
461 if let Some((loop_start, loop_end)) = self.sample_loop_range {
462 base_position = Self::fold_into_loop_range(
463 base_position as f64,
464 loop_start as f64,
465 loop_end as f64,
466 ) as f32;
467 }
468 }
469
470 base_position.rem_euclid(1.0)
472 }
473
474 pub fn start(
475 &mut self,
476 parameters: &GranularParameters,
477 speed: f64,
478 volume: f32,
479 panning: f32,
480 ) {
481 self.trigger_new_grains = true;
482 self.trigger_phase = 1.0;
483
484 self.speed = speed;
485 self.volume = volume;
486 self.panning = panning;
487 self.playhead = parameters.position;
488 self.playing_loop_range = false;
489 }
490
491 pub fn stop(&mut self) {
492 self.trigger_new_grains = false;
493 }
494
495 pub fn reset(&mut self) {
496 self.active_grain_indices.clear();
497 for grain in &mut self.grain_pool {
498 grain.deactivate();
499 }
500 self.trigger_new_grains = true;
501 self.primary_grain_index = None;
502 }
503
504 pub fn set_speed(&mut self, speed: f64) {
505 self.speed = speed;
506 }
507
508 pub fn set_volume(&mut self, volume: f32) {
509 self.volume = volume;
510 }
511
512 pub fn set_panning(&mut self, panning: f32) {
513 self.panning = panning;
514 }
515
516 pub fn set_loop_range(&mut self, loop_range: Option<(f32, f32)>) {
517 self.sample_loop_range = loop_range;
518 }
519
520 #[inline]
523 #[allow(clippy::too_many_arguments)]
524 fn try_trigger_grain(
525 &mut self,
526 parameters: &GranularParameters,
527 size_mod: f32,
528 density_mod: f32,
529 variation_mod: f32,
530 spray_mod: f32,
531 pan_spread_mod: f32,
532 position_mod: f32,
533 ) -> bool {
534 if self.overlap_mode != parameters.overlap_mode {
536 self.overlap_mode = parameters.overlap_mode;
537 self.primary_grain_index = None;
538 }
539
540 if self.overlap_mode == GrainOverlapMode::Sequential {
542 if let Some(primary_index) = self.primary_grain_index {
543 let primary_grain = &self.grain_pool[primary_index];
544 if primary_grain.is_active() {
545 let grain_progress = primary_grain.window_phase();
547 let crossfade_point = parameters.window.sequential_crossfade_point();
548
549 if grain_progress < crossfade_point as f64 {
551 return false;
552 }
553 }
554 }
555 }
556
557 if !self.trigger_new_grains || !self.update_trigger_phase(parameters, density_mod) {
558 return false;
559 }
560
561 let spray_variation = if !self.sample_buffer.is_empty() {
563 let file_duration = self.sample_buffer.len() as f64 / self.sample_rate as f64;
564 let modulated_spray = (parameters.spray + spray_mod).clamp(0.0, 1.0);
566 let spray_seconds = modulated_spray as f64 * 2.0 * (self.rng.random::<f64>() - 0.5);
568 spray_seconds / file_duration
569 } else {
570 0.0
571 };
572
573 let mut grain_position =
575 self.playback_position(parameters, position_mod) as f64 + spray_variation;
576 if self.playing_loop_range {
578 if let Some((loop_start, loop_end)) = self.sample_loop_range {
579 grain_position =
580 Self::fold_into_loop_range(grain_position, loop_start as f64, loop_end as f64);
581 }
582 }
583 grain_position = grain_position.rem_euclid(1.0);
585
586 let activated_index = self.activate_new_grain(
588 parameters,
589 size_mod,
590 variation_mod,
591 pan_spread_mod,
592 grain_position,
593 );
594
595 if self.overlap_mode == GrainOverlapMode::Sequential {
597 if let Some(index) = activated_index {
598 self.primary_grain_index = Some(index);
599 }
600 }
601
602 activated_index.is_some()
603 }
604
605 #[inline]
607 fn advance_playhead(&mut self, buffer_frame_count: usize, step: f32, speed_mod: f32) {
608 let speed_mult = 1.0 + speed_mod;
610 let modulated_step = step * speed_mult;
611
612 let position_increment = modulated_step / buffer_frame_count as f32;
614 self.playhead += position_increment;
615
616 if let Some((loop_start, loop_end)) = self.sample_loop_range {
618 if self.playing_loop_range {
619 self.playhead = Self::fold_into_loop_range(
620 self.playhead as f64,
621 loop_start as f64,
622 loop_end as f64,
623 ) as f32;
624 } else if self.playhead >= loop_start && self.playhead < loop_end {
625 self.playing_loop_range = true;
627 } else {
628 if self.playhead >= 1.0 {
630 self.playhead -= 1.0;
631 } else if self.playhead < 0.0 {
632 self.playhead += 1.0;
633 }
634 }
635 } else if self.playhead >= 1.0 {
636 self.playhead -= 1.0;
637 } else if self.playhead < 0.0 {
638 self.playhead += 1.0;
639 }
640 }
641
642 pub fn process(
643 &mut self,
644 mut output: &mut [f32],
645 channel_count: usize,
646 parameters: &GranularParameters,
647 modulation: &GranularParameterModulation,
648 ) -> usize {
649 let grain_window = &*GRAIN_WINDOW_LUT;
650
651 let sample_frame_count = self.sample_buffer.len();
652 let move_playhead = parameters.step != 0.0 && sample_frame_count > 0;
653
654 match channel_count {
656 1 => {
657 for (frame_index, frame) in output.as_frames_mut::<1>().iter_mut().enumerate() {
659 self.try_trigger_grain(
661 parameters,
662 modulation.size[frame_index],
663 modulation.density[frame_index],
664 modulation.variation[frame_index],
665 modulation.spray[frame_index],
666 modulation.pan_spread[frame_index],
667 modulation.position[frame_index],
668 );
669 if move_playhead {
671 self.advance_playhead(
672 sample_frame_count,
673 parameters.step,
674 modulation.speed[frame_index],
675 );
676 }
677 for &grain_index in &self.active_grain_indices {
679 let grain = &mut self.grain_pool[grain_index];
680 if !grain.is_active() {
681 continue;
682 }
683 let grain_output = grain.process(grain_window);
684 if grain_output.envelope > Self::ENVELOPE_THRESHOLD {
685 let sample = self.sample_at_position(grain_output.position);
686 frame[0] += sample * grain_output.envelope;
687 }
688 }
689 }
690 }
691 2 => {
692 for (frame_index, frame) in output.as_frames_mut::<2>().iter_mut().enumerate() {
694 self.try_trigger_grain(
696 parameters,
697 modulation.size[frame_index],
698 modulation.density[frame_index],
699 modulation.variation[frame_index],
700 modulation.spray[frame_index],
701 modulation.pan_spread[frame_index],
702 modulation.position[frame_index],
703 );
704 if move_playhead {
706 self.advance_playhead(
707 sample_frame_count,
708 parameters.step,
709 modulation.speed[frame_index],
710 );
711 }
712 for &grain_index in &self.active_grain_indices {
714 let grain = &mut self.grain_pool[grain_index];
715 if grain.is_active() {
716 let grain_output = grain.process(grain_window);
717 if grain_output.envelope > Self::ENVELOPE_THRESHOLD {
718 let sample = self.sample_at_position(grain_output.position);
719 let windowed_sample = sample * grain_output.envelope;
720
721 let left_gain = (1.0 - grain_output.panning) * 0.5;
722 let right_gain = (1.0 + grain_output.panning) * 0.5;
723 frame[0] += windowed_sample * left_gain;
724 frame[1] += windowed_sample * right_gain;
725 }
726 }
727 }
728 }
729 }
730 _ => {
731 for (frame_index, frame) in output.frames_mut(channel_count).enumerate() {
733 self.try_trigger_grain(
735 parameters,
736 modulation.size[frame_index],
737 modulation.density[frame_index],
738 modulation.variation[frame_index],
739 modulation.spray[frame_index],
740 modulation.pan_spread[frame_index],
741 modulation.position[frame_index],
742 );
743 if move_playhead {
745 self.advance_playhead(
746 sample_frame_count,
747 parameters.step,
748 modulation.speed[frame_index],
749 );
750 }
751 let mut stereo_out = [0.0; 2];
753 for &grain_index in &self.active_grain_indices {
754 let grain = &mut self.grain_pool[grain_index];
755 if !grain.is_active() {
756 continue;
757 }
758 let grain_output = grain.process(grain_window);
759 if grain_output.envelope > Self::ENVELOPE_THRESHOLD {
760 let sample = self.sample_at_position(grain_output.position);
761 let windowed_sample = sample * grain_output.envelope;
762
763 let left_gain = (1.0 - grain_output.panning) * 0.5;
764 let right_gain = (1.0 + grain_output.panning) * 0.5;
765 stereo_out[0] += windowed_sample * left_gain;
766 stereo_out[1] += windowed_sample * right_gain;
767 }
768 }
769 for (channel, sample) in frame.enumerate() {
771 if channel < 2 {
772 *sample += stereo_out[channel];
773 }
774 }
775 }
776 }
777 }
778
779 self.active_grain_indices
781 .retain(|&index| self.grain_pool[index].is_active());
782
783 output.len()
784 }
785
786 fn update_trigger_phase(
789 &mut self,
790 granular_params: &GranularParameters,
791 density_mod: f32,
792 ) -> bool {
793 if self.overlap_mode == GrainOverlapMode::Sequential {
795 return true;
796 }
797 let density_mult = 1.0 + density_mod;
799 let density = (granular_params.density * density_mult).clamp(1.0, 100.0);
800
801 let trigger_increment = density / self.sample_rate as f32;
802 self.trigger_phase += trigger_increment;
803
804 if self.trigger_phase >= 1.0 {
805 self.trigger_phase -= 1.0;
806 return true;
807 }
808 false
809 }
810
811 fn activate_new_grain(
814 &mut self,
815 parameters: &GranularParameters,
816 size_mod: f32,
817 variation_mod: f32,
818 pan_spread_mod: f32,
819 position: f64,
820 ) -> Option<usize> {
821 if let Some(index) = self.grain_pool.iter().position(|g| !g.is_active()) {
822 let grain = &mut self.grain_pool[index];
823 let window_mode = parameters.window;
824 let speed = self.speed;
825
826 let variation = (parameters.variation + variation_mod).clamp(0.0, 1.0);
828
829 let volume_scale = 1.0 - (variation * self.rng.random::<f32>());
831 let volume = self.volume * volume_scale;
832
833 let random_semitones = variation as f64 * (self.rng.random::<f64>() - 0.5);
835 let speed = if random_semitones != 0.0 {
836 speed * 2.0_f64.powf(random_semitones / 12.0)
837 } else {
838 speed
839 };
840
841 let min_scale = 1.0 - (0.75 * variation);
843 let max_scale = 1.0 + (2.0 * variation);
844 let size_scale = min_scale + (max_scale - min_scale) * self.rng.random::<f32>();
845
846 let size_mult = 1.0 + size_mod;
849 let grain_size_ms = (parameters.size * size_mult).clamp(1.0, 1000.0);
850
851 let grain_size =
852 ((grain_size_ms * size_scale * self.sample_rate as f32 / 1000.0) as usize).max(2);
853
854 let modulated_pan_spread = (parameters.pan_spread + pan_spread_mod).clamp(0.0, 1.0);
856 let panning_spread = modulated_pan_spread * (self.rng.random::<f32>() * 2.0 - 1.0);
857 let panning = (self.panning + panning_spread).clamp(-1.0, 1.0);
858
859 let pitch_variation_semitones =
861 variation * (self.rng.random::<f32>() * 2.0 - 1.0) * 0.5;
862 let pitch_variation_mult = 2.0_f64.powf(pitch_variation_semitones as f64 / 12.0);
863 let varied_speed = speed * pitch_variation_mult;
864
865 let file_length_frames = self.sample_buffer.len();
866 let reverse = match parameters.playback_direction {
867 GrainPlaybackDirection::Forward => false,
868 GrainPlaybackDirection::Backward => true,
869 GrainPlaybackDirection::Random => self.rng.random::<bool>(),
870 };
871 let loop_range = if self.playing_loop_range {
872 self.sample_loop_range
873 .map(|(start, end)| (start as f64, end as f64))
874 } else {
875 None
876 };
877 grain.activate(
878 window_mode,
879 position,
880 varied_speed,
881 volume,
882 panning,
883 grain_size,
884 file_length_frames,
885 reverse,
886 loop_range,
887 );
888 if let Some(position) = self.active_grain_indices.iter().position(|&v| v == index) {
889 self.active_grain_indices.remove(position);
891 }
892 self.active_grain_indices.push(index);
893 Some(index)
894 } else {
895 None
896 }
897 }
898
899 #[inline]
901 fn sample_at_position(&self, normalized_pos: f32) -> f32 {
902 let len = self.sample_buffer.len();
903
904 assume!(unsafe: len > 0, "Buffer len is asserted in constructor");
905 let max_index = len - 1;
906 let float_index = normalized_pos * max_index as f32;
907
908 let index = (float_index as usize).min(max_index);
909 let fraction = float_index - (index as f32);
910
911 let i1 = index;
913 let i2 = if i1 < max_index { i1 + 1 } else { 0 };
914 let i0 = if i1 > 0 { i1 - 1 } else { max_index };
915 let i3 = if i2 < max_index { i2 + 1 } else { 0 };
916
917 assume!(unsafe: i0 < len);
918 let y0 = self.sample_buffer[i0];
919 assume!(unsafe: i1 < len);
920 let y1 = self.sample_buffer[i1];
921 assume!(unsafe: i2 < len);
922 let y2 = self.sample_buffer[i2];
923 assume!(unsafe: i3 < len);
924 let y3 = self.sample_buffer[i3];
925
926 let a = -0.5 * y0 + 1.5 * y1 - 1.5 * y2 + 0.5 * y3;
928 let b = y0 - 2.5 * y1 + 2.0 * y2 - 0.5 * y3;
929 let c = -0.5 * y0 + 0.5 * y2;
930 let d = y1;
931
932 a * fraction * fraction * fraction + b * fraction * fraction + c * fraction + d
933 }
934}
935
936#[derive(Debug, Copy, Clone)]
944struct GrainOutput {
945 envelope: f32,
947 panning: f32,
949 position: f32,
951}
952
953#[derive(Debug, Clone, Copy)]
961struct Grain {
962 active: bool,
964 volume: f32,
966 panning: f32,
968 position: f64,
971 increment: f64,
974 samples_remaining: usize,
977 window_phase: f64,
979 window_increment: f64,
981 window_mode: GrainWindowMode,
983 loop_range: Option<(f64, f64)>,
985}
986
987impl Default for Grain {
988 fn default() -> Self {
989 Self::new()
990 }
991}
992
993impl Grain {
994 pub const fn new() -> Self {
996 Self {
997 active: false,
998 position: 0.0,
999 volume: 1.0,
1000 panning: 0.0,
1001 increment: 0.0,
1002 samples_remaining: 0,
1003 window_phase: 0.0,
1004 window_increment: 0.0,
1005 window_mode: GrainWindowMode::Triangle,
1006 loop_range: None,
1007 }
1008 }
1009
1010 #[inline]
1012 pub fn is_active(&self) -> bool {
1013 self.active
1014 }
1015
1016 #[inline]
1019 pub fn window_phase(&self) -> f64 {
1020 self.window_phase
1021 }
1022
1023 #[allow(clippy::too_many_arguments)]
1025 pub fn activate(
1026 &mut self,
1027 window_mode: GrainWindowMode,
1028 position: f64,
1029 speed: f64,
1030 volume: f32,
1031 panning: f32,
1032 grain_size_samples: usize,
1033 file_length_frames: usize,
1034 reverse: bool,
1035 loop_range: Option<(f64, f64)>,
1036 ) {
1037 self.active = true;
1038 self.window_mode = window_mode;
1039 self.position = position.clamp(0.0, 1.0);
1040 self.volume = volume.clamp(0.0, 100.0);
1041 self.panning = panning.clamp(-1.0, 1.0);
1042 self.samples_remaining = grain_size_samples;
1043 self.loop_range = loop_range;
1044
1045 let base_increment = if file_length_frames > 0 {
1051 speed / file_length_frames as f64
1052 } else {
1053 0.0
1054 };
1055
1056 self.increment = base_increment * if reverse { -1.0 } else { 1.0 };
1057
1058 self.window_phase = 0.0;
1061 if grain_size_samples > 0 {
1062 self.window_increment = 1.0 / grain_size_samples as f64;
1064 } else {
1065 self.window_increment = 0.0;
1066 }
1067 }
1068
1069 #[allow(dead_code)]
1071 pub fn deactivate(&mut self) {
1072 self.active = false;
1073 self.samples_remaining = 0;
1074 }
1075
1076 pub fn process(&mut self, grain_window: &GrainWindow<2048>) -> GrainOutput {
1082 #[cfg(not(test))]
1083 debug_assert!(self.active, "Should only process active grains");
1084
1085 let envelope_value = grain_window.sample(self.window_mode, self.window_phase);
1086
1087 let position = self.position as f32;
1089
1090 self.position += self.increment;
1092 self.window_phase += self.window_increment;
1093 self.samples_remaining = self.samples_remaining.saturating_sub(1);
1094
1095 if let Some((loop_start, loop_end)) = self.loop_range {
1097 let loop_len = loop_end - loop_start;
1098 if loop_len > 0.0 {
1099 self.position = loop_start + (self.position - loop_start).rem_euclid(loop_len);
1100 }
1101 } else if self.position < 0.0 {
1102 self.position += 1.0;
1103 } else if self.position > 1.0 {
1104 self.position -= 1.0;
1105 }
1106
1107 if self.samples_remaining == 0 {
1109 self.active = false;
1110 }
1111
1112 let envelope = envelope_value * self.volume;
1113 let panning = self.panning;
1114
1115 GrainOutput {
1116 envelope,
1117 panning,
1118 position,
1119 }
1120 }
1121}