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__
|
||||
python/lasprs/_lasprs*
|
||||
.venv
|
||||
.vscode/launch.json
|
||||
|
49
Cargo.toml
49
Cargo.toml
@ -17,24 +17,57 @@ name = "lasprs"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
# Error handling
|
||||
anyhow = "1.0.75"
|
||||
|
||||
# Numerics
|
||||
# Optional future feature for ndarray: blas
|
||||
ndarray = { version = "0.15.3", features = ["rayon"] }
|
||||
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"] }
|
||||
# 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]
|
||||
default = ["f64"]
|
||||
default = ["f64", "cpal_api"]
|
||||
# Use this for debugging extension
|
||||
# default = ["f64", "extension-module", "pyo3/extension-module"]
|
||||
cpal_api = ["dep:cpal"]
|
||||
# default = ["f64", "cpal_api"]
|
||||
f64 = []
|
||||
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")]
|
||||
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::*;
|
||||
/// Complex number floating point
|
||||
pub type Cflt = Complex<Flt>;
|
||||
|
||||
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)]
|
||||
use super::config::*;
|
||||
use anyhow::{bail, Result};
|
||||
use cfg_if::cfg_if;
|
||||
use numpy::ndarray::{ArrayD, ArrayViewD, ArrayViewMutD};
|
||||
use numpy::{IntoPyArray, PyArray1, PyArrayDyn, PyArrayLike1, PyReadonlyArrayDyn};
|
||||
use rayon::prelude::*;
|
||||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "extension-module")] {
|
||||
use pyo3::exceptions::PyValueError;
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::{pymodule, types::PyModule, PyResult};
|
||||
use rayon::prelude::*;
|
||||
} else {} }
|
||||
|
||||
pub trait Filter: Send {
|
||||
//! The filter trait is implemented by Biquad, SeriesBiquad, and BiquadBank
|
||||
@ -170,7 +175,7 @@ impl Filter for Biquad {
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// See [tests]
|
||||
/// See (tests)
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(feature = "extension-module", pyclass)]
|
||||
@ -359,7 +364,6 @@ impl BiquadBank {
|
||||
}
|
||||
self.set_gains_dB(gains_dB.as_slice()?);
|
||||
Ok(())
|
||||
|
||||
}
|
||||
#[pyo3(name = "len")]
|
||||
/// See: [BiquadBank::len()]
|
||||
|
38
src/lib.rs
38
src/lib.rs
@ -3,30 +3,32 @@
|
||||
//! This crate contains structures and functions to perform acoustic measurements, interact with
|
||||
//! data acquisition devices and apply common acoustic analysis operations on them.
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(non_upper_case_globals)]
|
||||
#![allow(unused_imports)]
|
||||
#![warn(missing_docs)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
mod config;
|
||||
pub mod filter;
|
||||
// pub mod window;
|
||||
// pub mod ps;
|
||||
pub mod siggen;
|
||||
#![allow(non_upper_case_globals)]
|
||||
#![allow(unused_imports)]
|
||||
|
||||
extern crate pyo3;
|
||||
#[cfg(feature = "extension-module")]
|
||||
use pyo3::prelude::*;
|
||||
mod config;
|
||||
pub mod filter;
|
||||
|
||||
/// A Python module implemented in Rust.
|
||||
#[cfg(feature = "extension-module")]
|
||||
#[pymodule]
|
||||
#[pyo3(name="_lasprs")]
|
||||
fn lasprs(py: Python, m: &PyModule) -> PyResult<()> {
|
||||
// pub mod window;
|
||||
// pub mod ps;
|
||||
pub mod daq;
|
||||
pub mod siggen;
|
||||
|
||||
#[cfg(feature = "extension-module")]
|
||||
use pyo3::prelude::*;
|
||||
|
||||
/// A Python module implemented in Rust.
|
||||
#[cfg(feature = "extension-module")]
|
||||
#[pymodule]
|
||||
#[pyo3(name="_lasprs")]
|
||||
fn lasprs(py: Python, m: &PyModule) -> PyResult<()> {
|
||||
|
||||
pyo3_add_submodule_filter(py, &m)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Add filter submodule to extension
|
||||
#[cfg(feature = "extension-module")]
|
||||
|
@ -5,6 +5,7 @@
|
||||
//! ## Create some white noise and print it.
|
||||
//!
|
||||
//! ```
|
||||
//! use lasprs::siggen::Siggen;
|
||||
//! let mut wn = Siggen::newWhiteNoise();
|
||||
//! wn.setGain(0.1);
|
||||
//! wn.setMute(false);
|
||||
@ -15,12 +16,14 @@
|
||||
//! ```
|
||||
use super::config::*;
|
||||
use super::filter::Filter;
|
||||
#[cfg(feature="extension-module")]
|
||||
use pyo3::prelude::*;
|
||||
use rand::prelude::*;
|
||||
use rand::rngs::ThreadRng;
|
||||
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.
|
||||
fn genSignal_unscaled(&mut self, sig: &mut [Flt]);
|
||||
/// Reset the source state, i.e. set phase to 0, etc
|
||||
@ -104,10 +107,14 @@ impl Source for Sine {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sweep signal
|
||||
|
||||
#[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 {
|
||||
// The source dynamic signal. Noise, a sine wave, sweep, etc
|
||||
source: Box<dyn Source>,
|
||||
@ -138,7 +145,7 @@ impl Siggen {
|
||||
}
|
||||
/// 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 {
|
||||
Siggen::new(Box::new(Sine::new(freq)))
|
||||
}
|
||||
@ -191,7 +198,7 @@ impl Siggen {
|
||||
///
|
||||
/// Args
|
||||
///
|
||||
/// * fs: (New) Sampling frequency [Hz]
|
||||
/// * fs: (New) Sampling frequency \[Hz\]
|
||||
///
|
||||
pub fn reset(&mut self, fs: Flt) {
|
||||
self.source.reset(fs);
|
||||
@ -199,6 +206,11 @@ impl Siggen {
|
||||
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)]
|
||||
@ -207,8 +219,19 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_whitenoise() {
|
||||
let mut t = &[0.; 10];
|
||||
Siggen::newWiteNoise().genSignal(&mut t);
|
||||
// This code is just to check syntax. We should really be listening to these outputs.
|
||||
let mut t = [0.; 10];
|
||||
Siggen::newWhiteNoise().genSignal(&mut 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