Examples demonstrating sigmf-python for creating, reading, and processing SigMF recordings.
This module follows the SigMF specification html/pdf from the spec repository.
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 SigMFMemory-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) # equivalentMetadata 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") # explicitSigMF 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 schemaGenerate 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")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)")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)}")