Skip to content

Commit

Permalink
Merge pull request #13 from tu-studio/dev_better_osc_receive_handlers
Browse files Browse the repository at this point in the history
Updated the OSC-Receive Handler and implemented tests
  • Loading branch information
themaxw authored Sep 12, 2024
2 parents 7e6c18a + bbfb58b commit 1a35ae9
Show file tree
Hide file tree
Showing 19 changed files with 1,972 additions and 714 deletions.
58 changes: 52 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ when using the `versioned_install` flag the installation of multiple different v

# OSC-Paths

The osc-kreuz listens on a number of paths for position changes, gain changes and special properties for the wfs system. For most paths there exists a version of the path, where the index of the source that is changed is included in the path, and one where the index is sent as the first OSC argument.
On port 4455 (default value) the osc-kreuz listens on a number of paths for [position changes](#positional-data), [gain changes](#gains) and [special properties](#special-properties) for the wfs system. For most paths there exists a version of the path, where the index of the source that is changed is included in the path, and one where the index is sent as the first OSC argument.

Additionally there is the config port (4999) for the [subscription protocol](#subscription-protocol) and full copying of all outputs with the [debug mode](#debug-functions)

## Positional Data

Expand Down Expand Up @@ -70,6 +72,51 @@ Gains can be set individually for each rendering system, `[rendering_system]` ca

Direct Sends also have an endpoint `/source/send/direct`, but it is not really used at the moment.

## Subscription Protocol

Subcription-Port: 4999

The Subscription Protocol enables Applications to subscribe to the osc-kreuz.

The connection is initialised by a subscricption request from the client which is followed by a regular ping-message from the osc-kreuz that must by answered by a pong-message in order to keep the subscription alive. Source-Position and gain messages should be sent to port 4455 and the subcription-messages to port 4999.

### Subscribe

A client can subscribe to all position and gain messages e.g. a viewer-client during production process. Subcriptions and pong messages should be send to port 4999.
The connection is initialised via:
`/oscrouer/subscribe s i s (i i)` with s = uniqueClientName, i=listeningPort, s=coordinateFormat, i=sourceIndexInOsc(0/1), i=minUpdateIntervall
The last three arguments are optional and are set to '1 10' by default.
e.g. `/oscrouter/subscribe maxViewer 55123 xyz 1 10`
will send source-position messages to the subscribing client as follows:

- For Position
`/source/1/xyz fff` with a max. rate of 100Hz (every 10 ms).
- For gains e.g.
`/source/1/ambi f`

The ip-Address of the client is retrieved automatically from the udp-packet by the OSC-Router.

### ping-pong

The osc-router regularly sends the message
`/oscrouter/ping 4999` to all subscribed clients
which should be answered (to port 4999) with
`/oscrouter/pong uniqueClientName`
The uniqueClientName has to be the same as in the subcription message.
If the client does not answer to the ping message it will be erased after a certain time.

## Debug functions

Port: 4999

A copy of all outgoing osc-messages from the osc-kreuz can requested by sending:
`/oscrouter/debug/osccopy ipAddress:port` with ipAddress and listening port of the receiving machine e.g. `/oscrouter/debug/osccopy 192.168.3.2:55112`
The debug-osc messages contain the name of the target as well as ip-address and port.
To deactivate this send a message without target address: `/oscrouter/debug/osccopy`

With the message `/oscrouter/debug/verbose i` a verbosity level can be set which activates console printing of incoming and outcoming messages as well as further informations.
Set verbosity to 0 when to stop console output which can significantly slow down the system.

# Configuration

The configuration is done using a YAML-Config file, an example config file can be found in `example_configs`.
Expand All @@ -80,13 +127,12 @@ the `globalconfig`-Section of the Config contains general settings:

| Setting | Description | Default |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
| `oscr_ip` | ip address the osc-kreuz listens on | 0.0.0.0 |
| `inputport_ui` | Highest Priority listen-port intended for GUI applications | 4455 |
| `inputport_data` | lower priority port for listening to automated clients. pauses listening when data is received on `inputport_ui` | 4007 |
| `inputport_settings` | global configs can be changed on this port | 4999 |
| `ip` | ip address the osc-kreuz listens on | 127.0.0.1 |
| `port_ui` | Highest Priority listen-port intended for GUI applications | 4455 |
| `port_data` | lower priority port for listening to automated clients. pauses listening when data is received on `inputport_ui` | 4007 |
| `port_settings` | global configs can be changed on this port | 4999 |
| `number_sources` | number of source audio channels | 64 |
| `max_gain` | max gain for the audiorouter | 2 |
| `min_dist` | | 0.001 |
| `number_direct_sends` | number of direct send channels | 46 |
| `send_changes_only` | only send when source data has changed, | true |
| `data_port_timeout` | when data is received on the ui port, the data port is paused for this timeout, in seconds, set to 0 to deactivate | 2 |
Expand Down
13 changes: 6 additions & 7 deletions example_configs/config_en325.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
globalconfig:
oscr_ip: 130.149.23.33
inputport_ui: 4455 # inputport for UI-Clients, blocks inputport_data for short time
inputport_data: 4007 # inputport for automation-data clients
inputport_settings: 4999 # global configs can be changed with this port
global:
ip: 0.0.0.0
port_ui: 4455 # inputport for UI-Clients, blocks inputport_data for short time
port_data: 4007 # inputport for automation-data clients
port_settings: 4999 # global configs can be changed with this port
number_sources: 64
max_gain: 2 # max gain for the audiorouter
min_dist: 0.001
number_direct_sends: 46 # including subwoofer
send_changes_only: 1 # checks every input for changes, if set to 1 might be slower
send_changes_only: true # 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"]
room_scaling_factor: 6.046 # all incoming positon changes are scaled by this factor
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ dynamic = ["version"]
dependencies = ["numpy", "oscpy", "click", "pyYAML"]
license = { file = "LICENSE" }

[project.optional-dependencies]
tests = ['pytest', 'pyfar']

[project.scripts]
osc-kreuz = "osc_kreuz.osc_kreuz:main"

Expand Down
97 changes: 97 additions & 0 deletions src/osc_kreuz/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
from collections.abc import Callable
from importlib.resources import files
import logging
from pathlib import Path
from typing import TypeVar

import yaml

import osc_kreuz.str_keys_conventions as skc


log = logging.getLogger()

# lists for constructing default config paths
default_config_file_path = Path("osc-kreuz")
default_config_file_name_options = [
"osc-kreuz_conf.yml",
"osc-kreuz-conf.yml",
"osc-kreuz_config.yml",
"osc-kreuz-config.yml",
"config.yml",
"conf.yml",
]
default_config_file_locations = [
Path.home() / ".config",
Path("/etc"),
Path("/usr/local/etc"),
]

deprecated_config_strings = {
skc.globalconfig: ["globalconfig"],
"ip": ["oscr_ip"],
skc.inputport_ui: ["inputport_ui"],
skc.inputport_data: ["inputport_data"],
skc.inputport_settings: ["inputport_settings"],
}


def read_config(config_path) -> dict:
# get Config Path:
if config_path is None:
# check different paths for a config file, with the highest one taking precedence
for possible_config_path in (
base / default_config_file_path / filename
for base in default_config_file_locations
for filename in default_config_file_name_options
):
if possible_config_path.exists():
config_path = possible_config_path
log.info(f"Loading config file {config_path}")
break

if config_path is None:
log.warning(f"Could not find config, loading default config")
# load the default config embedded into this package using files
config_path = files("osc_kreuz").joinpath("config_default.yml")
config = yaml.load(config_path.read_bytes(), Loader=yaml.Loader)
else:
# read config file
with open(config_path) as f:
config = yaml.load(f, Loader=yaml.Loader)

return config


T = TypeVar("T")


def read_config_option(
config,
option_name: str,
option_type: Callable[..., T] | None = None,
default: T = None,
) -> T:
if option_name in config:
pass
elif option_name in deprecated_config_strings:
for deprecated_option_name in deprecated_config_strings[option_name]:
if deprecated_option_name in config:
log.warning(
f"option {deprecated_option_name} is deprecated, please use {option_name} instead"
)
option_name = deprecated_option_name
break
else:
return default

val = config[option_name]

if option_type is None:
return val

try:
return option_type(val)
except Exception:
log.error(f"Could not read config option {option_name}, invalid type")
return config[option_name]
13 changes: 6 additions & 7 deletions src/osc_kreuz/config_default.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
globalconfig:
oscr_ip: 0.0.0.0
inputport_ui: 4455 # inputport for UI-Clients, blocks inputport_data for short time
inputport_data: 4007 # inputport for automation-data clients
inputport_settings: 4999 # global configs can be changed with this port
global:
ip: 127.0.0.1
port_ui: 4455 # inputport for UI-Clients, blocks inputport_data for short time
port_data: 4007 # inputport for automation-data clients
port_settings: 4999 # global configs can be changed with this port
number_sources: 64
max_gain: 2 # max gain for the audiorouter
min_dist: 0.001
number_direct_sends: 46 # including subwoofer
send_changes_only: 1 # checks every input for changes, if set to 1 might be slower
send_changes_only: true # 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"]
room_scaling_factor: 1.0 # all incoming positon changes are scaled by this factor
Expand Down
1 change: 1 addition & 0 deletions src/osc_kreuz/conversionsTools.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Any

import numpy as np


Expand Down
51 changes: 47 additions & 4 deletions src/osc_kreuz/coordinates.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from itertools import chain, combinations

import numpy as np

import osc_kreuz.conversionsTools as conversions


Expand Down Expand Up @@ -71,8 +72,7 @@ def __init__(
self.position = {}

# initialize position dict
for key, val in zip(position_keys, initial_values):
self.position[key] = val
self.set_all(*initial_values)

def convert_to(
self, coordinate_format: CoordinateSystemType
Expand All @@ -93,6 +93,8 @@ def set_all(self, *values: float):
for key, val in zip(self.position_keys, values):
self.position[key] = val

self.validate_coordinates()

def get_all(self) -> list[float]:
"""gets all coordinates in the order they were declared in the constructor
Expand All @@ -101,6 +103,13 @@ def get_all(self) -> list[float]:
"""
return self.get_coordinates(self.position_keys)

def validate_coordinates(self):
"""overwrite this function if for some coordinates special processing is required"""

def constrain_centered_coordinate(self, val, constrain_range):
half_range = constrain_range / 2
return ((val + half_range) % constrain_range) - half_range

def set_coordinates(
self,
coordinates: CoordinateKey | list[CoordinateKey],
Expand Down Expand Up @@ -129,7 +138,6 @@ def set_coordinates(
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
Expand All @@ -141,7 +149,8 @@ def set_coordinates(
coordinate_changed = True
except KeyError:
raise CoordinateFormatException(f"Invalid Coordinate Key: {c_key}")

if coordinate_changed:
self.validate_coordinates()
return coordinate_changed

def get_coordinates(
Expand Down Expand Up @@ -171,6 +180,8 @@ def convert_to(
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)
elif coordinate_format == CoordinateSystemType.Cartesian:
return self.get_all()
else:
raise CoordinateFormatException(
f"Invalid Conversion format for Cartesion Coordinates: {coordinate_format}"
Expand All @@ -181,6 +192,20 @@ class CoordinatePolar(Coordinate):
def __init__(self, a, e, d) -> None:
super().__init__([CoordinateKey.a, CoordinateKey.e, CoordinateKey.d], [a, e, d])

def validate_coordinates(self):

# constrain elevation between -180 and 180
if not (-180 <= self.position[CoordinateKey.e] <= 180):
self.position[CoordinateKey.e] = (
(self.position[CoordinateKey.e] + 180) % 360
) - 180

# constrain azim between -180 and 180
if not (-180 <= self.position[CoordinateKey.a] <= 180):
self.position[CoordinateKey.a] = (
(self.position[CoordinateKey.a] + 180) % 360
) - 180

def convert_to(
self, coordinate_format: CoordinateSystemType
) -> list[float] | tuple[float, float, float]:
Expand All @@ -189,6 +214,8 @@ def convert_to(
elif coordinate_format == CoordinateSystemType.PolarRadians:
a, e, d = self.get_all()
return a / 180 * np.pi, e / 180 * np.pi, d
elif coordinate_format == CoordinateSystemType.Polar:
return self.get_all()
else:
raise CoordinateFormatException(
f"Invalid Conversion format for Polar Coordinates: {coordinate_format}"
Expand All @@ -199,6 +226,20 @@ class CoordinatePolarRadians(Coordinate):
def __init__(self, a, e, d) -> None:
super().__init__([CoordinateKey.a, CoordinateKey.e, CoordinateKey.d], [a, e, d])

def validate_coordinates(self):

# constrain elev between -pi and pi
if not (-np.pi <= self.position[CoordinateKey.e] <= np.pi):
self.position[CoordinateKey.e] = (
(self.position[CoordinateKey.e] + np.pi) % (2 * np.pi)
) - np.pi

# constrain azim between -pi and pi
if not (-np.pi <= self.position[CoordinateKey.a] <= np.pi):
self.position[CoordinateKey.a] = (
(self.position[CoordinateKey.a] + np.pi) % (2 * np.pi)
) - np.pi

def convert_to(
self, coordinate_format: CoordinateSystemType
) -> list[float] | tuple[float, float, float]:
Expand All @@ -207,6 +248,8 @@ def convert_to(
elif coordinate_format == CoordinateSystemType.Polar:
a, e, d = self.get_all()
return a / np.pi * 180, e / np.pi * 180, d
elif coordinate_format == CoordinateSystemType.PolarRadians:
return self.get_all()
else:
raise CoordinateFormatException(
f"Invalid Conversion format for PolarRadians Coordinates: {coordinate_format}"
Expand Down
Loading

0 comments on commit 1a35ae9

Please sign in to comment.