First work on CPAL api. Input default seems to work. This is not yet production code!
This commit is contained in:
parent
92a66f4f20
commit
3984989873
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@
|
|||||||
__pycache__
|
__pycache__
|
||||||
python/lasprs/_lasprs*
|
python/lasprs/_lasprs*
|
||||||
.venv
|
.venv
|
||||||
|
.vscode/launch.json
|
||||||
|
49
Cargo.toml
49
Cargo.toml
@ -17,24 +17,57 @@ name = "lasprs"
|
|||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
# Error handling
|
||||||
anyhow = "1.0.75"
|
anyhow = "1.0.75"
|
||||||
|
|
||||||
|
# Numerics
|
||||||
# Optional future feature for ndarray: blas
|
# Optional future feature for ndarray: blas
|
||||||
ndarray = { version = "0.15.3", features = ["rayon"] }
|
ndarray = { version = "0.15.3", features = ["rayon"] }
|
||||||
num = "0.4.1"
|
num = "0.4.1"
|
||||||
rayon = "1.8.0"
|
|
||||||
numpy = { version = "0.20" }
|
|
||||||
strum_macros = "0.25.3"
|
|
||||||
pyo3 = { version = "0.20", features=["anyhow", "extension-module"]}
|
|
||||||
rand = "0.8.5"
|
|
||||||
rand_distr = "0.4.3"
|
|
||||||
# blas-src = { version = "0.8", features = ["openblas"] }
|
# blas-src = { version = "0.8", features = ["openblas"] }
|
||||||
# openblas-src = { version = "0.10", features = ["cblas", "system"] }
|
# openblas-src = { version = "0.10", features = ["cblas", "system"] }
|
||||||
|
|
||||||
|
# Parallel iterators
|
||||||
|
rayon = "1.8.0"
|
||||||
|
|
||||||
|
# Python bindings
|
||||||
|
pyo3 = { version = "0.20", features=["anyhow", "extension-module"], optional=true }
|
||||||
|
numpy = { version = "0.20" }
|
||||||
|
|
||||||
|
# White noise etc
|
||||||
|
rand = "0.8.5"
|
||||||
|
rand_distr = "0.4.3"
|
||||||
|
|
||||||
|
# Cross-platform audio lib
|
||||||
|
cpal = { version = "0.15.2", optional=true }
|
||||||
|
|
||||||
|
# Nice enumerations
|
||||||
|
strum = "0.25.0"
|
||||||
|
strum_macros = "0.25.3"
|
||||||
|
|
||||||
|
# Conditional compilation enhancements
|
||||||
|
cfg-if = "1.0.0"
|
||||||
|
|
||||||
|
# Reinterpret buffers. This is a workaround for the #[feature(specialize)] not
|
||||||
|
# being available in stable rust.
|
||||||
|
reinterpret = "0.2.1"
|
||||||
|
|
||||||
|
# Faster channels for multithreaded communication
|
||||||
|
crossbeam = "0.8.2"
|
||||||
|
|
||||||
|
# Serialization
|
||||||
|
serde = { version = "1.0.193", features = ["derive"] }
|
||||||
|
toml = "0.8.8"
|
||||||
|
|
||||||
|
# Initialize array for non-copy type
|
||||||
|
array-init = "2.1.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["f64"]
|
default = ["f64", "cpal_api"]
|
||||||
# Use this for debugging extension
|
# Use this for debugging extension
|
||||||
# default = ["f64", "extension-module", "pyo3/extension-module"]
|
# default = ["f64", "extension-module", "pyo3/extension-module"]
|
||||||
|
cpal_api = ["dep:cpal"]
|
||||||
|
# default = ["f64", "cpal_api"]
|
||||||
f64 = []
|
f64 = []
|
||||||
f32 = []
|
f32 = []
|
||||||
extension-module = ["pyo3/extension-module"]
|
extension-module = ["dep:pyo3", "pyo3/extension-module"]
|
||||||
|
14
src/bin/test_input.rs
Normal file
14
src/bin/test_input.rs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
use lasprs::daq::StreamMgr;
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
|
||||||
|
let mut smgr = StreamMgr::new();
|
||||||
|
|
||||||
|
smgr.startDefaultInputStream()?;
|
||||||
|
|
||||||
|
let _ = io::stdin().read_line(&mut (String::new()));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -9,7 +9,11 @@ pub type Flt = f64;
|
|||||||
#[cfg(feature = "f64")]
|
#[cfg(feature = "f64")]
|
||||||
pub const pi: Flt = std::f64::consts::PI;
|
pub const pi: Flt = std::f64::consts::PI;
|
||||||
|
|
||||||
|
/// The maximum number of input channels allowed. Compile time constant to make some structs Copy.
|
||||||
|
pub const MAX_INPUT_CHANNELS: usize = 128;
|
||||||
|
|
||||||
use num::complex::*;
|
use num::complex::*;
|
||||||
|
/// Complex number floating point
|
||||||
pub type Cflt = Complex<Flt>;
|
pub type Cflt = Complex<Flt>;
|
||||||
|
|
||||||
use numpy::ndarray::{Array1, Array2};
|
use numpy::ndarray::{Array1, Array2};
|
||||||
|
109
src/daq/api/api_cpal.rs
Normal file
109
src/daq/api/api_cpal.rs
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
use super::Stream;
|
||||||
|
use crate::daq::deviceinfo::DeviceInfo;
|
||||||
|
use crate::daq::streammsg::*;
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||||
|
use cpal::{Device, Host, Sample, SampleFormat};
|
||||||
|
use crossbeam::channel::Sender;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// Cpal api
|
||||||
|
pub struct CpalApi {
|
||||||
|
host: cpal::Host,
|
||||||
|
}
|
||||||
|
impl Stream for cpal::Stream {}
|
||||||
|
|
||||||
|
impl CpalApi {
|
||||||
|
pub fn new() -> CpalApi {
|
||||||
|
CpalApi {
|
||||||
|
host: cpal::default_host(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn getDeviceInfo(&self) -> Result<Vec<DeviceInfo>> {
|
||||||
|
let devs = vec![];
|
||||||
|
for dev in self.host.devices()? {
|
||||||
|
|
||||||
|
}
|
||||||
|
Ok(devs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_input_stream(
|
||||||
|
sf: cpal::SampleFormat,
|
||||||
|
config: cpal::StreamConfig,
|
||||||
|
device: &cpal::Device,
|
||||||
|
sender: Sender<RawStreamData>,
|
||||||
|
) -> Result<cpal::Stream> {
|
||||||
|
|
||||||
|
let sender_errcallback = sender.clone();
|
||||||
|
|
||||||
|
let errfn = move |err: cpal::StreamError| match err {
|
||||||
|
cpal::StreamError::DeviceNotAvailable => sender_errcallback
|
||||||
|
.send(RawStreamData::StreamError(StreamError::DeviceNotAvailable))
|
||||||
|
.unwrap(),
|
||||||
|
cpal::StreamError::BackendSpecific { err: _ } => sender_errcallback
|
||||||
|
.send(RawStreamData::StreamError(StreamError::DriverError))
|
||||||
|
.unwrap(),
|
||||||
|
};
|
||||||
|
|
||||||
|
macro_rules! build_stream{
|
||||||
|
($($cpaltype:pat, $rtype:ty);*) => {
|
||||||
|
match sf {
|
||||||
|
$(
|
||||||
|
$cpaltype => device.build_input_stream(
|
||||||
|
&config,
|
||||||
|
move |data, _: &_| InStreamCallback::<$rtype>(data, &sender),
|
||||||
|
errfn,
|
||||||
|
None)?
|
||||||
|
),*,
|
||||||
|
_ => bail!("Unsupported sample format '{}'", sf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let stream: cpal::Stream = build_stream!(
|
||||||
|
SampleFormat::I8, i8;
|
||||||
|
SampleFormat::I16, i16;
|
||||||
|
SampleFormat::I32, i32;
|
||||||
|
SampleFormat::F32, f32
|
||||||
|
);
|
||||||
|
Ok(stream)
|
||||||
|
}
|
||||||
|
/// Start a default input stream
|
||||||
|
///
|
||||||
|
///
|
||||||
|
pub fn startDefaultInputStream(
|
||||||
|
&mut self,
|
||||||
|
sender: Sender<RawStreamData>,
|
||||||
|
) -> Result<Box<dyn Stream>> {
|
||||||
|
if let Some(device) = self.host.default_input_device() {
|
||||||
|
if let Ok(config) = device.default_input_config() {
|
||||||
|
let final_config = cpal::StreamConfig {
|
||||||
|
channels: config.channels(),
|
||||||
|
sample_rate: config.sample_rate(),
|
||||||
|
buffer_size: cpal::BufferSize::Fixed(4096),
|
||||||
|
};
|
||||||
|
|
||||||
|
let sf = config.sample_format();
|
||||||
|
let stream = CpalApi::build_input_stream(sf, final_config, &device, sender)?;
|
||||||
|
stream.play()?;
|
||||||
|
println!("Stream started with sample format {:?}", sf);
|
||||||
|
|
||||||
|
Ok(Box::new(stream))
|
||||||
|
} else {
|
||||||
|
bail!("Could not obtain default input configuration")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bail!("Could not open default input device")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// pub fn getDeviceInfo(&self) -> Result<Vec<DeviceInfo>> {
|
||||||
|
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn InStreamCallback<T>(input: &[T], sender: &Sender<RawStreamData>)
|
||||||
|
where
|
||||||
|
T: Copy + num::ToPrimitive + 'static,
|
||||||
|
{
|
||||||
|
let msg = RawStreamData::from(input);
|
||||||
|
sender.send(msg).unwrap()
|
||||||
|
}
|
24
src/daq/api/mod.rs
Normal file
24
src/daq/api/mod.rs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
/// Daq apis that are optionally compiled in. Examples:
|
||||||
|
///
|
||||||
|
/// - CPAL (Cross-Platform Audio Library)
|
||||||
|
/// - ...
|
||||||
|
use strum::EnumMessage;
|
||||||
|
use strum_macros;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
|
cfg_if::cfg_if! {
|
||||||
|
if #[cfg(feature="cpal_api")] {
|
||||||
|
pub mod api_cpal;
|
||||||
|
} else { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A currently running stream
|
||||||
|
pub trait Stream { }
|
||||||
|
|
||||||
|
#[derive(strum_macros::EnumMessage, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub enum StreamApiDescr {
|
||||||
|
/// CPAL api
|
||||||
|
#[strum(message = "Cpal", detailed_message = "Cross-Platform Audio Library")]
|
||||||
|
Cpal = 0,
|
||||||
|
}
|
67
src/daq/deviceinfo.rs
Normal file
67
src/daq/deviceinfo.rs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
//! Data acquisition model. Provides abstract layers around DAQ devices.
|
||||||
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
|
use super::datatype::DataType;
|
||||||
|
use super::qty::Qty;
|
||||||
|
use super::api::StreamApiDescr;
|
||||||
|
|
||||||
|
/// Device info structure. Gives all information regarding a device, i.e. the number of input and
|
||||||
|
/// output channels, its name and available sample rates and types.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct DeviceInfo {
|
||||||
|
|
||||||
|
/// The api in use for this device
|
||||||
|
pub api: StreamApiDescr,
|
||||||
|
|
||||||
|
/// Name for the device.
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
/// Available data types for the sample
|
||||||
|
pub avDataTypes: Vec<DataType>,
|
||||||
|
/// Preferred data type for device
|
||||||
|
pub prefDataType: DataType,
|
||||||
|
|
||||||
|
/// Available frames per block
|
||||||
|
pub avFramesPerBlock: Vec<u16>,
|
||||||
|
/// Preferred frames per block for device
|
||||||
|
pub prefFramesPerBlock: u16,
|
||||||
|
|
||||||
|
/// Available sample rates
|
||||||
|
pub avSampleRates: Vec<u16>,
|
||||||
|
/// Preferred sample rate for device
|
||||||
|
pub prefSampleRate: u16,
|
||||||
|
|
||||||
|
/// Number of input channels available for this device
|
||||||
|
pub iChannelCount: u8,
|
||||||
|
/// Number of output channels available for this device
|
||||||
|
pub oChannelCount: u8,
|
||||||
|
|
||||||
|
/// Whether the device is capable to provide IEPE constant current power supply.
|
||||||
|
pub hasInputIEPE: bool,
|
||||||
|
|
||||||
|
/// Whether the device is capable of enabling a hardware AC-coupling
|
||||||
|
pub hasInputACCouplingSwitch: bool,
|
||||||
|
|
||||||
|
///Whether the device is able to trigger on input
|
||||||
|
pub hasInputTrigger: bool,
|
||||||
|
|
||||||
|
/// Whether the device has an internal monitor of the output signal. If
|
||||||
|
/// true, the device is able to monitor output signals internally and able to
|
||||||
|
/// present output signals as virtual input signals. This only works together
|
||||||
|
/// Daq's that are able to run in full duplex mode.
|
||||||
|
pub hasInternalOutputMonitor: bool,
|
||||||
|
|
||||||
|
/// This flag is used to be able to indicate that the device cannot run
|
||||||
|
/// input and output streams independently, without opening the device in
|
||||||
|
/// duplex mode. This is for example true for the UlDaq: only one handle to
|
||||||
|
/// the device can be given at the same time.
|
||||||
|
pub duplexModeForced: bool,
|
||||||
|
|
||||||
|
/// The physical quantity of the output signal. For 'normal' audio
|
||||||
|
/// devices, this is typically a 'number' between +/- full scale. For some
|
||||||
|
/// devices however, the output quantity corresponds to a physical signal,
|
||||||
|
/// such a Volts.
|
||||||
|
pub physicalIOQty: Qty,
|
||||||
|
}
|
||||||
|
|
227
src/daq/mod.rs
Normal file
227
src/daq/mod.rs
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
//! Data acquisition model. Provides abstract layers around DAQ devices.
|
||||||
|
|
||||||
|
mod api;
|
||||||
|
mod daqconfig;
|
||||||
|
mod datatype;
|
||||||
|
mod deviceinfo;
|
||||||
|
mod qty;
|
||||||
|
mod streammsg;
|
||||||
|
|
||||||
|
pub use datatype::*;
|
||||||
|
pub use deviceinfo::*;
|
||||||
|
pub use qty::*;
|
||||||
|
pub use streammsg::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "cpal_api")]
|
||||||
|
use api::api_cpal::CpalApi;
|
||||||
|
|
||||||
|
use crate::config::*;
|
||||||
|
use anyhow::{bail, Error, Result};
|
||||||
|
use api::Stream;
|
||||||
|
use core::time;
|
||||||
|
use crossbeam::{
|
||||||
|
channel::{unbounded, Receiver, Sender},
|
||||||
|
thread,
|
||||||
|
};
|
||||||
|
use deviceinfo::DeviceInfo;
|
||||||
|
use std::sync::{atomic::AtomicBool, Arc, Mutex};
|
||||||
|
use std::thread::{JoinHandle, Thread};
|
||||||
|
use streammsg::*;
|
||||||
|
|
||||||
|
/// Keep track of whether the stream has been created. To ensure singleton behaviour.
|
||||||
|
static smgr_created: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
struct InputStream {
|
||||||
|
streamtype: StreamType,
|
||||||
|
stream: Box<dyn Stream>,
|
||||||
|
threadhandle: JoinHandle<InQueues>,
|
||||||
|
comm: Sender<StreamCommand>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configure and manage input / output streams.
|
||||||
|
///
|
||||||
|
pub struct StreamMgr {
|
||||||
|
// Input stream can be both input and duplex
|
||||||
|
input_stream: Option<InputStream>,
|
||||||
|
|
||||||
|
// Output only stream
|
||||||
|
output_stream: Option<Box<dyn Stream>>,
|
||||||
|
|
||||||
|
// Signal generator
|
||||||
|
siggen: Option<crate::siggen::Siggen>,
|
||||||
|
|
||||||
|
#[cfg(feature = "cpal_api")]
|
||||||
|
cpal_api: CpalApi,
|
||||||
|
|
||||||
|
/// The storage of queues. When no streams are running, they
|
||||||
|
/// are here. When stream is running, they will become available
|
||||||
|
/// in the JoinHandle of the thread.
|
||||||
|
instreamqueues: Option<InQueues>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StreamMgr {
|
||||||
|
/// Create new stream manager. A stream manager is supposed to be a singleton.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// When a StreamMgr object is already alive.
|
||||||
|
pub fn new() -> StreamMgr {
|
||||||
|
if smgr_created.load(std::sync::atomic::Ordering::Relaxed) {
|
||||||
|
panic!("BUG: Only one stream manager is supposed to be a singleton");
|
||||||
|
}
|
||||||
|
smgr_created.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
|
||||||
|
StreamMgr {
|
||||||
|
input_stream: None,
|
||||||
|
output_stream: None,
|
||||||
|
siggen: None,
|
||||||
|
|
||||||
|
#[cfg(feature = "cpal_api")]
|
||||||
|
cpal_api: CpalApi::new(),
|
||||||
|
|
||||||
|
instreamqueues: Some(vec![]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Obtain a list of devices that are available for each available API
|
||||||
|
fn getDeviceInfo(&mut self) -> Vec<DeviceInfo> {
|
||||||
|
let mut devinfo = vec![];
|
||||||
|
#[cfg(feature="cpal_api")]
|
||||||
|
devinfo.extend(self.cpal_api.getDeviceInfo());
|
||||||
|
devinfo
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start a default input stream, using default settings on everything. This is only possible
|
||||||
|
/// when
|
||||||
|
pub fn startDefaultInputStream(&mut self) -> Result<()> {
|
||||||
|
#![allow(unreachable_code)]
|
||||||
|
if !self.input_stream.is_none() {
|
||||||
|
bail!("Input stream is already running. Please first stop existing input stream.")
|
||||||
|
}
|
||||||
|
|
||||||
|
let (tx, rx): (Sender<RawStreamData>, Receiver<RawStreamData>) = unbounded();
|
||||||
|
|
||||||
|
cfg_if::cfg_if! {
|
||||||
|
if #[cfg(feature="cpal_api")] {
|
||||||
|
let stream = self.cpal_api.startDefaultInputStream(tx)?;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
bail!("Unable to start default input stream: no CPAL api available")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap here, as the queues should be free to grab
|
||||||
|
let mut iqueues = self.instreamqueues.take().unwrap();
|
||||||
|
|
||||||
|
let (commtx, commrx) = unbounded();
|
||||||
|
|
||||||
|
// let metadata = StreamMetaData::new(
|
||||||
|
// nchannels:
|
||||||
|
// ).unwrap();
|
||||||
|
let threadhandle = std::thread::spawn(move || {
|
||||||
|
let mut ctr: usize = 0;
|
||||||
|
'infy: loop {
|
||||||
|
if let Ok(comm_msg) = commrx.try_recv() {
|
||||||
|
match comm_msg {
|
||||||
|
// New queue added
|
||||||
|
StreamCommand::AddInQueue(queue) => {
|
||||||
|
iqueues.push(queue);
|
||||||
|
// queue.send(streammsg::StreamMetaData(md))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove queue from list
|
||||||
|
StreamCommand::RemoveInQueue(queue) => {
|
||||||
|
iqueues.retain(|q| !Arc::ptr_eq(q, &queue))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop this thread. Returns the queue
|
||||||
|
StreamCommand::StopThread => {
|
||||||
|
for q in iqueues.iter() {
|
||||||
|
q.send(InStreamMsg::StreamStopped).unwrap();
|
||||||
|
}
|
||||||
|
break 'infy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(msg) = rx.recv_timeout(time::Duration::from_millis(10)) {
|
||||||
|
// println!("Obtained raw stream data!");
|
||||||
|
let msg = Arc::new(msg);
|
||||||
|
for q in iqueues.iter() {
|
||||||
|
q.send(InStreamMsg::RawStreamData(ctr, msg.clone()))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctr += 1;
|
||||||
|
}
|
||||||
|
iqueues
|
||||||
|
});
|
||||||
|
|
||||||
|
cfg_if::cfg_if! {
|
||||||
|
if #[cfg(feature="cpal_api")] {
|
||||||
|
self.input_stream = Some(InputStream {
|
||||||
|
streamtype: StreamType::Input,
|
||||||
|
stream,
|
||||||
|
threadhandle,
|
||||||
|
comm: commtx,
|
||||||
|
});
|
||||||
|
} else {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop existing input stream.
|
||||||
|
pub fn stopInputStream(&mut self) -> Result<()> {
|
||||||
|
if let Some(InputStream {
|
||||||
|
streamtype: _, // Ignored here
|
||||||
|
stream: _,
|
||||||
|
threadhandle,
|
||||||
|
comm,
|
||||||
|
}) = self.input_stream.take()
|
||||||
|
{
|
||||||
|
// println!("Stopping existing stream..");
|
||||||
|
// Send thread to stop
|
||||||
|
comm.send(StreamCommand::StopThread).unwrap();
|
||||||
|
|
||||||
|
// Store stream queues back into StreamMgr
|
||||||
|
self.instreamqueues = Some(threadhandle.join().expect("Stream thread panicked!"));
|
||||||
|
} else {
|
||||||
|
bail!("Stream is not running.")
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
/// Stop existing running stream.
|
||||||
|
///
|
||||||
|
/// Args
|
||||||
|
///
|
||||||
|
/// * st: The stream type.
|
||||||
|
pub fn stopStream(&mut self, st: StreamType) -> Result<()> {
|
||||||
|
match st {
|
||||||
|
StreamType::Input | StreamType::Duplex => self.stopInputStream(),
|
||||||
|
_ => bail!("Not implemented output stream"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // impl StreamMgr
|
||||||
|
impl Drop for StreamMgr {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// Kill input stream if there is one
|
||||||
|
if self.input_stream.is_some() {
|
||||||
|
self.stopStream(StreamType::Input).unwrap();
|
||||||
|
}
|
||||||
|
if self.output_stream.is_some() {
|
||||||
|
self.stopStream(StreamType::Output).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decref the singleton
|
||||||
|
smgr_created.store(false, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Daq devices
|
||||||
|
trait Daq {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
}
|
183
src/daq/streammsg.rs
Normal file
183
src/daq/streammsg.rs
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
//! Provides stream messages that come from a running stream
|
||||||
|
use crate::config::*;
|
||||||
|
use crate::daq::DataType;
|
||||||
|
use crate::daq::Qty;
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use crossbeam::channel::Sender;
|
||||||
|
use reinterpret::reinterpret_slice;
|
||||||
|
use std::any::TypeId;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::u128::MAX;
|
||||||
|
|
||||||
|
use super::daqconfig::DaqChannel;
|
||||||
|
|
||||||
|
/// Raw stream data coming from a stream.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum RawStreamData {
|
||||||
|
/// 8-bits integer
|
||||||
|
Datai8(Arc<Vec<i8>>),
|
||||||
|
/// 16-bits integer
|
||||||
|
Datai16(Arc<Vec<i16>>),
|
||||||
|
/// 32-bits integer
|
||||||
|
Datai32(Arc<Vec<i32>>),
|
||||||
|
/// 32-bits float
|
||||||
|
Dataf32(Arc<Vec<f32>>),
|
||||||
|
/// 64-bits float
|
||||||
|
Dataf64(Arc<Vec<f64>>),
|
||||||
|
|
||||||
|
/// Unknown data type. We cannot do anything with it, we could instead also create an error, although this is easier to pass downstream.
|
||||||
|
UnknownDataType,
|
||||||
|
|
||||||
|
/// A stream error occured
|
||||||
|
StreamError(StreamError),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create InStreamData object from
|
||||||
|
impl<T> From<&[T]> for RawStreamData
|
||||||
|
where
|
||||||
|
T: num::ToPrimitive + Clone + 'static,
|
||||||
|
{
|
||||||
|
fn from(input: &[T]) -> RawStreamData {
|
||||||
|
// Apparently, this code does not work with a match. I have searched around and have not found the
|
||||||
|
// reason for this. So this is a bit of stupid boilerplate.
|
||||||
|
let i8type: TypeId = TypeId::of::<i8>();
|
||||||
|
let i16type: TypeId = TypeId::of::<i16>();
|
||||||
|
let i32type: TypeId = TypeId::of::<i32>();
|
||||||
|
let f32type: TypeId = TypeId::of::<f32>();
|
||||||
|
let f64type: TypeId = TypeId::of::<f64>();
|
||||||
|
let thetype: TypeId = TypeId::of::<T>();
|
||||||
|
if i8type == thetype {
|
||||||
|
let v: Vec<i8> = unsafe { reinterpret_slice(input).to_vec() };
|
||||||
|
RawStreamData::Datai8(Arc::new(v))
|
||||||
|
} else if i16type == thetype {
|
||||||
|
let v: Vec<i16> = unsafe { reinterpret_slice(input).to_vec() };
|
||||||
|
RawStreamData::Datai16(Arc::new(v))
|
||||||
|
} else if i16type == thetype {
|
||||||
|
let v: Vec<i16> = unsafe { reinterpret_slice(input).to_vec() };
|
||||||
|
RawStreamData::Datai16(Arc::new(v))
|
||||||
|
} else if i32type == thetype {
|
||||||
|
let v: Vec<i32> = unsafe { reinterpret_slice(input).to_vec() };
|
||||||
|
RawStreamData::Datai32(Arc::new(v))
|
||||||
|
} else if f32type == thetype {
|
||||||
|
let v: Vec<f32> = unsafe { reinterpret_slice(input).to_vec() };
|
||||||
|
RawStreamData::Dataf32(Arc::new(v))
|
||||||
|
} else if f64type == thetype {
|
||||||
|
let v: Vec<f64> = unsafe { reinterpret_slice(input).to_vec() };
|
||||||
|
RawStreamData::Dataf64(Arc::new(v))
|
||||||
|
} else {
|
||||||
|
panic!("Not implemented sample type!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stream metadata. All information required for
|
||||||
|
pub struct StreamMetaData {
|
||||||
|
/// The number of channels. Should be <= MAX_INPUT_CHANNELS
|
||||||
|
pub nchannels: usize,
|
||||||
|
|
||||||
|
/// Information for each channel in the stream
|
||||||
|
pub channelInfo: [DaqChannel; MAX_INPUT_CHANNELS],
|
||||||
|
|
||||||
|
/// The data type of the device [Number / voltage]
|
||||||
|
pub rawDatatype: DataType,
|
||||||
|
|
||||||
|
/// Sample rate in [Hz]
|
||||||
|
pub samplerate: Flt,
|
||||||
|
}
|
||||||
|
impl StreamMetaData {
|
||||||
|
/// Create new metadata object. Throws an error if the number of channels for the sensitivity and quantities does
|
||||||
|
/// not match.
|
||||||
|
///
|
||||||
|
/// # Args
|
||||||
|
///
|
||||||
|
/// * nchannels: The number of channels that are send
|
||||||
|
/// * sens: Sensitivity values for each channel
|
||||||
|
/// * rawdtype: The data type of the raw stream data. For sound cards this is Number, for DAQ's, this might be a voltage.
|
||||||
|
/// * qtys_: The physical quantities for each channel
|
||||||
|
/// * sr: The sample rate in \[Hz\]
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// If the number of channels > MAX_INPUT_CHANNELS
|
||||||
|
pub fn new(channel_data: &[DaqChannel], rawdtype: DataType, sr: Flt) -> Result<StreamMetaData> {
|
||||||
|
if channel_data.len() > MAX_INPUT_CHANNELS {
|
||||||
|
bail!("Too many channels provided.")
|
||||||
|
}
|
||||||
|
let nchannels = channel_data.len();
|
||||||
|
let channelInfo: [DaqChannel; MAX_INPUT_CHANNELS] =
|
||||||
|
array_init::array_init(|_i: usize| DaqChannel::default());
|
||||||
|
|
||||||
|
Ok(StreamMetaData {
|
||||||
|
nchannels,
|
||||||
|
channelInfo,
|
||||||
|
rawDatatype: rawdtype,
|
||||||
|
samplerate: sr,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Input stream messages, to be send to handlers.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum InStreamMsg {
|
||||||
|
/// Raw stream data that is coming from a device. This is interleaved data. The number of channels is correct and
|
||||||
|
/// specified in the stream metadata.
|
||||||
|
RawStreamData(usize, Arc<RawStreamData>),
|
||||||
|
|
||||||
|
/// An error has occured in the stream
|
||||||
|
StreamError(StreamError),
|
||||||
|
|
||||||
|
/// Stream data converted to floating point with sample width as
|
||||||
|
/// compiled in.
|
||||||
|
ConvertedStreamData(usize, Arc<crate::config::Dmat>),
|
||||||
|
|
||||||
|
/// new Stream metadata enters the scene. Probably a new stream started.
|
||||||
|
StreamStarted(Arc<StreamMetaData>),
|
||||||
|
|
||||||
|
/// An existing stream stopped.
|
||||||
|
StreamStopped,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store a queue in a shared pointer, to share sending
|
||||||
|
/// and receiving part of the queue.
|
||||||
|
pub type SharedInQueue = Arc<Sender<InStreamMsg>>;
|
||||||
|
/// Vector of queues for stream messages
|
||||||
|
pub type InQueues = Vec<SharedInQueue>;
|
||||||
|
|
||||||
|
/// Commands that can be sent to a running stream
|
||||||
|
pub enum StreamCommand {
|
||||||
|
/// Add a new queue to a running stream
|
||||||
|
AddInQueue(SharedInQueue),
|
||||||
|
/// Remove a queue to a running stream
|
||||||
|
RemoveInQueue(SharedInQueue),
|
||||||
|
|
||||||
|
/// Stop the thread, do not listen for data anymore.
|
||||||
|
StopThread,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stream types that can be started
|
||||||
|
///
|
||||||
|
pub enum StreamType {
|
||||||
|
/// Input-only stream
|
||||||
|
Input,
|
||||||
|
/// Output-only stream
|
||||||
|
Output,
|
||||||
|
/// Input and output at the same time
|
||||||
|
Duplex,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Errors that happen in a stream
|
||||||
|
#[derive(strum_macros::EnumMessage, Debug, Clone)]
|
||||||
|
pub enum StreamError {
|
||||||
|
/// Input overrun
|
||||||
|
#[strum(message = "InputXRunError", detailed_message = "Input buffer overrun")]
|
||||||
|
InputXRunError,
|
||||||
|
/// Output underrun
|
||||||
|
#[strum(message = "OutputXRunError", detailed_message = "Output buffer overrun")]
|
||||||
|
OutputXRunError,
|
||||||
|
/// Driver specific error
|
||||||
|
#[strum(message = "DriverError", detailed_message = "Driver error")]
|
||||||
|
DriverError,
|
||||||
|
|
||||||
|
/// Device
|
||||||
|
#[strum(detailed_message = "Device not available")]
|
||||||
|
DeviceNotAvailable
|
||||||
|
}
|
@ -5,12 +5,17 @@
|
|||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
use super::config::*;
|
use super::config::*;
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
|
use cfg_if::cfg_if;
|
||||||
use numpy::ndarray::{ArrayD, ArrayViewD, ArrayViewMutD};
|
use numpy::ndarray::{ArrayD, ArrayViewD, ArrayViewMutD};
|
||||||
use numpy::{IntoPyArray, PyArray1, PyArrayDyn, PyArrayLike1, PyReadonlyArrayDyn};
|
use numpy::{IntoPyArray, PyArray1, PyArrayDyn, PyArrayLike1, PyReadonlyArrayDyn};
|
||||||
|
use rayon::prelude::*;
|
||||||
|
|
||||||
|
cfg_if! {
|
||||||
|
if #[cfg(feature = "extension-module")] {
|
||||||
use pyo3::exceptions::PyValueError;
|
use pyo3::exceptions::PyValueError;
|
||||||
use pyo3::prelude::*;
|
use pyo3::prelude::*;
|
||||||
use pyo3::{pymodule, types::PyModule, PyResult};
|
use pyo3::{pymodule, types::PyModule, PyResult};
|
||||||
use rayon::prelude::*;
|
} else {} }
|
||||||
|
|
||||||
pub trait Filter: Send {
|
pub trait Filter: Send {
|
||||||
//! The filter trait is implemented by Biquad, SeriesBiquad, and BiquadBank
|
//! The filter trait is implemented by Biquad, SeriesBiquad, and BiquadBank
|
||||||
@ -170,7 +175,7 @@ impl Filter for Biquad {
|
|||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// See [tests]
|
/// See (tests)
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
#[cfg_attr(feature = "extension-module", pyclass)]
|
#[cfg_attr(feature = "extension-module", pyclass)]
|
||||||
@ -359,7 +364,6 @@ impl BiquadBank {
|
|||||||
}
|
}
|
||||||
self.set_gains_dB(gains_dB.as_slice()?);
|
self.set_gains_dB(gains_dB.as_slice()?);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
||||||
}
|
}
|
||||||
#[pyo3(name = "len")]
|
#[pyo3(name = "len")]
|
||||||
/// See: [BiquadBank::len()]
|
/// See: [BiquadBank::len()]
|
||||||
|
@ -5,16 +5,18 @@
|
|||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
#![allow(non_upper_case_globals)]
|
#![allow(non_upper_case_globals)]
|
||||||
#![allow(unused_imports)]
|
#![allow(unused_imports)]
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
pub mod filter;
|
pub mod filter;
|
||||||
|
|
||||||
// pub mod window;
|
// pub mod window;
|
||||||
// pub mod ps;
|
// pub mod ps;
|
||||||
|
pub mod daq;
|
||||||
pub mod siggen;
|
pub mod siggen;
|
||||||
|
|
||||||
extern crate pyo3;
|
|
||||||
#[cfg(feature = "extension-module")]
|
#[cfg(feature = "extension-module")]
|
||||||
use pyo3::prelude::*;
|
use pyo3::prelude::*;
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
//! ## Create some white noise and print it.
|
//! ## Create some white noise and print it.
|
||||||
//!
|
//!
|
||||||
//! ```
|
//! ```
|
||||||
|
//! use lasprs::siggen::Siggen;
|
||||||
//! let mut wn = Siggen::newWhiteNoise();
|
//! let mut wn = Siggen::newWhiteNoise();
|
||||||
//! wn.setGain(0.1);
|
//! wn.setGain(0.1);
|
||||||
//! wn.setMute(false);
|
//! wn.setMute(false);
|
||||||
@ -15,12 +16,14 @@
|
|||||||
//! ```
|
//! ```
|
||||||
use super::config::*;
|
use super::config::*;
|
||||||
use super::filter::Filter;
|
use super::filter::Filter;
|
||||||
|
#[cfg(feature="extension-module")]
|
||||||
use pyo3::prelude::*;
|
use pyo3::prelude::*;
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use rand::rngs::ThreadRng;
|
use rand::rngs::ThreadRng;
|
||||||
use rand_distr::StandardNormal;
|
use rand_distr::StandardNormal;
|
||||||
|
|
||||||
trait Source: Send {
|
/// 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.
|
/// Generate the 'pure' source signal. Output is placed inside the `sig` argument.
|
||||||
fn genSignal_unscaled(&mut self, sig: &mut [Flt]);
|
fn genSignal_unscaled(&mut self, sig: &mut [Flt]);
|
||||||
/// Reset the source state, i.e. set phase to 0, etc
|
/// Reset the source state, i.e. set phase to 0, etc
|
||||||
@ -104,10 +107,14 @@ impl Source for Sine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sweep signal
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
/// Signal generator. Able to create acoustic output signals
|
/// 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)
|
||||||
|
///
|
||||||
pub struct Siggen {
|
pub struct Siggen {
|
||||||
// The source dynamic signal. Noise, a sine wave, sweep, etc
|
// The source dynamic signal. Noise, a sine wave, sweep, etc
|
||||||
source: Box<dyn Source>,
|
source: Box<dyn Source>,
|
||||||
@ -138,7 +145,7 @@ impl Siggen {
|
|||||||
}
|
}
|
||||||
/// Create a sine wave signal generator
|
/// Create a sine wave signal generator
|
||||||
///
|
///
|
||||||
/// * freq: Frequency of the sine wave in [Hz]
|
/// * freq: Frequency of the sine wave in \[Hz\]
|
||||||
pub fn newSineWave(freq: Flt) -> Siggen {
|
pub fn newSineWave(freq: Flt) -> Siggen {
|
||||||
Siggen::new(Box::new(Sine::new(freq)))
|
Siggen::new(Box::new(Sine::new(freq)))
|
||||||
}
|
}
|
||||||
@ -191,7 +198,7 @@ impl Siggen {
|
|||||||
///
|
///
|
||||||
/// Args
|
/// Args
|
||||||
///
|
///
|
||||||
/// * fs: (New) Sampling frequency [Hz]
|
/// * fs: (New) Sampling frequency \[Hz\]
|
||||||
///
|
///
|
||||||
pub fn reset(&mut self, fs: Flt) {
|
pub fn reset(&mut self, fs: Flt) {
|
||||||
self.source.reset(fs);
|
self.source.reset(fs);
|
||||||
@ -199,6 +206,11 @@ impl Siggen {
|
|||||||
f.reset();
|
f.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set mut on signal generator. If true, only DC signal offset is outputed from (Sigen::genSignal).
|
||||||
|
pub fn setMute(&mut self, mute: bool) {
|
||||||
|
self.muted = mute
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -207,8 +219,19 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_whitenoise() {
|
fn test_whitenoise() {
|
||||||
let mut t = &[0.; 10];
|
// This code is just to check syntax. We should really be listening to these outputs.
|
||||||
Siggen::newWiteNoise().genSignal(&mut t);
|
let mut t = [0.; 10];
|
||||||
|
Siggen::newWhiteNoise().genSignal(&mut t);
|
||||||
println!("{:?}", &t);
|
println!("{:?}", &t);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sine() {
|
||||||
|
// This code is just to check syntax. We should really be listening to these outputs.
|
||||||
|
let mut s = [0.; 9];
|
||||||
|
let mut siggen = Siggen::newSineWave(1.);
|
||||||
|
siggen.reset(1.);
|
||||||
|
siggen.genSignal(&mut s);
|
||||||
|
println!("{:?}", &s);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user