1818import os
1919import struct
2020from datetime import datetime , timezone
21+ from pathlib import Path
22+ from typing import Optional
2123
2224import numpy as np
2325
@@ -242,14 +244,16 @@ def parse_extended_header(file_path, hcb, endian="<"):
242244 return entries
243245
244246
245- def parse_data_values (file_path , hcb , endianness ):
247+ def parse_data_values (blue_path : Path , out_path : Path , hcb : dict , endianness : str ):
246248 """
247249 Parse key HCB values used for further processing.
248250
249251 Parameters
250252 ----------
251- file_path : str
253+ blue_path : Path
252254 Path to the Blue file.
255+ out_path : Path
256+ Path to output SigMF metadata file.
253257 hcb : dict
254258 Header Control Block dictionary.
255259 endianness : str
@@ -261,126 +265,155 @@ def parse_data_values(file_path, hcb, endianness):
261265 Parsed samples.
262266 """
263267 log .info ("parsing blue file data values" )
264- with open (file_path , "rb" ) as handle :
268+ with open (blue_path , "rb" ) as handle :
265269 data = handle .read (HEADER_SIZE_BYTES )
266270 if len (data ) < HEADER_SIZE_BYTES :
267271 raise ValueError ("Incomplete header" )
268272 dtype = data [52 :54 ].decode ("utf-8" ) # eg 'CI', 'CF', 'SD'
269273 log .debug (f"data type: { dtype } " )
270274
271-
272275 time_interval = np .frombuffer (data [264 :272 ], dtype = np .float64 )[0 ]
273276 if time_interval <= 0 :
274277 raise ValueError (f"Invalid time interval: { time_interval } " )
275278 sample_rate_hz = 1 / time_interval
276279 log .info (f"sample rate: { sample_rate_hz / 1e6 :.3f} MHz" )
277280 extended_header_data_size = int .from_bytes (data [28 :32 ], byteorder = "little" )
278- file_size_bytes = os .path .getsize (file_path )
281+ file_size_bytes = os .path .getsize (blue_path )
279282 log .debug (f"file size: { file_size_bytes } bytes" )
280283
281284 # Determine destination path for SigMF data file
282- dest_path = file_path . rsplit ("." , 1 )[ 0 ]
285+ dest_path = out_path . with_suffix (".sigmf-data" )
283286
284287 ### complex data parsing
285288
286289 # complex 16-bit integer IQ data > ci16_le in SigMF
287290 if dtype == "CI" :
288291 elem_size = np .dtype (np .int16 ).itemsize
289292 elem_count = (file_size_bytes - extended_header_data_size ) // elem_size
290- raw_samples = np .fromfile (file_path , dtype = np .int16 , offset = HEADER_SIZE_BYTES , count = elem_count )
293+ raw_samples = np .fromfile (blue_path , dtype = np .int16 , offset = HEADER_SIZE_BYTES , count = elem_count )
291294 # reassemble interleaved IQ samples
292295 samples = raw_samples [::2 ] + 1j * raw_samples [1 ::2 ] # convert to IQIQIQ...
293296 # normalize samples to -1.0 to +1.0 range
294297 samples = samples .astype (np .float32 ) / 32767.0
295298 # save out as SigMF IQ data file
296- samples .tofile (f" { dest_path } .sigmf-data" )
299+ samples .tofile (dest_path )
297300
298301 # complex 32-bit integer IQ data > ci32_le in SigMF
299302 elif dtype == "CL" :
300303 elem_size = np .dtype (np .int32 ).itemsize
301304 elem_count = (file_size_bytes - extended_header_data_size ) // elem_size
302- raw_samples = np .fromfile (file_path , dtype = np .int32 , offset = HEADER_SIZE_BYTES , count = elem_count )
305+ raw_samples = np .fromfile (blue_path , dtype = np .int32 , offset = HEADER_SIZE_BYTES , count = elem_count )
303306 # reassemble interleaved IQ samples
304307 samples = raw_samples [::2 ] + 1j * raw_samples [1 ::2 ] # convert to IQIQIQ...
305308 # normalize samples to -1.0 to +1.0 range
306309 samples = samples .astype (np .float32 ) / 2147483647.0
307310 # save out as SigMF IQ data file
308- samples .tofile (f" { dest_path } .sigmf-data" )
311+ samples .tofile (dest_path )
309312
310313 # complex 32-bit float IQ data > cf32_le in SigMF
311314 elif dtype == "CF" :
312315 # each complex sample is 8 bytes total (2 × float32), so np.complex64 is appropriate
313316 # no need to reassemble IQ — already complex
314317 elem_size = np .dtype (np .complex64 ).itemsize # will be 8 bytes
315318 elem_count = (file_size_bytes - extended_header_data_size ) // elem_size
316- samples = np .fromfile (file_path , dtype = np .complex64 , offset = HEADER_SIZE_BYTES , count = elem_count )
319+ samples = np .fromfile (blue_path , dtype = np .complex64 , offset = HEADER_SIZE_BYTES , count = elem_count )
317320 # save out as SigMF IQ data file
318- samples .tofile (f" { dest_path } .sigmf-data" )
321+ samples .tofile (dest_path )
319322
320323 ### scalar data parsing
321324
322325 # scalar data parsing > ri8_le in SigMF
323326 elif dtype == "SB" :
324327 elem_size = np .dtype (np .int8 ).itemsize
325328 elem_count = (file_size_bytes - extended_header_data_size ) // elem_size
326- samples = np .fromfile (file_path , dtype = np .int8 , offset = HEADER_SIZE_BYTES , count = elem_count )
329+ samples = np .fromfile (blue_path , dtype = np .int8 , offset = HEADER_SIZE_BYTES , count = elem_count )
327330 # normalize samples to -1.0 to +1.0 range
328331 samples = samples .astype (np .float32 ) / 127.0
329332 # save out as SigMF IQ data file
330- samples .tofile (f" { dest_path } .sigmf-data" )
333+ samples .tofile (dest_path )
331334
332335 # scalar data parsing > ri16_le in SigMF
333336 elif dtype == "SI" :
334337 elem_size = np .dtype (np .int16 ).itemsize
335338 elem_count = (file_size_bytes - extended_header_data_size ) // elem_size
336- samples = np .fromfile (file_path , dtype = np .int16 , offset = HEADER_SIZE_BYTES , count = elem_count )
339+ samples = np .fromfile (blue_path , dtype = np .int16 , offset = HEADER_SIZE_BYTES , count = elem_count )
337340 # normalize samples to -1.0 to +1.0 range
338341 samples = samples / 32767.0
339342 # save out as SigMF IQ data file
340- samples .tofile (f" { dest_path } .sigmf-data" )
343+ samples .tofile (dest_path )
341344
342345 # scalar data parsing > ri32_le in SigMF
343346 elif dtype == "SL" :
344347 elem_size = np .dtype (np .int32 ).itemsize
345348 elem_count = (file_size_bytes - extended_header_data_size ) // elem_size
346- samples = np .fromfile (file_path , dtype = np .int32 , offset = HEADER_SIZE_BYTES , count = elem_count )
349+ samples = np .fromfile (blue_path , dtype = np .int32 , offset = HEADER_SIZE_BYTES , count = elem_count )
347350 # normalize samples to -1.0 to +1.0 range
348351 samples = samples / 2147483647.0
349352 # save out as SigMF IQ data file
350- samples .tofile (f" { dest_path } .sigmf-data" )
353+ samples .tofile (dest_path )
351354
352355 # scalar data parsing > ri64_le in SigMF
353356 elif dtype == "SX" :
354357 elem_size = np .dtype (np .int64 ).itemsize
355358 elem_count = (file_size_bytes - extended_header_data_size ) // elem_size
356- samples = np .fromfile (file_path , dtype = np .int64 , offset = HEADER_SIZE_BYTES , count = elem_count )
359+ samples = np .fromfile (blue_path , dtype = np .int64 , offset = HEADER_SIZE_BYTES , count = elem_count )
357360 # save out as SigMF IQ data file
358- samples .tofile (f" { dest_path } .sigmf-data" )
361+ samples .tofile (dest_path )
359362
360363 # scalar data parsing > rf32_le in SigMF
361364 elif dtype == "SF" :
362365 elem_size = np .dtype (np .float32 ).itemsize
363366 elem_count = (file_size_bytes - extended_header_data_size ) // elem_size
364- samples = np .fromfile (file_path , dtype = np .float32 , offset = HEADER_SIZE_BYTES , count = elem_count )
367+ samples = np .fromfile (blue_path , dtype = np .float32 , offset = HEADER_SIZE_BYTES , count = elem_count )
365368 # save out as SigMF IQ data file
366- samples .tofile (f" { dest_path } .sigmf-data" )
369+ samples .tofile (dest_path )
367370
368371 # scalar data parsing > rf64_le in SigMF
369372 elif dtype == "SD" :
370373 elem_size = np .dtype (np .float64 ).itemsize
371374 elem_count = (file_size_bytes - extended_header_data_size ) // elem_size
372- samples = np .fromfile (file_path , dtype = np .float64 , offset = HEADER_SIZE_BYTES , count = elem_count )
375+ samples = np .fromfile (blue_path , dtype = np .float64 , offset = HEADER_SIZE_BYTES , count = elem_count )
373376 # save out as SigMF IQ data file
374- samples .astype (np .complex64 ).tofile (f" { dest_path } .sigmf-data" )
377+ samples .astype (np .complex64 ).tofile (dest_path )
375378 else :
376379 raise ValueError (f"Unsupported data type: { dtype } " )
377380
378- # TODO: validate handling of scalar types - reshape per mathlab port shown here?
381+ # TODO: validate handling of scalar types - Reshape per mathlab port shown here?
382+
383+ """
384+ # Save out as SigMF IQ data file
385+ if dtype in ("CI", "CL", "CF"):
386+ # Complex IQ data → save as cf32_le
387+ samples.astype(np.complex64).tofile(dest_path)
388+ else:
389+ # Scalar data → save in native dtype
390+ samples.tofile(dest_path)
391+ """
392+
393+ # TODO: validate handling of scalar types - Reshape per mathlab port shown here?
394+
395+ """
396+ fmt_size_char = self.hcb["format"][0]
397+ fmt_type_char = self.hcb["format"][1]
398+
399+ elementsPerSample = self.FormatSize(fmt_size_char)
400+ print('Elements Per Sample', elementsPerSample/1e6)
401+ elem_count_per_sample = (
402+ np.prod(elementsPerSample) if isinstance(elementsPerSample, (tuple, list)) else elementsPerSample
403+ )
404+
405+ dtype_str, elem_bytes = self.FormatType(fmt_type_char)
406+ bytesPerSample = int(elem_bytes) * int(elem_count_per_sample)
407+
408+ bytesRead = int(self.dataOffset - self.hcb["data_start"])
409+ bytes_remaining = int(self.hcb["data_size"] - bytesRead)
410+ """
411+
379412 # return the IQ data if needed for further processing if needed
380413 return samples
381414
382415
383- def blue_to_sigmf (hcb , ext_entries , file_path ):
416+ def construct_sigmf (hcb : dict , ext_entries : list , blue_path : Path , out_path : Path ):
384417 """
385418 Build a SigMF metadata dict from parsed Bluefile HCB and extended header.
386419
@@ -390,8 +423,10 @@ def blue_to_sigmf(hcb, ext_entries, file_path):
390423 Header Control Block from read_hcb().
391424 ext_entries : list of dict
392425 Parsed extended header entries from parse_extended_header().
393- file_path : str
426+ blue_path : Path
394427 Path to the original blue file.
428+ out_path : Path
429+ Path to output SigMF metadata file.
395430
396431 Returns
397432 -------
@@ -556,14 +591,12 @@ def compute_sha512(path, bufsize=1024 * 1024):
556591 chunk = handle .read (bufsize )
557592 return hash_obj .hexdigest ()
558593
559- # strip the extension from the original file path
560- base_file_name = os .path .splitext (file_path )[0 ]
561-
562594 # build the .sigmf-data path
563- data_file_path = base_file_name + ".sigmf-data"
595+ data_path = out_path .with_suffix (".sigmf-data" )
596+ meta_path = out_path .with_suffix (".sigmf-meta" )
564597
565598 # compute SHA-512 of the data file
566- data_sha512 = compute_sha512 (data_file_path ) # path to the .sigmf-data file
599+ data_sha512 = compute_sha512 (data_path ) # path to the .sigmf-data file
567600 global_md ["core:sha512" ] = data_sha512
568601
569602 # annotations array
@@ -615,36 +648,43 @@ def compute_sha512(path, bufsize=1024 * 1024):
615648 "annotations" : annotations ,
616649 }
617650
618- # write .sigmf-meta file
619- base_file_name = os .path .splitext (file_path )[0 ]
620- meta_path = base_file_name + ".sigmf-meta"
621-
622651 with open (meta_path , "w" ) as meta_handle :
623652 json .dump (sigmf_metadata , meta_handle , indent = 2 )
624653 log .info (f"wrote SigMF metadata to { meta_path } " )
625654
626655 return sigmf_metadata
627656
628657
629- def blue_file_to_sigmf (file_path ):
658+ def convert_blue (
659+ blue_path : str ,
660+ out_path : Optional [str ] = None ,
661+ ) -> np .ndarray :
630662 """
631663 Convert a MIDIS Bluefile to SigMF metadata and data.
632664
633665 Parameters
634666 ----------
635- file_path : str
667+ blue_path : str
636668 Path to the Blue file.
669+ out_path : str
670+ Path to the output SigMF metadata file.
637671
638672 Returns
639673 -------
640674 numpy.ndarray
641675 IQ Data.
642676 """
643- log .info ("starting blue file processing" )
677+ log .debug ("starting blue file processing" )
644678
645- # read Header control block (HCB) from blue file to determine how to process the rest of the file
646- hcb = read_hcb (file_path )
679+ blue_path = Path (blue_path )
680+ if out_path is None :
681+ # extension will be changed later
682+ out_path = Path (blue_path )
683+ else :
684+ out_path = Path (out_path )
647685
686+ # read Header control block (HCB) from blue file to determine how to process the rest of the file
687+ hcb = read_hcb (blue_path )
648688 log .debug ("Header Control Block (HCB) Fields" )
649689 for name , _ , _ , _ , desc in HCB_LAYOUT :
650690 log .debug (f"{ name :10s} : { hcb [name ]!r} # { desc } " )
@@ -663,7 +703,7 @@ def blue_file_to_sigmf(file_path):
663703 raise ValueError (f"Unknown head_rep value: { extended_header_endianness } " )
664704
665705 # parse extended header entries
666- ext_entries = parse_extended_header (file_path , hcb , ext_endianness )
706+ ext_entries = parse_extended_header (blue_path , hcb , ext_endianness )
667707 log .debug ("Extended Header Keywords" )
668708 for entry in ext_entries :
669709 log .debug (f"{ entry ['tag' ]:20s} :{ entry ['value' ]} " )
@@ -676,12 +716,12 @@ def blue_file_to_sigmf(file_path):
676716 # parse key data values
677717 # iq_data will be available if needed for further processing.
678718 try :
679- iq_data = parse_data_values (file_path , hcb , data_endianness )
719+ iq_data = parse_data_values (blue_path , out_path , hcb , data_endianness )
680720 except Exception as error :
681721 raise RuntimeError (f"Failed to parse data values: { error } " ) from error
682722
683723 # call the SigMF conversion for metadata generation
684- blue_to_sigmf (hcb , ext_entries , file_path )
724+ construct_sigmf (hcb , ext_entries , blue_path , out_path )
685725
686726 # return the IQ data if needed for further processing if needed
687727 return iq_data
@@ -704,4 +744,4 @@ def main() -> None:
704744 }
705745 logging .basicConfig (level = level_lut [min (args .verbose , 2 )])
706746
707- blue_file_to_sigmf (args .input )
747+ convert_blue (args .input )
0 commit comments