Adds pulse shaping, work stealing
This commit is contained in:
@ -7,3 +7,4 @@ edition = "2024"
|
||||
num = "0.4.3"
|
||||
oxydsp-flowgraph = {path = "../oxydsp-flowgraph/"}
|
||||
rustfft = "6.4.1"
|
||||
wide = "1.2.0"
|
||||
|
||||
@ -1 +1,2 @@
|
||||
pub mod fir;
|
||||
pub mod pulse_shaping;
|
||||
|
||||
@ -1,20 +1,24 @@
|
||||
use num::Zero;
|
||||
use oxydsp_flowgraph::BlockIO;
|
||||
use oxydsp_flowgraph::block::Block;
|
||||
use oxydsp_flowgraph::block::BlockResult;
|
||||
use oxydsp_flowgraph::block::SyncBlock;
|
||||
use oxydsp_flowgraph::io::In;
|
||||
use oxydsp_flowgraph::io::Out;
|
||||
use oxydsp_flowgraph::io::PopIterable;
|
||||
use oxydsp_flowgraph::sync_block;
|
||||
use std::iter::Sum;
|
||||
use std::ops::Add;
|
||||
use std::ops::Mul;
|
||||
|
||||
use crate::filtering::fir::Fir;
|
||||
|
||||
#[derive(BlockIO)]
|
||||
#[sync_block]
|
||||
pub struct FirFilter<F, T, O>
|
||||
where
|
||||
T: Clone + 'static,
|
||||
T: Clone + Zero + 'static,
|
||||
F: Mul<T, Output = O> + Clone + 'static,
|
||||
O: Sum + 'static,
|
||||
O: Add<O, Output = O> + Sum + Clone + Zero + 'static,
|
||||
{
|
||||
#[input]
|
||||
input: In<T>,
|
||||
@ -27,9 +31,9 @@ where
|
||||
|
||||
impl<F, T, O> FirFilter<F, T, O>
|
||||
where
|
||||
T: Clone + 'static,
|
||||
T: Clone + Zero + 'static,
|
||||
F: Mul<T, Output = O> + Clone + 'static,
|
||||
O: Sum + 'static,
|
||||
O: Add<O, Output = O> + Sum + Clone + Zero,
|
||||
{
|
||||
pub fn new(input: In<T>, impulse_response: Fir<F>) -> (Self, In<O>)
|
||||
{
|
||||
@ -45,14 +49,15 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<'view, F, T, O> SyncBlock<'view> for FirFilter<F, T, O>
|
||||
impl<F, T, O> Block for FirFilter<F, T, O>
|
||||
where
|
||||
T: Clone + 'view,
|
||||
T: Clone + Zero,
|
||||
F: Mul<T, Output = O> + Clone + 'static,
|
||||
O: Sum + 'static,
|
||||
O: Add<O, Output = O> + Sum + Clone + Zero,
|
||||
{
|
||||
fn sync_work(state: Self::StateView, input: Self::Input) -> Option<Self::Output>
|
||||
fn work(&mut self) -> oxydsp_flowgraph::block::BlockResult
|
||||
{
|
||||
Some(state.filter.next(input))
|
||||
self.output.push_iter(self.input.pop_iter().map(|x| (self.filter.next(x.0), x.1).into()));
|
||||
BlockResult::Ok
|
||||
}
|
||||
}
|
||||
|
||||
77
oxydsp-dsp/src/blocks/filtering/pulse_shaping.rs
Normal file
77
oxydsp-dsp/src/blocks/filtering/pulse_shaping.rs
Normal file
@ -0,0 +1,77 @@
|
||||
use std::iter::Sum;
|
||||
use std::ops::Add;
|
||||
|
||||
use num::Zero;
|
||||
use oxydsp_flowgraph::BlockIO;
|
||||
use oxydsp_flowgraph::block::Block;
|
||||
use oxydsp_flowgraph::io::In;
|
||||
use oxydsp_flowgraph::io::Out;
|
||||
|
||||
use crate::filtering::fir::Fir;
|
||||
use crate::filtering::fir::FirFilter;
|
||||
|
||||
#[derive(BlockIO)]
|
||||
pub struct PulseShaper<T: 'static + std::ops::Mul<Output = T> + std::iter::Sum + Add<T, Output = T> + Sum + Clone + Zero>
|
||||
{
|
||||
#[input]
|
||||
input: In<T>,
|
||||
|
||||
#[output]
|
||||
output: Out<T>,
|
||||
|
||||
symbol_length: usize,
|
||||
remaining: usize,
|
||||
pulse_shaper: FirFilter<T, T, T>,
|
||||
}
|
||||
|
||||
impl<T: 'static + std::ops::Mul<Output = T> + std::iter::Sum + std::clone::Clone + Zero> PulseShaper<T>
|
||||
{
|
||||
pub fn new(input: In<T>, pulse_shape: Fir<T>, symbol_length: usize) -> (Self, In<T>)
|
||||
{
|
||||
let (output, pulse_shaped) = oxydsp_flowgraph::io::stream();
|
||||
(
|
||||
Self {
|
||||
input,
|
||||
output,
|
||||
symbol_length,
|
||||
remaining: 0,
|
||||
pulse_shaper: FirFilter::new(pulse_shape),
|
||||
},
|
||||
pulse_shaped,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static + std::ops::Mul<Output = T> + std::iter::Sum + std::clone::Clone + Zero> Block
|
||||
for PulseShaper<T>
|
||||
{
|
||||
fn work(&mut self) -> oxydsp_flowgraph::block::BlockResult
|
||||
{
|
||||
let reader = self.input.read();
|
||||
let writer = self.output.write();
|
||||
|
||||
for _ in 0..writer.len()
|
||||
{
|
||||
if self.remaining == 0
|
||||
{
|
||||
if let Some(input) = reader.pop()
|
||||
{
|
||||
let (data, tag) = input.into();
|
||||
let _ = writer.push((self.pulse_shaper.next(data), tag).into());
|
||||
self.remaining = self.symbol_length - 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
return oxydsp_flowgraph::block::BlockResult::Ok;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
let _ = writer.push(self.pulse_shaper.next(T::zero()).into());
|
||||
self.remaining -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
oxydsp_flowgraph::block::BlockResult::Ok
|
||||
}
|
||||
}
|
||||
@ -1,19 +1,19 @@
|
||||
use num::{Complex, Float};
|
||||
use oxydsp_flowgraph::{
|
||||
BlockIO,
|
||||
block::SyncBlock,
|
||||
io::{In, Out},
|
||||
sync_block,
|
||||
};
|
||||
use num::Complex;
|
||||
use num::Float;
|
||||
use oxydsp_flowgraph::BlockIO;
|
||||
use oxydsp_flowgraph::block::Block;
|
||||
use oxydsp_flowgraph::block::SyncBlock;
|
||||
use oxydsp_flowgraph::io::In;
|
||||
use oxydsp_flowgraph::io::Out;
|
||||
use oxydsp_flowgraph::io::PopIterable;
|
||||
use oxydsp_flowgraph::sync_block;
|
||||
use rustfft::FftNum;
|
||||
|
||||
use crate::{
|
||||
filtering::fir::{Fir, FirFilter},
|
||||
synthesis::oscillator::Nco,
|
||||
};
|
||||
use crate::filtering::fir::Fir;
|
||||
use crate::filtering::fir::FirFilter;
|
||||
use crate::synthesis::oscillator::Nco;
|
||||
|
||||
#[derive(BlockIO)]
|
||||
#[sync_block]
|
||||
pub struct ZeroIf<T: std::clone::Clone + num::Num + Float + From<f32> + 'static>
|
||||
{
|
||||
#[input]
|
||||
@ -28,7 +28,13 @@ pub struct ZeroIf<T: std::clone::Clone + num::Num + Float + From<f32> + 'static>
|
||||
|
||||
impl<T> ZeroIf<T>
|
||||
where
|
||||
T: std::clone::Clone + num::Num + FftNum + From<f32> + 'static + num::Float,
|
||||
T: std::clone::Clone
|
||||
+ num::Num
|
||||
+ FftNum
|
||||
+ From<f32>
|
||||
+ 'static
|
||||
+ num::Float
|
||||
+ num::traits::FloatConst,
|
||||
{
|
||||
pub fn new(input: In<T>, lo: Nco<T>) -> (Self, In<Complex<T>>)
|
||||
{
|
||||
@ -38,7 +44,7 @@ where
|
||||
input,
|
||||
output,
|
||||
local_oscillator: lo,
|
||||
filter: FirFilter::new(Fir::lowpass(lo.frequency(), 100)),
|
||||
filter: FirFilter::new(Fir::lowpass(lo.frequency(), 100).normalized_len()),
|
||||
},
|
||||
port,
|
||||
)
|
||||
@ -50,16 +56,20 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<'view, T> SyncBlock<'view> for ZeroIf<T>
|
||||
impl<T> Block for ZeroIf<T>
|
||||
where
|
||||
T: std::clone::Clone + num::Num + Float + From<f32> + 'static + num::Float,
|
||||
{
|
||||
fn sync_work(state: Self::StateView, input: Self::Input) -> Option<Self::Output>
|
||||
fn work(&mut self) -> oxydsp_flowgraph::block::BlockResult
|
||||
{
|
||||
// Mix
|
||||
let lo_sample = state.local_oscillator.next().unwrap();
|
||||
let iq = Complex::new(input * lo_sample.re, input * lo_sample.im);
|
||||
self.output.push_iter(self.input.pop_iter().map(|input| {
|
||||
let (data, tag) = input.into();
|
||||
// Mix
|
||||
let lo_sample = self.local_oscillator.next().unwrap();
|
||||
let iq = Complex::new(data * lo_sample.re, data * lo_sample.im);
|
||||
|
||||
Some(state.filter.next(iq))
|
||||
(self.filter.next(iq), tag).into()
|
||||
}));
|
||||
oxydsp_flowgraph::block::BlockResult::Ok
|
||||
}
|
||||
}
|
||||
|
||||
@ -156,7 +156,6 @@ where
|
||||
}
|
||||
|
||||
#[derive(BlockIO)]
|
||||
#[sync_block]
|
||||
pub struct Scan<I: 'static, O: 'static, S, F>
|
||||
where
|
||||
F: Fn(&mut S, I) -> O,
|
||||
@ -191,16 +190,16 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<'view, I, O, S, F> SyncBlock<'view> for Scan<I, O, S, F>
|
||||
impl<I, O, S, F> Block for Scan<I, O, S, F>
|
||||
where
|
||||
I: 'static,
|
||||
O: 'static,
|
||||
S: 'view,
|
||||
F: Fn(&mut S, I) -> O + 'view,
|
||||
F: Fn(&mut S, I) -> O,
|
||||
{
|
||||
fn sync_work(state: Self::StateView, input: Self::Input) -> Option<Self::Output>
|
||||
{
|
||||
Some((*state.map)(state.state, input))
|
||||
fn work(&mut self) -> BlockResult {
|
||||
self.output.push_iter(self.input.pop_iter()
|
||||
.map(|x| ((self.map)(&mut self.state, x.0), x.1).into()));
|
||||
BlockResult::Ok
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
use std::{collections::VecDeque, iter::Sum};
|
||||
use std::collections::VecDeque;
|
||||
use std::iter::Sum;
|
||||
|
||||
use num::{Float, FromPrimitive, One, Zero, complex::ComplexFloat};
|
||||
use oxydsp_flowgraph::{
|
||||
BlockIO,
|
||||
block::{Block, BlockResult},
|
||||
io::{In, Out, PopIterable},
|
||||
};
|
||||
|
||||
use crate::filtering::fir::{Fir, FirFilter};
|
||||
use num::Float;
|
||||
use num::FromPrimitive;
|
||||
use num::One;
|
||||
use num::Zero;
|
||||
use num::complex::ComplexFloat;
|
||||
use oxydsp_flowgraph::BlockIO;
|
||||
use oxydsp_flowgraph::block::Block;
|
||||
use oxydsp_flowgraph::block::BlockResult;
|
||||
use oxydsp_flowgraph::io::In;
|
||||
use oxydsp_flowgraph::io::Out;
|
||||
use oxydsp_flowgraph::io::PopIterable;
|
||||
|
||||
#[derive(BlockIO)]
|
||||
pub struct Squelch<T>
|
||||
|
||||
@ -1 +1,2 @@
|
||||
pub mod fir;
|
||||
pub mod history_buf;
|
||||
|
||||
@ -1,29 +1,52 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::f64::consts::PI;
|
||||
use std::iter::Sum;
|
||||
use std::ops::Div;
|
||||
use std::ops::Mul;
|
||||
use std::process::Output;
|
||||
|
||||
use num::Complex;
|
||||
use num::Float;
|
||||
use num::FromPrimitive;
|
||||
use num::One;
|
||||
use num::Zero;
|
||||
use num::complex::ComplexFloat;
|
||||
use num::zero;
|
||||
use rustfft::FftNum;
|
||||
use rustfft::FftPlanner;
|
||||
use std::array;
|
||||
use std::collections::VecDeque;
|
||||
use std::f64::consts::PI;
|
||||
use std::iter::Sum;
|
||||
use std::ops::Add;
|
||||
use std::ops::Div;
|
||||
use std::ops::Mul;
|
||||
|
||||
use crate::filtering::history_buf::HistoryBuf;
|
||||
use crate::map;
|
||||
use crate::units::DigitalFrequency;
|
||||
|
||||
/// Finite impulse response
|
||||
|
||||
/// Represents a finite impulse response as a vector
|
||||
/// of values in time.
|
||||
///
|
||||
/// Convention
|
||||
/// indices : 0 ----------------- fir.0.len - 1
|
||||
/// time : ---------------->
|
||||
///
|
||||
/// For a reverb ir for example the clap would be at index 0
|
||||
/// and the reverb tail towards the end of the vector.
|
||||
pub struct Fir<T>(pub Vec<T>);
|
||||
|
||||
impl<T> Fir<Complex<T>>
|
||||
where
|
||||
T: FftNum + Float + Clone,
|
||||
{
|
||||
/// Synthesizes an impulse response from a transfer function using an inverse fourrier
|
||||
/// transform.
|
||||
///
|
||||
/// The input units are thus :
|
||||
/// Transfer function :
|
||||
/// start center end
|
||||
/// [ ]
|
||||
/// frequency :
|
||||
/// 0 pi 2*pi
|
||||
/// = pi, -pi 0
|
||||
/// nyquist's frequency
|
||||
/// (As the frequencies are periodic)
|
||||
///
|
||||
pub fn from_transfer_function(tf: impl AsRef<[Complex<T>]>) -> Fir<Complex<T>>
|
||||
{
|
||||
let mut planner = FftPlanner::new();
|
||||
@ -43,6 +66,12 @@ where
|
||||
Fir(shifted_fir)
|
||||
}
|
||||
|
||||
/// Creates a low pass filter with the following ideal transfer function using the ifft method:
|
||||
///
|
||||
/// ________________ ________________
|
||||
/// |____________________|
|
||||
/// 0 cuttoff -cuttoff 0
|
||||
///
|
||||
pub fn lowpass(cutoff: DigitalFrequency, length: usize) -> Fir<Complex<T>>
|
||||
{
|
||||
let mut tf = vec![Complex::<T>::zero(); length];
|
||||
@ -63,6 +92,8 @@ where
|
||||
T: ComplexFloat + Div<T::Real, Output = T> + Copy + Sum,
|
||||
T::Real: Float,
|
||||
{
|
||||
/// Returns the same impulse response
|
||||
/// normalized by the length of the sum of the vectors.
|
||||
pub fn normalized(mut self) -> Self
|
||||
{
|
||||
let sum: T = self.0.iter().copied().sum();
|
||||
@ -73,43 +104,173 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Fir<T>
|
||||
where
|
||||
T: ComplexFloat + Div<T::Real, Output = T>,
|
||||
T::Real: Float + FromPrimitive,
|
||||
{
|
||||
/// Returns the same impulse response
|
||||
/// normalized by the length of the impulse response.
|
||||
pub fn normalized_len(mut self) -> Self
|
||||
{
|
||||
let len = T::Real::from_usize(self.0.len()).unwrap();
|
||||
self.0.iter_mut().for_each(|x| *x = *x / len);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Fir<T>
|
||||
where
|
||||
T: ComplexFloat + Div<T::Real, Output = T> + Copy + Add<T, Output = T>,
|
||||
T::Real: Float,
|
||||
{
|
||||
/// Returns the same impulse response
|
||||
/// normalized by the energy or the sum of the squares of the magnitues
|
||||
/// of the impulse response
|
||||
pub fn normalized_sqr(mut self) -> Self
|
||||
{
|
||||
let sum = self
|
||||
.0
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|x| x.abs() * x.abs())
|
||||
.reduce(|x, y| x + y)
|
||||
.unwrap();
|
||||
|
||||
self.0.iter_mut().for_each(|x| *x = *x / sum);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Fir<T>
|
||||
where
|
||||
T: ComplexFloat + FromPrimitive,
|
||||
{
|
||||
/// Creates a square unit impulse response :
|
||||
/// a vector of length `length` filled with ones
|
||||
pub fn square(length: usize) -> Self
|
||||
{
|
||||
Self((0..length).map(|_| T::one()).collect())
|
||||
}
|
||||
|
||||
/// Creates a simple proportional integral (PI) loop impulse response :
|
||||
///
|
||||
/// FIR:
|
||||
/// ```text
|
||||
/// _ ................................... Kp (`proportional_gain`)
|
||||
/// |
|
||||
/// |
|
||||
/// |
|
||||
/// |____________________________ ...... Ki (`integral_gain`)
|
||||
/// |
|
||||
///
|
||||
/// 0 ------------------------- `length`
|
||||
/// ```
|
||||
///
|
||||
pub fn proportional_integral(length: usize, proportional_gain: T, integral_gain: T) -> Self
|
||||
{
|
||||
Self(
|
||||
(0..length)
|
||||
.map(|i| {
|
||||
if i == 0
|
||||
{
|
||||
proportional_gain + integral_gain
|
||||
}
|
||||
else
|
||||
{
|
||||
integral_gain
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a root raised cosine (RRC) FIR of length `length`
|
||||
/// with the given roll off factor.
|
||||
///
|
||||
/// The corresponding RC (convolution of this filter with itself)
|
||||
/// has its zero crossing every `symbol_length` samples (except at 0).
|
||||
pub fn root_raised_cosine(length: usize, roll_off: f64, symbol_length: usize) -> Self
|
||||
{
|
||||
Self(
|
||||
(0..length)
|
||||
.map(|i| {
|
||||
let t = map(
|
||||
i as f64,
|
||||
0.,
|
||||
length as f64,
|
||||
-(length as f64) * 0.5,
|
||||
length as f64 * 0.5,
|
||||
) / symbol_length as f64;
|
||||
T::from_f64(root_raised_cosine(t, roll_off, 1.)).unwrap()
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple convolutional finite impulse response filter
|
||||
pub struct FirFilter<F, T, O>
|
||||
where
|
||||
F: Mul<T, Output = O>,
|
||||
O: Sum,
|
||||
O: Add<O, Output = O> + Sum + Clone + Zero,
|
||||
{
|
||||
fir: Vec<F>,
|
||||
taps: VecDeque<T>,
|
||||
//taps: VecDeque<T>,
|
||||
taps: HistoryBuf<T>
|
||||
}
|
||||
|
||||
impl<F, T, O> FirFilter<F, T, O>
|
||||
where
|
||||
T: Clone,
|
||||
T: Clone + Zero,
|
||||
F: Mul<T, Output = O> + Clone,
|
||||
O: Sum,
|
||||
O: Add<O, Output = O> + Sum + Clone + Zero,
|
||||
{
|
||||
/// Creates a filter with the given impulse response
|
||||
pub fn new(impulse_response: Fir<F>) -> Self
|
||||
{
|
||||
let len = impulse_response.0.len();
|
||||
Self {
|
||||
fir: impulse_response.0,
|
||||
taps: VecDeque::with_capacity(len),
|
||||
taps: HistoryBuf::new(T::zero(), len),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the next output given an input sample.
|
||||
///
|
||||
/// At the beginning, the delay line starts with zeroes.
|
||||
pub fn next(&mut self, input: T) -> O
|
||||
{
|
||||
if self.taps.len() == self.fir.len()
|
||||
{
|
||||
self.taps.pop_front();
|
||||
}
|
||||
self.taps.push_back(input);
|
||||
self.taps.push(input);
|
||||
let taps = self.taps.as_slice();
|
||||
Self::dot_prod(&self.fir, taps)
|
||||
}
|
||||
|
||||
self.fir
|
||||
.iter()
|
||||
.zip(self.taps.iter())
|
||||
.map(|(a, b)| a.clone() * b.clone())
|
||||
.sum()
|
||||
pub fn dot_prod(a: &[F], b: &[T]) -> O
|
||||
{
|
||||
assert_eq!(a.len(), b.len());
|
||||
|
||||
let mut sum: [_; 4] = [O::zero(), O::zero(), O::zero(), O::zero()];
|
||||
|
||||
let (a_chunks, a_remainder) = a.as_chunks::<4>();
|
||||
let (b_chunks, b_remainder) = b.as_chunks::<4>();
|
||||
|
||||
for (x, y) in a_chunks.iter().zip(b_chunks.iter())
|
||||
|
||||
{
|
||||
sum[0] = sum[0].clone() + x[0].clone() * y[0].clone();
|
||||
sum[1] = sum[1].clone() + x[1].clone() * y[1].clone();
|
||||
sum[2] = sum[2].clone() + x[2].clone() * y[2].clone();
|
||||
sum[3] = sum[3].clone() + x[3].clone() * y[3].clone();
|
||||
}
|
||||
|
||||
let mut sum = sum[0].clone() + sum[1].clone() + sum[2].clone() + sum[3].clone();
|
||||
for (x, y) in a_remainder.iter().zip(b_remainder.iter())
|
||||
{
|
||||
sum = sum + x.clone() * y.clone();
|
||||
}
|
||||
|
||||
sum
|
||||
}
|
||||
}
|
||||
|
||||
@ -118,3 +279,31 @@ pub fn estimate_fir_length(transition_width: f64, sample_rate: f64) -> f64
|
||||
{
|
||||
3.8 * sample_rate / transition_width
|
||||
}
|
||||
|
||||
/// Root raised cosine function
|
||||
pub fn root_raised_cosine(t: f64, beta: f64, symbol_time: f64) -> f64
|
||||
{
|
||||
let eps = 1e-8;
|
||||
|
||||
if t.abs() < eps
|
||||
{
|
||||
// t = 0 special case
|
||||
return (1.0 / symbol_time.sqrt()) * (1.0 + beta * (4.0 / PI - 1.0));
|
||||
}
|
||||
|
||||
if beta > 0.0 && (t.abs() - symbol_time / (4.0 * beta)).abs() < eps
|
||||
{
|
||||
// t = ±T / (4β) special case
|
||||
let term1 = (1.0 + 2.0 / PI) * (PI / (4.0 * beta)).sin();
|
||||
let term2 = (1.0 - 2.0 / PI) * (PI / (4.0 * beta)).cos();
|
||||
return (beta / (symbol_time.sqrt() * 2.0_f64.sqrt())) * (term1 + term2);
|
||||
}
|
||||
|
||||
// General case
|
||||
let numerator = (PI * t * (1.0 - beta) / symbol_time).sin()
|
||||
+ 4.0 * beta * t / symbol_time * (PI * t * (1.0 + beta) / symbol_time).cos();
|
||||
|
||||
let denominator = PI * t * (1.0 - (4.0 * beta * t / symbol_time).powi(2)) / symbol_time;
|
||||
|
||||
(1.0 / symbol_time.sqrt()) * (numerator / denominator)
|
||||
}
|
||||
|
||||
48
oxydsp-dsp/src/filtering/history_buf.rs
Normal file
48
oxydsp-dsp/src/filtering/history_buf.rs
Normal file
@ -0,0 +1,48 @@
|
||||
/// A queue that always contains the same amount of elements.
|
||||
///
|
||||
/// It intuitively record the history of `length` values of a value
|
||||
/// This implementations allows to get a single contiguous slice view on the history
|
||||
pub struct HistoryBuf<T>
|
||||
{
|
||||
buffer: Box<[T]>,
|
||||
start: usize,
|
||||
length: usize,
|
||||
}
|
||||
|
||||
impl<T: Clone> HistoryBuf<T>
|
||||
{
|
||||
pub fn new(default: T, length: usize) -> Self
|
||||
{
|
||||
Self
|
||||
{
|
||||
buffer: vec![default; 2 * length].into_boxed_slice(),
|
||||
start: 0,
|
||||
length
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_fn(length: usize, mut f: impl FnMut(usize) -> T) -> Self
|
||||
{
|
||||
Self
|
||||
{
|
||||
buffer: (0..(2 * length)).map(|i| f(i)).collect(),
|
||||
start: 0,
|
||||
length
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, data: T)
|
||||
{
|
||||
self.buffer[self.start] = data.clone();
|
||||
self.buffer[self.start + self.length] = data.clone();
|
||||
|
||||
self.start += 1;
|
||||
self.start %= self.length;
|
||||
}
|
||||
|
||||
pub fn as_slice(&self) -> &[T]
|
||||
{
|
||||
&self.buffer[self.start..(self.start + self.length)]
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,8 @@ pub mod filtering;
|
||||
pub mod synthesis;
|
||||
pub mod units;
|
||||
|
||||
/// Maps a float from a range onto another
|
||||
/// linearly
|
||||
fn map<T: Float>(x: T, x_min: T, x_max: T, o_min: T, o_max: T) -> T
|
||||
{
|
||||
((x - x_min) / (x_max - x_min)) * (o_max - o_min) + o_min
|
||||
|
||||
@ -8,6 +8,21 @@ use crate::map;
|
||||
use crate::units::DigitalFrequency;
|
||||
use crate::units::Phase;
|
||||
|
||||
/// Numericaly controlled oscillator
|
||||
///
|
||||
/// ```
|
||||
/// let nco: Nco<f32> = DigitalFrequency::from_rad(2 * f32::PI).into();
|
||||
/// // Or
|
||||
/// let nco: Nco<f32> = Nco::from(DigitalFrequency::from_rad(2 * f32::PI));
|
||||
///
|
||||
/// // Rotates by 90deg per sample
|
||||
/// // The next function is from the iterator implementation
|
||||
/// assert_eq!(nco.next(), Complex::new(1., 0.));
|
||||
/// assert_eq!(nco.next(), Complex::new(0., 1.));
|
||||
/// assert_eq!(nco.next(), Complex::new(-1., 0.));
|
||||
/// assert_eq!(nco.next(), Complex::new(0., -1.));
|
||||
/// assert_eq!(nco.next(), Complex::new(1., 0.));
|
||||
/// ```
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Nco<T>
|
||||
{
|
||||
@ -21,6 +36,7 @@ pub struct Nco<T>
|
||||
|
||||
impl<T> Nco<T>
|
||||
{
|
||||
/// Creates a new Nco with a specific frequency starting at phase 0
|
||||
pub fn new(frequency: DigitalFrequency) -> Self
|
||||
{
|
||||
Self {
|
||||
@ -30,11 +46,13 @@ impl<T> Nco<T>
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the current frequency of the oscillator
|
||||
pub fn frequency(&self) -> DigitalFrequency
|
||||
{
|
||||
DigitalFrequency(self.d_phase)
|
||||
}
|
||||
|
||||
/// Creates a new Nco with a specific frequency and starting phase
|
||||
pub fn with_phase(frequency: DigitalFrequency, phase: Phase) -> Self
|
||||
{
|
||||
Self {
|
||||
@ -44,16 +62,19 @@ impl<T> Nco<T>
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the current phase.
|
||||
pub fn set_phase(&mut self, phase: Phase)
|
||||
{
|
||||
self.phase = phase.0.0;
|
||||
}
|
||||
|
||||
/// Sets the current phase
|
||||
pub fn set_frequency(&mut self, frequency: DigitalFrequency)
|
||||
{
|
||||
self.d_phase = frequency.0;
|
||||
}
|
||||
|
||||
/// Steps the oscillator by one sample
|
||||
pub fn step(&mut self)
|
||||
{
|
||||
let (new, _) = self.phase.overflowing_add(self.d_phase);
|
||||
@ -63,6 +84,8 @@ impl<T> Nco<T>
|
||||
|
||||
impl<T: Float + From<f32>> Nco<T>
|
||||
{
|
||||
/// Gets the current value of the oscillator as a
|
||||
/// complex number
|
||||
pub fn sample(&self) -> Complex<T>
|
||||
{
|
||||
let t = map(
|
||||
|
||||
@ -3,15 +3,21 @@ use std::ops::Neg;
|
||||
|
||||
use crate::map;
|
||||
|
||||
// Represents digital frequency
|
||||
/// Represents a digital, sampled frequency
|
||||
/// as radians per samples in [0; 2*pi[ range
|
||||
/// mapped to the whole [0; usize::MAX] range
|
||||
#[derive(Clone, Copy, PartialEq, PartialOrd)]
|
||||
pub struct DigitalFrequency(pub usize);
|
||||
|
||||
/// Represents an absolute phase offset
|
||||
/// as radians in [0; 2*pi[ range
|
||||
/// mapped to the whole [0; usize::MAX] range
|
||||
#[derive(Clone, Copy, PartialEq, PartialOrd)]
|
||||
pub struct Phase(pub DigitalFrequency);
|
||||
|
||||
impl DigitalFrequency
|
||||
{
|
||||
/// Creates a DigitalFrequency from rads per samples
|
||||
pub fn from_rad(radians_per_sample: f64) -> Self
|
||||
{
|
||||
// Frequnecy wraps arround : Going at 2 pi radians per second
|
||||
@ -22,16 +28,21 @@ impl DigitalFrequency
|
||||
DigitalFrequency(map(f, 0., 2. * PI, 0., usize::MAX as f64).floor() as usize)
|
||||
}
|
||||
|
||||
/// Creates a DigitalFrequency from a frequency given in hertz (s^(-1))
|
||||
/// in the context of a sample rate also given in hertz
|
||||
pub fn from_time_frequency(hertz: f64, sample_rate: f64) -> Self
|
||||
{
|
||||
Self::from_rad(map(hertz, 0., sample_rate, 0., 2. * PI))
|
||||
}
|
||||
|
||||
/// Gets the frequency as radians per sample
|
||||
pub fn as_rad(&self) -> f64
|
||||
{
|
||||
map(self.0 as f64, 0., usize::MAX as f64, 0., 2. * PI)
|
||||
}
|
||||
|
||||
/// Gets the frequency as hertz in the context of a sample rate
|
||||
/// also given in hert
|
||||
pub fn as_time_frequency(&self, sample_rate: f64) -> f64
|
||||
{
|
||||
map(self.0 as f64, 0., usize::MAX as f64, 0., sample_rate)
|
||||
@ -42,6 +53,7 @@ impl Neg for DigitalFrequency
|
||||
{
|
||||
type Output = Self;
|
||||
|
||||
/// Returns the "negative frequency"
|
||||
fn neg(self) -> Self::Output
|
||||
{
|
||||
DigitalFrequency(usize::MAX - self.0)
|
||||
|
||||
Reference in New Issue
Block a user