diff --git a/ivas_processing_scripts/__init__.py b/ivas_processing_scripts/__init__.py index a457eb5e403f62f5c44f054eb24a9ab2c6813ab1..98fa796b8e7bedbc75ed2d515555331f7f42f89d 100755 --- a/ivas_processing_scripts/__init__.py +++ b/ivas_processing_scripts/__init__.py @@ -117,12 +117,9 @@ def main(args): item_names=cfg.items_list, ) # print info about found and used metadata files - for i in range(len(metadata_ISM)): - metadata_str = [] - for o in range(len(metadata_ISM[i])): - metadata_str.append(str(metadata_ISM[i][o])) + for audio, meta in zip(cfg.items_list, metadata_ISM): logger.debug( - f" ISM metadata files item {cfg.items_list[i]}: {', '.join(metadata_str)}" + f" ISM metadata file item {audio}: {', '.join(str(m) for m in meta)}" ) metadata = metadata_ISM @@ -134,15 +131,14 @@ def main(args): item_names=cfg.items_list, ) # print info about found and used metadata files - for i in range(len(metadata_MASA)): - metadata_str = [] - for o in range(len(metadata_MASA[i])): - metadata_str.append(str(metadata_MASA[i][o])) + for audio, meta in zip(cfg.items_list, metadata_MASA): logger.debug( - f" MASA metadata file item {cfg.items_list[i]}: {', '.join(metadata_str)}" + f" MASA metadata file item {audio}: {', '.join(str(m) for m in meta)}" ) - for i, meta in enumerate(metadata): - meta.extend(metadata_MASA[i]) + # extend the list in case ISM metadata already is present + metadata = [ + meta_list + met for meta_list, met in zip(metadata, metadata_MASA) + ] if not cfg.input["fmt"].startswith("ISM") and "MASA" not in cfg.input["fmt"]: metadata = [None] * len(cfg.items_list) diff --git a/ivas_processing_scripts/audiotools/convert/__init__.py b/ivas_processing_scripts/audiotools/convert/__init__.py index d279c5614902b912bf384ac63a77c657644c413d..88654b6c13d7e74c3b2bb1cc90dc4312b191eb28 100755 --- a/ivas_processing_scripts/audiotools/convert/__init__.py +++ b/ivas_processing_scripts/audiotools/convert/__init__.py @@ -74,7 +74,7 @@ def convert_file( # get audio class object - can be either a regular single audio or scene description .txt if not isinstance(in_fmt, PurePath) and in_fmt.startswith("META"): - input = metadata.Metadata(in_file) + input = metadata.SceneDescription(in_file) else: # first check prevents crash on custom_ls setup formats if isinstance(in_fmt, str) and in_fmt.startswith("MASA") and in_meta is None: @@ -90,7 +90,7 @@ def convert_file( out_fs = input.fs if not out_fmt: - if isinstance(input, metadata.Metadata): + if isinstance(input, metadata.SceneDescription): raise ValueError( "Output format must be specified for scene description files!" ) @@ -139,7 +139,7 @@ def convert_file( output.metadata_files = copy(input.metadata_files) # apply actual conversion - if isinstance(input, metadata.Metadata): + if isinstance(input, metadata.SceneDescription): if logger: logger.debug(f"Converting metadata to {out_fmt} : {in_file} -> {out_file}") diff --git a/ivas_processing_scripts/audiotools/metadata.py b/ivas_processing_scripts/audiotools/metadata.py index ec1b6166fa640d18dca16a419ae4b6823ae31511..9fd202d16ca16e0c8a2ab352c3fe17887e4c14a7 100755 --- a/ivas_processing_scripts/audiotools/metadata.py +++ b/ivas_processing_scripts/audiotools/metadata.py @@ -31,7 +31,10 @@ # import csv +import logging from pathlib import Path +from shutil import copyfile, copyfileobj +from tempfile import TemporaryDirectory from typing import Optional, TextIO, Tuple, Union import numpy as np @@ -47,7 +50,7 @@ from ivas_processing_scripts.audiotools.constants import ( ) -class Metadata: +class SceneDescription: def __init__(self, meta_file: Union[str, Path]): self.meta_file = Path(meta_file) @@ -203,6 +206,8 @@ def write_ISM_metadata_in_file( file_names = file_name for i, csv_file in enumerate(file_names): + if not str(csv_file).endswith(".csv"): + continue # skip MASA metadata number_frames = metadata[i].shape[0] number_columns = metadata[i].shape[1] with open(csv_file, "w", newline="") as file: @@ -297,7 +302,7 @@ def trim_meta( return -def concat_meta_from_file( +def concat_ism_metadata_files( audio_files: list[str], meta_files: list[list[str]], out_file: list[str], @@ -376,7 +381,7 @@ def concat_meta_from_file( # add preamble if preamble: - concat_meta_all_obj = add_remove_metadata_preamble( + concat_meta_all_obj = add_remove_ism_metadata_preamble( concat_meta_all_obj, preamble ) @@ -623,7 +628,7 @@ def metadata_search_MASA( return list_meta -def add_remove_metadata_preamble( +def add_remove_ism_metadata_preamble( metadata, preamble, add: Optional[bool] = True, @@ -657,3 +662,90 @@ def add_remove_metadata_preamble( ) return metadata + + +def concat_masa_metadata_files( + meta_files: list[list[str]], + out_file: Union[str, Path], +) -> None: + """ + Concatenate ISM metadata from files + + Parameters + ---------- + meta_files: list[list[str]] + List of corresponding metadata file names + out_file: Union[str, Path] + Name of concatenated output file + """ + # meta_files is a list of lists + # this could be a list of ISM metadata files (OMASA) + # or just a single MASA metadata file, so we use the last index + with open(out_file, "wb") as out_file: + for meta in meta_files: + with open(meta[-1], "rb") as in_file: + copyfileobj(in_file, out_file) + + +def add_masa_metadata_preamble_repeat( + masa: audio.MetadataAssistedSpatialAudio, + metadata_in: Path, + metadata_out: Path, + preamble: int, + repeat: Optional[bool] = False, + logger: Optional[logging.Logger] = None, +) -> None: + preamble_frames = preamble / IVAS_FRAME_LEN_MS + if not preamble_frames.is_integer(): + raise ValueError( + f"Application of preamble for MASA metadata is only possible if preamble length is multiple of frame length. " + f"Frame length: {IVAS_FRAME_LEN_MS}ms" + ) + preamble_frames = int(preamble_frames) + + from ivas_processing_scripts.audiotools.wrappers.masaAnalyzer import masaAnalyzer + + with TemporaryDirectory() as tmp_dir: + tmp_dir = Path(tmp_dir) + tmp_meta = tmp_dir.joinpath("preamble.met") + + # generate zero metadata for preamble + if preamble > 0: + frame_smp = 48 * IVAS_FRAME_LEN_MS + preamble_smp = int(frame_smp * (preamble / IVAS_FRAME_LEN_MS)) + + fmt = "HOA2" if masa.dirs == 2 else "FOA" + sba = audio.fromtype(fmt) + sba.audio = np.zeros([preamble_smp, sba.num_channels]) + sba.fs = 48000 + + num_tc = masa.num_channels - getattr(masa, "num_ism_channels", 0) + + masaAnalyzer(sba, num_tc, masa.dirs, tmp_meta) + + if logger: + logger.debug(f"Preamble Metadata size {tmp_meta.stat().st_size}") + + # concatenate preamble and metadata + with open(tmp_meta, "ab") as out_meta: + # append out the original input metadata + with open(metadata_in, "rb") as in_meta: + copyfileobj(in_meta, out_meta) + + if logger: + logger.debug(f"Input Metadata size {metadata_in.stat().st_size}") + logger.debug( + f"Input + Preamble concatenated metadata size {tmp_meta.stat().st_size}" + ) + + if repeat: + # repeat the entire file + with open(tmp_meta, "rb") as in_meta: + md = in_meta.read() + with open(metadata_out, "wb") as out_meta: + out_meta.write(md * 2) + else: + copyfile(tmp_meta, metadata_out) + + if logger: + logger.debug(f"Output Metadata size {metadata_out.stat().st_size}") diff --git a/ivas_processing_scripts/processing/chains.py b/ivas_processing_scripts/processing/chains.py index b58ab4a91811773f0f575134fbd0862a0d3c80d3..9c52bdc8af1c93af291c71cf0871fa31cea3f857 100755 --- a/ivas_processing_scripts/processing/chains.py +++ b/ivas_processing_scripts/processing/chains.py @@ -54,7 +54,7 @@ from ivas_processing_scripts.processing.tx import get_tx_cfg from ivas_processing_scripts.utils import ( find_binary, get_abs_path, - list_audio, + list_audio_or_md, parse_gain, ) @@ -112,7 +112,7 @@ def init_processing_chains(cfg: TestConfig) -> None: ) # list items in input directory - cfg.items_list = list_audio( + cfg.items_list = list_audio_or_md( cfg.input_path, select_list=getattr(cfg, "input_select", None) ) if len(cfg.items_list) == 0: @@ -659,10 +659,10 @@ def validate_input_files(cfg: TestConfig): aligned_output_dir = cfg.output_path / "20ms_aligned_files" try: aligned_output_dir.mkdir(exist_ok=False, parents=True) - except FileExistsError: - raise ValueError( - "Folder for 20ms aligned files already exists. Please move or delete folder" - ) + except FileExistsError as e: + raise FileExistsError( + f"Folder for 20ms aligned files already exists. Please move or delete folder {aligned_output_dir}" + ) from e for i, item in enumerate(cfg.items_list): if "fs" in cfg.input: diff --git a/ivas_processing_scripts/processing/preprocessing_2.py b/ivas_processing_scripts/processing/preprocessing_2.py index cce42bbf20a5f7d9775cd799e6ac35a7c03859b5..2402a856ef519add7d89e1db535ab96c3af867d9 100644 --- a/ivas_processing_scripts/processing/preprocessing_2.py +++ b/ivas_processing_scripts/processing/preprocessing_2.py @@ -40,7 +40,8 @@ from ivas_processing_scripts.audiotools import audio from ivas_processing_scripts.audiotools.audioarray import trim from ivas_processing_scripts.audiotools.audiofile import write from ivas_processing_scripts.audiotools.metadata import ( - add_remove_metadata_preamble, + add_masa_metadata_preamble_repeat, + add_remove_ism_metadata_preamble, write_ISM_metadata_in_file, ) from ivas_processing_scripts.audiotools.wrappers.bs1770 import ( @@ -64,26 +65,15 @@ class Preprocessing2(Processing): self.in_fmt, in_file, fs=self.in_fs, in_meta=in_meta ) - if isinstance( - audio_object, (audio.MetadataAssistedSpatialAudio, audio.OMASAAudio) - ): - if self.preamble > 0 or self.background_noise or self.repeat_signal: - raise ValueError( - "No preprocessing 2 possible for formats including MASA metadata" - ) - # modify ISM metadata if self.in_fmt.startswith("ISM"): - if not self.preamble: - preamble = 0 - else: - preamble = self.preamble + preamble = self.preamble or 0 # read out old metadata = audio_object.object_pos # add preamble - metadata = add_remove_metadata_preamble(metadata, preamble) + metadata = add_remove_ism_metadata_preamble(metadata, preamble) # repeat signal if self.repeat_signal: @@ -95,6 +85,16 @@ class Preprocessing2(Processing): audio_object.metadata_files = meta_files audio_object.object_pos = metadata + if "MASA" in self.in_fmt: + add_masa_metadata_preamble_repeat( + audio_object, + in_meta[-1], + out_file.with_suffix(".wav.met"), + self.preamble or 0, + self.repeat_signal, + logger, + ) + # modify audio signal # add preamble if self.preamble > 0: diff --git a/ivas_processing_scripts/processing/processing.py b/ivas_processing_scripts/processing/processing.py index aa495efac20a7700fbcd3ac1ccd71bcfada3c817..64368954b9d9736b252db6bd1e0e1520cc66af41 100755 --- a/ivas_processing_scripts/processing/processing.py +++ b/ivas_processing_scripts/processing/processing.py @@ -44,13 +44,18 @@ from ivas_processing_scripts.audiotools.audiofile import concat, trim from ivas_processing_scripts.audiotools.constants import IVAS_FRAME_LEN_MS from ivas_processing_scripts.audiotools.convert.__init__ import convert from ivas_processing_scripts.audiotools.metadata import ( - add_remove_metadata_preamble, - concat_meta_from_file, + add_remove_ism_metadata_preamble, + concat_ism_metadata_files, + concat_masa_metadata_files, ) from ivas_processing_scripts.constants import LOGGER_DATEFMT, LOGGER_FORMAT from ivas_processing_scripts.processing.config import TestConfig from ivas_processing_scripts.processing.tx import get_timescaled_splits -from ivas_processing_scripts.utils import apply_func_parallel, list_audio, pairwise +from ivas_processing_scripts.utils import ( + apply_func_parallel, + list_audio_or_md, + pairwise, +) class Processing(ABC): @@ -120,30 +125,41 @@ def concat_setup(cfg: TestConfig, chain, logger: logging.Logger): except AttributeError: input_format = cfg.input["fmt"] - # concatenation of met files not possible -> do not concatenate MASA and OMASA - if "MASA" in input_format: - raise ValueError( - "Concatenation of formats including MASA metadata not possible" - ) + concat_md_prefix = cfg.tmp_dirs[0].joinpath( + f"{cfg.input_path.name}_concatenated.wav" + ) - # concatenate ISM metadata if input_format.startswith("ISM"): cfg.concat_meta = [] - for obj_idx in range(len(cfg.metadata_path[0])): - cfg.concat_meta.append( - cfg.tmp_dirs[0].joinpath( - f"{cfg.input_path.name}_concatenated.wav.{obj_idx}.csv" - ) - ) - concat_meta_from_file( + for i, md in enumerate(cfg.metadata_path[0]): + if md.suffix == ".csv": + cfg.concat_meta.append(Path(f"{concat_md_prefix}.{i}.csv")) + elif md.suffix == ".met": + cfg.concat_meta.append(Path(f"{concat_md_prefix}.met")) + + concat_ism_metadata_files( cfg.items_list, cfg.metadata_path, cfg.concat_meta, input_format, ) + if "MASA" in input_format: + concat_masa_metadata_files( + cfg.metadata_path, + cfg.concat_meta[-1], + ) # set input to the concatenated file we have just written to the output dir cfg.metadata_path = [cfg.concat_meta] + elif input_format.startswith("MASA"): + cfg.concat_meta = Path(f"{concat_md_prefix}.met") + concat_masa_metadata_files( + cfg.metadata_path, + cfg.concat_meta, + ) + + # set input to the concatenated file we have just written to the output dir + cfg.metadata_path = [[cfg.concat_meta]] # concatenate audio cfg.concat_file = cfg.tmp_dirs[0].joinpath( @@ -317,7 +333,7 @@ def preprocess(cfg, logger): ) # update the configuration to use preprocessing outputs as new inputs - cfg.items_list = list_audio( + cfg.items_list = list_audio_or_md( cfg.out_dirs[0], select_list=getattr(cfg, "input_select", None) ) @@ -394,17 +410,16 @@ def preprocess_2(cfg, logger): ) # update the configuration to use preprocessing 2 outputs as new inputs - cfg.items_list = list_audio( + cfg.items_list = list_audio_or_md( cfg.out_dirs[0], select_list=getattr(cfg, "input_select", None) ) - if cfg.metadata_path[0] is not None: - for item_idx in range(len(cfg.metadata_path)): - for obj_idx in range(len(cfg.metadata_path[item_idx])): - if cfg.metadata_path[item_idx][obj_idx]: - cfg.metadata_path[item_idx][obj_idx] = cfg.out_dirs[0] / Path( - f"{cfg.items_list[item_idx].stem}.wav.{obj_idx}.csv" - ) + # update the metadata list to use preprocessing 2 outputs as well + for md_files in cfg.metadata_path: + for i, md in enumerate(md_files): + if md: + md_files[i] = cfg.out_dirs[0] / md.name + # remove already applied processing stage cfg.proc_chains = cfg.proc_chains[1:] cfg.tmp_dirs = cfg.tmp_dirs[1:] @@ -536,7 +551,7 @@ def remove_preamble(x, out_fmt, fs, repeat_signal, preamble_len_ms, meta, logger # remove preamble if preamble_len_ms > 0: - meta = add_remove_metadata_preamble(meta, preamble_len_ms, add=False) + meta = add_remove_ism_metadata_preamble(meta, preamble_len_ms, add=False) # cut first half of signal if repeat_signal: diff --git a/ivas_processing_scripts/utils.py b/ivas_processing_scripts/utils.py index 945d07911c6ee8a7048399b5c23132b51f6a5aab..a12b886aa0fe28d4cc45c5f42cb147ef1d7a4853 100755 --- a/ivas_processing_scripts/utils.py +++ b/ivas_processing_scripts/utils.py @@ -45,6 +45,7 @@ from typing import Callable, Iterable, Optional, Union import yaml ALLOWED_INPUT_EXT = (".wav", ".pcm", ".txt", ".raw") +ALLOWED_INPUT_EXT_MD = (".csv", ".met") BIN_DIR = Path(__file__).parent.joinpath("bin") BUSY_SPINNER = cycle(["|", "/", "-", "\\"]) @@ -99,7 +100,9 @@ class DirManager: ) -def list_audio(path: str, select_list: list = None) -> list: +def list_audio_or_md( + path: str, select_list: list = None, allowed_ext=ALLOWED_INPUT_EXT +) -> list: """ Return list with all files with ALLOWED_INPUT_EXT found under the given path. @@ -114,11 +117,11 @@ def list_audio(path: str, select_list: list = None) -> list: audio_list = [ f.resolve().absolute() for f in path.iterdir() - if f.suffix in ALLOWED_INPUT_EXT + if f.suffix in allowed_ext ] else: ext = path.suffix - if ext in ALLOWED_INPUT_EXT: + if ext in allowed_ext: audio_list.append(path.resolve().absolute()) # filter according to select list diff --git a/tests/test_experiments.py b/tests/test_experiments.py index fe959f612743a44583a62ce6d420ef45c08075ed..34d8ddf32c829a646cb56711c9d853dda3410a92 100644 --- a/tests/test_experiments.py +++ b/tests/test_experiments.py @@ -41,7 +41,7 @@ from ivas_processing_scripts import main as generate_test from ivas_processing_scripts.audiotools import audio from ivas_processing_scripts.audiotools.audiofile import read, write from ivas_processing_scripts.processing.config import TestConfig -from ivas_processing_scripts.utils import list_audio +from ivas_processing_scripts.utils import list_audio_or_md from tests.constants import ( FORMAT_TO_METADATA_FILES, INPUT_EXPERIMENT_NAMES, @@ -108,7 +108,7 @@ def all_lengths_equal(cfg): all_lengths_equal = True for condition in cfg.conditions_to_generate.keys(): output_condition_folder = output_folder.joinpath(condition) - for input_file in list_audio(cfg.input_path): + for input_file in list_audio_or_md(cfg.input_path): output_file = output_condition_folder.joinpath(input_file.name).with_suffix( f".{condition}.wav" )