Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

misc. fixes and improvements from BS.2076-2 branch #63

Merged
merged 17 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ear/core/direct_speakers/panner.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ def __init__(self, layout, point_source_opts={}, additional_substitutions={}):
self._screen_edge_lock_handler = ScreenEdgeLockHandler(self.layout.screen, layout)

self.pvs = np.eye(self.n_channels)
self.pvs.flags.writeable = False

self.substitutions = {
"LFE": "LFE1",
Expand Down
2 changes: 1 addition & 1 deletion ear/core/objectbased/gain_calc.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def get_weighted_distances(self, channel_positions, position):

class AlloChannelLockHandler(ChannelLockHandlerBase):
"""Channel lock specialised for allocentric; allocentric loudspeaker
positions are used, and the distance calculation is unweighted."""
positions are used, and the distance calculation is weighted."""

def __init__(self, layout):
super(AlloChannelLockHandler, self).__init__(layout)
Expand Down
6 changes: 3 additions & 3 deletions ear/core/objectbased/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,11 @@ def __init__(self, layout, gain_calc_opts, decorrelator_opts, block_size):
# apply to the samples it produces.
self.block_processing_channels = []

decorrlation_filters = decorrelate.design_decorrelators(layout, **decorrelator_opts)
decorrelator_delay = (decorrlation_filters.shape[0] - 1) // 2
decorrelation_filters = decorrelate.design_decorrelators(layout, **decorrelator_opts)
decorrelator_delay = (decorrelation_filters.shape[0] - 1) // 2

decorrelators = OverlapSaveConvolver(
block_size, self._nchannels, decorrlation_filters)
block_size, self._nchannels, decorrelation_filters)
self.decorrelators_vbs = VariableBlockSizeAdapter(
block_size, self._nchannels, decorrelators.filter_block)

Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
64 changes: 47 additions & 17 deletions ear/core/objectbased/test/test_gain_calc_changes.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,36 @@ def generate_random_ObjectTypeMetadatas():
yield generate_random_ObjectTypeMetadata()


def load_jsonl_xz(file_name):
"""load objects from a lzma-compressed newline-separated JSON (jsonl) file"""
from ....test.json import json_to_value
import json
import lzma

objects = []
with lzma.open(file_name, "rb") as f:
for line in f:
json_line = json.loads(line.decode("utf8"))
obj = json_to_value(json_line)
objects.append(obj)

return objects


def dump_jsonl_xz(file_name, objects):
"""dump objects to a lzma-compressed newline-separated JSON (jsonl) file"""
from ....test.json import value_to_json
import json
import lzma

with lzma.open(file_name, "wb") as f:
for obj in objects:
json_obj = value_to_json(obj, include_defaults=False)
json_line = json.dumps(json_obj, sort_keys=True, separators=(",", ":"))
f.write(json_line.encode("utf8"))
f.write(b"\n")


@pytest.mark.no_cover
def test_changes_random(layout, gain_calc):
"""Check that the result of the gain calculator with a selection of
Expand All @@ -153,33 +183,33 @@ def test_changes_random(layout, gain_calc):
import py.path
files_dir = py.path.local(__file__).dirpath() / "data" / "gain_calc_pvs"

import pickle
inputs_f = files_dir / "inputs.pickle"
outputs_f = files_dir / "outputs.npz"
inputs_f = files_dir / "inputs.jsonl.xz"
outputs_f = files_dir / "outputs.jsonl.xz"

if inputs_f.check():
with open(str(inputs_f), 'rb') as f:
inputs = pickle.load(f)
inputs = load_jsonl_xz(str(inputs_f))
else:
inputs = list(generate_random_ObjectTypeMetadatas())

inputs_f.dirpath().ensure_dir()
with open(str(inputs_f), 'wb') as f:
pickle.dump(inputs, f, protocol=2)
dump_jsonl_xz(str(inputs_f), inputs)

pvs = [gain_calc.render(input) for input in inputs]
direct = np.array([pv.direct for pv in pvs])
diffuse = np.array([pv.diffuse for pv in pvs])

if outputs_f.check():
loaded = np.load(str(outputs_f))
loaded_directs = loaded["direct"]
loaded_diffuses = loaded["diffuse"]

for input, pv, loaded_direct, loaded_diffuse in zip(inputs, pvs, loaded_directs, loaded_diffuses):
npt.assert_allclose(pv.direct, loaded_direct, atol=1e-10, err_msg=repr(input))
npt.assert_allclose(pv.diffuse, loaded_diffuse, atol=1e-10, err_msg=repr(input))
loaded = load_jsonl_xz(str(outputs_f))

for input, pv, expected in zip(inputs, pvs, loaded):
npt.assert_allclose(
pv.direct, expected["direct"], atol=1e-10, err_msg=repr(input)
)
npt.assert_allclose(
pv.diffuse, expected["diffuse"], atol=1e-10, err_msg=repr(input)
)
else:
outputs_f.dirpath().ensure_dir()
np.savez_compressed(str(outputs_f), direct=direct, diffuse=diffuse)
pvs_json = [
dict(direct=pv.direct.tolist(), diffuse=pv.diffuse.tolist()) for pv in pvs
]
dump_jsonl_xz(str(outputs_f), pvs_json)
pytest.skip("generated pv file for gain calc")
2 changes: 1 addition & 1 deletion ear/core/plot_point_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def plot_triangulation(point_source_panner):
import mpl_toolkits.mplot3d.art3d # noqa

fig = plt.figure()
ax = fig.add_subplot(111, projection='3d', aspect=1)
ax = fig.add_subplot(111, projection='3d', aspect='equal')

if isinstance(point_source_panner, point_source.PointSourcePannerDownmix):
point_source_panner = point_source_panner.psp
Expand Down
Empty file.
52 changes: 52 additions & 0 deletions ear/core/scenebased/test/test_design.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import numpy as np
import pytest
from ... import hoa, point_source
from ...bs2051 import get_layout
from ...metadata_input import HOATypeMetadata
from ..design import HOADecoderDesign

# compare against a reference, as otherwise we'd end up reimplementing the
# whole thing, and still would not spot changes in allrad_design

ref_decoder = np.array(
[
[1.71634590e-01, 1.42431019e-01, -1.17545274e-01, 1.30305331e-01],
[1.71642551e-01, -1.42433751e-01, -1.17551622e-01, 1.30321095e-01],
[1.13881860e-01, 7.52101654e-06, -8.55161879e-02, 1.23448338e-01],
[3.68460920e-01, 2.13546281e-01, -1.68472356e-01, -2.38326574e-01],
[3.68450573e-01, -2.13530753e-01, -1.68465249e-01, -2.38333209e-01],
[1.30447818e-01, 6.67387033e-02, 1.45593901e-01, 9.44637862e-02],
[1.30433098e-01, -6.67320618e-02, 1.45594222e-01, 9.44437810e-02],
[1.76070328e-01, 9.23726786e-02, 2.01041658e-01, -6.00913147e-02],
[1.76077278e-01, -9.23760367e-02, 2.01043852e-01, -6.00980940e-02],
]
)


@pytest.fixture(scope="module")
def layout():
return get_layout("4+5+0").without_lfe


@pytest.fixture(scope="module")
def panner(layout):
return HOADecoderDesign(layout)


@pytest.fixture
def type_metadata():
order = 1
acn = np.arange((order + 1) ** 2)
orders, degrees = hoa.from_acn(acn)

return HOATypeMetadata(
orders=orders.tolist(),
degrees=degrees.tolist(),
normalization="N3D",
)


def test_basic(panner, type_metadata):
decoder = panner.design(type_metadata)
# print(repr(decoder))
np.testing.assert_allclose(decoder, ref_decoder, atol=1e-6)
1 change: 0 additions & 1 deletion ear/core/select_items/hoa.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from functools import partial
from ...fileio.adm.exceptions import AdmError
from .utils import get_path_param

# functions which take a path through the audioPackFormats and an
Expand Down
3 changes: 1 addition & 2 deletions ear/core/select_items/test/test_hoa.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@


class HOABuilder(ADMBuilder):
"""ADMBuilder with pre-defined encode and decode matrix, with references to
the various components"""
"""ADMBuilder with pre-defined first and second order HOA packs"""

def __init__(self):
super(HOABuilder, self).__init__()
Expand Down
2 changes: 1 addition & 1 deletion ear/fileio/adm/elements/main_elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ class AudioContent(ADMElement):
audioContentLanguage (Optional[str])
loudnessMetadata (list[LoudnessMetadata])
dialogue (Optional[int])
audioObject (list[AudioObject])
audioObjects (list[AudioObject])
"""

audioContentName = attrib(default=None, validator=instance_of(string_types))
Expand Down
111 changes: 111 additions & 0 deletions ear/test/json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import attrs
import ear.common
import ear.core.metadata_input
import ear.fileio.adm.elements
from fractions import Fraction


def value_to_json(value, include_defaults=True):
"""turn EAR types into JSON

This is not complete but will be extended as needed.

The result can be turned back into objects using json_to_value

Parameters:
include_defaults (bool): add default values (True), or skip them (False)
Returns:
a json-serialisable object, composed of dict, list, bool, int, float,
str or None

objects are represented as a dictionary containing the keyword
arguments for the constructor, and a _type key naming the type
"""

def recurse(v):
return value_to_json(v, include_defaults=include_defaults)

if value is None:
return None
elif isinstance(value, (bool, int, float, str)):
return value
elif isinstance(value, list):
return [recurse(v) for v in value]
elif isinstance(value, dict):
return {k: recurse(v) for k, v in value.items()}
elif attrs.has(type(value)):
d = dict(_type=type(value).__name__)

for field in attrs.fields(type(value)):
v = getattr(value, field.name)

if include_defaults or not _field_is_default(field, v):
d[field.name] = recurse(v)
return d
# types that need special handling; make this generic if we need to add more
elif isinstance(value, Fraction):
return dict(
_type="Fraction", numerator=value.numerator, denominator=value.denominator
)
else:
assert False, "unknown type"


def json_to_value(value):
"""turn the results of value_to_json back into objects"""
if isinstance(value, dict):
converted = {k: json_to_value(v) for k, v in value.items()}
if "_type" in value:
t = _known_types[value["_type"]]
del converted["_type"]
return t(**converted)
else:
return converted
if isinstance(value, list):
return [json_to_value(v) for v in value]
else:
return value


def _field_is_default(field: attrs.Attribute, value):
"""does an attrs field have the default value?

this depends on values being properly equality-comparable
"""
default = field.default
if default is attrs.NOTHING:
return False

if isinstance(default, attrs.Factory):
assert not default.takes_self, "not implemented"

default = default.factory()

return type(value) is type(default) and value == default


def _get_known_types():
known_types = [
ear.common.CartesianPosition,
ear.common.CartesianScreen,
ear.common.PolarPosition,
ear.common.PolarScreen,
ear.core.metadata_input.ExtraData,
ear.core.metadata_input.ObjectTypeMetadata,
ear.fileio.adm.elements.AudioBlockFormatObjects,
ear.fileio.adm.elements.CartesianZone,
ear.fileio.adm.elements.ChannelLock,
ear.fileio.adm.elements.Frequency,
ear.fileio.adm.elements.JumpPosition,
ear.fileio.adm.elements.ObjectCartesianPosition,
ear.fileio.adm.elements.ObjectDivergence,
ear.fileio.adm.elements.ObjectPolarPosition,
ear.fileio.adm.elements.PolarZone,
ear.fileio.adm.elements.ScreenEdgeLock,
Fraction,
]

return {t.__name__: t for t in known_types}


_known_types = _get_known_types()
55 changes: 55 additions & 0 deletions ear/test/test_json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import pytest
from .json import json_to_value, value_to_json
from ear.core.metadata_input import AudioBlockFormatObjects, ObjectTypeMetadata
from fractions import Fraction


def _check_json_subset(d1, d2):
"""check that dictionaries in d1 are a subset of d2, and that all values
are representable in json"""

def is_type(t):
return isinstance(d1, t) and isinstance(d2, t)

if is_type(dict):
for key in d1:
assert key in d2
_check_json_subset(d1[key], d2[key])
elif is_type(list):
assert len(d1) == len(d2)
for v1, v2 in zip(d1, d2):
_check_json_subset(v1, v2)
elif is_type(bool) or is_type(int) or is_type(float) or is_type(str):
assert d1 == d2
elif d1 is None and d2 is None:
pass
else:
assert False, "mismatching or unknown types"


@pytest.mark.parametrize("include_defaults", [False, True])
def test_json_OTM(include_defaults):
otm = ObjectTypeMetadata(
block_format=AudioBlockFormatObjects(
rtime=Fraction(1, 2), position=dict(azimuth=0, elevation=0)
)
)
otm_j = value_to_json(otm, include_defaults=include_defaults)
expected = {
"_type": "ObjectTypeMetadata",
"block_format": {
"_type": "AudioBlockFormatObjects",
"position": {
"_type": "ObjectPolarPosition",
"azimuth": 0.0,
"elevation": 0.0,
},
"rtime": {"_type": "Fraction", "denominator": 2, "numerator": 1},
},
}

_check_json_subset(expected, otm_j)

otm_j_otm = json_to_value(otm_j)

assert otm_j_otm == otm
7 changes: 4 additions & 3 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading