1use super::Datatype;
4use super::raw::{decode_all, write_creating_dirs, write_raw};
5use comnum_core::{Error, Result, ensure};
6use comnum_math::C64;
7use serde_json::{Value as Json, json};
8use std::path::{Path, PathBuf};
9
10#[derive(Clone, Debug, PartialEq)]
12pub struct Capture {
13 pub sample_start: usize,
15 pub frequency: Option<f64>,
17 pub datetime: Option<String>,
19}
20
21#[derive(Clone, Debug, Default, PartialEq)]
23pub struct Annotation {
24 pub sample_start: usize,
26 pub sample_count: Option<usize>,
28 pub label: Option<String>,
30 pub comment: Option<String>,
32 pub freq_lower_edge: Option<f64>,
34 pub freq_upper_edge: Option<f64>,
36}
37
38impl Annotation {
39 pub const KEYS: [&'static str; 6] =
41 ["sample_start", "sample_count", "label", "comment", "freq_lower_edge", "freq_upper_edge"];
42
43 pub fn from_mapping(a: &Json) -> Result<Annotation> {
59 let keys = Annotation::KEYS;
60 if let Some(k) = a.as_object().and_then(|o| o.keys().find(|k| !keys.contains(&k.as_str()))) {
61 return Err(Error::Config(format!("unknown annotation key {k:?} (one of {keys:?})")));
62 }
63 let start = a["sample_start"]
64 .as_u64()
65 .ok_or_else(|| Error::Config("every annotation needs a non-negative integer sample_start".into()))?;
66 Ok(Annotation {
67 sample_start: start as usize,
68 sample_count: a["sample_count"].as_u64().map(|v| v as usize),
69 label: a["label"].as_str().map(str::to_string),
70 comment: a["comment"].as_str().map(str::to_string),
71 freq_lower_edge: a["freq_lower_edge"].as_f64(),
72 freq_upper_edge: a["freq_upper_edge"].as_f64(),
73 })
74 }
75
76 fn from_json(a: &Json) -> Annotation {
77 let s = |k: &str| a[k].as_str().map(str::to_string);
78 Annotation {
79 sample_start: a["core:sample_start"].as_u64().unwrap_or(0) as usize,
80 sample_count: a["core:sample_count"].as_u64().map(|v| v as usize),
81 label: s("core:label"),
82 comment: s("core:comment"),
83 freq_lower_edge: a["core:freq_lower_edge"].as_f64(),
84 freq_upper_edge: a["core:freq_upper_edge"].as_f64(),
85 }
86 }
87
88 fn to_json(&self) -> Json {
89 let mut m = serde_json::Map::new();
90 m.insert("core:sample_start".into(), json!(self.sample_start));
91 if let Some(v) = self.sample_count {
92 m.insert("core:sample_count".into(), json!(v));
93 }
94 for (k, v) in [("core:label", &self.label), ("core:comment", &self.comment)] {
95 if let Some(v) = v {
96 m.insert(k.into(), json!(v));
97 }
98 }
99 for (k, v) in [("core:freq_lower_edge", self.freq_lower_edge), ("core:freq_upper_edge", self.freq_upper_edge)] {
100 if let Some(v) = v {
101 m.insert(k.into(), json!(v));
102 }
103 }
104 Json::Object(m)
105 }
106}
107
108#[derive(Clone, Debug)]
110pub struct SigMF {
111 pub samples: Vec<C64>,
113 pub num_channels: usize,
115 pub sample_rate: Option<f64>,
117 pub frequency: Option<f64>,
119 pub datatype: Datatype,
121 pub captures: Vec<Capture>,
123 pub annotations: Vec<Annotation>,
125 pub meta: Json,
127}
128
129impl SigMF {
130 pub fn channel(&self, k: usize) -> Vec<C64> {
132 self.samples.iter().skip(k).step_by(self.num_channels.max(1)).copied().collect()
133 }
134
135 pub fn try_channel(&self, k: usize) -> Result<Vec<C64>> {
140 ensure(k < self.num_channels, || format!("channel {k} of a recording of {}", self.num_channels))?;
141 Ok(self.channel(k))
142 }
143
144 pub fn full_scale(&self) -> Option<f64> {
147 self.datatype.scalar.is_integer().then(|| self.datatype.scalar.half_range())
148 }
149
150 pub fn annotated(&self, a: &Annotation, k: usize) -> Vec<C64> {
152 let x = self.channel(k);
153 let start = a.sample_start.min(x.len());
154 let end = a.sample_count.map_or(x.len(), |c| (start + c).min(x.len()));
155 x[start..end].to_vec()
156 }
157}
158
159fn sigmf_paths(path: &Path) -> (PathBuf, PathBuf) {
161 let s = path.to_string_lossy();
162 let base = s.strip_suffix(".sigmf-meta").or_else(|| s.strip_suffix(".sigmf-data")).unwrap_or(&s).to_string();
163 (PathBuf::from(format!("{base}.sigmf-meta")), PathBuf::from(format!("{base}.sigmf-data")))
164}
165
166pub(super) fn untar(bytes: &[u8]) -> Result<Vec<(String, &[u8])>> {
169 let mut files = Vec::new();
170 let mut at = 0;
171 let octal = |f: &[u8]| -> Result<usize> {
172 let s: String = f.iter().take_while(|&&c| c != 0).map(|&c| c as char).collect();
173 usize::from_str_radix(s.trim(), 8).map_err(|_| Error::Config(format!("tar: bad size field {s:?}")))
174 };
175 let text = |f: &[u8]| f.iter().take_while(|&&c| c != 0).map(|&c| c as char).collect::<String>();
176 while at + 512 <= bytes.len() {
177 let h = &bytes[at..at + 512];
178 if h.iter().all(|&c| c == 0) {
179 break;
180 }
181 let size = octal(&h[124..136])?;
182 let (prefix, name) = (text(&h[345..500]), text(&h[..100]));
183 let name = if prefix.is_empty() { name } else { format!("{prefix}/{name}") };
184 let start = at + 512;
185 let Some(end) = start.checked_add(size).filter(|&e| e <= bytes.len()) else {
187 return Err(Error::Config(format!("tar: {name} ends after the archive")));
188 };
189 if matches!(h[156], b'0' | 0) {
191 files.push((name, &bytes[start..end]));
192 }
193 at = end.div_ceil(512).saturating_mul(512);
194 }
195 Ok(files)
196}
197
198pub fn read_sigmf(path: impl AsRef<Path>) -> Result<SigMF> {
210 let path = path.as_ref();
211 if path.extension().is_some_and(|e| e == "sigmf") {
212 let bytes = std::fs::read(path)?;
213 let files = untar(&bytes)?;
214 let (meta_name, meta) = files
215 .iter()
216 .find(|(n, _)| n.ends_with(".sigmf-meta"))
217 .ok_or_else(|| Error::Config(format!("{}: no .sigmf-meta in the archive", path.display())))?;
218 let data_name = format!("{}.sigmf-data", meta_name.trim_end_matches(".sigmf-meta"));
219 let data = files.iter().find(|(n, _)| *n == data_name).map_or(&[][..], |(_, d)| *d);
220 let meta: Json =
221 serde_json::from_slice(meta).map_err(|e| Error::Config(format!("{}/{meta_name}: {e}", path.display())))?;
222 return parse_sigmf(meta, data, &format!("{}/{meta_name}", path.display()));
223 }
224 let (meta_path, mut data_path) = sigmf_paths(path);
225 let meta: Json = serde_json::from_str(&std::fs::read_to_string(&meta_path)?)
226 .map_err(|e| Error::Config(format!("{}: {e}", meta_path.display())))?;
227 if let Some(d) = meta["global"]["core:dataset"].as_str() {
228 data_path = meta_path.with_file_name(d);
229 }
230 let data = std::fs::read(&data_path)?;
231 parse_sigmf(meta, &data, &meta_path.display().to_string())
232}
233
234pub(super) fn parse_sigmf(meta: Json, data: &[u8], name: &str) -> Result<SigMF> {
235 let global = &meta["global"];
236 let dt =
237 global["core:datatype"].as_str().ok_or_else(|| Error::Config(format!("{name}: no global core:datatype")))?;
238 let datatype = Datatype::parse(dt)?;
239 let num_channels = global["core:num_channels"].as_u64().unwrap_or(1).max(1) as usize;
240 let trailing = global["core:trailing_bytes"].as_u64().unwrap_or(0) as usize;
241 let data = &data[..data.len().saturating_sub(trailing)];
242 let mut captures: Vec<(Capture, usize)> = meta["captures"]
243 .as_array()
244 .map(|cs| {
245 cs.iter()
246 .map(|c| {
247 let cap = Capture {
248 sample_start: c["core:sample_start"].as_u64().unwrap_or(0) as usize,
249 frequency: c["core:frequency"].as_f64(),
250 datetime: c["core:datetime"].as_str().map(str::to_string),
251 };
252 (cap, c["core:header_bytes"].as_u64().unwrap_or(0) as usize)
253 })
254 .collect()
255 })
256 .unwrap_or_default();
257 captures.sort_by_key(|(c, _)| c.sample_start);
258 let frame = datatype.sample_bytes() * num_channels;
260 let mut body = Vec::with_capacity(data.len());
261 let mut at = 0usize;
262 for (i, (c, header)) in captures.iter().enumerate() {
263 let skip_to = at.saturating_add(*header);
264 ensure_le(skip_to, data.len(), name)?;
265 let samples = captures.get(i + 1).map(|(n, _)| (n.sample_start - c.sample_start).saturating_mul(frame));
267 let end = samples.map_or(data.len(), |b| skip_to.saturating_add(b).min(data.len()));
268 body.extend_from_slice(&data[skip_to..end]);
269 at = end;
270 }
271 if captures.is_empty() {
272 body.extend_from_slice(data);
273 }
274 let samples = decode_all(&body, datatype)?;
275 if samples.len() % num_channels != 0 {
276 return Err(Error::Shape(format!(
277 "{name}: {} samples is not a whole number of {num_channels} channels",
278 samples.len()
279 )));
280 }
281 let mut annotations: Vec<Annotation> =
282 meta["annotations"].as_array().map(|a| a.iter().map(Annotation::from_json).collect()).unwrap_or_default();
283 annotations.sort_by_key(|a| a.sample_start);
284 let captures: Vec<Capture> = captures.into_iter().map(|(c, _)| c).collect();
285 Ok(SigMF {
286 samples,
287 num_channels,
288 sample_rate: global["core:sample_rate"].as_f64(),
289 frequency: captures.first().and_then(|c| c.frequency),
290 datatype,
291 captures,
292 annotations,
293 meta,
294 })
295}
296
297fn ensure_le(a: usize, b: usize, name: &str) -> Result<()> {
298 if a > b {
299 return Err(Error::Config(format!("{name}: the header bytes of a capture go beyond the data file")));
300 }
301 Ok(())
302}
303
304pub fn write_sigmf(
312 base: impl AsRef<Path>,
313 x: &[C64],
314 datatype: Datatype,
315 sample_rate: f64,
316 global: Option<&Json>,
317) -> Result<()> {
318 write_sigmf_annotated(base, x, datatype, sample_rate, None, global, &[])
319}
320
321pub fn write_sigmf_annotated(
327 base: impl AsRef<Path>,
328 x: &[C64],
329 datatype: Datatype,
330 sample_rate: f64,
331 frequency: Option<f64>,
332 global: Option<&Json>,
333 annotations: &[Annotation],
334) -> Result<()> {
335 let (meta_path, data_path) = sigmf_paths(base.as_ref());
336 write_raw(&data_path, x, datatype)?;
337 let mut g = json!({
338 "core:datatype": datatype.name(),
339 "core:sample_rate": sample_rate,
340 "core:version": "1.2.0",
341 "core:recorder": format!("comnum {}", env!("CARGO_PKG_VERSION")),
342 });
343 if let (Some(Json::Object(extra)), Json::Object(gm)) = (global, &mut g) {
344 gm.extend(extra.clone());
345 }
346 let mut capture = json!({"core:sample_start": 0});
347 if let (Some(f), Json::Object(c)) = (frequency, &mut capture) {
348 c.insert("core:frequency".into(), json!(f));
349 }
350 let mut annotations = annotations.to_vec();
351 annotations.sort_by_key(|a| a.sample_start);
352 let annotations: Vec<Json> = annotations.iter().map(Annotation::to_json).collect();
353 let meta = json!({"global": g, "captures": [capture], "annotations": annotations});
354 write_creating_dirs(&meta_path, (serde_json::to_string_pretty(&meta).unwrap_or_default() + "\n").as_bytes())
355}