From 942cc1fdf0d6460d6df8f7847250b888bd92c7df Mon Sep 17 00:00:00 2001 From: Albin Chaboissier Date: Thu, 2 Oct 2025 15:01:04 +0200 Subject: [PATCH] ELG Gate kind of working --- Cargo.lock | 244 +++++++++++++++++++++- Cargo.toml | 2 + main.rs.mt | 424 +++++++++++++++++++++++++++++++++++++ out.txt | 9 +- s.txt | 1 - src/filtering/fir.rs | 4 + src/main.rs | 351 ++++++++++++++++--------------- src/main.rs.old | 484 +++++++++++++++++++++++++++++++++++++++++++ src/signal.rs | 0 9 files changed, 1342 insertions(+), 177 deletions(-) create mode 100644 main.rs.mt create mode 100644 src/main.rs.old create mode 100644 src/signal.rs diff --git a/Cargo.lock b/Cargo.lock index 0beefa2..1620e7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -127,6 +127,28 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "alsa" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +dependencies = [ + "alsa-sys", + "bitflags 2.9.4", + "cfg-if", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "android-activity" version = "0.6.0" @@ -207,7 +229,7 @@ version = "0.38.0+1.3.281" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" dependencies = [ - "libloading", + "libloading 0.8.9", ] [[package]] @@ -690,6 +712,47 @@ dependencies = [ "libc", ] +[[package]] +name = "coreaudio-rs" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17" +dependencies = [ + "bitflags 1.3.2", + "libc", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", +] + +[[package]] +name = "cpal" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd307f43cc2a697e2d1f8bc7a1d824b5269e052209e28883e5bc04d095aaa3f" +dependencies = [ + "alsa", + "coreaudio-rs", + "dasp_sample", + "jack", + "jni", + "js-sys", + "libc", + "mach2", + "ndk", + "ndk-context", + "num-derive", + "num-traits", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.54.0", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -717,6 +780,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + [[package]] name = "dirs" version = "6.0.0" @@ -771,7 +840,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "libloading", + "libloading 0.8.9", ] [[package]] @@ -1313,7 +1382,7 @@ dependencies = [ "glutin_egl_sys", "glutin_glx_sys", "glutin_wgl_sys", - "libloading", + "libloading 0.8.9", "objc2 0.6.2", "objc2-app-kit 0.3.1", "objc2-core-foundation", @@ -1642,6 +1711,33 @@ dependencies = [ "hashbrown 0.16.0", ] +[[package]] +name = "jack" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f70ca699f44c04a32d419fc9ed699aaea89657fc09014bf3fa238e91d13041b9" +dependencies = [ + "bitflags 2.9.4", + "jack-sys", + "lazy_static", + "libc", + "log", +] + +[[package]] +name = "jack-sys" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6013b7619b95a22b576dfb43296faa4ecbe40abbdb97dfd22ead520775fc86ab" +dependencies = [ + "bitflags 1.3.2", + "lazy_static", + "libc", + "libloading 0.7.4", + "log", + "pkg-config", +] + [[package]] name = "jni" version = "0.21.1" @@ -1697,7 +1793,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" dependencies = [ "libc", - "libloading", + "libloading 0.8.9", "pkg-config", ] @@ -1719,6 +1815,16 @@ version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + [[package]] name = "libloading" version = "0.8.9" @@ -1786,6 +1892,15 @@ version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -1937,6 +2052,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2032,6 +2158,21 @@ dependencies = [ "objc2-foundation 0.3.1", ] +[[package]] +name = "objc2-audio-toolbox" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cbe18d879e20a4aea544f8befe38bcf52255eb63d3f23eca2842f3319e4c07" +dependencies = [ + "bitflags 2.9.4", + "libc", + "objc2 0.6.2", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation 0.3.1", +] + [[package]] name = "objc2-cloud-kit" version = "0.2.2" @@ -2056,6 +2197,28 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-core-audio" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca44961e888e19313b808f23497073e3f6b3c22bb485056674c8b49f3b025c82" +dependencies = [ + "dispatch2", + "objc2 0.6.2", + "objc2-core-audio-types", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-core-audio-types" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f1cc99bb07ad2ddb6527ddf83db6a15271bb036b3eb94b801cd44fdc666ee1" +dependencies = [ + "bitflags 2.9.4", + "objc2 0.6.2", +] + [[package]] name = "objc2-core-data" version = "0.2.2" @@ -2508,6 +2671,15 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "presser" version = "0.3.1" @@ -2587,6 +2759,35 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + [[package]] name = "range-alloc" version = "0.1.4" @@ -2603,10 +2804,12 @@ checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" name = "rdsp" version = "0.1.0" dependencies = [ + "cpal", "eframe", "egui_plot", "hound", "plotters", + "rand", ] [[package]] @@ -3550,7 +3753,7 @@ dependencies = [ "js-sys", "khronos-egl", "libc", - "libloading", + "libloading 0.8.9", "log", "metal", "naga", @@ -3617,6 +3820,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core 0.54.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.58.0" @@ -3649,6 +3862,16 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.58.0" @@ -3765,6 +3988,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-result" version = "0.2.0" @@ -4162,7 +4394,7 @@ dependencies = [ "as-raw-xcb-connection", "gethostname", "libc", - "libloading", + "libloading 0.8.9", "once_cell", "rustix 1.1.2", "x11rb-protocol", diff --git a/Cargo.toml b/Cargo.toml index 93223f3..781c5ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,9 @@ version = "0.1.0" edition = "2024" [dependencies] +cpal = { version = "0.16.0", features = ["jack"] } eframe = "0.32.3" egui_plot = "0.33.0" hound = "3.5.1" plotters = "0.3.7" +rand = "0.9.2" diff --git a/main.rs.mt b/main.rs.mt new file mode 100644 index 0000000..c89ab7f --- /dev/null +++ b/main.rs.mt @@ -0,0 +1,424 @@ +#![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/out.txt b/out.txt index 6fa8fb5..c7a2888 100644 --- a/out.txt +++ b/out.txt @@ -1,8 +1,3 @@ -!âhbidi Toilet[1],[2] est une web-série[3] machinima, créée par Alexey Gerasimov et diffusée sur sa chaîne YouTube DaFuq!?Boom! Produite à l'aide de Source Filmmaker, la série suit une guerre fictive entre des toilettes à tête humaine et des personnages humanoïdes dotés d'appareils électroniques à la place de la tête. - - -Après la publication du premier court métrage en février 2023, Skibidi Toilet devient un mème Internet viral sur divers réseaux sociaux, en particulier au sein de la génération Alpha. Les critiques ont vu dans cette série la première incursion de la génération Alpha dans la culture Internet, en concurrence avec la génération Z, plus âgée. - -Synopsis -Skibidi Toilet raconte les événements d'une guerre fictive entre des hommes à tête de caméra, de haut-parleur ou de télévision, et les Skibidi Toilets, une race extraterrestre prenant l’apparence de toilettes avec des têtes d'hommes ou de femmes et n'ayant que comme langage le « Skibidi ». Dirigés par le G-Toilet (personnage inspiré du G-Man dans la série de jeux vidéos Half-Life[4]), ils ont comme ambition de conquérir la Terre, en éliminant toute opposition[5],[6]. G-Toilet, qui est sous les ordres des Astro-Toilets, sa race d'origine qui se trouve dans l'espace, doit conquérir la terre avec l'armée qu'il a créée sur Terre, finira par perdre sa confiance aux Astros dû à sa défaite à l'épisode 57 et à sa dispute à l'épisode 60 avec les deux soldats d'élite des Astros. À partir de l'épisode 74, les Astros ont commencé à envahir la Terre. Les Skibidi Toilets et l'alliance n'ont d'autre choix que de s'allier pour vaincre les Astros. +ÿibidi Toilet[1],[2] est une web-série[3] machinima, créée par Alexey Gerasimov et diffusée sur sa chaîne YouTube DaFuq!?Boom! Produite à l'aide de Source Filmmaker, la série suit une guerre fictive entre des toilettes à tête humaine et des personnages humanoïdes dotés d'appareils électroniques à la place de la tête. + … 8¹aÔ9¶0¸:1¶´±0º´77²:8¹²¶´29±·:9:¶áT:¹°³227³áT;¹´29™©µ4±4²4ª·4¶2:²2»´27::7¶aÔ¶2$7º29·2:»4¹06¹:9²4»2¹9¹áÔ¹²°:<¹·±´°:<27¸09º´±:¶´29°:¹²47²2¶0³áT·áT¹0º´77 68´0¦²91¹4º´¸º²977:»:²0·9±2:º2¹áT¹´2¶08¹²¶´aT¹24·±:¹¹´77²2¶0³áT·áT¹0º´77 68´0²0·9¶0±:6º:¹2$7º29·2:27±7·±:9¹2·±20»²1¶0³áT·áT¹0º´77-8¶º9aѳáÔ2…©<·7¸¹´9…©µ4±4²4ª·4¶2:¹°±77º2¶²9áT»áT·²¶27º9²“:·2³º29¹2³´1º4»227:¹2²²9´·¶¶²9aPºaUº2²2±°¶áT¹0²2´°:º¸09¶²:9·:²2ºáT¶áT»´¹´772:¶²9©µ4±4²4ª·4¶2º9:·2¹°±22<:¹0º29¹²9:¹28¹2·07:6qÀÌ08¸0¹2·±2²2º·4¶2:º²90»²1²²9ºaUº²9²´·¶¶²9·:²2³²¶¶²92:·“°¼07:¸º2±·¶¶2¶0·³°³2¶2áU©µ4±4²4á]¢4¹´³áÔ9¸09¶2£ª·4¶2:¸2¹¹77·°³24·9¸4¹áT²:£–¦07²0·9¶0¹áT¹´2²2µ²:<»4²áÔ·9¤06³¦4³²-š®4¶977:±·¶¶2°6±4º´77²2±7·¸ºáT¹49¶0ª29¹227áT¶´¶4·07:º·:º278¸·¹4º´7·­š.–-›.£ª·4¶2:¸º4²9:¹·º9¶²9792¹²9²²9 9:¹·ª·4¶2º9¹0¹°±2²“7¹´³4·2¸º4¹2:¹·:»2²0·9¶“²9¸°±2²·4:±7·¸ºáT¹49¶0º29¹20»²1¶“0¹¶áÔ2¸º“4601¹áÔáÔ2¹:9ª29¹2³4·4¹0¸09¸292¹2¹0±77³´0·±2°:< 9:¹·9²á]aP¹0²áT³°4º2aP¶“áT¸´¹7²2š2:aP¹0²´9¸:º2aP¶“áT¸´¹7²20»²1¶²9²²:<¹76²0º9²“áT¶4º2²²9 9:¹·9a@¸09º49²2¶“áT¸´¹7²2¶²9 9:¹·977:±·¶¶2·±áTaP27»0´49¶0ª29¹2¦²9©µ4±4²4ª·4¶2º92:¶“06¶´0·±2·“77:²“°::¹21´·4<¸º2²2¹“06¶´29¸·:9»°4·1¹2¶²9 9:¹·9 \ No newline at end of file diff --git a/s.txt b/s.txt index 6ca6c39..cde1a8b 100644 --- a/s.txt +++ b/s.txt @@ -1,6 +1,5 @@ Skibidi Toilet[1],[2] est une web-série[3] machinima, créée par Alexey Gerasimov et diffusée sur sa chaîne YouTube DaFuq!?Boom! Produite à l'aide de Source Filmmaker, la série suit une guerre fictive entre des toilettes à tête humaine et des personnages humanoïdes dotés d'appareils électroniques à la place de la tête. - Après la publication du premier court métrage en février 2023, Skibidi Toilet devient un mème Internet viral sur divers réseaux sociaux, en particulier au sein de la génération Alpha. Les critiques ont vu dans cette série la première incursion de la génération Alpha dans la culture Internet, en concurrence avec la génération Z, plus âgée. Synopsis diff --git a/src/filtering/fir.rs b/src/filtering/fir.rs index 3bafd17..e2ae6c9 100644 --- a/src/filtering/fir.rs +++ b/src/filtering/fir.rs @@ -42,4 +42,8 @@ impl FIRFilter { .sum::() / self.normalization } + + pub fn next_real(&mut self, next: f32) -> f32 { + self.next(Complex32::new(next, 0.)).re + } } diff --git a/src/main.rs b/src/main.rs index 10ce1b9..e06bbc4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,18 @@ #![allow(dead_code)] use std::{ + collections::VecDeque, f32::consts::PI, fs::File, i16, io::{Read, Write}, ops::{Add, Div, Mul, Sub}, + os::unix::thread, + sync::{ + Arc, + mpsc::{self, Receiver, Sender, TryRecvError, sync_channel}, + }, + time::Duration, }; mod bfsk; @@ -14,18 +21,24 @@ 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 cpal::{ + SampleRate, + traits::{DeviceTrait, HostTrait, StreamTrait}, +}; use fft::DFTAlgorithm; use nco::Nco; -use eframe::egui::{self, Color32, Context, debug_text::print}; +use eframe::egui::{self, Color32, Context, Vec2b, debug_text::print}; use egui_plot::{self, Bar, BarChart, Legend, Line, LineStyle, Plot, PlotPoints, VLine}; use plotters::style::Color; +use rand::Rng; use crate::{ bfsk::BFSKDem, @@ -45,138 +58,184 @@ where { ((input - in_min.clone()) / (in_max - in_min)) * (out_max - out_min.clone()) + out_min } +const BAUD_RATE: u32 = 1200; fn main() { modulate(); - println!("Demodulating"); - demodulate(); + //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::(1024); + + // Build input stream + let stream = device + .build_input_stream( + &config.into(), + move |data: &[i16], _| { + for x in data.iter() { + let _ = tx.send(*x as f32 / i16::MAX as f32); // 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 * 1.; + 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(); + }); + */ - return; let native_options = eframe::NativeOptions::default(); let _ = eframe::run_native( "Egui", native_options, - Box::new(|cc| Ok(Box::new(EguiApp::new(cc)))), + Box::new(|cc| Ok(Box::new(EguiApp::new(cc, eye_rx, ctx_tx)))), ); } -#[derive(Default)] -struct EguiApp { - samples: Vec, +fn demodulator( + rx: Receiver, + eye_sender: Sender>, + ctx_rx: Receiver>, +) { + // Wait for egui context to request redraw + let ctx = ctx_rx.recv().unwrap(); - iq: Vec, - dem: Vec, - vlines: Vec, - elg_sampling: Vec, - eye_diagram: Vec>, -} -const BAUD_RATE: u32 = 3200; + // Modulation parameters + let frequency = 1700.; + let deviation = 500.; -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::>(); + // Data parameters + let sample_rate = 48000; + let baud_rate = BAUD_RATE; + let sample_per_symbol = sample_rate / baud_rate; - // Modulation parameters - let frequency = 1700.; - let deviation = 500.; + let mut iq_sampler = IQSampler::new(hz_to_rad_per_sample(frequency, sample_rate as f32)); - // 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(); + // 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_pos = (0..sample_per_symbol) + .map(|_| { + nco_pos.step(); + nco_pos.cexp() + }) + .collect::>(); + let corellator_neg = (0..sample_per_symbol) + .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 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 elg_buffer = VecDeque::new(); + let mut sps = sample_per_symbol as f32; + let delta = 0.3; + let loop_p = 0.1; + let loop_i = 0.3; + let mut loop_filter = FIRFilter::new(&[Complex32::new(1., 0.); 40]); + let mut matched_filter = FIRFilter::new(&[Complex32::new(1., 0.); 70]); + let mut current_position = 0.; + let mut next_sample = (sps as f32) / 2.; - 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 dem = vec![]; + // Timing recovery + while let Ok(real_sample) = rx.recv() { + let iq = iq_sampler.sample(real_sample); - let mut matched_filter_pos = FIRFilter::new(&corellator_pos); - let mut matched_filter_neg = FIRFilter::new(&corellator_neg); + // Perform corellation + let neg_energy = matched_filter_neg.next(iq); + let pos_energy = matched_filter_pos.next(iq); - let mut dem = vec![]; + let matched = matched_filter.next_real(pos_energy.mag() - neg_energy.mag()); + //dem.push(matched); + elg_buffer.push_front(matched); + current_position += 1.; + if current_position >= next_sample + sps / 2. { + // Compute current error + let early_id = (((sps / 2.) + sps * delta) as usize).min(elg_buffer.len() - 1); + let late_id = (((sps / 2.) - sps * delta) as usize).max(0); + let error = + elg_buffer[(sps / 2.) as usize] * (elg_buffer[early_id] - elg_buffer[late_id]); - 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() { + next_sample += + sps - loop_p * error - loop_i * loop_filter.next(Complex32::new(error, 0.)).re; + while elg_buffer.len() > sps as usize { + elg_buffer.pop_back(); + } + let _ = eye_sender.send(Vec::from(elg_buffer.clone())); + //elg_buffer.clear(); + ctx.request_repaint(); + /* + if dem.len() > 10_000 { + let _ = eye_sender.send(dem.clone()); 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(); +//#[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 { - samples: input_test.clone(), - iq, - dem, - vlines, - elg_sampling, - eye_diagram, + eye_rx, + eyes: VecDeque::new(), } } } @@ -184,65 +243,31 @@ impl EguiApp { 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") + 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(); + } + + Plot::new("Eye") .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), - ); + //plot_ui.set_auto_bounds(Vec2b { x: false, y: false }); + 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); } - - 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.), - ); }) }); } diff --git a/src/main.rs.old b/src/main.rs.old new file mode 100644 index 0000000..60a1972 --- /dev/null +++ b/src/main.rs.old @@ -0,0 +1,484 @@ +#![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, Vec2, Vec2b, 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, + elg_late: Vec, + elg_eary: Vec, + elg_error: Vec, + + eye_diagram: Vec>, +} +const BAUD_RATE: u32 = 1200; + +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 mut early_ids = vec![]; + let mut late_ids = vec![]; + let mut error_vals = vec![]; + let delta = 0.4; + let alpha = 0.1; + let mut filter = FIRFilter::new(&vec![Complex32::new(1., 0.); 10]); + let mut current_sps = sample_per_symbol as f32; + let mut current_position = current_sps / 2. + 23.; + + 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 - late; + let filtered_error = filter.next(Complex32::new(error, 0.0)).re; + //current_sps -= alpha * error; + + //error_vals.push(filtered_error); + error_vals.push(error); + sample_ids.push(current_position); + early_ids.push(current_position - delta * current_sps); + late_ids.push(current_position + delta * current_sps); + + /* + */ + let eye = ((current_position - current_sps / 2.).max(0.) as usize + ..(current_position + current_sps / 2.).min(sample_count as f32) as usize) + .map(|i| dem[i]) + .collect(); + + eye_diagram.push(eye); + + //current_position += current_sps; + current_position += + current_sps - current_sps * alpha * error - current_sps * 0.05 * filtered_error; + } + + 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, + elg_error: error_vals, + + elg_late: late_ids.iter().map(|x| *x / sample_count as f32).collect(), + elg_eary: early_ids.iter().map(|x| *x / sample_count as f32).collect(), + } + } +} + +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") + .show_grid(Vec2b::FALSE) + .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::WHITE) + .width(0.5) + .style(LineStyle::dotted_dense()), + ); + }); + + /* + self.elg_late.iter().for_each(|l| { + plot_ui.vline( + VLine::new("LATE", *l as f64) + .color(Color32::BLUE) + .width(1.5) + .style(LineStyle::dotted_dense()), + ); + }); + self.elg_eary.iter().for_each(|l| { + plot_ui.vline( + VLine::new("EARLY", *l as f64) + .color(Color32::RED) + .width(1.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.), + ); + + plot_ui.line( + Line::new( + "Error", + self.elg_error + .iter() + .enumerate() + .map(|(i, x)| { + [i as f64 / self.elg_error.len() as f64, 100. * *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/signal.rs b/src/signal.rs new file mode 100644 index 0000000..e69de29