Bunch'o things

This commit is contained in:
2026-03-08 22:46:33 +01:00
parent af9308c070
commit 7f466299ba
32 changed files with 8633 additions and 221 deletions

41
ntw_dsp/Cargo.lock generated
View File

@ -64,6 +64,8 @@ dependencies = [
"ntw_flowgraph",
"ntw_flowgraph_macros",
"num",
"ringbuf",
"rustfft",
]
[[package]]
@ -184,6 +186,15 @@ dependencies = [
"portable-atomic",
]
[[package]]
name = "primal-check"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08"
dependencies = [
"num-integer",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
@ -213,6 +224,20 @@ dependencies = [
"portable-atomic-util",
]
[[package]]
name = "rustfft"
version = "6.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21db5f9893e91f41798c88680037dba611ca6674703c1a18601b01a72c8adb89"
dependencies = [
"num-complex",
"num-integer",
"num-traits",
"primal-check",
"strength_reduce",
"transpose",
]
[[package]]
name = "serde"
version = "1.0.228"
@ -242,6 +267,12 @@ dependencies = [
"syn",
]
[[package]]
name = "strength_reduce"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82"
[[package]]
name = "syn"
version = "2.0.117"
@ -253,6 +284,16 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "transpose"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e"
dependencies = [
"num-integer",
"strength_reduce",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"

View File

@ -7,3 +7,5 @@ edition = "2024"
ntw_flowgraph = {path = "../ntw_flowgraph/"}
ntw_flowgraph_macros = {path = "../ntw_flowgraph_macros/"}
num = "0.4.3"
ringbuf = "0.4.8"
rustfft = "6.4.1"

View File

@ -1,5 +1,13 @@
pub mod complex;
pub mod early_late_gate;
pub mod fft;
pub mod fir;
pub mod iir;
pub mod iq;
pub mod iter;
pub mod map;
pub mod math;
pub mod nco;
pub mod oscillator;
pub mod tee;
pub mod utilities;

View File

@ -1,5 +1,5 @@
use ntw_flowgraph::Block;
use ntw_flowgraph::inout::{In, Out};
use ntw_flowgraph::inout::In;
use ntw_flowgraph::inout::Out;
use ntw_flowgraph_macros::Block;
use num::Complex;

View File

@ -0,0 +1,211 @@
use std::collections::VecDeque;
use std::sync::mpsc::Sender;
use ntw_flowgraph::BlockResult;
use ntw_flowgraph::BlockWork;
use ntw_flowgraph::inout::In;
use ntw_flowgraph::inout::Out;
use ntw_flowgraph::inout::Stream;
use ntw_flowgraph_macros::Block;
use num::Float;
use crate::filtering::fir::Fir;
use crate::filtering::impulse_response::ImpulseResponse;
#[derive(Clone, Copy)]
pub enum EarlyLateGateTag<T>
{
Symbol(T),
InterSymbol(T),
}
#[derive(Block)]
pub struct EarlyLateGate<T: Float>
{
#[input]
input: In<T>,
#[output]
output: Out<EarlyLateGateTag<T>>,
symbol_length: usize,
// Window looking at symbol_length samples at a time
window: VecDeque<T>,
// The current location of the window, in relation to the last sample
window_location: usize,
window_center: usize,
// The next window location, in relation to the last sample such that the window is centered on
// a symbol center (hopefully)
next_sample: usize,
loop_filter: Fir<T, T, T>,
}
impl<T: Float> EarlyLateGate<T>
{
pub fn new(
input: In<T>,
loop_filter: ImpulseResponse<T>,
symbol_length: usize,
) -> (Self, In<EarlyLateGateTag<T>>)
{
let (output, samples) = Stream::make(1024);
(
Self {
input,
output,
window: VecDeque::with_capacity(symbol_length),
symbol_length,
window_location: 0,
window_center: symbol_length / 2,
next_sample: symbol_length, // We assume that the first symbol is 1.5 windows into
// the stream
loop_filter: Fir::new(loop_filter),
},
samples,
)
}
}
impl<T: Float> BlockWork for EarlyLateGate<T>
{
fn work(&mut self) -> ntw_flowgraph::BlockResult
{
// Handle begining, window is empty
if self.window.len() < self.symbol_length
{
self.input
.pop_iter()
.take(self.symbol_length - self.window.len())
.for_each(|x| self.window.push_back(x));
if self.input.available_len() == 0
{
return BlockResult::Ok;
}
}
let max = self.input.available_len().min(self.output.vacant_len());
for _ in 0..max
{
// Bring new sample in
self.window.pop_front();
self.window.push_back(self.input.try_pop().unwrap());
self.window_location += 1;
let sample = self.window[self.window_center];
if self.window_location >= self.next_sample
{
// Window is centered on a symbol
let _ = self.output.try_push(EarlyLateGateTag::Symbol(sample));
// Sample early and late samples
let early_index = self.window_center - (0.25 * self.symbol_length as f32) as usize;
let late_index = self.window_center + (0.25 * self.symbol_length as f32) as usize;
let early_sample = self.window[early_index];
let late_sample = self.window[late_index];
let error = (late_sample - early_sample) * sample;
let correction = self.loop_filter.next(error);
// Figure out next sample location
self.next_sample += (self.symbol_length as isize
+ correction.floor().to_isize().unwrap_or(0))
.max(0) as usize;
// Turn everything back relative to current sample
self.next_sample -= self.window_location;
self.window_location = 0;
}
else
{
// Window is not centered on a symbol
let _ = self.output.try_push(EarlyLateGateTag::InterSymbol(sample));
}
}
BlockResult::Ok
}
fn ready(&mut self) -> bool
{
self.input.available_len() > 0 && self.output.vacant_len() > 0
}
}
#[derive(Block)]
pub struct EyeExtractor<T>
{
#[input]
input: In<EarlyLateGateTag<T>>,
output: Sender<Vec<T>>,
window: VecDeque<EarlyLateGateTag<T>>,
symbol_length: usize,
}
impl<T> EyeExtractor<T>
{
pub fn new(input: In<EarlyLateGateTag<T>>, output: Sender<Vec<T>>, symbol_length: usize)
-> Self
{
Self {
input,
output,
window: VecDeque::with_capacity(symbol_length),
symbol_length,
}
}
}
impl<T: Clone> BlockWork for EyeExtractor<T>
{
fn work(&mut self) -> BlockResult
{
// Handle begining, window is empty
if self.window.len() < self.symbol_length * 2
{
self.input
.pop_iter()
.take(2 * self.symbol_length - self.window.len())
.for_each(|x| self.window.push_back(x));
if self.input.available_len() == 0
{
return BlockResult::Ok;
}
}
for _ in 0..self.input.available_len()
{
let x = self.input.try_pop().unwrap();
self.window.pop_front();
self.window.push_back(x);
if let EarlyLateGateTag::Symbol(_) = self.window[self.symbol_length]
{
let vec = self
.window
.iter()
.cloned()
.map(|x| match x
{
EarlyLateGateTag::Symbol(x) | EarlyLateGateTag::InterSymbol(x) => x,
})
.collect::<Vec<_>>();
let _ = self.output.send(vec);
}
}
BlockResult::Ok
}
fn ready(&mut self) -> bool
{
self.input.available_len() > 0
}
}

75
ntw_dsp/src/blocks/fft.rs Normal file
View File

@ -0,0 +1,75 @@
use ntw_flowgraph::BlockResult;
use ntw_flowgraph::BlockWork;
use ntw_flowgraph::inout::In;
use ntw_flowgraph::inout::Out;
use ntw_flowgraph::inout::Stream;
use ntw_flowgraph_macros::Block;
use num::Complex;
use rustfft::FftDirection;
use rustfft::FftNum;
use rustfft::FftPlanner;
use std::sync::Arc;
#[derive(Block)]
pub struct Fft<T>
where
T: FftNum,
{
#[input]
input: In<Complex<T>>,
#[output]
output: Out<Vec<Complex<T>>>,
length: usize,
buffer: Vec<Complex<T>>,
fft: Arc<dyn rustfft::Fft<T> + 'static>,
}
impl<T: FftNum> Fft<T>
{
pub fn new(
input: In<Complex<T>>,
length: usize,
direction: FftDirection,
) -> (Self, In<Vec<Complex<T>>>)
{
let (output, ffts) = Stream::make(128);
let mut planner = FftPlanner::new();
let fft = planner.plan_fft(length, direction);
(
Self {
input,
output,
length,
buffer: Vec::with_capacity(length),
fft,
},
ffts,
)
}
}
impl<T: FftNum> BlockWork for Fft<T>
{
fn work(&mut self) -> ntw_flowgraph::BlockResult
{
if self.buffer.len() >= self.length
{
// Compute fft
self.fft.process(self.buffer.as_mut_slice());
let _ = self.output.try_push(self.buffer.clone());
self.buffer.clear();
}
self.buffer
.extend(self.input.pop_iter().take(self.length - self.buffer.len()));
BlockResult::Ok
}
fn ready(&mut self) -> bool
{
self.input.available_len() > 0 && self.output.vacant_len() > 0
}
}

184
ntw_dsp/src/blocks/fir.rs Normal file
View File

@ -0,0 +1,184 @@
use std::ops::Add;
use std::ops::Mul;
use crate::filtering::impulse_response;
use crate::filtering::impulse_response::ImpulseResponse;
use ntw_flowgraph::BlockResult;
use ntw_flowgraph::BlockWork;
use ntw_flowgraph::inout::In;
use ntw_flowgraph::inout::Out;
use ntw_flowgraph::inout::Stream;
use ntw_flowgraph_macros::Block;
use num::Complex;
use rustfft::FftDirection;
use rustfft::FftNum;
use rustfft::FftPlanner;
use std::sync::Arc;
#[derive(Block)]
pub struct Fir<C, V, U>
where
C: Clone,
V: Clone,
V: Mul<C, Output = U>,
U: Add<U, Output = U>,
{
filter: crate::filtering::fir::Fir<C, V, U>,
#[input]
input: In<V>,
#[output]
output: Out<U>,
}
impl<C, V, U> Fir<C, V, U>
where
C: Clone,
V: Clone,
V: Mul<C, Output = U>,
U: Add<U, Output = U>,
{
pub fn new(input: In<V>, impulse_response: ImpulseResponse<C>) -> (Fir<C, V, U>, In<U>)
{
let fir = crate::filtering::fir::Fir::new(impulse_response);
let (output, stream) = Stream::make(1024);
(
Self {
filter: fir,
input,
output,
},
stream,
)
}
}
impl<C, V, U> BlockWork for Fir<C, V, U>
where
C: Clone,
V: Clone,
V: Mul<C, Output = U>,
U: Add<U, Output = U>,
{
fn work(&mut self) -> BlockResult
{
self.output
.push_iter(self.input.pop_iter().map(|sample| self.filter.next(sample)));
BlockResult::Ok
}
fn ready(&mut self) -> bool
{
self.input.available_len() > 0 && self.output.vacant_len() > 0
}
}
#[derive(Block)]
pub struct FirFft<T>
where
T: FftNum,
{
#[input]
input: In<Complex<T>>,
#[output]
output: Out<Complex<T>>,
length: usize,
buffer: Vec<Complex<T>>,
transformed_ir: Vec<Complex<T>>,
fft: Arc<dyn rustfft::Fft<T> + 'static>,
ifft: Arc<dyn rustfft::Fft<T> + 'static>,
accumulating: bool,
}
impl<T: FftNum> FirFft<T>
{
pub fn new(
input: In<Complex<T>>,
impulse_response: ImpulseResponse<Complex<T>>,
) -> (Self, In<Complex<T>>)
{
let (output, filtered) = Stream::make(1024);
let length = impulse_response.len();
let mut planner = FftPlanner::new();
let fft = planner.plan_fft(impulse_response.len(), FftDirection::Forward);
let ifft = planner.plan_fft(impulse_response.len(), FftDirection::Inverse);
let mut transformed_ir = impulse_response.0;
fft.process(&mut transformed_ir);
(
Self {
input,
output,
length,
buffer: Vec::with_capacity(length),
transformed_ir,
fft,
ifft,
accumulating: true,
},
filtered,
)
}
}
impl<T: FftNum> BlockWork for FirFft<T>
{
fn work(&mut self) -> ntw_flowgraph::BlockResult
{
if self.accumulating
{
if self.buffer.len() >= self.length && self.accumulating
{
// Compute fft
self.fft.process(self.buffer.as_mut_slice());
// Convolve
self.buffer
.iter_mut()
.zip(self.transformed_ir.iter())
.for_each(|(x, y)| *x = *x * *y);
self.ifft.process(self.buffer.as_mut_slice());
self.accumulating = false;
}
else
{
self.buffer
.extend(self.input.pop_iter().take(self.length - self.buffer.len()));
}
}
if !self.accumulating
{
let len = self.output.vacant_len().min(self.buffer.len());
for _ in 0..len
{
let _ = self.output.try_push(self.buffer.pop().unwrap());
}
if self.buffer.is_empty()
{
self.accumulating = true;
}
}
BlockResult::Ok
}
fn ready(&mut self) -> bool
{
if self.accumulating
{
self.input.available_len() > 0
}
else
{
self.output.vacant_len() > 0
}
}
}

58
ntw_dsp/src/blocks/iir.rs Normal file
View File

@ -0,0 +1,58 @@
use ntw_flowgraph::BlockResult;
use ntw_flowgraph::BlockWork;
use ntw_flowgraph::inout::In;
use ntw_flowgraph::inout::Out;
use ntw_flowgraph::inout::Stream;
use ntw_flowgraph_macros::Block;
use num::complex::ComplexFloat;
#[derive(Block)]
pub struct SimpleDcBlock<T>
{
#[input]
input: In<T>,
#[output]
output: Out<T>,
prev_input: T,
prev_output: T,
r: T,
}
impl<T: ComplexFloat> SimpleDcBlock<T>
{
pub fn new(input: In<T>, r: T) -> (Self, In<T>)
{
let (output, dc_removed) = Stream::make(1024);
(
Self {
input,
output,
prev_input: T::zero(),
prev_output: T::zero(),
r,
},
dc_removed,
)
}
}
impl<T: ComplexFloat + Clone> BlockWork for SimpleDcBlock<T>
{
fn work(&mut self) -> ntw_flowgraph::BlockResult
{
self.output.push_iter(self.input.pop_iter().map(|x| {
let out = x - self.prev_input + self.r * self.prev_output;
self.prev_input = x;
self.prev_output = out;
out
}));
BlockResult::Ok
}
fn ready(&mut self) -> bool
{
self.input.available_len() > 0 && self.output.vacant_len() > 0
}
}

98
ntw_dsp/src/blocks/iq.rs Normal file
View File

@ -0,0 +1,98 @@
use std::f64::consts::PI;
use ntw_flowgraph::BlockResult;
use ntw_flowgraph::BlockWork;
use ntw_flowgraph::inout::In;
use ntw_flowgraph::inout::Out;
use ntw_flowgraph::inout::Stream;
use ntw_flowgraph_macros::Block;
use num::Complex;
use num::Float;
use num::One;
use num::Zero;
use rustfft::FftNum;
use crate::Frequency;
use crate::filtering::fir::Fir;
use crate::filtering::impulse_response::ImpulseResponse;
use crate::generation::Nco;
use crate::generation::NcoType;
#[derive(Block)]
pub struct IqSampler<T: Float>
{
#[input]
input: In<T>,
#[output]
output: Out<Complex<T>>,
bandpass_i: Fir<T, T, T>,
bandpass_q: Fir<T, T, T>,
local_oscillator: Nco<T>,
}
impl<T: Float + FftNum> IqSampler<T>
{
pub fn new(input: In<T>, local_oscillator_frequency: Frequency) -> (Self, In<Complex<T>>)
{
let (output, iq) = Stream::make(1024);
let fir_len = 64;
let mut transfer_function = vec![Complex::<T>::zero(); fir_len];
let lobes_length = ((local_oscillator_frequency.as_rad() / PI)
* (transfer_function.len() as f64 / 2.))
.floor() as usize;
for i in 0..lobes_length
{
transfer_function[i] = Complex::<T>::one();
transfer_function[fir_len - i - 1] = Complex::<T>::one();
}
let ir = ImpulseResponse(
ImpulseResponse::from_transfer_function(&transfer_function)
.0
.iter()
.map(|x| x.re)
.collect(),
)
.normalized();
(
Self {
input,
output,
bandpass_i: Fir::new(ir.clone()),
bandpass_q: Fir::new(ir),
local_oscillator: Nco::new(local_oscillator_frequency),
},
iq,
)
}
}
impl<T: Float + FftNum + NcoType> BlockWork for IqSampler<T>
{
fn work(&mut self) -> ntw_flowgraph::BlockResult
{
self.output.push_iter(
&mut self
.input
.pop_iter()
.zip(&mut self.local_oscillator)
.map(|(x, lo)| {
Complex::new(
self.bandpass_i.next(x * lo.re),
self.bandpass_q.next(x * lo.im),
)
//Complex::new(x * lo.re, x * lo.im)
}),
);
BlockResult::Ok
}
fn ready(&mut self) -> bool
{
self.input.available_len() > 0 && self.output.vacant_len() > 0
}
}

View File

@ -1,9 +1,10 @@
use std::iter::Peekable;
use ntw_flowgraph::{
BlockWork,
inout::{In, Out, Stream},
};
use ntw_flowgraph::BlockResult;
use ntw_flowgraph::BlockWork;
use ntw_flowgraph::inout::In;
use ntw_flowgraph::inout::Out;
use ntw_flowgraph::inout::Stream;
use ntw_flowgraph_macros::Block;
#[derive(Block)]
@ -38,9 +39,10 @@ impl<T, I> BlockWork for IterSource<T, I>
where
I: Iterator<Item = T>,
{
fn work(&mut self)
fn work(&mut self) -> BlockResult
{
self.output.push_iter(&mut self.iter);
BlockResult::Ok
}
fn ready(&mut self) -> bool
@ -48,3 +50,64 @@ where
self.iter.peek().is_some() && self.output.vacant_len() > 0
}
}
#[derive(Block)]
pub struct FiniteIterSource<T, I>
where
I: Iterator<Item = T>,
{
iter: Peekable<I>,
remaining: usize,
#[output]
output: Out<T>,
}
impl<T, I> FiniteIterSource<T, I>
where
I: Iterator<Item = T>,
{
pub fn new(iter: I, len: usize) -> (Self, In<T>)
{
let (output, input) = Stream::make(1024);
(
Self {
iter: iter.peekable(),
remaining: len,
output,
},
input,
)
}
}
impl<T, I> FiniteIterSource<T, I>
where
I: Iterator<Item = T> + ExactSizeIterator,
{
pub fn from_len_iter(iter: I) -> (Self, In<T>)
{
let len = iter.len();
Self::new(iter, len)
}
}
impl<T, I> BlockWork for FiniteIterSource<T, I>
where
I: Iterator<Item = T>,
{
fn work(&mut self) -> BlockResult
{
self.remaining -= self.output.push_iter((&mut self.iter).take(self.remaining));
match self.remaining
{
0 => BlockResult::Finished,
_ => BlockResult::Ok,
}
}
fn ready(&mut self) -> bool
{
self.remaining > 0 && self.output.vacant_len() > 0
}
}

View File

@ -1,7 +1,8 @@
use ntw_flowgraph::{
Block, BlockWork,
inout::{In, Out, Stream},
};
use ntw_flowgraph::BlockResult;
use ntw_flowgraph::BlockWork;
use ntw_flowgraph::inout::In;
use ntw_flowgraph::inout::Out;
use ntw_flowgraph::inout::Stream;
use ntw_flowgraph_macros::Block;
#[derive(Block)]
@ -33,12 +34,13 @@ impl<I, O, F> BlockWork for Map<I, O, F>
where
F: Fn(I) -> O,
{
fn work(&mut self)
fn work(&mut self) -> BlockResult
{
self.output.push_iter(self.input.pop_iter().map(&self.map));
BlockResult::Ok
}
fn ready(&self) -> bool
fn ready(&mut self) -> bool
{
self.input.available_len() > 0 && self.output.vacant_len() > 0
}

View File

@ -0,0 +1,60 @@
use std::ops::Mul;
use ntw_flowgraph::BlockResult;
use ntw_flowgraph::BlockWork;
use ntw_flowgraph::inout::In;
use ntw_flowgraph::inout::Out;
use ntw_flowgraph::inout::Stream;
use ntw_flowgraph_macros::Block;
#[derive(Block)]
pub struct Multiplier<A, B, C>
{
#[input]
input_a: In<A>,
#[input]
input_b: In<B>,
#[output]
product: Out<C>,
}
impl<A, B, C> Multiplier<A, B, C>
{
pub fn new(input_a: In<A>, input_b: In<B>) -> (Self, In<C>)
{
let (product, stream) = Stream::make(1024);
(
Multiplier {
input_a,
input_b,
product,
},
stream,
)
}
}
impl<A, B, C> BlockWork for Multiplier<A, B, C>
where
A: Mul<B, Output = C>,
{
fn work(&mut self) -> BlockResult
{
self.product.push_iter(
self.input_a
.pop_iter()
.zip(self.input_b.pop_iter())
.map(|(a, b)| a * b),
);
BlockResult::Ok
}
fn ready(&mut self) -> bool
{
self.input_a.available_len() > 0
&& self.input_b.available_len() > 0
&& self.product.vacant_len() > 0
}
}

View File

@ -1,6 +1,8 @@
use ntw_flowgraph::Block;
use ntw_flowgraph::inout::{In, Stream};
use ntw_flowgraph::{BlockWork, inout::Out};
use ntw_flowgraph::BlockResult;
use ntw_flowgraph::BlockWork;
use ntw_flowgraph::inout::In;
use ntw_flowgraph::inout::Out;
use ntw_flowgraph::inout::Stream;
use ntw_flowgraph_macros::Block;
use num::Complex;
@ -12,41 +14,65 @@ pub struct ComplexNco<T>
{
inner: crate::generation::Nco<T>,
#[input]
freq_in: In<Frequency>,
#[output]
out: Out<Complex<T>>,
}
impl BlockWork for ComplexNco<f32>
{
fn work(&mut self)
fn work(&mut self) -> BlockResult
{
self.out.push_iter(&mut self.inner);
let len = self.freq_in.available_len().min(self.out.vacant_len());
for _ in 0..len
{
self.inner.set_frequency(self.freq_in.try_pop().unwrap());
self.out.try_push(self.inner.sample()).unwrap();
self.inner.next();
}
BlockResult::Ok
}
fn ready(&self) -> bool
fn ready(&mut self) -> bool
{
self.out.vacant_len() > 0
self.freq_in.available_len() > 0 && self.out.vacant_len() > 0
}
}
impl BlockWork for ComplexNco<f64>
{
fn work(&mut self)
fn work(&mut self) -> BlockResult
{
self.out.push_iter(&mut self.inner);
let len = self.freq_in.available_len().min(self.out.vacant_len());
for _ in 0..len
{
self.inner.set_frequency(self.freq_in.try_pop().unwrap());
self.out.try_push(self.inner.sample()).unwrap();
self.inner.next();
}
BlockResult::Ok
}
fn ready(&self) -> bool
fn ready(&mut self) -> bool
{
self.out.vacant_len() > 0
self.freq_in.available_len() > 0 && self.out.vacant_len() > 0
}
}
impl<T> ComplexNco<T>
{
pub fn new(nco: Nco<T>) -> (Self, In<Complex<T>>)
pub fn new(input_freq: In<Frequency>) -> (Self, In<Complex<T>>)
{
let (a, b) = Stream::make(1024);
(Self { inner: nco, out: a }, b)
(
Self {
inner: Nco::new(Frequency::from_rad(0.)),
freq_in: input_freq,
out: a,
},
b,
)
}
}

View File

@ -0,0 +1,55 @@
use ntw_flowgraph::BlockResult;
use ntw_flowgraph::BlockWork;
use ntw_flowgraph::inout::In;
use ntw_flowgraph::inout::Out;
use ntw_flowgraph::inout::Stream;
use ntw_flowgraph_macros::Block;
use num::Complex;
use crate::generation::Nco;
#[derive(Block)]
pub struct ComplexOscillator<T>
{
inner: crate::generation::Nco<T>,
#[output]
out: Out<Complex<T>>,
}
impl BlockWork for ComplexOscillator<f32>
{
fn work(&mut self) -> BlockResult
{
self.out.push_iter(&mut self.inner);
BlockResult::Ok
}
fn ready(&mut self) -> bool
{
self.out.vacant_len() > 0
}
}
impl BlockWork for ComplexOscillator<f64>
{
fn work(&mut self) -> BlockResult
{
self.out.push_iter(&mut self.inner);
BlockResult::Ok
}
fn ready(&mut self) -> bool
{
self.out.vacant_len() > 0
}
}
impl<T> ComplexOscillator<T>
{
pub fn new(nco: Nco<T>) -> (Self, In<Complex<T>>)
{
let (a, b) = Stream::make(1024);
(Self { inner: nco, out: a }, b)
}
}

View File

@ -1,17 +1,21 @@
use ntw_flowgraph::BlockResult;
use ntw_flowgraph::BlockWork;
use ntw_flowgraph::inout::In;
use ntw_flowgraph::inout::Out;
use ntw_flowgraph::inout::Stream;
use ntw_flowgraph_macros::Block;
use ntw_flowgraph::{Block, BlockWork, inout::{In, Out, Stream}};
#[derive(Block)]
pub struct Tee<T>
{
#[input]
input: In<T>
input: In<T>,
#[output]
output_1: Out<T>
output_1: Out<T>,
#[output]
output_2: Out<T>
output_2: Out<T>,
}
impl<T> Tee<T>
@ -21,37 +25,41 @@ impl<T> Tee<T>
let (output_1, in_1) = Stream::make(1024);
let (output_2, in_2) = Stream::make(1024);
(
Tee
{
Tee {
input,
output_1,
output_2,
},
in_1,
in_2
in_2,
)
}
}
impl<T: Clone> BlockWork for Tee<T>
{
fn work(&mut self)
fn work(&mut self) -> BlockResult
{
let len = self.output_1.vacant_len().min(self.output_2.vacant_len())
let len = self
.output_1
.vacant_len()
.min(self.output_2.vacant_len())
.min(self.input.available_len());
for _ in 0..len
{
let elem = self.input.try_pop().unwrap(); // Should be available because of len check
self.output_1.try_push(elem.clone());
self.output_2.try_push(elem);
let _ = self.output_1.try_push(elem.clone());
let _ = self.output_2.try_push(elem);
}
BlockResult::Ok
}
fn ready(&self) -> bool {
self.input.available_len() > 0 &&
self.output_1.vacant_len() > 0 &&
self.output_2.vacant_len() > 0
fn ready(&mut self) -> bool
{
self.input.available_len() > 0
&& self.output_1.vacant_len() > 0
&& self.output_2.vacant_len() > 0
}
}

View File

@ -0,0 +1,441 @@
use std::collections::VecDeque;
use std::ops::Div;
use std::sync::mpsc::Receiver;
use std::sync::mpsc::SendError;
use std::sync::mpsc::Sender;
use std::sync::mpsc::TryRecvError;
use ntw_flowgraph::BlockResult;
use ntw_flowgraph::BlockWork;
use ntw_flowgraph::inout::In;
use ntw_flowgraph::inout::Out;
use ntw_flowgraph::inout::Stream;
use ntw_flowgraph_macros::Block;
use num::Complex;
use num::Float;
use num::One;
use num::complex::ComplexFloat;
use crate::filtering::fir::Fir;
use crate::filtering::impulse_response::ImpulseResponse;
#[derive(Block)]
pub struct NullSink<T>
{
#[input]
input: In<T>,
}
impl<T> NullSink<T>
{
pub fn new(input: In<T>) -> Self
{
Self { input }
}
}
impl<T> BlockWork for NullSink<T>
{
fn work(&mut self) -> BlockResult
{
self.input.pop_iter().for_each(|_| {});
BlockResult::Ok
}
fn ready(&mut self) -> bool
{
self.input.available_len() > 0
}
}
#[derive(Block)]
pub struct Repeat<T>
{
repetition: usize,
current: usize,
holding: Option<T>,
#[input]
input: In<T>,
#[output]
output: Out<T>,
}
impl<T: Clone> Repeat<T>
{
pub fn new(input: In<T>, repetition: usize) -> (Self, In<T>)
{
let (output, stream) = Stream::make(1024);
(
Self {
repetition,
current: 0,
holding: None,
input,
output,
},
stream,
)
}
}
impl<T: Clone> BlockWork for Repeat<T>
{
fn work(&mut self) -> BlockResult
{
if self.current == 0
{
self.holding = Some(self.input.try_pop().unwrap());
self.current = self.repetition;
}
let pushed = self.output.push_iter(std::iter::repeat_n(
self.holding.clone().unwrap(),
self.current,
));
self.current -= pushed;
BlockResult::Ok
}
fn ready(&mut self) -> bool
{
self.output.vacant_len() > 0 && (self.current != 0 || self.input.available_len() > 0)
}
}
#[derive(Block)]
pub struct ChannelSink<T>
{
#[input]
input: In<T>,
output: Sender<T>,
}
impl<T> ChannelSink<T>
{
pub fn new(input: In<T>) -> (Self, Receiver<T>)
{
let (tx, rx) = std::sync::mpsc::channel();
(Self { input, output: tx }, rx)
}
}
impl<T> BlockWork for ChannelSink<T>
{
fn work(&mut self) -> BlockResult
{
if self
.input
.pop_iter()
.map(|x| match self.output.send(x)
{
Ok(_) => BlockResult::Ok,
Err(SendError(_)) => BlockResult::Finished,
})
.any(|r| r == BlockResult::Finished)
{
BlockResult::Finished
}
else
{
BlockResult::Ok
}
}
fn ready(&mut self) -> bool
{
self.input.available_len() > 0
}
}
#[derive(Block)]
pub struct ChannelSource<T>
{
#[output]
output: Out<T>,
input: Option<Receiver<T>>,
}
impl<T> ChannelSource<T>
{
pub fn new(input: Receiver<T>) -> (Self, In<T>)
{
let (tx, rx) = Stream::make(1024);
(
Self {
input: Some(input),
output: tx,
},
rx,
)
}
}
impl<T> BlockWork for ChannelSource<T>
{
fn work(&mut self) -> BlockResult
{
if let Some(input) = &self.input
{
let len = self.output.vacant_len();
for _ in 0..len
{
match input.try_recv()
{
Ok(x) =>
{
let _ = self.output.try_push(x);
}
Err(TryRecvError::Empty) => return BlockResult::Ok,
Err(TryRecvError::Disconnected) =>
{
println!("FINISHED");
self.input = None;
return BlockResult::Finished;
}
}
}
BlockResult::Ok
}
else
{
BlockResult::Finished
}
}
fn ready(&mut self) -> bool
{
self.input.is_some() && self.output.vacant_len() > 0
}
}
#[derive(Block)]
pub struct Chunks<T, const N: usize>
{
#[input]
input: In<T>,
#[output]
output: Out<[T; N]>,
}
impl<T, const N: usize> Chunks<T, N>
{
pub fn new(input: In<T>) -> (Self, In<[T; N]>)
{
let (output, chunks) = Stream::make(1024);
(Self { input, output }, chunks)
}
}
impl<T: Copy, const N: usize> BlockWork for Chunks<T, N>
{
fn work(&mut self) -> BlockResult
{
let len = (self.input.available_len() / N).min(self.output.vacant_len());
let mut acc = vec![];
for _ in 0..len
{
for _ in 0..N
{
acc.push(self.input.try_pop().unwrap());
}
let _ = self.output.try_push(*acc.as_array().unwrap());
}
BlockResult::Ok
}
fn ready(&mut self) -> bool
{
self.input.available_len() > N && self.output.vacant_len() > 0
}
}
#[derive(Block)]
pub struct Windows<T, const N: usize>
{
#[input]
input: In<T>,
#[output]
output: Out<[T; N]>,
buffer: VecDeque<T>,
}
impl<T, const N: usize> Windows<T, N>
{
pub fn new(input: In<T>) -> (Self, In<[T; N]>)
{
let (output, chunks) = Stream::make(1024);
(
Self {
input,
output,
buffer: VecDeque::with_capacity(N),
},
chunks,
)
}
}
impl<T: Copy, const N: usize> BlockWork for Windows<T, N>
{
fn work(&mut self) -> BlockResult
{
let len = (self.input.available_len()).min(self.output.vacant_len());
for _ in 0..len
{
let x = self.input.try_pop().unwrap();
if self.buffer.len() == N
{
let (a, b) = self.buffer.as_slices();
let _ = self.output.try_push(
*a.iter()
.chain(b.iter())
.cloned()
.collect::<Vec<T>>()
.as_array()
.unwrap(),
);
self.buffer.pop_front();
}
self.buffer.push_back(x);
}
BlockResult::Ok
}
fn ready(&mut self) -> bool
{
self.input.available_len() > 0 && self.output.vacant_len() > 0
}
}
#[derive(Block)]
pub struct SimpleAgc<T: ComplexFloat>
where
T::Real: Float,
{
#[input]
input: In<T>,
#[output]
output: Out<T>,
level_fir: Fir<T::Real, T::Real, T::Real>,
}
impl<T: ComplexFloat> SimpleAgc<T>
where
T::Real: Float,
{
pub fn new(input: In<T>, filter_length: usize) -> (Self, In<T>)
{
let (output, agc) = Stream::make(1024);
(
Self {
input,
output,
level_fir: Fir::new(
ImpulseResponse(vec![T::Real::one(); filter_length]).normalized(),
),
},
agc,
)
}
}
impl<T: ComplexFloat> BlockWork for SimpleAgc<T>
where
T::Real: Float,
T: Div<T::Real, Output = T>,
{
fn work(&mut self) -> BlockResult
{
self.output.push_iter(self.input.pop_iter().map(|x| {
let norm = num::traits::real::Real::sqrt(x.re() * x.re() + x.im() * x.im());
let level = self.level_fir.next(norm);
x / level
}));
BlockResult::Ok
}
fn ready(&mut self) -> bool
{
self.input.available_len() > 0 && self.output.vacant_len() > 0
}
}
#[derive(Block)]
pub struct SimpleSquelch<T: Float>
{
#[input]
input: In<Complex<T>>,
#[output]
output: Out<Complex<T>>,
level_fir: Fir<T, T, T>,
minimum_level: T,
open: bool,
}
impl<T: Float> SimpleSquelch<T>
{
pub fn new(
input: In<Complex<T>>,
filter_length: usize,
minimum_level: T,
) -> (Self, In<Complex<T>>)
{
let (output, agc) = Stream::make(1024);
(
Self {
input,
output,
minimum_level,
level_fir: Fir::new(ImpulseResponse(vec![T::one(); filter_length]).normalized()),
open: false,
},
agc,
)
}
}
impl<T: Float> BlockWork for SimpleSquelch<T>
{
fn work(&mut self) -> BlockResult
{
for _ in 0..(self.input.available_len())
{
let x = self.input.try_pop().unwrap();
let level = self.level_fir.next(x.norm());
self.open = level >= self.minimum_level;
if self.open && self.output.vacant_len() == 0
{
return BlockResult::Ok;
}
else if self.open
{
let _ = self.output.try_push(x);
}
}
BlockResult::Ok
}
fn ready(&mut self) -> bool
{
self.input.available_len() > 0 && (!self.open || self.output.vacant_len() > 0)
}
}

2
ntw_dsp/src/filtering.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod fir;
pub mod impulse_response;

View File

@ -0,0 +1,67 @@
use std::collections::VecDeque;
use std::ops::Add;
use std::ops::Mul;
use ringbuf::traits::Consumer;
use crate::filtering::impulse_response::ImpulseResponse;
pub struct Fir<C, V, U>
where
V: Mul<C, Output = U>,
U: Add<U, Output = U>,
{
// Elements at index 0 represent the earliest elements
taps: ImpulseResponse<C>,
queue: VecDeque<V>,
length: usize,
}
impl<C, V, U> Fir<C, V, U>
where
C: Clone,
V: Clone,
V: Mul<C, Output = U>,
U: Add<U, Output = U>,
{
pub fn new(taps: ImpulseResponse<C>) -> Self
{
let len = taps.len();
assert!(len > 0);
Self {
taps,
queue: VecDeque::with_capacity(len),
length: len,
}
}
pub fn next(&mut self, x: V) -> U
{
if self.queue.len() >= self.length
{
let _ = self.queue.pop_front();
}
self.queue.push_back(x);
// Convolve
self.taps
.0
.iter()
.zip(self.queue.iter())
.map(|(tap, sample)| sample.clone() * tap.clone())
.reduce(Add::add)
.unwrap() // Will not panic, and queue are not empty
}
pub fn clear(&mut self)
{
self.queue.clear();
}
}
// Completely stolen from sdrpp dsp code
pub fn estimate_fir_length(transition_width: f32, sample_rate: f32) -> f32
{
3.8 * sample_rate / transition_width
}

View File

@ -0,0 +1,154 @@
use std::ops::Add;
use std::ops::Div;
use std::ops::Mul;
use num::Complex;
use rustfft::FftNum;
use rustfft::FftPlanner;
use crate::filtering::impulse_response::window::Window;
use crate::generation::Nco;
use crate::generation::NcoType;
#[derive(Clone)]
pub struct ImpulseResponse<T>(pub Vec<T>);
impl<T> ImpulseResponse<T>
{
pub fn len(&self) -> usize
{
self.0.len()
}
pub fn is_empty(&self) -> bool
{
self.len() == 0
}
}
impl<T> ImpulseResponse<Complex<T>>
where
T: NcoType,
{
pub fn from_nco(nco: &mut Nco<T>, length: usize) -> Self
{
ImpulseResponse(nco.take(length).collect::<Vec<Complex<T>>>())
}
}
impl<T> ImpulseResponse<Complex<T>>
where
T: FftNum,
{
pub fn from_transfer_function(tranfer_function: &[Complex<T>]) -> Self
{
let mut planner = FftPlanner::new();
let ifft = planner.plan_fft_inverse(tranfer_function.len());
let mut tf = tranfer_function.to_vec();
ifft.process(tf.as_mut_slice());
let mut ir = vec![];
for i in 0..tranfer_function.len()
{
let k = (tranfer_function.len() - (tranfer_function.len() / 2) + i)
% tranfer_function.len();
ir.push(tf[k]);
}
Self(ir)
}
}
impl<T> ImpulseResponse<T>
where
T: Add<T, Output = T> + Div<T, Output = T> + Clone,
{
pub fn normalize(&mut self)
{
let sum = self
.0
.iter()
.cloned()
.reduce(|a, b| T::add(a.clone(), b.clone()));
if sum.is_none()
{
return;
}
let sum = sum.unwrap().clone();
self.0.iter_mut().for_each(|x| *x = x.clone() / sum.clone());
}
pub fn normalized(&self) -> ImpulseResponse<T>
{
let mut new = self.clone();
new.normalize();
new
}
}
impl<T, U> From<U> for ImpulseResponse<T>
where
U: AsRef<[T]>,
T: Clone,
{
fn from(value: U) -> Self
{
ImpulseResponse(Vec::from(value.as_ref()))
}
}
impl<T> ImpulseResponse<T>
where
T: Mul<f32, Output = T> + Clone,
{
pub fn windowed(&self, w: Window) -> Self
{
let mut new = self.clone();
let len = new.len();
new.0
.iter_mut()
.enumerate()
.for_each(|(i, x)| *x = x.clone() * w(i as f32 / len as f32));
new
}
}
pub mod window
{
use std::f32::consts::PI;
pub type Window = fn(f32) -> f32;
pub fn rectangular(_: f32) -> f32
{
1.
}
pub fn bartlett(t: f32) -> f32
{
if t < 0.5 { 2. * t } else { 2. - 2. * t }
}
pub fn hann(t: f32) -> f32
{
0.5 - 0.5 * (2. * PI * t).cos()
}
pub fn hamming(t: f32) -> f32
{
0.54 - 0.46 * (2. * PI * t).cos()
}
pub fn blackmann(t: f32) -> f32
{
let x = 2. * PI * t;
0.45 - 0.5 * x.cos() + 0.08 * (2. * x).cos()
}
pub fn gaussian(sigma: f32, t: f32) -> f32
{
let sq = (t - 0.5) / sigma;
(-sq * sq).exp()
}
}

View File

@ -1,9 +1,32 @@
use std::{f64::consts::PI, marker::PhantomData};
use std::f64::consts::PI;
use std::marker::PhantomData;
use num::Complex;
use crate::Frequency;
pub trait NcoType: Sized
{
fn angle(t: f64) -> Complex<Self>;
}
impl NcoType for f32
{
fn angle(t: f64) -> Complex<Self>
{
Complex::new(t.cos() as f32, t.sin() as f32)
}
}
impl NcoType for f64
{
fn angle(t: f64) -> Complex<Self>
{
Complex::new(t.cos(), t.sin())
}
}
#[derive(Clone, Copy)]
pub struct Nco<T>
{
// Phase offset : 0 = 0, usize::MAX = 2*pi
@ -44,63 +67,34 @@ impl<T> Nco<T>
}
}
impl Nco<f32>
impl<T> Nco<T>
where
T: NcoType,
{
pub fn sample_sin(&self) -> f32
pub fn sample_sin(&self) -> T
{
let t = (self.phase as f32 / usize::MAX as f32) * 2. * std::f32::consts::PI;
t.sin()
let t = (self.phase as f64 / usize::MAX as f64) * 2. * std::f64::consts::PI;
T::angle(t).im
}
pub fn sample_cos(&self) -> f32
pub fn sample_cos(&self) -> T
{
let t = (self.phase as f32 / usize::MAX as f32) * 2. * std::f32::consts::PI;
t.cos()
let t = (self.phase as f64 / usize::MAX as f64) * 2. * std::f64::consts::PI;
T::angle(t).re
}
pub fn sample(&self) -> Complex<f32>
pub fn sample(&self) -> Complex<T>
{
let t = (self.phase as f32 / usize::MAX as f32) * 2. * std::f32::consts::PI;
Complex::new(t.cos(), t.sin())
let t = (self.phase as f64 / usize::MAX as f64) * 2. * std::f64::consts::PI;
T::angle(t)
}
}
impl Nco<f64>
impl<T> Iterator for Nco<T>
where
T: NcoType,
{
pub fn sample_sin(&self) -> f64
{
let t = (self.phase as f64 / usize::MAX as f64) * 2. * std::f64::consts::PI;
t.sin()
}
pub fn sample_cos(&self) -> f64
{
let t = (self.phase as f64 / usize::MAX as f64) * 2. * std::f64::consts::PI;
t.cos()
}
pub fn sample(&self) -> Complex<f64>
{
let t = (self.phase as f64 / usize::MAX as f64) * 2. * std::f64::consts::PI;
Complex::new(t.cos(), t.sin())
}
}
impl Iterator for Nco<f32>
{
type Item = Complex<f32>;
fn next(&mut self) -> Option<Self::Item>
{
let v = self.sample();
self.step();
Some(v)
}
}
impl Iterator for Nco<f64>
{
type Item = Complex<f64>;
type Item = Complex<T>;
fn next(&mut self) -> Option<Self::Item>
{

View File

@ -1,6 +1,8 @@
use std::f64::consts::PI;
use std::ops::Neg;
pub mod blocks;
pub mod filtering;
pub mod generation;
#[derive(Clone, Copy, PartialEq, Debug)]
@ -35,3 +37,13 @@ fn euclid_mod(a: f64, m: f64) -> f64
let r = a % m;
if r < 0.0 { r + m } else { r }
}
impl Neg for Frequency
{
type Output = Frequency;
fn neg(self) -> Self::Output
{
Frequency(usize::MAX - self.0)
}
}