Adds pulse shaping, work stealing

This commit is contained in:
2026-04-09 16:33:42 +02:00
parent 4d548a7973
commit 54f26a0dd2
32 changed files with 1305 additions and 340 deletions

View File

@ -7,3 +7,4 @@ edition = "2024"
num = "0.4.3"
oxydsp-flowgraph = {path = "../oxydsp-flowgraph/"}
rustfft = "6.4.1"
wide = "1.2.0"

View File

@ -1 +1,2 @@
pub mod fir;
pub mod pulse_shaping;

View File

@ -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
}
}

View 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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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>

View File

@ -1 +1,2 @@
pub mod fir;
pub mod history_buf;

View File

@ -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)
}

View 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)]
}
}

View File

@ -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

View File

@ -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(

View File

@ -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)