Skip to main content

comnum_spec/io/
datatype.rs

1//! The sample formats of SigMF ([`Datatype`]): the raw files and the SigMF recordings use them.
2
3use comnum_core::{Error, Result};
4use comnum_math::C64;
5
6/// The scalar type of the components of a SigMF sample.
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub enum Scalar {
9    /// `f32`.
10    F32,
11    /// `f64`.
12    F64,
13    /// `i32`.
14    I32,
15    /// `i16`.
16    I16,
17    /// `i8`.
18    I8,
19    /// `u32`.
20    U32,
21    /// `u16`.
22    U16,
23    /// `u8`.
24    U8,
25}
26
27impl Scalar {
28    /// Bytes per component.
29    pub fn bytes(self) -> usize {
30        match self {
31            Scalar::F64 => 8,
32            Scalar::F32 | Scalar::I32 | Scalar::U32 => 4,
33            Scalar::I16 | Scalar::U16 => 2,
34            Scalar::I8 | Scalar::U8 => 1,
35        }
36    }
37
38    pub(super) fn name(self) -> &'static str {
39        match self {
40            Scalar::F32 => "f32",
41            Scalar::F64 => "f64",
42            Scalar::I32 => "i32",
43            Scalar::I16 => "i16",
44            Scalar::I8 => "i8",
45            Scalar::U32 => "u32",
46            Scalar::U16 => "u16",
47            Scalar::U8 => "u8",
48        }
49    }
50
51    pub(super) fn is_integer(self) -> bool {
52        !matches!(self, Scalar::F32 | Scalar::F64)
53    }
54
55    /// $2^{b-1}$ for an integer of $b$ bits: the full scale, and the offset of the
56    /// unsigned types.
57    pub(super) fn half_range(self) -> f64 {
58        2f64.powi(8 * self.bytes() as i32 - 1)
59    }
60}
61
62/// A sample format of SigMF: `[c|r][f32|f64|i32|i16|i8|u32|u16|u8][_le|_be]` (the
63/// 8-bit types without endianness).
64///
65/// Integer samples are converted to floating point without scaling; the unsigned types
66/// are centred by $2^{b-1}$ (128 for `cu8`), as the reference implementation
67/// `sigmf-python` does. [`SigMF::full_scale`](super::SigMF::full_scale) gives the scale of its `autoscale`.
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69pub struct Datatype {
70    /// Complex (interleaved I and Q) or real.
71    pub complex: bool,
72    /// The type of a component.
73    pub scalar: Scalar,
74    /// Big-endian components.
75    pub big_endian: bool,
76}
77
78impl Datatype {
79    /// `cf32_le`, the `.cfile` of GNU Radio.
80    pub const CF32: Datatype = Datatype { complex: true, scalar: Scalar::F32, big_endian: false };
81    /// `cf64_le`.
82    pub const CF64: Datatype = Datatype { complex: true, scalar: Scalar::F64, big_endian: false };
83    /// `ci32_le`.
84    pub const CI32: Datatype = Datatype { complex: true, scalar: Scalar::I32, big_endian: false };
85    /// `ci16_le`, the IQ format of most SDRs.
86    pub const CI16: Datatype = Datatype { complex: true, scalar: Scalar::I16, big_endian: false };
87    /// `ci8`.
88    pub const CI8: Datatype = Datatype { complex: true, scalar: Scalar::I8, big_endian: false };
89    /// `cu8` (RTL-SDR).
90    pub const CU8: Datatype = Datatype { complex: true, scalar: Scalar::U8, big_endian: false };
91    /// `rf32_le`.
92    pub const RF32: Datatype = Datatype { complex: false, scalar: Scalar::F32, big_endian: false };
93    /// `rf64_le`.
94    pub const RF64: Datatype = Datatype { complex: false, scalar: Scalar::F64, big_endian: false };
95    /// `ri16_le`.
96    pub const RI16: Datatype = Datatype { complex: false, scalar: Scalar::I16, big_endian: false };
97
98    /// Parse a SigMF data type (`cf32_le`, `ci16_be`, `cu8`, `ri8`, …; the endianness
99    /// may be omitted, little endian then).
100    ///
101    /// # Errors
102    /// [`Error::Config`] for an unknown type.
103    pub fn parse(s: &str) -> Result<Datatype> {
104        let bad = || {
105            Error::Config(format!(
106                "unsupported data type {s:?}: [c|r][f32|f64|i32|i16|i8|u32|u16|u8][_le|_be] (cf32_le, ci16_le, cu8, …)"
107            ))
108        };
109        let (complex, rest) = match s.split_at_checked(1) {
110            Some(("c", r)) => (true, r),
111            Some(("r", r)) => (false, r),
112            _ => return Err(bad()),
113        };
114        let (body, big_endian) = match rest.rsplit_once('_') {
115            Some((b, "le")) => (b, false),
116            Some((b, "be")) => (b, true),
117            Some(_) => return Err(bad()),
118            None => (rest, false),
119        };
120        let scalar = match body {
121            "f32" => Scalar::F32,
122            "f64" => Scalar::F64,
123            "i32" => Scalar::I32,
124            "i16" => Scalar::I16,
125            "i8" => Scalar::I8,
126            "u32" => Scalar::U32,
127            "u16" => Scalar::U16,
128            "u8" => Scalar::U8,
129            _ => return Err(bad()),
130        };
131        Ok(Datatype { complex, scalar, big_endian: big_endian && scalar.bytes() > 1 })
132    }
133
134    /// The SigMF name.
135    pub fn name(self) -> String {
136        let end = match (self.scalar.bytes(), self.big_endian) {
137            (1, _) => "",
138            (_, false) => "_le",
139            (_, true) => "_be",
140        };
141        format!("{}{}{end}", if self.complex { "c" } else { "r" }, self.scalar.name())
142    }
143
144    /// Bytes per sample (both components for a complex type).
145    pub fn sample_bytes(self) -> usize {
146        self.scalar.bytes() * if self.complex { 2 } else { 1 }
147    }
148
149    pub(super) fn component(self, b: &[u8]) -> f64 {
150        macro_rules! num {
151            ($t:ty, $n:expr) => {{
152                let a: [u8; $n] = b[..$n].try_into().unwrap_or([0; $n]);
153                if self.big_endian { <$t>::from_be_bytes(a) } else { <$t>::from_le_bytes(a) }
154            }};
155        }
156        let h = self.scalar.half_range();
157        match self.scalar {
158            Scalar::F32 => f64::from(num!(f32, 4)),
159            Scalar::F64 => num!(f64, 8),
160            Scalar::I32 => f64::from(num!(i32, 4)),
161            Scalar::I16 => f64::from(num!(i16, 2)),
162            Scalar::I8 => f64::from(b[0] as i8),
163            Scalar::U32 => f64::from(num!(u32, 4)) - h,
164            Scalar::U16 => f64::from(num!(u16, 2)) - h,
165            Scalar::U8 => f64::from(b[0]) - h,
166        }
167    }
168
169    pub(super) fn decode(self, b: &[u8]) -> C64 {
170        let k = self.scalar.bytes();
171        C64::new(self.component(b), if self.complex { self.component(&b[k..]) } else { 0.0 })
172    }
173
174    pub(super) fn put(self, x: f64, out: &mut Vec<u8>) {
175        macro_rules! put {
176            ($v:expr) => {{
177                let v = $v;
178                if self.big_endian { out.extend(v.to_be_bytes()) } else { out.extend(v.to_le_bytes()) }
179            }};
180        }
181        let h = self.scalar.half_range();
182        // integers: rounded and saturated
183        let sat = |lo: f64, hi: f64| x.round().clamp(lo, hi);
184        match self.scalar {
185            Scalar::F32 => put!(x as f32),
186            Scalar::F64 => put!(x),
187            Scalar::I32 => put!(sat(-h, h - 1.0) as i32),
188            Scalar::I16 => put!(sat(-h, h - 1.0) as i16),
189            Scalar::I8 => put!(sat(-h, h - 1.0) as i8),
190            Scalar::U32 => put!((sat(-h, h - 1.0) + h) as u32),
191            Scalar::U16 => put!((sat(-h, h - 1.0) + h) as u16),
192            Scalar::U8 => put!((sat(-h, h - 1.0) + h) as u8),
193        }
194    }
195
196    pub(super) fn encode(self, v: C64, out: &mut Vec<u8>) {
197        self.put(v.re, out);
198        if self.complex {
199            self.put(v.im, out);
200        }
201    }
202}