Moving examples

This commit is contained in:
2026-03-23 21:17:26 +01:00
parent 02145b6ef0
commit c37fa47b28
10 changed files with 23 additions and 19 deletions

1
examples/bfsk-modem/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

View File

@ -0,0 +1,15 @@
[package]
name = "bfsk-modem"
version = "0.1.0"
edition = "2024"
[dependencies]
oxydsp-flowgraph = {path = "../../oxydsp-flowgraph/"}
oxydsp-dsp = {path = "../../oxydsp-dsp/"}
egui = "0.33.3"
egui_plot = "0.34.1"
eframe = { version = "0.33.3", features = ["default_fonts", "wayland"] }
num = "0.4.3"
hound = "3.5.1"
rand = "0.10.0"
cpal = "0.17.3"

BIN
examples/bfsk-modem/mod.wav Normal file

Binary file not shown.

View File

@ -0,0 +1,25 @@
digraph G {
node [shape=record];
rankdir=TB;
IterSource_0 [label="{ IterSource |{<o0> output} }"];
FirFilter_1 [label="{ {<i0> input}| FirFilter |{<o0> output} }"];
Map_2 [label="{ {<i0> input}| Map |{<o0> output} }"];
Repeat_3 [label="{ {<i0> input}| Repeat |{<o0> output} }"];
Nco_4 [label="{ {<i0> frequency}| Nco |{<o0> output} }"];
OscillatorSource_5 [label="{ OscillatorSource |{<o0> output} }"];
Multiplier_6 [label="{ {<i0> input_a|<i1> input_b}| Multiplier |{<o0> output} }"];
MapResultTagged_7 [label="{ {<i0> input}| MapResultTagged |{<o0> output} }"];
NullSink_8 [label="{ {<i0> input}| NullSink }"];
IterSource_0:o0 -> Repeat_3:i0 [label="f32"];
FirFilter_1:o0 -> Map_2:i0 [label="f32"];
Map_2:o0 -> Nco_4:i0 [label="oxydsp_dsp::units::DigitalFrequency"];
Repeat_3:o0 -> FirFilter_1:i0 [label="f32"];
Nco_4:o0 -> Multiplier_6:i0 [label="num_complex::Complex<f32>"];
OscillatorSource_5:o0 -> Multiplier_6:i1 [label="num_complex::Complex<f32>"];
Multiplier_6:o0 -> MapResultTagged_7:i0 [label="num_complex::Complex<f32>"];
MapResultTagged_7:o0 -> NullSink_8:i0 [label="num_complex::Complex<f32>"];
}

Binary file not shown.

View File

