Working fsk transmitter

This commit is contained in:
2026-03-22 19:34:21 +01:00
parent f1f769e0e6
commit 6429685cd2
16 changed files with 921 additions and 406 deletions

View File

@ -1,4 +1,5 @@
pub mod filtering;
pub mod iq;
pub mod math;
pub mod synthesis;
pub mod ted;

View File

@ -0,0 +1 @@
pub mod zero_if;

View File

@ -0,0 +1,65 @@
use num::{Complex, Float};
use oxydsp_flowgraph::{
BlockIO,
block::SyncBlock,
io::{In, Out},
sync_block,
};
use rustfft::FftNum;
use crate::{
filtering::fir::{Fir, FirFilter},
synthesis::oscillator::Nco,
};
#[derive(BlockIO)]
#[sync_block]
pub struct ZeroIf<T: std::clone::Clone + num::Num + Float + From<f32> + 'static>
{
#[input]
input: In<T>,
#[output]
output: Out<Complex<T>>,
local_oscillator: Nco<T>,
filter: FirFilter<Complex<T>, Complex<T>, Complex<T>>,
}
impl<T> ZeroIf<T>
where
T: std::clone::Clone + num::Num + FftNum + From<f32> + 'static + num::Float,
{
pub fn new(input: In<T>, lo: Nco<T>) -> (Self, In<Complex<T>>)
{
let (output, port) = oxydsp_flowgraph::io::stream();
(
Self {
input,
output,
local_oscillator: lo,
filter: FirFilter::new(Fir::lowpass(lo.frequency(), 100)),
},
port,
)
}
pub fn set_fir(&mut self, fir: Fir<Complex<T>>)
{
self.filter = FirFilter::new(fir);
}
}
impl<'view, T> SyncBlock<'view> 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>
{
// Mix
let lo_sample = state.local_oscillator.next().unwrap();
let iq = Complex::new(input * lo_sample.re, input * lo_sample.im);
Some(state.filter.next(iq))
}
}

View File

