Skip to main content

pictorus_blocks/core_blocks/
trianglewave_block.rs

1use crate::traits::Float;
2use pictorus_block_data::BlockData;
3use pictorus_traits::GeneratorBlock;
4
5#[derive(Debug, Clone)]
6/// Outputs a triangle wave signal with specified amplitude, frequency, phase, and bias.
7pub struct TrianglewaveBlock<T>
8where
9    T: Float,
10    f64: From<T>,
11{
12    phantom: core::marker::PhantomData<T>,
13    pub data: BlockData,
14}
15
16impl<T> Default for TrianglewaveBlock<T>
17where
18    T: Float,
19    f64: From<T>,
20{
21    fn default() -> Self {
22        Self {
23            phantom: core::marker::PhantomData,
24            data: BlockData::from_scalar(f64::from(T::zero())),
25        }
26    }
27}
28
29impl<T> GeneratorBlock for TrianglewaveBlock<T>
30where
31    T: Float,
32    f64: From<T>,
33{
34    type Parameters = Parameters<T>;
35    type Output = T;
36
37    fn generate(
38        &mut self,
39        parameters: &Self::Parameters,
40        context: &dyn pictorus_traits::Context,
41    ) -> pictorus_traits::PassBy<Self::Output> {
42        // These two variables are used to construct constants used in the math below in a way that is infallible and generic
43        let two: T = T::one() + T::one();
44        let four: T = two + two;
45        let t =
46            (parameters.frequency * T::from_duration(context.time()) + parameters.phase) / (T::TAU);
47        let t = num_traits::Float::fract(t);
48        let y = if t < T::one() / two { t } else { T::one() - t };
49        // y is in the range [0, 0.5] over a t value from 0 to 1. Scale it by 4 ( to a range of [0, 2] )
50        // then shift it down by 1 to get it in the range [-1, 1], then scale it by the amplitude and add the bias.
51        let val = (four * y - T::one()) * parameters.amplitude + parameters.bias;
52        self.data = BlockData::from_scalar(val.into());
53        val
54    }
55}
56
57pub struct Parameters<T: Float> {
58    pub amplitude: T,
59    pub frequency: T,
60    pub phase: T,
61    pub bias: T,
62}
63
64impl<T: Float> Parameters<T> {
65    pub fn new(amplitude: T, frequency: T, phase: T, bias: T) -> Parameters<T> {
66        Parameters {
67            amplitude,
68            frequency,
69            phase,
70            bias,
71        }
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use crate::testing::{StubContext, StubRuntime};
79    use approx::assert_relative_eq;
80    use core::time::Duration;
81
82    const PI: f64 = core::f64::consts::PI;
83
84    #[test]
85    fn test_trianglewave_block_simple() {
86        let context = StubContext::new(
87            Duration::from_secs(0),
88            None,
89            Duration::from_secs_f64(PI / 2.0),
90        );
91        let mut runtime = StubRuntime::new(context);
92
93        let amplitude = 1.0;
94        let frequency = 1.0;
95        let phase = 0.0;
96        let bias = 0.0;
97        let params = Parameters::new(amplitude, frequency, phase, bias);
98
99        let mut block = TrianglewaveBlock::default();
100
101        block.generate(&params, &runtime.context()); // T = 0
102        assert_relative_eq!(block.data.scalar(), -1.0, epsilon = 1e-6);
103
104        runtime.tick();
105        block.generate(&params, &runtime.context()); // T = PI / 2
106        assert_relative_eq!(block.data.scalar(), 0.0, epsilon = 1e-6);
107
108        runtime.tick();
109        block.generate(&params, &runtime.context()); // T = PI
110        assert_relative_eq!(block.data.scalar(), 1.0, epsilon = 1e-6);
111
112        runtime.tick();
113        block.generate(&params, &runtime.context()); // T = 3PI / 2
114        assert_relative_eq!(block.data.scalar(), 0.0, epsilon = 1e-6);
115
116        runtime.tick();
117        block.generate(&params, &runtime.context()); // T = 2PI
118        assert_relative_eq!(block.data.scalar(), -1.0, epsilon = 1e-6);
119    }
120
121    #[test]
122    fn test_trianglewave_block_phase() {
123        let context = StubContext::new(
124            Duration::from_secs(0),
125            None,
126            Duration::from_secs_f64(PI / 2.0),
127        );
128        let mut runtime = StubRuntime::new(context);
129
130        let amplitude = 1.0;
131        let frequency = 1.0;
132        let phase = 0.5 * PI;
133        let bias = 0.0;
134        let params = Parameters::new(amplitude, frequency, phase, bias);
135
136        let mut block = TrianglewaveBlock::default();
137
138        block.generate(&params, &runtime.context()); // T = 0
139        assert_relative_eq!(block.data.scalar(), 0.0, epsilon = 1e-6);
140
141        runtime.tick();
142        block.generate(&params, &runtime.context()); // T = PI / 2
143        assert_relative_eq!(block.data.scalar(), 1.0, epsilon = 1e-6);
144
145        runtime.tick();
146        block.generate(&params, &runtime.context()); // T = PI
147        assert_relative_eq!(block.data.scalar(), 0.0, epsilon = 1e-6);
148
149        runtime.tick();
150        block.generate(&params, &runtime.context()); // T = 3PI / 2
151        assert_relative_eq!(block.data.scalar(), -1.0, epsilon = 1e-6);
152
153        runtime.tick();
154        block.generate(&params, &runtime.context()); // T = 2PI
155        assert_relative_eq!(block.data.scalar(), 0.0, epsilon = 1e-6);
156    }
157
158    #[test]
159    fn test_trianglewave_block_bias() {
160        let context = StubContext::new(
161            Duration::from_secs(0),
162            None,
163            Duration::from_secs_f64(PI / 2.0),
164        );
165        let mut runtime = StubRuntime::new(context);
166
167        let amplitude = 1.0;
168        let frequency = 1.0;
169        let phase = 0.0;
170        let bias = 1.0;
171        let params = Parameters::new(amplitude, frequency, phase, bias);
172
173        let mut block = TrianglewaveBlock::default();
174
175        block.generate(&params, &runtime.context()); // T = 0
176        assert_relative_eq!(block.data.scalar(), 0.0, epsilon = 1e-6);
177
178        runtime.tick();
179        block.generate(&params, &runtime.context()); // T = PI / 2
180        assert_relative_eq!(block.data.scalar(), 1.0, epsilon = 1e-6);
181
182        runtime.tick();
183        block.generate(&params, &runtime.context()); // T = PI
184        assert_relative_eq!(block.data.scalar(), 2.0, epsilon = 1e-6);
185
186        runtime.tick();
187        block.generate(&params, &runtime.context()); // T = 3PI / 2
188        assert_relative_eq!(block.data.scalar(), 1.0, epsilon = 1e-6);
189
190        runtime.tick();
191        block.generate(&params, &runtime.context()); // T = 2PI
192        assert_relative_eq!(block.data.scalar(), 0.0, epsilon = 1e-6);
193    }
194
195    #[test]
196    fn test_trianglewave_block_amplitude() {
197        let context = StubContext::new(
198            Duration::from_secs(0),
199            None,
200            Duration::from_secs_f64(PI / 2.0),
201        );
202        let mut runtime = StubRuntime::new(context);
203
204        let amplitude = 2.0;
205        let frequency = 1.0;
206        let phase = 0.0;
207        let bias = 0.0;
208        let params = Parameters::new(amplitude, frequency, phase, bias);
209
210        let mut block = TrianglewaveBlock::default();
211
212        block.generate(&params, &runtime.context()); // T = 0
213        assert_relative_eq!(block.data.scalar(), -2.0, epsilon = 1e-6);
214
215        runtime.tick();
216        block.generate(&params, &runtime.context()); // T = PI / 2
217        assert_relative_eq!(block.data.scalar(), 0.0, epsilon = 1e-6);
218
219        runtime.tick();
220        block.generate(&params, &runtime.context()); // T = PI
221        assert_relative_eq!(block.data.scalar(), 2.0, epsilon = 1e-6);
222
223        runtime.tick();
224        block.generate(&params, &runtime.context()); // T = 3PI / 2
225        assert_relative_eq!(block.data.scalar(), 0.0, epsilon = 1e-6);
226
227        runtime.tick();
228        block.generate(&params, &runtime.context()); // T = 2PI
229        assert_relative_eq!(block.data.scalar(), -2.0, epsilon = 1e-6);
230    }
231
232    #[test]
233    fn test_trianglewave_block_high_time() {
234        let context = StubContext::new(
235            Duration::from_secs(0),
236            None,
237            Duration::from_secs_f64(PI / 2.0),
238        );
239        let mut runtime = StubRuntime::new(context);
240
241        let amplitude = 1.0;
242        let frequency = 2.0;
243        let phase = 0.0;
244        let bias = 0.0;
245
246        let params = Parameters::new(amplitude, frequency, phase, bias);
247        let mut block = TrianglewaveBlock::default();
248
249        block.generate(&params, &runtime.context()); // T = 0
250        assert_relative_eq!(block.data.scalar(), -1.0, epsilon = 1e-6);
251
252        runtime.set_time(Duration::from_secs_f64(400.0 * PI));
253        block.generate(&params, &runtime.context()); // T = 400PI
254        assert_relative_eq!(block.data.scalar(), -1.0, epsilon = 1e-6);
255    }
256
257    #[test]
258    fn test_trianglewave_block_frequency() {
259        let context = StubContext::new(
260            Duration::from_secs(0),
261            None,
262            Duration::from_secs_f64(PI / 4.0),
263        );
264        let mut runtime = StubRuntime::new(context);
265
266        let amplitude = 1.0;
267        let frequency = 2.0;
268        let phase = 0.0;
269        let bias = 0.0;
270
271        let params = Parameters::new(amplitude, frequency, phase, bias);
272        let mut block = TrianglewaveBlock::default();
273
274        block.generate(&params, &runtime.context()); // T = 0
275        assert_relative_eq!(block.data.scalar(), -1.0, epsilon = 1e-6);
276
277        runtime.tick();
278        block.generate(&params, &runtime.context()); // T = PI / 4
279        assert_relative_eq!(block.data.scalar(), 0.0, epsilon = 1e-6);
280
281        runtime.tick();
282        block.generate(&params, &runtime.context()); // T = PI / 2
283        assert_relative_eq!(block.data.scalar(), 1.0, epsilon = 1e-6);
284
285        runtime.tick();
286        block.generate(&params, &runtime.context()); // T = 3PI / 4
287        assert_relative_eq!(block.data.scalar(), 0.0, epsilon = 1e-6);
288
289        runtime.tick();
290        block.generate(&params, &runtime.context()); // T = PI
291        assert_relative_eq!(block.data.scalar(), -1.0, epsilon = 1e-6);
292    }
293}