Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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/classes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

from .audio import (
SphericalHarmonicSignal,
SphericalHarmonicTimeData,
SphericalHarmonicFrequencyData
)

from .coordinates import (
Expand All @@ -13,5 +15,7 @@
__all__ = [
'SphericalHarmonics'
'SphericalHarmonicSignal',
'SphericalHarmonicTimeData',
'SphericalHarmonicFrequencyData',
'SamplingSphere',
]
252 changes: 197 additions & 55 deletions spharpy/classes/audio.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,27 @@
from pyfar import Signal
from pyfar import Signal, TimeData, FrequencyData
from pyfar.classes.audio import _Audio
from spharpy.spherical import renormalize, change_channel_convention
import numpy as np


class SphericalHarmonicSignal(Signal):
"""Create audio object with spherical harmonics coefficients in time or
frequency domain.
class SphericalHarmonicAudio(_Audio):
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
class SphericalHarmonicAudio(_Audio):
class _SphericalHarmonicAudio(_Audio):

shouldnt this be private, too?

"""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 time and frequency domain (equally spaced
samples and frequency bins), the channel conventions ACN and FUMA, as
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, [#]_, [#]_.


Parameters
----------
data : ndarray, double
Raw data of the spherical harmonics signal in the time or
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 samples or frequency
bins each. Time data is converted to ``float``. Frequency is
converted to ``complex`` and must be provided as single
sided spectra, i.e., for all frequencies between 0 Hz and
half the sampling rate.
sampling_rate : double
Sampling rate in Hz
basis_type : str
Type of spherical harmonic basis, either ``'complex'`` or
``'real'``.
Expand All @@ -41,24 +34,10 @@ class SphericalHarmonicSignal(Signal):
condon_shortley : bool
Flag to indicate if the Condon-Shortley phase term is included
(``True``) or not (``False``).
n_samples : int, optional
Number of time domain samples. Required if domain is ``'freq'``.
The default is ``None``, which assumes an even number of samples
if the data is provided in the frequency domain.
domain : ``'time'``, ``'freq'``, optional
Domain of data. The default is ``'time'``
fft_norm : str, optional
The normalization of the Discrete Fourier Transform (DFT). Can be
``'none'``, ``'unitary'``, ``'amplitude'``, ``'rms'``, ``'power'``,
or ``'psd'``. See :py:func:`~pyfar.dsp.fft.normalization` and [#]_
for more information. The default is ``'none'``, which is typically
used for energy signals, such as impulse responses.
comment : str
A comment related to `data`. The default is ``None``.
is_complex : bool, optional
Specifies if the underlying time domain data are complex
or real-valued. If ``True`` and `domain` is ``'time'``, the
input data will be cast to complex. The default is ``False``.

References
----------
Expand All @@ -70,32 +49,20 @@ 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):
"""
Create SphericalHarmonicSignal with data, and sampling rate.
"""
def __init__(self, basis_type, normalization, channel_convention,
condon_shortley, domain, comment=""):
Copy link
Member

Choose a reason for hiding this comment

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

domain and time are not accessed here, I assume because they are set in the audio classes from this inherits.

Suggested change
condon_shortley, domain, comment=""):
condon_shortley):

_Audio.__init__(self, domain=domain, comment=comment)
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
def __init__(self, basis_type, normalization, channel_convention,
condon_shortley, domain, comment=""):
_Audio.__init__(self, domain=domain, comment=comment)
def __init__(self, basis_type, normalization, channel_convention,
condon_shortley):

i wounder if we really need it here, because it would be set twice, by the audio class and then overwritten by this class. I think the _Audio.init is not setting anything else then this to entries, so i guess we can remove it.


