Bfsk modem isolated

This commit is contained in:
2026-03-26 10:44:46 +01:00
parent 4d548a7973
commit cc3ae754ff
14 changed files with 623 additions and 92 deletions

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

@ -0,0 +1 @@
/target

View File

@ -0,0 +1,12 @@
[package]
name = "bfsk-modem-tx"
version = "0.1.0"
edition = "2024"
[dependencies]
oxydsp-flowgraph = {path = "../../oxydsp-flowgraph/"}
oxydsp-dsp = {path = "../../oxydsp-dsp/"}
num = "0.4.3"
hound = "3.5.1"
rand = "0.10.0"
cpal = "0.17.3"

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,54 @@
use crate::transmitter::Transmitter;
pub mod transmitter;
fn main()
{
println!("Transmitter");
let tx = Transmitter::start_new();
loop
{
let mut user_input = String::new();
std::io::stdin().read_line(&mut user_input).unwrap();
println!("Transmitting ...");
tx.transmit(user_input.as_bytes().to_vec());
}
}
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,211 @@
use cpal::Stream;
use cpal::traits::DeviceTrait;
use cpal::traits::HostTrait;
use num::Complex;
use oxydsp_dsp::blocks::filtering::fir::FirFilter;
use oxydsp_dsp::blocks::math::basic::Multiplier;
use oxydsp_dsp::blocks::synthesis::Nco;
use oxydsp_dsp::blocks::synthesis::OscillatorSource;
use oxydsp_dsp::blocks::utilities::adapters::FlatMap;
use oxydsp_dsp::blocks::utilities::adapters::Map;
use oxydsp_dsp::blocks::utilities::adapters::Scan;
use oxydsp_dsp::blocks::utilities::channels::RxSource;
use oxydsp_dsp::blocks::utilities::channels::TxSink;
use oxydsp_dsp::filtering::fir::Fir;
use oxydsp_dsp::units::DigitalFrequency;
use oxydsp_flowgraph::flowgraph;
use oxydsp_flowgraph::io::In;
use rand::random;
use std::f32::consts::PI;
use std::net::UdpSocket;
use std::ops::BitXor;
use std::sync::mpsc;
use std::sync::mpsc::Receiver;
use std::sync::mpsc::SyncSender;
use std::sync::mpsc::sync_channel;
use std::thread::JoinHandle;
use std::time::Duration;
use crate::CARRIER;
use crate::DEVIATION;
use crate::SAMPLE_PER_SYMBOL;
use crate::SAMPLE_RATE;
use crate::gaussian;
use crate::to_bits;
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) = FlatMap::new(bits, |symbol| {
let mut v = vec![0.; SAMPLE_PER_SYMBOL - 1];
v.push(symbol);
v
});
// gaussian fir
// let fir = Fir((0..SAMPLE_PER_SYMBOL)
// .map(|x| gaussian(0.3, x as f32 / SAMPLE_PER_SYMBOL as f32))
// .collect());
//.normalized();
// RRC fir
let rrc_symbol_count = 4;
let rrc_fir_length = SAMPLE_PER_SYMBOL * rrc_symbol_count;
let fir = Fir((0..rrc_fir_length)
.map(|x| {
let centered = oxydsp_dsp::map(
x as f32,
0.,
rrc_fir_length as f32,
-(rrc_symbol_count as f32) * 0.5,
rrc_symbol_count as f32 * 0.5,
);
root_raised_cosine(centered, 1., 1.)
})
.collect());
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 = 200;
// let (reverb, passband) = FirFilter::new(
// passband,
// Fir((0..reverb_length)
// .map(|x| (-5. * (x as f32) / (reverb_length as f32)).exp())
// .collect())
// .normalized(),
// );
//
let (awgn, passband) = Map::new(passband, |x| {
x + Complex::<f32>::new(2. * (random::<f32>() - 0.5), 2. * (random::<f32>() - 0.5))
* 0.0
});
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,
awgn,
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 * 0.01;
}
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);
}
}
pub fn root_raised_cosine(t: f32, beta: f32, ts: f32) -> f32
{
let eps = 1e-8;
if t.abs() < eps
{
// t = 0 special case
return (1.0 / ts.sqrt()) * (1.0 + beta * (4.0 / PI - 1.0));
}
if beta > 0.0 && (t.abs() - ts / (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 / (ts.sqrt() * 2.0_f32.sqrt())) * (term1 + term2);
}
// General case
let numerator = (PI * t * (1.0 - beta) / ts).sin()
+ 4.0 * beta * t / ts * (PI * t * (1.0 + beta) / ts).cos();
let denominator = PI * t * (1.0 - (4.0 * beta * t / ts).powi(2)) / ts;
(1.0 / ts.sqrt()) * (numerator / denominator)
}