Skip to content

Instantly share code, notes, and snippets.

@Teque5
Created April 29, 2026 17:33
Show Gist options
  • Select an option

  • Save Teque5/9d7cf6a1432ffec0d217afb5233f486c to your computer and use it in GitHub Desktop.

Select an option

Save Teque5/9d7cf6a1432ffec0d217afb5233f486c to your computer and use it in GitHub Desktop.
Draft SigMF SKILL file

SigMF Python Examples

Examples demonstrating sigmf-python for creating, reading, and processing SigMF recordings. This module follows the SigMF specification html/pdf from the spec repository.

Usage

Loading files — sigmf.fromfile() auto-detects format by extension:

meta = sigmf.fromfile("recording")  # .sigmf extension optional
meta = sigmf.fromfile("recording.sigmf")  # archive
meta = sigmf.fromfile("recording.sigmf-meta")  # metadata file
meta = sigmf.fromfile("recording.wav")  # converts WAV to SigMF

Memory-efficient slicing — avoids loading entire files into RAM:

samples = meta[0:1_000_000]  # first 1M samples only
samples = meta[1_000_000:2_000_000]  # next 1M samples
samples = meta.read_samples(start_index=0, count=1_000_000)  # equivalent

Metadata access — dynamic properties or explicit field access:

# dynamic properties (preferred, raises SigMFAccessError if field is unset)
samp_rate_hz = meta.sample_rate
dtype_str = meta.datatype

# explicit field access (returns None if field is unset)
author = meta.get_global_field("core:author")
annotations = meta.get_annotations()
captures = meta.get_captures()

# setting fields
meta.sample_rate = 2.4e6  # via property
meta.set_global_field("core:description", "my signal")  # explicit

SigMF datatypes — format is {r|c}{type}{bits}_{endian} (r=real, c=complex, le/be endianness, omitted for 8-bit):

cf32_le  - complex float32, little-endian (most common for IQ data)
ci16_le  - complex int16, little-endian
ci8      - complex int8
rf32_le  - real float32, little-endian
ri16_le  - real int16, little-endian
ru8      - real unsigned int8

Writing and archiving:

meta.tofile("output.sigmf-meta")  # metadata only
meta.tofile("output", toarchive=True)  # .sigmf archive (meta + data)
meta.validate()  # validate metadata against schema

Example 1: Low-Pass FIR Filtering with SciPy

Generate a noisy multi-tone signal, write it to SigMF, load it back, and apply a low-pass FIR filter to remove an out-of-band interferer.

import sigmf
import numpy as np
from scipy import signal as sp_signal

# generate noisy signal: 5 kHz tone + 40 kHz interferer + awgn
sample_rate_hz = 100000
num_samples = 100000
t = np.arange(num_samples) / sample_rate_hz
samples = (
    np.exp(2j * np.pi * 5000 * t)
    + 0.5 * np.exp(2j * np.pi * 40000 * t)
    + 0.1 * (np.random.randn(num_samples) + 1j * np.random.randn(num_samples))
).astype(np.complex64)

# write data file
samples.tofile("noisy_tones.sigmf-data")

# create sigmf metadata
meta = sigmf.SigMFFile(
    data_file="noisy_tones.sigmf-data",
    global_info={
        "core:datatype": "cf32_le",
        "core:sample_rate": sample_rate_hz,
        "core:author": "jane.doe@example.org",
        "core:description": "5 kHz tone + 40 kHz interferer + AWGN",
    },
)

meta.add_capture(
    0,
    metadata={
        "core:frequency": 915e6,
    },
)

meta.add_annotation(
    0,
    num_samples,
    metadata={
        "core:label": "noisy_tones",
        "core:comment": "Two tones with additive white gaussian noise",
    },
)

# save as archive
meta.tofile("noisy_tones", toarchive=True)

# load it back
loopback = sigmf.fromfile("noisy_tones.sigmf")
print(f"sample rate: {loopback.sample_rate} Hz")
print(f"datatype: {loopback.datatype}")
print(f"total samples: {len(loopback)}")
print(f"annotations: {loopback.get_annotations()}")

# read all samples
samples = loopback.read_samples()

# design and apply low-pass fir filter (20 kHz cutoff) to remove 40 kHz interferer
cutoff_hz = 20000
num_taps = 101
lpf = sp_signal.firwin(num_taps, cutoff_hz, fs=loopback.sample_rate)
filtered = sp_signal.lfilter(lpf, 1.0, samples)

