1mod datatype;
31mod npy;
32mod raw;
33mod sigmf;
34
35pub use datatype::{Datatype, Scalar};
36pub use npy::{Npy, NpyData, read_npy, write_npy, write_npy_i64};
37pub use raw::{read_raw, write_raw};
38pub use sigmf::{Annotation, Capture, SigMF, read_sigmf, write_sigmf, write_sigmf_annotated};
39
40use comnum_core::{Error, Result};
41use comnum_math::C64;
42use std::path::Path;
43
44#[derive(Clone, Debug, PartialEq)]
46pub struct Samples {
47 pub samples: Vec<C64>,
49 pub sample_rate: Option<f64>,
51}
52
53pub fn read_samples(path: impl AsRef<Path>, datatype: Option<Datatype>, channel: usize) -> Result<Samples> {
61 let path = path.as_ref();
62 let name = path.to_string_lossy();
63 let channels = |n: usize| {
64 if channel < n {
65 Ok(())
66 } else {
67 Err(Error::Config(format!("{}: channel {channel} of a recording of {n}", path.display())))
68 }
69 };
70 if name.ends_with(".sigmf-meta") || name.ends_with(".sigmf-data") || name.ends_with(".sigmf") {
71 let s = read_sigmf(path)?;
72 channels(s.num_channels)?;
73 return Ok(Samples { samples: s.channel(channel), sample_rate: s.sample_rate });
74 }
75 channels(1)?;
76 if name.ends_with(".npy") {
77 return Ok(Samples { samples: read_npy(path)?.to_complex(), sample_rate: None });
78 }
79 let dt = datatype.ok_or_else(|| {
80 Error::Config(format!("{}: a raw file needs a datatype (cf32_le, ci16_le, …)", path.display()))
81 })?;
82 Ok(Samples { samples: read_raw(path, dt)?, sample_rate: None })
83}
84
85#[cfg(test)]
86mod tests {
87 use super::raw::encode_all;
88 use super::sigmf::{parse_sigmf, untar};
89 use super::*;
90 use serde_json::json;
91 use std::path::PathBuf;
92
93 fn tmp(name: &str) -> PathBuf {
94 std::env::temp_dir().join(format!("comnum-io-{}-{name}", std::process::id()))
95 }
96
97 #[test]
98 fn raw_round_trips_for_every_type() {
99 let x: Vec<C64> = (0..50).map(|k| C64::new(k as f64 - 25.0, (3.0 - k as f64 / 2.0).round())).collect();
100 for dt in [Datatype::CF32, Datatype::CF64, Datatype::CI32, Datatype::CI16, Datatype::CI8] {
101 let p = tmp(&dt.name());
102 write_raw(&p, &x, dt).unwrap();
103 assert_eq!(read_raw(&p, dt).unwrap(), x, "{}", dt.name());
104 assert_eq!(std::fs::metadata(&p).unwrap().len() as usize, x.len() * dt.sample_bytes());
105 std::fs::remove_file(p).ok();
106 }
107 let p = tmp("rf32");
108 write_raw(&p, &x, Datatype::RF32).unwrap();
109 assert!(read_raw(&p, Datatype::RF32).unwrap().iter().zip(&x).all(|(a, b)| a.re == b.re && a.im == 0.0));
110 std::fs::remove_file(p).ok();
111 }
112
113 #[test]
114 fn every_sigmf_type_round_trips() {
115 let x: Vec<C64> = (0..40).map(|k| C64::new(k as f64 - 20.0, (5.0 - k as f64 / 3.0).round())).collect();
116 for c in ["c", "r"] {
117 for t in ["f32", "f64", "i32", "i16", "i8", "u32", "u16", "u8"] {
118 for e in ["_le", "_be"] {
119 let name = format!("{c}{t}{e}");
120 let dt = Datatype::parse(&name).unwrap();
121 let p = tmp(&format!("all-{name}"));
122 write_raw(&p, &x, dt).unwrap();
123 let y = read_raw(&p, dt).unwrap();
124 let want: Vec<C64> = x.iter().map(|v| if dt.complex { *v } else { C64::new(v.re, 0.0) }).collect();
125 assert_eq!(y, want, "{name}");
126 std::fs::remove_file(p).ok();
127 assert_eq!(dt.name(), if t.ends_with('8') { format!("{c}{t}") } else { name });
129 }
130 }
131 }
132 let dt = Datatype::parse("rf32_be").unwrap();
134 assert_eq!(dt.decode(&[0x3f, 0x80, 0, 0]), C64::new(1.0, 0.0));
135 assert_eq!(Datatype::CU8.decode(&[128, 0]), C64::new(0.0, -128.0));
137 assert!(Datatype::parse("cx16").is_err() && Datatype::parse("ci16_xx").is_err());
138 }
139
140 #[test]
141 fn sigmf_headers_channels_and_annotations() {
142 let dir = tmp("ncd");
144 std::fs::create_dir_all(&dir).unwrap();
145 let x: Vec<C64> = (0..12).map(|k| C64::new(k as f64, -(k as f64))).collect();
146 let raw = encode_all(&x, Datatype::CI16);
147 let mut data = vec![9u8; 5];
150 data.extend_from_slice(&raw[..24]);
151 data.extend(vec![8u8; 7]);
152 data.extend_from_slice(&raw[24..]);
153 data.extend([1, 2, 3]);
154 std::fs::write(dir.join("other.bin"), &data).unwrap();
155 let meta = json!({
156 "global": {"core:datatype": "ci16_le", "core:num_channels": 2, "core:sample_rate": 1e6,
157 "core:trailing_bytes": 3, "core:dataset": "other.bin", "core:version": "1.2.0"},
158 "captures": [{"core:sample_start": 3, "core:header_bytes": 7, "core:frequency": 2.4e9},
159 {"core:sample_start": 0, "core:header_bytes": 5, "core:frequency": 2.4e9}],
160 "annotations": [{"core:sample_start": 4, "core:sample_count": 2, "core:label": "QPSK"}]
161 });
162 std::fs::write(dir.join("rec.sigmf-meta"), meta.to_string()).unwrap();
163 let r = read_sigmf(dir.join("rec.sigmf-meta")).unwrap();
164 assert_eq!(r.samples, x);
165 assert_eq!((r.num_channels, r.frequency, r.captures.len()), (2, Some(2.4e9), 2));
166 assert_eq!(r.channel(1), x.iter().skip(1).step_by(2).copied().collect::<Vec<_>>());
167 let a = &r.annotations[0];
168 assert_eq!((a.label.as_deref(), r.annotated(a, 0)), (Some("QPSK"), vec![x[8], x[10]]));
169 assert_eq!(r.full_scale(), Some(32768.0));
170 let base = dir.join("w");
172 let ann = Annotation {
173 sample_start: 2,
174 sample_count: Some(3),
175 label: Some("16-QAM".into()),
176 ..Annotation::default()
177 };
178 write_sigmf_annotated(&base, &x, Datatype::CF32, 1e6, Some(915e6), None, std::slice::from_ref(&ann)).unwrap();
179 let w = read_sigmf(&base).unwrap();
180 assert_eq!((w.annotations, w.frequency), (vec![ann], Some(915e6)));
181 std::fs::remove_dir_all(dir).ok();
182 }
183
184 #[test]
185 fn npy_round_trips_and_reads_numpy_headers() {
186 let x: Vec<C64> = (0..7).map(|k| C64::new(k as f64 * 0.5, -1.0)).collect();
187 let p = tmp("a.npy");
188 write_npy(&p, &x).unwrap();
189 let a = read_npy(&p).unwrap();
190 assert_eq!((a.shape.clone(), a.data.clone()), (vec![7], NpyData::Complex(x)));
191 let bytes = std::fs::read(&p).unwrap();
192 assert_eq!((10 + u16::from_le_bytes([bytes[8], bytes[9]]) as usize) % 64, 0);
193 write_npy_i64(&p, &[3, -1, 7]).unwrap();
194 assert_eq!(read_npy(&p).unwrap().data, NpyData::Int(vec![3, -1, 7]));
195 std::fs::write(&p, b"not numpy").unwrap();
196 assert!(read_npy(&p).is_err());
197 std::fs::remove_file(p).ok();
198 }
199
200 #[test]
201 fn corrupt_archives_and_arrays_are_errors_not_panics() {
202 for size in [&b"77777777777\0"[..], b"zz\0"] {
204 let mut tar = vec![0u8; 1024];
205 tar[..14].copy_from_slice(b"rec.sigmf-meta");
206 tar[124..124 + size.len()].copy_from_slice(size);
207 assert!(untar(&tar).is_err());
208 let p = tmp("corrupt.sigmf");
209 std::fs::write(&p, &tar).unwrap();
210 assert!(read_sigmf(&p).is_err());
211 std::fs::remove_file(p).ok();
212 }
213 for (start, header) in [(0u64, u64::MAX), (u64::MAX, 0)] {
215 let meta = json!({"global": {"core:datatype": "cf32_le"},
216 "captures": [{"core:sample_start": 0, "core:header_bytes": header},
217 {"core:sample_start": start, "core:header_bytes": 0}]});
218 let r = parse_sigmf(meta, &[0u8; 64], "corrupt");
219 assert!(r.is_err() || r.is_ok_and(|s| s.samples.len() == 8), "{start} {header}");
220 }
221 let npy = |header: &str| {
223 let mut b = b"\x93NUMPY\x01\x00".to_vec();
224 b.extend((header.len() as u16).to_le_bytes());
225 b.extend(header.as_bytes());
226 b.extend([0u8; 16]);
227 b
228 };
229 let p = tmp("corrupt.npy");
230 for header in [
231 "{'descr': '<f8', 'fortran_order': False, 'shape': (4294967296, 4294967296, 4294967296), }",
232 "{'descr': '<c16', 'fortran_order': False, 'shape': (1152921504606846976,), }",
233 "{'descr': '<f8', 'fortran_order': False, 'shape': (-1,), }",
234 "{'descr': '",
235 ] {
236 std::fs::write(&p, npy(header)).unwrap();
237 assert!(read_npy(&p).is_err(), "{header}");
238 }
239 let mut cut = npy("{'descr': '<f8', 'fortran_order': False, 'shape': (2,), }");
240 cut[8] = 0xff;
241 std::fs::write(&p, cut).unwrap();
242 assert!(read_npy(&p).is_err());
243 std::fs::remove_file(p).ok();
244 }
245
246 #[test]
247 fn sigmf_keeps_the_metadata() {
248 let x = vec![C64::new(0.25, -0.5); 16];
249 let base = tmp("rec");
250 write_sigmf(&base, &x, Datatype::CF32, 80e9, Some(&json!({"comnum:symbol_rate": 32e9}))).unwrap();
251 let r = read_sigmf(format!("{}.sigmf-data", base.display())).unwrap();
252 assert_eq!(r.samples, x);
253 assert_eq!(r.meta["global"]["comnum:symbol_rate"], json!(32e9));
254 let Samples { samples: s, sample_rate: fs } =
255 read_samples(format!("{}.sigmf-meta", base.display()), None, 0).unwrap();
256 assert_eq!((s.len(), fs), (16, Some(80e9)));
257 assert!(read_samples(&base, None, 0).is_err());
258 assert!(read_samples(format!("{}.sigmf-meta", base.display()), None, 1).is_err());
259 }
260}