Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ sample_rate = meta.sample_rate # get sample rate
# read other formats containing RF time series as SigMF
meta = sigmf.fromfile("recording.wav") # WAV
meta = sigmf.fromfile("recording.cdif") # BLUE / Platinum
meta = sigmf.fromfile("recording.xml") # Signal Hound Spike
```

### Docs
Expand Down
5 changes: 2 additions & 3 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
# pinned 2025-01-15
sphinx==8.1.3
sphinx-rtd-theme==3.0.2
sphinx>=8.0
sphinx-rtd-theme>=3.0
47 changes: 45 additions & 2 deletions docs/source/converters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Conversion is available for:

* **BLUE files** - MIDAS Blue and Platinum BLUE RF recordings (usually ``.cdif``)
* **WAV files** - Audio recordings (``.wav``)
* **Signal Hound Spike files** - Signal Hound zero-span recordings (``.xml`` + ``.iq``)

All converters return a :class:`~sigmf.SigMFFile` object with converted metadata.

Expand All @@ -29,6 +30,7 @@ formats and reads without writing any output files:
# auto-detect and create NCD for any supported format
meta = sigmf.fromfile("recording.cdif") # BLUE file
meta = sigmf.fromfile("recording.wav") # WAV file
meta = sigmf.fromfile("recording.xml") # Signal Hound Spike file
meta = sigmf.fromfile("recording.sigmf") # SigMF archive

all_samples = meta.read_samples()
Expand All @@ -44,13 +46,17 @@ For programmatic access, use the individual converter functions directly:

from sigmf.convert.wav import wav_to_sigmf
from sigmf.convert.blue import blue_to_sigmf
from sigmf.convert.signalhound import signalhound_to_sigmf

# convert WAV to SigMF archive
_ = wav_to_sigmf(wav_path="recording.wav", out_path="recording", create_archive=True)

# convert BLUE to SigMF pair and return metadata for new files
meta = blue_to_sigmf(blue_path="recording.cdif", out_path="recording")

# convert Signal Hound Spike to SigMF pair
meta = signalhound_to_sigmf(signalhound_path="recording.xml", out_path="recording")


Command Line Usage
~~~~~~~~~~~~~~~~~~
Expand All @@ -65,8 +71,9 @@ Converters are accessed through a unified command-line interface that automatica
# examples
sigmf_convert recording.cdif recording.sigmf
sigmf_convert recording.wav recording.sigmf
sigmf_convert recording.xml recording.sigmf

The converter uses magic byte detection to automatically identify BLUE and WAV file formats.
The converter uses magic byte detection to automatically identify BLUE, WAV, and Signal Hound Spike file formats.
No need to remember format-specific commands!


Expand Down Expand Up @@ -168,4 +175,40 @@ Examples

# access standard SigMF data & metadata
all_samples = meta.read_samples()
sample_rate_hz = meta.sample_rate
sample_rate_hz = meta.sample_rate


Signal Hound Spike Converter
-----------------------------

The Signal Hound Spike converter handles recordings from Signal Hound devices.
These recordings consist of two files: an XML metadata file (``.xml``) and a binary IQ data file (``.iq``).
The converter extracts metadata from the XML file and references the IQ data file, storing Signal Hound-specific
fields in the ``spike:`` namespace extension.

.. autofunction:: sigmf.convert.signalhound.signalhound_to_sigmf

Examples
~~~~~~~~

.. code-block:: python

from sigmf.convert.signalhound import signalhound_to_sigmf

# standard conversion (provide path to XML file)
meta = signalhound_to_sigmf(signalhound_path="recording.xml", out_path="recording")

# create NCD automatically (metadata-only, references original .iq file)
meta = signalhound_to_sigmf(signalhound_path="recording.xml")

# access standard SigMF data & metadata
all_samples = meta.read_samples()
sample_rate = meta.sample_rate
center_freq = meta.get_captures()[0]["core:frequency"]

# access Signal Hound-specific metadata in spike: namespace
reference_level_dbm = meta.get_global_field("spike:reference_level_dbm")
scale_factor_mw = meta.get_global_field("spike:scale_factor_mw")
if_bandwidth_hz = meta.get_global_field("spike:if_bandwidth_hz")
iq_filename = meta.get_global_field("spike:iq_filename") # original IQ file name
preview_trace = meta.get_global_field("spike:preview_trace") # max-hold trace
1 change: 1 addition & 0 deletions docs/source/developers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ To build the docs and host locally:
.. code-block:: console

$ cd docs
$ pip install -r requirements.txt
$ make clean
$ make html
$ python3 -m http.server --directory build/html/
Expand Down
91 changes: 91 additions & 0 deletions sigmf/convert/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Copyright: Multiple Authors
#
# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python
#
# SPDX-License-Identifier: LGPL-3.0-or-later

"""Convert non-SigMF recordings to SigMF format"""

from pathlib import Path

from ..error import SigMFConversionError


def get_magic_bytes(file_path: Path, count: int = 4, offset: int = 0) -> bytes:
"""
Get magic bytes from a file to help identify file type.

Parameters
----------
file_path : Path
Path to the file to read magic bytes from.
count : int, optional
Number of bytes to read. Default is 4.
offset : int, optional
Byte offset to start reading from. Default is 0.

Returns
-------
bytes
Magic bytes from the file.

Raises
------
SigMFConversionError
If file cannot be read or is too small.
"""
try:
with open(file_path, "rb") as handle:
handle.seek(offset)
magic_bytes = handle.read(count)
if len(magic_bytes) < count:
raise SigMFConversionError(f"File {file_path} too small to read {count} magic bytes at offset {offset}")
return magic_bytes
except OSError as err:
raise SigMFConversionError(f"Failed to read magic bytes from {file_path}: {err}") from err


def detect_converter(file_path: Path):
"""
Detect the appropriate converter for a non-SigMF file.

Parameters
----------
file_path : Path
Path to the file to detect.

Returns
-------
str
The converter name: "wav", "blue", or "signalhound"

Raises
------
SigMFConversionError
If the file format is not supported or cannot be detected.
"""
magic_bytes = get_magic_bytes(file_path, count=4, offset=0)

if magic_bytes == b"RIFF":
return "wav"

elif magic_bytes == b"BLUE":
return "blue"

elif magic_bytes == b"<?xm": # <?xml version="1.0" encoding="UTF-8"?>
# Check if it's a Signal Hound Spike file
# Skip XML declaration (40 bytes) and check for SignalHoundIQFile root element
expanded_magic_bytes = get_magic_bytes(file_path, count=17, offset=40)
if expanded_magic_bytes == b"SignalHoundIQFile":
return "signalhound"
else:
raise SigMFConversionError(
f"Unsupported XML file format. Root element: {expanded_magic_bytes}. "
f"Expected SignalHoundIQFile for Signal Hound Spike files."
)

else:
raise SigMFConversionError(
f"Unsupported file format. Magic bytes: {magic_bytes}. "
f"Supported formats for conversion are WAV, BLUE/Platinum, and Signal Hound Spike."
)
40 changes: 12 additions & 28 deletions sigmf/convert/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@

from .. import __version__ as toolversion
from ..error import SigMFConversionError
from ..utils import get_magic_bytes
from . import detect_converter
from .blue import blue_to_sigmf
from .signalhound import signalhound_to_sigmf
from .wav import wav_to_sigmf


Expand Down Expand Up @@ -60,8 +61,8 @@ def main() -> None:
exclusive_group.add_argument(
"--ncd", action="store_true", help="Output .sigmf-meta only and process as a Non-Conforming Dataset (NCD)"
)
parser.add_argument("--overwrite", action="store_true", help="Overwrite existing output files")
parser.add_argument("--version", action="version", version=f"%(prog)s v{toolversion}")

args = parser.parse_args()

level_lut = {
Expand All @@ -85,33 +86,16 @@ def main() -> None:
if output_path.is_dir():
raise SigMFConversionError(f"Output path must be a filename, not a directory: {output_path}")

# detect file type using magic bytes (same logic as fromfile())
magic_bytes = get_magic_bytes(input_path, count=4, offset=0)

if magic_bytes == b"RIFF":
# WAV file
_ = wav_to_sigmf(
wav_path=input_path,
out_path=output_path,
create_archive=args.archive,
create_ncd=args.ncd,
overwrite=args.overwrite,
)

elif magic_bytes == b"BLUE":
# BLUE file
_ = blue_to_sigmf(
blue_path=input_path,
out_path=output_path,
create_archive=args.archive,
create_ncd=args.ncd,
overwrite=args.overwrite,
)
# detect file type using magic bytes
converter_type = detect_converter(input_path)

else:
raise SigMFConversionError(
f"Unsupported file format. Magic bytes: {magic_bytes}. "
f"Supported formats for conversion are WAV and BLUE/Platinum."
if converter_type == "wav":
_ = wav_to_sigmf(wav_path=input_path, out_path=output_path, create_archive=args.archive, create_ncd=args.ncd)
elif converter_type == "blue":
_ = blue_to_sigmf(blue_path=input_path, out_path=output_path, create_archive=args.archive, create_ncd=args.ncd)
elif converter_type == "signalhound":
_ = signalhound_to_sigmf(
signalhound_path=input_path, out_path=output_path, create_archive=args.archive, create_ncd=args.ncd
)


Expand Down
1 change: 0 additions & 1 deletion sigmf/convert/blue.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import numpy as np
from packaging.version import InvalidVersion, Version

from .. import __version__ as toolversion
from ..error import SigMFConversionError
from ..sigmffile import SigMFFile, fromfile, get_sigmf_filenames
from ..utils import SIGMF_DATETIME_ISO8601_FMT
Expand Down
Loading
Loading