@ -2,6 +2,7 @@ use std::collections::VecDeque;
use std::iter::Sum;
use num::Float;
use num::NumCast;
use oxydsp_flowgraph::BlockIO;
use oxydsp_flowgraph::block::SyncBlock;
use oxydsp_flowgraph::io::In;
@ -14,7 +15,7 @@ use crate::filtering::fir::FirFilter;
#[derive(BlockIO)]
#[sync_block(tagged)]
pub struct EarlyLateGate<T: Float + Sum + Clone + 'static>
pub struct EarlyLateGate<T: Float + Send + Sync + Sum + Clone + NumCast + 'static>
{
#[input]
input: In<T>,
@ -34,13 +35,13 @@ pub struct EarlyLateGate<T: Float + Sum + Clone + 'static>
// The next window location, in relation to the last sample such that the window is centered on
// a symbol center (hopefully)
next_sample: usize,
next_sample: f32,
loop_filter: FirFilter<T, T, T>,
}
impl<T> EarlyLateGate<T>
where
T: Float + Sum + Clone + 'static,
T: Float + Sum + Clone + 'static + Send + Sync + NumCast,
{
pub fn new(input: In<T>, loop_filter: Fir<T>, symbol_length: usize) -> (Self, In<T>)
{
@ -53,7 +54,7 @@ where
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
next_sample: symbol_length as f32, // We assume that the first symbol is 1.5 windows into
// the stream
loop_filter: FirFilter::new(loop_filter),
},
@ -64,13 +65,14 @@ where
impl<'view, T> SyncBlock<'view> for EarlyLateGate<T>
where
T: Float + Sum + Clone + 'static,
T: Float + Sum + Clone + 'static + Send + Sync + NumCast,
{
fn sync_work(state: Self::StateView, input: Self::Input) -> Option<Self::Output>
{
if state.window.len() < *state.symbol_length
{
state.window.push_back(input.0);
*state.window_location += 1;
return Some(input.0.into());
}
@ -81,12 +83,8 @@ where
let sample = state.window[*state.window_center];
let mut tag = None;
if *state.window_location >= *state.next_sample
if *state.window_location >= *state.next_sample as usize
{
let new_tag = Tag::default();
new_tag.tag("elg_symbol", ());
tag = Some(new_tag);
let early_index = *state.window_center - (0.25 * *state.symbol_length as f32) as usize;
let late_index = *state.window_center + (0.25 * *state.symbol_length as f32) as usize;
@ -97,13 +95,16 @@ where
let correction = state.loop_filter.next(error);
// Figure out next sample location
*state.next_sample += (*state.symbol_length as isize
+ correction.floor().to_isize().unwrap_or(0))
.max(0) as usize;
*state.next_sample +=
(*state.symbol_length as f32 + correction.to_f32().unwrap()).max(0.);
// Turn everything back relative to current sample
*state.next_sample -= *state.window_location;
*state.next_sample -= *state.window_location as f32;
*state.window_location = 0;
let new_tag = Tag::default();
new_tag.tag("elg_symbol", error);
tag = Some(new_tag);
}
Some((sample, tag).into())

View File

@ -1,3 +1,4 @@
pub mod adapters;
pub mod channels;
pub mod iter;
pub mod squelch;

View File

@ -204,6 +204,55 @@ where
}
}
#[derive(BlockIO)]
#[sync_block(tagged)]
pub struct ScanTagged<I: 'static, O: 'static, S, F>
where
F: Fn(&mut S, Tagged<I>) -> Tagged<O>,
{
#[input]
input: In<I>,
#[output]
output: Out<O>,
state: S,
map: F,
}
impl<I: 'static, O: 'static, S, F> ScanTagged<I, O, S, F>
where
F: Fn(&mut S, Tagged<I>) -> Tagged<O>,
{
pub fn new(input: In<I>, initial_state: S, map: F) -> (Self, In<O>)
{
let (output, mapped) = stream();
(
Self {
input,
output,
state: initial_state,
map,
},
mapped,
)
}
}
impl<'view, I, O, S, F> SyncBlock<'view> for ScanTagged<I, O, S, F>
where
I: 'static,
O: 'static,
S: 'view,
F: Fn(&mut S, Tagged<I>) -> Tagged<O> + 'view,
{
fn sync_work(state: Self::StateView, input: Self::Input) -> Option<Self::Output>
{
Some((*state.map)(state.state, input))
}
}
#[derive(BlockIO)]
pub struct Repeat<T: 'static>
{
@ -248,22 +297,24 @@ impl<T: Clone + 'static> Block for Repeat<T>
{
if self.remaining == 0 || self.current.is_none()
{
self.current = Some(reader.pop().unwrap().into());
self.remaining = self.repetitions;
if let Some(x) = reader.pop()
{
self.current = Some(x.into());
self.remaining = self.repetitions;
}
else
{
return BlockResult::Ok;
}
}
writer
.push(self.current.clone().unwrap().into())
.unwrap_or_else(|_| panic!());
match &mut self.current
if let Some((_, tag)) = &mut self.current
{
Some((_, tag)) =>
{
*tag = None;
}
None =>
{}
*tag = None;
}
self.remaining -= 1;

View File

@ -51,7 +51,7 @@ impl<I: 'static> Block for RxSource<Receiver<I>, I>
{
if self
.output
.push_iter(self.input.iter().map(|x| (x, None).into()))
.push_iter(self.input.try_iter().map(|x| (x, None).into()))
{
BlockResult::Ok
}

View File

@ -0,0 +1,79 @@
use std::{collections::VecDeque, 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};
#[derive(BlockIO)]
pub struct Squelch<T>
where
T: ComplexFloat + 'static,
T::Real: Float + One + Zero + FromPrimitive + Sum + Clone,
{
#[input]
input: In<T>,
#[output]
output: Out<T>,
trigger_level: T::Real,
history: VecDeque<T::Real>,
sum: T::Real,
divider: T::Real,
}
impl<T> Squelch<T>
where
T: ComplexFloat + 'static,
T::Real: Float + Sum + Clone + FromPrimitive,
{
pub fn new(input: In<T>, trigger_level: T::Real, mean_length: usize) -> (Self, In<T>)
{
let (output, port) = oxydsp_flowgraph::io::stream();
(
Self {
input,
output,
trigger_level,
history: VecDeque::from(vec![T::Real::zero(); mean_length]),
sum: T::Real::zero(),
divider: T::Real::from_usize(mean_length).unwrap(),
},
port,
)
}
}
impl<T> Block for Squelch<T>
where
T: ComplexFloat + 'static,
T::Real: Float + Sum + Clone + FromPrimitive,
{
fn work(&mut self) -> oxydsp_flowgraph::block::BlockResult
{
let writer = self.output.write();
for x in self.input.pop_iter().take(writer.len())
{
let (element, tag) = x.into();
let oldest = self.history.pop_front().unwrap();
let newest = element.abs();
self.history.push_back(newest);
self.sum = self.sum - oldest;
self.sum = self.sum + newest;
if (self.sum / self.divider) > self.trigger_level
{
let _ = writer.push((element, tag).into());
}
}
BlockResult::Ok
}
}

View File

@ -30,6 +30,11 @@ impl<T> Nco<T>
}
}
pub fn frequency(&self) -> DigitalFrequency
{
DigitalFrequency(self.d_phase)
}
pub fn with_phase(frequency: DigitalFrequency, phase: Phase) -> Self
{
Self {