from attr import attrs, attrib, evolve, Factory from attr.validators import instance_of, optional from ....common import ( PolarPositionMixin, CartesianPositionMixin, PolarPosition, CartesianPosition, cart, finite_float, validate_range, ) try: # moved in py3.3 from collections.abc import Mapping except ImportError: from collections import Mapping def convert_object_position(value): if isinstance(value, (ObjectPolarPosition, ObjectCartesianPosition)): return value elif isinstance(value, PolarPosition): return ObjectPolarPosition.from_PolarPosition(value) elif isinstance(value, CartesianPosition): return ObjectCartesianPosition.from_CartesianPosition(value) elif isinstance(value, Mapping): if 'azimuth' in value: return ObjectPolarPosition(**value) else: return ObjectCartesianPosition(**value) else: raise TypeError("cannot convert {value!r} to ObjectPolarPosition or ObjectCartesianPosition".format(value=value)) @attrs(slots=True) class ScreenEdgeLock(object): """ADM screenEdgeLock information from position elements Attributes: horizontal (Optional[str]): screenEdgeLock from azimuth or X coordinates; must be ``left`` or ``right``. vertical (Optional[str]): screenEdgeLock from elevation or Z coordinates; must be ``top`` or ``bottom``. """ horizontal = attrib(default=None) vertical = attrib(default=None) class ObjectPosition(object): """Base for classes representing data contained in `audioBlockFormat` `position` elements for Objects. See Also: :class:`ObjectPolarPosition` and :class:`ObjectCartesianPosition` """ __slots__ = () @attrs(slots=True) class ObjectPolarPosition(ObjectPosition, PolarPositionMixin): """Represents data contained in `audioBlockFormat` `position` elements for Objects where polar coordinates are used. Attributes are formatted according to the ADM coordinate convention. Attributes: azimuth (float): anti-clockwise azimuth in degrees, measured from the front elevation (float): elevation in degrees, measured upwards from the equator distance (float): distance relative to the audioPackFormat absoluteDistance parameter screenEdgeLock (ScreenEdgeLock) """ azimuth = attrib(converter=float, validator=validate_range(-180, 180)) elevation = attrib(converter=float, validator=validate_range(-90, 90)) distance = attrib(converter=float, validator=validate_range(0, float('inf')), default=1.0) screenEdgeLock = attrib(default=Factory(ScreenEdgeLock), validator=instance_of(ScreenEdgeLock)) @classmethod def from_PolarPosition(cls, position): return cls(azimuth=position.azimuth, elevation=position.elevation, distance=position.distance) @attrs(slots=True) class ObjectCartesianPosition(ObjectPosition, CartesianPositionMixin): """Represents data contained in `audioBlockFormat` `position` elements for Objects where Cartesian coordinates are used. Attributes are formatted according to the ADM coordinate convention. Attributes: X (float): left-to-right position, from -1 to 1 Y (float): back-to-front position, from -1 to 1 Z (float): bottom-to-top position, from -1 to 1 screenEdgeLock (ScreenEdgeLock) """ X = attrib(converter=float) Y = attrib(converter=float) Z = attrib(converter=float) screenEdgeLock = attrib(default=Factory(ScreenEdgeLock), validator=instance_of(ScreenEdgeLock)) @classmethod def from_CartesianPosition(cls, position): return cls(X=position.X, Y=position.Y, Z=position.Z) @attrs(slots=True) class BoundCoordinate(object): """ADM position coordinate for DirectSpeakers This represents multiple position elements with the same coordinate, so for azimuth this translates to: .. code-block:: xml {value} {min} {max} Attributes are formatted according to the ADM coordinate convention. Attributes: value (float): value for unbounded position element min (Optional[float]): value for position element with ``bound="min"`` max (Optional[float]): value for position element with ``bound="max"`` """ value = attrib(validator=finite_float()) min = attrib(validator=optional(finite_float()), default=None) max = attrib(validator=optional(finite_float()), default=None) class DirectSpeakerPosition(object): """Base for classes representing data contained in `audioBlockFormat` `position` elements for DirectSpeakers. See Also: :class:`DirectSpeakerPolarPosition` and :class:`DirectSpeakerCartesianPosition` """ __slots__ = () @attrs(slots=True) class DirectSpeakerPolarPosition(DirectSpeakerPosition, PolarPositionMixin): """Represents data contained in `audioBlockFormat` `position` elements for DirectSpeakers where polar coordinates are used. Attributes are formatted according to the ADM coordinate convention. Attributes: bounded_azimuth (BoundCoordinate): data for position elements with ``coordinate="azimuth"`` bounded_elevation (BoundCoordinate): data for position elements with ``coordinate="elevation"`` bounded_distance (BoundCoordinate): data for position elements with ``coordinate="distance"`` screenEdgeLock (ScreenEdgeLock) """ bounded_azimuth = attrib(validator=instance_of(BoundCoordinate)) bounded_elevation = attrib(validator=instance_of(BoundCoordinate)) bounded_distance = attrib(validator=instance_of(BoundCoordinate), default=Factory(lambda: BoundCoordinate(1.))) screenEdgeLock = attrib(default=Factory(ScreenEdgeLock), validator=instance_of(ScreenEdgeLock)) @property def azimuth(self): """float: anti-clockwise azimuth in degrees, measured from the front""" return self.bounded_azimuth.value @property def elevation(self): """float: elevation in degrees, measured upwards from the equator""" return self.bounded_elevation.value @property def distance(self): """float: distance relative to the audioPackFormat absoluteDistance parameter""" return self.bounded_distance.value def as_cartesian_array(self): return cart(self.azimuth, self.elevation, self.distance) @attrs(slots=True) class DirectSpeakerCartesianPosition(DirectSpeakerPosition, CartesianPositionMixin): """Represents data contained in `audioBlockFormat` `position` elements for DirectSpeakers where Cartesian coordinates are used. Attributes: bounded_X (BoundCoordinate): data for position elements with ``coordinate="X"`` bounded_Y (BoundCoordinate): data for position elements with ``coordinate="Y"`` bounded_Z (BoundCoordinate): data for position elements with ``coordinate="Z"`` screenEdgeLock (ScreenEdgeLock) """ bounded_X = attrib(validator=instance_of(BoundCoordinate)) bounded_Y = attrib(validator=instance_of(BoundCoordinate)) bounded_Z = attrib(validator=instance_of(BoundCoordinate)) screenEdgeLock = attrib(default=Factory(ScreenEdgeLock), validator=instance_of(ScreenEdgeLock)) @property def X(self): """float: left-to-right position, from -1 to 1""" return self.bounded_X.value @property def Y(self): """float: back-to-front position, from -1 to 1""" return self.bounded_Y.value @property def Z(self): """float: bottom-to-top position, from -1 to 1""" return self.bounded_Z.value class PositionOffset: """representation of positionOffset elements in audioObject or alternativeValueSet""" __slots__ = () @attrs(slots=True) class PolarPositionOffset(PositionOffset): """representation of a polar positionOffset""" azimuth = attrib(default=0.0, validator=finite_float()) elevation = attrib(default=0.0, validator=finite_float()) distance = attrib(default=0.0, validator=finite_float()) def apply(self, pos): if not isinstance(pos, ObjectPolarPosition): raise ValueError( "can only apply a polar position offset to a polar position" ) return evolve( pos, azimuth=pos.azimuth + self.azimuth, elevation=pos.elevation + self.elevation, distance=pos.distance + self.distance, ) @attrs(slots=True) class CartesianPositionOffset(PositionOffset): """representation of a cartesian positionOffset""" X = attrib(default=0.0, validator=finite_float()) Y = attrib(default=0.0, validator=finite_float()) Z = attrib(default=0.0, validator=finite_float()) def apply(self, pos): if not isinstance(pos, ObjectCartesianPosition): raise ValueError( "can only apply a cartesian position offset to a cartesian position" ) return evolve( pos, X=pos.X + self.X, Y=pos.Y + self.Y, Z=pos.Z + self.Z, ) @attrs(slots=True) class InteractionRange(object): """a minimum and maximum bound for a single number Attributes: min (Optional[float]): lower bound max (Optional[float]): upper bound """ min = attrib(validator=optional(finite_float()), default=None) max = attrib(validator=optional(finite_float()), default=None) class PositionInteractionRange: """representation of a set of positionInteractionRange elements, for either Cartesian or polar coordinates """ __slots__ = () @attrs(slots=True) class PolarPositionInteractionRange(PositionInteractionRange): """polar positionInteractionRange elements Attributes: azimuth (InteractionRange): upper and lower bound for azimuth elevation (InteractionRange): upper and lower bound for elevation distance (InteractionRange): upper and lower bound for distance """ azimuth = attrib( validator=instance_of(InteractionRange), default=Factory(InteractionRange) ) elevation = attrib( validator=instance_of(InteractionRange), default=Factory(InteractionRange) ) distance = attrib( validator=instance_of(InteractionRange), default=Factory(InteractionRange) ) @attrs(slots=True) class CartesianPositionInteractionRange(PositionInteractionRange): """Cartesian positionInteractionRange elements Attributes: X (InteractionRange): upper and lower bound for X Y (InteractionRange): upper and lower bound for Y Z (InteractionRange): upper and lower bound for Z """ X = attrib( validator=instance_of(InteractionRange), default=Factory(InteractionRange) ) Y = attrib( validator=instance_of(InteractionRange), default=Factory(InteractionRange) ) Z = attrib( validator=instance_of(InteractionRange), default=Factory(InteractionRange) )