diff --git a/README.md b/README.md index 56242e8..1837e60 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -

Rendered SigMF Logo

+

Rendered SigMF Logo

This python module makes it easy to interact with Signal Metadata Format (SigMF) recordings. This module works with Python 3.7+ and is distributed freely under the terms GNU Lesser GPL v3 License. The [SigMF specification document](https://github.com/sigmf/SigMF/blob/HEAD/sigmf-spec.md) -is located in the [SigMF](https://github.com/gnuradio/SigMF) repository. +is located in the [SigMF](https://github.com/sigmf/SigMF) repository. # Installation diff --git a/pyproject.toml b/pyproject.toml index ee425f7..15422cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "SigMF" description = "Easily interact with Signal Metadata Format (SigMF) recordings." -keywords = ["gnuradio"] +keywords = ["gnuradio", "radio"] classifiers = [ "Development Status :: 5 - Production/Stable", "License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", @@ -13,6 +13,8 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Topic :: Scientific/Engineering", + "Topic :: Communications :: Ham Radio", ] dynamic = ["version", "readme"] requires-python = ">=3.7" diff --git a/sigmf/__init__.py b/sigmf/__init__.py index e11844a..38eed7b 100644 --- a/sigmf/__init__.py +++ b/sigmf/__init__.py @@ -1,19 +1,22 @@ # Copyright: Multiple Authors # -# This file is part of SigMF. https://github.com/sigmf/sigmf-python +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python # # SPDX-License-Identifier: LGPL-3.0-or-later -__version__ = "1.2.0" +# version of this python module +__version__ = "1.2.1" +# matching version of the SigMF specification +__specification__ = "1.2.0" from .archive import SigMFArchive from .sigmffile import SigMFFile, SigMFCollection from .archivereader import SigMFArchiveReader from . import archive +from . import archivereader from . import error from . import schema from . import sigmffile -from . import validate from . import utils -from . import archivereader +from . import validate diff --git a/sigmf/apps/convert_wav.py b/sigmf/apps/convert_wav.py index 638bd46..9151d2f 100755 --- a/sigmf/apps/convert_wav.py +++ b/sigmf/apps/convert_wav.py @@ -1,29 +1,35 @@ # Copyright: Multiple Authors # -# This file is part of SigMF. https://github.com/sigmf/sigmf-python +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python # # SPDX-License-Identifier: LGPL-3.0-or-later """converter for wav containers""" -import os -import tempfile -import datetime -import pathlib import argparse +import datetime import getpass +import logging +import os +import pathlib +import tempfile from scipy.io import wavfile +from .. import SigMFFile, __specification__ +from .. import __version__ as toolversion from .. import archive -from ..sigmffile import SigMFFile from ..utils import get_data_type_str +log = logging.getLogger() + def convert_wav(input_wav_filename, archive_filename=None, start_datetime=None, author=None): """ read a .wav and write a .sigmf archive """ + input_path = pathlib.Path(input_wav_filename) + input_stem = input_path.stem samp_rate, wav_data = wavfile.read(input_wav_filename) global_info = { @@ -33,11 +39,11 @@ def convert_wav(input_wav_filename, archive_filename=None, start_datetime=None, SigMFFile.NUM_CHANNELS_KEY: 1 if len(wav_data.shape) < 2 else wav_data.shape[1], SigMFFile.RECORDER_KEY: os.path.basename(__file__), SigMFFile.SAMPLE_RATE_KEY: samp_rate, + SigMFFile.VERSION_KEY: __specification__, } if start_datetime is None: - fname = pathlib.Path(input_wav_filename) - mtime = datetime.datetime.fromtimestamp(fname.stat().st_mtime) + mtime = datetime.datetime.fromtimestamp(input_path.stat().st_mtime) start_datetime = mtime.isoformat() + "Z" capture_info = {SigMFFile.START_INDEX_KEY: 0} @@ -45,7 +51,7 @@ def convert_wav(input_wav_filename, archive_filename=None, start_datetime=None, capture_info[SigMFFile.DATETIME_KEY] = start_datetime tmpdir = tempfile.mkdtemp() - sigmf_data_filename = input_wav_filename + archive.SIGMF_DATASET_EXT + sigmf_data_filename = input_stem + archive.SIGMF_DATASET_EXT sigmf_data_path = os.path.join(tmpdir, sigmf_data_filename) wav_data.tofile(sigmf_data_path) @@ -53,7 +59,7 @@ def convert_wav(input_wav_filename, archive_filename=None, start_datetime=None, meta.add_capture(0, metadata=capture_info) if archive_filename is None: - archive_filename = os.path.basename(input_wav_filename) + archive.SIGMF_ARCHIVE_EXT + archive_filename = input_stem + archive.SIGMF_ARCHIVE_EXT meta.tofile(archive_filename, toarchive=True) return os.path.abspath(archive_filename) @@ -65,13 +71,22 @@ def main(): parser = argparse.ArgumentParser(description="Convert .wav to .sigmf container.") parser.add_argument("input", type=str, help="Wavfile path") parser.add_argument("--author", type=str, default=None, help=f"set {SigMFFile.AUTHOR_KEY} metadata") + parser.add_argument('-v', '--verbose', action='count', default=0) + parser.add_argument('--version', action='version', version=f'%(prog)s v{toolversion}') args = parser.parse_args() + level_lut = { + 0: logging.WARNING, + 1: logging.INFO, + 2: logging.DEBUG, + } + logging.basicConfig(level=level_lut[min(args.verbose, 2)]) + out_fname = convert_wav( input_wav_filename=args.input, author=args.author, ) - print("Wrote", out_fname) + log.info(f"Write {out_fname}") if __name__ == "__main__": diff --git a/sigmf/apps/gui.py b/sigmf/apps/gui.py index ac51da3..9beda03 100644 --- a/sigmf/apps/gui.py +++ b/sigmf/apps/gui.py @@ -1,17 +1,20 @@ # Copyright: Multiple Authors # -# This file is part of SigMF. https://github.com/sigmf/sigmf-python +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python # # SPDX-License-Identifier: LGPL-3.0-or-later '''GUI for creating & editing SigMF Files''' -import os +import argparse import logging +import os + from PySimpleGUI import * -from ..sigmffile import SigMFFile, fromarchive, dtype_info +from .. import __version__ as toolversion from ..archive import SIGMF_ARCHIVE_EXT +from ..sigmffile import SigMFFile, dtype_info, fromarchive log = logging.getLogger() @@ -381,13 +384,10 @@ def add_capture(capture_data_input, values, capture_selector_dict, file_data, fr def main(): - import argparse - from sigmf import __version__ as toolversion - parser = argparse.ArgumentParser(description='Edit SigMF Archive.') parser.add_argument('-i', '--input', help='Input SigMF Archive Path.', default=None) parser.add_argument('-v', '--verbose', action='count', default=0) - parser.add_argument('--version', action='version', version=f'%(prog)s {toolversion}') + parser.add_argument('--version', action='version', version=f'%(prog)s v{toolversion}') args = parser.parse_args() level_lut = { @@ -638,3 +638,6 @@ def main(): break window.Close() + +if __name__ == "__main__": + main() diff --git a/sigmf/archive.py b/sigmf/archive.py index de6bd50..897c0ee 100644 --- a/sigmf/archive.py +++ b/sigmf/archive.py @@ -1,6 +1,6 @@ # Copyright: Multiple Authors # -# This file is part of SigMF. https://github.com/sigmf/sigmf-python +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python # # SPDX-License-Identifier: LGPL-3.0-or-later @@ -13,7 +13,6 @@ from .error import SigMFFileError - SIGMF_ARCHIVE_EXT = ".sigmf" SIGMF_METADATA_EXT = ".sigmf-meta" SIGMF_DATASET_EXT = ".sigmf-data" diff --git a/sigmf/archivereader.py b/sigmf/archivereader.py index 5759b74..556e84d 100644 --- a/sigmf/archivereader.py +++ b/sigmf/archivereader.py @@ -1,6 +1,6 @@ # Copyright: Multiple Authors # -# This file is part of SigMF. https://github.com/gnuradio/SigMF +# This file is part of sigmf-python. https://github.com/sigmf/SigMF # # SPDX-License-Identifier: LGPL-3.0-or-later @@ -11,11 +11,11 @@ import tarfile import tempfile -from . import __version__ #, schema, sigmf_hash, validate +from . import __version__ +from .archive import SIGMF_ARCHIVE_EXT, SIGMF_DATASET_EXT, SIGMF_METADATA_EXT, SigMFArchive +from .error import SigMFFileError from .sigmffile import SigMFFile -from .archive import SigMFArchive, SIGMF_DATASET_EXT, SIGMF_METADATA_EXT, SIGMF_ARCHIVE_EXT from .utils import dict_merge -from .error import SigMFFileError class SigMFArchiveReader(): diff --git a/sigmf/error.py b/sigmf/error.py index df4e2ae..92ac194 100644 --- a/sigmf/error.py +++ b/sigmf/error.py @@ -1,6 +1,6 @@ # Copyright: Multiple Authors # -# This file is part of SigMF. https://github.com/sigmf/sigmf-python +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python # # SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/sigmf/schema-collection.json b/sigmf/schema-collection.json index b4540b5..96b28f3 100644 --- a/sigmf/schema-collection.json +++ b/sigmf/schema-collection.json @@ -1,5 +1,5 @@ { - "$id": "https://github.com/gnuradio/SigMF", + "$id": "https://github.com/sigmf/SigMF", "$schema": "http://json-schema.org/draft-07/schema", "default": {}, "required": [ @@ -20,7 +20,7 @@ "$id": "#/properties/collection/properties/core%3Aversion", "description": "The version of the SigMF specification used to create the Collection file.", "examples": [ - "1.0.0" + "1.2.0" ], "type": "string" }, diff --git a/sigmf/schema-meta.json b/sigmf/schema-meta.json index c3b7ad9..9e83cf3 100644 --- a/sigmf/schema-meta.json +++ b/sigmf/schema-meta.json @@ -1,5 +1,5 @@ { - "$id": "https://github.com/gnuradio/SigMF", + "$id": "https://github.com/sigmf/SigMF", "$schema": "http://json-schema.org/draft-07/schema", "default": [ "global", diff --git a/sigmf/schema.py b/sigmf/schema.py index a1ca14f..a34c5d2 100644 --- a/sigmf/schema.py +++ b/sigmf/schema.py @@ -1,13 +1,13 @@ # Copyright: Multiple Authors # -# This file is part of SigMF. https://github.com/sigmf/sigmf-python +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python # # SPDX-License-Identifier: LGPL-3.0-or-later '''Schema IO''' -import os import json +import os from . import utils diff --git a/sigmf/sigmf_hash.py b/sigmf/sigmf_hash.py index 5f07768..414dc36 100644 --- a/sigmf/sigmf_hash.py +++ b/sigmf/sigmf_hash.py @@ -1,6 +1,6 @@ # Copyright: Multiple Authors # -# This file is part of SigMF. https://github.com/sigmf/sigmf-python +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python # # SPDX-License-Identifier: LGPL-3.0-or-later diff --git a/sigmf/sigmffile.py b/sigmf/sigmffile.py index 91a31b7..098d65d 100644 --- a/sigmf/sigmffile.py +++ b/sigmf/sigmffile.py @@ -1,24 +1,26 @@ # Copyright: Multiple Authors # -# This file is part of SigMF. https://github.com/sigmf/sigmf-python +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python # # SPDX-License-Identifier: LGPL-3.0-or-later '''SigMFFile Object''' -from collections import OrderedDict import codecs import json import tarfile import tempfile -from os import path import warnings +from collections import OrderedDict +from os import path + import numpy as np -from . import __version__, schema, sigmf_hash, validate -from .archive import SigMFArchive, SIGMF_DATASET_EXT, SIGMF_METADATA_EXT, SIGMF_ARCHIVE_EXT, SIGMF_COLLECTION_EXT +from . import __specification__, __version__, schema, sigmf_hash, validate +from .archive import SIGMF_ARCHIVE_EXT, SIGMF_COLLECTION_EXT, SIGMF_DATASET_EXT, SIGMF_METADATA_EXT, SigMFArchive +from .error import SigMFAccessError, SigMFFileError from .utils import dict_merge -from .error import SigMFFileError, SigMFAccessError + class SigMFMetafile(): VALID_KEYS = {} @@ -174,7 +176,7 @@ def __init__(self, metadata=None, data_file=None, global_info=None, skip_checksu if metadata is None: self._metadata = {self.GLOBAL_KEY:{}, self.CAPTURE_KEY:[], self.ANNOTATION_KEY:[]} self._metadata[self.GLOBAL_KEY][self.NUM_CHANNELS_KEY] = 1 - self._metadata[self.GLOBAL_KEY][self.VERSION_KEY] = "1.0.0" + self._metadata[self.GLOBAL_KEY][self.VERSION_KEY] = __specification__ elif isinstance(metadata, dict): self._metadata = metadata else: diff --git a/sigmf/utils.py b/sigmf/utils.py index 18c7cf8..2e61b42 100644 --- a/sigmf/utils.py +++ b/sigmf/utils.py @@ -1,16 +1,17 @@ # Copyright: Multiple Authors # -# This file is part of SigMF. https://github.com/sigmf/sigmf-python +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python # # SPDX-License-Identifier: LGPL-3.0-or-later """Utilities""" +import re +import sys from copy import deepcopy from datetime import datetime -import sys + import numpy as np -import re from . import error diff --git a/sigmf/validate.py b/sigmf/validate.py index ce18fcc..95f8900 100644 --- a/sigmf/validate.py +++ b/sigmf/validate.py @@ -1,14 +1,19 @@ # Copyright: Multiple Authors # -# This file is part of SigMF. https://github.com/sigmf/sigmf-python +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python # # SPDX-License-Identifier: LGPL-3.0-or-later '''SigMF Validator''' +import argparse +import json +import logging + import jsonschema -from . import schema +from . import __version__ as toolversion +from . import error, schema, sigmffile def extend_with_default(validator_class): @@ -80,15 +85,6 @@ def validate(metadata, ref_schema=schema.get_schema()): def main(): - import argparse - import logging - import json - - from . import sigmffile - from . import error - - from sigmf import __version__ as toolversion - parser = argparse.ArgumentParser(description='Validate SigMF Archive or file pair against JSON schema.', prog='sigmf_validate') parser.add_argument('filename', help='SigMF path (extension optional).') diff --git a/tests/conftest.py b/tests/conftest.py index f8f6aa4..0e46aaf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,27 +1,16 @@ -# Copyright 2017 GNU Radio Foundation +# Copyright: Multiple Authors # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. +# SPDX-License-Identifier: LGPL-3.0-or-later + +"""Provides pytest fixtures for other tests.""" import tempfile import pytest +from sigmf import __specification__ from sigmf.sigmffile import SigMFFile from .testdata import TEST_FLOAT32_DATA, TEST_METADATA @@ -38,11 +27,11 @@ def test_data_file(): @pytest.fixture def test_sigmffile(test_data_file): """If pytest uses this signature, will return valid SigMF file.""" - sigf = SigMFFile() - sigf.set_global_field("core:datatype", "rf32_le") - sigf.set_global_field("core:version", "1.0.0") - sigf.add_annotation(start_index=0, length=len(TEST_FLOAT32_DATA)) - sigf.add_capture(start_index=0) - sigf.set_data_file(test_data_file.name) - assert sigf._metadata == TEST_METADATA - return sigf + meta = SigMFFile() + meta.set_global_field("core:datatype", "rf32_le") + meta.set_global_field("core:version", __specification__) + meta.add_annotation(start_index=0, length=len(TEST_FLOAT32_DATA)) + meta.add_capture(start_index=0) + meta.set_data_file(test_data_file.name) + assert meta._metadata == TEST_METADATA + return meta diff --git a/tests/test_archive.py b/tests/test_archive.py index 5c3d67b..eccb1b4 100644 --- a/tests/test_archive.py +++ b/tests/test_archive.py @@ -1,12 +1,20 @@ +# Copyright: Multiple Authors +# +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +"""Tests for SigMFArchive""" + import codecs import json import tarfile import tempfile from os import path +import jsonschema import numpy as np import pytest -import jsonschema from sigmf import error from sigmf.archive import SIGMF_DATASET_EXT, SIGMF_METADATA_EXT @@ -75,7 +83,7 @@ def test_unwritable_fileobj_throws_fileerror(test_sigmffile): def test_unwritable_name_throws_fileerror(test_sigmffile): # Cannot assume /root/ is unwritable (e.g. Docker environment) # so use invalid filename - unwritable_file = '/bad_name/' + unwritable_file = "/bad_name/" with pytest.raises(error.SigMFFileError): test_sigmffile.archive(name=unwritable_file) diff --git a/tests/test_archivereader.py b/tests/test_archivereader.py index 1818285..a6053f5 100644 --- a/tests/test_archivereader.py +++ b/tests/test_archivereader.py @@ -1,10 +1,17 @@ -# Copyright 2023 GNU Radio Foundation +# Copyright: Multiple Authors +# +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +"""Tests for SigMFArchiveReader""" + import tempfile -import numpy as np import unittest -import sigmf -from sigmf import SigMFFile, SigMFArchiveReader +import numpy as np + +from sigmf import SigMFArchiveReader, SigMFFile, __specification__ class TestArchiveReader(unittest.TestCase): @@ -42,7 +49,7 @@ def test_access_data_without_untar(self): global_info={ SigMFFile.DATATYPE_KEY: f"{complex_prefix}{key}_le", SigMFFile.NUM_CHANNELS_KEY: num_channels, - SigMFFile.VERSION_KEY: "1.0.0", + SigMFFile.VERSION_KEY: __specification__, }, ) temp_meta.tofile(temp_archive, toarchive=True) diff --git a/tests/test_sigmffile.py b/tests/test_sigmffile.py index 55672a3..54c3be6 100644 --- a/tests/test_sigmffile.py +++ b/tests/test_sigmffile.py @@ -1,31 +1,20 @@ -# Copyright 2017 GNU Radio Foundation +# Copyright: Multiple Authors # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. +# SPDX-License-Identifier: LGPL-3.0-or-later + +"""Tests for SigMFFile Object""" +import copy +import json import os import shutil import tempfile -import json +import unittest from pathlib import Path + import numpy as np -import unittest -import copy from sigmf import sigmffile, utils from sigmf.sigmffile import SigMFFile @@ -62,8 +51,8 @@ def test_iterator_basic(self): count += 1 self.assertEqual(count, len(self.sigmf_object)) -class TestAnnotationHandling(unittest.TestCase): +class TestAnnotationHandling(unittest.TestCase): def test_get_annotations_with_index(self): """Test that only annotations containing index are returned from get_annotations()""" smf = SigMFFile(copy.deepcopy(TEST_METADATA)) @@ -77,7 +66,7 @@ def test_get_annotations_with_index(self): {SigMFFile.START_INDEX_KEY: 1}, ] ) - + def test__count_samples_from_annotation(self): """Make sure sample count from annotations use correct end index""" smf = SigMFFile(copy.deepcopy(TEST_METADATA)) @@ -85,7 +74,7 @@ def test__count_samples_from_annotation(self): smf.add_annotation(start_index=4, length=4) sample_count = smf._count_samples() self.assertEqual(sample_count, 32) - + def test_set_data_file_without_annotations(self): """ Make sure setting data_file with no annotations registered does not @@ -98,7 +87,7 @@ def test_set_data_file_without_annotations(self): TEST_FLOAT32_DATA.tofile(temp_path_data) smf.set_data_file(temp_path_data) samples = smf.read_samples() - self.assertTrue(len(samples)==16) + self.assertTrue(len(samples) == 16) def test_set_data_file_with_annotations(self): """ @@ -115,7 +104,8 @@ def test_set_data_file_with_annotations(self): # Issues warning since file ends before the final annotatio smf.set_data_file(temp_path_data) samples = smf.read_samples() - self.assertTrue(len(samples)==16) + self.assertTrue(len(samples) == 16) + def simulate_capture(sigmf_md, n, capture_len): start_index = capture_len * n @@ -129,9 +119,7 @@ def simulate_capture(sigmf_md, n, capture_len): "core:longitude": -105.0 + 0.0001 * n, } - sigmf_md.add_annotation(start_index=start_index, - length=capture_len, - metadata=annotation_md) + sigmf_md.add_annotation(start_index=start_index, length=capture_len, metadata=annotation_md) def test_default_constructor(): @@ -140,7 +128,7 @@ def test_default_constructor(): def test_set_non_required_global_field(): sigf = SigMFFile() - sigf.set_global_field('this_is:not_in_the_schema', None) + sigf.set_global_field("this_is:not_in_the_schema", None) def test_add_capture(): @@ -237,89 +225,95 @@ def test_multichannel_seek(self): def test_key_validity(): - '''assure the keys in test metadata are valid''' + """assure the keys in test metadata are valid""" for top_key, top_val in TEST_METADATA.items(): if type(top_val) is dict: for core_key in top_val.keys(): - assert core_key in vars(SigMFFile)[f'VALID_{top_key.upper()}_KEYS'] + assert core_key in vars(SigMFFile)[f"VALID_{top_key.upper()}_KEYS"] elif type(top_val) is list: # annotations are in a list for annot in top_val: for core_key in annot.keys(): assert core_key in SigMFFile.VALID_ANNOTATION_KEYS else: - raise ValueError('expected list or dict') + raise ValueError("expected list or dict") def test_ordered_metadata(): - '''check to make sure the metadata is sorted as expected''' + """check to make sure the metadata is sorted as expected""" sigf = SigMFFile() - top_sort_order = ['global', 'captures', 'annotations'] + top_sort_order = ["global", "captures", "annotations"] for kdx, key in enumerate(sigf.ordered_metadata()): assert kdx == top_sort_order.index(key) def test_captures_checking(): - ''' + """ these tests make sure the various captures access tools work properly - ''' - np.array(TEST_U8_DATA0, dtype=np.uint8).tofile('/tmp/d0.sigmf-data') - with open('/tmp/d0.sigmf-meta','w') as f0: json.dump(TEST_U8_META0, f0) - np.array(TEST_U8_DATA1, dtype=np.uint8).tofile('/tmp/d1.sigmf-data') - with open('/tmp/d1.sigmf-meta','w') as f1: json.dump(TEST_U8_META1, f1) - np.array(TEST_U8_DATA2, dtype=np.uint8).tofile('/tmp/d2.sigmf-data') - with open('/tmp/d2.sigmf-meta','w') as f2: json.dump(TEST_U8_META2, f2) - np.array(TEST_U8_DATA3, dtype=np.uint8).tofile('/tmp/d3.sigmf-data') - with open('/tmp/d3.sigmf-meta','w') as f3: json.dump(TEST_U8_META3, f3) - np.array(TEST_U8_DATA4, dtype=np.uint8).tofile('/tmp/d4.sigmf-data') - with open('/tmp/d4.sigmf-meta','w') as f4: json.dump(TEST_U8_META4, f4) - - sigmf0 = sigmffile.fromfile('/tmp/d0.sigmf-meta', skip_checksum=True) - sigmf1 = sigmffile.fromfile('/tmp/d1.sigmf-meta', skip_checksum=True) - sigmf2 = sigmffile.fromfile('/tmp/d2.sigmf-meta', skip_checksum=True) - sigmf3 = sigmffile.fromfile('/tmp/d3.sigmf-meta', skip_checksum=True) - sigmf4 = sigmffile.fromfile('/tmp/d4.sigmf-meta', skip_checksum=True) + """ + np.array(TEST_U8_DATA0, dtype=np.uint8).tofile("/tmp/d0.sigmf-data") + with open("/tmp/d0.sigmf-meta", "w") as f0: + json.dump(TEST_U8_META0, f0) + np.array(TEST_U8_DATA1, dtype=np.uint8).tofile("/tmp/d1.sigmf-data") + with open("/tmp/d1.sigmf-meta", "w") as f1: + json.dump(TEST_U8_META1, f1) + np.array(TEST_U8_DATA2, dtype=np.uint8).tofile("/tmp/d2.sigmf-data") + with open("/tmp/d2.sigmf-meta", "w") as f2: + json.dump(TEST_U8_META2, f2) + np.array(TEST_U8_DATA3, dtype=np.uint8).tofile("/tmp/d3.sigmf-data") + with open("/tmp/d3.sigmf-meta", "w") as f3: + json.dump(TEST_U8_META3, f3) + np.array(TEST_U8_DATA4, dtype=np.uint8).tofile("/tmp/d4.sigmf-data") + with open("/tmp/d4.sigmf-meta", "w") as f4: + json.dump(TEST_U8_META4, f4) + + sigmf0 = sigmffile.fromfile("/tmp/d0.sigmf-meta", skip_checksum=True) + sigmf1 = sigmffile.fromfile("/tmp/d1.sigmf-meta", skip_checksum=True) + sigmf2 = sigmffile.fromfile("/tmp/d2.sigmf-meta", skip_checksum=True) + sigmf3 = sigmffile.fromfile("/tmp/d3.sigmf-meta", skip_checksum=True) + sigmf4 = sigmffile.fromfile("/tmp/d4.sigmf-meta", skip_checksum=True) assert sigmf0._count_samples() == 256 assert sigmf0._is_conforming_dataset() - assert (0,0) == sigmf0.get_capture_byte_boundarys(0) - assert (0,256) == sigmf0.get_capture_byte_boundarys(1) + assert (0, 0) == sigmf0.get_capture_byte_boundarys(0) + assert (0, 256) == sigmf0.get_capture_byte_boundarys(1) assert np.array_equal(TEST_U8_DATA0, sigmf0.read_samples(autoscale=False)) assert np.array_equal(np.array([]), sigmf0.read_samples_in_capture(0)) - assert np.array_equal(TEST_U8_DATA0, sigmf0.read_samples_in_capture(1,autoscale=False)) + assert np.array_equal(TEST_U8_DATA0, sigmf0.read_samples_in_capture(1, autoscale=False)) assert sigmf1._count_samples() == 192 assert not sigmf1._is_conforming_dataset() - assert (32,160) == sigmf1.get_capture_byte_boundarys(0) - assert (160,224) == sigmf1.get_capture_byte_boundarys(1) - assert np.array_equal(np.array(range(128)), sigmf1.read_samples_in_capture(0,autoscale=False)) - assert np.array_equal(np.array(range(128,192)), sigmf1.read_samples_in_capture(1,autoscale=False)) + assert (32, 160) == sigmf1.get_capture_byte_boundarys(0) + assert (160, 224) == sigmf1.get_capture_byte_boundarys(1) + assert np.array_equal(np.array(range(128)), sigmf1.read_samples_in_capture(0, autoscale=False)) + assert np.array_equal(np.array(range(128, 192)), sigmf1.read_samples_in_capture(1, autoscale=False)) assert sigmf2._count_samples() == 192 assert not sigmf2._is_conforming_dataset() - assert (32,160) == sigmf2.get_capture_byte_boundarys(0) - assert (176,240) == sigmf2.get_capture_byte_boundarys(1) - assert np.array_equal(np.array(range(128)), sigmf2.read_samples_in_capture(0,autoscale=False)) - assert np.array_equal(np.array(range(128,192)), sigmf2.read_samples_in_capture(1,autoscale=False)) + assert (32, 160) == sigmf2.get_capture_byte_boundarys(0) + assert (176, 240) == sigmf2.get_capture_byte_boundarys(1) + assert np.array_equal(np.array(range(128)), sigmf2.read_samples_in_capture(0, autoscale=False)) + assert np.array_equal(np.array(range(128, 192)), sigmf2.read_samples_in_capture(1, autoscale=False)) assert sigmf3._count_samples() == 192 assert not sigmf3._is_conforming_dataset() - assert (32,64) == sigmf3.get_capture_byte_boundarys(0) - assert (64,160) == sigmf3.get_capture_byte_boundarys(1) - assert (192,256) == sigmf3.get_capture_byte_boundarys(2) - assert np.array_equal(np.array(range(32)), sigmf3.read_samples_in_capture(0,autoscale=False)) - assert np.array_equal(np.array(range(32,128)), sigmf3.read_samples_in_capture(1,autoscale=False)) - assert np.array_equal(np.array(range(128,192)), sigmf3.read_samples_in_capture(2,autoscale=False)) + assert (32, 64) == sigmf3.get_capture_byte_boundarys(0) + assert (64, 160) == sigmf3.get_capture_byte_boundarys(1) + assert (192, 256) == sigmf3.get_capture_byte_boundarys(2) + assert np.array_equal(np.array(range(32)), sigmf3.read_samples_in_capture(0, autoscale=False)) + assert np.array_equal(np.array(range(32, 128)), sigmf3.read_samples_in_capture(1, autoscale=False)) + assert np.array_equal(np.array(range(128, 192)), sigmf3.read_samples_in_capture(2, autoscale=False)) assert sigmf4._count_samples() == 96 assert not sigmf4._is_conforming_dataset() - assert (32,160) == sigmf4.get_capture_byte_boundarys(0) - assert (160,224) == sigmf4.get_capture_byte_boundarys(1) - assert np.array_equal(np.array(range(64)), sigmf4.read_samples_in_capture(0,autoscale=False)[:,0]) - assert np.array_equal(np.array(range(64,96)), sigmf4.read_samples_in_capture(1,autoscale=False)[:,1]) + assert (32, 160) == sigmf4.get_capture_byte_boundarys(0) + assert (160, 224) == sigmf4.get_capture_byte_boundarys(1) + assert np.array_equal(np.array(range(64)), sigmf4.read_samples_in_capture(0, autoscale=False)[:, 0]) + assert np.array_equal(np.array(range(64, 96)), sigmf4.read_samples_in_capture(1, autoscale=False)[:, 1]) + def test_slicing(): - '''Test __getitem___ builtin for sigmffile, make sure slicing and indexing works as expected.''' + """Test __getitem___ builtin for sigmffile, make sure slicing and indexing works as expected.""" _, temp_data0 = tempfile.mkstemp() np.array(TEST_U8_DATA0, dtype=np.uint8).tofile(temp_data0) sigmf0 = SigMFFile(metadata=TEST_U8_META0, data_file=temp_data0) @@ -337,8 +331,8 @@ def test_slicing(): _, temp_data2 = tempfile.mkstemp() np.array(TEST_U8_DATA4, dtype=np.uint8).tofile(temp_data2) sigmf2 = SigMFFile(TEST_U8_META4, data_file=temp_data2) - channelized = np.array(TEST_U8_DATA4).reshape((128,2)) + channelized = np.array(TEST_U8_DATA4).reshape((128, 2)) assert np.array_equal(channelized, sigmf2[:][:]) assert np.array_equal(sigmf2[10:20, 91:112], sigmf2.read_samples(autoscale=False)[10:20, 91:112]) assert np.array_equal(sigmf2[0], channelized[0]) - assert np.array_equal(sigmf2[1,:], channelized[1,:]) + assert np.array_equal(sigmf2[1, :], channelized[1, :]) diff --git a/tests/test_utils.py b/tests/test_utils.py index 9a98ed4..e3599dc 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,3 +1,11 @@ +# Copyright: Multiple Authors +# +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python +# +# SPDX-License-Identifier: LGPL-3.0-or-later + +"""Tests for Utilities""" + from datetime import datetime import pytest diff --git a/tests/test_validation.py b/tests/test_validation.py index ce427a1..7b74be9 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -1,79 +1,66 @@ -# Copyright 2016 GNU Radio Foundation +# Copyright: Multiple Authors # -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python # -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. +# SPDX-License-Identifier: LGPL-3.0-or-later + +"""Tests for Validator""" import tempfile import unittest +from jsonschema.exceptions import ValidationError + import sigmf from sigmf import SigMFFile -from jsonschema.exceptions import ValidationError - from .testdata import TEST_FLOAT32_DATA, TEST_METADATA def test_valid_data(): - '''assure the supplied metadata is OK''' + """assure the supplied metadata is OK""" invalid_metadata = dict(TEST_METADATA) SigMFFile(TEST_METADATA).validate() + class FailingCases(unittest.TestCase): - '''Cases where the validator should throw an exception.''' + """Cases where the validator should throw an exception.""" + def setUp(self): self.metadata = dict(TEST_METADATA) def test_no_version(self): - '''core:version must be present''' + """core:version must be present""" del self.metadata[SigMFFile.GLOBAL_KEY][SigMFFile.VERSION_KEY] with self.assertRaises(ValidationError): SigMFFile(self.metadata).validate() def test_extra_top_level_key(self): - '''no extra keys allowed on the top level''' - self.metadata['extra'] = 0 + """no extra keys allowed on the top level""" + self.metadata["extra"] = 0 with self.assertRaises(ValidationError): SigMFFile(self.metadata).validate() def test_extra_top_level_key(self): - '''label must be less than 20 chars''' - self.metadata[SigMFFile.ANNOTATION_KEY][0][SigMFFile.LABEL_KEY] = 'a' * 21 + """label must be less than 20 chars""" + self.metadata[SigMFFile.ANNOTATION_KEY][0][SigMFFile.LABEL_KEY] = "a" * 21 with self.assertRaises(ValidationError): SigMFFile(self.metadata).validate() def test_invalid_type(self): - '''license key must be string''' + """license key must be string""" self.metadata[SigMFFile.GLOBAL_KEY][SigMFFile.LICENSE_KEY] = 1 with self.assertRaises(ValidationError): SigMFFile(self.metadata).validate() def test_invalid_capture_order(self): - '''metadata must have captures in order''' - self.metadata[SigMFFile.CAPTURE_KEY] = [ - {SigMFFile.START_INDEX_KEY: 10}, - {SigMFFile.START_INDEX_KEY: 9} - ] + """metadata must have captures in order""" + self.metadata[SigMFFile.CAPTURE_KEY] = [{SigMFFile.START_INDEX_KEY: 10}, {SigMFFile.START_INDEX_KEY: 9}] with self.assertRaises(ValidationError): SigMFFile(self.metadata).validate() def test_invalid_annotation_order(self): - '''metadata must have annotations in order''' + """metadata must have annotations in order""" self.metadata[SigMFFile.ANNOTATION_KEY] = [ { SigMFFile.START_INDEX_KEY: 2, @@ -82,24 +69,19 @@ def test_invalid_annotation_order(self): { SigMFFile.START_INDEX_KEY: 1, SigMFFile.LENGTH_INDEX_KEY: 120000, - } + }, ] with self.assertRaises(ValidationError): SigMFFile(self.metadata).validate() def test_annotation_without_sample_count(self): - '''annotation without length should be accepted''' - self.metadata[SigMFFile.ANNOTATION_KEY] = [ - { - SigMFFile.START_INDEX_KEY: 2 - } - ] + """annotation without length should be accepted""" + self.metadata[SigMFFile.ANNOTATION_KEY] = [{SigMFFile.START_INDEX_KEY: 2}] SigMFFile(self.metadata).validate() - def test_invalid_hash(self): _, temp_path = tempfile.mkstemp() TEST_FLOAT32_DATA.tofile(temp_path) - self.metadata[SigMFFile.GLOBAL_KEY][SigMFFile.HASH_KEY] = 'derp' + self.metadata[SigMFFile.GLOBAL_KEY][SigMFFile.HASH_KEY] = "derp" with self.assertRaises(sigmf.error.SigMFFileError): SigMFFile(metadata=self.metadata, data_file=temp_path) diff --git a/tests/testdata.py b/tests/testdata.py index cb05bee..b91ad67 100644 --- a/tests/testdata.py +++ b/tests/testdata.py @@ -1,29 +1,14 @@ -# flake8: noqa - -# Copyright 2017 GNU Radio Foundation -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: +# Copyright: Multiple Authors # -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python # -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. +# SPDX-License-Identifier: LGPL-3.0-or-later +"""Shared test data for tests.""" import numpy as np -from sigmf import __version__ -from sigmf import SigMFFile + +from sigmf import SigMFFile, __specification__, __version__ TEST_FLOAT32_DATA = np.arange(16, dtype=np.float32) @@ -31,51 +16,65 @@ SigMFFile.ANNOTATION_KEY: [{SigMFFile.LENGTH_INDEX_KEY: 16, SigMFFile.START_INDEX_KEY: 0}], SigMFFile.CAPTURE_KEY: [{SigMFFile.START_INDEX_KEY: 0}], SigMFFile.GLOBAL_KEY: { - SigMFFile.DATATYPE_KEY: 'rf32_le', - SigMFFile.HASH_KEY: 'f4984219b318894fa7144519185d1ae81ea721c6113243a52b51e444512a39d74cf41a4cec3c5d000bd7277cc71232c04d7a946717497e18619bdbe94bfeadd6', + SigMFFile.DATATYPE_KEY: "rf32_le", + SigMFFile.HASH_KEY: "f4984219b318894fa7144519185d1ae81ea721c6113243a52b51e444512a39d74cf41a4cec3c5d000bd7277cc71232c04d7a946717497e18619bdbe94bfeadd6", SigMFFile.NUM_CHANNELS_KEY: 1, - SigMFFile.VERSION_KEY: '1.0.0' - } + SigMFFile.VERSION_KEY: __specification__, + }, } # Data0 is a test of a compliant two capture recording TEST_U8_DATA0 = list(range(256)) TEST_U8_META0 = { SigMFFile.ANNOTATION_KEY: [], - SigMFFile.CAPTURE_KEY: [ {SigMFFile.START_INDEX_KEY: 0, SigMFFile.HEADER_BYTES_KEY: 0}, - {SigMFFile.START_INDEX_KEY: 0, SigMFFile.HEADER_BYTES_KEY: 0} ], # very strange..but technically legal? - SigMFFile.GLOBAL_KEY: {SigMFFile.DATATYPE_KEY: 'ru8', SigMFFile.TRAILING_BYTES_KEY: 0} + SigMFFile.CAPTURE_KEY: [ + {SigMFFile.START_INDEX_KEY: 0, SigMFFile.HEADER_BYTES_KEY: 0}, + {SigMFFile.START_INDEX_KEY: 0, SigMFFile.HEADER_BYTES_KEY: 0}, + ], # very strange..but technically legal? + SigMFFile.GLOBAL_KEY: {SigMFFile.DATATYPE_KEY: "ru8", SigMFFile.TRAILING_BYTES_KEY: 0}, } # Data1 is a test of a two capture recording with header_bytes and trailing_bytes set -TEST_U8_DATA1 = [0xfe]*32 + list(range(192)) + [0xff]*32 +TEST_U8_DATA1 = [0xFE] * 32 + list(range(192)) + [0xFF] * 32 TEST_U8_META1 = { SigMFFile.ANNOTATION_KEY: [], - SigMFFile.CAPTURE_KEY: [ {SigMFFile.START_INDEX_KEY: 0, SigMFFile.HEADER_BYTES_KEY: 32}, - {SigMFFile.START_INDEX_KEY: 128} ], - SigMFFile.GLOBAL_KEY: {SigMFFile.DATATYPE_KEY: 'ru8', SigMFFile.TRAILING_BYTES_KEY: 32} + SigMFFile.CAPTURE_KEY: [ + {SigMFFile.START_INDEX_KEY: 0, SigMFFile.HEADER_BYTES_KEY: 32}, + {SigMFFile.START_INDEX_KEY: 128}, + ], + SigMFFile.GLOBAL_KEY: {SigMFFile.DATATYPE_KEY: "ru8", SigMFFile.TRAILING_BYTES_KEY: 32}, } # Data2 is a test of a two capture recording with multiple header_bytes set -TEST_U8_DATA2 = [0xfe]*32 + list(range(128)) + [0xfe]*16 + list(range(128,192)) + [0xff]*16 +TEST_U8_DATA2 = [0xFE] * 32 + list(range(128)) + [0xFE] * 16 + list(range(128, 192)) + [0xFF] * 16 TEST_U8_META2 = { SigMFFile.ANNOTATION_KEY: [], - SigMFFile.CAPTURE_KEY: [ {SigMFFile.START_INDEX_KEY: 0, SigMFFile.HEADER_BYTES_KEY: 32}, - {SigMFFile.START_INDEX_KEY: 128, SigMFFile.HEADER_BYTES_KEY: 16} ], - SigMFFile.GLOBAL_KEY: {SigMFFile.DATATYPE_KEY: 'ru8', SigMFFile.TRAILING_BYTES_KEY: 16} + SigMFFile.CAPTURE_KEY: [ + {SigMFFile.START_INDEX_KEY: 0, SigMFFile.HEADER_BYTES_KEY: 32}, + {SigMFFile.START_INDEX_KEY: 128, SigMFFile.HEADER_BYTES_KEY: 16}, + ], + SigMFFile.GLOBAL_KEY: {SigMFFile.DATATYPE_KEY: "ru8", SigMFFile.TRAILING_BYTES_KEY: 16}, } # Data3 is a test of a three capture recording with multiple header_bytes set -TEST_U8_DATA3 = [0xfe]*32 + list(range(128)) + [0xfe]*32 + list(range(128,192)) +TEST_U8_DATA3 = [0xFE] * 32 + list(range(128)) + [0xFE] * 32 + list(range(128, 192)) TEST_U8_META3 = { SigMFFile.ANNOTATION_KEY: [], - SigMFFile.CAPTURE_KEY: [ {SigMFFile.START_INDEX_KEY: 0, SigMFFile.HEADER_BYTES_KEY: 32}, - {SigMFFile.START_INDEX_KEY: 32}, - {SigMFFile.START_INDEX_KEY: 128, SigMFFile.HEADER_BYTES_KEY: 32} ], - SigMFFile.GLOBAL_KEY: {SigMFFile.DATATYPE_KEY: 'ru8'} + SigMFFile.CAPTURE_KEY: [ + {SigMFFile.START_INDEX_KEY: 0, SigMFFile.HEADER_BYTES_KEY: 32}, + {SigMFFile.START_INDEX_KEY: 32}, + {SigMFFile.START_INDEX_KEY: 128, SigMFFile.HEADER_BYTES_KEY: 32}, + ], + SigMFFile.GLOBAL_KEY: {SigMFFile.DATATYPE_KEY: "ru8"}, } # Data4 is a two channel version of Data0 -TEST_U8_DATA4 = [0xfe]*32 + [y for y in list(range(96)) for i in [0,1]] + [0xff]*32 +TEST_U8_DATA4 = [0xFE] * 32 + [y for y in list(range(96)) for i in [0, 1]] + [0xFF] * 32 TEST_U8_META4 = { SigMFFile.ANNOTATION_KEY: [], - SigMFFile.CAPTURE_KEY: [ {SigMFFile.START_INDEX_KEY: 0, SigMFFile.HEADER_BYTES_KEY: 32}, - {SigMFFile.START_INDEX_KEY: 64} ], - SigMFFile.GLOBAL_KEY: {SigMFFile.DATATYPE_KEY: 'ru8', SigMFFile.TRAILING_BYTES_KEY: 32, SigMFFile.NUM_CHANNELS_KEY: 2} + SigMFFile.CAPTURE_KEY: [ + {SigMFFile.START_INDEX_KEY: 0, SigMFFile.HEADER_BYTES_KEY: 32}, + {SigMFFile.START_INDEX_KEY: 64}, + ], + SigMFFile.GLOBAL_KEY: { + SigMFFile.DATATYPE_KEY: "ru8", + SigMFFile.TRAILING_BYTES_KEY: 32, + SigMFFile.NUM_CHANNELS_KEY: 2, + }, }