From 5394a947e8c3062b5f4c6cf0d8530e3334169f2e Mon Sep 17 00:00:00 2001 From: aboutheo Date: Tue, 14 Jan 2025 15:23:30 +0100 Subject: [PATCH 1/9] feat: Added tonality ECMA418_2 --- doc/source/api/psychoacoustics.rst | 1 + .../sound/core/psychoacoustics/__init__.py | 2 + .../psychoacoustics/tonality_ecma_418_2.py | 224 +++++++++++++++++ ...est_psychoacoustics_tonality_ecma_418_2.py | 235 ++++++++++++++++++ 4 files changed, 462 insertions(+) create mode 100644 src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py create mode 100644 tests/tests_psychoacoustics/test_psychoacoustics_tonality_ecma_418_2.py diff --git a/doc/source/api/psychoacoustics.rst b/doc/source/api/psychoacoustics.rst index 0530b6e45..1430bcb2e 100644 --- a/doc/source/api/psychoacoustics.rst +++ b/doc/source/api/psychoacoustics.rst @@ -15,3 +15,4 @@ Psychoacoustics ProminenceRatio ToneToNoiseRatio TonalityDIN45681 + TonalityECMA418_2 diff --git a/src/ansys/sound/core/psychoacoustics/__init__.py b/src/ansys/sound/core/psychoacoustics/__init__.py index d44dbc7f6..8e79390b0 100644 --- a/src/ansys/sound/core/psychoacoustics/__init__.py +++ b/src/ansys/sound/core/psychoacoustics/__init__.py @@ -34,6 +34,7 @@ from .sharpness import Sharpness from .spectral_centroid import SpectralCentroid from .tonality_din_45681 import TonalityDIN45681 +from .tonality_ecma_418_2 import TonalityECMA418_2 from .tone_to_noise_ratio import ToneToNoiseRatio __all__ = ( @@ -47,4 +48,5 @@ "FluctuationStrength", "TonalityDIN45681", "SpectralCentroid", + "TonalityECMA418_2", ) diff --git a/src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py b/src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py new file mode 100644 index 000000000..f16620770 --- /dev/null +++ b/src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py @@ -0,0 +1,224 @@ +# Copyright (C) 2023 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Computes ECMA 418-2 tonality.""" +import warnings + +from ansys.dpf.core import Field, Operator, types +import matplotlib.pyplot as plt +import numpy as np + +from . import PsychoacousticsParent +from .._pyansys_sound import PyAnsysSoundException, PyAnsysSoundWarning + +ID_COMPUTE_TONALITY_ECMA_418_2 = "compute_tonality_ecma418_2" + + +class TonalityECMA418_2(PsychoacousticsParent): + """Computes ECMA 418-2 tonality. + + This class is used to compute the tonality according to the ECMA 418-2 standard (Hearing Model + of Sottek). The standard is also know as ECMA74_G. + """ + + def __init__(self, signal: Field = None): + """Create a ``TonalityECMA418_2`` object. + + Parameters + ---------- + signal: Field, default: None + Signal in Pa on which to calculate the tonality, as a DPF field. + + For more information about the parameters, please refer to Ansys Sound SAS' user guide. + """ + super().__init__() + self.signal = signal + self.__operator = Operator(ID_COMPUTE_TONALITY_ECMA_418_2) + + def __str__(self): + """Return the string representation of the object.""" + return ( + f"{__class__.__name__} object.\n" + "Data\n" + f'Signal name: "{self.signal.name}"\n' + f"Tonality: {self.get_tonality()} tuHMS\n" + ) + + @property + def signal(self) -> Field: + """Signal in Pa, as a DPF field. Default is None.""" + return self.__signal + + @signal.setter + def signal(self, signal: Field): + """Set signal.""" + if not (isinstance(signal, Field) or signal is None): + raise PyAnsysSoundException("Signal must be specified as a DPF field.") + self.__signal = signal + + def process(self): + """Compute the ECMA 418-2 tonality. + + This method calls the appropriate DPF Sound operator. + """ + if self.signal == None: + raise PyAnsysSoundException( + f"No input signal defined. Use ``{__class__.__name__}.signal``." + ) + + # Connect the operator input(s). + self.__operator.connect(0, self.signal) + + # Run the operator. + self.__operator.run() + + # Store the operator outputs in a tuple. + self._output = ( + self.__operator.get_output(0, types.double), + self.__operator.get_output(1, types.field), + self.__operator.get_output(2, types.field), + ) + + def get_output(self) -> tuple[float, Field, Field, Field]: + """Get the ECMA 418-2 tonality data, in a tuple containing data of various types. + + Returns + ------- + tuple + First element (float) is the ECMA 418-2 tonality, in tuHMS. + + Second element (Field) is the ECMA 418-2 tonality over time, in tuHMS. + + Third element (Field) is the ECMA 418-2 tone frequency over time, in Hz. + """ + if self._output == None: + warnings.warn( + PyAnsysSoundWarning( + f"Output is not processed yet. " + f"Use the ``{__class__.__name__}.process()`` method." + ) + ) + + return self._output + + def get_output_as_nparray(self) -> tuple[float, np.ndarray, np.ndarray]: + """Get the ECMA 418-2 tonality data, in a tuple of NumPy arrays. + + Returns + ------- + tuple[numpy.ndarray] + First element is the ECMA 418-2 tonality, in tuHMS. + + Second element is the ECMA 418-2 tonality over time, in tuHMS. + + Third element is the ECMA 418-2 tone frequency over time, in Hz. + + Fourth element is the associated time scale, in s. + """ + output = self.get_output() + + if output == None: + return ( + np.nan, + np.array([]), + np.array([]), + np.array([]), + ) + + return ( + np.array(output[0]), + np.array(output[1].data), + np.array(output[2].data), + np.array(output[1].time_freq_support.time_frequencies.data), + ) + + def get_tonality(self) -> float: + """Get the ECMA 418-2 tonality, in tuHMS. + + Returns + ------- + float + ECMA 418-2 tonality, in tuHMS. + """ + return self.get_output_as_nparray()[0] + + def get_tonality_over_time(self) -> np.ndarray: + """Get the ECMA 418-2 tonality over time, in tuHMS. + + Returns + ------- + numpy.ndarray + ECMA 418-2 tonality over time, in tuHMS. + """ + return self.get_output_as_nparray()[1] + + def get_tone_frequency_over_time(self) -> np.ndarray: + """Get the ECMA 418-2 tone frequency over time, in Hz. + + Returns + ------- + numpy.ndarray + ECMA 418-2 tone frequency over time, in Hz. + """ + return self.get_output_as_nparray()[2] + + def get_time_scale(self) -> np.ndarray: + """Get the ECMA 418-2 time scale, in s. + + Returns + ------- + numpy.ndarray + Array of the computation times, in seconds, of the ECMA 418-2 parameters over time + (tonality and tonal frequencies). + """ + return self.get_output_as_nparray()[3] + + def plot(self): + """Plot the ECMA 418-2's toanlity and tone frequency over time. + + This method creates a figure window that displays the tonality in dB + and the tone frequencies in Hz over time. + """ + if self._output == None: + raise PyAnsysSoundException( + f"Output is not processed yet. Use the ``{__class__.__name__}.process()`` method." + ) + + # Get data to plot + tonality_over_time = self.get_tonality_over_time() + ft_over_time = self.get_tone_frequency_over_time() + time_scale = self.get_time_scale() + + # Plot DIN 45681 parameters over time. + _, axes = plt.subplots(2, 1, sharex=True) + axes[0].plot(time_scale, tonality_over_time) + axes[0].set_title("ECMA418-2 psychoacoustic tonality") + axes[0].set_ylabel(r"T $\mathregular{tu_HMS}$") + axes[0].grid(True) + + axes[1].plot(time_scale, ft_over_time) + axes[1].set_title("DIN45681 decisive frequency") + axes[1].set_ylabel(r"$\mathregular{f_ton}$ (Hz)") + axes[1].grid(True) + + plt.tight_layout() + plt.show() diff --git a/tests/tests_psychoacoustics/test_psychoacoustics_tonality_ecma_418_2.py b/tests/tests_psychoacoustics/test_psychoacoustics_tonality_ecma_418_2.py new file mode 100644 index 000000000..9b7aa3425 --- /dev/null +++ b/tests/tests_psychoacoustics/test_psychoacoustics_tonality_ecma_418_2.py @@ -0,0 +1,235 @@ +# Copyright (C) 2023 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from unittest.mock import patch + +from ansys.dpf.core import Field +import numpy as np +import pytest + +from ansys.sound.core._pyansys_sound import PyAnsysSoundException, PyAnsysSoundWarning +from ansys.sound.core.psychoacoustics import TonalityECMA418_2 +from ansys.sound.core.signal_utilities import LoadWav + +EXP_TONALITY = 4.9181241989135742 +EXP_TONALITY_OVER_TIME = 5.7180857658386230 +EXP_FT_OVER_TIME = 795.41015625000000 +EXP_TIME = 0.0138125 +EXP_STR = ( + "TonalityECMA418_2 object.\n" + + "Data\n" + + f'Signal name: "flute"\n' + + f"Tonality: 4.918124198913574 tuHMS\n" +) + + +def test_tonality_ecma_418_2_instantiation(): + """Test TonalityECMA418_2 instantiation.""" + tonality = TonalityECMA418_2() + assert tonality.signal == None + + +def test_tonality_ecma_418_2_properties(): + """Test TonalityECMA418_2 properties.""" + tonality = TonalityECMA418_2() + tonality.signal = Field() + assert type(tonality.signal) == Field + + +def test_tonality_ecma_418_2___str__(): + """Test __str__ method.""" + wav_loader = LoadWav(pytest.data_path_flute_nonUnitaryCalib_in_container) + wav_loader.process() + fc = wav_loader.get_output() + + tonality = TonalityECMA418_2(signal=fc[0]) + tonality.process() + + assert tonality.__str__() == EXP_STR + + +def test_tonality_ecma_418_2_setters_exceptions(): + """Test TonalityECMA418_2 setters' exceptions.""" + tonality = TonalityECMA418_2() + with pytest.raises( + PyAnsysSoundException, + match="Signal must be specified as a DPF field.", + ): + tonality.signal = "Invalid" + + +def test_tonality_ecma_418_2_process(): + """Test process method.""" + wav_loader = LoadWav(pytest.data_path_flute_nonUnitaryCalib_in_container) + wav_loader.process() + fc = wav_loader.get_output() + + tonality = TonalityECMA418_2(signal=fc[0]) + tonality.process() + + +def test_tonality_ecma_418_2_process_exception(): + """Test process method's exception.""" + tonality = TonalityECMA418_2() + + with pytest.raises( + PyAnsysSoundException, + match="No input signal defined. Use ``TonalityECMA418_2.signal``.", + ): + tonality.process() + + +def test_tonality_ecma_418_2_get_output(): + """Test get_output method.""" + wav_loader = LoadWav(pytest.data_path_flute_nonUnitaryCalib_in_container) + wav_loader.process() + fc = wav_loader.get_output() + + tonality = TonalityECMA418_2(signal=fc[0]) + tonality.process() + + output = tonality.get_output() + assert output is not None + + +def test_tonality_ecma_418_2_get_output_unprocessed(): + """Test get_output method's warning.""" + tonality = TonalityECMA418_2() + + with pytest.warns( + PyAnsysSoundWarning, + match="Output is not processed yet. Use the ``TonalityECMA418_2.process\\(\\)`` method.", + ): + output = tonality.get_output() + assert output is None + + +def test_tonality_ecma_418_2_get_output_as_nparray(): + """Test get_output_as_nparray method.""" + wav_loader = LoadWav(pytest.data_path_flute_nonUnitaryCalib_in_container) + wav_loader.process() + fc = wav_loader.get_output() + + tonality = TonalityECMA418_2(signal=fc[0]) + tonality.process() + + tonality_float, tonality_over_time, ft_over_time, time = tonality.get_output_as_nparray() + + assert tonality_float == pytest.approx(EXP_TONALITY) + assert tonality_over_time[225] == pytest.approx(EXP_TONALITY_OVER_TIME) + assert ft_over_time[225] == pytest.approx(EXP_FT_OVER_TIME) + assert time[-1] == pytest.approx(EXP_TIME) + + +def test_tonality_ecma_418_2_get_output_as_nparray_unprocessed(): + """Test get_output_as_nparray method's warning.""" + tonality = TonalityECMA418_2() + + with pytest.warns( + PyAnsysSoundWarning, + match="Output is not processed yet. Use the ``TonalityECMA418_2.process\\(\\)`` method.", + ): + tonality_float, tonality_over_time, ft_over_time, time = tonality.get_output_as_nparray() + assert np.isnan(tonality_float) + assert len(tonality_over_time) == 0 + assert len(ft_over_time) == 0 + assert len(time) == 0 + + +def test_tonality_ecma_418_2_get_tonality(): + """Test get_tonality method.""" + wav_loader = LoadWav(pytest.data_path_flute_nonUnitaryCalib_in_container) + wav_loader.process() + fc = wav_loader.get_output() + + tonality = TonalityECMA418_2(signal=fc[0]) + tonality.process() + + tonality_float = tonality.get_tonality() + assert tonality_float == pytest.approx(EXP_TONALITY) + + +def test_tonality_ecma_418_2_get_tonality_over_time(): + """Test get_tonality_over_time method.""" + wav_loader = LoadWav(pytest.data_path_flute_nonUnitaryCalib_in_container) + wav_loader.process() + fc = wav_loader.get_output() + + tonality = TonalityECMA418_2(signal=fc[0]) + tonality.process() + + tonality_over_time = tonality.get_tonality_over_time() + assert tonality_over_time[225] == pytest.approx(EXP_TONALITY_OVER_TIME) + + +def test_tonality_ecma_418_2_get_tone_frequency_over_time(): + """Test get_tone_frequency_over_time method.""" + wav_loader = LoadWav(pytest.data_path_flute_nonUnitaryCalib_in_container) + wav_loader.process() + fc = wav_loader.get_output() + + tonality = TonalityECMA418_2(signal=fc[0]) + tonality.process() + + ft_over_time = tonality.get_tone_frequency_over_time() + assert ft_over_time[225] == pytest.approx(EXP_FT_OVER_TIME) + + +def test_tonality_ecma_418_2_get_time_scale(): + """Test get_time_scale method.""" + wav_loader = LoadWav(pytest.data_path_flute_nonUnitaryCalib_in_container) + wav_loader.process() + fc = wav_loader.get_output() + + tonality = TonalityECMA418_2(signal=fc[0]) + tonality.process() + + time = tonality.get_time_scale() + assert time[-1] == pytest.approx(EXP_TIME) + + +@patch("matplotlib.pyplot.show") +def test_tonality_ecma_418_2_plot(mock_show): + """Test plot method.""" + wav_loader = LoadWav(pytest.data_path_flute_nonUnitaryCalib_in_container) + wav_loader.process() + fc = wav_loader.get_output() + + tonality = TonalityECMA418_2(signal=fc[0]) + tonality.process() + + tonality.plot() + + +def test_tonality_ecma_418_2_plot_exception(): + """Test plot method's exception.""" + wav_loader = LoadWav(pytest.data_path_flute_nonUnitaryCalib_in_container) + wav_loader.process() + fc = wav_loader.get_output() + + tonality = TonalityECMA418_2(signal=fc[0]) + + with pytest.raises( + PyAnsysSoundException, + match="Output is not processed yet. Use the ``TonalityECMA418_2.process\\(\\)`` method.", + ): + tonality.plot() From 53643e170e63412ac530997b20c1208e6fb0c4ad Mon Sep 17 00:00:00 2001 From: aboutheo Date: Wed, 15 Jan 2025 16:47:13 +0100 Subject: [PATCH 2/9] feat: minor modifs in tonality ecma 418_2 --- .../sound/core/psychoacoustics/tonality_ecma_418_2.py | 7 ++++--- .../test_psychoacoustics_tonality_ecma_418_2.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py b/src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py index f16620770..474fe06bd 100644 --- a/src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py +++ b/src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py @@ -206,16 +206,17 @@ def plot(self): # Get data to plot tonality_over_time = self.get_tonality_over_time() ft_over_time = self.get_tone_frequency_over_time() - time_scale = self.get_time_scale() + time_scale_tonality = self.get_time_scale() + time_scale_ft = self.get_output()[2].time_freq_support.time_frequencies.data # Plot DIN 45681 parameters over time. _, axes = plt.subplots(2, 1, sharex=True) - axes[0].plot(time_scale, tonality_over_time) + axes[0].plot(time_scale_tonality, tonality_over_time) axes[0].set_title("ECMA418-2 psychoacoustic tonality") axes[0].set_ylabel(r"T $\mathregular{tu_HMS}$") axes[0].grid(True) - axes[1].plot(time_scale, ft_over_time) + axes[1].plot(time_scale_ft, ft_over_time) axes[1].set_title("DIN45681 decisive frequency") axes[1].set_ylabel(r"$\mathregular{f_ton}$ (Hz)") axes[1].grid(True) diff --git a/tests/tests_psychoacoustics/test_psychoacoustics_tonality_ecma_418_2.py b/tests/tests_psychoacoustics/test_psychoacoustics_tonality_ecma_418_2.py index 9b7aa3425..e0028d0f7 100644 --- a/tests/tests_psychoacoustics/test_psychoacoustics_tonality_ecma_418_2.py +++ b/tests/tests_psychoacoustics/test_psychoacoustics_tonality_ecma_418_2.py @@ -33,7 +33,7 @@ EXP_TONALITY = 4.9181241989135742 EXP_TONALITY_OVER_TIME = 5.7180857658386230 EXP_FT_OVER_TIME = 795.41015625000000 -EXP_TIME = 0.0138125 +EXP_TIME = 3.5360000133514404 EXP_STR = ( "TonalityECMA418_2 object.\n" + "Data\n" From 9e20989313e2b8053c2e8038bff3361db41a7177 Mon Sep 17 00:00:00 2001 From: aboutheo Date: Wed, 15 Jan 2025 16:56:51 +0100 Subject: [PATCH 3/9] feat: minor modifs in tonality ecma 418_2 (2) --- .../sound/core/psychoacoustics/tonality_ecma_418_2.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py b/src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py index 474fe06bd..9987f8e7f 100644 --- a/src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py +++ b/src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py @@ -210,16 +210,17 @@ def plot(self): time_scale_ft = self.get_output()[2].time_freq_support.time_frequencies.data # Plot DIN 45681 parameters over time. - _, axes = plt.subplots(2, 1, sharex=True) + _, axes = plt.subplots(2, 1, sharex=False) axes[0].plot(time_scale_tonality, tonality_over_time) axes[0].set_title("ECMA418-2 psychoacoustic tonality") - axes[0].set_ylabel(r"T $\mathregular{tu_HMS}$") + axes[0].set_ylabel(r"T $\mathregular{tu_{HMS}}$") axes[0].grid(True) axes[1].plot(time_scale_ft, ft_over_time) axes[1].set_title("DIN45681 decisive frequency") - axes[1].set_ylabel(r"$\mathregular{f_ton}$ (Hz)") + axes[1].set_ylabel(r"$\mathregular{f_{ton}}$ (Hz)") axes[1].grid(True) + axes[1].set_xlabel("Time (s)") plt.tight_layout() plt.show() From b846b138df75bf32f5a3acbeb215ef27b2377b8c Mon Sep 17 00:00:00 2001 From: Antoine Minard Date: Wed, 15 Jan 2025 18:15:21 +0100 Subject: [PATCH 4/9] Fixed a few comment sections --- .../core/psychoacoustics/tonality_ecma_418_2.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py b/src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py index 9987f8e7f..402be259d 100644 --- a/src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py +++ b/src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py @@ -30,6 +30,7 @@ from . import PsychoacousticsParent from .._pyansys_sound import PyAnsysSoundException, PyAnsysSoundWarning +# Name of the DPF Sound operator used in this module. ID_COMPUTE_TONALITY_ECMA_418_2 = "compute_tonality_ecma418_2" @@ -37,18 +38,16 @@ class TonalityECMA418_2(PsychoacousticsParent): """Computes ECMA 418-2 tonality. This class is used to compute the tonality according to the ECMA 418-2 standard (Hearing Model - of Sottek). The standard is also know as ECMA74_G. + of Sottek), formerly known as ECMA 74, annex G. """ def __init__(self, signal: Field = None): - """Create a ``TonalityECMA418_2`` object. + """Class instantiation takes the following parameters. Parameters ---------- signal: Field, default: None Signal in Pa on which to calculate the tonality, as a DPF field. - - For more information about the parameters, please refer to Ansys Sound SAS' user guide. """ super().__init__() self.signal = signal @@ -98,7 +97,7 @@ def process(self): self.__operator.get_output(2, types.field), ) - def get_output(self) -> tuple[float, Field, Field, Field]: + def get_output(self) -> tuple[float, Field, Field]: """Get the ECMA 418-2 tonality data, in a tuple containing data of various types. Returns @@ -193,10 +192,10 @@ def get_time_scale(self) -> np.ndarray: return self.get_output_as_nparray()[3] def plot(self): - """Plot the ECMA 418-2's toanlity and tone frequency over time. + """Plot the ECMA 418-2's tonality and tone frequency over time. This method creates a figure window that displays the tonality in dB - and the tone frequencies in Hz over time. + and the tone frequency in Hz over time. """ if self._output == None: raise PyAnsysSoundException( From 6df6af7d5c4f2415554b16baae3f255f63cf943b Mon Sep 17 00:00:00 2001 From: Antoine Minard Date: Thu, 16 Jan 2025 09:40:25 +0100 Subject: [PATCH 5/9] Fixed some docstrings and plot strings --- .../sound/core/psychoacoustics/tonality_ecma_418_2.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py b/src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py index 402be259d..0d4f34d2e 100644 --- a/src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py +++ b/src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py @@ -186,8 +186,7 @@ def get_time_scale(self) -> np.ndarray: Returns ------- numpy.ndarray - Array of the computation times, in seconds, of the ECMA 418-2 parameters over time - (tonality and tonal frequencies). + Array of the computation times, in seconds, of the ECMA 418-2 tonality over time. """ return self.get_output_as_nparray()[3] @@ -195,7 +194,7 @@ def plot(self): """Plot the ECMA 418-2's tonality and tone frequency over time. This method creates a figure window that displays the tonality in dB - and the tone frequency in Hz over time. + and the tone frequency in Hz, over time. """ if self._output == None: raise PyAnsysSoundException( @@ -212,11 +211,11 @@ def plot(self): _, axes = plt.subplots(2, 1, sharex=False) axes[0].plot(time_scale_tonality, tonality_over_time) axes[0].set_title("ECMA418-2 psychoacoustic tonality") - axes[0].set_ylabel(r"T $\mathregular{tu_{HMS}}$") + axes[0].set_ylabel(r"T ($\mathregular{tu_{HMS}})$") axes[0].grid(True) axes[1].plot(time_scale_ft, ft_over_time) - axes[1].set_title("DIN45681 decisive frequency") + axes[1].set_title("ECMA418-2 tone frequency") axes[1].set_ylabel(r"$\mathregular{f_{ton}}$ (Hz)") axes[1].grid(True) axes[1].set_xlabel("Time (s)") From 095a4d1c1c1e683cd514043d2839b527bbabf2c7 Mon Sep 17 00:00:00 2001 From: Antoine Minard Date: Thu, 16 Jan 2025 09:46:16 +0100 Subject: [PATCH 6/9] fixed a comment --- src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py b/src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py index 0d4f34d2e..19ba29ca7 100644 --- a/src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py +++ b/src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py @@ -207,7 +207,7 @@ def plot(self): time_scale_tonality = self.get_time_scale() time_scale_ft = self.get_output()[2].time_freq_support.time_frequencies.data - # Plot DIN 45681 parameters over time. + # Plot ECMA 418-2 parameters over time. _, axes = plt.subplots(2, 1, sharex=False) axes[0].plot(time_scale_tonality, tonality_over_time) axes[0].set_title("ECMA418-2 psychoacoustic tonality") From 7b99c20bb0e5d46323191c908dff5abf4091ad9a Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Thu, 16 Jan 2025 09:30:34 +0000 Subject: [PATCH 7/9] chore: adding changelog file 208.added.md [dependabot-skip] --- doc/changelog.d/208.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/208.added.md diff --git a/doc/changelog.d/208.added.md b/doc/changelog.d/208.added.md new file mode 100644 index 000000000..5f9da9cd1 --- /dev/null +++ b/doc/changelog.d/208.added.md @@ -0,0 +1 @@ +feat: Tonality ECMA418-2 \ No newline at end of file From 32ba270c0b856a2c1215a915245fe31f76fd39bd Mon Sep 17 00:00:00 2001 From: aboutheo Date: Thu, 16 Jan 2025 13:55:40 +0100 Subject: [PATCH 8/9] feat: minor modifs in tonality ecma 418_2 (3) --- .../sound/core/psychoacoustics/tonality_ecma_418_2.py | 7 ++++++- .../test_psychoacoustics_tonality_ecma_418_2.py | 10 +++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py b/src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py index 19ba29ca7..36a589906 100644 --- a/src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py +++ b/src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py @@ -55,11 +55,16 @@ def __init__(self, signal: Field = None): def __str__(self): """Return the string representation of the object.""" + if self._output is None: + str_tonality = "Not processed\n" + else: + str_tonality = f"{self.get_tonality():.2f} tuHMS\n" + return ( f"{__class__.__name__} object.\n" "Data\n" f'Signal name: "{self.signal.name}"\n' - f"Tonality: {self.get_tonality()} tuHMS\n" + f"Tonality: {str_tonality}" ) @property diff --git a/tests/tests_psychoacoustics/test_psychoacoustics_tonality_ecma_418_2.py b/tests/tests_psychoacoustics/test_psychoacoustics_tonality_ecma_418_2.py index e0028d0f7..3f5702bd2 100644 --- a/tests/tests_psychoacoustics/test_psychoacoustics_tonality_ecma_418_2.py +++ b/tests/tests_psychoacoustics/test_psychoacoustics_tonality_ecma_418_2.py @@ -34,11 +34,14 @@ EXP_TONALITY_OVER_TIME = 5.7180857658386230 EXP_FT_OVER_TIME = 795.41015625000000 EXP_TIME = 3.5360000133514404 -EXP_STR = ( +EXP_STR_1 = ( "TonalityECMA418_2 object.\n" + "Data\n" + f'Signal name: "flute"\n' - + f"Tonality: 4.918124198913574 tuHMS\n" + + f"Tonality: Not processed\n" +) +EXP_STR_2 = ( + "TonalityECMA418_2 object.\n" + "Data\n" + f'Signal name: "flute"\n' + f"Tonality: 4.92 tuHMS\n" ) @@ -62,9 +65,10 @@ def test_tonality_ecma_418_2___str__(): fc = wav_loader.get_output() tonality = TonalityECMA418_2(signal=fc[0]) + assert tonality.__str__() == EXP_STR_1 tonality.process() - assert tonality.__str__() == EXP_STR + assert tonality.__str__() == EXP_STR_2 def test_tonality_ecma_418_2_setters_exceptions(): From 8c0dc3c3ede58d4ed7753a8b262f34d3f2b0ec01 Mon Sep 17 00:00:00 2001 From: aboutheo Date: Mon, 20 Jan 2025 11:21:09 +0100 Subject: [PATCH 9/9] feat: modifications after PR review --- .../psychoacoustics/tonality_ecma_418_2.py | 26 ++++++++++--- ...est_psychoacoustics_tonality_ecma_418_2.py | 38 ++++++++++++++----- 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py b/src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py index 36a589906..18dd80644 100644 --- a/src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py +++ b/src/ansys/sound/core/psychoacoustics/tonality_ecma_418_2.py @@ -136,7 +136,9 @@ def get_output_as_nparray(self) -> tuple[float, np.ndarray, np.ndarray]: Third element is the ECMA 418-2 tone frequency over time, in Hz. - Fourth element is the associated time scale, in s. + Fourth element is the associated time scale for tonality, in s. + + Fifth element is the associated time scale for tone frequency, in s. """ output = self.get_output() @@ -146,6 +148,7 @@ def get_output_as_nparray(self) -> tuple[float, np.ndarray, np.ndarray]: np.array([]), np.array([]), np.array([]), + np.array([]), ) return ( @@ -153,6 +156,7 @@ def get_output_as_nparray(self) -> tuple[float, np.ndarray, np.ndarray]: np.array(output[1].data), np.array(output[2].data), np.array(output[1].time_freq_support.time_frequencies.data), + np.array(output[2].time_freq_support.time_frequencies.data), ) def get_tonality(self) -> float: @@ -185,8 +189,8 @@ def get_tone_frequency_over_time(self) -> np.ndarray: """ return self.get_output_as_nparray()[2] - def get_time_scale(self) -> np.ndarray: - """Get the ECMA 418-2 time scale, in s. + def get_tonality_time_scale(self) -> np.ndarray: + """Get the ECMA 418-2 tonality time scale, in s. Returns ------- @@ -195,6 +199,16 @@ def get_time_scale(self) -> np.ndarray: """ return self.get_output_as_nparray()[3] + def get_tone_frequency_time_scale(self) -> np.ndarray: + """Get the ECMA 418-2 tone frequency time scale, in s. + + Returns + ------- + numpy.ndarray + Array of the computation times, in seconds, of the ECMA 418-2 tone frequency over time. + """ + return self.get_output_as_nparray()[4] + def plot(self): """Plot the ECMA 418-2's tonality and tone frequency over time. @@ -209,11 +223,11 @@ def plot(self): # Get data to plot tonality_over_time = self.get_tonality_over_time() ft_over_time = self.get_tone_frequency_over_time() - time_scale_tonality = self.get_time_scale() - time_scale_ft = self.get_output()[2].time_freq_support.time_frequencies.data + time_scale_tonality = self.get_tonality_time_scale() + time_scale_ft = self.get_tone_frequency_time_scale() # Plot ECMA 418-2 parameters over time. - _, axes = plt.subplots(2, 1, sharex=False) + _, axes = plt.subplots(2, 1, sharex=True) axes[0].plot(time_scale_tonality, tonality_over_time) axes[0].set_title("ECMA418-2 psychoacoustic tonality") axes[0].set_ylabel(r"T ($\mathregular{tu_{HMS}})$") diff --git a/tests/tests_psychoacoustics/test_psychoacoustics_tonality_ecma_418_2.py b/tests/tests_psychoacoustics/test_psychoacoustics_tonality_ecma_418_2.py index 3f5702bd2..017c7c6b4 100644 --- a/tests/tests_psychoacoustics/test_psychoacoustics_tonality_ecma_418_2.py +++ b/tests/tests_psychoacoustics/test_psychoacoustics_tonality_ecma_418_2.py @@ -33,7 +33,8 @@ EXP_TONALITY = 4.9181241989135742 EXP_TONALITY_OVER_TIME = 5.7180857658386230 EXP_FT_OVER_TIME = 795.41015625000000 -EXP_TIME = 3.5360000133514404 +EXP_TIME_TONALITY = 3.5360000133514404 +EXP_TIME_TONE_FREQUENCY = 3.237333297729492 EXP_STR_1 = ( "TonalityECMA418_2 object.\n" + "Data\n" @@ -136,12 +137,15 @@ def test_tonality_ecma_418_2_get_output_as_nparray(): tonality = TonalityECMA418_2(signal=fc[0]) tonality.process() - tonality_float, tonality_over_time, ft_over_time, time = tonality.get_output_as_nparray() + tonality_float, tonality_over_time, ft_over_time, time_tonality, time_tone = ( + tonality.get_output_as_nparray() + ) assert tonality_float == pytest.approx(EXP_TONALITY) assert tonality_over_time[225] == pytest.approx(EXP_TONALITY_OVER_TIME) assert ft_over_time[225] == pytest.approx(EXP_FT_OVER_TIME) - assert time[-1] == pytest.approx(EXP_TIME) + assert time_tonality[-1] == pytest.approx(EXP_TIME_TONALITY) + assert time_tone[-1] == pytest.approx(EXP_TIME_TONE_FREQUENCY) def test_tonality_ecma_418_2_get_output_as_nparray_unprocessed(): @@ -152,11 +156,14 @@ def test_tonality_ecma_418_2_get_output_as_nparray_unprocessed(): PyAnsysSoundWarning, match="Output is not processed yet. Use the ``TonalityECMA418_2.process\\(\\)`` method.", ): - tonality_float, tonality_over_time, ft_over_time, time = tonality.get_output_as_nparray() + tonality_float, tonality_over_time, ft_over_time, time_tonality, time_tones = ( + tonality.get_output_as_nparray() + ) assert np.isnan(tonality_float) assert len(tonality_over_time) == 0 assert len(ft_over_time) == 0 - assert len(time) == 0 + assert len(time_tonality) == 0 + assert len(time_tones) == 0 def test_tonality_ecma_418_2_get_tonality(): @@ -198,8 +205,8 @@ def test_tonality_ecma_418_2_get_tone_frequency_over_time(): assert ft_over_time[225] == pytest.approx(EXP_FT_OVER_TIME) -def test_tonality_ecma_418_2_get_time_scale(): - """Test get_time_scale method.""" +def test_tonality_ecma_418_2_get_tonality_time_scale(): + """Test get_tonality_time_scale method.""" wav_loader = LoadWav(pytest.data_path_flute_nonUnitaryCalib_in_container) wav_loader.process() fc = wav_loader.get_output() @@ -207,8 +214,21 @@ def test_tonality_ecma_418_2_get_time_scale(): tonality = TonalityECMA418_2(signal=fc[0]) tonality.process() - time = tonality.get_time_scale() - assert time[-1] == pytest.approx(EXP_TIME) + time = tonality.get_tonality_time_scale() + assert time[-1] == pytest.approx(EXP_TIME_TONALITY) + + +def test_tonality_ecma_418_2_get_tone_frequency_time_scale(): + """Test get_tone_frequency_time_scale method.""" + wav_loader = LoadWav(pytest.data_path_flute_nonUnitaryCalib_in_container) + wav_loader.process() + fc = wav_loader.get_output() + + tonality = TonalityECMA418_2(signal=fc[0]) + tonality.process() + + time = tonality.get_tone_frequency_time_scale() + assert time[-1] == pytest.approx(EXP_TIME_TONE_FREQUENCY) @patch("matplotlib.pyplot.show")