From 257ebeaca24a81b80b0e6ee2db082e3d7b03e364 Mon Sep 17 00:00:00 2001 From: Max Weidauer Date: Tue, 6 Aug 2024 19:31:19 +0200 Subject: [PATCH 1/5] fixed conv_pol2ncart (not used anymore tho) --- src/osc_kreuz/conversionsTools.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/osc_kreuz/conversionsTools.py b/src/osc_kreuz/conversionsTools.py index 115e6a8..646f6e1 100644 --- a/src/osc_kreuz/conversionsTools.py +++ b/src/osc_kreuz/conversionsTools.py @@ -1,5 +1,3 @@ -# Comment on Conversions: In General x and y axis were swapped because 0° should be at the front which can easily achieved throu that - from typing import Any import numpy as np @@ -41,7 +39,8 @@ def conv_pol2cart(azim, elev, dist) -> list[float]: def conv_pol2ncart(azim, elev, dist) -> list[float]: - return aed2xyz(azim, elev, 1, coordinates_in_degree=True) + x, y, z = aed2xyz(azim, elev, 1, coordinates_in_degree=True) + return [x, y, z, dist] def conv_cart2pol(x, y, z) -> list[float]: From 6550f6cf78056dcfe9b9e33c7a930b7a3cdd2afe Mon Sep 17 00:00:00 2001 From: Max Weidauer Date: Tue, 6 Aug 2024 19:31:43 +0200 Subject: [PATCH 2/5] formatting --- src/osc_kreuz/__init__.py | 4 +- src/osc_kreuz/_version.py | 157 +++++++++++++++++++++++--------------- 2 files changed, 97 insertions(+), 64 deletions(-) diff --git a/src/osc_kreuz/__init__.py b/src/osc_kreuz/__init__.py index ecd3379..4d52a61 100644 --- a/src/osc_kreuz/__init__.py +++ b/src/osc_kreuz/__init__.py @@ -1,3 +1,3 @@ - from . import _version -__version__ = _version.get_versions()['version'] + +__version__ = _version.get_versions()["version"] diff --git a/src/osc_kreuz/_version.py b/src/osc_kreuz/_version.py index 6083db3..8778e3b 100644 --- a/src/osc_kreuz/_version.py +++ b/src/osc_kreuz/_version.py @@ -1,4 +1,3 @@ - # This file helps to compute a version number in source trees obtained from # git-archive tarball (such as those provided by githubs download-from-tag # feature). Distribution tarballs (built by setup.py sdist) and build @@ -68,12 +67,14 @@ class NotThisMethod(Exception): def register_vcs_handler(vcs: str, method: str) -> Callable: # decorator """Create decorator to mark a method as the handler of a VCS.""" + def decorate(f: Callable) -> Callable: """Store f in HANDLERS[vcs][method].""" if vcs not in HANDLERS: HANDLERS[vcs] = {} HANDLERS[vcs][method] = f return f + return decorate @@ -100,10 +101,14 @@ def run_command( try: dispcmd = str([command] + args) # remember shell=False, so use git.cmd on windows, not just git - process = subprocess.Popen([command] + args, cwd=cwd, env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None), **popen_kwargs) + process = subprocess.Popen( + [command] + args, + cwd=cwd, + env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr else None), + **popen_kwargs, + ) break except OSError as e: if e.errno == errno.ENOENT: @@ -141,15 +146,21 @@ def versions_from_parentdir( for _ in range(3): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): - return {"version": dirname[len(parentdir_prefix):], - "full-revisionid": None, - "dirty": False, "error": None, "date": None} + return { + "version": dirname[len(parentdir_prefix) :], + "full-revisionid": None, + "dirty": False, + "error": None, + "date": None, + } rootdirs.append(root) root = os.path.dirname(root) # up a level if verbose: - print("Tried directories %s but none started with prefix %s" % - (str(rootdirs), parentdir_prefix)) + print( + "Tried directories %s but none started with prefix %s" + % (str(rootdirs), parentdir_prefix) + ) raise NotThisMethod("rootdir doesn't start with parentdir_prefix") @@ -212,7 +223,7 @@ def git_versions_from_keywords( # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " - tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} + tags = {r[len(TAG) :] for r in refs if r.startswith(TAG)} if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %d @@ -221,7 +232,7 @@ def git_versions_from_keywords( # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". - tags = {r for r in refs if re.search(r'\d', r)} + tags = {r for r in refs if re.search(r"\d", r)} if verbose: print("discarding '%s', no digits" % ",".join(refs - tags)) if verbose: @@ -229,32 +240,36 @@ def git_versions_from_keywords( for ref in sorted(tags): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): - r = ref[len(tag_prefix):] + r = ref[len(tag_prefix) :] # Filter out refs that exactly match prefix or that don't start # with a number once the prefix is stripped (mostly a concern # when prefix is '') - if not re.match(r'\d', r): + if not re.match(r"\d", r): continue if verbose: print("picking %s" % r) - return {"version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None, - "date": date} + return { + "version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, + "error": None, + "date": date, + } # no suitable tags, so version is "0+unknown", but full hex is still there if verbose: print("no suitable tags, using unknown + full revision id") - return {"version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": "no suitable tags", "date": None} + return { + "version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, + "error": "no suitable tags", + "date": None, + } @register_vcs_handler("git", "pieces_from_vcs") def git_pieces_from_vcs( - tag_prefix: str, - root: str, - verbose: bool, - runner: Callable = run_command + tag_prefix: str, root: str, verbose: bool, runner: Callable = run_command ) -> Dict[str, Any]: """Get version from 'git describe' in the root of the source tree. @@ -273,8 +288,7 @@ def git_pieces_from_vcs( env.pop("GIT_DIR", None) runner = functools.partial(runner, env=env) - _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=not verbose) + _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=not verbose) if rc != 0: if verbose: print("Directory %s not under git control" % root) @@ -282,10 +296,19 @@ def git_pieces_from_vcs( # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = runner(GITS, [ - "describe", "--tags", "--dirty", "--always", "--long", - "--match", f"{tag_prefix}[[:digit:]]*" - ], cwd=root) + describe_out, rc = runner( + GITS, + [ + "describe", + "--tags", + "--dirty", + "--always", + "--long", + "--match", + f"{tag_prefix}[[:digit:]]*", + ], + cwd=root, + ) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") @@ -300,8 +323,7 @@ def git_pieces_from_vcs( pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None - branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], - cwd=root) + branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], cwd=root) # --abbrev-ref was added in git-1.6.3 if rc != 0 or branch_name is None: raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") @@ -341,17 +363,16 @@ def git_pieces_from_vcs( dirty = git_describe.endswith("-dirty") pieces["dirty"] = dirty if dirty: - git_describe = git_describe[:git_describe.rindex("-dirty")] + git_describe = git_describe[: git_describe.rindex("-dirty")] # now we have TAG-NUM-gHEX or HEX if "-" in git_describe: # TAG-NUM-gHEX - mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) + mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe) if not mo: # unparsable. Maybe git-describe is misbehaving? - pieces["error"] = ("unable to parse git-describe output: '%s'" - % describe_out) + pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out return pieces # tag @@ -360,10 +381,12 @@ def git_pieces_from_vcs( if verbose: fmt = "tag '%s' doesn't start with prefix '%s'" print(fmt % (full_tag, tag_prefix)) - pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" - % (full_tag, tag_prefix)) + pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % ( + full_tag, + tag_prefix, + ) return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix):] + pieces["closest-tag"] = full_tag[len(tag_prefix) :] # distance: number of commits since tag pieces["distance"] = int(mo.group(2)) @@ -412,8 +435,7 @@ def render_pep440(pieces: Dict[str, Any]) -> str: rendered += ".dirty" else: # exception #1 - rendered = "0+untagged.%d.g%s" % (pieces["distance"], - pieces["short"]) + rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered @@ -442,8 +464,7 @@ def render_pep440_branch(pieces: Dict[str, Any]) -> str: rendered = "0" if pieces["branch"] != "master": rendered += ".dev0" - rendered += "+untagged.%d.g%s" % (pieces["distance"], - pieces["short"]) + rendered += "+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered @@ -604,11 +625,13 @@ def render_git_describe_long(pieces: Dict[str, Any]) -> str: def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: """Render the given version pieces into the requested style.""" if pieces["error"]: - return {"version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"], - "date": None} + return { + "version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"], + "date": None, + } if not style or style == "default": style = "pep440" # the default @@ -632,9 +655,13 @@ def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: else: raise ValueError("unknown style '%s'" % style) - return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None, - "date": pieces.get("date")} + return { + "version": rendered, + "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], + "error": None, + "date": pieces.get("date"), + } def get_versions() -> Dict[str, Any]: @@ -648,8 +675,7 @@ def get_versions() -> Dict[str, Any]: verbose = cfg.verbose try: - return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, - verbose) + return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, verbose) except NotThisMethod: pass @@ -658,13 +684,16 @@ def get_versions() -> Dict[str, Any]: # versionfile_source is the relative path from the top of the source # tree (where the .git directory might live) to this file. Invert # this to find the root from __file__. - for _ in cfg.versionfile_source.split('/'): + for _ in cfg.versionfile_source.split("/"): root = os.path.dirname(root) except NameError: - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to find root of source tree", - "date": None} + return { + "version": "0+unknown", + "full-revisionid": None, + "dirty": None, + "error": "unable to find root of source tree", + "date": None, + } try: pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) @@ -678,6 +707,10 @@ def get_versions() -> Dict[str, Any]: except NotThisMethod: pass - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to compute version", "date": None} + return { + "version": "0+unknown", + "full-revisionid": None, + "dirty": None, + "error": "unable to compute version", + "date": None, + } From d9efaf68622771f31475da31e083ac79bf064bc8 Mon Sep 17 00:00:00 2001 From: Max Weidauer Date: Tue, 6 Aug 2024 19:33:43 +0200 Subject: [PATCH 3/5] completely reworked handling of coordinates internally --- src/osc_kreuz/coordinates.py | 287 ++++++++++++++++++++++++++ src/osc_kreuz/osccomcenter.py | 4 +- src/osc_kreuz/renderer.py | 247 +++++++++++----------- src/osc_kreuz/soundobject.py | 283 +++++-------------------- src/osc_kreuz/str_keys_conventions.py | 169 +-------------- tests/test_coordinates.py | 52 +++++ tests/test_renderer.py | 6 +- tests/test_soundobject.py | 47 ++++- 8 files changed, 562 insertions(+), 533 deletions(-) create mode 100644 src/osc_kreuz/coordinates.py create mode 100644 tests/test_coordinates.py diff --git a/src/osc_kreuz/coordinates.py b/src/osc_kreuz/coordinates.py new file mode 100644 index 0000000..73d5844 --- /dev/null +++ b/src/osc_kreuz/coordinates.py @@ -0,0 +1,287 @@ +from collections.abc import Iterable +from enum import Enum +from functools import lru_cache +from itertools import chain, combinations + +import numpy as np +import osc_kreuz.conversionsTools as conversions + + +class CoordinateFormatException(Exception): + pass + + +class CoordinateSystemType(Enum): + Cartesian = 0 + Polar = 1 + PolarRadians = 2 + + +class CoordinateKey(Enum): + a = "a" + e = "e" + d = "d" + x = "x" + y = "y" + z = "z" + + +radians_suffix = "rad" + +# coordinate keys for the different coordinate systems +allowed_coordinate_keys = { + CoordinateSystemType.Cartesian: [CoordinateKey.x, CoordinateKey.y, CoordinateKey.z], + CoordinateSystemType.Polar: [CoordinateKey.a, CoordinateKey.e, CoordinateKey.d], + CoordinateSystemType.PolarRadians: [ + CoordinateKey.a, + CoordinateKey.e, + CoordinateKey.d, + ], +} + +# coordinates that should be scaled when a scaling factor exists +scalable_coordinates = [ + CoordinateKey.x, + CoordinateKey.y, + CoordinateKey.z, + CoordinateKey.d, +] + +# aliases for the single keys of polar coordinates +polar_coordinate_aliases = { + "azimuth": CoordinateKey.a, + "azim": CoordinateKey.a, + "elevation": CoordinateKey.e, + "elev": CoordinateKey.e, + "distance": CoordinateKey.d, + "dist": CoordinateKey.d, +} + +# type to make typing easier to read +CoordinateFormatTuple = tuple[CoordinateSystemType, list[CoordinateKey]] + + +class Coordinate: + """Baseclass for all coordinates, only __init__() and convert_to() need to be overwritten.""" + + def __init__( + self, position_keys: list[CoordinateKey], initial_values: list + ) -> None: + self.position_keys = position_keys + self.position = {} + + # initialize position dict + for key, val in zip(position_keys, initial_values): + self.position[key] = val + + def convert_to( + self, coordinate_format: CoordinateSystemType + ) -> list[float] | tuple[float, float, float]: + raise NotImplementedError + + def set_all(self, *values: float): + """Sets all coordinates in the order they were declared in the constructor. + + Raises: + CoordinateFormatException: raised when the number of values is not correct + """ + if len(values) != len(self.position_keys): + raise CoordinateFormatException( + f"Invalid Number of values for coordinate: {len(values)}" + ) + + for key, val in zip(self.position_keys, values): + self.position[key] = val + + def get_all(self) -> list[float]: + """gets all coordinates in the order they were declared in the constructor + + Returns: + list[float]: list of coordinates + """ + return self.get_coordinates(self.position_keys) + + def set_coordinates( + self, + coordinates: CoordinateKey | list[CoordinateKey], + values: float | list[float], + scaling_factor: float = 1.0, + ) -> bool: + """Sets values for all specified coordinates. values and coordinates need to be lists of the same length + + Args: + coordinates (CoordinateKey | list[CoordinateKey]): single coordinate or list of coordinates to set + values (float | list[float]): single value or list of values to set + scaling_factor (float, optional): used to scale the coordinate system . Defaults to 1.0. + + Raises: + CoordinateFormatException: raised when the Coordinate Format is invalid + + Returns: + bool: True if the coordinate changed, False if the same value as before was set + """ + # make sure input is iterable + if not isinstance(coordinates, Iterable): + coordinates = [coordinates] + if not isinstance(values, Iterable): + values = [values] + + coordinate_changed = False + + for c_key, val in zip(coordinates, values): + + # scale coordinate if necessary + if c_key in scalable_coordinates: + val = val * scaling_factor + + # set the coordinate if it exists + try: + if self.position[c_key] != val: + self.position[c_key] = val + coordinate_changed = True + except KeyError: + raise CoordinateFormatException(f"Invalid Coordinate Key: {c_key}") + + return coordinate_changed + + def get_coordinates( + self, coordinates: CoordinateKey | list[CoordinateKey] + ) -> list[float]: + """Get all specified coordinates + + Args: + coordinates (CoordinateKey | list[CoordinateKey]): single coordinate or list of desired coordinates + + Returns: + list[float]: list of coordinates + """ + if not isinstance(coordinates, Iterable): + coordinates = [coordinates] + return [self.position[key] for key in coordinates] + + +class CoordinateCartesian(Coordinate): + def __init__(self, x, y, z) -> None: + super().__init__([CoordinateKey.x, CoordinateKey.y, CoordinateKey.z], [x, y, z]) + + def convert_to( + self, coordinate_format: CoordinateSystemType + ) -> list[float] | tuple[float, float, float]: + if coordinate_format == CoordinateSystemType.Polar: + return conversions.xyz2aed(*self.get_all(), coordinates_in_degree=True) + elif coordinate_format == CoordinateSystemType.PolarRadians: + return conversions.xyz2aed(*self.get_all(), coordinates_in_degree=False) + else: + raise CoordinateFormatException( + f"Invalid Conversion format for Cartesion Coordinates: {coordinate_format}" + ) + + +class CoordinatePolar(Coordinate): + def __init__(self, a, e, d) -> None: + super().__init__([CoordinateKey.a, CoordinateKey.e, CoordinateKey.d], [a, e, d]) + + def convert_to( + self, coordinate_format: CoordinateSystemType + ) -> list[float] | tuple[float, float, float]: + if coordinate_format == CoordinateSystemType.Cartesian: + return conversions.aed2xyz(*self.get_all(), coordinates_in_degree=True) + elif coordinate_format == CoordinateSystemType.PolarRadians: + a, e, d = self.get_all() + return a / 180 * np.pi, e / 180 * np.pi, d + else: + raise CoordinateFormatException( + f"Invalid Conversion format for Polar Coordinates: {coordinate_format}" + ) + + +class CoordinatePolarRadians(Coordinate): + def __init__(self, a, e, d) -> None: + super().__init__([CoordinateKey.a, CoordinateKey.e, CoordinateKey.d], [a, e, d]) + + def convert_to( + self, coordinate_format: CoordinateSystemType + ) -> list[float] | tuple[float, float, float]: + if coordinate_format == CoordinateSystemType.Cartesian: + return conversions.aed2xyz(*self.get_all(), coordinates_in_degree=False) + elif coordinate_format == CoordinateSystemType.Polar: + a, e, d = self.get_all() + return a / np.pi * 180, e / np.pi * 180, d + else: + raise CoordinateFormatException( + f"Invalid Conversion format for PolarRadians Coordinates: {coordinate_format}" + ) + + +@lru_cache(maxsize=100) +def parse_coordinate_format(format_str: str) -> CoordinateFormatTuple: + """Parse an incoming coordinate format string to the format required by Coordinate + + Args: + format_str (str): coordinate format string, for example "aed", "xy", "azimrad" + + Raises: + CoordinateFormatException: _description_ + + Returns: + CoordinateFormatTuple: Tuple containing the type of the coordinate system and a list of the individual coordinate keys + """ + + # find the coordinate system type + coordinate_format = CoordinateSystemType.Cartesian + + if format_str.endswith(radians_suffix): + format_str = format_str[: -len(radians_suffix)] + coordinate_format = CoordinateSystemType.PolarRadians + + elif ( + CoordinateKey[format_str[0]] + in allowed_coordinate_keys[CoordinateSystemType.Polar] + ): + coordinate_format = CoordinateSystemType.Polar + + coordinate_keys = [] + + # parse special aliases + if ( + coordinate_format == CoordinateSystemType.Polar + or coordinate_format == CoordinateSystemType.PolarRadians + ): + try: + coordinate_keys.append(polar_coordinate_aliases[format_str]) + format_str = "" + except KeyError: + pass + + # parse remaining letters individually + for key in format_str: + try: + coordinate_keys.append(CoordinateKey[key]) + except ValueError: + raise CoordinateFormatException(f"Invalid coordinate key {key}") + + # TODO check that only coordinate keys legal to the current system are used + return (coordinate_format, coordinate_keys) + + +def powerset(iterable): + "powerset([1,2,3]) --> () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)" + s = list(iterable) + return chain.from_iterable(combinations(s, r) for r in range(1, len(s) + 1)) + + +def get_all_coordinate_formats() -> list[str]: + format_strings: list[str] = [] + for key in allowed_coordinate_keys: + suffix = radians_suffix if key == CoordinateSystemType.PolarRadians else "" + all_combinations = [ + "".join([k.value for k in combinations]) + suffix + for combinations in powerset(allowed_coordinate_keys[key]) + ] + format_strings.extend(all_combinations) + + for key in polar_coordinate_aliases: + format_strings.append(key) + format_strings.append(key + radians_suffix) + + return format_strings diff --git a/src/osc_kreuz/osccomcenter.py b/src/osc_kreuz/osccomcenter.py index e15954f..5af19a7 100644 --- a/src/osc_kreuz/osccomcenter.py +++ b/src/osc_kreuz/osccomcenter.py @@ -7,6 +7,8 @@ import ipaddress import logging +from osc_kreuz.coordinates import get_all_coordinate_formats + log = logging.getLogger("OSCcomcenter") @@ -297,7 +299,7 @@ def setupOscBindings(): n_sources = globalconfig["number_sources"] # Setup OSC Callbacks for positional data - for key in skc.posformat.keys(): + for key in get_all_coordinate_formats(): for addr in build_osc_paths(skc.OscPathType.Position, key): bindToDataAndUiPort(addr, partial(oscreceived_setPosition, key)) diff --git a/src/osc_kreuz/renderer.py b/src/osc_kreuz/renderer.py index b101b79..13a448f 100644 --- a/src/osc_kreuz/renderer.py +++ b/src/osc_kreuz/renderer.py @@ -88,7 +88,7 @@ def __init__( self, path: bytes, soundobject: SoundObject, - coord_fmt: skc.CoordFormats, + coord_fmt: str, source_index: int | None = None, pre_arg: Any = None, post_arg: Any = None, @@ -185,7 +185,7 @@ def setVerbosity(cls, v: int): def __init__( self, - dataformat: skc.CoordFormats | str = skc.CoordFormats.xyz, + dataformat: str = "xyz", updateintervall=10, hostname="127.0.0.1", hosts: list[dict] | None = None, @@ -195,7 +195,7 @@ def __init__( ): self.setVerbosity(verbosity) - self.posFormat = skc.CoordFormats(dataformat) + self.posFormat = dataformat self.sourceAttributes = sourceattributes # check if hosts are defined as an array @@ -227,7 +227,7 @@ def __init__( ] self.debugPrefix = "/genericRenderer" - self.oscPre = ("/source/" + self.posFormat.value).encode() + self.oscPre = ("/source/" + self.posFormat).encode() self.receivers: list[OSCClient] = [] for ip, port in self.hosts: @@ -242,7 +242,7 @@ def print_self_information(self, print_pos_format=True): hosts_str = ", ".join([f"{hostname}:{port}" for hostname, port in self.hosts]) log.info(f"\thosts: {hosts_str}") if print_pos_format: - log.info(f"\tlistening to format {self.posFormat.value}") + log.info(f"\tlistening to format {self.posFormat}") def my_type(self) -> str: return "basic Rendererclass: abstract class, doesnt listen" @@ -364,7 +364,7 @@ def sourcePositionChanged(self, source_idx): class Wonder(SpatialRenderer): def __init__(self, **kwargs): if not "dataformat" in kwargs.keys(): - kwargs["dataformat"] = skc.CoordFormats.xy + kwargs["dataformat"] = "xy" if not "sourceattributes" in kwargs.keys(): kwargs["sourceattributes"] = ( skc.SourceAttributes.doppler, @@ -452,7 +452,7 @@ def update_auto_angle(self, source_idx: int): path=self.attributeOsc[skc.SourceAttributes.angle], soundobject=self.sources[source_idx], source_index=source_idx, - coord_fmt=skc.CoordFormats.azim, + coord_fmt="azim", post_arg=self.interpolTime, ), ) @@ -538,7 +538,7 @@ def __init__(self, paths: Iterable[dict["str", Any]], **kwargs): super().__init__(**kwargs) self.debugPrefix = "/dAudioMatrix" self.gain_paths: dict[int, list[bytes]] = {} - self.pos_paths: list[tuple[bytes, skc.CoordFormats]] = [] + self.pos_paths: list[tuple[bytes, str]] = [] # this dict is used to translate between render unit index and render unit name self.render_unit_indices = {} @@ -561,9 +561,9 @@ def __init__(self, paths: Iterable[dict["str", Any]], **kwargs): self.gain_paths[renderer_index].append(osc_path.encode()) elif path_type in ["position", "pos"]: try: - coord_fmt = skc.CoordFormats(path["format"]) + coord_fmt = path["format"] except: - coord_fmt = skc.CoordFormats("xyz") + coord_fmt = "xyz" self.pos_paths.append((osc_path.encode(), coord_fmt)) log.debug("Audio Matrix initialized") @@ -627,7 +627,7 @@ def sourcePositionChanged(self, source_idx): class SuperColliderEngine(SpatialRenderer): def __init__(self, **kwargs): if not "dataformat" in kwargs.keys(): - kwargs["dataformat"] = skc.aed + kwargs["dataformat"] = "aed" super(SuperColliderEngine, self).__init__(**kwargs) self.oscPre = b"/source/pos/aed" @@ -650,8 +650,6 @@ def __init__(self, aliasname, **kwargs): self.pingCounter = 0 self.debugPrefix = "/d{}".format(aliasname.decode()) - # self.biAlias = b'' - # self.setAlias(aliasname) self.indexAsValue = False if "indexAsValue" in kwargs.keys(): @@ -670,7 +668,8 @@ def __init__(self, aliasname, **kwargs): # self.idxSourceOscPreAttri self.pingTimer: Timer | None = None - # TODO upon initialization send full current state + + # send current state to viewclient for i in range(self.globalConfig["number_sources"]): self.sourcePositionChanged(i) for j in range(self.globalConfig["n_renderengines"]): @@ -679,7 +678,7 @@ def __init__(self, aliasname, **kwargs): def createOscPrefixes(self): for i in range(self.numberOfSources): self.idxSourceOscPrePos[i] = "/source/{}/{}".format( - i + 1, self.posFormat.value + i + 1, self.posFormat ).encode() _aDic = {} for attr in skc.knownAttributes: @@ -773,100 +772,100 @@ def sourceRenderGainChanged(self, source_idx, render_idx): ) -class Oscar(SpatialRenderer): - def __init__(self, **kwargs): - if not "dataformat" in kwargs.keys(): - kwargs["dataformat"] = skc.aed - super(Oscar, self).__init__(**kwargs) - - self.sourceAttributes = ( - skc.SourceAttributes.doppler, - skc.SourceAttributes.planewave, - ) +# class Oscar(SpatialRenderer): +# def __init__(self, **kwargs): +# if not "dataformat" in kwargs.keys(): +# kwargs["dataformat"] = "aed" +# super(Oscar, self).__init__(**kwargs) - # self.posAddrs = [] +# self.sourceAttributes = ( +# skc.SourceAttributes.doppler, +# skc.SourceAttributes.planewave, +# ) - self.oscPosPre = [] - self.oscAttrPre = [] - self.oscRenderPre = [] - self.oscDirectPre = [] +# # self.posAddrs = [] - for i in range(self.numberOfSources): - sourceAddrs = {} - for kk in skc.fullformat[self.posFormat.value]: - addrStr = "/source/" + str(i + 1) + "/" + kk - sourceAddrs[kk] = addrStr.encode() - self.oscPosPre.append(sourceAddrs) +# self.oscPosPre = [] +# self.oscAttrPre = [] +# self.oscRenderPre = [] +# self.oscDirectPre = [] - attrDic = {} - for key in self.sourceAttributes: - oscStr = "/source" + str(i + 1) + "/" + key.value - attrDic[key] = oscStr.encode() - self.oscAttrPre.append(attrDic) +# for i in range(self.numberOfSources): +# sourceAddrs = {} +# for kk in skc.fullformat[self.posFormat]: +# addrStr = "/source/" + str(i + 1) + "/" + kk +# sourceAddrs[kk] = addrStr.encode() +# self.oscPosPre.append(sourceAddrs) - renderGainOscs = [] - for rId in range(self.globalConfig["n_renderengines"]): - riOsc = "/source/" + str(i + 1) + "/render/" + str(rId) - renderGainOscs.append(riOsc.encode()) - self.oscRenderPre.append(renderGainOscs) +# attrDic = {} +# for key in self.sourceAttributes: +# oscStr = "/source" + str(i + 1) + "/" + key.value +# attrDic[key] = oscStr.encode() +# self.oscAttrPre.append(attrDic) - channelSend = [] - for cId in range(self.globalConfig["number_direct_sends"]): - csOsc = "/source/" + str(i + 1) + "/direct/" + str(cId) - channelSend.append(csOsc.encode()) - self.oscDirectPre.append(channelSend) +# renderGainOscs = [] +# for rId in range(self.globalConfig["n_renderengines"]): +# riOsc = "/source/" + str(i + 1) + "/render/" + str(rId) +# renderGainOscs.append(riOsc.encode()) +# self.oscRenderPre.append(renderGainOscs) - # self.posAddrs.append(sourceAddrs) +# channelSend = [] +# for cId in range(self.globalConfig["number_direct_sends"]): +# csOsc = "/source/" + str(i + 1) + "/direct/" + str(cId) +# channelSend.append(csOsc.encode()) +# self.oscDirectPre.append(channelSend) - self.validPosKeys = {skc.dist} +# # self.posAddrs.append(sourceAddrs) - self.isDataClient = True +# self.validPosKeys = {skc.dist} - self.debugPrefix = "/dOscar" +# self.isDataClient = True - def my_type(self) -> str: - return "Oscar" +# self.debugPrefix = "/dOscar" - def sourcePositionChanged(self, source_idx): - for key in skc.fullformat[self.posFormat.value]: - self.add_update( - source_idx, - PositionUpdate( - self.oscPosPre[source_idx][key], - soundobject=self.sources[source_idx], - coord_fmt=skc.CoordFormats(key), - ), - ) - - def sourceAttributeChanged(self, source_idx, attribute): - self.add_update( - source_idx, - AttributeUpdate( - path=self.oscAttrPre[source_idx][attribute], - soundobject=self.sources[source_idx], - attribute=attribute, - ), - ) - - def sourceDirectSendChanged(self, source_idx, send_idx): - self.add_update( - source_idx, - DirectSendUpdate( - path=self.oscDirectPre[source_idx][send_idx], - soundobject=self.sources[source_idx], - send_index=send_idx, - ), - ) - - def sourceRenderGainChanged(self, source_idx, render_idx): - self.add_update( - source_idx, - GainUpdate( - path=self.oscRenderPre[source_idx][render_idx], - soundobject=self.sources[source_idx], - render_idx=render_idx, - ), - ) +# def my_type(self) -> str: +# return "Oscar" + +# def sourcePositionChanged(self, source_idx): +# for key in skc.fullformat[self.posFormat.value]: +# self.add_update( +# source_idx, +# PositionUpdate( +# self.oscPosPre[source_idx][key], +# soundobject=self.sources[source_idx], +# coord_fmt=skc.CoordFormats(key), +# ), +# ) + +# def sourceAttributeChanged(self, source_idx, attribute): +# self.add_update( +# source_idx, +# AttributeUpdate( +# path=self.oscAttrPre[source_idx][attribute], +# soundobject=self.sources[source_idx], +# attribute=attribute, +# ), +# ) + +# def sourceDirectSendChanged(self, source_idx, send_idx): +# self.add_update( +# source_idx, +# DirectSendUpdate( +# path=self.oscDirectPre[source_idx][send_idx], +# soundobject=self.sources[source_idx], +# send_index=send_idx, +# ), +# ) + +# def sourceRenderGainChanged(self, source_idx, render_idx): +# self.add_update( +# source_idx, +# GainUpdate( +# path=self.oscRenderPre[source_idx][render_idx], +# soundobject=self.sources[source_idx], +# render_idx=render_idx, +# ), +# ) class SeamlessPlugin(SpatialRenderer): @@ -875,7 +874,7 @@ def my_type(self) -> str: def __init__(self, **kwargs): if not "dataformat" in kwargs.keys(): - kwargs["dataformat"] = skc.xyz + kwargs["dataformat"] = "xyz" super(SeamlessPlugin, self).__init__(**kwargs) self.sourceAttributes = ( @@ -885,8 +884,7 @@ def __init__(self, **kwargs): self.oscAddrs: dict = {} - for key in skc.fullformat[self.posFormat.value]: - self.oscAddrs[key] = "/source/pos/{}".format(key).encode() + self.oscAddrs[self.posFormat] = f"/source/pos/{self.posFormat}".encode() for vv in self.sourceAttributes: self.oscAddrs[vv.value] = "/{}".format(vv.value).encode() @@ -920,16 +918,15 @@ def sourceRenderGainChanged(self, source_idx, render_idx): ) def sourcePositionChanged(self, source_idx): - for key in skc.fullformat[self.posFormat.value]: - self.add_update( - source_idx, - PositionUpdate( - path=self.oscAddrs[key], - soundobject=self.sources[source_idx], - coord_fmt=skc.CoordFormats(key), - source_index=source_idx + 1, - ), - ) + self.add_update( + source_idx, + PositionUpdate( + path=self.oscAddrs[self.posFormat], + soundobject=self.sources[source_idx], + coord_fmt=self.posFormat, + source_index=source_idx + 1, + ), + ) class DataClient(Audiorouter, SpatialRenderer): @@ -940,7 +937,7 @@ class DataClient(Audiorouter, SpatialRenderer): "wonder": Wonder, # "panoramix": Panoramix, "viewclient": ViewClient, - "oscar": Oscar, + # "oscar": Oscar, "scengine": SuperColliderEngine, "audiorouter": Audiorouter, "seamlessplugin": SeamlessPlugin, @@ -951,20 +948,20 @@ class DataClient(Audiorouter, SpatialRenderer): def createRendererClient(config: dict) -> Renderer: - # XXX some weird shit is happening here - if "dataformat" in config: - tmp_dataFormat = config["dataformat"] - if not tmp_dataFormat in skc.posformat.keys(): - if len(tmp_dataFormat.split("_")) == 2: - preStr = "" - if tmp_dataFormat.split("_")[0] == "normcartesian": - preStr = "n" - - dFo = preStr + tmp_dataFormat.split("_")[1] - config["dataformat"] = dFo - else: - log.warn("unknown position format") - del config["dataformat"] + # (probably) a workaround for OSCAR, removed for now + # if "dataformat" in config: + # tmp_dataFormat = config["dataformat"] + # if not tmp_dataFormat in skc.posformat.keys(): + # if len(tmp_dataFormat.split("_")) == 2: + # preStr = "" + # if tmp_dataFormat.split("_")[0] == "normcartesian": + # preStr = "n" + + # dFo = preStr + tmp_dataFormat.split("_")[1] + # config["dataformat"] = dFo + # else: + # log.warn("unknown position format") + # del config["dataformat"] if "type" not in config: raise RendererException("Type of receiver unspecified") diff --git a/src/osc_kreuz/soundobject.py b/src/osc_kreuz/soundobject.py index 4562219..815a707 100644 --- a/src/osc_kreuz/soundobject.py +++ b/src/osc_kreuz/soundobject.py @@ -4,64 +4,50 @@ from functools import partial from time import time +from osc_kreuz.coordinates import ( + CoordinateSystemType, + parse_coordinate_format, + Coordinate, + CoordinateCartesian, + CoordinatePolarRadians, + CoordinatePolar, +) + _tt = "time" _uiBlock = "_uiDidBlock" +class SoundObjectException(Exception): + pass + + class SoundObject(object): globalConfig: dict = {} - # data_port_timeout = 0.5 number_renderer = 1 - # maxGain = 2. send_change_only = False - # + preferUi = True dataPortTimeOut = 1.0 - conversionMap = { - skc.polar: { - (False, True, True): (ct.conv_ncart2pol, skc.CoordFormats.nxyzd), - (False, True, False): (ct.conv_cart2pol, skc.CoordFormats.xyz), - (False, False, True): (ct.conv_ncart2pol, skc.CoordFormats.nxyzd), - }, - skc.cartesian: { - (True, False, True): (ct.conv_ncart2cart, skc.CoordFormats.nxyzd), - (True, False, False): (ct.conv_pol2cart, skc.CoordFormats.aed), - (False, False, True): (ct.conv_ncart2cart, skc.CoordFormats.nxyzd), - }, - skc.normcartesian: { - (True, True, False): (ct.conv_cart2ncart, skc.CoordFormats.xyz), - (True, False, False): (ct.conv_pol2ncart, skc.CoordFormats.aed), - (False, True, False): (ct.conv_cart2ncart, skc.CoordFormats.xyz), - }, - } @classmethod def readGlobalConfig(cls, config: dict): cls.globalConfig = config cls.preferUi = bool(not config["data_port_timeout"] == 0) cls.dataPortTimeOut = float(config["data_port_timeout"]) - # cls.number_renderer = def __init__(self, objectID: int = 0, coordinate_scaling_factor: float = 1): self.objectID = objectID - self._position: dict[str, float] = { - skc.x: 0.0, - skc.y: 1.0, - skc.z: 0.0, - skc.azim: 0.0, - skc.elev: 0.0, - skc.dist: 1.0, - skc.angle: 0.0, - skc.nx: 0.0, - skc.ny: 1.0, - skc.nz: 0.0, - skc.az_rad: 0.0, - skc.el_rad: 0.0, + self.position: dict[CoordinateSystemType, Coordinate] = { + CoordinateSystemType.Cartesian: CoordinateCartesian(1, 0, 0), + CoordinateSystemType.Polar: CoordinatePolar(0, 0, 1), + CoordinateSystemType.PolarRadians: CoordinatePolarRadians(0, 0, 1), } + self.coordinate_scaling_factor = coordinate_scaling_factor + self._sourceattributes = { skc.SourceAttributes.planewave: 0, skc.SourceAttributes.doppler: 0, @@ -72,19 +58,7 @@ def __init__(self, objectID: int = 0, coordinate_scaling_factor: float = 1): self.number_renderer = len(self.globalConfig["render_units"]) self._torendererSends = [float(0.0) for _ in range(self.number_renderer)] - self._changedSends: set = set() self._directSends = [0.0] * self.globalConfig["number_direct_sends"] - self._changedDirSends = set() - - self._positionIsSet = { - skc.polar: False, - skc.cartesian: True, - skc.normcartesian: True, - skc.polar_rad: False, - } - self._lastUpdateKey = "" - - self._contState = skc.sControl_state.auto_switch_control self.uiBlockingDict = {} self.uiBlockingDict["position"] = self.createBlockingDict() @@ -98,179 +72,45 @@ def __init__(self, objectID: int = 0, coordinate_scaling_factor: float = 1): for i in range(self.globalConfig["number_direct_sends"]): self.uiBlockingDict["directsend"].append(self.createBlockingDict()) - # self.t_renderGain = [time()] * self.number_renderer - - # self._uiDidSetRenderGain = [False] * self.number_renderer - # self.t_directSend = [time()] * self.globalConfig['number_direct_sends'] - # self._uiDidSetDirectSend = [False] * self.globalConfig['number_direct_sends'] - def createBlockingDict(self) -> dict: return {_tt: time(), _uiBlock: False} - def _getPositionValuesForKey(self, key: skc.CoordFormats) -> list[float]: - vals = [] - for kk in skc.posformat[key.value][1]: - vals.append(self._position[kk]) - - return vals - - def setControlState(self, state: skc.sControl_state): - self._contState = state - - def updateCoordinateFormat(self, updateFormat): - - # if updateFormat = - calcRad = False - calcDeg = False - if updateFormat == skc.polar_rad: - if self._positionIsSet[skc.polar]: - _convert = False - else: - _convert = True - updateFormat = skc.polar - calcRad = True - - elif updateFormat == skc.polar and self._positionIsSet[skc.polar_rad]: - calcDeg = True - _convert = False - - else: - calcRad = False - _convert = True - - if _convert: - statusTupel = ( - self._positionIsSet[skc.polar], - self._positionIsSet[skc.cartesian], - self._positionIsSet[skc.normcartesian], - ) - - convert_metadata = self.conversionMap[updateFormat][statusTupel] - conversion = partial( - convert_metadata[0] - ) # , skc.posformat[convert_metadata[1]]) - - updatedCoo = conversion(*self._getPositionValuesForKey(convert_metadata[1])) - for idx, coo in enumerate(skc.fullformat[updateFormat]): - self._position[coo] = updatedCoo[idx] - - if calcRad: - self._position[skc.az_rad] = np.deg2rad(self._position[skc.azim]) - self._position[skc.el_rad] = np.deg2rad(self._position[skc.elev]) - # [self._position[skc.az_rad], self._position[skc.el_rad]] = ( - # ct.convert_deg_angleToRad_correctMath( - # self._position[skc.azim], self._position[skc.elev] - # ) - # ) - self._positionIsSet[skc.polar_rad] = True - elif calcDeg: - self._position[skc.azim] = np.rad2deg(self._position[skc.az_rad]) - self._position[skc.elev] = np.rad2deg(self._position[skc.el_rad]) - - self._positionIsSet[updateFormat] = True - - def setPosition(self, coordinate_key: str, *values, fromUi: bool = True) -> bool: - """This function is called to set the position of this sound object. - - Args: - coordinate_key (str): _description_ - fromUi (bool, optional): _description_. Defaults to True. - - Returns: - bool: True if the position was changed - """ + def setPosition( + self, coordinate_format_str: str, *values: float, fromUi: bool = True + ) -> bool: if not self.shouldProcessInput(self.uiBlockingDict["position"], fromUi): return False - coordinateType = skc.posformat[coordinate_key][0] - sthChanged = False - - # handle formats that are not full where coordinates are - # currently not present in the right format - if ( - not self._positionIsSet[coordinateType] - and not skc.posformat[coordinate_key][2] - ): - self.updateCoordinateFormat(coordinateType) - sthChanged = True - - # reset positionIsSet for all coordinate types - for key in self._positionIsSet.keys(): - self._positionIsSet[key] = False - - self._lastUpdateKey = coordinate_key - - # TODO:copy old position - - # here the Position is set - for idx, key in enumerate(skc.posformat[coordinate_key][1]): - newValue = values[idx] - - # don't let distance become zero. coordinate_scaling also happens here - # TODO find better solution, this just works for aed, not for xyz coordinates - if key == skc.dist: - newValue = newValue * self.coordinate_scaling_factor - newValue = np.maximum(self.globalConfig["min_dist"], newValue) - elif coordinateType == skc.cartesian or key == skc.nd: - newValue *= self.coordinate_scaling_factor - - if self.globalConfig[skc.send_changes_only]: - if not newValue == self._position[key]: - sthChanged = True - self._position[key] = newValue - else: - self._position[key] = newValue - sthChanged = True - - self._positionIsSet[coordinateType] = True - - # TODO: compare new Position with old position for return - return sthChanged - - def getSingleValueUpdate(self, keys): - if self._lastUpdateKey in keys: - return (self._lastUpdateKey, self._position[self._lastUpdateKey]) - else: - return False - - def getPosition(self, pos_key: skc.CoordFormats) -> list[float] | float: - """Get Position of this sound object in the format specified in pos_key - - Args: - pos_key (str): str key of the desired position format - - Returns: - list[float]: desired coordinates in a list. ATTENTION: If only one coordinate is requested, it is instead returned as a float. - """ - coordinateType = skc.posformat[pos_key.value][0] - - if not self._positionIsSet[coordinateType]: - self.updateCoordinateFormat(coordinateType) - - coords = [] # np.array([], dtype=float) - for key in skc.posformat[pos_key.value][1]: - coords.append(float(self._position[key])) - # float_coords = coords. - if len(coords) == 1: - return coords[0] - else: - return coords + coordinate_format, coordinate_keys = parse_coordinate_format( + coordinate_format_str + ) + + position_has_changed = self.position[coordinate_format].set_coordinates( + coordinate_keys, values, self.coordinate_scaling_factor + ) + + if position_has_changed: + for c_fmt in self.position: + if c_fmt == coordinate_format: + continue + self.position[c_fmt].set_all( + *(self.position[coordinate_format].convert_to(c_fmt)) + ) + if not self.globalConfig[skc.send_changes_only]: + position_has_changed = True + return position_has_changed + + def getPosition(self, coordinate_format_str: str) -> list[float]: + coordinate_format, coordinate_keys = parse_coordinate_format( + coordinate_format_str + ) + return self.position[coordinate_format].get_coordinates(coordinate_keys) def setAttribute(self, attribute, value, fromUi: bool = True) -> bool: if not self.shouldProcessInput(self.uiBlockingDict["attribute"], fromUi): return False - # if self.preferUi: - # if not fromUi and self._uiDidSetAttribute: - # if self.dataPortStillBlocked(self.t_setAttribute): - # return False - # else: - # self._uiDidSetAttribute = False - # else: - # self.t_position = time() - # self._uiDidSetAttribute = True - if not self._sourceattributes[attribute] == value: self._sourceattributes[attribute] = value return True @@ -287,16 +127,6 @@ def setRendererGain(self, rendIdx: int, gain: float, fromUi: bool = True) -> boo ): return False - # if self.preferUi: - # if not fromUi and self._uiDidSetRenderGain[rendIdx]: - # if self.dataPortStillBlocked(self.t_renderGain[rendIdx]): - # return False - # else: - # self._uiDidSetRenderGain[rendIdx] = False - # else: - # self.t_position = time() - # self._uiDidSetRenderGain[rendIdx] = True - _gain = np.clip(gain, a_min=0, a_max=self.globalConfig[skc.max_gain]) if self.globalConfig[skc.send_changes_only]: @@ -304,26 +134,14 @@ def setRendererGain(self, rendIdx: int, gain: float, fromUi: bool = True) -> boo return False self._torendererSends[rendIdx] = _gain - # self._changedSends.add(rendIdx) return True def setDirectSend(self, directIdx: int, gain: float, fromUi: bool = True) -> bool: - # TODO: replace the fromUi thing with a function if not self.shouldProcessInput( self.uiBlockingDict["directsend"][directIdx], fromUi ): return False - # if self.preferUi: - # if not fromUi and self._uiDidSetDirectSend[directIdx]: - # if self.dataPortStillBlocked(self.t_directSend[directIdx]): - # return False - # else: - # self._uiDidSetDirectSend[directIdx] = False - # else: - # self.t_position = time() - # self._uiDidSetDirectSend[directIdx] = True - _gain = np.clip(gain, a_min=0, a_max=self.globalConfig[skc.max_gain]) if self.globalConfig[skc.send_changes_only]: @@ -331,15 +149,8 @@ def setDirectSend(self, directIdx: int, gain: float, fromUi: bool = True) -> boo return False self._directSends[directIdx] = _gain - # self._changedDirSends.add(directIdx) return True - def checkIsBlocked(self, *args): - pass - - def getBoolAndTime(self, *args): - pass - def getAllRendererGains(self) -> list[float]: return self._torendererSends diff --git a/src/osc_kreuz/str_keys_conventions.py b/src/osc_kreuz/str_keys_conventions.py index f17e89e..82eb0d4 100644 --- a/src/osc_kreuz/str_keys_conventions.py +++ b/src/osc_kreuz/str_keys_conventions.py @@ -1,130 +1,14 @@ -coord_format = "coord_format" -cartesian = "cartesian" -polar = "polar" -normcartesian = "normcartesian" - -x = "x" -y = "y" -z = "z" -azim = "azim" -azimuth = "azim" -elev = "elev" -elevation = "elev" -dist = "dist" -distance = "dist" -angle = "angle" - -az_rad = "az_r" -el_rad = "ev_r" -polar_rad = "pol_rad" - - -a = "a" -e = "e" -d = "d" - -# normalised xyz on a sphere with r = 1 -nx = "nx" -nxd = "nxd" -ny = "ny" -nyd = "nyd" -nz = "nz" -nzd = "nzd" -nd = "dist" # distance - -aed = "aed" -ad = "ad" -ae = "ae" -ed = "ed" -aedrad = "aedrad" -# arad = 'arad' -# erad = 'erad' - -xyz = "xyz" -xy = "xy" -xz = "xz" -yz = "yz" - -nxyz = "nxyz" -nxyzd = "nxyzd" -nxy = "nxy" -nxyd = "nxyd" -nxz = "nxz" -nxzd = "nxzd" -nyz = "nyz" -nyzd = "nyzd" - -source_attributes = "sourceattributes" -doppler = "doppler" -plane = "planewave" -planewave = "planewave" -angle = "angle" - -# this dict contains all supported coordinate formats. -# the tuples follow this format: (coordinatesystemtype, (single coordinates), full positional info?) -posformat = { - # cartesian xyz - x: (cartesian, (x,), False), - y: (cartesian, (y,), False), - z: (cartesian, (z,), False), - xy: (cartesian, (x, y), False), - xz: (cartesian, (x, z), False), - yz: (cartesian, (y, z), False), - xyz: (cartesian, (x, y, z), True), - # polar aed - azim: (polar, (azim,), False), - a: (polar, (azim,), False), - elev: (polar, (elev,), False), - e: (polar, (elev,), False), - dist: (polar, (dist,), False), - d: (polar, (dist,), False), - ad: (polar, (azim, dist), False), - ed: (polar, (elev, dist), False), - ae: (polar, (azim, elev), False), - aed: (polar, (azim, elev, dist), True), - # Polar in rad - aedrad: (polar_rad, (az_rad, el_rad, dist), True), - # oscar specific "normcartesian - nx: (normcartesian, (nx,), False), - nxd: (normcartesian, (nx, nd), False), - ny: (normcartesian, (ny,), False), - nyd: (normcartesian, (ny, nd), False), - nz: (normcartesian, (nz,), False), - nzd: (normcartesian, (nzd,), False), - nxy: (normcartesian, (nx, ny), False), - nxyd: (normcartesian, (nx, ny, nd), False), - nyz: (normcartesian, (ny, nz), False), - nyzd: (normcartesian, (ny, nz, nd), False), - nxyz: (normcartesian, (nx, ny, nz), False), - nxyzd: (normcartesian, (nx, ny, nz, nd), True), - nd: (normcartesian, (nd,), False), -} -fullformat = { - xyz: (x, y, z), - nxyzd: (nx, ny, nz, nd), - aed: (azim, elev, dist), - cartesian: (x, y, z), - normcartesian: (nx, ny, nz, nd), - polar: (azim, elev, dist), -} -fullnames = {azim: "azimuth", elev: "elevation", dist: "distance"} - from enum import Enum import numpy as np - -class sControl_state(Enum): - automation_control = np.ubyte(1) - manually_control = np.ubyte(2) - auto_switch_control = np.ubyte(0) - - class OscPathType(Enum): Position = 1 Properties = 2 Gain = 3 +# This dict contains all the required osc path blueprints. The outermost key is the type of data sent to this path (position, gain, properties). +# each type has two different kinds of paths, base (source index is sent as parameter) and extended (source index is part of the path) osc_paths = { OscPathType.Position: { "base": [ @@ -168,54 +52,19 @@ class OscPathType(Enum): "reverb": ["reverb", "rev"], } +# TODO merge with SourceAttributes +doppler = "doppler" +planewave = "planewave" +angle = "angle" + knownAttributes = {planewave, doppler, angle} class SourceAttributes(Enum): - planewave = planewave - doppler = doppler - angle = angle - - -class CoordFormats(Enum): - x = "x" - y = "y" - z = "z" - azim = "azim" - azimuth = "azim" - elev = "elev" - elevation = "elev" - dist = "dist" - distance = "dist" + planewave = "planewave" + doppler = "doppler" angle = "angle" - nx = "nx" - nxd = "nxd" - ny = "ny" - nyd = "nyd" - nz = "nz" - nzd = "nzd" - nd = "dist" # distance - - aed = "aed" - aedrad = "aedrad" - ad = "ad" - ae = "ae" - ed = "ed" - xyz = "xyz" - xy = "xy" - xz = "xz" - yz = "yz" - - nxyz = "nxyz" - nxyzd = "nxyzd" - nxy = "nxy" - nxyd = "nxyd" - nxz = "nxz" - nxzd = "nxzd" - nyz = "nyz" - nyzd = "nyzd" - # global config keywords globalconfig = "globalconfig" diff --git a/tests/test_coordinates.py b/tests/test_coordinates.py new file mode 100644 index 0000000..8ffb180 --- /dev/null +++ b/tests/test_coordinates.py @@ -0,0 +1,52 @@ +from osc_kreuz.coordinates import ( + CoordinateSystemType, + CoordinateKey, + parse_coordinate_format, +) + + +def test_format_str_parsing(): + for format_str, format_tuple in [ + ( + "aed", + ( + CoordinateSystemType.Polar, + [CoordinateKey.a, CoordinateKey.e, CoordinateKey.d], + ), + ), + ( + "aedrad", + ( + CoordinateSystemType.PolarRadians, + [CoordinateKey.a, CoordinateKey.e, CoordinateKey.d], + ), + ), + ( + "xyz", + ( + CoordinateSystemType.Cartesian, + [CoordinateKey.x, CoordinateKey.y, CoordinateKey.z], + ), + ), + ("x", (CoordinateSystemType.Cartesian, [CoordinateKey.x])), + ("distance", (CoordinateSystemType.Polar, [CoordinateKey.d])), + ("azim", (CoordinateSystemType.Polar, [CoordinateKey.a])), + ("elevrad", (CoordinateSystemType.PolarRadians, [CoordinateKey.e])), + ("arad", (CoordinateSystemType.PolarRadians, [CoordinateKey.a])), + ]: + format_parsed = parse_coordinate_format(format_str) + print("parsing ", format_str) + print(format_parsed[0], format_tuple[0]) + assert format_parsed[0] == format_tuple[0] + + print(format_parsed) + print(len(format_parsed[1]), len(format_tuple[1])) + + assert len(format_parsed[1]) == len(format_tuple[1]) + for key_correct, key_parsed in zip(format_tuple[1], format_parsed[1]): + assert key_correct == key_parsed + print() + + +if __name__ == "__main__": + test_format_str_parsing() diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 51a06fc..360496e 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -141,7 +141,7 @@ def test_wonder_renderer(): "pos", 0, b"/WONDER/source/position", - [0, *so.getPosition(skc.CoordFormats.xy), c.interpolTime], + [0, *so.getPosition("xy"), c.interpolTime], ) for attr, path, expected in [ @@ -179,8 +179,8 @@ def test_audiomatrix_renderer(): 0, [b"/source/pos", b"/source/xyz"], [ - [0, *so.getPosition(skc.CoordFormats.aed)], - [0, *so.getPosition(skc.CoordFormats.xyz)], + [0, *so.getPosition("aed")], + [0, *so.getPosition("xyz")], ], ) diff --git a/tests/test_soundobject.py b/tests/test_soundobject.py index 304d3af..ef6a7af 100644 --- a/tests/test_soundobject.py +++ b/tests/test_soundobject.py @@ -14,14 +14,45 @@ def test_scaling(): SoundObject.readGlobalConfig(global_conf) for scaling, input_format, input_pos, output_xyz, output_aed in [ - (0.5, skc.aed, (90, 0, 1), (0, 0.5, 0), (90, 0, 0.5)), - (0.7, skc.aed, (45, 45, 3), (1.05, 1.05, 1.48492424), (45, 45, 2.1)), - (0.7, skc.aed, (-45, -45, 3), (1.05, -1.05, -1.48492424), (-45, -45, 2.1)), - (2, skc.xyz, (1, 0, 0), (2, 0, 0), (0, 0, 2)), - (2.565, skc.xyz, (5, 0, 0), (12.825, 0, 0), (0, 0, 12.825)), - (25.65, skc.xyz, (0.5, 0, 0), (12.825, 0, 0), (0, 0, 12.825)), + (0.5, "aed", (90, 0, 1), (0, 0.5, 0), (90, 0, 0.5)), + (0.7, "aed", (45, 45, 3), (1.05, 1.05, 1.48492424), (45, 45, 2.1)), + (0.7, "aed", (-45, -45, 3), (1.05, -1.05, -1.48492424), (-45, -45, 2.1)), + (2, "xyz", (1, 0, 0), (2, 0, 0), (0, 0, 2)), + (2.565, "xyz", (5, 0, 0), (12.825, 0, 0), (0, 0, 12.825)), + (25.65, "xyz", (0.5, 0, 0), (12.825, 0, 0), (0, 0, 12.825)), ]: so = SoundObject(coordinate_scaling_factor=scaling) so.setPosition(input_format, *input_pos) - assert np.allclose(so.getPosition(skc.CoordFormats.xyz), output_xyz) - assert np.allclose(so.getPosition(skc.CoordFormats.aed), output_aed) + print(so.getPosition("xyz")) + print(output_xyz) + assert np.allclose(so.getPosition("xyz"), output_xyz) + assert np.allclose(so.getPosition("aed"), output_aed) + + +def test_distance(): + global_conf = { + "number_direct_sends": 2, + "data_port_timeout": 0, + "render_units": ["ambi", "wfs", "reverb"], + "send_changes_only": False, + "min_dist": 0, + } + + SoundObject.readGlobalConfig(global_conf) + + so = SoundObject() + so.setPosition("xyz", *(2, 0, 0)) + assert np.allclose([2.0], [so.getPosition("dist")]) + so.setPosition("xyz", *(3, 0, 0)) + + assert np.allclose([3.0], [so.getPosition("d")]) + + so.setPosition("aed", *(0, 0, 2.5)) + assert np.allclose([2.5], [so.getPosition("distance")]) + + so.setPosition("xyz", *(2, 0, 0)) + assert np.allclose([2.0], [so.getPosition("distance")]) + + +if __name__ == "__main__": + test_scaling() From 5ee925e3f2cc65484092a22348a50e9b694b8286 Mon Sep 17 00:00:00 2001 From: Max Weidauer Date: Thu, 29 Aug 2024 14:36:39 +0200 Subject: [PATCH 4/5] small typing change --- src/osc_kreuz/coordinates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/osc_kreuz/coordinates.py b/src/osc_kreuz/coordinates.py index 73d5844..fd3999a 100644 --- a/src/osc_kreuz/coordinates.py +++ b/src/osc_kreuz/coordinates.py @@ -104,7 +104,7 @@ def get_all(self) -> list[float]: def set_coordinates( self, coordinates: CoordinateKey | list[CoordinateKey], - values: float | list[float], + values: float | Iterable[float], scaling_factor: float = 1.0, ) -> bool: """Sets values for all specified coordinates. values and coordinates need to be lists of the same length From cd9f8f356f79e873eb63cd081e97bc6e2d5caf42 Mon Sep 17 00:00:00 2001 From: Max Weidauer Date: Thu, 29 Aug 2024 14:36:55 +0200 Subject: [PATCH 5/5] changed example and default config --- example_configs/config_en325.yml | 49 +++++++++++++++++--------------- src/osc_kreuz/config_default.yml | 2 +- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/example_configs/config_en325.yml b/example_configs/config_en325.yml index b87f2cf..b4cd708 100644 --- a/example_configs/config_en325.yml +++ b/example_configs/config_en325.yml @@ -9,39 +9,42 @@ globalconfig: number_direct_sends: 46 # including subwoofer send_changes_only: 1 # checks every input for changes, if set to 1 might be slower data_port_timeout: 2 # time a change in the ui-port blocks incoming automation, set to 0 to deactivate this feature - render_units: ["ambi", "wfs", "reverb"] -# changed to remove sendport, rename listenport to port -receivers: - - type: audiorouter - hosts: - - hostname: riviera.ak.tu-berlin.de - port: 57120 - updateintervall: 5 + render_units: ["ambi", "wfs"] + room_scaling_factor: 6.046 # all incoming positon changes are scaled by this factor - - type: audiorouterWFS +receivers: + - type: audiomatrix hosts: - - hostname: riviera.ak.tu-berlin.de - port: 57120 - updateintervall: 5 - + - hostname: localhost + port: 8080 + paths: + - path: /source/send/wfs + renderer: wfs + type: gain + - path: /source/send/hoa + renderer: ambi + type: gain + - path: /source/send/sub + renderer: ambi + type: gain + - path: /source/pos/aed + type: position + format: aedrad + - path: /source/pos/dist + type: position + format: dist + updateintervall: 0 - type: audiomatrix hosts: - - hostname: newmark.ak.tu-berlin.de + - hostname: wintermute.ak.tu-berlin.de port: 8080 paths: - path: /source/send/wfs renderer: wfs type: gain - updateintervall: 5 - + updateintervall: 0 - type: wonder hosts: - - hostname: riviera.ak.tu-berlin.de + - hostname: newmark.ak.tu-berlin.de port: 58100 updateintervall: 50 - - - type: scengine - hostname: riviera.ak.tu-berlin.de - port: 57120 - updateintervall: 10 - dataformat: aedrad diff --git a/src/osc_kreuz/config_default.yml b/src/osc_kreuz/config_default.yml index d7c85c3..db3af71 100644 --- a/src/osc_kreuz/config_default.yml +++ b/src/osc_kreuz/config_default.yml @@ -9,7 +9,7 @@ globalconfig: number_direct_sends: 46 # including subwoofer send_changes_only: 1 # checks every input for changes, if set to 1 might be slower data_port_timeout: 2 # time a change in the ui-port blocks incoming automation, set to 0 to deactivate this feature - render_units: ["ambi", "wfs", "reverb"] + render_units: ["ambi", "wfs"] room_scaling_factor: 1.0 # all incoming positon changes are scaled by this factor receivers: - type: audiorouter