GFSK Weirdness with bandpass at baseband

This commit is contained in:
2025-10-21 17:23:50 +02:00
parent 1b03b13e3c
commit 6caa6c7d82
3 changed files with 83 additions and 37 deletions

View File

@ -1,43 +1,44 @@
// Utilities for impulse response design
pub mod design
{
pub mod design {
use crate::complex::Complex32;
use crate::fft::FFT;
use crate::windows::{self, Window};
use crate::complex::Complex32;
// Completely stolen from sdrpp dsp code
pub fn estimate_fir_length(transition_width: f32, sample_rate: f32) -> f32 {
3.8 * sample_rate / transition_width
}
///Designs a impulse response from a desired transfer function using windowing technique
pub fn ir_from_transfer_function(transfer_function: &[Complex32], ir_length: usize, window: Window) -> Vec<Complex32>
{
pub fn ir_from_transfer_function(
transfer_function: &[Complex32],
window: Window,
) -> Vec<Complex32> {
let tf_len = transfer_function.len();
let mut ifft = FFT::new_inv(tf_len);
// Compute ideal convolution kernel/impulse response
ifft.execute(transfer_function);
// Shorten and window
let mut ir = vec![];
for i in 0..ir_length
{
for i in 0..tf_len {
// Get value within ifft result (centering/trimming)
let k = (tf_len - (ir_length / 2) + i) % tf_len;
let k = (tf_len - (tf_len / 2) + i) % tf_len;
// Windowing
ir.push(
ifft.get_output()[k] * window(i as f32 / ir_length as f32) / tf_len as f32
);
ir.push(ifft.get_output()[k] * window(i as f32 / tf_len as f32) / tf_len as f32);
}
ir
}
pub fn frequency_response(impulse_response: &[Complex32]) -> Vec<Complex32>
{
pub fn frequency_response(impulse_response: &[Complex32]) -> Vec<Complex32> {
let len = impulse_response.len();
let mut fft = FFT::new(len, windows::rectangular);
// Recenter impulse response
let mut centered_ir = vec![];
for i in 0..len
{
let mut centered_ir = vec![];
for i in 0..len {
let k = (len / 2) + i;
centered_ir.push(impulse_response[k % len]);
}

View File

@ -1,15 +1,13 @@
use std::f32::consts::PI;
use crate::{complex::Complex32, filtering::fir::FIRFilter, math::map, nco::Nco, windows};
use std::f32::consts::PI;
pub struct IQSampler
{
pub struct IQSampler {
local_oscillator: Nco,
low_pass_i: FIRFilter,
low_pass_q: FIRFilter,
}
impl IQSampler
{
impl IQSampler {
pub fn new(center_freq: f32) -> Self {
// Design a lowpass filter that cuts off at the center freq
// Estimate FIR length :
@ -17,14 +15,17 @@ impl IQSampler
// Ideal transfer function :
let mut transfer_function = vec![Complex32::zero(); fir_length];
let bin_id = map(center_freq, 0., PI, 0., transfer_function.len() as f32 / 2.).floor() as usize;
for i in 0..bin_id
{
let bin_id =
map(center_freq, 0., PI, 0., transfer_function.len() as f32 / 2.).floor() as usize;
for i in 0..bin_id {
transfer_function[i] = Complex32::new(1., 0.);
transfer_function[fir_length - 1 - i] = Complex32::new(1., 0.);
}
let ir = crate::filtering::impulse_response::design::ir_from_transfer_function(&transfer_function, fir_length, windows::blackmann);
let ir = crate::filtering::impulse_response::design::ir_from_transfer_function(
&transfer_function,
windows::blackmann,
);
let mut low_pass_i = FIRFilter::new(&ir);
let mut low_pass_q = FIRFilter::new(&ir);
low_pass_i.normalize_dc();
@ -33,12 +34,11 @@ impl IQSampler
IQSampler {
local_oscillator: Nco::new(center_freq),
low_pass_i,
low_pass_q
low_pass_q,
}
}
pub fn sample(&mut self, input_sample: f32) -> Complex32
{
pub fn sample(&mut self, input_sample: f32) -> Complex32 {
let i_mixed = self.local_oscillator.cexp().re * input_sample;
let q_mixed = self.local_oscillator.cexp().im * input_sample;
self.local_oscillator.step();
@ -46,8 +46,7 @@ impl IQSampler
// TODO: Could use one filter for both I and Q
Complex32::new(
self.low_pass_i.next(Complex32::new(i_mixed, 0.)).re,
self.low_pass_q.next(Complex32::new(q_mixed, 0.)).re
self.low_pass_q.next(Complex32::new(q_mixed, 0.)).re,
) * 2.
}
}

View File

@ -20,6 +20,7 @@ use std::{
cell::{Cell, RefCell},
collections::VecDeque,
env::{self, args},
f32::consts::PI,
fmt::Display,
fs::File,
io::{BufWriter, Read, Sink, Write, stdout},
@ -41,8 +42,16 @@ use tun_tap::Iface;
use crate::{
bfsk::BFSKMod,
complex::Complex32,
filtering::{dc_block::DCBlocker, fir::FIRFilter},
filtering::{
dc_block::DCBlocker,
fir::FIRFilter,
impulse_response::{
self,
design::{estimate_fir_length, frequency_response},
},
},
iq::IQSampler,
math::map,
nco::Nco,
squelch::Squelch,
ted::elg::ELGate,
@ -111,6 +120,7 @@ impl SampleSender for WavSampleSender {
struct FSKReceiver {
eye_sender: Sender<Vec<f32>>,
phase_lowpass: FIRFilter,
baseband_filter: FIRFilter,
elg: ELGate,
last_byte: u8,
frame_constructor: FrameConstructor,
@ -130,12 +140,35 @@ impl FSKReceiver {
let loop_i = 0.03;
let loop_p = 0.1;
let mut loop_ir = vec![Complex32::new(loop_i, 0.); samples_per_symbol as usize / 2];
let mut loop_ir = vec![Complex32::new(loop_i, 0.); samples_per_symbol as usize / 4];
loop_ir.push(Complex32::new(loop_p, 0.));
let elg = ELGate::new(samples_per_symbol, FIRFilter::new(&loop_ir));
// Baseband filter
let bbf_length = estimate_fir_length(500., SAMPLE_RATE as f32).floor() as usize;
let mut frequency_response = vec![Complex32::zero(); bbf_length].into_boxed_slice();
let cutoff_bin = map(
hz_to_rad_per_sample(DEVIATION + 100., SAMPLE_RATE as f32),
0.,
2. * PI,
0.,
bbf_length as f32,
)
.floor() as usize;
// Design transfer function
for i in 0..cutoff_bin {
frequency_response[i] = Complex32::new(1., 0.);
frequency_response[bbf_length - 1 - i] = Complex32::new(1., 0.);
}
Self {
//iq_sampler: IQSampler::new(hz_to_rad_per_sample(CENTER_FREQ, SAMPLE_RATE as f32)),
phase_lowpass,
baseband_filter: FIRFilter::new(&impulse_response::design::ir_from_transfer_function(
&frequency_response,
windows::blackmann,
)),
elg,
last_byte: 0x00u8,
frame_constructor: FrameConstructor::new(),
@ -147,10 +180,11 @@ impl FSKReceiver {
async fn receive(&mut self, iq: Complex32) -> Result<Option<Frame>, FrameConstructionError> {
// Frame reconstruction
let filtered_bb = self.baseband_filter.next(iq);
let dphi = self
.phase_lowpass
.next_real((self.last_sample * iq.conj()).arg());
self.last_sample = iq;
.next_real((self.last_sample * filtered_bb.conj()).arg());
self.last_sample = filtered_bb;
if let Some((bit_sample, eye)) = self.elg.next_eye(dphi) {
let _ = self.eye_sender.send(eye).await;
self.last_byte >>= 1;
@ -253,7 +287,13 @@ impl Transceiver {
{
match recv.as_mut().unwrap().receive(iq).await
{
Ok(Some(Frame::Data(dat))) => {rx_stream_sender.try_send(dat); println!("GOT DATA"); send_ack = false; recv = None; state_tx.try_send(TransceiverState::EOT);},
Ok(Some(Frame::Data(dat))) => {
println!("GOT DATA");
let _ = rx_stream_sender.try_send(dat);
send_ack = false;
recv = None;
state_tx.try_send(TransceiverState::EOT);
},
//Ok(Some(Frame::Ack)) => {current_message = None; recv = None; state_tx.try_send(TransceiverState::EOT);},
Err(()) => {recv = None;},
_ => {}
@ -460,6 +500,10 @@ impl Frame {
#[tokio::main]
async fn main() {
// Read instance
println!(
"fir length: {}",
impulse_response::design::estimate_fir_length(1000., 48000.)
);
let id = std::env::args().collect::<Vec<_>>()[1]
.parse::<u32>()
.expect("NO INPUT ID");
@ -579,7 +623,8 @@ impl EguiApp {
*d = stream[progression
.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
as usize];
as usize]
* 0.;
}
},
move |err| {
@ -611,6 +656,7 @@ impl eframe::App for EguiApp {
// INTERFACE
let mut frame = [0u8; 2000];
while let Ok(length) = self.iface.recv(&mut frame) {
break;
let _ = self
.transceiver
.get_sender()