print(f"filtered {len(filtered)} samples at {loopback.sample_rate} Hz")

Example 2: Spectrogram Generation with PyTorch

Use sigmf.SigMFGenerator to create a multi-signal recording, then compute and save a spectrogram with PyTorch.

import sigmf
import numpy as np
import torch
from PIL import Image

# generate multi-signal recording with builder pattern
meta = (
    sigmf.SigMFGenerator()
    .sample_rate(48e3)
    .duration(1.0)
    .tone(frequency_hz=5e3, amplitude=0.8)
    .tone(frequency_hz=-12e3, amplitude=0.4)
    .sweep(start_frequency_hz=-20e3, end_frequency_hz=20e3, amplitude=0.3)
    .snr(20)
    .generate()
)

# save as archive and reload
meta.tofile("multisignal", toarchive=True)
loopback = sigmf.fromfile("multisignal.sigmf")

print(f"sample rate: {loopback.sample_rate} Hz")
print(f"datatype: {loopback.datatype}")
print(f"total samples: {len(loopback)}")
print(f"annotations: {loopback.get_annotations()}")

# read samples and convert to torch tensor
samples = loopback.read_samples()
signal_tensor = torch.from_numpy(samples.astype(np.complex64))

# compute stft spectrogram (25% overlap, hann window)
nfft = 256
hop = int(nfft * 0.75)
window = torch.hann_window(nfft)

stft = torch.stft(signal_tensor, n_fft=nfft, hop_length=hop, window=window, return_complex=True)

# shift 0 Hz to center
stft = torch.fft.fftshift(stft, dim=0)

# magnitude spectrogram in db
mag_db = 20 * torch.log10(torch.abs(stft) + 1e-10)

# normalize to 0-255 and save as grayscale png
normed = (mag_db - mag_db.min()) / (mag_db.max() - mag_db.min())
spec_u8 = (normed * 255).byte().numpy()
Image.fromarray(spec_u8, mode="L").save("spectrogram.png")

print(f"spectrogram: {spec_u8.shape} (freq x time)")

Example 3: Shaped BPSK Demodulation

Generate a pulse-shaped BPSK signal, write it to SigMF, then demodulate with an RRC matched filter.

import sigmf
import numpy as np
from commpy.filters import rrcosfilter

# generate bpsk symbols with padding
np.random.seed(0x5EED)
bits = np.array([1, 0, 1, 1, 0, 1, 0, 0, 1, 1])
sps = 8  # samples per symbol
alpha = 0.35  # roll-off factor
num_taps = 4 * sps + 1  # 4 symbol-period filter

# pulse-shape with rrc and add noise
symbols = np.pad(2 * bits - 1, (num_taps // sps // 2, num_taps // sps // 2))
upsampled = np.zeros(len(symbols) * sps, dtype=np.complex64)
upsampled[::sps] = symbols
_, rrc = rrcosfilter(num_taps, alpha, Ts=1, Fs=sps)
rrc = rrc.astype(np.float32)
shaped = np.convolve(upsampled, rrc, mode="same")
noisy = shaped + 0.2 * (np.random.randn(len(shaped)) + 1j * np.random.randn(len(shaped)))

# write sigmf archive
noisy.astype(np.complex64).tofile("bpsk.sigmf-data")
meta = sigmf.SigMFFile(
    data_file="bpsk.sigmf-data",
    global_info={
        "core:datatype": "cf32_le",
        "core:sample_rate": 10000,
        "core:description": "Shaped BPSK baseband test signal",
    },
)
meta.add_capture(0)
meta.add_annotation(0, len(noisy), metadata={"core:label": "bpsk"})
meta.tofile("bpsk", toarchive=True)

# load and demodulate with matched filter
loopback = sigmf.fromfile("bpsk.sigmf")
matched = np.convolve(loopback[:], rrc, mode="same")
decoded = (matched[::sps].real > 0).astype(int)
decoded_bits = decoded[num_taps // sps // 2 : num_taps // sps // 2 + len(bits)]

print(f"original: {bits}")
print(f"decoded:  {decoded_bits}")
print(f"errors:   {np.sum(bits != decoded_bits)} / {len(bits)}")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment