wcsng-sigmf 0.1.1

Read, write, and validate SigMF (Signal Metadata Format) recordings, metadata, collections, and archives
Documentation

wcsng-sigmf

crates.io docs.rs CI MSRV 1.89 License: MIT

A Rust library for reading, writing, and validating SigMF (Signal Metadata Format) recordings, collections, and archives.

SigMF pairs a binary dataset of RF samples (.sigmf-data) with JSON metadata (.sigmf-meta) that describes how the samples were captured. This crate implements the SigMF 1.2 specification with an emphasis on correctness and on preserving data it doesn't understand.

  • Typed metadata. global, captures, and annotations are strongly typed. Unknown and extension fields round-trip unchanged.
  • Every datatype. Real and complex f32/f64/i8/i16/i32/u8/u16/u32 samples in either byte order, multi-channel datasets, and non-conforming datasets (NCD) with capture headers and trailing bytes.
  • Streaming I/O. Read frames by absolute sample index or iterate in fixed-size blocks without loading the whole dataset. Crop recordings while preserving sample indices.
  • Archives and collections. Create and read .sigmf tar archives and .sigmf-collection files, with SHA-512 integrity checks.
  • Safe writes. Files are written to a temporary sibling and published atomically. Existing files are never overwritten unless you ask.
  • Validation. Policy-driven checks that return machine-readable issues with JSON pointers and remediation hints.
  • Typed extensions. Map namespaced extension fields such as lab:gain_db to Rust structs with #[derive(SigmfExtension)].

Installation

cargo add wcsng-sigmf

The package is named wcsng-sigmf, and the library is imported as sigmf:

use sigmf::meta::Metadata;

Feature flags

Feature Description
derive #[derive(SigmfExtension)] for typed metadata extensions.
schema JSON Schema generation and validation for extensions.
[dependencies]
wcsng-sigmf = { version = "0.1", features = ["derive"] }

Usage

Read a recording

use sigmf::recording_file::Recording;

fn main() -> Result<(), sigmf::SigmfError> {
    let mut recording = Recording::open("capture.sigmf-meta")?;
    recording.verify_dataset_digest()?;

    let metadata = recording.metadata();
    println!("datatype:    {}", metadata.global().datatype);
    println!("sample rate: {:?}", metadata.get_sample_rate());
    println!("frames:      {}", recording.frame_count()?);

    // Stream samples in blocks of 4096 frames.
    let start = recording.first_sample_index();
    let count = recording.frame_count()?;
    for block in recording.frame_blocks(start, count, 4096)? {
        let _samples = block?;
        // process samples ...
    }
    Ok(())
}

Write a recording

use sigmf::datatype::Datatype;
use sigmf::global::Global;
use sigmf::meta::Metadata;
use sigmf::num_complex::Complex;
use sigmf::recording_file::RecordingWriter;
use sigmf::utils::data::SampleValue;
use std::str::FromStr;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut metadata = Metadata::new(
        Global::new("1.2.6", Datatype::from_str("cf32_le")?),
        Vec::new(),
        Vec::new(),
    );
    metadata.set_sample_rate(1e6);
    metadata.set_author("Jane Doe <jane@example.com>".to_string());

    let samples: Vec<SampleValue> = (0..1024)
        .map(|n| SampleValue::ComplexF32(Complex::from_polar(1.0, n as f32 * 0.1)))
        .collect();

    // Writes tone.sigmf-data and tone.sigmf-meta. core:sha512 is filled in.
    let mut writer = RecordingWriter::new("tone.sigmf-meta", metadata)?;
    writer.write_frames(&samples)?;
    let digest = writer.finish()?;
    println!("sha512: {digest}");
    Ok(())
}

Archives

use sigmf::Archive;

fn main() -> Result<(), sigmf::SigmfError> {
    let archive = Archive::load("recordings.sigmf")?;
    for recording in archive.get_recordings() {
        let metadata = archive.get_meta_from_recording(&recording)?;
        println!("{}: {}", recording.get_name(), metadata.global().datatype);
    }
    Ok(())
}

Use sigmf::ArchiveWriter to create archives.

Validation

use sigmf::meta::Metadata;
use sigmf::validation::{ValidationPolicy, ValidationStatus};

fn main() -> Result<(), sigmf::SigmfError> {
    let metadata = Metadata::from_path("capture.sigmf-meta")?;
    let report = metadata.validate(ValidationPolicy::StrictSupportedVersion);
    if report.status != ValidationStatus::Passed {
        for issue in &report.issues {
            eprintln!("{:?} {} at {}: {}", issue.severity, issue.code, issue.json_pointer, issue.message);
        }
    }
    Ok(())
}

Typed extensions

With the derive feature:

use serde::{Deserialize, Serialize};
use sigmf::SigmfExtension;

#[derive(Debug, Serialize, Deserialize, SigmfExtension)]
#[sigmf(namespace = "lab", version = "1.0.0", scope = "global")]
struct LabReceiver {
    #[serde(rename = "lab:gain_db")]
    gain_db: f64,
}

metadata.set_global_extension(&LabReceiver { gain_db: 30.0 })?;
let receiver: LabReceiver = metadata.global_extension()?;

Examples

Runnable programs are in examples/:

Example Description
save_sigmf Build metadata and write a .sigmf archive.
load_sigmf List the recordings in a .sigmf archive.
load_recording Stream every frame of a .sigmf-meta/.sigmf-data pair.
derive_extension Define, attach, and validate typed extensions (--features derive).
cargo run --example save_sigmf -- /tmp/example.sigmf
cargo run --example load_sigmf -- /tmp/example.sigmf
cargo run --example derive_extension --features derive

Documentation

Build the docs locally with:

cargo doc --all-features --no-deps --open

Minimum supported Rust version

Rust 1.89 or newer. Raising the MSRV is considered a minor change.

Contributing

Issues and pull requests are welcome. Before submitting, run:

cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace --all-targets --all-features
cargo test --workspace --doc --all-features

See RELEASING.md for the release process.

License

Licensed under the MIT License. Developed by WCSNG at UC San Diego.