From cde2c7446785fb0dc4893dc5d50a72019c85746b Mon Sep 17 00:00:00 2001 From: "J.A. de Jong - Redu-Sone B.V., ASCEE V.O.F." Date: Sun, 13 Oct 2024 13:02:49 +0200 Subject: [PATCH] Implemented sweep signal generator. Some sweep implementations can error when something goes wrong. Changing params on running signal generator give results back that need to be handled --- .gitignore | 1 + src/bin/lasp_output.rs | 10 +- src/bin/lasp_outputdefault.rs | 30 ++-- src/daq/streamcmd.rs | 3 + src/daq/streamdata.rs | 2 +- src/daq/streammgr.rs | 157 +++++++++++++------- src/ps/timebuffer.rs | 2 +- src/siggen/mod.rs | 7 +- src/siggen/siggen.rs | 239 +++++++++++++++--------------- src/siggen/siggenchannel.rs | 78 ++++++++++ src/siggen/siggencmd.rs | 38 +++++ src/siggen/source.rs | 190 +++++++++++++++++++----- src/siggen/sweep.rs | 264 ++++++++++++++++++++++++++++++++++ src/slm/slm.rs | 2 +- 14 files changed, 792 insertions(+), 231 deletions(-) create mode 100644 src/siggen/siggenchannel.rs create mode 100644 src/siggen/siggencmd.rs create mode 100644 src/siggen/sweep.rs diff --git a/.gitignore b/.gitignore index 6dd9671..ac13301 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ python/lasprs/_lasprs* .vscode/launch.json .vscode examples_py/.ipynb_checkpoints +.ipynb_checkpoints diff --git a/src/bin/lasp_output.rs b/src/bin/lasp_output.rs index 2f15841..1c888a4 100644 --- a/src/bin/lasp_output.rs +++ b/src/bin/lasp_output.rs @@ -1,10 +1,10 @@ use anyhow::Result; -use crossbeam::channel::{ unbounded, Receiver, TryRecvError }; -use lasprs::daq::{ DaqConfig, StreamMgr, StreamStatus, StreamType }; +use crossbeam::channel::{unbounded, Receiver, TryRecvError}; +use lasprs::daq::{DaqConfig, StreamMgr, StreamStatus, StreamType}; use lasprs::siggen::Siggen; use std::io; use std::time::Duration; -use std::{ thread, time }; +use std::{thread, time}; // use /// Spawns a thread and waits for a single line, pushes it to the receiver and returns @@ -30,13 +30,13 @@ fn main() -> Result<()> { let stdin_channel = stdin_channel_wait_for_return(); println!("Creating signal generator..."); - let mut siggen = Siggen::newSine(2, 432.0); + let mut siggen = Siggen::newSine(1., 2, 432.0).unwrap(); // Reduce all gains a bit... siggen.setAllGains(0.1); // Apply signal generator - smgr.setSiggen(siggen); + smgr.setSiggen(siggen)?; println!("Starting stream..."); let devs = smgr.getDeviceInfo(); diff --git a/src/bin/lasp_outputdefault.rs b/src/bin/lasp_outputdefault.rs index ed17c56..2e258f8 100644 --- a/src/bin/lasp_outputdefault.rs +++ b/src/bin/lasp_outputdefault.rs @@ -1,16 +1,15 @@ use anyhow::Result; use crossbeam::channel::{unbounded, Receiver, TryRecvError}; use lasprs::daq::{StreamMgr, StreamStatus, StreamType}; -use lasprs::siggen::Siggen; +use lasprs::siggen::{Siggen, SweepType}; use std::io; use std::{thread, time}; // use /// Spawns a thread and waits for a single line, pushes it to the receiver and returns fn stdin_channel_wait_for_return() -> Receiver { - let (tx, rx) = unbounded(); - thread::spawn(move || { + thread::spawn(move || { let mut buffer = String::new(); io::stdin().read_line(&mut buffer).unwrap(); // Do not care whether we succeed here. @@ -28,7 +27,19 @@ fn main() -> Result<()> { let stdin_channel = stdin_channel_wait_for_return(); println!("Creating signal generator..."); - let mut siggen = Siggen::newSine(2, 432.); + // let mut siggen = Siggen::newSine(44100., 2, 432.)?; + let mut siggen = Siggen::newSweep( + 44100., + 1, // nchannels: usize, + 100., // fl: Flt, + 10000., //fu: Flt, + 1.0, // sweep_time: Flt, + // 1.0, //quiet_time: Flt, + 0., //quiet_time: Flt, + // SweepType::ForwardLin//sweep_type: SweepType, + // SweepType::ForwardLog, //sweep_type: SweepType, + SweepType::ContinuousLog, //sweep_type: SweepType, + )?; // Some things that can be done // siggen.setDCOffset(&[0.1, 0.]); @@ -36,12 +47,11 @@ fn main() -> Result<()> { // Reduce all gains a bit... siggen.setAllGains(0.1); - println!("Starting stream..."); smgr.startDefaultOutputStream()?; - + // Apply signal generator - smgr.setSiggen(siggen); + smgr.setSiggen(siggen)?; println!("Press key to quit..."); 'infy: loop { @@ -52,12 +62,12 @@ fn main() -> Result<()> { } sleep(100); match smgr.getStatus(StreamType::Output) { - StreamStatus::NotRunning{} => { + StreamStatus::NotRunning {} => { println!("Stream is not running?"); break 'infy; } - StreamStatus::Running{} => {} - StreamStatus::Error{e} => { + StreamStatus::Running {} => {} + StreamStatus::Error { e } => { println!("Stream error: {}", e); break 'infy; } diff --git a/src/daq/streamcmd.rs b/src/daq/streamcmd.rs index f9f3ef2..b928de1 100644 --- a/src/daq/streamcmd.rs +++ b/src/daq/streamcmd.rs @@ -10,6 +10,9 @@ pub enum StreamCommand { /// New signal generator config to be used in OUTPUT stream NewSiggen(Siggen), + /// Apply command to the signal generator. + SiggenCommand(SiggenCommand), + /// Stop the thread, do not listen for data anymore. StopThread, diff --git a/src/daq/streamdata.rs b/src/daq/streamdata.rs index c5517eb..964a2b9 100644 --- a/src/daq/streamdata.rs +++ b/src/daq/streamdata.rs @@ -307,7 +307,7 @@ mod test { const Nframes: usize = 20; const Nch: usize = 2; let mut signal = [0.; Nch * Nframes]; - let mut siggen = Siggen::newSine(Nch, 1.); + let mut siggen = Siggen::newSine(fs, Nch, 1.).unwrap(); siggen.reset(fs); siggen.setMute(&[false, true]); diff --git a/src/daq/streammgr.rs b/src/daq/streammgr.rs index d79cca9..b5f2324 100644 --- a/src/daq/streammgr.rs +++ b/src/daq/streammgr.rs @@ -4,7 +4,7 @@ use crate::{ config::*, siggen::{self, Siggen}, }; -use anyhow::{bail, Error, Result}; +use anyhow::{anyhow, bail, Error, Result}; use api::StreamApiDescr; use array_init::from_iter; use core::time; @@ -13,8 +13,11 @@ use crossbeam::{ channel::{unbounded, Receiver, Sender, TrySendError}, thread, }; -use std::sync::{atomic::AtomicBool, Arc, Mutex}; use std::thread::{JoinHandle, Thread}; +use std::{ + sync::{atomic::AtomicBool, Arc, Mutex}, + time::Duration, +}; use streamcmd::StreamCommand; use streamdata::*; use streammetadata::*; @@ -33,7 +36,8 @@ struct StreamInfo { streamtype: StreamType, stream: Box, threadhandle: JoinHandle, - comm: Sender, + commtx: Sender, + commrx: Receiver>, } /// Keep track of whether the stream has been created. To ensure singleton behaviour. @@ -108,8 +112,9 @@ impl StreamMgr { self.getStatus(st) } #[pyo3(name = "setSiggen")] - fn setSiggen_py(&mut self, siggen: Siggen) { - self.setSiggen(siggen) + fn setSiggen_py(&mut self, siggen: Siggen) -> PyResult<()> { + self.setSiggen(siggen)?; + Ok(()) } #[pyo3(name = "getStreamMetaData")] fn getStreamMetaData_py(&self, st: StreamType) -> Option { @@ -195,18 +200,27 @@ impl StreamMgr { /// Set a new signal generator. Returns an error if it is unapplicable. /// It is unapplicable if the number of channels of output does not match the /// number of output channels in a running stream. - pub fn setSiggen(&mut self, siggen: Siggen) { + pub fn setSiggen(&mut self, siggen: Siggen) -> Result<()> { // Current signal generator. Where to place it? - if let Some(istream) = &self.input_stream { + if let Some(os) = &self.output_stream { + assert!(self.siggen.is_none()); + os.commtx.send(StreamCommand::NewSiggen(siggen)).unwrap(); + os.commrx.recv().unwrap() + } else if let Some(istream) = &self.input_stream { if let StreamType::Duplex = istream.streamtype { assert!(self.siggen.is_none()); - istream.comm.send(StreamCommand::NewSiggen(siggen)).unwrap(); + istream + .commtx + .send(StreamCommand::NewSiggen(siggen)) + .unwrap(); + istream.commrx.recv().unwrap() + } else { + self.siggen = Some(siggen); + Ok(()) } - } else if let Some(os) = &self.output_stream { - assert!(self.siggen.is_none()); - os.comm.send(StreamCommand::NewSiggen(siggen)).unwrap(); } else { self.siggen = Some(siggen); + Ok(()) } } @@ -235,7 +249,7 @@ impl StreamMgr { /// of queues that get data from the stream. pub fn addInQueue(&mut self, tx: Sender) { if let Some(is) = &self.input_stream { - is.comm.send(StreamCommand::AddInQueue(tx)).unwrap() + is.commtx.send(StreamCommand::AddInQueue(tx)).unwrap() } else { self.instreamqueues.as_mut().unwrap().push(tx); } @@ -245,8 +259,14 @@ impl StreamMgr { &mut self, meta: Arc, rx: Receiver, - ) -> (JoinHandle, Sender) { - let (commtx, commrx) = unbounded(); + ) -> ( + JoinHandle, + Sender, + Receiver>, + ) { + // Bi-directional communication between input stream thread and stream manager + let (commtx_ret, commrx) = unbounded(); + let (commtx, commrx_ret) = unbounded(); // Unwrap here, as the queues should be free to grab let mut iqueues = self @@ -261,8 +281,17 @@ impl StreamMgr { // New queue added StreamCommand::AddInQueue(queue) => { match queue.send(InStreamMsg::StreamStarted(meta.clone())) { - Ok(()) => iqueues.push(queue), - Err(_) => {} + Ok(()) => { + iqueues.push(queue); + commtx.send(Ok(())).unwrap(); + } + Err(e) => { + commtx + .send(Err(anyhow!( + "Cannot push to queue: {e}. Object destructed?" + ))) + .unwrap(); + } } } @@ -272,11 +301,15 @@ impl StreamMgr { &mut iqueues, InStreamMsg::StreamStopped, ); + commtx.send(Ok(())).unwrap(); break 'infy; } StreamCommand::NewSiggen(_) => { panic!("Error: signal generator send to input-only stream."); } + StreamCommand::SiggenCommand(_) => { + panic!("Error: signal generator command send to input-only stream."); + } } } if let Ok(msg) = rx.recv_timeout(time::Duration::from_millis(10)) { @@ -285,7 +318,7 @@ impl StreamMgr { } iqueues }); - (threadhandle, commtx) + (threadhandle, commtx_ret, commrx_ret) } // Match device info struct on given daq config. @@ -303,8 +336,13 @@ impl StreamMgr { &mut self, meta: Arc, tx: Sender, - ) -> (JoinHandle, Sender) { - let (commtx, commrx) = unbounded(); + ) -> ( + JoinHandle, + Sender, + Receiver>, + ) { + let (commtx_res, commrx) = unbounded(); + let (commtx, commrx_res) = unbounded(); // Number of channels to output for let nchannels = meta.nchannels(); @@ -314,7 +352,7 @@ impl StreamMgr { let mut siggen = self .siggen .take() - .unwrap_or_else(|| Siggen::newSilence(nchannels)); + .unwrap_or_else(|| Siggen::newSilence(meta.samplerate, nchannels)); if siggen.nchannels() != nchannels { // Updating number of channels @@ -323,9 +361,15 @@ impl StreamMgr { siggen.reset(meta.samplerate); let threadhandle = std::thread::spawn(move || { - let mut floatbuf: Vec = Vec::with_capacity(nchannels * meta.framesPerBlock); + // What is a good sleep time? We have made sure that there are + // two buffers available for the output stream. We choose to wake up twice per frame. + let sleep_time_us = Duration::from_micros( + (0.5 * 1e6 * meta.framesPerBlock as Flt / meta.samplerate) as u64, + ); + + let mut floatbuf: Vec = vec![0.; nchannels * meta.framesPerBlock]; 'infy: loop { - if let Ok(comm_msg) = commrx.try_recv() { + if let Ok(comm_msg) = commrx.recv_timeout(sleep_time_us) { match comm_msg { // New queue added StreamCommand::AddInQueue(_) => { @@ -334,6 +378,7 @@ impl StreamMgr { // Stop this thread. Returns the queue StreamCommand::StopThread => { + commtx.send(Ok(())).unwrap(); break 'infy; } StreamCommand::NewSiggen(new_siggen) => { @@ -344,16 +389,20 @@ impl StreamMgr { // println!("Updating channels"); siggen.setNChannels(nchannels); } + commtx.send(Ok(())).unwrap(); + } + StreamCommand::SiggenCommand(cmd) => { + // Apply command to signal generator. + let res = siggen.applyCommand(cmd); + commtx.send(res).unwrap(); } } } while tx.len() < 2 { - unsafe { - floatbuf.set_len(nchannels * meta.framesPerBlock); - } - // Obtain signal + // Obtain signal from signal generator siggen.genSignal(&mut floatbuf); - // println!("level: {}", floatbuf.iter().sum::()); + + // Convert signal generator data to raw data and push to the stream thread let msg = match meta.rawDatatype { DataType::I8 => { let v = Vec::::from_iter(floatbuf.iter().map(|f| f.to_sample())); @@ -377,14 +426,17 @@ impl StreamMgr { } }; if let Err(_e) = tx.send(msg) { - // println!("Error sending raw stream data to output stream!"); - break 'infy; + // An error occured while trying to send the raw data to + // the stream. This might be because the stream has + // stopped or has an error. + + // There is nothing we can do here, but we should not stop the thread. } } } siggen }); - (threadhandle, commtx) + (threadhandle, commtx_res, commrx_res) } /// Start a stream of certain type, using given configuration @@ -418,13 +470,14 @@ impl StreamMgr { _ => bail!("API {} not implemented!", cfg.api), }; let meta = stream.metadata(); - let (threadhandle, commtx) = self.startOuputStreamThread(meta, tx); + let (threadhandle, commtx, commrx) = self.startOuputStreamThread(meta, tx); self.output_stream = Some(StreamInfo { streamtype: StreamType::Input, stream, threadhandle, - comm: commtx, + commtx, + commrx, }); Ok(()) @@ -472,13 +525,14 @@ impl StreamMgr { sendMsgToAllQueuesRemoveUnused(iqueues, InStreamMsg::StreamStarted(meta.clone())); - let (threadhandle, commtx) = self.startInputStreamThread(meta, rx); + let (threadhandle, commtx, commrx) = self.startInputStreamThread(meta, rx); self.input_stream = Some(StreamInfo { streamtype: stype, stream, threadhandle, - comm: commtx, + commtx, + commrx, }); Ok(()) @@ -503,13 +557,14 @@ impl StreamMgr { let meta = stream.metadata(); sendMsgToAllQueuesRemoveUnused(iqueues, InStreamMsg::StreamStarted(meta.clone())); - let (threadhandle, commtx) = self.startInputStreamThread(meta, rx); + let (threadhandle, commtx, commrx) = self.startInputStreamThread(meta, rx); self.input_stream = Some(StreamInfo { streamtype: StreamType::Input, stream, threadhandle, - comm: commtx, + commtx, + commrx, }); Ok(()) @@ -537,15 +592,14 @@ impl StreamMgr { let (tx, rx)= unbounded(); let stream = self.cpal_api.startDefaultOutputStream(rx)?; let meta = stream.metadata(); - let (threadhandle, commtx) = self.startOuputStreamThread(meta, tx); - // Inform all listeners of new stream data - + let (threadhandle, commtx, commrx) = self.startOuputStreamThread(meta, tx); self.output_stream = Some(StreamInfo { streamtype: StreamType::Input, stream, threadhandle, - comm: commtx, + commtx, + commrx, }); Ok(()) @@ -563,19 +617,22 @@ impl StreamMgr { streamtype: _, // Ignored here stream: _, threadhandle, - comm, + commtx, + commrx, }) = self.input_stream.take() { // println!("Stopping existing stream.."); // Send thread to stop - comm.send(StreamCommand::StopThread).unwrap(); + commtx.send(StreamCommand::StopThread).unwrap(); // Store stream queues back into StreamMgr self.instreamqueues = Some(threadhandle.join().expect("Stream thread panicked!")); + + let res = commrx.recv().unwrap(); + return res; } else { bail!("Stream is not running.") } - Ok(()) } /// Stop existing output stream pub fn stopOutputStream(&mut self) -> Result<()> { @@ -583,21 +640,17 @@ impl StreamMgr { streamtype: _, // Ignored here stream: _, threadhandle, - comm, + commtx, + commrx, }) = self.output_stream.take() { - if comm.send(StreamCommand::StopThread).is_err() { - // Failed to send command over channel. This means the thread is - // already finished due to some other reason. - assert!(threadhandle.is_finished()); - } - // println!("Wainting for threadhandle to join..."); + commtx.send(StreamCommand::StopThread).unwrap(); + // eprintln!("Wainting for threadhandle to join..."); self.siggen = Some(threadhandle.join().expect("Output thread panicked!")); - // println!("Threadhandle joined!"); + commrx.recv().unwrap() } else { bail!("Stream is not running."); } - Ok(()) } /// Stop existing running stream. /// diff --git a/src/ps/timebuffer.rs b/src/ps/timebuffer.rs index 6e620f2..83e8d1c 100644 --- a/src/ps/timebuffer.rs +++ b/src/ps/timebuffer.rs @@ -8,7 +8,7 @@ use std::collections::VecDeque; /// TimeBuffer, storage to add blocks of data in a ring buffer, that can be /// extracted by blocks of other size. Also, we can keep samples in a buffer to /// create, for example, overlapping windows of time data. -#[derive(Default, Debug)] +#[derive(Default, Debug, Clone)] pub struct TimeBuffer { data: Vec>, } diff --git a/src/siggen/mod.rs b/src/siggen/mod.rs index 6236ad6..d144d1a 100644 --- a/src/siggen/mod.rs +++ b/src/siggen/mod.rs @@ -21,6 +21,11 @@ //! //! ``` mod siggen; +mod siggenchannel; mod source; -pub use source::{Silence, Sine, WhiteNoise}; +mod siggencmd; +mod sweep; +pub use source::Source; pub use siggen::Siggen; +pub use sweep::SweepType; +pub use siggencmd::SiggenCommand; \ No newline at end of file diff --git a/src/siggen/siggen.rs b/src/siggen/siggen.rs index 62fe5ec..6854bdf 100644 --- a/src/siggen/siggen.rs +++ b/src/siggen/siggen.rs @@ -1,26 +1,30 @@ +use super::siggenchannel::SiggenChannelConfig; +use super::SiggenCommand; +use super::source::{self, *}; +use super::sweep::SweepType; use crate::config::*; use crate::filter::Filter; -use super::source::*; +use anyhow::{bail, Result}; use dasp_sample::{FromSample, Sample}; use rayon::prelude::*; use std::fmt::Debug; use std::iter::ExactSizeIterator; use std::slice::IterMut; - - -/// Signal generator. Able to create acoustic output signals. See above example on how to use. +/// Multiple channel signal generator. Able to create (acoustic) output signals. See above example on how to use. /// Typical signal that can be created are: /// -/// * (Siggen::newWhiteNoise) -/// * (Siggen::newSine) +/// * [Siggen::newWhiteNoise] +/// * [Siggen::newSine] +/// * [Siggen::newSilence] /// #[derive(Clone)] #[cfg_attr(feature = "python-bindings", pyclass)] pub struct Siggen { // The source dynamic signal. Noise, a sine wave, sweep, etc - source: Box, - // Filter applied to the source signal + source: Source, + + // Channel configuration for each output channel channels: Vec, // Temporary source signal buffer @@ -34,40 +38,117 @@ pub struct Siggen { impl Siggen { #[pyo3(name = "newWhiteNoise")] #[staticmethod] - fn newWhiteNoise_py() -> Siggen { - Siggen::newWhiteNoise(0) + fn newWhiteNoise_py(fs: Flt) -> Siggen { + Siggen::newWhiteNoise(fs, 0) } #[pyo3(name = "newSine")] #[staticmethod] - fn newSine_py(freq: Flt) -> Siggen { - Siggen::newSine(0, freq) + fn newSine_py(fs: Flt, freq: Flt, nchannels: usize) -> PyResult { + Ok(Siggen::newSine(fs, nchannels, freq)?) } } -/// Multiple channel signal generator. Can use a single source (coherent) to provide multiple signals -/// that can be sent out through different EQ's impl Siggen { + /// Create a new signal generator with an arbitrary source. + /// # Args + /// + /// - `nchannels` - The number of channels to output + /// - `source` - Source function + pub fn new(nchannels: usize, source: Source) -> Siggen { + Siggen { + source, + channels: vec![SiggenChannelConfig::new(); nchannels], + source_buf: vec![], + chout_buf: vec![], + } + } + /// Create sine sweep signal generator + /// + /// # Args + /// + /// - `fs` - Sample rate \[Hz\] + /// - `nchannels`: The number of channels to output + /// - `fl` - Lower frequency \[Hz\] + /// - `fu` - Upper frequency \[Hz\] + /// - `sweep_time` - The duration of a single sweep \[s\] + /// - `quiet_time` - Time of silence after one sweep and start of the next \[s\] + /// - `sweep_type` - The type of the sweep, see [SweepType]. + pub fn newSweep( + fs: Flt, + nchannels: usize, + fl: Flt, + fu: Flt, + sweep_time: Flt, + quiet_time: Flt, + sweep_type: SweepType, + ) -> Result { + let source = Source::newSweep(fs, fl, fu, sweep_time, quiet_time, sweep_type)?; + Ok(Self::new(nchannels, source)) + } + /// Create a sine wave signal generator + /// + /// # Args + /// + /// - `fs` - Sampling frequency \[Hz\] + /// - `nchannels`: The number of channels to output + /// * `freq` - Frequency of the sine wave in \[Hz\] + pub fn newSine(fs: Flt, nchannels: usize, freq: Flt) -> Result { + Ok(Siggen::new(nchannels, Source::newSine(fs, freq)?)) + } + + /// Silence: create a signal generator that does not output any dynamic + /// signal at all. + /// # Args + /// + /// - `fs` - Sampling frequency \[Hz\] + /// - `nchannels` - The number of channels to output + pub fn newSilence(_fs: Flt, nchannels: usize) -> Siggen { + Siggen::new(nchannels, Source::newSilence()) + } + + /// Create a white noise signal generator. + /// + /// # Args + /// + /// - `fs` - Sampling frequency \[Hz\] + /// - `nchannels` - The number of channels to output + pub fn newWhiteNoise(_fs: Flt, nchannels: usize) -> Siggen { + Siggen::new(nchannels, Source::newWhiteNoise()) + } + /// Returns the number of channels this signal generator is generating for. pub fn nchannels(&self) -> usize { self.channels.len() } - /// Silence: create a signal generator that does not output any dynamic - /// signal at all. - pub fn newSilence(nchannels: usize) -> Siggen { - Siggen { - channels: vec![SiggenChannelConfig::new(); nchannels], - source: Box::new(Silence {}), - source_buf: vec![], - chout_buf: vec![], + /// Apply command to current signal generator to change its state. + pub fn applyCommand(&mut self, msg: SiggenCommand) -> Result<()> { + match msg { + SiggenCommand::ChangeSource { src } => { + self.source = src; + Ok(()) + } + SiggenCommand::ResetSiggen { fs } => { + self.reset(fs); + Ok(()) + } + SiggenCommand::SetMuteAllChannels { mute } => { + self.setAllMute(mute); + Ok(()) + } + SiggenCommand::SetMuteChannel { ch, mute } => { + if ch > self.channels.len() { + bail!("Invalid channel index: {ch}"); + } + self.channels[ch].setMute(mute); + Ok(()) + } + SiggenCommand::SetAllGains { g } => { + self.setAllGains(g); + Ok(()) + } } } - - /// Create a white noise signal generator. - pub fn newWhiteNoise(nchannels: usize) -> Siggen { - Siggen::new(nchannels, Box::new(WhiteNoise::new())) - } - /// Set gains of all channels in signal generator to the same value /// /// # Args @@ -97,23 +178,6 @@ impl Siggen { }); } - /// Create a sine wave signal generator - /// - /// * freq: Frequency of the sine wave in \[Hz\] - pub fn newSine(nchannels: usize, freq: Flt) -> Siggen { - Siggen::new(nchannels, Box::new(Sine::new(freq))) - } - - /// Create a new signal generator wiht an arbitrary source. - pub fn new(nchannels: usize, source: Box) -> Siggen { - Siggen { - source, - channels: vec![SiggenChannelConfig::new(); nchannels], - source_buf: vec![], - chout_buf: vec![], - } - } - /// Creates *interleaved* output signal pub fn genSignal(&mut self, out: &mut [T]) where @@ -181,83 +245,6 @@ impl Siggen { } } -/// Signal generator config for a certain channel -#[derive(Clone)] -struct SiggenChannelConfig { - muted: bool, - prefilter: Option>, - gain: Flt, - DCOffset: Flt, -} -unsafe impl Send for SiggenChannelConfig {} -impl SiggenChannelConfig { - /// Set new pre-filter that filters the source signal - pub fn setPreFilter(&mut self, pref: Option>) { - self.prefilter = pref; - } - /// Set the gain applied to the source signal - /// - /// * g: Gain value. Can be any float. If set to 0.0, the source is effectively muted. Only - /// using (setMute) is a more efficient way to do this. - pub fn setGain(&mut self, g: Flt) { - self.gain = g; - } - - /// Reset signal channel config. Only resets the prefilter state - pub fn reset(&mut self, _fs: Flt) { - if let Some(f) = &mut self.prefilter { - f.reset() - } - } - /// Generate new channel configuration using 'arbitrary' initial config: muted false, gain 1.0, DC offset 0. - /// and no prefilter - pub fn new() -> SiggenChannelConfig { - SiggenChannelConfig { - muted: false, - prefilter: None, - gain: 1.0, - DCOffset: 0.0, - } - } - - /// Set mute on channel. If true, only DC signal offset is outputed from (SiggenChannelConfig::transform). - pub fn setMute(&mut self, mute: bool) { - self.muted = mute; - } - /// Generate new signal data, given input source data. - /// - /// # Args - /// - /// source: Input source signal. - /// result: Reference of array of float values to be filled with signal data. - /// - /// # Details - /// - /// - When muted, the DC offset is still applied - /// - The order of the generation is: - /// - If a prefilter is installed, this pre-filter is applied to the source signal. - /// - Gain is applied. - /// - Offset is applied (thus, no gain is applied to the DC offset). - /// - pub fn genSignal(&mut self, source: &[Flt], result: &mut [Flt]) { - if self.muted { - result.iter_mut().for_each(|x| { - *x = 0.0; - }); - } else { - result.copy_from_slice(source); - if let Some(f) = &mut self.prefilter { - f.filter(result); - } - } - result.iter_mut().for_each(|x| { - // First apply gain, then offset - *x *= self.gain; - *x += self.DCOffset; - }); - } -} - #[cfg(test)] mod test { use approx::assert_abs_diff_eq; @@ -269,7 +256,7 @@ mod test { fn test_whitenoise() { // This code is just to check syntax. We should really be listening to these outputs. let mut t = [0.0; 10]; - Siggen::newWhiteNoise(1).genSignal(&mut t); + Siggen::newWhiteNoise(1., 1).genSignal(&mut t); // println!("{:?}", &t); } @@ -280,7 +267,7 @@ mod test { const N: usize = 10000; let mut s1 = [0.0; N]; let mut s2 = [0.0; N]; - let mut siggen = Siggen::newSine(1, 1.0); + let mut siggen = Siggen::newSine(1., 1, 1.0).unwrap(); siggen.reset(10.0); siggen.setAllMute(false); @@ -305,7 +292,7 @@ mod test { const Nframes: usize = 10000; const Nch: usize = 2; let mut signal = [0.0; Nch * Nframes]; - let mut siggen = Siggen::newSine(Nch, 1.0); + let mut siggen = Siggen::newSine(fs, Nch, 1.0).unwrap(); siggen.reset(fs); siggen.setMute(&[false, true]); @@ -327,7 +314,7 @@ mod test { .sum::() / (Nframes as Flt); - assert_abs_diff_eq!(Flt::abs(ms1 - 0.5) , 0., epsilon= Flt::EPSILON * 1e3); + assert_abs_diff_eq!(Flt::abs(ms1 - 0.5), 0., epsilon = Flt::EPSILON * 1e3); assert_eq!(ms2, 0.0); } diff --git a/src/siggen/siggenchannel.rs b/src/siggen/siggenchannel.rs new file mode 100644 index 0000000..64c03ef --- /dev/null +++ b/src/siggen/siggenchannel.rs @@ -0,0 +1,78 @@ +use crate::config::*; +use crate::filter::Filter; +/// Signal generator config for a certain channel +#[derive(Clone)] +pub struct SiggenChannelConfig { + muted: bool, + prefilter: Option>, + gain: Flt, + pub DCOffset: Flt, +} +unsafe impl Send for SiggenChannelConfig {} +impl SiggenChannelConfig { + /// Set new pre-filter that filters the source signal + pub fn setPreFilter(&mut self, pref: Option>) { + self.prefilter = pref; + } + /// Set the gain applied to the source signal + /// + /// * g: Gain value. Can be any float. If set to 0.0, the source is effectively muted. Only + /// using (setMute) is a more efficient way to do this. + pub fn setGain(&mut self, g: Flt) { + self.gain = g; + } + + /// Reset signal channel config. Only resets the prefilter state + pub fn reset(&mut self, _fs: Flt) { + if let Some(f) = &mut self.prefilter { + f.reset() + } + } + /// Generate new channel configuration using 'arbitrary' initial config: muted false, gain 1.0, DC offset 0. + /// and no prefilter + pub fn new() -> SiggenChannelConfig { + SiggenChannelConfig { + muted: false, + prefilter: None, + gain: 1.0, + DCOffset: 0.0, + } + } + + /// Set mute on channel. If true, only DC signal offset is outputed from (SiggenChannelConfig::transform). + pub fn setMute(&mut self, mute: bool) { + self.muted = mute; + } + /// Generate new signal data, given input source data. + /// + /// # Args + /// + /// source: Input source signal. + /// result: Reference of array of float values to be filled with signal data. + /// + /// # Details + /// + /// - When muted, the DC offset is still applied + /// - The order of the generation is: + /// - If a prefilter is installed, this pre-filter is applied to the source signal. + /// - Gain is applied. + /// - Offset is applied (thus, no gain is applied to the DC offset). + /// + pub fn genSignal(&mut self, source: &[Flt], result: &mut [Flt]) { + if self.muted { + result.iter_mut().for_each(|x| { + *x = 0.0; + }); + } else { + result.copy_from_slice(source); + if let Some(f) = &mut self.prefilter { + f.filter(result); + } + } + result.iter_mut().for_each(|x| { + // First apply gain, then offset + *x *= self.gain; + *x += self.DCOffset; + }); + } +} diff --git a/src/siggen/siggencmd.rs b/src/siggen/siggencmd.rs new file mode 100644 index 0000000..8d74287 --- /dev/null +++ b/src/siggen/siggencmd.rs @@ -0,0 +1,38 @@ +use super::source::*; +use crate::config::*; + +/// Messages that can be send to a given signal generator [Siggen], to change its behaviour + +#[cfg_attr(feature = "python-bindings", pyclass)] +pub enum SiggenCommand { + /// Change the source to a sine wave with given frequency. + ChangeSource{ + /// New signal source to apply for signal generator + src: Source, + }, + + /// Reset the signal generator state + ResetSiggen { + /// New sampling frequency \[Hz\] + fs: Flt, + }, + + /// Set all gains to value g + SetAllGains { + /// Linear gain level to apply + g: Flt, + }, + + /// Change the mute state for a certain channel + SetMuteChannel { + /// channel index + ch: usize, + /// mute state + mute: bool, + }, + /// Change the mute state for all channels + SetMuteAllChannels { + /// mute state + mute: bool, + }, +} diff --git a/src/siggen/source.rs b/src/siggen/source.rs index 65f2a12..3917e62 100644 --- a/src/siggen/source.rs +++ b/src/siggen/source.rs @@ -1,64 +1,107 @@ +//! All sources for a signal generator. Sine waves, sweeps, noise, etc. +use super::sweep::{SweepParams, SweepType}; use crate::config::*; +use std::ops::{Deref, DerefMut}; + +/// Ratio between circumference and radius of a circle +const twopi: Flt = 2.0 * pi; +use crate::config::*; +use anyhow::{bail, Result}; use rand::prelude::*; use rand::rngs::ThreadRng; use rand_distr::StandardNormal; -/// Ratio between circumference and radius of a circle -const twopi: Flt = 2.0 * pi; - -/// Source for the signal generator. Implementations are sine waves, sweeps, noise. -pub trait Source: Send { - /// Generate the 'pure' source signal. Output is placed inside the `sig` argument. - fn genSignal_unscaled(&mut self, sig: &mut dyn ExactSizeIterator); - /// Reset the source state, i.e. set phase to 0, etc - fn reset(&mut self, fs: Flt); - /// Used to make the Siggen struct cloneable - fn clone_dyn(&self) -> Box; +/// Signal source for a signal generator. A signal source is capable of creating +/// new signal data. +#[cfg_attr(feature = "python-bindings", pyclass)] +#[derive(Clone)] +pub struct Source { + src: Box, } -impl Clone for Box { - fn clone(&self) -> Self { - self.clone_dyn() +impl Source { + /// Create a sine wave signal source + /// + /// # Args + /// + /// - `fs` - Sampling frequency \[Hz\] + /// * `freq` - Frequency of the sine wave in \[Hz\] + pub fn newSine(fs: Flt, freq: Flt) -> Result { + Ok(Source { + src: Box::new(Sine::new(fs, freq)?), + }) + } + /// Silence: create a signal source that does not output any dynamic + /// signal at all. + pub fn newSilence() -> Source { + Source { + src: Box::new(Silence {}), + } + } + + /// Create a white noise signal source + pub fn newWhiteNoise() -> Source { + Source { + src: Box::new(WhiteNoise {}), + } + } + + /// Sine sweep source + /// + /// # Args + /// + /// - `fs` - Sample rate \[Hz\] + /// - `fl` - Lower frequency \[Hz\] + /// - `fu` - Upper frequency \[Hz\] + /// - `sweep_time` - The duration of a single sweep \[s\] + /// - `quiet_time` - Time of silence after one sweep and start of the next \[s\] + /// - `sweep_type` - The type of the sweep, see [SweepType]. + pub fn newSweep( + fs: Flt, + fl: Flt, + fu: Flt, + sweep_time: Flt, + quiet_time: Flt, + sweep_type: SweepType, + ) -> Result { + Ok(Source { + src: Box::new(Sweep::new(fs, fl, fu, sweep_time, quiet_time, sweep_type)?), + }) } } #[derive(Clone)] -pub struct Silence {} +/// Silence source. Most simple one does only send out a 0. +struct Silence {} -impl Source for Silence { +impl SourceImpl for Silence { fn genSignal_unscaled(&mut self, sig: &mut dyn ExactSizeIterator) { sig.for_each(|s| { *s = 0.0; }); } fn reset(&mut self, _fs: Flt) {} - fn clone_dyn(&self) -> Box { + fn clone_dyn(&self) -> Box { Box::new(self.clone()) } } -/// White noise source +/// White noise source. Can be colored by applying a color filter to the source #[derive(Clone)] -pub struct WhiteNoise {} -impl WhiteNoise { - /// Generate new WhiteNoise generator - pub fn new() -> WhiteNoise { - WhiteNoise {} - } -} -impl Source for WhiteNoise { +struct WhiteNoise {} +impl SourceImpl for WhiteNoise { fn genSignal_unscaled(&mut self, sig: &mut dyn ExactSizeIterator) { sig.for_each(|s| { *s = thread_rng().sample(StandardNormal); }); } fn reset(&mut self, _fs: Flt) {} - fn clone_dyn(&self) -> Box { + fn clone_dyn(&self) -> Box { Box::new(self.clone()) } } /// Sine wave, with configurable frequency #[derive(Clone)] -pub struct Sine { +struct Sine { // Sampling freq \[Hz\] fs: Flt, // current stored phase @@ -73,15 +116,22 @@ impl Sine { /// /// * fs: Sampling freq [Hz] /// * - pub fn new(freq: Flt) -> Sine { - Sine { - fs: -1.0, + pub fn new(fs: Flt, freq: Flt) -> Result { + if fs <= 0. { + bail!("Invalid sampling frequency"); + } + if freq >= fs / 2. { + bail!("Frequency of sine wave should be smaller than Nyquist frequency"); + } + + Ok(Sine { + fs, phase: 0.0, omg: 2.0 * pi * freq, - } + }) } } -impl Source for Sine { +impl SourceImpl for Sine { fn genSignal_unscaled(&mut self, sig: &mut dyn ExactSizeIterator) { if self.fs <= 0.0 { sig.for_each(|s| { @@ -99,7 +149,79 @@ impl Source for Sine { self.fs = fs; self.phase = 0.0; } - fn clone_dyn(&self) -> Box { + fn clone_dyn(&self) -> Box { Box::new(self.clone()) } } + +#[cfg_attr(feature = "python-bindings", pyclass)] +#[derive(Debug, Clone)] +struct Sweep { + params: SweepParams, + // Generated time-periodic buffer + gen: Dcol, + N: usize, +} +impl Sweep { + fn new( + fs: Flt, + fl_: Flt, + fu_: Flt, + sweep_time: Flt, + quiet_time: Flt, + sweeptype: SweepType, + ) -> Result { + let params = SweepParams::new(fs, fl_, fu_, sweep_time, quiet_time, sweeptype)?; + let gen = params.getSignal(); + + Ok(Sweep { params, gen, N: 0 }) + } +} +// Linear forward or backward sweep phase +impl SourceImpl for Sweep { + fn genSignal_unscaled(&mut self, sig: &mut dyn ExactSizeIterator) { + let sweep_iter = self.gen.as_slice().unwrap().iter().cycle().skip(self.N); + for (sig, sweep_sample) in sig.zip(sweep_iter) { + *sig = *sweep_sample; + self.N += 1; + } + // Modulo number of samples in generator + self.N %= self.gen.len(); + } + + fn reset(&mut self, fs: Flt) { + self.gen = self.params.reset(fs); + self.N = 0; + } + + fn clone_dyn(&self) -> Box { + Box::new(self.clone()) + } +} + +impl Deref for Source { + type Target = Box; + fn deref(&self) -> &Self::Target { + &self.src + } +} +impl DerefMut for Source { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.src + } +} + +/// Source for the signal generator. Implementations are sine waves, sweeps, noise. +pub trait SourceImpl: Send { + /// Generate the 'pure' source signal. Output is placed inside the `sig` argument. + fn genSignal_unscaled(&mut self, sig: &mut dyn ExactSizeIterator); + /// Reset the source state, i.e. set phase to 0, etc + fn reset(&mut self, fs: Flt); + /// Used to make the Siggen struct cloneable + fn clone_dyn(&self) -> Box; +} +impl Clone for Box { + fn clone(&self) -> Self { + self.clone_dyn() + } +} diff --git a/src/siggen/sweep.rs b/src/siggen/sweep.rs new file mode 100644 index 0000000..4afe5c6 --- /dev/null +++ b/src/siggen/sweep.rs @@ -0,0 +1,264 @@ +//! Sweep signal generation code +use { + crate::config::*, + anyhow::{bail, Result}, +}; +const NITER_NEWTON: usize = 20; +const twopi: Flt = 2. * pi; + +#[cfg_attr(feature = "python-bindings", pyclass)] +#[derive(Debug, Clone)] +pub enum SweepType { + /// Forward only logarithmic sweep, repeats itself + ForwardLog, + /// Reverse only logarithmic sweep, repeats itself + BackwardLog, + /// Continuous logarithmic sweep, repeats itself + ContinuousLog, + + /// Forward only linear sweep, repeats itself + ForwardLin, + /// Reverse only linear sweep, repeats itself + BackwardLin, + /// Continuous linear sweep, repeats itself + ContinuousLin, +} + +#[derive(Debug, Clone)] +pub struct SweepParams { + // These parameters are described at [Source::newSweep] + fs: Flt, + fl: Flt, + fu: Flt, + sweep_time: Flt, + quiet_time: Flt, + sweeptype: SweepType, +} +impl SweepParams { + pub fn new( + fs: Flt, + fl_: Flt, + fu_: Flt, + sweep_time: Flt, + quiet_time: Flt, + sweeptype: SweepType, + ) -> Result { + if fs <= 0. { + bail!("Invalid sampling frequency: {} Hz", fs); + } + if fl_ > fu_ { + bail!("Lower frequency should be smaller than upper frequency"); + } + if fu_ >= fs / 2. { + bail!("Upper frequency should be smaller than sampling frequency"); + } + if sweep_time <= 0. { + bail!("Invalid sweep time, should be > 0."); + } + if 1. / sweep_time > fs / 2. { + bail!("Invalid sweep time: too short"); + } + + let (fl, fu) = if matches!(sweeptype, SweepType::BackwardLin | SweepType::BackwardLog) { + (fu_, fl_) + } else { + (fl_, fu_) + }; + Ok(SweepParams { + fs, + fl, + fu, + sweep_time, + quiet_time, + sweeptype, + }) + } + pub fn reset(&mut self, fs: Flt) -> Dcol { + self.fs = fs; + self.getSignal() + } + + fn Ns(&self) -> usize { + (self.sweep_time * self.fs) as usize + } + /// Returns the phase as a function of time + fn getPhase(&self) -> Dcol { + match self.sweeptype { + SweepType::BackwardLin | SweepType::ForwardLin => self.getLinSweepFBPhase(), + SweepType::BackwardLog | SweepType::ForwardLog => self.getLogSweepFBPhase(), + SweepType::ContinuousLin => self.getLinSweepContPhase(), + SweepType::ContinuousLog => self.getLogSweepContPhase(), + } + } + pub fn getSignal(&self) -> Dcol { + let fs = self.fs; + // Number of samples in sweep + let Ns = (self.sweep_time * fs) as usize; + // Number of samples in quiet time + let Nq = (self.quiet_time * fs) as usize; + + // Total number of samples + let N = Ns + Nq; + + let phase = self.getPhase(); + Dcol::from_iter((0..N).map(|i| if i < Ns { Flt::sin(phase[i]) } else { 0. })) + } + + // Linear forward or backward sweep phase + fn getLinSweepFBPhase(&self) -> Dcol { + assert!(matches!( + self.sweeptype, + SweepType::BackwardLin | SweepType::ForwardLin + )); + let (Ns, fl, fu, fs) = (self.Ns(), self.fl, self.fu, self.fs); + + // Time step + let Dt = 1. / fs; + let Nsf = Ns as Flt; + let K = (Dt * (fl * Nsf + 0.5 * (Nsf - 1.) * (fu - fl))).floor(); + let eps_num = K / Dt - fl * Nsf - 0.5 * (Nsf - 1.) * (fu - fl); + let eps = eps_num / (0.5 * (Nsf - 1.)); + let mut phase = 0.; + Dcol::from_iter((0..Ns).map(|n| { + let freq = fl + (n as Flt - 1.) / (Ns as Flt) * (fu + eps - fl); + let phase_out = phase; + phase += twopi * Dt * freq; + phase_out + })) + } + + // Logarithmic forward or backward sweep phase + fn getLogSweepFBPhase(&self) -> Dcol { + assert!(matches!( + self.sweeptype, + SweepType::BackwardLog | SweepType::ForwardLog + )); + + let (Ns, fl, fu, fs) = (self.Ns(), self.fl, self.fu, self.fs); + // // Time step + let Dt = 1. / fs; + let Nsf = Ns as Flt; + let mut k = fu / fl; + let K = (Dt * fl * (k - 1.) / ((k.powf(1.0 / Nsf)) - 1.)).floor(); + + /* Iterate k to the right solution */ + (0..10).for_each(|_| { + let E = 1. + K / (Dt * fl) * (k.powf(1.0 / Nsf) - 1.) - k; + let dEdk = K / (Dt * fl) * k.powf(1.0 / Nsf) / (Nsf * k) - 1.; + k -= E / dEdk; + }); + + let mut phase = 0.; + Dcol::from_iter((0..Ns).map(|n| { + let nf = n as Flt; + let fnn = fl * k.powf(nf / Nsf); + let phase_old = phase; + phase += twopi * Dt * fnn; + phase_old + })) + } + + // Continuous log sweep phase + fn getLogSweepContPhase(&self) -> Dcol { + assert!(matches!(self.sweeptype, SweepType::ContinuousLog)); + + let (Ns, fl, fu, fs) = (self.Ns(), self.fl, self.fu, self.fs); + // // Time step + let Dt = 1. / fs; + let Nf = Ns / 2; + let Nff = Nf as Flt; + let Nb = Ns - Nf; + let Nbf = Nb as Flt; + let k1 = fu / fl; + let phif1 = twopi * Dt * fl * (k1 - 1.) / (k1.powf(1.0 / Nff) - 1.); + + let K = + (phif1 / twopi + Dt * fu * (1. / k1 - 1.) / ((1. / k1).powf(1.0 / Nbf) - 1.)).floor(); + let mut k = k1; + + /* Newton iterations to converge k to the value such that the sweep is + * continuous */ + (0..NITER_NEWTON).for_each(|_| { + let E = (k - 1.) / (k.powf(1.0 / Nff) - 1.) + (k - 1.) / (1. - k.powf(-1.0 / Nbf)) + - K / Dt / fl; + + // /* All parts of the derivative of above error E to k */ + let dEdk1 = 1. / (k.powf(1.0 / Nff) - 1.); + let dEdk2 = (1. / k - 1.) / (k.powf(-1.0 / Nbf) - 1.); + let dEdk3 = -1. / (k * (k.powf(-1.0 / Nbf) - 1.)); + let dEdk4 = k.powf(-1.0 / Nbf) * (1. / k - 1.) + / (Nbf * Flt::powi(Flt::powf(k, -1.0 / Nbf) - 1., 2)); + + let dEdk5 = -Flt::powf(k, 1.0 / Nff) * (k - 1.) + / (Nff * k * Flt::powi(Flt::powf(k, 1.0 / Nff) - 1., 2)); + + let dEdk = dEdk1 + dEdk2 + dEdk3 + dEdk4 + dEdk5; + k -= E / dEdk; + }); + + let mut phase = 0.; + Dcol::from_iter((0..Ns).map(|n| { + let nf = n as Flt; + let fnn = if n <= Nf { + fl * k.powf(nf / Nff) + } else { + fl * k * (1. / k).powf((nf - Nff) / Nbf) + }; + let phase_old = phase; + phase += twopi * Dt * fnn; + + phase_old + })) + } + + // Continuous linear sweep phase + fn getLinSweepContPhase(&self) -> Dcol { + assert!(matches!(self.sweeptype, SweepType::ContinuousLin)); + + let (Ns, fl, fu, fs) = (self.Ns(), self.fl, self.fu, self.fs); + let Dt = 1. / fs; + let Nf = Ns / 2; + let Nb = Ns - Nf; + let Nff = Nf as Flt; + let Nbf = Nb as Flt; + /* Phi halfway */ + let phih = twopi * Dt * (fl * Nff + 0.5 * (Nff - 1.) * (fu - fl)); + let K = (phih / twopi + Dt * (fu * Nbf - (Nb as Flt - 1.) * (fu - fl))).floor(); + + let eps_num1 = (K - phih / twopi) / Dt; + let eps_num2 = -fu * Nbf + (Nbf - 1.) * (fu - fl); + + let eps = (eps_num1 + eps_num2) / (0.5 * (Nbf + 1.)); + let mut phase = 0.; + Dcol::from_iter((0..Ns).map(|n| { + let nf = n as Flt; + let freq = if n < Nf { + fl + nf / Nff * (fu - fl) + } else { + fu - (nf - Nff) / Nbf * (fu + eps - fl) + }; + let phase_out = phase; + phase += twopi * Dt * freq; + phase_out + })) + } +} + +#[cfg(test)] +mod test { + use approx::assert_abs_diff_eq; + + use super::*; + + #[test] + fn test_phase_linsweep1() { + let fs = 10.; + let fl = 1.; + let fu = 1.; + let phase = SweepParams::new(fs, fl, fu, 10., 0., SweepType::ForwardLin) + .unwrap() + .getLinSweepFBPhase(); + + assert_abs_diff_eq!(phase[10], &(twopi)); + } +} diff --git a/src/slm/slm.rs b/src/slm/slm.rs index 1c41720..a5297b9 100644 --- a/src/slm/slm.rs +++ b/src/slm/slm.rs @@ -285,7 +285,7 @@ mod test { .build() .unwrap(); - let mut siggen = Siggen::newSine(1, 1000.); + let mut siggen = Siggen::newSine(1., 1, 1000.).unwrap(); siggen.setAllMute(false); siggen.reset(fs); let mut data = vec![0.; N];