-
Notifications
You must be signed in to change notification settings - Fork 4
Add SphericalHarmonicsAudio classes #166
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
6c64125
700bcb9
67b163f
d4e0556
90d05ba
40a973b
de6eaaa
490dfa8
3424e41
40785fa
ef86b74
d991c26
a1bf902
b3be88d
a49f4cd
dfd4d54
6666f3a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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`. | ||||||||||||||
|
|
||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See above. But maybe wait if anyone disagrees.
Suggested change
|
||||||||||||||
|
|
||||||||||||||
| """ | ||||||||||||||
| 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``. | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||
| 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. | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||
| 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'``. | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||
| (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``. | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||||||||||
|
|
@@ -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) | ||||||||||||||
|
|
@@ -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) | ||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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.