caw_modules/
super_saw.rs

1use caw_builder_proc_macros::builder;
2use caw_core::{Buf, Sig, SigCtx, SigT};
3use itertools::izip;
4use rand::{Rng, SeedableRng, rngs::StdRng};
5use wide::f32x8;
6
7#[derive(Debug, Clone, Copy)]
8pub enum SuperSawInit {
9    Random,
10    RandomWithSeed(u64),
11    Const(f32),
12}
13
14pub struct SuperSaw<F, D>
15where
16    F: SigT<Item = f32>,
17    D: SigT<Item = f32>,
18{
19    freq_hz: F,
20    detune_ratio: D,
21    state_01: Vec<f32x8>,
22    // Used to deal with the case where the number of oscillators is not divisible by 8. Each
23    // element is either 0 or 1, with 0s in the positions of the left over values in the final
24    // f32x8 in state_01.
25    mask: Vec<f32x8>,
26    // Also used to deal with the case where the number of oscillators is not divisible by 8.
27    // Stores half of the number of oscillators in the corresponding f32x8
28    offset_per_f32x8: Vec<f32>,
29    num_oscillators: usize,
30    buf: Vec<f32>,
31}
32
33impl<F, D> SigT for SuperSaw<F, D>
34where
35    F: SigT<Item = f32>,
36    D: SigT<Item = f32>,
37{
38    type Item = f32;
39
40    fn sample(&mut self, ctx: &SigCtx) -> impl Buf<Self::Item> {
41        const RANGE_0_7: [f32; 8] = [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0];
42        let range_0_7: f32x8 = RANGE_0_7.into();
43        // scales the sum of num_oscillators values in the range 0..1 to a value in the range -1..1
44        let scale_factor = 2.0 / (self.num_oscillators as f32);
45        let freq_hz = self.freq_hz.sample(ctx);
46        let detune_ratio = self.detune_ratio.sample(ctx);
47        self.buf.resize(ctx.num_samples, 0.0);
48        for (sample, freq_hz, detune_ratio) in izip! {
49            self.buf.iter_mut(),
50            freq_hz.iter(),
51            detune_ratio.iter(),
52        } {
53            *sample = 0.0;
54            let (mut freq_hz, detune_step_hz) = if self.num_oscillators == 1 {
55                (f32x8::splat(freq_hz), 0.0)
56            } else {
57                let detune_hz = freq_hz * detune_ratio;
58                // Split the detune evenly among each oscillator.
59                let detune_step_hz =
60                    detune_hz / ((self.num_oscillators - 1) as f32);
61                let detune_offsets_hz = range_0_7 * detune_step_hz;
62                let min_freq_hz = freq_hz - (detune_hz / 2.0);
63                (detune_offsets_hz + min_freq_hz, detune_step_hz)
64            };
65            for ((state_01, mask), offset) in self
66                .state_01
67                .iter_mut()
68                .zip(self.mask.iter())
69                .zip(self.offset_per_f32x8.iter())
70            {
71                let state_delta = freq_hz / ctx.sample_rate_hz;
72                // Continue the ramp of the saw wave.
73                *state_01 += state_delta * mask;
74                // Subtract the rounded-down value from itself so that the wave wraps around from 1
75                // to 0.
76                *state_01 = *state_01 - (*state_01 - 0.5).round();
77                let total_with_offset = state_01.reduce_add() - offset;
78                *sample += total_with_offset * scale_factor;
79                freq_hz = freq_hz + (detune_step_hz * 8.0);
80            }
81        }
82        &self.buf
83    }
84}
85
86impl<F, D> SuperSaw<F, D>
87where
88    F: SigT<Item = f32>,
89    D: SigT<Item = f32>,
90{
91    pub fn new(
92        freq_hz: F,
93        detune_ratio: D,
94        num_oscillators: usize,
95        init: SuperSawInit,
96    ) -> Sig<Self> {
97        assert!(
98            num_oscillators >= 1,
99            "There must be at least one oscillator"
100        );
101        let num_f32x8s = num_oscillators / 8;
102        let mut state_01 = vec![f32x8::ZERO; num_f32x8s];
103        let mut mask = vec![f32x8::ONE; num_f32x8s];
104        let mut offset_per_f32x8 = vec![4.0; num_f32x8s];
105        if num_oscillators % 8 != 0 {
106            state_01.push(f32x8::ZERO);
107            let mut final_mask = [0.0; 8];
108            for x in final_mask.iter_mut().take(num_oscillators % 8) {
109                *x = 1.0;
110            }
111            mask.push(final_mask.into());
112            offset_per_f32x8.push((num_oscillators % 8) as f32 / 2.0);
113        }
114        match init {
115            SuperSawInit::Const(c) => {
116                for (x, mask) in state_01.iter_mut().zip(mask.iter()) {
117                    *x = f32x8::splat(c);
118                    *x *= mask;
119                }
120            }
121            SuperSawInit::Random => {
122                let mut rng = StdRng::from_os_rng();
123                for (x, mask) in state_01.iter_mut().zip(mask.iter()) {
124                    *x = rng.random::<[f32; 8]>().into();
125                    *x *= mask;
126                }
127            }
128            SuperSawInit::RandomWithSeed(seed) => {
129                let mut rng = StdRng::seed_from_u64(seed);
130                for (x, mask) in state_01.iter_mut().zip(mask.iter()) {
131                    *x = rng.random::<[f32; 8]>().into();
132                    *x *= mask;
133                }
134            }
135        }
136        Sig(Self {
137            freq_hz,
138            detune_ratio,
139            state_01,
140            mask,
141            offset_per_f32x8,
142            num_oscillators,
143            buf: Vec::new(),
144        })
145    }
146}
147
148builder! {
149    #[constructor = "super_saw"]
150    #[constructor_doc = "The mean of multiple saw wave oscillators with slight differences in frequency"]
151    #[build_fn = "SuperSaw::new"]
152    #[build_ty = "Sig<SuperSaw<F, D>>"]
153    #[generic_setter_type_name = "X"]
154    pub struct SuperSawBuilder {
155        #[generic_with_constraint = "SigT<Item = f32>"]
156        #[generic_name = "F"]
157        freq_hz: _,
158        #[generic_with_constraint = "SigT<Item = f32>"]
159        #[default = 0.01]
160        #[generic_name = "D"]
161        detune_ratio: f32,
162        #[default = 16]
163        num_oscillators: usize,
164        #[default = SuperSawInit::Random]
165        init: SuperSawInit,
166    }
167}