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 } => (sweep.next().map(|c| c.im), amp),
166            Self::Periodic { accu, signal, amp } => {
167                (accu.next().map(|p| signal.map(p)), amp)
168            }
169            Self::WhiteNoise { rng, count, amp } => (
170                count.checked_sub(1).map(|m| {
171                    *count = m;
172                    rng.next_u32() as i32
173                }),
174                amp,
175            ),
176        };
177        Some(a.map(s.unwrap_or_default()))
178    }
179}
180
181impl Config {
182    /// Convert from SI config
183    pub fn build(&self, period: f32, scale: f32) -> Result<Source, Error> {
184        if !(0.0..1.0).contains(&self.symmetry) {
185            return Err(Error::Symmetry);
186        }
187
188        const NYQUIST: f32 = (1u32 << 31) as _;
189        let ftw0 = self.frequency * period * NYQUIST;
190        if !(0.0..2.0 * NYQUIST).contains(&ftw0) {
191            return Err(Error::Frequency);
192        }
193
194        // Clip both frequency tuning words to within Nyquist before rounding.
195        let ftw = [
196            if self.symmetry * NYQUIST > ftw0 {
197                ftw0 / self.symmetry
198            } else {
199                NYQUIST
200            } as i32,
201            if (1.0 - self.symmetry) * NYQUIST > ftw0 {
202                ftw0 / (1.0 - self.symmetry)
203            } else {
204                NYQUIST
205            } as i32,
206        ];
207
208        let offset = self.offset * scale;
209        let amplitude = self.amplitude * scale;
210        fn abs(x: f32) -> f32 {
211            if x.is_sign_negative() { -x } else { x }
212        }
213        if abs(offset) + abs(amplitude) >= 1.0 {
214            return Err(Error::Amplitude);
215        }
216        let amp = Scaler {
217            amp: (amplitude * NYQUIST) as _,
218            offset: (offset * NYQUIST) as _,
219        };
220
221        Ok(match self.signal {
222            signal @ (Signal::Cosine | Signal::Square | Signal::Triangle) => {
223                Source::Periodic {
224                    accu: AsymmetricAccu {
225                        ftw,
226                        pow: (self.phase * NYQUIST) as i32,
227                        accu: 0,
228                        count: self.length,
229                    },
230                    signal,
231                    amp,
232                }
233            }
234            Signal::SweptSine => Source::SweptSine {
235                sweep: AccuOsc::new(Sweep::new(self.rate, self.state))
236                    .take(self.length as _),
237                amp,
238            },
239            Signal::WhiteNoise => Source::WhiteNoise {
240                rng: XorShiftRng::from_seed(Default::default()),
241                count: self.length,
242                amp,
243            },
244        })
245    }
246}