signal_generator/
lib.rs

1#![no_std]
2
3use core::iter::Take;
4
5use idsp::{AccuOsc, Sweep};
6use miniconf::Tree;
7use rand_core::{RngCore, SeedableRng};
8use rand_xorshift::XorShiftRng;
9use serde::{Deserialize, Serialize};
10
11/// Types of signals that can be generated.
12#[derive(Copy, Clone, Debug, Deserialize, Serialize)]
13pub enum Signal {
14    Cosine,
15    Square,
16    Triangle,
17    WhiteNoise,
18    SweptSine,
19}
20
21impl Signal {
22    #[inline]
23    fn map(&self, x: i32) -> i32 {
24        match self {
25            Self::Cosine => idsp::cossin(x).0,
26            Self::Square => {
27                if x.is_negative() {
28                    -i32::MAX
29                } else {
30                    i32::MAX
31                }
32            }
33            Self::Triangle => i32::MIN + (x.saturating_abs() << 1),
34            _ => unimplemented!(),
35        }
36    }
37}
38
39/// Basic configuration for a generated signal.
40#[derive(Clone, Debug, Tree, Serialize, Deserialize)]
41#[tree(meta(doc, typename))]
42pub struct Config {
43    /// The signal type that should be generated. See [Signal] variants.
44    #[tree(with=miniconf::leaf)]
45    signal: Signal,
46
47    /// The frequency of the generated signal in Hertz.
48    frequency: f32,
49
50    /// The normalized symmetry of the signal. At 0% symmetry, the duration of the first half oscillation is minimal.
51    /// At 25% symmetry, the first half oscillation lasts for 25% of the signal period. For square wave output this
52    /// symmetry is the duty cycle.
53    symmetry: f32,
54
55    /// The amplitude of the output signal
56    amplitude: f32,
57
58    /// Output offset
59    offset: f32,
60
61    /// The initial phase of the period output signal in turns
62    phase: f32,
63
64    /// Number of half periods (periodic) or samples (sweep and noise), 0 for infinte
65    length: u32,
66
67    /// Sweep: initial state
68    state: i64,
69
70    /// Sweep: Sweep rate
71    rate: i32,
72}
73
74impl Default for Config {
75    fn default() -> Self {
76        Self {
77            frequency: 1.0e3,
78            symmetry: 0.5,
79            signal: Signal::Cosine,
80            amplitude: 0.0,
81            phase: 0.0,
82            offset: 0.0,
83            state: 0,
84            rate: 0,
85            length: 0,
86        }
87    }
88}
89
90#[derive(Clone, Debug)]
91pub struct AsymmetricAccu {
92    ftw: [i32; 2],
93    pow: i32,
94    accu: i32,
95    count: u32,
96}
97
98impl Iterator for AsymmetricAccu {
99    type Item = i32;
100    fn next(&mut self) -> Option<Self::Item> {
101        let sign = self.accu.is_negative();
102        self.accu = self.accu.wrapping_add(self.ftw[sign as usize]);
103        self.count
104            .checked_sub(sign as u32 ^ self.accu.is_negative() as u32)
105            .map(|c| {
106                self.count = c;
107                self.accu.wrapping_add(self.pow)
108            })
109    }
110}
111
112#[derive(Clone, Debug)]
113pub struct Scaler {
114    amp: i32,
115    offset: i32,
116}
117
118impl Scaler {
119    fn map(&self, x: i32) -> i32 {
120        (((x as i64 * self.amp as i64) >> 31) as i32)
121            .saturating_add(self.offset)
122    }
123}
124
125/// Represents the errors that can occur when attempting to configure the signal generator.
126#[derive(Copy, Clone, Debug, thiserror::Error)]
127pub enum Error {
128    /// The provided amplitude is out-of-range.
129    #[error("Invalid amplitude")]
130    Amplitude,
131    /// The provided symmetry is out of range.
132    #[error("Invalid symmetry")]
133    Symmetry,
134    /// The provided frequency is out of range.
135    #[error("Invalid frequency")]
136    Frequency,
137    /// Sweep would wrap/invalid
138    #[error("Sweep would wrap")]
139    Wrap,
140}
141
142#[derive(Clone, Debug)]
143pub enum Source {
144    SweptSine {
145        sweep: Take<AccuOsc<Sweep>>,
146        amp: Scaler,
147    },
148    Periodic {
149        accu: AsymmetricAccu,
150        signal: Signal,
151        amp: Scaler,
152    },
153    WhiteNoise {
154        rng: XorShiftRng,
155        count: u32,
156        amp: Scaler,
157    },
158}
159
160impl Iterator for Source {
161    type Item = i32;
162    #[inline]
163    fn next(&mut self) -> Option<Self::Item> {
164        let (s, a) = match self {
165            Self::SweptSine { sweep, amp } => {
166                (sweep.next().map(|c| c.im()), amp)
167            }
168            Self::Periodic { accu, signal, amp } => {
169                (accu.next().map(|p| signal.map(p)), amp)
170            }
171            Self::WhiteNoise { rng, count, amp } => (
172                count.checked_sub(1).map(|m| {
173                    *count = m;
174                    rng.next_u32() as i32
175                }),
176                amp,
177            ),
178        };
179        Some(a.map(s.unwrap_or_default()))
180    }
181}
182
183impl Config {
184    /// Convert from SI config
185    pub fn build(&self, period: f32, scale: f32) -> Result<Source, Error> {
186        if !(0.0..1.0).contains(&self.symmetry) {
187            return Err(Error::Symmetry);
188        }
189
190        const NYQUIST: f32 = (1u32 << 31) as _;
191        let ftw0 = self.frequency * period * NYQUIST;
192        if !(0.0..2.0 * NYQUIST).contains(&ftw0) {
193            return Err(Error::Frequency);
194        }
195
196        // Clip both frequency tuning words to within Nyquist before rounding.
197        let ftw = [
198            if self.symmetry * NYQUIST > ftw0 {
199                ftw0 / self.symmetry
200            } else {
201                NYQUIST
202            } as i32,
203            if (1.0 - self.symmetry) * NYQUIST > ftw0 {
204                ftw0 / (1.0 - self.symmetry)
205            } else {
206                NYQUIST
207            } as i32,
208        ];
209
210        let offset = self.offset * scale;
211        let amplitude = self.amplitude * scale;
212        fn abs(x: f32) -> f32 {
213            if x.is_sign_negative() { -x } else { x }
214        }
215        if abs(offset) + abs(amplitude) >= 1.0 {
216            return Err(Error::Amplitude);
217        }
218        let amp = Scaler {
219            amp: (amplitude * NYQUIST) as _,
220            offset: (offset * NYQUIST) as _,
221        };
222
223        Ok(match self.signal {
224            signal @ (Signal::Cosine | Signal::Square | Signal::Triangle) => {
225                Source::Periodic {
226                    accu: AsymmetricAccu {
227                        ftw,
228                        pow: (self.phase * NYQUIST) as i32,
229                        accu: 0,
230                        count: self.length,
231                    },
232                    signal,
233                    amp,
234                }
235            }
236            Signal::SweptSine => Source::SweptSine {
237                sweep: AccuOsc::new(Sweep::new(self.rate, self.state))
238                    .take(self.length as _),
239                amp,
240            },
241            Signal::WhiteNoise => Source::WhiteNoise {
242                rng: XorShiftRng::from_seed(Default::default()),
243                count: self.length,
244                amp,
245            },
246        })
247    }
248}