Skip to content

Commit 407c11e

Browse files
committed
Documentation & Simplified Install
* drop scipy optional dependency and [apps] entirely * add documentation for converters * update converter entry points
1 parent 1358927 commit 407c11e

10 files changed

Lines changed: 138 additions & 37 deletions

File tree

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
- name: Install dependencies
2323
run: |
2424
python -m pip install --upgrade pip
25-
pip install .[test,apps]
25+
pip install .[test]
2626
- name: Test with pytest
2727
run: |
2828
coverage run

.readthedocs.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ python:
1818
path: .
1919
extra_requirements:
2020
- test
21-
- apps
2221
- requirements: docs/requirements.txt
2322

2423
# Build documentation in the "docs/" directory with Sphinx

docs/source/api.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ SigMF API
77
:template: custom-module-template.rst
88
:recursive:
99

10-
sigmf.apps.convert_wav
1110
sigmf.archive
1211
sigmf.archivereader
12+
sigmf.convert.blue
13+
sigmf.convert.wav
1314
sigmf.error
1415
sigmf.schema
1516
sigmf.sigmf_hash

docs/source/converters.rst

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
=================
2+
Format Converters
3+
=================
4+
5+
The SigMF Python library includes converters to import data from various file formats into SigMF format.
6+
These converters make it easy to migrate existing RF recordings to the standardized SigMF format while preserving metadata when possible.
7+
8+
Overview
9+
--------
10+
11+
Converters are available for:
12+
13+
* **BLUE files** - MIDAS Blue and Platinum BLUE RF recordings (``.cdif``)
14+
* **WAV files** - Audio recordings (``.wav``)
15+
16+
All converters return a :class:`~sigmf.SigMFFile` object that can be used immediately or saved to disk.
17+
Converters preserve datatypes and metadata where possible.
18+
19+
20+
Command Line Usage
21+
~~~~~~~~~~~~~~~~~~
22+
23+
Converters can be used from the command line after ``pip install sigmf``:
24+
25+
.. code-block:: bash
26+
27+
sigmf_convert_blue input.cdif
28+
sigmf_convert_wav input.wav
29+
30+
or by using module syntax:
31+
32+
.. code-block:: bash
33+
34+
python3 -m sigmf.convert.blue input.cdif
35+
python3 -m sigmf.convert.wav input.wav
36+
37+
38+
BLUE Converter
39+
--------------
40+
41+
The BLUE converter handles CDIF (.cdif) recordings while placing BLUE header information into the following global fields:
42+
43+
* ``blue:fixed`` - fixed header information (at start of file)
44+
* ``blue:adjunct`` - adjunct header information (after fixed header)
45+
* ``blue:extended`` - extended header information (at end of file)
46+
* ``blue:keywords`` - user-defined key-value pairs
47+
48+
.. autofunction:: sigmf.convert.blue.blue_to_sigmf
49+
50+
51+
.. code-block:: python
52+
53+
from sigmf.convert.blue import blue_to_sigmf
54+
55+
# read BLUE, write SigMF, and return SigMFFile object
56+
meta = blue_to_sigmf(blue_path="recording.cdif", out_path="recording.sigmf")
57+
58+
# access converted data
59+
samples = meta.read_samples()
60+
sample_rate_hz = meta.sample_rate
61+
62+
# access BLUE-specific metadata
63+
blue_type = meta.get_global_field("blue:fixed")["type"] # e.g., 1000
64+
blue_version = meta.get_global_field("blue:keywords")["IO"] # e.g., "X-Midas"
65+
66+
67+
WAV Converter
68+
-------------
69+
70+
This is useful when working with audio datasets.
71+
72+
.. autofunction:: sigmf.convert.wav.wav_to_sigmf
73+
74+
75+
.. code-block:: python
76+
77+
from sigmf.convert.wav import wav_to_sigmf
78+
79+
# read WAV, write SigMF, and return SigMFFile object
80+
meta = wav_to_sigmf(wav_path="recording.wav", out_path="recording.sigmf")
81+
82+
# access converted data
83+
samples = meta.read_samples()
84+
sample_rate_hz = meta.sample_rate

