From e3abda2e78548684a8d50b7b83a380cba60ef7d7 Mon Sep 17 00:00:00 2001 From: Robin Scheibler <fakufaku@gmail.com> Date: Mon, 12 Apr 2021 15:10:12 +0900 Subject: [PATCH 1/2] Draft for a file format for rooms and scenes. --- pyroomacoustics/__init__.py | 1 + pyroomacoustics/io/__init__.py | 1 + pyroomacoustics/io/reader.py | 164 +++++++++++++++++++++++++++++++++ pyroomacoustics/parameters.py | 4 +- 4 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 pyroomacoustics/io/__init__.py create mode 100644 pyroomacoustics/io/reader.py diff --git a/pyroomacoustics/__init__.py b/pyroomacoustics/__init__.py index be8a73f2..be37ab1c 100644 --- a/pyroomacoustics/__init__.py +++ b/pyroomacoustics/__init__.py @@ -140,6 +140,7 @@ from . import bss from . import denoise from . import phase +from . import io import warnings warnings.warn( diff --git a/pyroomacoustics/io/__init__.py b/pyroomacoustics/io/__init__.py new file mode 100644 index 00000000..08130027 --- /dev/null +++ b/pyroomacoustics/io/__init__.py @@ -0,0 +1 @@ +from .reader import read_scene diff --git a/pyroomacoustics/io/reader.py b/pyroomacoustics/io/reader.py new file mode 100644 index 00000000..9d50beea --- /dev/null +++ b/pyroomacoustics/io/reader.py @@ -0,0 +1,164 @@ +import json +from pathlib import Path + +import numpy as np +from scipy.io import wavfile + +from ..acoustics import OctaveBandsFactory +from ..parameters import Material +from ..room import Room, wall_factory + + +def parse_vertex(line, line_num): + assert line[0] == v + + line = line.strip().split(" ") + + if len(line) < 4 or len(line > 5): + raise ValueError("Malformed vertex on line {line_num}") + + return np.array([float(line[i]) for i in range(3)]) + + +def read_obj(filename): + with open(filename, "r") as f: + content = f.readlines() + + # keep track of the faces to process later + vertices = [] + unprocessed_faces = [] + + for no, line in enumerate(content): + if line[0] == "v": + vertices.append(parse_vertex(line, no)) + elif line[0] == "f": + unprocessed_faces.append([no, line]) + + for no, line in faces: + pass + + +def read_room_json(filename, fs): + + with open(filename, "r") as f: + content = json.load(f) + + vertices = np.array(content["vertices"]) + + faces = [] + materials = [] + names = [] + walls_args = [] + + for name, wall_info in content["walls"].items(): + vertex_ids = np.array(wall_info["vertices"]) - 1 + wall_vertices = vertices[vertex_ids].T + + try: + mat_info = wall_info["material"] + if isinstance(mat_info, dict): + mat = Material(**mat_info) + elif isinstance(mat_info, list): + mat = Material(*mat_info) + else: + mat = Material(mat_info) + except KeyError: + mat = Material(energy_absorption=0.0) + + walls_args.append([wall_vertices, mat, name]) + + octave_bands = OctaveBandsFactory(fs=fs) + materials = [a[1] for a in walls_args] + if not Material.all_flat(materials): + for mat in materials: + mat.resample(octave_bands) + + walls = [ + wall_factory(w, m.absorption_coeffs, m.scattering_coeffs, name=n) + for w, m, n in walls_args + ] + + return walls + + +def read_source(source, scene_parent_dir, fs_room): + + if isinstance(source, list): + return {"position": source, "signal": np.zeros(1)} + + elif isinstance(source, dict): + kwargs = {"position": source["loc"]} + + if "signal" in source: + fs_audio, audio = wavfile.read(scene_parent_dir / source["signal"]) + + # convert to float if necessary + if audio.dtype == np.int16: + audio = audio / 2 ** 15 + + if audio.ndim == 2: + import warnings + + warnings.warn( + "The audio file was multichannel. Only keeping channel 1." + ) + audio = audio[:, 0] + + if fs_audio != fs_room: + try: + import samplerate + + fs_ratio = fs_room / fs_audio + audio = samplerate.resample(audio, fs_ratio, "sinc_best") + except ImportError: + raise ImportError( + "The samplerate package must be installed for" + " resampling of the signals." + ) + + kwargs["signal"] = audio + + else: + # add a zero signal is the source is not active + kwargs["signal"] = np.zeros(1) + + if "delay" in source: + kwargs["delay"] = source["delay"] + + return kwargs + + else: + raise TypeError("Unexpected type.") + + +def read_scene(filename): + + filename = Path(filename) + parent_dir = filename.parent + + with open(filename, "r") as f: + scene_info = json.load(f) + + # the sampling rate + try: + fs = scene_info["samplerate"] + except KeyError: + fs = 16000 + + # read the room file + room_filename = parent_dir / scene_info["room"] + walls = read_room_json(room_filename, fs) + + if "room_kwargs" not in scene_info: + scene_info["room_kwargs"] = {} + + room = Room(walls, fs=fs, **scene_info["room_kwargs"]) + for source in scene_info["sources"]: + room.add_source(**read_source(source, parent_dir, fs)) + for mic in scene_info["microphones"]: + room.add_microphone(mic) + + if "ray_tracing" in scene_info: + room.set_ray_tracing(**scene_info["ray_tracing"]) + + return room diff --git a/pyroomacoustics/parameters.py b/pyroomacoustics/parameters.py index d828f940..dc98266d 100644 --- a/pyroomacoustics/parameters.py +++ b/pyroomacoustics/parameters.py @@ -29,7 +29,7 @@ * Scattering coefficients * Air absorption """ -import io +import io as _io import json import os @@ -238,7 +238,7 @@ def from_speed(cls, c): } -with io.open(_materials_database_fn, "r", encoding="utf8") as f: +with _io.open(_materials_database_fn, "r", encoding="utf8") as f: materials_data = json.load(f) center_freqs = materials_data["center_freqs"] From f51e105f8df50bc140f5a65cca6351ea93c2f848 Mon Sep 17 00:00:00 2001 From: Robin Scheibler <fakufaku@gmail.com> Date: Wed, 14 Apr 2021 17:56:04 +0900 Subject: [PATCH 2/2] Adds the sub-module io into the list in the setup file --- setup.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 4fd30e98..ea4ea4c0 100644 --- a/setup.py +++ b/setup.py @@ -12,15 +12,14 @@ exec(f.read()) try: - from setuptools import setup, Extension + from setuptools import Extension, distutils, setup from setuptools.command.build_ext import build_ext - from setuptools import distutils except ImportError: print("Setuptools unavailable. Falling back to distutils.") import distutils + from distutils.command.build_ext import build_ext from distutils.core import setup from distutils.extension import Extension - from distutils.command.build_ext import build_ext class get_pybind_include(object): @@ -28,7 +27,7 @@ class get_pybind_include(object): The purpose of this class is to postpone importing pybind11 until it is actually installed, so that the ``get_include()`` - method can be invoked. """ + method can be invoked.""" def __init__(self, user=False): self.user = user @@ -172,12 +171,18 @@ def build_extensions(self): "pyroomacoustics.bss", "pyroomacoustics.denoise", "pyroomacoustics.phase", + "pyroomacoustics.io", ], # Libroom C extension ext_modules=ext_modules, # Necessary to keep the source files package_data={"pyroomacoustics": ["*.pxd", "*.pyx", "data/materials.json"]}, - install_requires=["Cython", "numpy", "scipy>=0.18.0", "pybind11>=2.2",], + install_requires=[ + "Cython", + "numpy", + "scipy>=0.18.0", + "pybind11>=2.2", + ], cmdclass={"build_ext": BuildExt}, # taken from pybind11 example zip_safe=False, test_suite="nose.collector",