# check dimensions
if len(data.shape) < 3:
if len(self._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
n_max = np.sqrt(self._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.")
f"{self._data.shape[-2]}. It must match (n_max + 1)^2.")
self._n_max = int(n_max)

# set basis_type
Expand All @@ -121,10 +88,6 @@ def __init__(
raise ValueError("Condon_shortley has to be a bool.")
self._condon_shortley = condon_shortley

Signal.__init__(self, 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."""
Expand Down Expand Up @@ -178,3 +141,182 @@ def channel_convention(self, value):
self.channel_convention,
value, axis=-2)
self._channel_convention = value


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 frequency domain, i.e., non-equidistant samples.
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
to frequency domain, i.e., non-equidistant samples.
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`` or
``float`` and is converted to ``float`` in any case.
Copy link
Member

Choose a reason for hiding this comment

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

To be consistent with FrequencyData

Suggested change
coefficients with 1024 samples each. The data can be ``int`` or
``float`` and is converted to ``float`` in any case.
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`.
Copy link
Member

Choose a reason for hiding this comment

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

This would be more descriptive, I think:

Suggested change
must match the `size` of the last dimension of `data`.
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'``, ``'maxN'`` or
``'sn3d'``. (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 ``""``.

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, domain="time", comment=comment)
Copy link
Member

Choose a reason for hiding this comment

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

see comment above:

Suggested change
condon_shortley, domain="time", comment=comment)
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'``, ``'maxN'`` or
``'sn3d'``. (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, domain="time", comment=comment)
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
SphericalHarmonicAudio.__init__(
self, basis_type, normalization, channel_convention,
condon_shortley, domain="time", 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
directly convertible between time and frequency domain (equally spaced
samples and frequency bins), the 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, [#]_, [#]_.


Parameters
----------
data : ndarray, double
Raw data of the spherical harmonics signal in the time or
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 samples or frequency
bins each. Time data is converted to ``float``. Frequency is
converted to ``complex`` and must be provided as single
sided spectra, i.e., for all frequencies between 0 Hz and
half the sampling rate.
sampling_rate : double
Sampling rate in Hz
basis_type : str
Type of spherical harmonic basis, either ``'complex'`` or
``'real'``.
normalization : str
Normalization convention, either ``'n3d'``, ``'maxN'`` or
``'sn3d'``. (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``).
n_samples : int, optional
Number of time domain samples. Required if domain is ``'freq'``.
The default is ``None``, which assumes an even number of samples
if the data is provided in the frequency domain.
domain : ``'time'``, ``'freq'``, optional
Domain of data. The default is ``'time'``
fft_norm : str, optional
The normalization of the Discrete Fourier Transform (DFT). Can be
``'none'``, ``'unitary'``, ``'amplitude'``, ``'rms'``, ``'power'``,
or ``'psd'``. See :py:func:`~pyfar.dsp.fft.normalization` and [#]_
for more information. The default is ``'none'``, which is typically
used for energy signals, such as impulse responses.
comment : str
A comment related to `data`. The default is ``None``.
is_complex : bool, optional
Specifies if the underlying time domain data are complex
or real-valued. If ``True`` and `domain` is ``'time'``, the
input data will be cast to complex. The default is ``False``.

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

"""
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):
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)
SphericalHarmonicAudio.__init__(
self, basis_type, normalization, channel_convention,
condon_shortley, domain="time", comment=comment)
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
condon_shortley, domain="time", comment=comment)
condon_shortley, domain=domain, comment=comment)

domain="time"would overwrite the given domain. But i think we can remove it here anyway

26 changes: 26 additions & 0 deletions tests/test_spherical_harmonics_audio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from pytest import raises
from spharpy.classes.audio import SphericalHarmonicAudio
from spharpy.classes.audio import SphericalHarmonicTimeData
from spharpy.classes.audio import SphericalHarmonicFrequencyData
import numpy as np
import re


def test_init_sh_time_data():
data = np.ones((1, 4, 4))
times = [1, 2, 3, 4]
sh_time_data = SphericalHarmonicTimeData(
data, times, basis_type='real', normalization='sn3d',
channel_convention="acn", condon_shortley=False,
comment="")
assert isinstance(sh_time_data, SphericalHarmonicTimeData)
Copy link
Member

Choose a reason for hiding this comment

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

This would be more strict

Suggested change
assert isinstance(sh_time_data, SphericalHarmonicTimeData)
assert type(sh_time_data) == SphericalHarmonicTimeData

(maybe needs is instead of ==)



def test_init_sh_frequency_data():
data = np.ones((1, 4, 4))
frequencies = [1, 2, 3, 4]
sh_freq_data = SphericalHarmonicFrequencyData(
data, frequencies, basis_type='real', normalization='sn3d',
channel_convention="acn", condon_shortley=False,
comment="")
assert isinstance(sh_freq_data, SphericalHarmonicFrequencyData)
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
assert isinstance(sh_freq_data, SphericalHarmonicFrequencyData)
assert type(sh_freq_data) == SphericalHarmonicFrequencyData

Loading