docs/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ To get started, see the :doc:`quickstart` section or learn how to :ref:`install`
2323

2424
quickstart
2525
advanced
26+
converters
2627
developers
2728

2829
.. toctree::

pyproject.toml

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,15 @@ dependencies = [
3333

3434
[project.scripts]
3535
sigmf_validate = "sigmf.validate:main"
36-
sigmf_convert_wav = "sigmf.apps.convert_wav:main [apps]"
37-
sigmf_convert_blue = "sigmf.apps.convert_blue:main [apps]"
36+
sigmf_convert_wav = "sigmf.convert.wav:main"
37+
sigmf_convert_blue = "sigmf.convert.blue:main"
3838
[project.optional-dependencies]
3939
test = [
4040
"pylint",
4141
"pytest",
4242
"pytest-cov",
4343
"hypothesis", # next-gen testing framework
4444
]
45-
apps = [
46-
"scipy", # for wav i/o
47-
]
4845

4946
[tool.setuptools]
5047
packages = ["sigmf"]
@@ -107,6 +104,6 @@ legacy_tox_ini = '''
107104
108105
[testenv]
109106
usedevelop = True
110-
deps = .[test,apps]
107+
deps = .[test]
111108
commands = coverage run
112109
'''
Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@
1010
import getpass
1111
import logging
1212
import tempfile
13+
import wave
1314
from datetime import datetime, timezone
14-
from os import PathLike
1515
from pathlib import Path
1616
from typing import Optional
1717

18-
from scipy.io import wavfile
18+
import numpy as np
1919

2020
from .. import SigMFFile
2121
from .. import __version__ as toolversion
@@ -25,19 +25,27 @@
2525
log = logging.getLogger()
2626

2727

28-
def convert_wav(
28+
def wav_to_sigmf(
2929
wav_path: str,
3030
out_path: Optional[str] = None,
3131
to_archive: bool = True,
3232
author: Optional[str] = None,
33-
) -> PathLike:
33+
) -> SigMFFile:
3434
"""
35-
Read a wav and write a sigmf archive.
35+
Read a wav, write a sigmf, return SigMFFile object.
36+
37+
Note: Can only read PCM wav files. Use scipy.io.wavefile for broader support.
3638
"""
3739
wav_path = Path(wav_path)
3840
wav_stem = wav_path.stem
39-
samp_rate, wav_data = wavfile.read(wav_path)
40-
41+
with wave.open(str(wav_path), "rb") as wav_reader:
42+
n_channels = wav_reader.getnchannels()
43+
samp_width = wav_reader.getsampwidth()
44+
samp_rate = wav_reader.getframerate()
45+
n_frames = wav_reader.getnframes()
46+
raw_data = wav_reader.readframes(n_frames)
47+
np_dtype = f"int{samp_width * 8}"
48+
wav_data = np.frombuffer(raw_data, dtype=np_dtype).reshape(-1, n_channels)
4149
global_info = {
4250
SigMFFile.AUTHOR_KEY: getpass.getuser() if author is None else author,
4351
SigMFFile.DATATYPE_KEY: get_data_type_str(wav_data),
@@ -93,7 +101,11 @@ def main() -> None:
93101
}
94102
logging.basicConfig(level=level_lut[min(args.verbose, 2)])
95103

96-
_ = convert_wav(
104+
_ = wav_to_sigmf(
97105
wav_path=args.input,
98106
author=args.author,
99107
)
108+
109+
110+
if __name__ == "__main__":
111+
main()

tests/test_convert.py

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,14 @@
99
import os
1010
import tempfile
1111
import unittest
12+
import wave
1213
from pathlib import Path
1314

1415
import numpy as np
1516

16-
try:
17-
from scipy.io import wavfile
18-
19-
SCIPY_AVAILABLE = True
20-
except ImportError:
21-
SCIPY_AVAILABLE = False
22-
2317
import sigmf
24-
from sigmf.apps.convert_blue import convert_blue
25-
from sigmf.apps.convert_wav import convert_wav
18+
from sigmf.convert.blue import blue_to_sigmf
19+
from sigmf.convert.wav import wav_to_sigmf
2620

2721
BLUE_ENV_VAR = "NONSIGMF_RECORDINGS_PATH"
2822

@@ -32,8 +26,6 @@ class TestWAVConverter(unittest.TestCase):
3226

3327
def setUp(self) -> None:
3428
"""create temp wav file for testing"""
35-
if not SCIPY_AVAILABLE:
36-
self.skipTest("scipy is required for WAV file tests")
3729
self.tmp_dir = tempfile.TemporaryDirectory()
3830
self.tmp_path = Path(self.tmp_dir.name)
3931
self.wav_path = self.tmp_path / "foo.wav"
@@ -42,19 +34,28 @@ def setUp(self) -> None:
4234
ttt = np.linspace(0, duration_s, int(samp_rate * duration_s), endpoint=False)
4335
freq = 440 # A4 note
4436
self.audio_data = 0.5 * np.sin(2 * np.pi * freq * ttt)
45-
wavfile.write(self.wav_path, samp_rate, self.audio_data.astype(np.float32))
37+
# note scipy could write float wav files directly,
38+
# but to avoid adding scipy as a dependency for sigmf-python,
39+
# convert float audio to 16-bit PCM integer format
40+
audio_int16 = (self.audio_data * 32767).astype(np.int16)
41+
42+
# write wav file using built-in wave module
43+
with wave.open(str(self.wav_path), "wb") as wav_file:
44+
wav_file.setnchannels(1) # mono
45+
wav_file.setsampwidth(2) # 16-bit = 2 bytes
46+
wav_file.setframerate(samp_rate)
47+
wav_file.writeframes(audio_int16.tobytes())
4648

4749
def tearDown(self) -> None:
4850
"""clean up temporary directory"""
4951
self.tmp_dir.cleanup()
5052

5153
def test_wav_to_sigmf(self):
5254
sigmf_path = self.tmp_path / "bar"
53-
_ = convert_wav(wav_path=self.wav_path, out_path=sigmf_path)
54-
meta = sigmf.fromfile(sigmf_path)
55+
meta = wav_to_sigmf(wav_path=self.wav_path, out_path=sigmf_path)
5556
data = meta.read_samples()
56-
# allow small numerical differences due to data type conversions
57-
self.assertTrue(np.allclose(self.audio_data, data, atol=1e-8))
57+
# allow numerical differences due to PCM conversion
58+
self.assertTrue(np.allclose(self.audio_data, data, atol=1e-4))
5859

5960

6061
class TestBlueConverter(unittest.TestCase):
@@ -79,18 +80,24 @@ def tearDown(self) -> None:
7980

8081
def test_blue_to_sigmf(self):
8182
for bdx, bluefile in enumerate(self.bluefiles):
82-
sigmf_path = self.tmp_path / f"converted_{bdx}"
83-
_ = convert_blue(blue_path=bluefile, out_path=sigmf_path)
84-
meta = sigmf.fromfile(sigmf_path)
83+
sigmf_path = self.tmp_path / bluefile.stem
84+
meta = blue_to_sigmf(blue_path=bluefile, out_path=sigmf_path)
8585

8686
### EVERYTHING BELOW HERE IS FOR DEBUGGING ONLY _ REMOVE LATER ###
8787
# plot stft of RF data for visual inspection
88+
import matplotlib.pyplot as plt
8889
from scipy.signal import spectrogram
90+
from swiftfox import summary
8991

9092
samples = meta.read_samples()
93+
plt.figure(figsize=(10, 10))
94+
summary(samples, detail=0.1, samp_rate=meta.get_global_field("core:sample_rate"))
95+
plt.figure()
96+
plt.plot(samples.real)
97+
plt.plot(samples.imag)
98+
9199
freqs, times, spec = spectrogram(samples, fs=meta.get_global_field("core:sample_rate"), nperseg=1024)
92100
# use imshow to plot spectrogram
93-
import matplotlib.pyplot as plt
94101

95102
plt.figure()
96103
plt.imshow(

0 commit comments

Comments
 (0)