@ -0,0 +1,101 @@
use std::collections::VecDeque;
use std::fs::File;
use std::io;
use std::io::Write;
use std::sync::mpsc;
use cpal::traits::DeviceTrait;
use cpal::traits::HostTrait;
use eframe::NativeOptions;
use egui::Color32;
use egui_plot::Line;
use egui_plot::PlotPoints;
use num::Complex;
use num::Zero;
use oxydsp_dsp::blocks::filtering::fir::FirFilter;
use oxydsp_dsp::blocks::iq::zero_if::ZeroIf;
use oxydsp_dsp::blocks::math::basic::Adder;
use oxydsp_dsp::blocks::math::basic::Multiplier;
use oxydsp_dsp::blocks::synthesis::Nco;
use oxydsp_dsp::blocks::synthesis::OscillatorSource;
use oxydsp_dsp::blocks::ted::early_late::EarlyLateGate;
use oxydsp_dsp::blocks::utilities::adapters::Map;
use oxydsp_dsp::blocks::utilities::adapters::MapResultTagged;
use oxydsp_dsp::blocks::utilities::adapters::NullSink;
use oxydsp_dsp::blocks::utilities::adapters::Repeat;
use oxydsp_dsp::blocks::utilities::adapters::Scan;
use oxydsp_dsp::blocks::utilities::adapters::ScanTagged;
use oxydsp_dsp::blocks::utilities::channels::RxSource;
use oxydsp_dsp::blocks::utilities::iter::IterSource;
use oxydsp_dsp::blocks::utilities::squelch::Squelch;
use oxydsp_dsp::filtering::fir::Fir;
use oxydsp_dsp::units::DigitalFrequency;
use oxydsp_flowgraph::block::BlockResult;
use oxydsp_flowgraph::flowgraph;
use oxydsp_flowgraph::graph::FlowGraph;
use rand::random;
use crate::receiver::RadioReceiver;
use crate::transmitter::Transmitter;
pub mod receiver;
pub mod transmitter;
fn main()
{
if std::env::args().len() == 2
{
println!("Transmitter");
let tx = Transmitter::start_new();
loop
{
let mut user_input = String::new();
io::stdin().read_line(&mut user_input).unwrap();
println!("Transmitting ...");
tx.transmit(user_input.as_bytes().to_vec());
}
}
else
{
println!("Receiver");
let _tx = RadioReceiver::start_new();
}
}
pub const SAMPLE_RATE: usize = 48_000;
pub const SAMPLE_PER_SYMBOL: usize = 48;
pub const DEVIATION: f64 = 500.;
pub const CARRIER: f64 = 1700.;
pub fn to_bits(n: u8) -> [bool; 8]
{
[
(n & 1) == 1,
(n >> 1) & 1 == 1,
(n >> 2) & 1 == 1,
(n >> 3) & 1 == 1,
(n >> 4) & 1 == 1,
(n >> 5) & 1 == 1,
(n >> 6) & 1 == 1,
(n >> 7) & 1 == 1,
]
}
pub fn from_bits(n: [bool; 8]) -> u8
{
(n[0] as u8)
| ((n[1] as u8) << 1)
| ((n[2] as u8) << 2)
| ((n[3] as u8) << 3)
| ((n[4] as u8) << 4)
| ((n[5] as u8) << 5)
| ((n[6] as u8) << 6)
| ((n[7] as u8) << 7)
}
pub fn gaussian(sigma: f32, t: f32) -> f32
{
let sq = (t - 0.5) / sigma;
(-sq * sq).exp()
}

View File

