Skip to content
Open
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
4 changes: 4 additions & 0 deletions spharpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from .classes.sh import SphericalHarmonics
from .classes.coordinates import SamplingSphere
from .classes.audio import SphericalHarmonicSignal
from .classes.audio import SphericalHarmonicTimeData
from .classes.audio import SphericalHarmonicFrequencyData
from . import spherical
from . import samplings
from . import plot
Expand All @@ -22,6 +24,8 @@
__all__ = [
'SphericalHarmonics',
'SphericalHarmonicSignal',
'SphericalHarmonicTimeData',
'SphericalHarmonicFrequencyData',
'spherical',
'samplings',
'plot',
Expand Down
4 changes: 4 additions & 0 deletions spharpy/classes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

from .audio import (
SphericalHarmonicSignal,
SphericalHarmonicTimeData,
SphericalHarmonicFrequencyData
)

from .coordinates import (
Expand All @@ -17,5 +19,7 @@
'SphericalHarmonicDefinition',
'SphericalHarmonics',
'SphericalHarmonicSignal',
'SphericalHarmonicTimeData',
'SphericalHarmonicFrequencyData',
'SamplingSphere',
]
334 changes: 220 additions & 114 deletions spharpy/classes/audio.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,209 @@
"""Implementations of audio data container classes."""

from pyfar import Signal
from pyfar import Signal, TimeData, FrequencyData
from pyfar.classes.audio import _Audio
from spharpy.spherical import renormalize, change_channel_convention
from spharpy.classes import SphericalHarmonicDefinition
import numpy as np


class SphericalHarmonicSignal(Signal):
"""Create audio object with spherical harmonics coefficients in time or
class _SphericalHarmonicAudio(_Audio, SphericalHarmonicDefinition):
"""
Base class for spherical harmonics audio objects.

This class extends the pyfar Audio class with all methods and
properties required for spherical harmonics data and are common to the
three sub-classes :py:func:`SphericalHarmonicsTimeData`,
:py:func:`SphericalHarmonicsFrequencyData`, and
:py:func:`SphericalHarmonicsSignal`.
Comment on lines +14 to +16
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
three sub-classes :py:func:`SphericalHarmonicsTimeData`,
:py:func:`SphericalHarmonicsFrequencyData`, and
:py:func:`SphericalHarmonicsSignal`.
three sub-classes :py:func:`SphericalHarmonicTimeData`,
:py:func:`SphericalHarmonicFrequencyData`, and
:py:func:`SphericalHarmonicSignal`.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, reverted. I kind of had the feeling it was a typo cause it did't made sense to me :D ...
I agree that it makes sense to move the saving of the old parameters to the SHDefinitions class. However, I think it could be an option to do that in a separate PR. The functionality from outside would not change, and we could move on with other stuff, like SHtrafo etc.


Objects of this class contain spherical harmonics coefficients which are
directly convertible between channel conventions ACN and FUMA, as
well as the normalizations N3D, SN3D, or MaxN, see [#]_. The definition of
the spherical harmonics basis functions is based on the scipy convention
which includes the Condon-Shortley phase, [#]_, [#]_.
Comment on lines +20 to +22
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest to keep this plain and without references. We will have a large general documentation for SHs for this:

Suggested change
well as the normalizations N3D, SN3D, or MaxN, see [#]_. The definition of
the spherical harmonics basis functions is based on the scipy convention
which includes the Condon-Shortley phase, [#]_, [#]_.
well as the normalizations N3D, NM, SN3D, SNM, or MaxN.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This re-appears in the SphericalHarmonicSignal class. But I can't comment there because the lines were not changed in this pull.



Parameters
----------
basis_type : str
Type of spherical harmonic basis, either ``'complex'`` or
``'real'``.
normalization : str
Normalization convention, either ``'N3D'``, ``'NM'``,
``'maxN'``, ``'SN3D'`` or ``'SNM'``. (maxN is only supported up
to 3rd order)
channel_convention : str
Channel ordering convention, either ``'ACN'`` or ``'FuMa'``.
(FuMa is only supported up to 3rd order)
condon_shortley : bool
Flag to indicate if the Condon-Shortley phase term is included
(``True``) or not (``False``).
domain : ``'time'``, ``'freq'``, optional
Domain of data. The default is ``'time'``
comment : str
A comment related to `data`. The default is ``None``.

References
----------
.. [#] F. Zotter, M. Frank, "Ambisonics A Practical 3D Audio Theory
for Recording, Studio Production, Sound Reinforcement, and
Virtual Reality", (2019), Springer-Verlag
.. [#] B. Rafely, "Fundamentals of Spherical Array Processing", (2015),
Springer-Verlag
.. [#] E.G. Williams, "Fourier Acoustics", (1999), Academic Press
Comment on lines +47 to +52
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above. But maybe wait if anyone disagrees.

Suggested change
.. [#] F. Zotter, M. Frank, "Ambisonics A Practical 3D Audio Theory
for Recording, Studio Production, Sound Reinforcement, and
Virtual Reality", (2019), Springer-Verlag
.. [#] B. Rafely, "Fundamentals of Spherical Array Processing", (2015),
Springer-Verlag
.. [#] E.G. Williams, "Fourier Acoustics", (1999), Academic Press


"""
def __init__(self, basis_type, normalization, channel_convention,
condon_shortley):

# check dimensions
if len(self._data.shape) < 3:
raise ValueError("Invalid number of dimensions. Data should have "
"at least 3 dimensions.")

# calculate n_max
n_max = np.sqrt(self._data.shape[-2])-1
if n_max - int(n_max) != 0:
raise ValueError("Invalid number of SH channels: "
f"{self._data.shape[-2]}. It must match "
"(n_max + 1)^2.")
n_max = int(n_max)

# helpers for setting normalization and channel convention
self._current_normalization = None
self._current_channel_convention = None

SphericalHarmonicDefinition.__init__(
self,
n_max,
basis_type,
channel_convention,
normalization,
condon_shortley,
)

def _on_property_change(self):
# check if normalization has changed and recompute accordingly
if (self._current_normalization is not None and
self._current_normalization != self.normalization):
self._data = renormalize(
self._data,
self.channel_convention,
self._current_normalization,
self.normalization,
axis=-2)
self._current_normalization = self.normalization

# check if channel convention has changed and recompute accordingly
if (self._current_channel_convention is not None and
self._current_channel_convention != self.channel_convention):
self._data = change_channel_convention(
self._data,
self._current_channel_convention,
self.channel_convention,
axis=-2)
self._current_channel_convention = self.channel_convention


class SphericalHarmonicTimeData(_SphericalHarmonicAudio, TimeData):
"""
Create spherical harmonic audio object with time domain spherical
harmonic coefficients and times.

Objects of this class contain time data which is not directly convertible
to the frequency domain, i.e., non-equidistant samples.

Parameters
----------
data : array, double
Raw data in the time domain. The data should have at least 3
dimensions, according to the 'C' memory layout, e.g. data of
``shape = (1, 4, 1024)`` has 1 channel with 4 spherical harmonic
coefficients with 1024 samples each. The data can be ``int``,
``float`` or ``complex``. Data of type ``int`` is converted to
``float``.
times : array, double
Times in seconds at which the data is sampled. The number of times
must match the size of the last dimension of `data`, i.e., ``data.shape[-1]``.
basis_type : str
Type of spherical harmonic basis, either ``'complex'`` or
``'real'``.
normalization : str
Normalization convention, either ``'N3D'``, ``'NM'``,
``'maxN'``, ``'SN3D'`` or ``'SNM'``. (maxN is only supported up
to 3rd order)
channel_convention : str
Channel ordering convention, either ``'ACN'`` or ``'FuMa'``.
(FuMa is only supported up to 3rd order)
condon_shortley : bool
Flag to indicate if the Condon-Shortley phase term is included
(``True``) or not (``False``).
comment : str
A comment related to `data`. The default is ``None``.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
A comment related to `data`. The default is ``None``.
A comment related to `data`. The default is ``""``.

is_complex : bool, optional
A flag which indicates if the time data are real or complex-valued.
The default is ``False``.
"""
def __init__(self, data, times, basis_type, normalization,
channel_convention, condon_shortley, comment="",
is_complex=False):

TimeData.__init__(self, data=data, times=times, comment=comment,
is_complex=is_complex)
_SphericalHarmonicAudio.__init__(
self, basis_type, normalization, channel_convention,
condon_shortley)


class SphericalHarmonicFrequencyData(_SphericalHarmonicAudio, FrequencyData):
"""
Create spherical harmonic audio object with frequency domain spherical
harmonic coefficients and frequencies.

Objects of this class contain frequency data which is not directly
convertible to the time domain, i.e., non-equidistantly spaced bins or
incomplete spectra.

Parameters
----------
data : array, double
Raw data in the frequency domain. The data should have at least
3 dimensions, according to the 'C' memory layout, e.g. data of
``shape = (1, 4, 1024)`` has 1 channel with 4 spherical harmonic
coefficients with 1024 frequency bins each.. Data can be ``int``,
``float`` or ``complex``. Data of type ``int`` is converted to
``float``.
frequencies : array, double
Frequencies of the data in Hz. The number of frequencies must match
the size of the last dimension of data.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
the size of the last dimension of data.
the size of the last dimension of `data`, i.e., ``data.shape[-1]``.

basis_type : str
Type of spherical harmonic basis, either ``'complex'`` or
``'real'``.
normalization : str
Normalization convention, either ``'N3D'``, ``'NM'``,
``'maxN'``, ``'SN3D'`` or ``'SNM'``. (maxN is only supported up
to 3rd order)
channel_convention : str
Channel ordering convention, either ``'acn'`` or ``'fuma'``.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Channel ordering convention, either ``'acn'`` or ``'fuma'``.
Channel ordering convention, either ``'ACN'`` or ``'FuMa'``.

(FuMa is only supported up to 3rd order)
condon_shortley : bool
Flag to indicate if the Condon-Shortley phase term is included
(``True``) or not (``False``).
comment : str
A comment related to `data`. The default is ``None``.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
A comment related to `data`. The default is ``None``.
A comment related to `data`. The default is ``""``.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also appears in the SphericalHarmonicSignal Class where I can't comment.

"""

def __init__(self, data, frequencies, basis_type, normalization,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to have default values for the SH parameters as in the SH class, or are there not defaults by intention to make sure people think about this when inputting data from sources other than our internal SHT functions?

channel_convention, condon_shortley, comment=""):
FrequencyData.__init__(self, data=data, frequencies=frequencies,
comment=comment)
_SphericalHarmonicAudio.__init__(
self, basis_type, normalization, channel_convention,
condon_shortley)


class SphericalHarmonicSignal(_SphericalHarmonicAudio, Signal):
"""
Create audio object with spherical harmonics coefficients in time or
frequency domain.

Objects of this class contain spherical harmonics coefficients which are
Expand Down Expand Up @@ -36,8 +233,8 @@ class SphericalHarmonicSignal(Signal):
``'real'``.
normalization : str
Normalization convention, either ``'N3D'``, ``'NM'``,
``'maxN'``, ``'SN3D'``, or ``'SNM'``.
(maxN is only supported up to 3rd order)
``'maxN'``, ``'SN3D'`` or ``'SNM'``. (maxN is only supported up
to 3rd order)
channel_convention : str
Channel ordering convention, either ``'ACN'`` or ``'FuMa'``.
(FuMa is only supported up to 3rd order)
Expand Down Expand Up @@ -73,113 +270,22 @@ class SphericalHarmonicSignal(Signal):
.. [#] E.G. Williams, "Fourier Acoustics", (1999), Academic Press

"""
def __init__(self,
data,
sampling_rate,
basis_type,
normalization,
channel_convention,
condon_shortley,
n_samples=None,
domain='time',
fft_norm='none',
comment="",
is_complex=False):

def __init__(
self,
data,
sampling_rate,
basis_type,
normalization,
channel_convention,
condon_shortley,
n_samples=None,
domain='time',
fft_norm='none',
comment="",
is_complex=False):
"""
Create SphericalHarmonicSignal with data, and sampling rate.
"""
# check dimensions
if len(data.shape) < 3:
raise ValueError("Invalid number of dimensions. Data should have "
"at least 3 dimensions.")

# set n_max
n_max = np.sqrt(data.shape[-2])-1
if n_max - int(n_max) != 0:
raise ValueError("Invalid number of SH channels: "
f"{data.shape[-2]}. It must match (n_max + 1)^2.")
self._n_max = int(n_max)

# set basis_type
if basis_type not in ["complex", "real"]:
raise ValueError("Invalid basis type, only "
"'complex' and 'real' are supported")
self._basis_type = basis_type

# set normalization
if normalization not in ["N3D", "NM", "maxN", "SN3D", "SNM"]:
raise ValueError("Invalid normalization, has to be 'N3D', 'NM', "
"'maxN', 'SN3D', or 'SNM', "
f"but is {normalization}")
self._normalization = normalization

# set channel_convention
if channel_convention not in ["ACN", "FuMa"]:
raise ValueError("Invalid channel convention, has to be 'ACN' "
f"or 'FuMa', but is {channel_convention}")
self._channel_convention = channel_convention

# set Condon Shortley
if not isinstance(condon_shortley, bool):
raise ValueError("Condon_shortley has to be a bool.")
self._condon_shortley = condon_shortley

Signal.__init__(self, data, sampling_rate=sampling_rate,
Signal.__init__(self, data=data, sampling_rate=sampling_rate,
n_samples=n_samples, domain=domain, fft_norm=fft_norm,
comment=comment, is_complex=is_complex)

@property
def n_max(self):
"""Get the maximum spherical harmonic order."""
return self._n_max

@property
def basis_type(self):
"""Get the type of the spherical harmonic basis."""
return self._basis_type

@property
def normalization(self):
"""
Get or set and apply the normalization of the spherical harmonic
coefficients.
"""
return self._normalization

@normalization.setter
def normalization(self, value):
"""
Get or set and apply the normalization of the spherical harmonic
coefficients.
"""
if self.normalization is not value:
self._data = renormalize(self._data, self.channel_convention,
self.normalization, value, axis=-2)
self._normalization = value

@property
def condon_shortley(self):
"""Get info whether to include the Condon-Shortley phase term."""
return self._condon_shortley

@property
def channel_convention(self):
"""
Get or set and apply the channel convention of the spherical harmonic
coefficients.
"""
return self._channel_convention

@channel_convention.setter
def channel_convention(self, value):
"""
Get or set and apply the channel convention of the spherical harmonic
coefficients.
"""
if self.channel_convention is not value:
self._data = change_channel_convention(self._data,
self.channel_convention,
value, axis=-2)
self._channel_convention = value
_SphericalHarmonicAudio.__init__(
self, basis_type, normalization, channel_convention,
condon_shortley)
Loading