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",