Source code for pyatsyn.ats_io

# -*- coding: utf-8 -*-

# This source code is licensed under the BSD-style license found in the
# LICENSE.rst file in the root directory of this source tree. 

# pyatsyn Copyright (c) <2023>, <Johnathan G Lyon>
# All rights reserved.

# Except where otherwise noted, ATSA and ATSH is Copyright (c) <2002-2004>, 
# <Oscar Pablo Di Liscia, Pete Moss, and Juan Pampin>


"""ATS File I/O

Functions for handling loading and saving of .ats files.

.ats files are written in binary format using double floats.

The expected structure of a .ats is:

+-------------------------------------------+
| ATS Header (all double floats)            |
+===========================================+
| :obj:`~pyatsyn.ats_io.ATS_MAGIC_NUMBER`   |
+-------------------------------------------+
| sampling-rate (samples/sec)               |
+-------------------------------------------+
| frame-size (samples)                      |
+-------------------------------------------+
| window-size (samples)                     |
+-------------------------------------------+
| partials (number)                         |
+-------------------------------------------+
| frames (number)                           |
+-------------------------------------------+
| ampmax (max. amplitude)                   |
+-------------------------------------------+
| frqmax (max. frequency)                   |
+-------------------------------------------+
| dur (duration)                            |
+-------------------------------------------+
| type (# specifying frame type, see below) |
+-------------------------------------------+

The frame data immediately follows the header, 
again all double floats, frame by frame, with a 
format matching the `type` (int) as specified in the header

ATS frames can be one of four different types:

+---------------------------------+---------------------------------+---------------------------------+---------------------------------+
| TYPE 1: NO phase & NO noise     | TYPE 2: WITH phase & NO noise   | TYPE 3: NO phase & WITH noise   | TYPE 4: WITH phase & WITH noise |
+=================================+=================================+=================================+=================================+
| time (frame starting time)      | time (frame starting time)      | time (frame starting time)      | time (frame starting time)      |
+---------------------------------+---------------------------------+---------------------------------+---------------------------------+
| amp (partial #0 amplitude)      | amp (partial #0 amplitude)      | amp (partial #0 amplitude)      | amp (partial #0 amplitude)      |
+---------------------------------+---------------------------------+---------------------------------+---------------------------------+
| frq (partial #0 frequency)      | frq (partial #0 frequency)      | frq (partial #0 frequency)      | frq (partial #0 frequency)      |
+---------------------------------+---------------------------------+---------------------------------+---------------------------------+
| ...                             | pha (partial #0 phase)          | ...                             | pha (partial #0 phase)          |
+---------------------------------+---------------------------------+---------------------------------+---------------------------------+
| amp (partial #n amplitude)      | ...                             | amp (partial #n amplitude)      | ...                             |
+---------------------------------+---------------------------------+---------------------------------+---------------------------------+
| frq (partial #n frequency)      | amp (partial #n amplitude)      | frq (partial #n frequency)      | amp (partial #n amplitude)      |
+---------------------------------+---------------------------------+---------------------------------+---------------------------------+
|                                 | frq (partial #n frequency)      | noise (band #0 energy)          | frq (partial #n frequency)      |
+---------------------------------+---------------------------------+---------------------------------+---------------------------------+
|                                 | pha (partial #n phase)          | ...                             | pha (partial #n phase)          |
+---------------------------------+---------------------------------+---------------------------------+---------------------------------+
|                                 |                                 | noise (band #n energy)          | noise (band #0 energy)          |
+---------------------------------+---------------------------------+---------------------------------+---------------------------------+
|                                 |                                 |                                 | ...                             |
+---------------------------------+---------------------------------+---------------------------------+---------------------------------+
|                                 |                                 |                                 | noise (band #n energy)          |
+---------------------------------+---------------------------------+---------------------------------+---------------------------------+

Attributes 
----------
ATS_MAGIC_NUMBER : float
    'magic' number used to validate and check endianness when using .ats files: 123.0
"""

