From 96689cc3a69ff691d1b711b3619beaf387a743b3 Mon Sep 17 00:00:00 2001 From: Albin Chaboissier Date: Sun, 5 Oct 2025 14:49:04 +0200 Subject: [PATCH] No warnings ! Starts csma-like behaviour --- Cargo.lock | 82 +++++ Cargo.toml | 1 + main.rs | 525 +++++++++++++++++++++++++++ main.rs.mt | 424 ---------------------- src/bfsk.rs | 5 +- src/complex.rs | 2 +- src/fft/mixed_radix.rs | 2 +- src/fft/rader.rs | 2 +- src/fft/rader2.rs | 2 +- src/filtering/fir.rs | 20 +- src/filtering/impulse_response.rs | 3 - src/iq.rs | 7 +- src/main.rs | 585 ++++-------------------------- src/math.rs | 9 + src/nco.rs | 1 - src/signal.rs | 0 src/ted.rs | 1 + src/ted/elg.rs | 66 ++++ 18 files changed, 769 insertions(+), 968 deletions(-) create mode 100644 main.rs delete mode 100644 main.rs.mt create mode 100644 src/math.rs delete mode 100644 src/signal.rs create mode 100644 src/ted.rs create mode 100644 src/ted/elg.rs diff --git a/Cargo.lock b/Cargo.lock index 1620e7b..5b7ccd5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -108,6 +108,15 @@ dependencies = [ "winit", ] +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.1" @@ -425,6 +434,21 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link 0.2.0", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -1346,6 +1370,12 @@ dependencies = [ "weezl", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + [[package]] name = "gl_generator" version = "0.14.0" @@ -1711,6 +1741,17 @@ dependencies = [ "hashbrown 0.16.0", ] +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags 2.9.4", + "cfg-if", + "libc", +] + [[package]] name = "jack" version = "0.13.3" @@ -1959,6 +2000,17 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + [[package]] name = "moxcms" version = "0.7.5" @@ -2412,6 +2464,15 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -2810,6 +2871,7 @@ dependencies = [ "hound", "plotters", "rand", + "tokio", ] [[package]] @@ -2847,6 +2909,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + [[package]] name = "rustc-hash" version = "1.1.0" @@ -3250,6 +3318,20 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "io-uring", + "libc", + "mio", + "pin-project-lite", + "slab", +] + [[package]] name = "toml_datetime" version = "0.7.2" diff --git a/Cargo.toml b/Cargo.toml index 781c5ee..7a14e2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,4 @@ egui_plot = "0.33.0" hound = "3.5.1" plotters = "0.3.7" rand = "0.9.2" +tokio = "1.47.1" diff --git a/main.rs b/main.rs new file mode 100644 index 0000000..f42dad3 --- /dev/null +++ b/main.rs @@ -0,0 +1,525 @@ +#![allow(dead_code)] + +use std::{ + collections::VecDeque, + f32::consts::PI, + fs::File, + i16, + io::{self, Read, Write, stdout}, + ops::{Add, Div, Mul, Sub}, + os::unix::thread, + sync::{ + Arc, + atomic::{AtomicU32, Ordering}, + mpsc::{self, Receiver, Sender, TryRecvError, channel, sync_channel}, + }, + time::Duration, +}; + +mod bfsk; +mod complex; +pub mod fft; +mod filtering; +mod iq; +mod nco; +mod units; +mod windows; +mod ted; + +use bfsk::BFSKMod; +use complex::Complex; +use complex::Complex32; +use cpal::{ + SampleRate, + traits::{DeviceTrait, HostTrait, StreamTrait}, +}; +use fft::DFTAlgorithm; +use nco::Nco; + +use eframe::{ + egui::{self, Color32, Context, Vec2b, debug_text::print, decode_animated_image_uri}, + glow::TOP_LEVEL_ARRAY_STRIDE, +}; +use egui_plot::{ + self, AxisHints, Bar, BarChart, HLine, Legend, Line, LineStyle, Plot, PlotPoints, VLine, +}; +use plotters::style::Color; +use rand::{Rng, seq::index::sample}; + +use crate::{ + bfsk::BFSKDem, fft::{dft::NaiveDFT, FFT}, filtering::{ + dc_block::DCBlocker, + fir::FIRFilter, + impulse_response::design::{self, frequency_response, ir_from_transfer_function}, + }, iq::IQSampler, ted::elg::ELGate, units::frequency::{self, hz_to_rad_per_sample} +}; + +// Utilities +fn map(input: T, in_min: T, in_max: T, out_min: T, out_max: T) -> T +where + T: Clone + Add + Mul + Sub + Div, +{ + ((input - in_min.clone()) / (in_max - in_min)) * (out_max - out_min.clone()) + out_min +} +const BAUD_RATE: u32 = 1000; + +fn main() { + //modulate(); + //demodulate(); + //return; + // Set up CPAL + let host = cpal::default_host(); + + let device = host + .default_input_device() + .expect("No input device available"); + let mut config = device + .supported_input_configs() + .unwrap() + .next() + .unwrap() + .with_sample_rate(SampleRate(48000)); + + // Channel to move samples from callback to main thread + let (tx, rx) = sync_channel::(4096); + + // Build input stream + /* + let stream = device + .build_input_stream( + &config.into(), + move |data: &[f32], _| { + for x in data.iter() { + let _ = tx.send(*x * 30.); // non-blocking send + } + }, + move |err| eprintln!("Stream error: {}", err), + None, + ) + .unwrap(); + stream.play().unwrap(); + */ + + let (eye_tx, eye_rx) = mpsc::channel::>(); + let (ctx_tx, ctx_rx) = mpsc::channel::>(); + std::thread::spawn(move || demodulator(rx, eye_tx, ctx_rx)); + + std::thread::spawn(move || { + let spec = hound::WavSpec { + channels: 1, + sample_rate: 48000, + bits_per_sample: 16, + sample_format: hound::SampleFormat::Int, + }; + + let mut writer = hound::WavWriter::create("audio/noised.wav", spec).unwrap(); + + let mut reader = hound::WavReader::open("audio/rec.wav").unwrap(); + let mut rand = rand::rng(); + let samples = reader.samples::(); + for x in samples { + let noise = rand.random::() * 2. - 1.; + let sample = x.unwrap() as f32 / i16::MAX as f32 + noise * 0.9; + let _ = tx.send(sample); + writer + .write_sample((sample * i16::MAX as f32) as i16) + .unwrap(); + std::thread::sleep(Duration::from_micros(21)); + } + writer.finalize().unwrap(); + }); + + let native_options = eframe::NativeOptions::default(); + let _ = eframe::run_native( + "Egui", + native_options, + Box::new(|cc| Ok(Box::new(EguiApp::new(cc, eye_rx, ctx_tx)))), + ); +} + + +fn demodulator( + rx: Receiver, + eye_sender: Sender>, + ctx_rx: Receiver>, +) { + // Wait for egui context to request redraw + let ctx = ctx_rx.recv().unwrap(); + + // Modulation parameters + let frequency = 1700.; + let deviation = 500.; + + // Data parameters + let sample_rate = 48000; + let baud_rate = BAUD_RATE; + let sample_per_symbol = sample_rate / baud_rate; + + let mut iq_sampler = IQSampler::new(hz_to_rad_per_sample(frequency, sample_rate as f32)); + + // Corellators + let mut nco_pos = Nco::new(hz_to_rad_per_sample(deviation, sample_rate as f32)); + let mut nco_neg = Nco::new(hz_to_rad_per_sample(-deviation, sample_rate as f32)); + let corellator_length = (sample_per_symbol as f32 * 1.5) as usize; + let corellator_pos = (0..corellator_length) + .map(|i| { + nco_pos.step(); + nco_pos.cexp() * windows::blackmann(i as f32 / (corellator_length as f32)) + //nco_pos.cexp() + }) + .collect::>(); + let corellator_neg = (0..corellator_length) + .map(|i| { + nco_neg.step(); + nco_neg.cexp() * windows::blackmann(i as f32 / (corellator_length as f32)) + //nco_neg.cexp() + }) + .collect::>(); + let mut matched_filter_pos = FIRFilter::new(&corellator_pos); + let mut matched_filter_neg = FIRFilter::new(&corellator_neg); + matched_filter_pos.normalize_freq(hz_to_rad_per_sample(deviation,sample_rate as f32)); + matched_filter_neg.normalize_freq(hz_to_rad_per_sample(deviation,sample_rate as f32)); + + let loop_p = 0.003; + let loop_i = 0.001; + let mut matched_filter = FIRFilter::new(&[Complex32::new(1., 0.); 20]); + matched_filter.normalize_dc(); + + let mut loop_filter_ir = vec![Complex32::new(loop_i, 0.); 30]; + let len = loop_filter_ir.len(); + //loop_filter_ir[0] = Complex32::new(loop_p + loop_i, 0.); + loop_filter_ir[len - 1] = Complex32::new(loop_p + loop_i, 0.); + + let mut loop_filter = FIRFilter::new(&loop_filter_ir); + //loop_filter.normalize_dc(); + //loop_filter.normalize_sum(); + let mut elg = ELGate::new(sample_per_symbol as f32, loop_filter); + + let mut dc_blocker = DCBlocker::new(0.999); + + // Timing recovery + let mut preamble_count = 0; + let mut bit_index = 0; + let mut last = 0u8; + while let Ok(real_sample) = rx.recv() { + let iq = iq_sampler.sample(real_sample); + + // Perform corellation + let neg_energy = matched_filter_neg.next(iq); + let pos_energy = matched_filter_pos.next(iq); + + let matched = + matched_filter.next_real(dc_blocker.next_real(pos_energy.mag() - neg_energy.mag())); + + if let Some((elg_sample, eye)) = elg.next_eye(matched) { + let _ = eye_sender.send(eye); + ctx.request_repaint(); + + //last >>= 1; + //last |= (!bit as u8) << 7; + let bit = elg_sample > 0.; + if elg_sample * elg_sample > 0.005 { + last >>= 1; + last |= (bit as u8) << 7; + //last <<= 1; + //last |= ((bit) as u8); + + if preamble_count >= 2 { + bit_index += 1; + if bit_index >= 8 { + if last == 4 { + print!(" -- EOT"); + println!(); + preamble_count = 0; + bit_index = 0; + last = 0; + } else { + print!("{}", last as char); + bit_index = 0; + let _ = stdout().flush(); + } + } + } else if last == 0xD8 { + preamble_count += 1; + if preamble_count == 2 { + println!("Incoming: "); + } + } + } + } + } +} + +//#[derive(Default)] +struct EguiApp { + eye_rx: Receiver>, + eyes: VecDeque>, +} + +impl EguiApp { + fn new( + cc: &eframe::CreationContext<'_>, + eye_rx: Receiver>, + ctx_tx: Sender>, + ) -> Self { + ctx_tx.send(Arc::new(cc.egui_ctx.clone())).unwrap(); + + Self { + eye_rx, + eyes: VecDeque::new(), + } + } +} + +impl eframe::App for EguiApp { + fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + let max_eyes = 100; + + while let Ok(eye) = self.eye_rx.try_recv() { + self.eyes.push_back(eye); + } + + while self.eyes.len() > max_eyes { + self.eyes.pop_front(); + } + + let axis_hints = AxisHints::new_x().min_thickness(2.); + + Plot::new("Eye") + .legend(Legend::default()) + .show_axes(Vec2b::TRUE) + .custom_x_axes(vec![axis_hints]) + .show(ui, |plot_ui| { + //plot_ui.set_auto_bounds(Vec2b { x: false, y: false }); + plot_ui.hline(HLine::new("", 0.).color(Color32::DARK_RED).width(2.)); + for eye in self.eyes.iter() { + let line = Line::new( + "Eye", + eye.iter() + .enumerate() + .map(|(i, x)| [i as f64, *x as f64]) + .collect::>(), + ) + .color(Color32::LIGHT_GREEN); + plot_ui.line(line); + } + }) + }); + } +} + +fn demodulate() { + let mut reader = hound::WavReader::open("audio/modulated.wav").unwrap(); + let samples = reader.samples::(); + + // Modulation parameters + let frequency = 1700.; + let deviation = 500.; + + // Data parameters + let sample_rate = 48000; + let baud_rate = BAUD_RATE; + let sample_per_symbol = sample_rate / baud_rate; + + let mut iq_sampler = IQSampler::new(hz_to_rad_per_sample(frequency, sample_rate as f32)); + let iq = samples + .map(|x| iq_sampler.sample(x.unwrap() as f32 / i16::MAX as f32)) + .collect::>(); + + let mut nco_pos = Nco::new(hz_to_rad_per_sample(deviation, sample_rate as f32)); + let mut nco_neg = Nco::new(hz_to_rad_per_sample(-deviation, sample_rate as f32)); + let corellator_pos = (0..(sample_per_symbol * 1)) + .map(|_| { + nco_pos.step(); + nco_pos.cexp() + }) + .collect::>(); + let corellator_neg = (0..(sample_per_symbol * 1)) + .map(|_| { + nco_neg.step(); + nco_neg.cexp() + }) + .collect::>(); + + let mut matched_filter_pos = FIRFilter::new(&corellator_pos); + let mut matched_filter_neg = FIRFilter::new(&corellator_neg); + + let mut dem = vec![]; + + for x in &iq { + let pos = matched_filter_pos.next(x.clone()); + let neg = matched_filter_neg.next(*x); + dem.push(pos.mag() - neg.mag()); + } + + // Symbol recovery + + let mut bits = vec![]; + let delta = 0.5; + let alpha = 0.01; + let mut current_sps = sample_per_symbol as f32; + let mut current_position = current_sps / 2.; + + while current_position < dem.len() as f32 { + // Sample before after + let early_id = (current_position - (delta * current_sps)).max(0.).floor() as u32; + let late_id = (current_position + (delta * current_sps)).max(0.).floor() as u32; + if late_id as usize >= dem.len() { + break; + } + let early = dem[early_id as usize]; + let late = dem[late_id as usize]; + let error = early * early - late * late; + current_sps -= alpha * error; + + bits.push(dem[current_position.floor() as usize] > 0.); + + current_position += current_sps; + } + + //assert!(bits.len() % 8 == 0); + + let mut out_file = File::create("out.txt").unwrap(); + let mut strip = 0; + + let bit_slice = bits.as_slice(); + for i in 0..100 { + let byte = bits_to_byte(&bit_slice[(i as usize)..(i as usize + 8)]); + if byte == 0b01010111u8 { + strip = i + 8; + } + } + + for i in 0..strip { + bits.remove(i as usize); + } + for x in bits.chunks(8) { + if x.len() != 8 { + break; + } + out_file.write_all(&[bits_to_byte(x)]).unwrap(); + } +} + +fn modulate() { + // Modulation parameters + let frequency = 1700.; + let deviation = 500.; + + // Data parameter + let sample_rate = 48000; + let baud_rate = BAUD_RATE; + + let host = cpal::default_host(); + let device = host.default_output_device().unwrap(); + let mut supported_configs_range = device.supported_output_configs().unwrap(); + let supported_config = supported_configs_range + .find(|config| { + config.sample_format() == cpal::SampleFormat::F32 + && config.min_sample_rate().0 <= 48000 + && config.max_sample_rate().0 >= 48000 + }) + .expect("Device does not support 48kHz f32 output"); + let config = supported_config + .with_sample_rate(cpal::SampleRate(48_000)) + .config(); + + loop { + let mut buffer = String::new(); + let stdin = io::stdin(); // We get `Stdin` here. + stdin.read_line(&mut buffer).unwrap(); + + for c in buffer.bytes() { + print!("{}", c as char); + } + println!(); + + // Construct payload + let mut bitstream = std::iter::repeat_n(0b01010101u8, 64) + .chain(std::iter::repeat_n(0xD8, 2)) + .chain(buffer.bytes()) + .chain(std::iter::repeat_n(4u8, 32)) + .flat_map(byte_to_bits); + + let mut modulator = BFSKMod::new( + sample_rate / baud_rate, + units::frequency::hz_to_rad_per_sample(deviation, sample_rate as f32), + &mut bitstream, + ); + + let mut lo = Nco::new(units::frequency::hz_to_rad_per_sample( + frequency, + sample_rate as f32, + )); + + // To send + let stream = modulator + .zip(lo) + .map(|(s, up)| (s * up).re) + .collect::>(); + + let sample_clock = Arc::new(AtomicU32::new(0)); + let (tx, rx) = channel::<()>(); + + let stream = device + .build_output_stream( + &config, + move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { + for d in data.iter_mut() { + if sample_clock.load(Ordering::Relaxed) as usize == stream.len() { + tx.send(()).unwrap(); + break; + } + *d = stream[sample_clock.fetch_add(1, Ordering::Relaxed) as usize] + } + }, + move |err| { + eprintln!("Stream error: {}", err); + }, + None, + ) + .unwrap(); + + stream.play().unwrap(); + let _ = rx.recv(); + stream.pause().unwrap(); + } +} + +fn byte_to_bits(byte: u8) -> Vec { + vec![ + byte & 1 == 1, + (byte >> 1) & 1 == 1, + (byte >> 2) & 1 == 1, + (byte >> 3) & 1 == 1, + (byte >> 4) & 1 == 1, + (byte >> 5) & 1 == 1, + (byte >> 6) & 1 == 1, + (byte >> 7) & 1 == 1, + ] +} + +/* +fn bits_to_byte(bits: &[bool]) -> u8 { + bits[7] as u8 | + bits[6] as u8 >> 1 | + bits[5] as u8 >> 2 | + bits[4] as u8 >> 3 | + bits[3] as u8 >> 4 | + bits[2] as u8 >> 5 | + bits[1] as u8 >> 6 | + bits[0] as u8 >> 7 +} +*/ + +fn bits_to_byte(bits: &[bool]) -> u8 { + bits[0] as u8 + | (bits[1] as u8) << 1 + | (bits[2] as u8) << 2 + | (bits[3] as u8) << 3 + | (bits[4] as u8) << 4 + | (bits[5] as u8) << 5 + | (bits[6] as u8) << 6 + | (bits[7] as u8) << 7 +} diff --git a/main.rs.mt b/main.rs.mt deleted file mode 100644 index c89ab7f..0000000 --- a/main.rs.mt +++ /dev/null @@ -1,424 +0,0 @@ -#![allow(dead_code)] - -use std::{ - f32::consts::PI, - fs::File, - i16, - io::{Read, Write}, - ops::{Add, Div, Mul, Sub}, -}; - -mod bfsk; -mod complex; -pub mod fft; -mod filtering; -mod iq; -mod nco; -mod signal; -mod units; -mod windows; - -use bfsk::BFSKMod; -use complex::Complex; -use complex::Complex32; -use fft::DFTAlgorithm; -use nco::Nco; - -use eframe::egui::{self, Color32, Context, debug_text::print}; -use egui_plot::{self, Bar, BarChart, Legend, Line, LineStyle, Plot, PlotPoints, VLine}; -use plotters::style::Color; - -use crate::{ - bfsk::BFSKDem, - fft::{FFT, dft::NaiveDFT}, - filtering::{ - fir::FIRFilter, - impulse_response::design::{self, frequency_response, ir_from_transfer_function}, - }, - iq::IQSampler, - units::frequency::{self, hz_to_rad_per_sample}, -}; - -// Utilities -fn map(input: T, in_min: T, in_max: T, out_min: T, out_max: T) -> T -where - T: Clone + Add + Mul + Sub + Div, -{ - ((input - in_min.clone()) / (in_max - in_min)) * (out_max - out_min.clone()) + out_min -} - -fn main() { - modulate(); - println!("Demodulating"); - demodulate(); - - return; - let native_options = eframe::NativeOptions::default(); - let _ = eframe::run_native( - "Egui", - native_options, - Box::new(|cc| Ok(Box::new(EguiApp::new(cc)))), - ); -} - -#[derive(Default)] -struct EguiApp { - samples: Vec, - - iq: Vec, - dem: Vec, - vlines: Vec, - elg_sampling: Vec, - eye_diagram: Vec>, -} -const BAUD_RATE: u32 = 3200; - -impl EguiApp { - fn new(cc: &eframe::CreationContext<'_>) -> Self { - let mut reader = hound::WavReader::open("audio/modulated.wav").unwrap(); - let samples = reader.samples::(); - let sample_count = 10_000; - let input_test = samples - .take(sample_count) - .map(|x| x.unwrap() as f32 / i16::MAX as f32) - .collect::>(); - - // Modulation parameters - let frequency = 1700.; - let deviation = 500.; - - // Data parameters - let sample_rate = 48000; - let baud_rate = BAUD_RATE; - let sample_per_symbol = sample_rate / baud_rate; - let vlines = (0..sample_count) - .filter(|i| i % sample_per_symbol as usize == 0) - .map(|x| x as f32 / sample_count as f32) - .collect(); - - let mut iq_sampler = IQSampler::new(hz_to_rad_per_sample(frequency, sample_rate as f32)); - let iq = input_test - .iter() - .map(|x| iq_sampler.sample(*x)) - .collect::>(); - - let mut nco_pos = Nco::new(hz_to_rad_per_sample(deviation, sample_rate as f32)); - let mut nco_neg = Nco::new(hz_to_rad_per_sample(-deviation, sample_rate as f32)); - let corellator_pos = (0..(sample_per_symbol * 1)) - .map(|_| { - nco_pos.step(); - nco_pos.cexp() - }) - .collect::>(); - let corellator_neg = (0..(sample_per_symbol * 1)) - .map(|_| { - nco_neg.step(); - nco_neg.cexp() - }) - .collect::>(); - - let mut matched_filter_pos = FIRFilter::new(&corellator_pos); - let mut matched_filter_neg = FIRFilter::new(&corellator_neg); - - let mut dem = vec![]; - - let mut loop_filter = FIRFilter::new(&[Complex32::new(1., 0.); 1]); - for x in &iq { - let pos = matched_filter_pos.next(x.clone()); - let neg = matched_filter_neg.next(*x); - dem.push( - loop_filter - .next(Complex32::new(pos.mag() - neg.mag(), 0.)) - .re, - ); - } - - // Symbol recovery - - let mut sample_ids = vec![]; - let delta = 0.5; - let alpha = 0.01; - let mut current_sps = sample_per_symbol as f32; - let mut current_position = current_sps / 2.; - - let mut eye_diagram = vec![]; - while current_position < dem.len() as f32 { - // Sample before after - let early_id = (current_position - (delta * current_sps)).max(0.).floor() as u32; - let late_id = (current_position + (delta * current_sps)).max(0.).floor() as u32; - if late_id as usize >= dem.len() { - break; - } - let early = dem[early_id as usize]; - let late = dem[late_id as usize]; - let error = early * early - late * late; - current_sps -= alpha * error; - - sample_ids.push(current_position); - - let eye = ((current_position - current_sps).max(0.) as usize - ..(current_position + current_sps).min(sample_count as f32) as usize) - .map(|i| dem[i]) - .collect(); - - eye_diagram.push(eye); - - current_position += current_sps; - } - - let elg_sampling = sample_ids - .iter() - .map(|x| *x / sample_count as f32) - .collect(); - - Self { - samples: input_test.clone(), - iq, - dem, - vlines, - elg_sampling, - eye_diagram, - } - } -} - -impl eframe::App for EguiApp { - fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { - egui::CentralPanel::default().show(ctx, |ui| { - ui.heading("Hello World!"); - Plot::new("Fourrier transform") - .legend(Legend::default()) - .show(ui, |plot_ui| { - for (i, eye) in self.eye_diagram.iter().enumerate().skip(1) { - plot_ui.line( - Line::new( - "eye", - eye.iter() - .enumerate() - .map(|(i, x)| [i as f64 / eye.len() as f64, *x as f64]) - .collect::>(), - ) - .width(1.) - .color(Color32::RED), - ); - } - - self.vlines.iter().for_each(|l| { - plot_ui.vline( - VLine::new("Boundaries", *l as f64) - .color(Color32::LIGHT_BLUE) - .width(0.5) - .style(LineStyle::dashed_dense()), - ); - }); - - self.elg_sampling.iter().for_each(|l| { - plot_ui.vline( - VLine::new("ELG", *l as f64) - .color(Color32::RED) - .width(0.5) - .style(LineStyle::dotted_dense()), - ); - }); - - plot_ui.line( - Line::new( - "Passband", - self.samples - .iter() - .enumerate() - .map(|(i, x)| [i as f64 / self.samples.len() as f64, *x as f64]) - .collect::>(), - ) - .width(2.), - ); - - plot_ui.line( - Line::new( - "Demodulated", - self.dem - .iter() - .enumerate() - .map(|(i, x)| [i as f64 / self.iq.len() as f64, *x as f64]) - .collect::>(), - ) - .width(2.), - ); - }) - }); - } -} - -fn demodulate() { - let mut reader = hound::WavReader::open("audio/modulated.wav").unwrap(); - let samples = reader.samples::(); - - // Modulation parameters - let frequency = 1700.; - let deviation = 500.; - - // Data parameters - let sample_rate = 48000; - let baud_rate = BAUD_RATE; - let sample_per_symbol = sample_rate / baud_rate; - - let mut iq_sampler = IQSampler::new(hz_to_rad_per_sample(frequency, sample_rate as f32)); - let iq = samples - .map(|x| iq_sampler.sample(x.unwrap() as f32 / i16::MAX as f32)) - .collect::>(); - - let mut nco_pos = Nco::new(hz_to_rad_per_sample(deviation, sample_rate as f32)); - let mut nco_neg = Nco::new(hz_to_rad_per_sample(-deviation, sample_rate as f32)); - let corellator_pos = (0..(sample_per_symbol * 1)) - .map(|_| { - nco_pos.step(); - nco_pos.cexp() - }) - .collect::>(); - let corellator_neg = (0..(sample_per_symbol * 1)) - .map(|_| { - nco_neg.step(); - nco_neg.cexp() - }) - .collect::>(); - - let mut matched_filter_pos = FIRFilter::new(&corellator_pos); - let mut matched_filter_neg = FIRFilter::new(&corellator_neg); - - let mut dem = vec![]; - - for x in &iq { - let pos = matched_filter_pos.next(x.clone()); - let neg = matched_filter_neg.next(*x); - dem.push(pos.mag() - neg.mag()); - } - - // Symbol recovery - - let mut bits = vec![]; - let delta = 0.5; - let alpha = 0.01; - let mut current_sps = sample_per_symbol as f32; - let mut current_position = current_sps / 2.; - - while current_position < dem.len() as f32 { - // Sample before after - let early_id = (current_position - (delta * current_sps)).max(0.).floor() as u32; - let late_id = (current_position + (delta * current_sps)).max(0.).floor() as u32; - if late_id as usize >= dem.len() { - break; - } - let early = dem[early_id as usize]; - let late = dem[late_id as usize]; - let error = early * early - late * late; - current_sps -= alpha * error; - - bits.push(dem[current_position.floor() as usize] > 0.); - - current_position += current_sps; - } - - //assert!(bits.len() % 8 == 0); - - let mut out_file = File::create("out.txt").unwrap(); - let mut strip = 0; - - let bit_slice = bits.as_slice(); - for i in 0..100 { - let byte = bits_to_byte(&bit_slice[(i as usize)..(i as usize + 8)]); - if byte == 0b01010111u8 { - strip = i + 8; - } - } - - for i in 0..strip { - bits.remove(i as usize); - } - for x in bits.chunks(8) { - if x.len() != 8 { - break; - } - out_file.write_all(&[bits_to_byte(x)]).unwrap(); - } -} - -fn modulate() { - // Modulation parameters - let frequency = 1700.; - let deviation = 500.; - - // Data parameter - let sample_rate = 48000; - let baud_rate = BAUD_RATE; - - // File to modulate - let f = File::open("s.txt").unwrap(); - let mut bitstream = std::iter::repeat_n(0b01010101u8, 1) - .chain(std::iter::repeat_n(0b01010111u8, 1)) - .chain(f.bytes().map(|x| x.unwrap())) - .chain(std::iter::repeat_n(0u8, 1)) - .flat_map(byte_to_bits); - - let mut modulator = BFSKMod::new( - sample_rate / baud_rate, - units::frequency::hz_to_rad_per_sample(deviation, sample_rate as f32), - &mut bitstream, - ); - - let mut lo = Nco::new(units::frequency::hz_to_rad_per_sample( - frequency, - sample_rate as f32, - )); - - let spec = hound::WavSpec { - channels: 1, - sample_rate, - bits_per_sample: 16, - sample_format: hound::SampleFormat::Int, - }; - - let mut writer = hound::WavWriter::create("audio/modulated.wav", spec).unwrap(); - for (s, up) in modulator.zip(lo) { - let sample = (s * up).re; // Project to I coords - let amplitude = i16::MAX as f32; - writer.write_sample((sample * amplitude) as i16).unwrap(); - } - writer.finalize().unwrap(); -} - -fn byte_to_bits(byte: u8) -> Vec { - vec![ - byte & 1 == 1, - (byte >> 1) & 1 == 1, - (byte >> 2) & 1 == 1, - (byte >> 3) & 1 == 1, - (byte >> 4) & 1 == 1, - (byte >> 5) & 1 == 1, - (byte >> 6) & 1 == 1, - (byte >> 7) & 1 == 1, - ] -} - -/* -fn bits_to_byte(bits: &[bool]) -> u8 { - bits[7] as u8 | - bits[6] as u8 >> 1 | - bits[5] as u8 >> 2 | - bits[4] as u8 >> 3 | - bits[3] as u8 >> 4 | - bits[2] as u8 >> 5 | - bits[1] as u8 >> 6 | - bits[0] as u8 >> 7 -} -*/ - -fn bits_to_byte(bits: &[bool]) -> u8 { - bits[0] as u8 - | (bits[1] as u8) << 1 - | (bits[2] as u8) << 2 - | (bits[3] as u8) << 3 - | (bits[4] as u8) << 4 - | (bits[5] as u8) << 5 - | (bits[6] as u8) << 6 - | (bits[7] as u8) << 7 -} diff --git a/src/bfsk.rs b/src/bfsk.rs index 2c562a0..67adf6f 100644 --- a/src/bfsk.rs +++ b/src/bfsk.rs @@ -1,11 +1,8 @@ // 2-FSK Modulator -use std::f32::consts::PI; use crate::complex::{Complex, Complex32}; -use crate::fft::{self, FFT, FFTDirection}; -use crate::{units, windows}; -use crate::map; +use crate::windows; use crate::nco::Nco; pub struct BFSKMod<'a, T: Iterator> { diff --git a/src/complex.rs b/src/complex.rs index 2e4ed76..05b892c 100644 --- a/src/complex.rs +++ b/src/complex.rs @@ -2,7 +2,7 @@ use std::{ f32::consts::PI, fmt::Display, iter::Sum, - ops::{Add, Deref, Div, Mul, Neg, Sub}, + ops::{Add, Div, Mul, Neg, Sub}, }; #[derive(Copy, Clone, Debug)] diff --git a/src/fft/mixed_radix.rs b/src/fft/mixed_radix.rs index 60822c4..2193a1d 100644 --- a/src/fft/mixed_radix.rs +++ b/src/fft/mixed_radix.rs @@ -4,7 +4,7 @@ use std::f32::consts::PI; use crate::{ complex::Complex32, - fft::{DFTAlgorithm, FFTDirection, create_fft, dft::NaiveDFT, prime_factors, windows}, + fft::{DFTAlgorithm, FFTDirection, create_fft, prime_factors}, }; pub struct MixedRadixFFT { diff --git a/src/fft/rader.rs b/src/fft/rader.rs index cd5bd24..bcef75d 100644 --- a/src/fft/rader.rs +++ b/src/fft/rader.rs @@ -5,7 +5,7 @@ use std::f32::consts::PI; use crate::{ complex::Complex32, fft::{ - DFTAlgorithm, FFTDirection, create_fft, dft::NaiveDFT, is_prime, mixed_radix::MixedRadixFFT, + create_fft, is_prime, DFTAlgorithm, FFTDirection }, }; diff --git a/src/fft/rader2.rs b/src/fft/rader2.rs index 7a09942..de0a728 100644 --- a/src/fft/rader2.rs +++ b/src/fft/rader2.rs @@ -5,7 +5,7 @@ use std::f32::consts::PI; use crate::{ complex::Complex32, fft::{ - DFTAlgorithm, FFTDirection, create_fft, dft::NaiveDFT, is_prime, mixed_radix::MixedRadixFFT, + create_fft, is_prime, DFTAlgorithm, FFTDirection }, }; diff --git a/src/filtering/fir.rs b/src/filtering/fir.rs index fcc40fd..2acc36a 100644 --- a/src/filtering/fir.rs +++ b/src/filtering/fir.rs @@ -1,8 +1,7 @@ // Finite impulse response filters -use std::collections::VecDeque; - -use crate::{complex::Complex32, filtering::impulse_response}; +use std::{collections::VecDeque, f32::consts::PI}; +use crate::complex::Complex32; pub struct FIRFilter { size: usize, @@ -29,11 +28,24 @@ impl FIRFilter { } } - pub fn normalize_sum(&mut self) + // Normalizes FIR Filter such that DC is at unity gain + pub fn normalize_dc(&mut self) { self.normalization = self.impulse_response.iter().copied().sum::().mag() } + // Normalizes FIR Filter such that frequency is at unity gain + pub fn normalize_freq(&mut self, frequency: f32) + { + // Perform DTFT of specified frequency + let dft: Complex32 = self.impulse_response + .iter() + .enumerate() + .map(|(i, x)| *x * Complex32::cexp(- 2. * PI * i as f32 * frequency / self.impulse_response.len() as f32)) + .sum(); + self.normalization = dft.mag(); + } + pub fn next(&mut self, next: Complex32) -> Complex32 { let _ = self.taps.pop_front(); diff --git a/src/filtering/impulse_response.rs b/src/filtering/impulse_response.rs index d9ccb6d..2f1204a 100644 --- a/src/filtering/impulse_response.rs +++ b/src/filtering/impulse_response.rs @@ -1,7 +1,4 @@ // Utilities for impulse response design - -use crate::complex::Complex32; - pub mod design { use crate::fft::FFT; diff --git a/src/iq.rs b/src/iq.rs index e9cbc75..5da81ae 100644 --- a/src/iq.rs +++ b/src/iq.rs @@ -1,6 +1,5 @@ use std::f32::consts::PI; - -use crate::{complex::Complex32, filtering::fir::FIRFilter, map, nco::Nco, windows}; +use crate::{complex::Complex32, filtering::fir::FIRFilter, math::map, nco::Nco, windows}; pub struct IQSampler { @@ -28,8 +27,8 @@ impl IQSampler let ir = crate::filtering::impulse_response::design::ir_from_transfer_function(&transfer_function, fir_length, windows::blackmann); let mut low_pass_i = FIRFilter::new(&ir); let mut low_pass_q = FIRFilter::new(&ir); - low_pass_i.normalize_sum(); - low_pass_q.normalize_sum(); + low_pass_i.normalize_dc(); + low_pass_q.normalize_dc(); IQSampler { local_oscillator: Nco::new(center_freq), diff --git a/src/main.rs b/src/main.rs index f3c24ee..d847efb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,557 +1,107 @@ #![allow(dead_code)] -use std::{ - collections::VecDeque, - f32::consts::PI, - fs::File, - i16, - io::{self, Read, Write, stdout}, - ops::{Add, Div, Mul, Sub}, - os::unix::thread, - sync::{ - Arc, - atomic::{AtomicU32, Ordering}, - mpsc::{self, Receiver, Sender, TryRecvError, channel, sync_channel}, - }, - time::Duration, -}; - mod bfsk; mod complex; pub mod fft; mod filtering; mod iq; mod nco; -mod signal; mod units; mod windows; +mod ted; +mod math; -use bfsk::BFSKMod; -use complex::Complex; -use complex::Complex32; -use cpal::{ - SampleRate, - traits::{DeviceTrait, HostTrait, StreamTrait}, -}; -use fft::DFTAlgorithm; -use nco::Nco; +use cpal::Data; +use eframe::egui; -use eframe::{ - egui::{self, Color32, Context, Vec2b, debug_text::print, decode_animated_image_uri}, - glow::TOP_LEVEL_ARRAY_STRIDE, -}; -use egui_plot::{ - self, AxisHints, Bar, BarChart, HLine, Legend, Line, LineStyle, Plot, PlotPoints, VLine, -}; -use plotters::style::Color; -use rand::{Rng, seq::index::sample}; +const BAUD_RATE: u32 = 1000; -use crate::{ - bfsk::BFSKDem, - fft::{FFT, dft::NaiveDFT}, - filtering::{ - dc_block::DCBlocker, - fir::FIRFilter, - impulse_response::design::{self, frequency_response, ir_from_transfer_function}, - }, - iq::IQSampler, - units::frequency::{self, hz_to_rad_per_sample}, -}; - -// Utilities -fn map(input: T, in_min: T, in_max: T, out_min: T, out_max: T) -> T -where - T: Clone + Add + Mul + Sub + Div, +enum TransceiverState { - ((input - in_min.clone()) / (in_max - in_min)) * (out_max - out_min.clone()) + out_min + Idle, + Receiving, + Transmitting, +} + +enum Frame +{ + Data(Vec), + Ack +} + +impl Frame +{ + pub fn bytes(&self) -> Vec + { + let mut output_bytes = vec![]; + + // Initial training sequence + output_bytes.append(&mut vec![0b01010101; 64]); + + // Preamble byte + output_bytes.push(0xD8); + + // Command + match self + { + Frame::Data(x) => + { + let mut checksum = 0u8; + x.iter().for_each(|x| checksum ^= x); + + assert!(x.len() < 65536, "Data size over MTU"); + let len_u16 = x.len() as u16; + output_bytes.push((len_u16 & 0xFF).try_into().unwrap()); + output_bytes.push(((len_u16 >> 8) & 0xFF).try_into().unwrap()); + + output_bytes.extend(x.iter()); + + output_bytes.push(checksum); + } + Frame::Ack => + { + output_bytes.push(0xC4); + } + } + + // SEND EOT + output_bytes.extend(std::iter::repeat_n(4, 32)); + output_bytes + } } -const BAUD_RATE: u32 = 1200; fn main() { - //modulate(); - //demodulate(); - //return; - // Set up CPAL - let host = cpal::default_host(); - - let device = host - .default_input_device() - .expect("No input device available"); - let mut config = device - .supported_input_configs() - .unwrap() - .next() - .unwrap() - .with_sample_rate(SampleRate(48000)); - - // Channel to move samples from callback to main thread - let (tx, rx) = sync_channel::(4096); - - // Build input stream - let stream = device - .build_input_stream( - &config.into(), - move |data: &[f32], _| { - for x in data.iter() { - let _ = tx.send(*x * 30.); // non-blocking send - } - }, - move |err| eprintln!("Stream error: {}", err), - None, - ) - .unwrap(); - stream.play().unwrap(); - - let (eye_tx, eye_rx) = mpsc::channel::>(); - let (ctx_tx, ctx_rx) = mpsc::channel::>(); - std::thread::spawn(move || demodulator(rx, eye_tx, ctx_rx)); - - /* - std::thread::spawn(move || { - let spec = hound::WavSpec { - channels: 1, - sample_rate: 48000, - bits_per_sample: 16, - sample_format: hound::SampleFormat::Int, - }; - - let mut writer = hound::WavWriter::create("audio/noised.wav", spec).unwrap(); - - let mut reader = hound::WavReader::open("audio/modulated.wav").unwrap(); - let mut rand = rand::rng(); - let samples = reader.samples::(); - for x in samples { - let noise = rand.random::() * 2. - 1.; - let sample = x.unwrap() as f32 / i16::MAX as f32 + noise * 0.9; - let _ = tx.send(sample); - writer - .write_sample((sample * i16::MAX as f32) as i16) - .unwrap(); - std::thread::sleep(Duration::from_micros(21)); - } - writer.finalize().unwrap(); - }); - */ - let native_options = eframe::NativeOptions::default(); let _ = eframe::run_native( "Egui", native_options, - Box::new(|cc| Ok(Box::new(EguiApp::new(cc, eye_rx, ctx_tx)))), + Box::new(|cc| Ok(Box::new(EguiApp::new(cc)))), ); } -// Early late gate -struct ELGate { - samples_per_symbol: f32, - buffer: VecDeque, // Store baseband, matched filtered samples, - - loop_filter: FIRFilter, - delta: f32, - next_sample: f32, - current_position: f32, -} - -impl ELGate { - pub fn new(samples_per_symbol: f32, loop_filter: FIRFilter) -> Self { - Self { - samples_per_symbol, - loop_filter, - buffer: VecDeque::with_capacity(2 * samples_per_symbol.ceil() as usize), - delta: 0.5, - next_sample: samples_per_symbol, - current_position: 0., - } - } - - pub fn next(&mut self, sample: f32) -> Option { - Some(self.next_eye(sample)?.0) // Ignore eye - } - - pub fn next_eye(&mut self, sample: f32) -> Option<(f32, Vec)> { - self.buffer.push_front(sample); - self.current_position += 1.; - if self.current_position >= self.next_sample { - // Sample center, early late - let early_id = (self.samples_per_symbol / 2. + self.samples_per_symbol * self.delta) - .floor() - .min(self.buffer.len() as f32 - 1.) as usize; - let late_id = (self.samples_per_symbol / 2. - self.samples_per_symbol * self.delta) - .floor() - .max(0.) as usize; - let sample_id = (self.samples_per_symbol / 2.) as usize; - - let early_sample = self.buffer[early_id]; - let late_sample = self.buffer[late_id]; - let sample = self.buffer[sample_id]; - - let error = (early_sample - late_sample) * sample; - - // Remove until only current sample is in buffer (the next sample might need data from - // current sample if error advances sample position) - while self.buffer.len() > self.samples_per_symbol as usize { - let _ = self.buffer.pop_back(); - } - - // Predict next length based on error - self.current_position = self.next_sample - self.next_sample.floor(); - self.next_sample = self.samples_per_symbol - self.loop_filter.next_real(error); - - Some((sample, Vec::from(self.buffer.clone()))) - } else { - None - } - } -} - -fn demodulator( - rx: Receiver, - eye_sender: Sender>, - ctx_rx: Receiver>, -) { - // Wait for egui context to request redraw - let ctx = ctx_rx.recv().unwrap(); - - // Modulation parameters - let frequency = 1700.; - let deviation = 500.; - - // Data parameters - let sample_rate = 48000; - let baud_rate = BAUD_RATE; - let sample_per_symbol = sample_rate / baud_rate; - - let mut iq_sampler = IQSampler::new(hz_to_rad_per_sample(frequency, sample_rate as f32)); - - // Corellators - let mut nco_pos = Nco::new(hz_to_rad_per_sample(deviation, sample_rate as f32)); - let mut nco_neg = Nco::new(hz_to_rad_per_sample(-deviation, sample_rate as f32)); - let corellator_length = (sample_per_symbol as f32 * 1.5) as usize; - let corellator_pos = (0..corellator_length) - .map(|i| { - nco_pos.step(); - nco_pos.cexp() * windows::blackmann(i as f32 / (corellator_length as f32)) - //nco_pos.cexp() - }) - .collect::>(); - let corellator_neg = (0..corellator_length) - .map(|i| { - nco_neg.step(); - nco_neg.cexp() * windows::blackmann(i as f32 / (corellator_length as f32)) - //nco_neg.cexp() - }) - .collect::>(); - let mut matched_filter_pos = FIRFilter::new(&corellator_pos); - let mut matched_filter_neg = FIRFilter::new(&corellator_neg); - matched_filter_pos.normalize_sum(); - matched_filter_neg.normalize_sum(); - - let loop_p = 0.003; - let loop_i = 0.001; - let mut matched_filter = FIRFilter::new(&[Complex32::new(1., 0.); 20]); - matched_filter.normalize_sum(); - - let mut loop_filter_ir = vec![Complex32::new(loop_i, 0.); 30]; - let len = loop_filter_ir.len(); - //loop_filter_ir[0] = Complex32::new(loop_p + loop_i, 0.); - loop_filter_ir[len - 1] = Complex32::new(loop_p + loop_i, 0.); - - let mut loop_filter = FIRFilter::new(&loop_filter_ir); - //loop_filter.normalize_sum(); - let mut elg = ELGate::new(sample_per_symbol as f32, loop_filter); - - let mut dc_blocker = DCBlocker::new(0.999); - - // Timing recovery - let mut preamble_count = 0; - let mut bit_index = 0; - let mut last = 0u8; - while let Ok(real_sample) = rx.recv() { - let iq = iq_sampler.sample(real_sample); - - // Perform corellation - let neg_energy = matched_filter_neg.next(iq); - let pos_energy = matched_filter_pos.next(iq); - - let matched = - matched_filter.next_real(dc_blocker.next_real(pos_energy.mag() - neg_energy.mag())); - - if let Some((elg_sample, eye)) = elg.next_eye(matched) { - let _ = eye_sender.send(eye); - ctx.request_repaint(); - - //last >>= 1; - //last |= (!bit as u8) << 7; - let bit = elg_sample > 0.; - if elg_sample * elg_sample > 0.005 { - last >>= 1; - last |= (bit as u8) << 7; - //last <<= 1; - //last |= ((bit) as u8); - - if preamble_count >= 2 { - bit_index += 1; - if bit_index >= 8 { - if last == 4 { - print!(" -- EOT"); - println!(); - preamble_count = 0; - bit_index = 0; - last = 0; - } else { - print!("{}", last as char); - bit_index = 0; - let _ = stdout().flush(); - } - } - } else if last == 0xD8 { - preamble_count += 1; - if preamble_count == 2 { - println!("Incoming: "); - } - } - } - } - } -} //#[derive(Default)] struct EguiApp { - eye_rx: Receiver>, - eyes: VecDeque>, } impl EguiApp { fn new( - cc: &eframe::CreationContext<'_>, - eye_rx: Receiver>, - ctx_tx: Sender>, + _cc: &eframe::CreationContext<'_>, ) -> Self { - ctx_tx.send(Arc::new(cc.egui_ctx.clone())).unwrap(); Self { - eye_rx, - eyes: VecDeque::new(), + } } } impl eframe::App for EguiApp { - fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { - egui::CentralPanel::default().show(ctx, |ui| { - let max_eyes = 100; - - while let Ok(eye) = self.eye_rx.try_recv() { - self.eyes.push_back(eye); - } - - while self.eyes.len() > max_eyes { - self.eyes.pop_front(); - } - - let axis_hints = AxisHints::new_x().min_thickness(2.); - - Plot::new("Eye") - .legend(Legend::default()) - .show_axes(Vec2b::TRUE) - .custom_x_axes(vec![axis_hints]) - .show(ui, |plot_ui| { - //plot_ui.set_auto_bounds(Vec2b { x: false, y: false }); - plot_ui.hline(HLine::new("", 0.).color(Color32::DARK_RED).width(2.)); - for eye in self.eyes.iter() { - let line = Line::new( - "Eye", - eye.iter() - .enumerate() - .map(|(i, x)| [i as f64, *x as f64]) - .collect::>(), - ) - .color(Color32::LIGHT_GREEN); - plot_ui.line(line); - } - }) + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |_ui| { }); } } -fn demodulate() { - let mut reader = hound::WavReader::open("audio/modulated.wav").unwrap(); - let samples = reader.samples::(); - - // Modulation parameters - let frequency = 1700.; - let deviation = 500.; - - // Data parameters - let sample_rate = 48000; - let baud_rate = BAUD_RATE; - let sample_per_symbol = sample_rate / baud_rate; - - let mut iq_sampler = IQSampler::new(hz_to_rad_per_sample(frequency, sample_rate as f32)); - let iq = samples - .map(|x| iq_sampler.sample(x.unwrap() as f32 / i16::MAX as f32)) - .collect::>(); - - let mut nco_pos = Nco::new(hz_to_rad_per_sample(deviation, sample_rate as f32)); - let mut nco_neg = Nco::new(hz_to_rad_per_sample(-deviation, sample_rate as f32)); - let corellator_pos = (0..(sample_per_symbol * 1)) - .map(|_| { - nco_pos.step(); - nco_pos.cexp() - }) - .collect::>(); - let corellator_neg = (0..(sample_per_symbol * 1)) - .map(|_| { - nco_neg.step(); - nco_neg.cexp() - }) - .collect::>(); - - let mut matched_filter_pos = FIRFilter::new(&corellator_pos); - let mut matched_filter_neg = FIRFilter::new(&corellator_neg); - - let mut dem = vec![]; - - for x in &iq { - let pos = matched_filter_pos.next(x.clone()); - let neg = matched_filter_neg.next(*x); - dem.push(pos.mag() - neg.mag()); - } - - // Symbol recovery - - let mut bits = vec![]; - let delta = 0.5; - let alpha = 0.01; - let mut current_sps = sample_per_symbol as f32; - let mut current_position = current_sps / 2.; - - while current_position < dem.len() as f32 { - // Sample before after - let early_id = (current_position - (delta * current_sps)).max(0.).floor() as u32; - let late_id = (current_position + (delta * current_sps)).max(0.).floor() as u32; - if late_id as usize >= dem.len() { - break; - } - let early = dem[early_id as usize]; - let late = dem[late_id as usize]; - let error = early * early - late * late; - current_sps -= alpha * error; - - bits.push(dem[current_position.floor() as usize] > 0.); - - current_position += current_sps; - } - - //assert!(bits.len() % 8 == 0); - - let mut out_file = File::create("out.txt").unwrap(); - let mut strip = 0; - - let bit_slice = bits.as_slice(); - for i in 0..100 { - let byte = bits_to_byte(&bit_slice[(i as usize)..(i as usize + 8)]); - if byte == 0b01010111u8 { - strip = i + 8; - } - } - - for i in 0..strip { - bits.remove(i as usize); - } - for x in bits.chunks(8) { - if x.len() != 8 { - break; - } - out_file.write_all(&[bits_to_byte(x)]).unwrap(); - } -} - -fn modulate() { - // Modulation parameters - let frequency = 1700.; - let deviation = 500.; - - // Data parameter - let sample_rate = 48000; - let baud_rate = BAUD_RATE; - - let host = cpal::default_host(); - let device = host.default_output_device().unwrap(); - let mut supported_configs_range = device.supported_output_configs().unwrap(); - let supported_config = supported_configs_range - .find(|config| { - config.sample_format() == cpal::SampleFormat::F32 - && config.min_sample_rate().0 <= 48000 - && config.max_sample_rate().0 >= 48000 - }) - .expect("Device does not support 48kHz f32 output"); - let config = supported_config - .with_sample_rate(cpal::SampleRate(48_000)) - .config(); - - loop { - let mut buffer = String::new(); - let stdin = io::stdin(); // We get `Stdin` here. - stdin.read_line(&mut buffer).unwrap(); - - for c in buffer.bytes() { - print!("{}", c as char); - } - println!(); - - // Construct payload - let mut bitstream = std::iter::repeat_n(0b01010101u8, 64) - .chain(std::iter::repeat_n(0xD8, 2)) - .chain(buffer.bytes()) - .chain(std::iter::repeat_n(4u8, 32)) - .flat_map(byte_to_bits); - - let mut modulator = BFSKMod::new( - sample_rate / baud_rate, - units::frequency::hz_to_rad_per_sample(deviation, sample_rate as f32), - &mut bitstream, - ); - - let mut lo = Nco::new(units::frequency::hz_to_rad_per_sample( - frequency, - sample_rate as f32, - )); - - // To send - let stream = modulator - .zip(lo) - .map(|(s, up)| (s * up).re) - .collect::>(); - - let sample_clock = Arc::new(AtomicU32::new(0)); - let (tx, rx) = channel::<()>(); - - let stream = device - .build_output_stream( - &config, - move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { - for d in data.iter_mut() { - if sample_clock.load(Ordering::Relaxed) as usize == stream.len() { - tx.send(()).unwrap(); - break; - } - *d = stream[sample_clock.fetch_add(1, Ordering::Relaxed) as usize] - } - }, - move |err| { - eprintln!("Stream error: {}", err); - }, - None, - ) - .unwrap(); - - stream.play().unwrap(); - let _ = rx.recv(); - stream.pause().unwrap(); - } -} - fn byte_to_bits(byte: u8) -> Vec { vec![ byte & 1 == 1, @@ -565,19 +115,6 @@ fn byte_to_bits(byte: u8) -> Vec { ] } -/* -fn bits_to_byte(bits: &[bool]) -> u8 { - bits[7] as u8 | - bits[6] as u8 >> 1 | - bits[5] as u8 >> 2 | - bits[4] as u8 >> 3 | - bits[3] as u8 >> 4 | - bits[2] as u8 >> 5 | - bits[1] as u8 >> 6 | - bits[0] as u8 >> 7 -} -*/ - fn bits_to_byte(bits: &[bool]) -> u8 { bits[0] as u8 | (bits[1] as u8) << 1 diff --git a/src/math.rs b/src/math.rs new file mode 100644 index 0000000..f64a06b --- /dev/null +++ b/src/math.rs @@ -0,0 +1,9 @@ +use std::ops::{Add, Mul, Sub, Div}; + +// Useful math used throughout +pub fn map(input: T, in_min: T, in_max: T, out_min: T, out_max: T) -> T +where + T: Clone + Add + Mul + Sub + Div, +{ + ((input - in_min.clone()) / (in_max - in_min)) * (out_max - out_min.clone()) + out_min +} diff --git a/src/nco.rs b/src/nco.rs index 7ad39f4..16c4a12 100644 --- a/src/nco.rs +++ b/src/nco.rs @@ -67,7 +67,6 @@ impl Nco { } pub fn step(&mut self) { - let bef = self.theta as i64; self.theta = self.theta.overflowing_add(self.dtheta).0; } diff --git a/src/signal.rs b/src/signal.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/ted.rs b/src/ted.rs new file mode 100644 index 0000000..36dd008 --- /dev/null +++ b/src/ted.rs @@ -0,0 +1 @@ +pub mod elg; diff --git a/src/ted/elg.rs b/src/ted/elg.rs new file mode 100644 index 0000000..599c6be --- /dev/null +++ b/src/ted/elg.rs @@ -0,0 +1,66 @@ +use std::collections::VecDeque; +use crate::filtering::fir::FIRFilter; + +// Crued Early late gate timing error detector +pub struct ELGate { + samples_per_symbol: f32, + buffer: VecDeque, // Store baseband, matched filtered samples, + + loop_filter: FIRFilter, + delta: f32, + next_sample: f32, + current_position: f32, +} + +impl ELGate { + pub fn new(samples_per_symbol: f32, loop_filter: FIRFilter) -> Self { + Self { + samples_per_symbol, + loop_filter, + buffer: VecDeque::with_capacity(2 * samples_per_symbol.ceil() as usize), + delta: 0.5, + next_sample: samples_per_symbol, + current_position: 0., + } + } + + pub fn next(&mut self, sample: f32) -> Option { + Some(self.next_eye(sample)?.0) // Ignore eye + } + + pub fn next_eye(&mut self, sample: f32) -> Option<(f32, Vec)> { + self.buffer.push_front(sample); + self.current_position += 1.; + if self.current_position >= self.next_sample { + // Sample center, early late + let early_id = (self.samples_per_symbol / 2. + self.samples_per_symbol * self.delta) + .floor() + .min(self.buffer.len() as f32 - 1.) as usize; + let late_id = (self.samples_per_symbol / 2. - self.samples_per_symbol * self.delta) + .floor() + .max(0.) as usize; + let sample_id = (self.samples_per_symbol / 2.) as usize; + + let early_sample = self.buffer[early_id]; + let late_sample = self.buffer[late_id]; + let sample = self.buffer[sample_id]; + + let error = (early_sample - late_sample) * sample; + + // Remove until only current sample is in buffer (the next sample might need data from + // current sample if error advances sample position) + while self.buffer.len() > self.samples_per_symbol as usize { + let _ = self.buffer.pop_back(); + } + + // Predict next length based on error + self.current_position = self.next_sample - self.next_sample.floor(); + self.next_sample = self.samples_per_symbol - self.loop_filter.next_real(error); + + Some((sample, Vec::from(self.buffer.clone()))) + } else { + None + } + } +} +