@@ -824,38 +824,79 @@ def archive(self, name=None, fileobj=None, compression=None, overwrite=False):
824824
825825 def tofile (self , file_path , pretty = True , toarchive = False , compression = None , skip_validate = False , overwrite = False ):
826826 """
827- Write metadata file or full archive containing metadata & dataset.
827+ Write metadata file or archive based on file extension.
828+
829+ The file extension determines the output format:
830+ - No extension or other extension → `.sigmf-meta` file (and `.sigmf-data` if data_buffer exists)
831+ - `.sigmf` → uncompressed archive
832+ - `.sigmf.gz`, `.sigmf.xz`, `.sigmf.zip` → compressed archive
828833
829834 Parameters
830835 ----------
831836 file_path : string
832- Location to save.
837+ Location to save. Extension determines output format.
833838 pretty : bool, default True
834- When True will write more human-readable output , otherwise will be flat JSON.
839+ When True will write human-readable JSON , otherwise flat JSON.
835840 toarchive : bool, default False
836- If True will write both dataset & metadata into SigMF archive format.
837- If False will only write metadata to `sigmf-meta`.
841+ If True, forces archive creation (writes metadata and data to archive) regardless of file extension.
838842 compression : str, optional
839- Compression type when toarchive=True: "gz", "xz", "zip", or None.
843+ Compression type: "gz", "xz", "zip", or None.
844+ If specified, must match file extension if extension implies compression.
845+ If no archive extension is present, creates a compressed archive.
840846 skip_validate : bool, default False
841847 Skip validation of metadata before writing.
842848 overwrite : bool, default False
843849 If False, raise exception if output file already exists.
850+
851+ Examples
852+ --------
853+ >>> meta.tofile('recording') # creates recording.sigmf-meta
854+ >>> meta.tofile('recording.sigmf') # creates recording.sigmf (archive)
855+ >>> meta.tofile('recording.sigmf.gz') # creates recording.sigmf.gz (compressed)
856+ >>> meta.tofile('recording', compression='xz') # creates recording.sigmf.xz
844857 """
845858 if not skip_validate :
846859 self .validate ()
847- fns = get_sigmf_filenames (file_path )
860+
861+ path = Path (file_path )
862+
863+ # auto-detect compression from extension
864+ detected_compression = _detect_compression (path )
865+ if detected_compression is not None :
866+ if compression is not None and compression != detected_compression :
867+ raise SigMFFileError (
868+ f"Extension implies '{ detected_compression } ' compression but compression='{ compression } ' was specified."
869+ )
870+ compression = detected_compression
871+ toarchive = True
872+
873+ # auto-detect archive from .sigmf extension
874+ if path .name .lower ().endswith (SIGMF_ARCHIVE_EXT ):
875+ toarchive = True
876+
877+ # compression implies archive
878+ if compression is not None :
879+ toarchive = True
848880
849881 if toarchive :
850- self .archive (fns ["archive_fn" ], compression = compression , overwrite = overwrite )
882+ # pass the original file_path to archive() so it handles extension properly
883+ self .archive (file_path , compression = compression , overwrite = overwrite )
851884 else :
852- # check if metadata file exists
885+ # write metadata file (and data file if data_buffer exists)
886+ fns = get_sigmf_filenames (file_path )
853887 if not overwrite and fns ["meta_fn" ].exists ():
854888 raise SigMFFileExistsError (fns ["meta_fn" ], "Metadata file" )
855889 with open (fns ["meta_fn" ], "w" ) as fp :
856890 self .dump (fp , pretty = pretty )
857891 fp .write ("\n " ) # text files should end in carriage return
858892
893+ # write data file if data_buffer exists
894+ if self .data_buffer is not None :
895+ if not overwrite and fns ["data_fn" ].exists ():
896+ raise SigMFFileExistsError (fns ["data_fn" ], "Data file" )
897+ with open (fns ["data_fn" ], "wb" ) as fp :
898+ fp .write (self .data_buffer .getbuffer ())
899+
859900 def read_samples_in_capture (self , index = 0 ):
860901 """
861902 Reads samples from the specified captures segment in its entirety.
@@ -1260,69 +1301,48 @@ def get_dataset_filename_from_metadata(meta_fn, metadata=None):
12601301 return None
12611302
12621303
1263- def tofile ( filename , data , sample_rate , frequency = None , toarchive = False , compression = None , global_info = None ):
1304+ def fromarray ( data , sample_rate , frequency = None , global_info = None ):
12641305 """
1265- Convenience method to write a numpy array to a SigMF recording .
1306+ Create a SigMFFile from a numpy array.
12661307
1267- For quick saves — infers the SigMF datatype from the numpy dtype, writes
1268- the data file, creates metadata with a single capture at index 0, and
1269- saves to disk. For full control over captures, annotations, and global
1308+ Convenience function that infers the SigMF datatype from the numpy dtype,
1309+ creates an in-memory SigMFFile with a single capture at index 0. The
1310+ returned object can then be written to disk using ``tofile()`` or
1311+ ``archive()``. For full control over captures, annotations, and global
12701312 fields, use ``SigMFFile`` directly.
12711313
12721314 Parameters
12731315 ----------
1274- filename : str | PathLike
1275- Base filename or archive path. Accepts:
1276- - ``"recording"`` — produces ``recording.sigmf-data`` and ``recording.sigmf-meta``
1277- - ``"recording.sigmf"`` — produces uncompressed archive (auto-detects toarchive)
1278- - ``"recording.sigmf.xz"`` — produces compressed archive (auto-detects compression)
12791316 data : np.ndarray
1280- Signal samples to write .
1317+ Signal samples.
12811318 sample_rate : float
12821319 Sample rate in Hz.
12831320 frequency : float, optional
12841321 Center frequency in Hz for the capture.
1285- toarchive : bool, default False
1286- If True, produce a ``.sigmf`` archive instead of loose data/meta files.
1287- Auto-detected from filename extension if not specified.
1288- compression : str, optional
1289- If set, also creates a compressed archive. One of "gz", "xz", "zip".
1290- Auto-detected from filename extension if not specified. Implies toarchive.
12911322 global_info : dict, optional
12921323 Additional global metadata fields to include.
12931324
12941325 Returns
12951326 -------
12961327 SigMFFile
1297- The SigMFFile object with data and metadata.
1328+ The SigMFFile object with in-memory data and metadata.
1329+
1330+ Examples
1331+ --------
1332+ >>> import numpy as np
1333+ >>> data = np.random.randn(1000) + 1j * np.random.randn(1000)
1334+ >>> meta = fromarray(data, sample_rate=1e6, frequency=915e6)
1335+ >>> meta.tofile('recording') # creates recording.sigmf-meta and recording.sigmf-data
1336+ >>> meta.tofile('recording.sigmf') # creates recording.sigmf archive
12981337 """
1299- file_path = Path (filename )
1300-
1301- # detect compressed extension and extract base name
1302- detected = _detect_compression (file_path )
1303- if detected is not None :
1304- if compression is not None and compression != detected :
1305- raise SigMFFileError (
1306- f"Extension implies '{ detected } ' compression but compression='{ compression } ' was specified."
1307- )
1308- compression = detected
1309- base_name = _get_archive_basename (file_path )
1310- base_path = file_path .parent / base_name
1311- elif file_path .name .endswith (SIGMF_ARCHIVE_EXT ):
1312- toarchive = True
1313- base_path = file_path .parent / file_path .stem
1314- else :
1315- base_path = file_path
1316-
1317- # compression implies archive
1318- if compression is not None :
1319- toarchive = True
1320-
1321- fns = get_sigmf_filenames (base_path )
1322- data_path = fns ["data_fn" ]
1338+ import io
13231339
1324- data .tofile (data_path )
1340+ # create in-memory data buffer
1341+ data_buffer = io .BytesIO ()
1342+ data_buffer .write (data .tobytes ())
1343+ data_buffer .seek (0 )
13251344
1345+ # build metadata
13261346 info = {
13271347 SigMFFile .DATATYPE_KEY : get_data_type_str (data ),
13281348 SigMFFile .SAMPLE_RATE_KEY : sample_rate ,
@@ -1334,16 +1354,11 @@ def tofile(filename, data, sample_rate, frequency=None, toarchive=False, compres
13341354 if frequency is not None :
13351355 capture_meta = {SigMFFile .FREQUENCY_KEY : frequency }
13361356
1337- meta = SigMFFile (data_file = data_path , global_info = info )
1357+ # create sigmffile object with in-memory buffer
1358+ meta = SigMFFile (global_info = info )
1359+ meta .set_data_file (data_buffer = data_buffer )
13381360 meta .add_capture (0 , metadata = capture_meta )
13391361
1340- if toarchive :
1341- # create archive only — no loose files
1342- meta .archive (str (fns ["base_fn" ]), compression = compression )
1343- data_path .unlink ()
1344- else :
1345- meta .tofile (base_path )
1346-
13471362 return meta
13481363
13491364
0 commit comments