from struct import pack, unpack, calcsize
from numpy import zeros, arange, mean
import argparse

from pyatsyn.ats_structure import AtsSound

ATS_MAGIC_NUMBER = 123.0

DOUBLE_SIZE = calcsize('d')
DOUBLE_BIG_ENDIAN = '>d'
DOUBLE_LIL_ENDIAN = '<d'

[docs]def ats_save(sound, file, save_phase=True, save_noise=True): """Function to save an :obj:`~pyatsyn.ats_structure.AtsSound` to a file Parameters ---------- sound : :obj:`~pyatsyn.ats_structure.AtsSound` ats sound object to save file : str file path to save to save_phase : bool, optional whether to include phase data in file output (default: True) save_noise : bool, optional whether to include noise band energy data in file output (default: True) """ has_pha = save_phase and len(sound.pha) > 0 has_noi = save_noise and (len(sound.energy) > 0 or len(sound.band_energy) > 0) if has_noi and len(sound.bands) != 25: print("WARNING: outputing noise energies with other than the expected 25 critical bands") ats_type = 1 if has_pha and has_noi: ats_type = 4 elif not has_pha and has_noi: ats_type = 3 elif has_pha and not has_noi: ats_type = 2 with open(file, 'wb') as fil: # write header fil.write(pack('d',ATS_MAGIC_NUMBER)) fil.write(pack('d',sound.sampling_rate)) fil.write(pack('d',sound.frame_size)) fil.write(pack('d',sound.window_size)) fil.write(pack('d',sound.partials)) fil.write(pack('d',sound.frames)) fil.write(pack('d',sound.amp_max)) fil.write(pack('d',sound.frq_max)) fil.write(pack('d',sound.dur)) fil.write(pack('d',ats_type)) for frame_n in range(sound.frames): fil.write(pack('d',sound.time[frame_n])) for partial in range(sound.partials): fil.write(pack('d',sound.amp[partial][frame_n])) fil.write(pack('d',sound.frq[partial][frame_n])) if has_pha: fil.write(pack('d',sound.pha[partial][frame_n])) if has_noi: for band in range(len(sound.bands)): fil.write(pack('d',sound.band_energy[band][frame_n]))
[docs]def ats_load( file, optimize=False, min_gap_size = None, min_segment_length = None, amp_threshold = None, highest_frequency = None, lowest_frequency = None): """Function to load a .ats file into python Loads a .ats file into python and provides routines to re-optimize the :obj:`~pyatsyn.ats_structure.AtsSound` data if required. Parameters ---------- file : str filepath to .ats file to load optimize : bool, optional determined whether to call :obj:`~pyatsyn.ats_structure.AtsSound.optimize` upon load (default: True) min_gap_size : int, optional when optimizing, tracked partial gaps smaller than or equal to this (in frames) will be interpolated and filled. If None, no gap filling will occur (default: None) amp_threshold : float, optional minimum amplitude threshold used during optimization to prune tracks. If None, no amplitude thresholding will occur (default: None) highest_frequency : float, optional maximum frequency threshold used during optimization to prune tracks. If None, no maximum frequency thresholding will occur (default: None) lowest_frequency : float, optional minimum frequency threshold used during optimization to prune tracks. If None, no minimum frequency thresholding will occur (default: None) Returns ------- :obj:`~pyatsyn.ats_structure.AtsSound` the loaded ats data Raises ------ ValueError if file is not a compatible ATS format (i.e., the ATS magic number was not decodable) """ with open(file, 'rb') as fil: # check ATS_MAGIC_NUMBER and set endian order check_magic_number_raw = fil.read(DOUBLE_SIZE) ordered_double = None if unpack(DOUBLE_BIG_ENDIAN,check_magic_number_raw)[0] == ATS_MAGIC_NUMBER: ordered_double = DOUBLE_BIG_ENDIAN elif unpack(DOUBLE_LIL_ENDIAN,check_magic_number_raw)[0] == ATS_MAGIC_NUMBER: ordered_double = DOUBLE_LIL_ENDIAN else: raise ValueError("File is not a compatible ATS format (ATS magic number was not accurate)") sampling_rate = int(unpack(ordered_double, fil.read(DOUBLE_SIZE))[0]) frame_size = int(unpack(ordered_double, fil.read(DOUBLE_SIZE))[0]) window_size = int(unpack(ordered_double, fil.read(DOUBLE_SIZE))[0]) partials = int(unpack(ordered_double, fil.read(DOUBLE_SIZE))[0]) frames = int(unpack(ordered_double, fil.read(DOUBLE_SIZE))[0]) amp_max = unpack(ordered_double, fil.read(DOUBLE_SIZE))[0] frq_max = unpack(ordered_double, fil.read(DOUBLE_SIZE))[0] dur = unpack(ordered_double, fil.read(DOUBLE_SIZE))[0] ats_type = int(unpack(ordered_double, fil.read(DOUBLE_SIZE))[0]) has_noise = False if ats_type == 3 or ats_type == 4: has_noise = True has_phase = False if ats_type == 2 or ats_type == 4: has_phase = True ats_snd = AtsSound(sampling_rate, frame_size, window_size, partials, frames, dur, has_phase=has_phase) ats_snd.amp_max = amp_max ats_snd.frq_max = frq_max ats_snd.frq_av = zeros(partials, "float64") ats_snd.amp_av = zeros(partials, "float64") ats_snd.time = zeros(frames, "float64") ats_snd.frq = zeros([partials,frames], "float64") ats_snd.amp = zeros([partials,frames], "float64") if has_phase: ats_snd.pha = zeros([partials,frames], "float64") if has_noise: # NOTE: hard-coded for expected number of critical bands ats_snd.bands = arange(25) ats_snd.band_energy = zeros([25, frames], "float64") for frame_n in range(ats_snd.frames): ats_snd.time[frame_n] = unpack(ordered_double, fil.read(DOUBLE_SIZE))[0] for partial in range(ats_snd.partials): ats_snd.amp[partial][frame_n] = unpack(ordered_double, fil.read(DOUBLE_SIZE))[0] ats_snd.frq[partial][frame_n] = unpack(ordered_double, fil.read(DOUBLE_SIZE))[0] if has_phase: ats_snd.pha[partial][frame_n] = unpack(ordered_double, fil.read(DOUBLE_SIZE))[0] if has_noise: for band in range(len(ats_snd.bands)): ats_snd.band_energy[band][frame_n] = unpack(ordered_double, fil.read(DOUBLE_SIZE))[0] # load frq/amp averages for partial in range(ats_snd.partials): ats_snd.frq_av[partial] = mean(ats_snd.frq[partial][ats_snd.frq[partial] > 0.0]) ats_snd.amp_av[partial] = mean(ats_snd.amp[partial][ats_snd.amp[partial] > 0.0]) if optimize: ats_snd.optimize(min_gap_size, min_segment_length, amp_threshold, highest_frequency, lowest_frequency) return ats_snd
[docs]def ats_info(file, partials_info=False): """Function to print information about a .ats to the stdout Parameters ---------- file : str an .ats file to print information about partials_info : bool, optional whether to include frq and amp averages about each partial in the output (default: False) """ if not partials_info: with open(file, 'rb') as fil: # check ATS_MAGIC_NUMBER and set endian order check_magic_number_raw = fil.read(DOUBLE_SIZE) ordered_double = None if unpack(DOUBLE_BIG_ENDIAN,check_magic_number_raw)[0] == ATS_MAGIC_NUMBER: ordered_double = DOUBLE_BIG_ENDIAN print(f"BIG ENDIAN: ", ATS_MAGIC_NUMBER) elif unpack(DOUBLE_LIL_ENDIAN,check_magic_number_raw)[0] == ATS_MAGIC_NUMBER: ordered_double = DOUBLE_LIL_ENDIAN print(f"LITTLE ENDIAN: ", ATS_MAGIC_NUMBER) else: print("File is not a compatible ATS format (ATS magic number was not accurate)") return sampling_rate = int(unpack(ordered_double, fil.read(DOUBLE_SIZE))[0]) frame_size = int(unpack(ordered_double, fil.read(DOUBLE_SIZE))[0]) window_size = int(unpack(ordered_double, fil.read(DOUBLE_SIZE))[0]) partials = int(unpack(ordered_double, fil.read(DOUBLE_SIZE))[0]) frames = int(unpack(ordered_double, fil.read(DOUBLE_SIZE))[0]) amp_max = unpack(ordered_double, fil.read(DOUBLE_SIZE))[0] frq_max = unpack(ordered_double, fil.read(DOUBLE_SIZE))[0] dur = unpack(ordered_double, fil.read(DOUBLE_SIZE))[0] ats_type = int(unpack(ordered_double, fil.read(DOUBLE_SIZE))[0]) print(f"sampling rate (samples/s):", sampling_rate) print(f"frame size:", frame_size) print(f"window size:", window_size) print(f"n partials:", partials) print(f"n frames:", frames) print(f"maximum amplitude:", amp_max) print(f"maximum frequency (Hz):", frq_max) print(f"duration (s):", dur) print(f"ATS frame type: ", ats_type) else: with open(file, 'rb') as fil: # check ATS_MAGIC_NUMBER and set endian order check_magic_number_raw = fil.read(DOUBLE_SIZE) ordered_double = None if unpack(DOUBLE_BIG_ENDIAN,check_magic_number_raw)[0] == ATS_MAGIC_NUMBER: ordered_double = DOUBLE_BIG_ENDIAN print(f"BIG ENDIAN: ", ATS_MAGIC_NUMBER) elif unpack(DOUBLE_LIL_ENDIAN,check_magic_number_raw)[0] == ATS_MAGIC_NUMBER: ordered_double = DOUBLE_LIL_ENDIAN print(f"LITTLE ENDIAN: ", ATS_MAGIC_NUMBER) else: print("File is not a compatible ATS format (ATS magic number was not accurate)") return ats_snd = ats_load(file) print(f"sampling rate (samples/s):", ats_snd.sampling_rate) print(f"frame size:", ats_snd.frame_size) print(f"window size:", ats_snd.window_size) print(f"n partials:", ats_snd.partials) print(f"n frames:", ats_snd.frames) print(f"maximum amplitude:", ats_snd.amp_max) print(f"maximum frequency (Hz):", ats_snd.frq_max) print(f"duration (s):", ats_snd.dur) has_noise = False if len(ats_snd.bands) > 0 and len(ats_snd.band_energy) > 0: has_noise = True has_phase = False if ats_snd.pha is not None: has_phase = True ats_type = 1 if has_phase and has_noise: ats_type = 4 elif not has_phase and has_noise: ats_type = 3 elif has_phase and not has_noise: ats_type = 2 print(f"ATS frame type: ", ats_type) print(f"\nPartial Information:") for partial in range(ats_snd.partials): print(f"\tpartial #{partial}:\t\tfrq_av {ats_snd.frq_av[partial]:.2f}\t\tamp_av {ats_snd.amp_av[partial]:.5f}")
[docs]def ats_info_CLI(): """Command line wrapper for :obj:`~pyatsyn.ats_io.ats_info` Example ------- Display usage details with help flag :: $ pyatsyn-info -h Print the header information of a .ats file :: $ pyatsyn-info example.ats Print the header information and partials information of a .ats file :: $ pyatsyn-info example.ats -p """ parser = argparse.ArgumentParser( description = "Print information about an .ats file" ) parser.add_argument("ats_file", help="the path to the .ats file to print information about") parser.add_argument("-p", "--partials", help="include a summary for each partial in the output", action="store_true") args = parser.parse_args() ats_info(args.ats_file, args.partials)