#[cfg(any(feature = "mod", feature = "demod"))]
use crate::error::ConfigError;
#[cfg(feature = "mod")]
use crate::types::sine_at;
#[cfg(any(feature = "mod", feature = "demod"))]
use crate::types::{BaudRate, Bit, SampleRate};
#[cfg(feature = "mod")]
const AMPLITUDE: i32 = 32_767;
#[cfg(feature = "mod")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BasebandModulator {
whole_per_bit: u32,
rem_per_bit: u32,
baud: u32,
rem_acc: u32,
pending_level: i32,
have_pending: bool,
pending_in_trans: bool,
level: i32,
first_shaped: bool,
second_shaped: bool,
ph: u32,
ph_step: u32,
remaining: u32,
}
#[cfg(feature = "mod")]
impl BasebandModulator {
pub const fn new(sample_rate: SampleRate, baud: BaudRate) -> Result<Self, ConfigError> {
let sr = sample_rate.hz();
let bd = baud.bps();
if sr / bd < 2 {
return Err(ConfigError::BaudExceedsSampleRate {
baud: bd,
sample_rate: sr,
});
}
Ok(Self {
whole_per_bit: sr / bd,
rem_per_bit: sr % bd,
baud: bd,
rem_acc: 0,
pending_level: 0,
have_pending: false,
pending_in_trans: false,
level: 0,
first_shaped: false,
second_shaped: false,
ph: 0,
ph_step: 0,
remaining: 0,
})
}
fn start_cell(&mut self, level: i32, first_shaped: bool, second_shaped: bool) {
self.rem_acc += self.rem_per_bit;
let extra = if self.rem_acc >= self.baud {
self.rem_acc -= self.baud;
1
} else {
0
};
let n = self.whole_per_bit + extra;
self.level = level;
self.first_shaped = first_shaped;
self.second_shaped = second_shaped;
self.ph = 0;
self.ph_step = ((1u64 << 32) / n as u64) as u32;
self.remaining = n;
}
pub fn feed(&mut self, bit: Bit) {
let new_level = match bit {
Bit::One => 1,
Bit::Zero => -1,
};
if self.have_pending {
let out_trans = self.pending_level != new_level;
let (level, in_trans) = (self.pending_level, self.pending_in_trans);
self.start_cell(level, in_trans, out_trans);
self.pending_in_trans = out_trans;
} else {
self.have_pending = true;
self.pending_in_trans = false;
}
self.pending_level = new_level;
}
pub fn finish(&mut self) {
if self.have_pending {
let (level, in_trans) = (self.pending_level, self.pending_in_trans);
self.start_cell(level, in_trans, false);
self.have_pending = false;
self.pending_in_trans = false;
}
}
fn next_raw(&mut self) -> Option<i32> {
if self.remaining == 0 {
return None;
}
self.remaining -= 1;
let in_first_half = self.ph < (1 << 31);
let shaped = if in_first_half {
self.first_shaped
} else {
self.second_shaped
};
let magnitude = if shaped {
sine_at(self.ph >> 1) as i32
} else {
AMPLITUDE
};
self.ph = self.ph.wrapping_add(self.ph_step);
Some(self.level * magnitude)
}
pub fn next_i16(&mut self) -> Option<i16> {
self.next_raw()
.map(|v| v.clamp(i16::MIN as i32, i16::MAX as i32) as i16)
}
pub fn next_f32(&mut self) -> Option<f32> {
self.next_raw().map(|v| v as f32 / AMPLITUDE as f32)
}
pub fn i16_samples<I>(self, bits: I) -> BasebandI16Samples<I>
where
I: Iterator<Item = Bit>,
{
BasebandI16Samples {
modulator: self,
bits,
flushed: false,
}
}
pub fn f32_samples<I>(self, bits: I) -> BasebandF32Samples<I>
where
I: Iterator<Item = Bit>,
{
BasebandF32Samples {
modulator: self,
bits,
flushed: false,
}
}
}
#[cfg(feature = "mod")]
#[derive(Debug, Clone)]
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct BasebandI16Samples<I> {
modulator: BasebandModulator,
bits: I,
flushed: bool,
}
#[cfg(feature = "mod")]
impl<I> Iterator for BasebandI16Samples<I>
where
I: Iterator<Item = Bit>,
{
type Item = i16;
fn next(&mut self) -> Option<i16> {
loop {
if let Some(sample) = self.modulator.next_i16() {
return Some(sample);
}
match self.bits.next() {
Some(bit) => self.modulator.feed(bit),
None => {
if self.flushed {
return None;
}
self.flushed = true;
self.modulator.finish();
}
}
}
}
}
#[cfg(feature = "mod")]
#[derive(Debug, Clone)]
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct BasebandF32Samples<I> {
modulator: BasebandModulator,
bits: I,
flushed: bool,
}
#[cfg(feature = "mod")]
impl<I> Iterator for BasebandF32Samples<I>
where
I: Iterator<Item = Bit>,
{
type Item = f32;
fn next(&mut self) -> Option<f32> {
loop {
if let Some(sample) = self.modulator.next_f32() {
return Some(sample);
}
match self.bits.next() {
Some(bit) => self.modulator.feed(bit),
None => {
if self.flushed {
return None;
}
self.flushed = true;
self.modulator.finish();
}
}
}
}
}
#[cfg(feature = "demod")]
pub const MAX_FIR_TAPS: usize = 15;
#[cfg(feature = "demod")]
const TAP_UNITY: i32 = 1 << 15;
#[cfg(feature = "demod")]
const BASELINE_SHIFT: u32 = 9;
#[cfg(feature = "demod")]
const FIR_SPAN_BITS: u32 = 3;
#[cfg(feature = "demod")]
const FIR_CUTOFF_RATIO: f64 = 0.8;
#[cfg(feature = "demod")]
fn sin_taylor(x: f64) -> f64 {
const PI: f64 = core::f64::consts::PI;
let mut r = x % (2.0 * PI);
if r < 0.0 {
r += 2.0 * PI;
}
let r = if r <= 0.5 * PI {
r
} else if r <= 1.5 * PI {
PI - r
} else {
r - 2.0 * PI
};
let r2 = r * r;
r * (1.0
+ r2 * (-1.0 / 6.0
+ r2 * (1.0 / 120.0
+ r2 * (-1.0 / 5_040.0 + r2 * (1.0 / 362_880.0 + r2 * (-1.0 / 39_916_800.0))))))
}
#[cfg(feature = "demod")]
#[derive(Debug, Clone)]
pub struct BasebandDemodulator {
filter: BasebandFilter,
slicer: crate::slicer::Slicer,
}
#[cfg(feature = "demod")]
#[derive(Debug, Clone)]
pub(crate) struct BasebandFilter {
taps: [i32; MAX_FIR_TAPS],
history: [i32; MAX_FIR_TAPS],
taps_len: usize,
pos: usize,
baseline: i32,
amplitude: i32,
}
#[cfg(feature = "demod")]
impl BasebandFilter {
pub(crate) fn new(sample_rate: SampleRate, baud: BaudRate) -> Self {
let sr = sample_rate.hz();
let bd = baud.bps();
let spb = FIR_SPAN_BITS * ((sr + bd / 2) / bd);
let len = if spb.is_multiple_of(2) { spb + 1 } else { spb } as usize;
let len = len.clamp(3, MAX_FIR_TAPS);
let fc = FIR_CUTOFF_RATIO * bd as f64 / sr as f64;
let center = (len - 1) as f64 / 2.0;
let pi = core::f64::consts::PI;
let mut raw = [0.0f64; MAX_FIR_TAPS];
let mut sum = 0.0f64;
for (k, slot) in raw.iter_mut().enumerate().take(len) {
let t = k as f64 - center;
let x = 2.0 * pi * fc * t;
let sinc = if x.abs() < 1e-9 {
1.0
} else {
sin_taylor(x) / x
};
let window =
0.54 - 0.46 * sin_taylor(2.0 * pi * k as f64 / (len - 1) as f64 + pi / 2.0);
*slot = sinc * window;
sum += *slot;
}
let mut taps = [0i32; MAX_FIR_TAPS];
let mut acc = 0i32;
for (k, slot) in taps.iter_mut().enumerate().take(len) {
*slot = (raw[k] / sum * TAP_UNITY as f64 + 0.5) as i32;
acc += *slot;
}
if let Some(center_tap) = taps.get_mut(len / 2) {
*center_tap += TAP_UNITY - acc;
}
Self {
taps,
history: [0; MAX_FIR_TAPS],
taps_len: len,
pos: 0,
baseline: 0,
amplitude: 0,
}
}
pub(crate) fn push(&mut self, sample: i32) -> i32 {
if let Some(slot) = self.history.get_mut(self.pos) {
*slot = sample;
}
self.pos += 1;
if self.pos == self.taps_len {
self.pos = 0;
}
let len = self.taps_len;
let pos = self.pos;
let mut acc = 0i64;
let (hist_old, taps_head) = (
self.history.get(pos..len).unwrap_or(&[]),
self.taps.get(..len - pos).unwrap_or(&[]),
);
for (&h, &t) in hist_old.iter().zip(taps_head) {
acc += (h as i64) * (t as i64);
}
let (hist_new, taps_tail) = (
self.history.get(..pos).unwrap_or(&[]),
self.taps.get(len - pos..len).unwrap_or(&[]),
);
for (&h, &t) in hist_new.iter().zip(taps_tail) {
acc += (h as i64) * (t as i64);
}
let filtered = (acc >> 15) as i32;
let metric = filtered - self.baseline;
let sign = if metric >= 0 { 1 } else { -1 };
self.amplitude += (metric.abs() - self.amplitude) >> BASELINE_SHIFT;
let residual = filtered - sign * self.amplitude;
self.baseline += (residual - self.baseline) >> BASELINE_SHIFT;
metric
}
}
#[cfg(feature = "demod")]
impl BasebandDemodulator {
pub fn new(sample_rate: SampleRate, baud: BaudRate) -> Result<Self, ConfigError> {
let slicer = crate::slicer::Slicer::new(sample_rate, baud)?;
Ok(Self {
filter: BasebandFilter::new(sample_rate, baud),
slicer,
})
}
pub fn push_i16(&mut self, sample: i16) -> Option<Bit> {
let metric = self.filter.push(sample as i32);
self.slicer.push(metric)
}
pub fn push_f32(&mut self, sample: f32) -> Option<Bit> {
let scaled = (sample * 32_767.0).clamp(-32_768.0, 32_767.0) as i16;
self.push_i16(scaled)
}
}
#[cfg(all(test, feature = "mod"))]
mod tests {
extern crate std;
use super::*;
use std::vec::Vec;
fn rates(sr: u32, bd: u32) -> (SampleRate, BaudRate) {
(
SampleRate::new(sr).unwrap_or_else(|e| panic!("rate: {e}")),
BaudRate::new(bd).unwrap_or_else(|e| panic!("baud: {e}")),
)
}
fn modulator(sr: u32, bd: u32) -> BasebandModulator {
let (sr, bd) = rates(sr, bd);
BasebandModulator::new(sr, bd).unwrap_or_else(|e| panic!("config: {e}"))
}
#[test]
fn rejects_fewer_than_two_samples_per_bit() {
let (sr, bd) = rates(8_000, 9_600);
assert_eq!(
BasebandModulator::new(sr, bd).map(|_| ()),
Err(ConfigError::BaudExceedsSampleRate {
baud: 9_600,
sample_rate: 8_000
})
);
}
#[test]
fn steady_ones_are_flat_positive_full_scale() {
let v: Vec<i16> = modulator(48_000, 9_600)
.i16_samples(core::iter::repeat_n(Bit::One, 10))
.collect();
assert_eq!(v.len(), 50);
assert!(v.iter().all(|&s| s == 32_767), "{v:?}");
}
#[test]
fn steady_zeros_are_flat_negative_full_scale() {
let v: Vec<i16> = modulator(48_000, 9_600)
.i16_samples(core::iter::repeat_n(Bit::Zero, 10))
.collect();
assert!(v.iter().all(|&s| s == -32_767), "{v:?}");
}
#[test]
fn sample_count_exact_at_44100() {
let n = modulator(44_100, 9_600)
.i16_samples(core::iter::repeat_n(Bit::One, 9_600))
.count();
assert_eq!(n, 44_100);
}
#[test]
fn transition_crosses_zero_at_boundary_and_peaks_mid_cell() {
let bits = [
Bit::One,
Bit::One,
Bit::One,
Bit::Zero,
Bit::Zero,
Bit::Zero,
];
let v: Vec<i16> = modulator(48_000, 9_600)
.i16_samples(bits.into_iter())
.collect();
assert_eq!(v.len(), 30);
assert_eq!(v[15], 0);
for cell in 0..6 {
let mid = v[5 * cell + 2] as i32;
assert!(mid.abs() >= 31_000, "cell {cell}: {mid}");
}
for cell in 0..3 {
assert!(v[5 * cell + 2] > 0);
}
for cell in 3..6 {
assert!(v[5 * cell + 2] < 0);
}
}
#[test]
fn alternating_bits_are_a_smooth_half_baud_tone() {
let bits: Vec<Bit> = (0..40).map(|i| Bit::from(i % 2 == 0)).collect();
let v: Vec<i16> = modulator(48_000, 9_600)
.i16_samples(bits.into_iter())
.collect();
for w in v.windows(2) {
let step = (w[1] as i32 - w[0] as i32).abs();
assert!(step <= 19_800, "step {step}");
}
}
#[test]
fn i16_and_f32_paths_agree() {
let bits: Vec<Bit> = (0..32).map(|i| Bit::from(i % 3 == 0)).collect();
let vi: Vec<i16> = modulator(44_100, 9_600)
.i16_samples(bits.iter().copied())
.collect();
let vf: Vec<f32> = modulator(44_100, 9_600)
.f32_samples(bits.iter().copied())
.collect();
assert_eq!(vi.len(), vf.len());
for (a, b) in vi.iter().zip(vf.iter()) {
assert!((*a as f32 / 32_767.0 - b).abs() < 1e-4);
}
}
#[test]
fn manual_feed_finish_matches_iterator() {
let bits = [Bit::One, Bit::Zero, Bit::Zero, Bit::One];
let via_iter: Vec<i16> = modulator(44_100, 9_600)
.i16_samples(bits.iter().copied())
.collect();
let mut m = modulator(44_100, 9_600);
let mut manual = Vec::new();
for b in bits {
m.feed(b);
while let Some(s) = m.next_i16() {
manual.push(s);
}
}
m.finish();
while let Some(s) = m.next_i16() {
manual.push(s);
}
assert_eq!(via_iter, manual);
}
#[test]
fn no_samples_before_second_feed() {
let mut m = modulator(48_000, 9_600);
assert_eq!(m.next_i16(), None);
m.feed(Bit::One);
assert_eq!(m.next_i16(), None);
m.finish();
assert_eq!(m.next_i16(), Some(32_767));
}
#[cfg(feature = "demod")]
fn demodulator(sr: u32, bd: u32) -> BasebandDemodulator {
let (sr, bd) = rates(sr, bd);
BasebandDemodulator::new(sr, bd).unwrap_or_else(|e| panic!("config: {e}"))
}
#[cfg(feature = "demod")]
#[test]
fn fir_taps_sum_to_unity() {
for (sr, bd) in [(44_100, 9_600), (48_000, 9_600), (22_050, 9_600)] {
let d = demodulator(sr, bd);
let sum: i32 = d.filter.taps.iter().take(d.filter.taps_len).sum();
assert_eq!(sum, TAP_UNITY, "{sr}/{bd}");
assert!(d.filter.taps_len % 2 == 1);
let center = d.filter.taps[d.filter.taps_len / 2];
assert!(
d.filter
.taps
.iter()
.take(d.filter.taps_len)
.all(|&t| t <= center)
);
}
}
#[cfg(feature = "demod")]
#[test]
fn baseline_converges_onto_a_channel_dc_offset() {
const OFFSET: i32 = 4_000;
const SWING: i32 = 10_000;
let mut f = BasebandFilter::new(
SampleRate::new(48_000).unwrap_or_else(|e| panic!("{e}")),
BaudRate::new(9_600).unwrap_or_else(|e| panic!("{e}")),
);
let mut positives = 0i64;
let mut negatives = 0i64;
for i in 0..(16 << BASELINE_SHIFT) {
let symbol = if (i / 5) % 2 == 0 { SWING } else { -SWING };
let metric = f.push(OFFSET + symbol);
if i > (8 << BASELINE_SHIFT) {
if metric > 0 {
positives += 1;
} else {
negatives += 1;
}
}
}
assert!(
(f.baseline - OFFSET).abs() < SWING / 4,
"baseline {} did not converge onto the {OFFSET} offset",
f.baseline
);
let ratio = positives as f64 / (positives + negatives) as f64;
assert!(
(0.4..=0.6).contains(&ratio),
"metric biased: {ratio:.3} of samples positive"
);
}
#[cfg(feature = "demod")]
#[test]
fn loopback_recovers_bits_both_rates() {
for sr in [44_100u32, 48_000] {
let mut bits: Vec<Bit> = (0..32).map(|i| Bit::from(i % 2 == 0)).collect();
let payload: Vec<Bit> = (0..200).map(|i| Bit::from((i * 7) % 3 == 0)).collect();
bits.extend(payload.iter().copied());
let pcm: Vec<i16> = modulator(sr, 9_600)
.i16_samples(bits.iter().copied())
.collect();
let mut rx = demodulator(sr, 9_600);
let out: Vec<Bit> = pcm.iter().filter_map(|&s| rx.push_i16(s)).collect();
let target = &payload[8..];
let found = out.windows(target.len()).any(|w| w == target);
assert!(found, "rate {sr}: payload not recovered");
}
}
#[cfg(feature = "demod")]
#[test]
fn loopback_survives_dc_offset_and_attenuation() {
let mut bits: Vec<Bit> = (0..32).map(|i| Bit::from(i % 2 == 0)).collect();
let payload: Vec<Bit> = (0..150).map(|i| Bit::from((i * 5) % 4 < 2)).collect();
bits.extend(payload.iter().copied());
let pcm: Vec<i16> = modulator(44_100, 9_600)
.i16_samples(bits.iter().copied())
.collect();
let mut rx = demodulator(44_100, 9_600);
let out: Vec<Bit> = pcm
.iter()
.map(|&s| (s as i32 / 4 + 3_000).clamp(-32_768, 32_767) as i16)
.filter_map(|s| rx.push_i16(s))
.collect();
let target = &payload[8..];
let found = out.windows(target.len()).any(|w| w == target);
assert!(found, "payload not recovered under DC + attenuation");
}
#[cfg(feature = "demod")]
#[test]
fn f32_path_matches_i16_path() {
let bits: Vec<Bit> = (0..100).map(|i| Bit::from(i % 2 == 0)).collect();
let pcm_i: Vec<i16> = modulator(48_000, 9_600)
.i16_samples(bits.iter().copied())
.collect();
let mut rx_i = demodulator(48_000, 9_600);
let out_i: Vec<Bit> = pcm_i.iter().filter_map(|&s| rx_i.push_i16(s)).collect();
let pcm_f: Vec<f32> = modulator(48_000, 9_600)
.f32_samples(bits.iter().copied())
.collect();
let mut rx_f = demodulator(48_000, 9_600);
let out_f: Vec<Bit> = pcm_f.iter().filter_map(|&s| rx_f.push_f32(s)).collect();
assert_eq!(out_i, out_f);
}
}