@ -0,0 +1,336 @@
use std::collections::VecDeque;
use std::net::UdpSocket;
use std::sync::mpsc;
use std::sync::mpsc::Receiver;
use std::thread::JoinHandle;
use cpal::Stream;
use cpal::traits::DeviceTrait;
use cpal::traits::HostTrait;
use eframe::NativeOptions;
use egui::Color32;
use egui_plot::Line;
use egui_plot::PlotPoints;
use num::Complex;
use num::Zero;
use oxydsp_dsp::blocks::filtering::fir::FirFilter;
use oxydsp_dsp::blocks::iq::zero_if::ZeroIf;
use oxydsp_dsp::blocks::ted::early_late::EarlyLateGate;
use oxydsp_dsp::blocks::utilities::adapters::Map;
use oxydsp_dsp::blocks::utilities::adapters::NullSink;
use oxydsp_dsp::blocks::utilities::adapters::Scan;
use oxydsp_dsp::blocks::utilities::adapters::ScanTagged;
use oxydsp_dsp::blocks::utilities::channels::RxSource;
use oxydsp_dsp::blocks::utilities::squelch::Squelch;
use oxydsp_dsp::filtering::fir::Fir;
use oxydsp_dsp::units::DigitalFrequency;
use oxydsp_flowgraph::flowgraph;
use oxydsp_flowgraph::graph::FlowGraph;
use oxydsp_flowgraph::tag::Tag;
use oxydsp_flowgraph::tag::Tagged;
use crate::CARRIER;
use crate::DEVIATION;
use crate::SAMPLE_PER_SYMBOL;
use crate::SAMPLE_RATE;
pub enum PacketBuilderBitState
{
WaitingForPreamble,
InPacket,
}
pub enum PacketBuilderByteState
{
Length1,
Length2,
Data,
}
pub struct PacketBuilder
{
current_byte: u8,
bit_index: u8,
bit_state: PacketBuilderBitState,
packet_state: PacketBuilderByteState,
// Packet building
length: u16,
data: Vec<u8>,
}
impl PacketBuilder
{
pub fn new() -> Self
{
Self {
current_byte: 0,
bit_index: 0,
bit_state: PacketBuilderBitState::WaitingForPreamble,
packet_state: PacketBuilderByteState::Length1,
length: 0,
data: vec![],
}
}
fn next_byte(&mut self) -> Option<Vec<u8>>
{
match self.packet_state
{
PacketBuilderByteState::Length1 =>
{
self.length = 0;
self.length |= self.current_byte as u16;
println!("starting packet, length 1 {}", self.current_byte);
self.packet_state = PacketBuilderByteState::Length2;
}
PacketBuilderByteState::Length2 =>
{
println!("starting packet, length 2 {}", self.current_byte);
self.length |= (self.current_byte as u16) << 8;
self.data = vec![];
self.packet_state = PacketBuilderByteState::Data;
println!("length : {}", self.length);
}
PacketBuilderByteState::Data =>
{
self.data.push(self.current_byte);
self.length -= 1;
if self.length == 0
{
println!("finished");
let current = std::mem::replace(self, Self::new());
return Some(current.data);
}
}
}
None
}
pub fn next_bit(&mut self, bit: bool) -> Option<Vec<u8>>
{
self.current_byte >>= 1;
self.current_byte |= (bit as u8) << 7;
match self.bit_state
{
PacketBuilderBitState::WaitingForPreamble =>
{
if self.current_byte == 0b01100111
{
println!("preamble heard !");
self.bit_state = PacketBuilderBitState::InPacket;
self.bit_index = 0;
}
return None;
}
PacketBuilderBitState::InPacket =>
{
self.bit_index += 1;
if self.bit_index == 8
{
let out = self.next_byte();
self.bit_index = 0;
return out;
}
None
}
}
}
}
pub struct RadioReceiver {
//stream: Stream,
//pub packet_receiver: Receiver<Vec<u8>>,
}
impl RadioReceiver
{
pub fn start_new() -> Self
{
let carrier = DigitalFrequency::from_time_frequency(CARRIER, SAMPLE_RATE as f64);
let (audio_tx, audio_rx) = mpsc::channel();
let (packet_tx, packet_rx) = mpsc::channel::<Vec<u8>>();
let (source, signal) = RxSource::new(audio_rx);
let (inspect, signal) = Map::new(signal, |x| {
//println!("{x}");
x
});
let (mut zero_if, iq) = ZeroIf::new(signal, carrier.into());
zero_if.set_fir(Fir::lowpass(
DigitalFrequency::from_time_frequency(2. * DEVIATION + 100., SAMPLE_RATE as f64),
SAMPLE_PER_SYMBOL * 4,
));
let (squelch, iq) = Squelch::new(iq, 5., 100);
let (arg_extract, arg) = Scan::new(iq, Complex::zero(), |state, sample| {
let angle: Complex<f32> = sample / *state;
*state = sample;
angle.arg() * 14.
});
let mut elg_loop = Fir(vec![1. / 20.; 30]);
*elg_loop.0.last_mut().unwrap() = 1.;
let (elg, arg) = EarlyLateGate::new(arg, elg_loop, SAMPLE_PER_SYMBOL);
// // Eye diagram
let (tx, rx) = mpsc::channel::<(Vec<f32>, f32)>();
//let (eye_sender, arg) = ScanTagged::new(arg, VecDeque::<()>::new(), move |history, x| {
let (eye_sender, arg) = ScanTagged::new(arg, VecDeque::new(), move |history, x| {
let cloned_tag = x.1.clone();
if history.len() == 2 * SAMPLE_PER_SYMBOL
{
history.pop_back();
}
let mut error: f32 = 0.;
let is_symbol_center = x.1.as_ref().is_some_and(|t| {
if let Some(err) = t.retrieve("elg_symbol")
{
error = *err.downcast().unwrap();
true
}
else
{
false
}
});
history.push_front(((is_symbol_center, error), x.0));
if history.len() > SAMPLE_PER_SYMBOL && history[SAMPLE_PER_SYMBOL].0.0
{
let _ = tx.send((
history.iter().map(|(_, x)| *x).collect::<Vec<_>>(),
history[SAMPLE_PER_SYMBOL].0.1,
));
}
Tagged::new(x.0, None)
});
let (packet_map, arg) =
ScanTagged::new(arg, PacketBuilder::new(), move |builder, sample| {
if sample
.1
.as_ref()
.is_some_and(|t| t.retrieve("elg_symbol").is_some())
&& let Some(packet) = builder.next_bit(sample.0 < 0.)
{
let _ = packet_tx.send(packet);
}
Tagged::new(sample.0, None)
});
let null_sink = NullSink::new(arg);
let graph = flowgraph![
source,
inspect,
squelch,
zero_if,
packet_map,
arg_extract,
//sig_lowpass,
elg,
eye_sender,
null_sink
];
let t = graph.run();
// Setup input
// let host = cpal::default_host();
// let device = host.default_input_device().expect("No input device");
// let mut supported_configs_range = device
// .supported_input_configs()
// .expect("error while querying configs");
// let supported_config = supported_configs_range
// .next()
// .expect("no supported config?!")
// .with_sample_rate(SAMPLE_RATE as u32);
// let stream = device
// .build_input_stream(
// &supported_config.into(),
// move |data: &[f32], _: &cpal::InputCallbackInfo| {
// for x in data.iter()
// {
// let _ = audio_tx.send(*x);
// }
// },
// move |err| {
// panic!() // react to errors here.
// },
// None, // None=blocking, Some(Duration)=timeout
// )
// .unwrap();
std::thread::spawn(move || {
let socket = UdpSocket::bind("0.0.0.0:25565").unwrap();
let mut buffer = [0u8; 4096];
while let Ok(read) = socket.recv(&mut buffer)
{
let read_buffer = &mut buffer[0..read];
for x in read_buffer.chunks(4)
{
let val = f32::from_le_bytes([x[0], x[1], x[2], x[3]]);
let _ = audio_tx.send(val);
}
}
});
let mut eyes = VecDeque::new();
eframe::run_simple_native("Plot", NativeOptions::default(), move |ctx, _frame| {
while let Ok(x) = packet_rx.try_recv()
{
println!("Got data: {} bytes.", x.len());
let str: String = x.iter().map(|x| *x as char).collect();
println!("-----\n\n{}\n\n-----", str);
}
while let Ok(eye) = rx.try_recv()
{
if eyes.len() >= 100
{
let _ = eyes.pop_back();
}
eyes.push_front(eye);
}
egui::CentralPanel::default().show(ctx, |ui| {
egui_plot::Plot::new("hello").show(ui, |plot_ui| {
for eye in eyes.iter()
{
plot_ui.line(
Line::new(
"eyes",
eye.0
.iter()
.enumerate()
.map(|(i, s)| [i as f64 / 2., *s as f64])
.collect::<PlotPoints>(),
)
.id("eyes")
.color(Color32::GREEN),
);
}
});
ctx.request_repaint();
});
})
.unwrap();
Self {
//stream,
//packet_receiver: packet_rx,
}
}
}
pub fn color_from_err(error: f32, max: f32) -> Color32
{
Color32::RED
.linear_multiply(error.abs() / max)
.blend(Color32::GREEN.linear_multiply((1. - error.abs() / max).max(0.)))
}

