use crate::error::ConfigError;
pub(crate) const TABLE_BITS: u32 = 12;
pub(crate) const TABLE_LEN: usize = 1 << TABLE_BITS;
pub(crate) const TABLE_MASK: usize = TABLE_LEN - 1;
pub(crate) static SINE_I16: [i16; TABLE_LEN] = build_sine_table();
const fn build_sine_table() -> [i16; TABLE_LEN] {
let mut table = [0i16; TABLE_LEN];
let mut i = 0;
while i < TABLE_LEN {
table[i] = sine_entry(i);
i += 1;
}
table
}
const fn sine_entry(i: usize) -> i16 {
const PI: f64 = core::f64::consts::PI;
let x = 2.0 * PI * (i as f64) / (TABLE_LEN as f64); let r = if x <= 0.5 * PI {
x
} else if x <= 1.5 * PI {
PI - x
} else {
x - 2.0 * PI
};
let r2 = r * r;
let s = 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))))));
let scaled = s * 32_767.0;
if scaled >= 0.0 {
(scaled + 0.5) as i16
} else {
(scaled - 0.5) as i16
}
}
pub(crate) fn sine_table_at(index: usize) -> i16 {
SINE_I16.get(index & TABLE_MASK).copied().unwrap_or(0)
}
pub(crate) fn sine_at(phase: u32) -> i16 {
let idx = (phase >> (32 - TABLE_BITS)) as usize & TABLE_MASK;
SINE_I16.get(idx).copied().unwrap_or(0)
}
pub(crate) fn sine_at_interpolated(phase: u32) -> i32 {
let index = (phase >> (32 - TABLE_BITS)) as usize & TABLE_MASK;
let next = (index + 1) & TABLE_MASK;
let a = i64::from(sine_table_at(index));
let b = i64::from(sine_table_at(next));
let fraction_bits = 32 - TABLE_BITS;
let fraction = i64::from(phase & ((1 << fraction_bits) - 1));
let value = a + (((b - a) * fraction + (1 << (fraction_bits - 1))) >> fraction_bits);
#[allow(clippy::cast_possible_truncation)]
{
value as i32
}
}
#[cfg(feature = "mod")]
pub(crate) fn sine_at_f32(phase: u32) -> f32 {
let idx = (phase >> (32 - TABLE_BITS)) as usize & TABLE_MASK;
let frac_bits = phase & ((1 << (32 - TABLE_BITS)) - 1);
let frac = frac_bits as f32 / (1u32 << (32 - TABLE_BITS)) as f32;
let a = SINE_I16.get(idx).copied().unwrap_or(0) as f32;
let b = SINE_I16.get((idx + 1) & TABLE_MASK).copied().unwrap_or(0) as f32;
(a + (b - a) * frac) / 32_767.0
}
pub(crate) const fn phase_increment(hz: u32, sample_rate: u32) -> u32 {
((((hz as u64) << 32) + (sample_rate as u64) / 2) / (sample_rate as u64)) as u32
}
pub const SAMPLE_RATE_MIN: u32 = 8_000;
pub const SAMPLE_RATE_MAX: u32 = 48_000;
pub const BAUD_MIN: u32 = 1;
pub const BAUD_MAX: u32 = 9_600;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SampleRate(u32);
impl SampleRate {
pub const fn new(hz: u32) -> Result<Self, ConfigError> {
if hz >= SAMPLE_RATE_MIN && hz <= SAMPLE_RATE_MAX {
Ok(Self(hz))
} else {
Err(ConfigError::SampleRateOutOfRange {
got: hz,
min: SAMPLE_RATE_MIN,
max: SAMPLE_RATE_MAX,
})
}
}
#[must_use]
pub const fn hz(self) -> u32 {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BaudRate(u32);
impl BaudRate {
pub const BELL_202: Self = Self(1_200);
pub const fn new(bps: u32) -> Result<Self, ConfigError> {
if bps >= BAUD_MIN && bps <= BAUD_MAX {
Ok(Self(bps))
} else {
Err(ConfigError::BaudRateInvalid {
got: bps,
min: BAUD_MIN,
max: BAUD_MAX,
})
}
}
#[must_use]
pub const fn bps(self) -> u32 {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TonePair {
mark: u32,
space: u32,
}
impl TonePair {
pub const BELL_202: Self = Self {
mark: 1_200,
space: 2_200,
};
pub const HF_APRS: Self = Self {
mark: 1_600,
space: 1_800,
};
pub const BELL_103_ORIGINATE: Self = Self {
mark: 1_270,
space: 1_070,
};
pub const BELL_103_ANSWER: Self = Self {
mark: 2_225,
space: 2_025,
};
pub const fn new(mark: u32, space: u32, sample_rate: SampleRate) -> Result<Self, ConfigError> {
let nyquist = sample_rate.hz() / 2;
if mark == 0 || mark >= nyquist {
return Err(ConfigError::ToneOutOfRange { got: mark, nyquist });
}
if space == 0 || space >= nyquist {
return Err(ConfigError::ToneOutOfRange {
got: space,
nyquist,
});
}
Ok(Self { mark, space })
}
#[must_use]
pub const fn mark_hz(self) -> u32 {
self.mark
}
#[must_use]
pub const fn space_hz(self) -> u32 {
self.space
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModulationScheme {
ToneAfsk,
ScrambledBaseband,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ModemProfile {
baud: BaudRate,
tones: TonePair,
scheme: ModulationScheme,
}
impl ModemProfile {
pub const BELL_202: Self = Self {
baud: BaudRate(1_200),
tones: TonePair::BELL_202,
scheme: ModulationScheme::ToneAfsk,
};
pub const HF_APRS_300: Self = Self {
baud: BaudRate(300),
tones: TonePair::HF_APRS,
scheme: ModulationScheme::ToneAfsk,
};
pub const BELL_103_ORIGINATE: Self = Self {
baud: BaudRate(300),
tones: TonePair::BELL_103_ORIGINATE,
scheme: ModulationScheme::ToneAfsk,
};
pub const BELL_103_ANSWER: Self = Self {
baud: BaudRate(300),
tones: TonePair::BELL_103_ANSWER,
scheme: ModulationScheme::ToneAfsk,
};
pub const BELL_103: Self = Self::BELL_103_ORIGINATE;
#[cfg(feature = "g3ruh")]
pub const G3RUH_9600: Self = Self {
baud: BaudRate(9_600),
tones: TonePair::BELL_202,
scheme: ModulationScheme::ScrambledBaseband,
};
#[must_use]
pub const fn new(baud: BaudRate, tones: TonePair) -> Self {
Self {
baud,
tones,
scheme: ModulationScheme::ToneAfsk,
}
}
#[must_use]
pub const fn baud(self) -> BaudRate {
self.baud
}
#[must_use]
pub const fn tones(self) -> TonePair {
self.tones
}
#[must_use]
pub const fn scheme(self) -> ModulationScheme {
self.scheme
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DevicePreset {
Esp32C3,
Esp32C3FullBank,
Esp32C6,
Esp32C6FullBank,
Esp32H2,
Esp32P4,
#[cfg(feature = "g3ruh")]
Esp32P4G3ruh,
}
impl DevicePreset {
pub const ALL: &'static [DevicePreset] = &[
DevicePreset::Esp32C3,
DevicePreset::Esp32C3FullBank,
DevicePreset::Esp32C6,
DevicePreset::Esp32C6FullBank,
DevicePreset::Esp32H2,
DevicePreset::Esp32P4,
#[cfg(feature = "g3ruh")]
DevicePreset::Esp32P4G3ruh,
];
#[must_use]
pub const fn profile(self) -> ModemProfile {
match self {
#[cfg(feature = "g3ruh")]
DevicePreset::Esp32P4G3ruh => ModemProfile::G3RUH_9600,
_ => ModemProfile::BELL_202,
}
}
#[must_use]
pub const fn sample_rate(self) -> SampleRate {
SampleRate(48_000)
}
#[must_use]
pub const fn full_chain_bank(self) -> bool {
matches!(
self,
DevicePreset::Esp32C3FullBank | DevicePreset::Esp32C6FullBank | DevicePreset::Esp32P4
)
}
#[must_use]
pub const fn description(self) -> &'static str {
match self {
DevicePreset::Esp32C3 => {
"ESP32-C3 (160 MHz, no FPU): 1200-baud Bell 202 AFSK, single \
balanced decision chain, i16 fixed-point path"
}
DevicePreset::Esp32C3FullBank => {
"ESP32-C3 (160 MHz, no FPU): 1200-baud Bell 202 AFSK, full \
11-chain diversity bank, i16 fixed-point path"
}
DevicePreset::Esp32C6 => {
"ESP32-C6 (160 MHz, no FPU): 1200-baud Bell 202 AFSK, single \
balanced decision chain, i16 fixed-point path"
}
DevicePreset::Esp32C6FullBank => {
"ESP32-C6 (160 MHz, no FPU): 1200-baud Bell 202 AFSK, full \
11-chain diversity bank, i16 fixed-point path"
}
DevicePreset::Esp32H2 => {
"ESP32-H2 (96 MHz, no FPU): 1200-baud Bell 202 AFSK, single \
balanced decision chain, i16 fixed-point path"
}
DevicePreset::Esp32P4 => {
"ESP32-P4 (400 MHz, FPU): 1200-baud Bell 202 AFSK, full \
11-chain diversity bank, i16 fixed-point path"
}
#[cfg(feature = "g3ruh")]
DevicePreset::Esp32P4G3ruh => {
"ESP32-P4 (400 MHz, FPU): G3RUH 9600-baud scrambled baseband, \
i16 fixed-point path"
}
}
}
#[must_use]
pub const fn expected_cpu(self) -> &'static str {
match self {
DevicePreset::Esp32C3 | DevicePreset::Esp32C6 => {
"~390 ESTIMATED rv32 cycles/sample: ~12% of the 3333 cycles \
available per 48 kHz sample at 160 MHz. Unconfirmed without \
on-device measurement."
}
DevicePreset::Esp32C3FullBank | DevicePreset::Esp32C6FullBank => {
"~1330 ESTIMATED rv32 cycles/sample: ~40% of the core at \
160 MHz / 48 kHz (about 3.4x the single-chain variant). \
Unconfirmed without on-device measurement."
}
DevicePreset::Esp32H2 => {
"~390 ESTIMATED rv32 cycles/sample: ~20% of the 2000 cycles \
available per 48 kHz sample at 96 MHz. Unconfirmed without \
on-device measurement."
}
DevicePreset::Esp32P4 => {
"~1330 ESTIMATED rv32 cycles/sample: ~16% of one core (8333 \
cycles available per 48 kHz sample at 400 MHz)."
}
#[cfg(feature = "g3ruh")]
DevicePreset::Esp32P4G3ruh => {
"~125 ESTIMATED rv32 cycles/sample against the 8333 cycles \
available per 48 kHz sample at 400 MHz."
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Bit {
Zero,
One,
}
impl From<bool> for Bit {
fn from(b: bool) -> Self {
if b { Bit::One } else { Bit::Zero }
}
}
impl From<Bit> for bool {
fn from(bit: Bit) -> bool {
match bit {
Bit::Zero => false,
Bit::One => true,
}
}
}
impl From<Bit> for u8 {
fn from(bit: Bit) -> u8 {
match bit {
Bit::Zero => 0,
Bit::One => 1,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn device_presets_resolve_to_consistent_parts() {
for &preset in DevicePreset::ALL {
let profile = preset.profile();
let rate = preset.sample_rate();
assert_eq!(rate.hz(), 48_000, "{preset:?}");
assert!(rate.hz() / profile.baud().bps() >= 2, "{preset:?}");
assert!(
TonePair::new(profile.tones().mark_hz(), profile.tones().space_hz(), rate).is_ok(),
"{preset:?}"
);
assert!(!preset.description().is_empty(), "{preset:?}");
assert!(preset.expected_cpu().contains("ESTIMATED"), "{preset:?}");
}
}
#[test]
fn device_preset_taxonomy_matches_feasibility_table() {
assert!(!DevicePreset::Esp32C3.full_chain_bank());
assert!(DevicePreset::Esp32C3FullBank.full_chain_bank());
assert!(!DevicePreset::Esp32C6.full_chain_bank());
assert!(DevicePreset::Esp32C6FullBank.full_chain_bank());
assert!(!DevicePreset::Esp32H2.full_chain_bank());
assert!(DevicePreset::Esp32P4.full_chain_bank());
#[cfg(feature = "g3ruh")]
{
assert!(!DevicePreset::Esp32P4G3ruh.full_chain_bank());
assert_eq!(
DevicePreset::Esp32P4G3ruh.profile().scheme(),
ModulationScheme::ScrambledBaseband
);
}
}
#[test]
fn sample_rate_accepts_boundaries() {
assert_eq!(SampleRate::new(8_000).map(SampleRate::hz), Ok(8_000));
assert_eq!(SampleRate::new(48_000).map(SampleRate::hz), Ok(48_000));
}
#[test]
fn sample_rate_accepts_tested_set() {
for hz in [8_000, 11_025, 22_050, 44_100, 48_000] {
assert!(SampleRate::new(hz).is_ok(), "{hz} should be accepted");
}
}
#[test]
fn sample_rate_rejects_below_min() {
assert_eq!(
SampleRate::new(7_999),
Err(ConfigError::SampleRateOutOfRange {
got: 7_999,
min: 8_000,
max: 48_000
})
);
}
#[test]
fn sample_rate_rejects_above_max() {
assert_eq!(
SampleRate::new(48_001),
Err(ConfigError::SampleRateOutOfRange {
got: 48_001,
min: 8_000,
max: 48_000
})
);
}
#[test]
fn sample_rate_rejects_zero() {
assert!(SampleRate::new(0).is_err());
}
#[test]
fn baud_rate_accepts_boundaries() {
assert_eq!(BaudRate::new(1).map(BaudRate::bps), Ok(1));
assert_eq!(BaudRate::new(9_600).map(BaudRate::bps), Ok(9_600));
assert_eq!(BaudRate::new(1_200).map(BaudRate::bps), Ok(1_200));
}
#[test]
fn baud_rate_rejects_zero() {
assert_eq!(
BaudRate::new(0),
Err(ConfigError::BaudRateInvalid {
got: 0,
min: 1,
max: 9_600
})
);
}
#[test]
fn baud_rate_rejects_above_max() {
assert_eq!(
BaudRate::new(9_601),
Err(ConfigError::BaudRateInvalid {
got: 9_601,
min: 1,
max: 9_600
})
);
}
#[test]
fn baud_rate_bell_202_preset() {
assert_eq!(BaudRate::BELL_202.bps(), 1_200);
}
#[test]
fn tone_pair_bell_202_preset() {
assert_eq!(TonePair::BELL_202.mark_hz(), 1_200);
assert_eq!(TonePair::BELL_202.space_hz(), 2_200);
}
#[test]
fn tone_pair_accepts_below_nyquist() {
let sr = match SampleRate::new(8_000) {
Ok(sr) => sr,
Err(e) => panic!("unexpected: {e}"),
};
let pair = TonePair::new(1_200, 2_200, sr);
assert_eq!(pair.map(TonePair::mark_hz), Ok(1_200));
}
#[test]
fn tone_pair_rejects_zero_mark() {
let sr = match SampleRate::new(48_000) {
Ok(sr) => sr,
Err(e) => panic!("unexpected: {e}"),
};
assert_eq!(
TonePair::new(0, 2_200, sr),
Err(ConfigError::ToneOutOfRange {
got: 0,
nyquist: 24_000
})
);
}
#[test]
fn tone_pair_rejects_space_at_nyquist() {
let sr = match SampleRate::new(8_000) {
Ok(sr) => sr,
Err(e) => panic!("unexpected: {e}"),
};
assert_eq!(
TonePair::new(1_200, 4_000, sr),
Err(ConfigError::ToneOutOfRange {
got: 4_000,
nyquist: 4_000
})
);
}
#[test]
fn tone_pair_accepts_just_below_nyquist() {
let sr = match SampleRate::new(8_000) {
Ok(sr) => sr,
Err(e) => panic!("unexpected: {e}"),
};
assert!(TonePair::new(1_200, 3_999, sr).is_ok());
}
#[test]
fn bit_from_bool_roundtrip() {
assert_eq!(Bit::from(true), Bit::One);
assert_eq!(Bit::from(false), Bit::Zero);
assert!(bool::from(Bit::One));
assert!(!bool::from(Bit::Zero));
}
#[test]
fn bit_to_u8() {
assert_eq!(u8::from(Bit::Zero), 0);
assert_eq!(u8::from(Bit::One), 1);
}
}