View File

@ -0,0 +1,297 @@
use std::collections::VecDeque;
use std::fs::File;
use std::io::Write;
use std::iter::FusedIterator;
use std::net::UdpSocket;
use std::ops::BitXor;
use std::sync::mpsc;
use std::sync::mpsc::Receiver;
use std::sync::mpsc::Sender;
use std::sync::mpsc::SyncSender;
use std::sync::mpsc::sync_channel;
use std::thread::JoinHandle;
use std::time::Duration;
use cpal::Stream;
use cpal::traits::DeviceTrait;
use cpal::traits::HostTrait;
use eframe::NativeOptions;
use egui::Color32;
use egui::output;
use egui_plot::Line;
use egui_plot::PlotPoints;
use num::Complex;
use num::Zero;
use oxydsp_dsp::blocks::filtering::fir::FirFilter;
use oxydsp_dsp::blocks::iq::zero_if::ZeroIf;
use oxydsp_dsp::blocks::math::basic::Adder;
use oxydsp_dsp::blocks::math::basic::Multiplier;
use oxydsp_dsp::blocks::synthesis::Nco;
use oxydsp_dsp::blocks::synthesis::OscillatorSource;
use oxydsp_dsp::blocks::ted::early_late::EarlyLateGate;
use oxydsp_dsp::blocks::utilities::adapters::Map;
use oxydsp_dsp::blocks::utilities::adapters::MapResultTagged;
use oxydsp_dsp::blocks::utilities::adapters::NullSink;
use oxydsp_dsp::blocks::utilities::adapters::Repeat;
use oxydsp_dsp::blocks::utilities::adapters::Scan;
use oxydsp_dsp::blocks::utilities::adapters::ScanTagged;
use oxydsp_dsp::blocks::utilities::channels::RxSource;
use oxydsp_dsp::blocks::utilities::channels::TxSink;
use oxydsp_dsp::blocks::utilities::iter::IterSource;
use oxydsp_dsp::blocks::utilities::squelch::Squelch;
use oxydsp_dsp::filtering::fir::Fir;
use oxydsp_dsp::units::DigitalFrequency;
use oxydsp_flowgraph::BlockIO;
use oxydsp_flowgraph::block::Block;
use oxydsp_flowgraph::block::BlockResult;
use oxydsp_flowgraph::flowgraph;
use oxydsp_flowgraph::graph::FlowGraph;
use oxydsp_flowgraph::io::In;
use oxydsp_flowgraph::io::Out;
use rand::random;
use crate::CARRIER;
use crate::DEVIATION;
use crate::SAMPLE_PER_SYMBOL;
use crate::SAMPLE_RATE;
use crate::gaussian;
use crate::to_bits;
#[derive(BlockIO)]
pub struct FlatMap<I, O, F>
where
I: 'static,
O: IntoIterator + 'static,
O::IntoIter: FusedIterator,
F: Fn(I) -> O,
{
#[input]
input: In<I>,
#[output]
output: Out<O::Item>,
current_iter: Option<O::IntoIter>,
map: F,
}
impl<I, O, F> FlatMap<I, O, F>
where
I: 'static,
O: IntoIterator + 'static,
O::IntoIter: FusedIterator,
F: Fn(I) -> O,
{
pub fn new(input: In<I>, map: F) -> (Self, In<O::Item>)
{
let (output, port) = oxydsp_flowgraph::io::stream();
(
Self {
input,
output,
current_iter: None,
map,
},
port,
)
}
}
impl<I, O, F> Block for FlatMap<I, O, F>
where
I: 'static,
O: IntoIterator + 'static,
O::IntoIter: FusedIterator,
F: Fn(I) -> O,
{
fn work(&mut self) -> BlockResult
{
let writer = self.output.write();
let reader = self.input.read();
let max_write = writer.len();
let mut written = 0;
while written < max_write
{
if let Some(current_iter) = self.current_iter.as_mut()
{
if let Some(next_elt) = current_iter.next()
{
let _ = writer.push((next_elt, None).into());
written += 1;
continue;
}
else
{
// Iterator empty
self.current_iter = None;
}
}
if self.current_iter.is_none()
{
// Get input
if let Some(input) = reader.pop()
{
let mut new_iter = (self.map)(input.0).into_iter();
if let Some(first_elt) = new_iter.next()
{
self.current_iter = Some(new_iter);
let _ = writer.push((first_elt, input.1).into());
written += 1;
}
else
{
// Iterator empty
self.current_iter = None;
continue;
}
}
else
{
// Cannot continue
break;
}
}
}
BlockResult::Ok
}
}
pub struct Transmitter
{
flowgraph_handle: JoinHandle<()>,
packet_sender: SyncSender<Vec<u8>>,
stream: Stream,
}
impl Transmitter
{
pub fn start_new() -> Self
{
let carrier = DigitalFrequency::from_time_frequency(CARRIER, SAMPLE_RATE as f64);
let deviation = DigitalFrequency::from_time_frequency(DEVIATION, SAMPLE_RATE as f64);
let (packet_tx, packet_rx): (_, Receiver<Vec<u8>>) = sync_channel(128);
let (packet_rec, packets): (_, In<Vec<u8>>) = RxSource::new(packet_rx);
let (linearizer, bits) = FlatMap::new(packets, |packet| {
// +1 for chksum
let packet_length = packet.len() as u16;
let checksum = packet.iter().copied().reduce(BitXor::bitxor).unwrap();
// Learning sequence
let mut frame = vec![0b10101010; 8];
// Preamble
frame.push(0b01100111);
frame.push(packet_length.to_le_bytes()[0]);
frame.push(packet_length.to_le_bytes()[1]);
frame.extend(packet.iter());
frame.push(checksum);
frame.extend((0..16).map(|_| 0));
frame
.into_iter()
.flat_map(to_bits)
.map(|x| if x { 1. } else { -1. })
});
let (repeat, bits) = Repeat::new(bits, SAMPLE_PER_SYMBOL);
// gaussian fir
let fir = Fir((0..SAMPLE_PER_SYMBOL)
.map(|x| gaussian(0.1, x as f32 / SAMPLE_PER_SYMBOL as f32))
.collect())
.normalized();
//let (bit_filter, bits) = FirFilter::new(bits, fir);
let (to_freq, freq) = Map::new(bits, move |x| {
DigitalFrequency::from_time_frequency(DEVIATION * x as f64, SAMPLE_RATE as f64)
});
let (base_oscillator, baseband) = Nco::<f32>::new(freq);
let (local_oscillator, lo) = OscillatorSource::<f32>::new(carrier.into());
let (frontend, passband) = Multiplier::new(baseband, lo);
let (audio_tx, audio_rx) = mpsc::channel::<Complex<f32>>();
let reverb_length = 20;
// let (reverb, passband) = FirFilter::new(
// passband,
// Fir((0..reverb_length)
// .map(|x| (-15. * (x as f32) / (reverb_length as f32)).exp())
// .collect())
// .normalized(),
// );
let (udp_map, passband) = Scan::new(
passband,
UdpSocket::bind("127.0.0.1:25566").unwrap(),
|sckt, sample| {
sckt.send_to(
&(sample.re + ((random::<f32>() * 2.) - 1.) * 0.0).to_le_bytes(),
"127.0.0.1:25565",
)
.unwrap();
sample
},
);
let tx_sink = TxSink::new(passband, audio_tx);
let graph = flowgraph![
packet_rec,
linearizer,
//reverb,
//bit_filter,
udp_map,
to_freq,
repeat,
base_oscillator,
local_oscillator,
frontend,
tx_sink,
];
// Open output device
let host = cpal::default_host();
let device = host
.default_output_device()
.expect("no output device available");
let mut supported_configs_range = device
.supported_output_configs()
.expect("error while querying configs");
let supported_config = supported_configs_range
.next()
.expect("no supported config?!")
.with_sample_rate(SAMPLE_RATE as u32);
let stream = device
.build_output_stream(
&supported_config.into(),
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
for x in data.iter_mut()
{
if let Ok(y) = audio_rx.try_recv()
{
*x = y.re;
}
else
{
*x = 0.;
}
}
},
move |err| panic!(),
None, // None=blocking, Some(Duration)=timeout
)
.unwrap();
Self {
flowgraph_handle: graph.run(),
packet_sender: packet_tx,
stream,
}
}
pub fn transmit(&self, data: Vec<u8>)
{
let _ = self.packet_sender.send(data);
}
}