diff --git a/doc/changelog.d/244.added.md b/doc/changelog.d/244.added.md new file mode 100644 index 000000000..688c01843 --- /dev/null +++ b/doc/changelog.d/244.added.md @@ -0,0 +1 @@ +feat: Loudness ISO 532-2 \ No newline at end of file diff --git a/doc/source/api/psychoacoustics.rst b/doc/source/api/psychoacoustics.rst index 125b8e3b7..f490b93df 100644 --- a/doc/source/api/psychoacoustics.rst +++ b/doc/source/api/psychoacoustics.rst @@ -11,6 +11,7 @@ which are useful to study and assess sound quality. LoudnessISO532_1_Stationary LoudnessISO532_1_TimeVarying + LoudnessISO532_2 LoudnessANSI_S3_4 SpectralCentroid Sharpness diff --git a/src/ansys/sound/core/psychoacoustics/__init__.py b/src/ansys/sound/core/psychoacoustics/__init__.py index 86c02c64b..8339b3bb6 100644 --- a/src/ansys/sound/core/psychoacoustics/__init__.py +++ b/src/ansys/sound/core/psychoacoustics/__init__.py @@ -30,6 +30,7 @@ from .loudness_ansi_s3_4 import LoudnessANSI_S3_4 from .loudness_iso_532_1_stationary import LoudnessISO532_1_Stationary from .loudness_iso_532_1_time_varying import LoudnessISO532_1_TimeVarying +from .loudness_iso_532_2 import LoudnessISO532_2 from .prominence_ratio import ProminenceRatio from .prominence_ratio_for_orders_over_time import ProminenceRatioForOrdersOverTime from .roughness import Roughness @@ -51,6 +52,7 @@ "PsychoacousticsParent", "LoudnessISO532_1_Stationary", "LoudnessISO532_1_TimeVarying", + "LoudnessISO532_2", "LoudnessANSI_S3_4", "ProminenceRatio", "ToneToNoiseRatio", diff --git a/src/ansys/sound/core/psychoacoustics/loudness_iso_532_1_stationary.py b/src/ansys/sound/core/psychoacoustics/loudness_iso_532_1_stationary.py index e42fe7ee9..ac67d009c 100644 --- a/src/ansys/sound/core/psychoacoustics/loudness_iso_532_1_stationary.py +++ b/src/ansys/sound/core/psychoacoustics/loudness_iso_532_1_stationary.py @@ -35,10 +35,10 @@ class LoudnessISO532_1_Stationary(PsychoacousticsParent): - """Computes ISO 532-1 loudness for stationary sounds. + """Computes ISO 532-1:2017 loudness for stationary sounds. - This class computes the loudness of a signal according to the ISO 532-1 standard for stationary - sounds. + This class computes the loudness of a signal according to the ISO 532-1:2017 standard, + corresponding to the "Zwicker method", for stationary sounds. """ def __init__( diff --git a/src/ansys/sound/core/psychoacoustics/loudness_iso_532_1_time_varying.py b/src/ansys/sound/core/psychoacoustics/loudness_iso_532_1_time_varying.py index d84c0f9b8..9092b4620 100644 --- a/src/ansys/sound/core/psychoacoustics/loudness_iso_532_1_time_varying.py +++ b/src/ansys/sound/core/psychoacoustics/loudness_iso_532_1_time_varying.py @@ -35,10 +35,10 @@ class LoudnessISO532_1_TimeVarying(PsychoacousticsParent): - """Computes ISO 532-1 loudness for time-varying sounds. + """Computes ISO 532-1:2017 loudness for time-varying sounds. - This class computes the loudness of a signal according to the ISO 532-1 standard for - time-varying sounds. + This class computes the loudness of a signal according to the ISO 532-1:2017 standard, + corresponding to the "Zwicker method", for time-varying sounds. """ def __init__( diff --git a/src/ansys/sound/core/psychoacoustics/loudness_iso_532_2.py b/src/ansys/sound/core/psychoacoustics/loudness_iso_532_2.py new file mode 100644 index 000000000..b2b387b45 --- /dev/null +++ b/src/ansys/sound/core/psychoacoustics/loudness_iso_532_2.py @@ -0,0 +1,396 @@ +# 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 ISO 532-2:2017 loudness.""" +import warnings + +from ansys.dpf.core import Field, FieldsContainer, Operator, types +import matplotlib.pyplot as plt +import numpy as np + +from . import FIELD_DIFFUSE, FIELD_FREE, PsychoacousticsParent +from .._pyansys_sound import PyAnsysSoundException, PyAnsysSoundWarning + +# Name of the DPF Sound operator used in this module. +ID_COMPUTE_LOUDNESS_ISO_532_2 = "compute_loudness_iso532_2" + +RECORDING_MIC = "Mic" +RECORDING_HEAD = "Head" + + +class LoudnessISO532_2(PsychoacousticsParent): + """Computes ISO 532-2:2017 loudness. + + This class computes the binaural and monaural loudness of a signal according to the + ISO 532-2:2017 standard, corresponding to the "Moore-Glasberg method". + """ + + def __init__( + self, + signal: Field | FieldsContainer = None, + field_type: str = FIELD_FREE, + recording_type: str = RECORDING_MIC, + ): + """Class instantiation takes the following parameters. + + Parameters + ---------- + signal : Field | FieldsContainer, default: None + Signal in Pa on which to compute loudness. If ``signal`` is a + :class:`Field `, the listening assumption is diotic (same + signal presented at both ears). If ``signal`` is a + :class:`FieldsContainer `, with + exactly 2 fields, the listening assumption is dichotic (each field's signal presented + at each ear). + field_type : str, default: "Free" + Sound field type. Available options are `"Free"` and `"Diffuse"`. + recording_type : str, default: "Mic" + Recording type. Available options are `"Mic"` for a single microphone and `"Head"` for + a head and torso simulator. + """ + super().__init__() + self.signal = signal + self.field_type = field_type + self.recording_type = recording_type + self.__operator = Operator(ID_COMPUTE_LOUDNESS_ISO_532_2) + + def __str__(self): + """Return the string representation of the class.""" + if self.signal is not None: + signal_str = ( + f'\tSignal name: "{self.signal.name}"\n' + "\tListening assumption: " + f"{"Diotic" if type(self.signal) is Field else "Dichotic"}\n" + ) + else: + signal_str = "\tSignal name: Not set\n" + + if self.recording_type == RECORDING_MIC: + rec_str = "Single microphone" + else: + rec_str = "Head and torso simulator" + + if self._output is not None: + output_str = ( + f"Binaural loudness: {self.get_binaural_loudness_sone():.3} sones\n" + f"Binaural loudness level: {self.get_binaural_loudness_level_phon():.1f} phons" + ) + else: + output_str = "Binaural loudness: Not processed\nBinaural loudness level: Not processed" + + return ( + f"{__class__.__name__} object.\n" + "Data\n" + f"{signal_str}" + f"\tField type: {self.field_type}\n" + f"\tRecording type: {self.recording_type} ({rec_str})\n" + f"{output_str}" + ) + + @property + def signal(self) -> Field | FieldsContainer: + """Input sound signal in Pa. + + Signal in Pa on which to compute loudness. If ``signal`` is a + :class:`Field `, the listening assumption is diotic (same + signal presented at both ears). If ``signal`` is a + :class:`FieldsContainer `, with exactly 2 + fields, the listening assumption is dichotic (each field's signal presented at each ear). + """ + return self.__signal + + @signal.setter + def signal(self, signal: Field | FieldsContainer): + """Set the signal.""" + if signal is not None: + if isinstance(signal, FieldsContainer): + if len(signal) != 2: + raise PyAnsysSoundException( + "The input FieldsContainer signal must contain exactly 2 fields " + "corresponding to the signals presented at the two ears." + ) + elif not isinstance(signal, Field): + raise PyAnsysSoundException( + "Signal must be specified as a DPF field or fields container." + ) + self.__signal = signal + + @property + def field_type(self) -> str: + """Sound field type. + + Available options are `"Free"` and `"Diffuse"`. + """ + return self.__field_type + + @field_type.setter + def field_type(self, field_type: str): + """Set the sound field type.""" + if field_type.lower() not in [FIELD_FREE.lower(), FIELD_DIFFUSE.lower()]: + raise PyAnsysSoundException( + f'Invalid field type "{field_type}". Available options are "{FIELD_FREE}" and ' + f'"{FIELD_DIFFUSE}".' + ) + self.__field_type = field_type + + @property + def recording_type(self) -> str: + """Recording type. + + Available options are `"Mic"` for a single microphone and `"Head"` for a head and torso + simulator. + """ + return self.__recording_type + + @recording_type.setter + def recording_type(self, recording_type: str): + """Set the recording type.""" + if recording_type.lower() not in [RECORDING_MIC.lower(), RECORDING_HEAD.lower()]: + raise PyAnsysSoundException( + f'Invalid recording type "{recording_type}". Available options are ' + f'"{RECORDING_MIC}" and "{RECORDING_HEAD}".' + ) + self.__recording_type = recording_type + + def process(self): + """Compute the loudness. + + This method calls the appropriate DPF Sound operator to compute the loudness of the signal. + """ + if self.signal == None: + raise PyAnsysSoundException(f"No input signal set. Use `{__class__.__name__}.signal`.") + + self.__operator.connect(0, self.signal) + self.__operator.connect(1, self.field_type) + self.__operator.connect(2, self.recording_type) + + # Runs the operator + self.__operator.run() + + self._output = ( + self.__operator.get_output(0, types.double), + self.__operator.get_output(1, types.double), + self.__operator.get_output(2, types.vec_double), + self.__operator.get_output(3, types.vec_double), + self.__operator.get_output(4, types.field), + self.__operator.get_output(5, types.fields_container), + ) + + def get_output(self) -> tuple: + """Get the binaural and monaural loudness, loudness level, and specific loudness. + + Returns + ------- + tuple + - First element (float): binaural loudness in sone. + + - Second element (float): binaural loudness level in phon. + + - Third element (DPFarray): monaural loudness in sone at each ear. + + - Fourth element (DPFarray): monaural loudness level in phon at each ear. + + - Fifth element (Field): binaural specific loudness in sone/Cam, as a function of the + ERB center frequency. + + - Sixth element (FieldsContainer): monaural specific loudness in sone/Cam at each ear, + as a function of the ERB center frequency. + """ + if self._output == None: + warnings.warn( + PyAnsysSoundWarning( + "Output is not processed yet. Use the " + f"`{__class__.__name__}.process()` method." + ) + ) + + return self._output + + def get_output_as_nparray(self) -> tuple[np.ndarray]: + """Get loudness data in a tuple of NumPy arrays. + + Returns + ------- + tuple[numpy.ndarray] + - First element: binaural loudness in sone. + + - Second element: binaural loudness level in phon. + + - Third element: monaural loudness in sone at each ear. + + - Fourth element: monaural loudness level in phon at each ear. + + - Fifth element: binaural specific loudness in sone/Cam, as a function of the ERB + center frequency. + + - Sixth element: monaural specific loudness in sone/Cam at each ear, as a function of + the ERB center frequency. + + - Seventh element: center frequencies in Hz of the equivalent rectangular + bandwidths (ERB), where specific loudness is defined. + """ + output = self.get_output() + + if output == None: + return ( + np.nan, + np.nan, + np.array([]), + np.array([]), + np.array([]), + np.array([]), + np.array([]), + ) + + return ( + np.array(output[0]), + np.array(output[1]), + np.array(output[2]), + np.array(output[3]), + np.array(output[4].data), + self.convert_fields_container_to_np_array(output[5]), + np.array(output[4].time_freq_support.time_frequencies.data), + ) + + def get_binaural_loudness_sone(self) -> float: + """Get the binaural loudness in sone. + + Returns + ------- + float + Binaural loudness in sone. + """ + return self.get_output()[0] + + def get_binaural_loudness_level_phon(self) -> float: + """Get the binaural loudness level in phon. + + Returns + ------- + float + Binaural loudness level in phon. + """ + return self.get_output()[1] + + def get_monaural_loudness_sone(self) -> np.ndarray: + """Get the monaural loudness in sone at each ear. + + Returns + ------- + numpy.ndarray + Monaural loudness in sone at each ear. + """ + output = self.get_output_as_nparray()[2] + if len(output) == 1: + return np.array([output[0], output[0]]) + else: + return output + + def get_monaural_loudness_level_phon(self) -> np.ndarray: + """Get the monaural loudness level in phon at each ear. + + Returns + ------- + numpy.ndarray + Monaural loudness level in phon at each ear. + """ + output = self.get_output_as_nparray()[3] + if len(output) == 1: + return np.array([output[0], output[0]]) + else: + return output + + def get_binaural_specific_loudness(self) -> np.ndarray: + """Get the binaural specific loudness. + + Returns + ------- + numpy.ndarray + Binaural specific loudness array in sone/Cam, as a function of the ERB center frequency. + """ + return self.get_output_as_nparray()[4] + + def get_monaural_specific_loudness(self) -> np.ndarray: + """Get the monaural specific loudness at each ear. + + Returns + ------- + numpy.ndarray + Monaural specific loudness array in sone/Cam at each ear, as a function of the ERB + center frequency. + """ + output = self.get_output_as_nparray()[5] + + # If signal is a FieldsContainer, then output's length is 2. + # However, here, if signal is a Field, then it is not 1, it is the length of the specific + # loudness. So the test below has to compare the length to 2, not 1. + if len(output) != 2: + return np.array([output, output]) + else: + return output + + def get_erb_center_frequencies(self) -> np.ndarray: + """Get the ERB center frequencies in Hz. + + This method returns the center frequencies in Hz of the equivalent rectangular bandwidths + (ERB), where the specific loudness is defined. + + Returns + ------- + numpy.ndarray + Array of ERB center frequencies in Hz. + """ + return self.get_output_as_nparray()[6] + + def get_erbn_numbers(self) -> np.ndarray: + """Get the ERBn-number scale in Cam. + + This method uses the equation (6) in ISO 532-2:2017 to convert the ERB center frequencies + into the ERBn-number scale in Cam. + + Returns + ------- + numpy.ndarray + ERBn-number scale in Cam. + """ + return 21.366 * np.log10(0.004368 * self.get_erb_center_frequencies() + 1) + + def plot(self): + """Plot the binaural specific loudness. + + This method displays the binaural specific loudness in sone/Cam as a function of the ERB + center frequency. + """ + if self._output == None: + raise PyAnsysSoundException( + "Output is not processed yet. Use the " f"`{__class__.__name__}.process()` method." + ) + + center_frequency = self.get_erb_center_frequencies() + specific_loudness = self.get_binaural_specific_loudness() + + plt.plot(center_frequency, specific_loudness) + plt.title("Binaural specific loudness") + plt.xlabel("ERB center frequency (Hz)") + plt.ylabel("N' (sone/Cam)") + plt.grid(True) + plt.show() diff --git a/tests/conftest.py b/tests/conftest.py index 2f20f7229..4ebe91309 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -36,16 +36,21 @@ def pytest_configure(): # configuration. That's why we authorize the use of this function here. server = connect_to_or_start_server(use_license_context=True) - # # Get the current directory of the conftest.py file + ## Get the current directory of the conftest.py file base_dir = os.path.join(os.path.dirname(__file__), "data") - # Construct the paths of the different test files after uploading them on the server. + ## Construct the paths of the different test files after uploading them on the server. + # Audio samples pytest.data_path_flute_in_container = upload_file_in_tmp_folder( os.path.join(base_dir, "flute.wav"), server=server ) pytest.data_path_flute_nonUnitaryCalib_in_container = upload_file_in_tmp_folder( os.path.join(base_dir, "flute_nonUnitaryCalib.wav"), server=server ) + pytest.data_path_flute_nonUnitaryCalib_as_txt_in_container = upload_file_in_tmp_folder( + os.path.join(base_dir, "flute_nonUnitaryCalib_as_text_2024R2_20241125.txt"), + server=server, + ) pytest.data_path_sharp_noise_in_container = upload_file_in_tmp_folder( os.path.join(base_dir, "sharp_noise.wav"), server=server ) @@ -64,12 +69,30 @@ def pytest_configure(): pytest.data_path_white_noise_in_container = upload_file_in_tmp_folder( os.path.join(base_dir, "white_noise.wav"), server=server ) + pytest.data_path_aircraft_nonUnitaryCalib_in_container = upload_file_in_tmp_folder( + os.path.join(base_dir, "Aircraft-App2_nonUnitaryCalib.wav"), server=server + ) + pytest.data_path_Acceleration_stereo_nonUnitaryCalib = upload_file_in_tmp_folder( + os.path.join(base_dir, "Acceleration_stereo_nonUnitaryCalib.wav"), + server=server, + ) pytest.data_path_accel_with_rpm_in_container = upload_file_in_tmp_folder( os.path.join(base_dir, "accel_with_rpm.wav"), server=server ) - pytest.data_path_aircraft_nonUnitaryCalib_in_container = upload_file_in_tmp_folder( - os.path.join(base_dir, "Aircraft-App2_nonUnitaryCalib.wav"), server=server + pytest.data_path_Acceleration_with_Tacho_nonUnitaryCalib = upload_file_in_tmp_folder( + os.path.join(base_dir, "Acceleration_with_Tacho_nonUnitaryCalib.wav"), + server=server, + ) + + # RPM profiles + pytest.data_path_rpm_profile_as_wav_in_container = upload_file_in_tmp_folder( + os.path.join(base_dir, "RPM_profile_2024R2_20241126.wav"), server=server + ) + pytest.data_path_rpm_profile_as_txt_in_container = upload_file_in_tmp_folder( + os.path.join(base_dir, "RPM_profile_2024R2_20241126.txt"), server=server ) + + # Sound power level projects pytest.data_path_swl_project_file_in_container = upload_file_in_tmp_folder( os.path.join(base_dir, "SoundPowerLevelProject_hemisphere_2025R1_20243008.spw"), server=server, @@ -81,6 +104,8 @@ def pytest_configure(): ), server=server, ) + + # Sound composer files (including spectrum, harmonics, etc. data files) pytest.data_path_sound_composer_spectrum_source_in_container = upload_file_in_tmp_folder( os.path.join(base_dir, "AnsysSound_Spectrum_v3_-_nominal_-_dBSPLperHz_2024R2_20241121.txt"), server=server, @@ -138,22 +163,6 @@ def pytest_configure(): pytest.data_path_sound_composer_harmonics_source_xml_in_container = upload_file_in_tmp_folder( os.path.join(base_dir, "VRX_Waterfall_2024R2_20241203.xml"), server=server ) - pytest.data_path_filter_frf = upload_file_in_tmp_folder( - os.path.join(base_dir, "AnsysSound_FRF_2024R2_20241206.txt"), server=server - ) - pytest.data_path_filter_frf_wrong_header = upload_file_in_tmp_folder( - os.path.join(base_dir, "AnsysSound_FRF_bad_2024R2_20241206.txt"), server=server - ) - pytest.data_path_flute_nonUnitaryCalib_as_txt_in_container = upload_file_in_tmp_folder( - os.path.join(base_dir, "flute_nonUnitaryCalib_as_text_2024R2_20241125.txt"), - server=server, - ) - pytest.data_path_rpm_profile_as_wav_in_container = upload_file_in_tmp_folder( - os.path.join(base_dir, "RPM_profile_2024R2_20241126.wav"), server=server - ) - pytest.data_path_rpm_profile_as_txt_in_container = upload_file_in_tmp_folder( - os.path.join(base_dir, "RPM_profile_2024R2_20241126.txt"), server=server - ) pytest.data_path_sound_composer_bbn_source_in_container = upload_file_in_tmp_folder( os.path.join(base_dir, "AnsysSound_BBN dBSPL OCTAVE Constants.txt"), server=server ) @@ -175,19 +184,24 @@ def pytest_configure(): ), server=server, ) - pytest.data_path_Acceleration_with_Tacho_nonUnitaryCalib = upload_file_in_tmp_folder( - os.path.join(base_dir, "Acceleration_with_Tacho_nonUnitaryCalib.wav"), - server=server, - ) pytest.data_path_sound_composer_project_in_container = upload_file_in_tmp_folder( os.path.join(base_dir, "20250130_SoundComposerProjectForDpfSoundTesting_valid.scn"), server=server, ) - # This path is different that the other, we need a local path + # FRF files + pytest.data_path_filter_frf = upload_file_in_tmp_folder( + os.path.join(base_dir, "AnsysSound_FRF_2024R2_20241206.txt"), server=server + ) + pytest.data_path_filter_frf_wrong_header = upload_file_in_tmp_folder( + os.path.join(base_dir, "AnsysSound_FRF_bad_2024R2_20241206.txt"), server=server + ) + + # PSD file + # This path is different from the other files': we need a local path # and not a server path because we will use a native python # `open()` to read this file and not a DPF operator pytest.data_path_flute_psd_locally = os.path.join(base_dir, "flute_psd.txt") - # The temporary folder is the folder in the server where are stored the files + ## The temporary folder is the folder in the server where the files are stored. pytest.temporary_folder = os.path.dirname(pytest.data_path_flute_in_container) diff --git a/tests/data/Acceleration_stereo_nonUnitaryCalib.wav b/tests/data/Acceleration_stereo_nonUnitaryCalib.wav new file mode 100644 index 000000000..cd4bad346 Binary files /dev/null and b/tests/data/Acceleration_stereo_nonUnitaryCalib.wav differ diff --git a/tests/tests_psychoacoustics/test_psychoacoustics_loudness_iso_532_2.py b/tests/tests_psychoacoustics/test_psychoacoustics_loudness_iso_532_2.py new file mode 100644 index 000000000..53453d2ca --- /dev/null +++ b/tests/tests_psychoacoustics/test_psychoacoustics_loudness_iso_532_2.py @@ -0,0 +1,658 @@ +# 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, FieldsContainer +from ansys.dpf.core.fields_container_factory import over_time_freq_fields_container +import numpy as np +import pytest + +from ansys.sound.core._pyansys_sound import PyAnsysSoundException, PyAnsysSoundWarning +from ansys.sound.core.psychoacoustics import LoudnessISO532_2 +from ansys.sound.core.signal_utilities import LoadWav + +# Expected values for Acceleration_stereo_nonUnitaryCalib, in Free/Mic conditions +EXP_BIN_LOUDNESS_DICHOTIC_FREE_MIC = 18.90975 +EXP_BIN_LOUDNESS_LEVEL_DICHOTIC_FREE_MIC = 82.25344 +EXP_MON_LOUDNESS_DICHOTIC_FREE_MIC_LEFT = 9.673014 +EXP_MON_LOUDNESS_DICHOTIC_FREE_MIC_RIGHT = 14.93052 +EXP_MON_LOUDNESS_LEVEL_DICHOTIC_FREE_MIC_LEFT = 72.58388 +EXP_MON_LOUDNESS_LEVEL_DICHOTIC_FREE_MIC_RIGHT = 79.05491 +EXP_BIN_SPECIFIC_LOUDNESS_DICHOTIC_FREE_MIC_0 = 0.3092778 +EXP_BIN_SPECIFIC_LOUDNESS_DICHOTIC_FREE_MIC_45 = 1.656872 +EXP_BIN_SPECIFIC_LOUDNESS_DICHOTIC_FREE_MIC_98 = 0.6377245 +EXP_MON_SPECIFIC_LOUDNESS_DICHOTIC_FREE_MIC_L_0 = 0.1548998 +EXP_MON_SPECIFIC_LOUDNESS_DICHOTIC_FREE_MIC_L_45 = 0.8625394 +EXP_MON_SPECIFIC_LOUDNESS_DICHOTIC_FREE_MIC_L_98 = 0.3725382 +EXP_MON_SPECIFIC_LOUDNESS_DICHOTIC_FREE_MIC_R_0 = 0.2467274 +EXP_MON_SPECIFIC_LOUDNESS_DICHOTIC_FREE_MIC_R_45 = 1.298307 +EXP_MON_SPECIFIC_LOUDNESS_DICHOTIC_FREE_MIC_R_98 = 0.4703090 + +# Expected values for Acceleration_stereo_nonUnitaryCalib, in Diffuse/Mic conditions +EXP_BIN_LOUDNESS_DICHOTIC_DIFFUSE_MIC = 18.86968 +EXP_BIN_LOUDNESS_LEVEL_DICHOTIC_DIFFUSE_MIC = 82.22441 +EXP_MON_LOUDNESS_DICHOTIC_DIFFUSE_MIC_LEFT = 9.641939 +EXP_MON_LOUDNESS_DICHOTIC_DIFFUSE_MIC_RIGHT = 14.90496 +EXP_BIN_SPECIFIC_LOUDNESS_DICHOTIC_DIFFUSE_MIC_0 = 0.3092621 +EXP_BIN_SPECIFIC_LOUDNESS_DICHOTIC_DIFFUSE_MIC_45 = 1.639453 +EXP_BIN_SPECIFIC_LOUDNESS_DICHOTIC_DIFFUSE_MIC_98 = 0.6317677 + +# Expected values for Acceleration_stereo_nonUnitaryCalib, in Head condition (field type irrelevant) +EXP_BIN_LOUDNESS_DICHOTIC_HEAD = 14.52840 +EXP_BIN_LOUDNESS_LEVEL_DICHOTIC_HEAD = 78.61783 +EXP_MON_LOUDNESS_DICHOTIC_HEAD_LEFT = 7.324051 +EXP_MON_LOUDNESS_DICHOTIC_HEAD_RIGHT = 11.53435 +EXP_BIN_SPECIFIC_LOUDNESS_DICHOTIC_HEAD_0 = 0.3093223 +EXP_BIN_SPECIFIC_LOUDNESS_DICHOTIC_HEAD_45 = 1.603330 +EXP_BIN_SPECIFIC_LOUDNESS_DICHOTIC_HEAD_98 = 0.5659737 + +# Expected values for flute_nonUnitaryCalib (mono), in Free/Mic conditions +EXP_BIN_LOUDNESS_DIOTIC_FREE_MIC = 58.42287 +EXP_BIN_LOUDNESS_LEVEL_DIOTIC_FREE_MIC = 97.44814 +EXP_MON_LOUDNESS_DIOTIC_FREE_MIC = 38.94790 +EXP_MON_LOUDNESS_LEVEL_DIOTIC_FREE_MIC = 92.04321 +EXP_BIN_SPECIFIC_LOUDNESS_DIOTIC_FREE_MIC_0 = 3.350576e-7 +EXP_BIN_SPECIFIC_LOUDNESS_DIOTIC_FREE_MIC_45 = 1.477295 +EXP_BIN_SPECIFIC_LOUDNESS_DIOTIC_FREE_MIC_98 = 3.738140 +EXP_MON_SPECIFIC_LOUDNESS_DIOTIC_FREE_MIC_0 = 2.233678e-7 +EXP_MON_SPECIFIC_LOUDNESS_DIOTIC_FREE_MIC_45 = 0.9848462 +EXP_MON_SPECIFIC_LOUDNESS_DIOTIC_FREE_MIC_98 = 2.492050 + +# Expected fc/ERBn values +EXP_ERB_LEN = 372 +EXP_ERB_0 = 1.8 +EXP_ERB_45 = 6.3 +EXP_ERB_98 = 11.6 +EXP_FREQ_0 = 49.01015 +EXP_FREQ_45 = 222.4797 +EXP_FREQ_98 = 570.2265 + +# Expected string representations +EXP_STR_DEFAULT = ( + "LoudnessISO532_2 object.\nData\n\tSignal name: Not set\n" + "\tField type: Free\n\tRecording type: Mic (Single microphone)\n" + "Binaural loudness: Not processed\n" + "Binaural loudness level: Not processed" +) +EXP_STR_ALLSET = ( + 'LoudnessISO532_2 object.\nData\n\tSignal name: "flute"\n' + "\tListening assumption: Diotic\n" + "\tField type: Diffuse\n\tRecording type: Head (Head and torso simulator)\n" + "Binaural loudness: 44.1 sones\n" + "Binaural loudness level: 93.8 phons" +) + + +def test_loudness_iso_532_2_instantiation(): + """Test the instantiation of the LoudnessISO532_2 class.""" + loudness_computer = LoudnessISO532_2() + assert isinstance(loudness_computer, LoudnessISO532_2) + assert loudness_computer.signal is None + assert loudness_computer.field_type == "Free" + assert loudness_computer.recording_type == "Mic" + + +def test_loudness_iso_532_2_properties(): + """Test the properties of the LoudnessISO532_2 class.""" + loudness_computer = LoudnessISO532_2() + + # Check signal property as a Field + loudness_computer.signal = Field() + assert type(loudness_computer.signal) == Field + + # Check signal property as a FieldsContainer + loudness_computer.signal = over_time_freq_fields_container([Field(), Field()]) + assert type(loudness_computer.signal) == FieldsContainer + assert len(loudness_computer.signal) == 2 + + # Check field_type property + loudness_computer.field_type = "Diffuse" + assert loudness_computer.field_type == "Diffuse" + + # Check case insensitivity + loudness_computer.field_type = "diffuse" + assert loudness_computer.field_type == "diffuse" + + loudness_computer.field_type = "DIFFUSE" + assert loudness_computer.field_type == "DIFFUSE" + + # Check recording_type property + loudness_computer.recording_type = "Head" + assert loudness_computer.recording_type == "Head" + + # Check case insensitivity + loudness_computer.recording_type = "head" + assert loudness_computer.recording_type == "head" + + loudness_computer.recording_type = "HEAD" + assert loudness_computer.recording_type == "HEAD" + + +def test_loudness_iso_532_2_properties_exceptions(): + """Test the exceptions of the properties of the LoudnessISO532_2 class.""" + loudness_computer = LoudnessISO532_2() + + # Check invalid value for signal property + with pytest.raises( + PyAnsysSoundException, + match="Signal must be specified as a DPF field or fields container.", + ): + loudness_computer.signal = "WrongType" + + # Check incorrect field number in fields container for signal property + with pytest.raises( + PyAnsysSoundException, + match=( + "The input FieldsContainer signal must contain exactly 2 fields corresponding to the " + "signals presented at the two ears." + ), + ): + loudness_computer.signal = FieldsContainer() + + # Check invalid value for field_type property + with pytest.raises( + PyAnsysSoundException, + match='Invalid field type "Invalid". Available options are "Free" and "Diffuse".', + ): + loudness_computer.field_type = "Invalid" + + # Check invalid value for recording_type property + with pytest.raises( + PyAnsysSoundException, + match='Invalid recording type "Invalid". Available options are "Mic" and "Head".', + ): + loudness_computer.recording_type = "Invalid" + + +def test_loudness_iso_532_2___str__(): + """Test the __str__ method of the LoudnessISO532_2 class.""" + loudness_computer = LoudnessISO532_2() + + assert loudness_computer.__str__() == EXP_STR_DEFAULT + + wav_loader = LoadWav(pytest.data_path_flute_nonUnitaryCalib_in_container) + wav_loader.process() + fc = wav_loader.get_output() + + loudness_computer.signal = fc[0] + loudness_computer.field_type = "Diffuse" + loudness_computer.recording_type = "Head" + loudness_computer.process() + + assert loudness_computer.__str__() == EXP_STR_ALLSET + + +def test_loudness_iso_532_2_process(): + """Test the process method of the LoudnessISO532_2 class.""" + loudness_computer = LoudnessISO532_2() + + wav_loader = LoadWav(pytest.data_path_flute_nonUnitaryCalib_in_container) + wav_loader.process() + fc = wav_loader.get_output() + + loudness_computer.signal = fc[0] + + loudness_computer.process() + assert loudness_computer._output is not None + + +def test_loudness_iso_532_2_process_exceptions(): + """Test the exceptions of the process method of the LoudnessISO532_2 class.""" + loudness_computer = LoudnessISO532_2() + + with pytest.raises( + PyAnsysSoundException, + match="No input signal set. Use `LoudnessISO532_2.signal`.", + ): + loudness_computer.process() + + +def test_loudness_iso_532_2_get_output_diotic_case(): + """Test the get_output method of the LoudnessISO532_2 class, in the diotic case.""" + loudness_computer = LoudnessISO532_2() + + wav_loader = LoadWav(pytest.data_path_flute_nonUnitaryCalib_in_container) + wav_loader.process() + fc = wav_loader.get_output() + + # A single signal channel -> Diotic case + loudness_computer.signal = fc[0] + + loudness_computer.process() + + N_bin, LN_bin, N_mon, LN_mon, Nprime_bin, Nprime_mon = loudness_computer.get_output() + assert N_bin == pytest.approx(EXP_BIN_LOUDNESS_DIOTIC_FREE_MIC) + assert LN_bin == pytest.approx(EXP_BIN_LOUDNESS_LEVEL_DIOTIC_FREE_MIC) + assert len(N_mon) == 1 + assert N_mon[0] == pytest.approx(EXP_MON_LOUDNESS_DIOTIC_FREE_MIC) + assert len(LN_mon) == 1 + assert LN_mon[0] == pytest.approx(EXP_MON_LOUDNESS_LEVEL_DIOTIC_FREE_MIC) + assert len(Nprime_bin) == EXP_ERB_LEN + assert Nprime_bin.data[0] == pytest.approx(EXP_BIN_SPECIFIC_LOUDNESS_DIOTIC_FREE_MIC_0) + assert Nprime_bin.data[45] == pytest.approx(EXP_BIN_SPECIFIC_LOUDNESS_DIOTIC_FREE_MIC_45) + assert Nprime_bin.data[98] == pytest.approx(EXP_BIN_SPECIFIC_LOUDNESS_DIOTIC_FREE_MIC_98) + assert len(Nprime_mon) == 1 + assert len(Nprime_mon[0]) == EXP_ERB_LEN + assert Nprime_mon[0].data[0] == pytest.approx(EXP_MON_SPECIFIC_LOUDNESS_DIOTIC_FREE_MIC_0) + assert Nprime_mon[0].data[45] == pytest.approx(EXP_MON_SPECIFIC_LOUDNESS_DIOTIC_FREE_MIC_45) + assert Nprime_mon[0].data[98] == pytest.approx(EXP_MON_SPECIFIC_LOUDNESS_DIOTIC_FREE_MIC_98) + + +@pytest.mark.parametrize( + "field_type, recording_type, exp_values", + [ + ( + "Free", + "Mic", + ( + EXP_BIN_LOUDNESS_DICHOTIC_FREE_MIC, + EXP_BIN_LOUDNESS_LEVEL_DICHOTIC_FREE_MIC, + EXP_MON_LOUDNESS_DICHOTIC_FREE_MIC_LEFT, + EXP_MON_LOUDNESS_DICHOTIC_FREE_MIC_RIGHT, + EXP_BIN_SPECIFIC_LOUDNESS_DICHOTIC_FREE_MIC_0, + EXP_BIN_SPECIFIC_LOUDNESS_DICHOTIC_FREE_MIC_45, + EXP_BIN_SPECIFIC_LOUDNESS_DICHOTIC_FREE_MIC_98, + ), + ), + ( + "Diffuse", + "Mic", + ( + EXP_BIN_LOUDNESS_DICHOTIC_DIFFUSE_MIC, + EXP_BIN_LOUDNESS_LEVEL_DICHOTIC_DIFFUSE_MIC, + EXP_MON_LOUDNESS_DICHOTIC_DIFFUSE_MIC_LEFT, + EXP_MON_LOUDNESS_DICHOTIC_DIFFUSE_MIC_RIGHT, + EXP_BIN_SPECIFIC_LOUDNESS_DICHOTIC_DIFFUSE_MIC_0, + EXP_BIN_SPECIFIC_LOUDNESS_DICHOTIC_DIFFUSE_MIC_45, + EXP_BIN_SPECIFIC_LOUDNESS_DICHOTIC_DIFFUSE_MIC_98, + ), + ), + ( + "Free", + "Head", + ( + EXP_BIN_LOUDNESS_DICHOTIC_HEAD, + EXP_BIN_LOUDNESS_LEVEL_DICHOTIC_HEAD, + EXP_MON_LOUDNESS_DICHOTIC_HEAD_LEFT, + EXP_MON_LOUDNESS_DICHOTIC_HEAD_RIGHT, + EXP_BIN_SPECIFIC_LOUDNESS_DICHOTIC_HEAD_0, + EXP_BIN_SPECIFIC_LOUDNESS_DICHOTIC_HEAD_45, + EXP_BIN_SPECIFIC_LOUDNESS_DICHOTIC_HEAD_98, + ), + ), + ( + "Diffuse", + "Head", + ( + EXP_BIN_LOUDNESS_DICHOTIC_HEAD, + EXP_BIN_LOUDNESS_LEVEL_DICHOTIC_HEAD, + EXP_MON_LOUDNESS_DICHOTIC_HEAD_LEFT, + EXP_MON_LOUDNESS_DICHOTIC_HEAD_RIGHT, + EXP_BIN_SPECIFIC_LOUDNESS_DICHOTIC_HEAD_0, + EXP_BIN_SPECIFIC_LOUDNESS_DICHOTIC_HEAD_45, + EXP_BIN_SPECIFIC_LOUDNESS_DICHOTIC_HEAD_98, + ), + ), + ], +) +def test_loudness_iso_532_2_get_output_dichotic_case(field_type, recording_type, exp_values): + """Test the get_output method of the LoudnessISO532_2 class, in the dichotic case.""" + loudness_computer = LoudnessISO532_2() + + wav_loader = LoadWav(pytest.data_path_Acceleration_stereo_nonUnitaryCalib) + wav_loader.process() + fc = wav_loader.get_output() + + loudness_computer.signal = fc + + loudness_computer.field_type = field_type + loudness_computer.recording_type = recording_type + + loudness_computer.process() + + N_bin, LN_bin, N_mon, _, Nprime_bin, _ = loudness_computer.get_output() + + assert N_bin == pytest.approx(exp_values[0]) + assert LN_bin == pytest.approx(exp_values[1]) + assert N_mon[0] == pytest.approx(exp_values[2]) + assert N_mon[1] == pytest.approx(exp_values[3]) + assert len(Nprime_bin) == EXP_ERB_LEN + assert Nprime_bin.data[0] == pytest.approx(exp_values[4]) + assert Nprime_bin.data[45] == pytest.approx(exp_values[5]) + assert Nprime_bin.data[98] == pytest.approx(exp_values[6]) + + +def test_loudness_iso_532_2_get_output_monaural_outputs(): + """Test the get_output method of the LoudnessISO532_2 class for some other monaural outputs. + + Here we test some other monaural outputs, but without repeating the tests for all cases of + field and recording types. + """ + loudness_computer = LoudnessISO532_2() + + wav_loader = LoadWav(pytest.data_path_Acceleration_stereo_nonUnitaryCalib) + wav_loader.process() + fc = wav_loader.get_output() + + loudness_computer.signal = fc + + loudness_computer.process() + + _, _, _, LN_mon, _, Nprime_mon = loudness_computer.get_output() + assert LN_mon[0] == pytest.approx(EXP_MON_LOUDNESS_LEVEL_DICHOTIC_FREE_MIC_LEFT) + assert LN_mon[1] == pytest.approx(EXP_MON_LOUDNESS_LEVEL_DICHOTIC_FREE_MIC_RIGHT) + assert len(Nprime_mon[0]) == EXP_ERB_LEN + assert Nprime_mon[0].data[0] == pytest.approx(EXP_MON_SPECIFIC_LOUDNESS_DICHOTIC_FREE_MIC_L_0) + assert Nprime_mon[0].data[45] == pytest.approx(EXP_MON_SPECIFIC_LOUDNESS_DICHOTIC_FREE_MIC_L_45) + assert Nprime_mon[0].data[98] == pytest.approx(EXP_MON_SPECIFIC_LOUDNESS_DICHOTIC_FREE_MIC_L_98) + assert len(Nprime_mon[1]) == EXP_ERB_LEN + assert Nprime_mon[1].data[0] == pytest.approx(EXP_MON_SPECIFIC_LOUDNESS_DICHOTIC_FREE_MIC_R_0) + assert Nprime_mon[1].data[45] == pytest.approx(EXP_MON_SPECIFIC_LOUDNESS_DICHOTIC_FREE_MIC_R_45) + assert Nprime_mon[1].data[98] == pytest.approx(EXP_MON_SPECIFIC_LOUDNESS_DICHOTIC_FREE_MIC_R_98) + + +def test_loudness_iso_532_2_get_output_warning(): + """Test the get_output method's warning of the LoudnessISO532_2 class.""" + loudness_computer = LoudnessISO532_2() + + wav_loader = LoadWav(pytest.data_path_flute_nonUnitaryCalib_in_container) + wav_loader.process() + fc = wav_loader.get_output() + + loudness_computer.signal = fc[0] + + # Loudness not calculated yet -> warning + with pytest.warns( + PyAnsysSoundWarning, + match=("Output is not processed yet. " "Use the `LoudnessISO532_2.process\\(\\)` method."), + ): + output = loudness_computer.get_output() + assert output is None + + +def test_loudness_iso_532_2_get_output_as_nparray(): + """Test the get_output_as_nparray method of the LoudnessISO532_2 class.""" + loudness_computer = LoudnessISO532_2() + + wav_loader = LoadWav(pytest.data_path_flute_nonUnitaryCalib_in_container) + wav_loader.process() + fc = wav_loader.get_output() + + loudness_computer.signal = fc[0] + + # Loudness not calculated yet -> warning + with pytest.warns( + PyAnsysSoundWarning, + match=("Output is not processed yet. " "Use the `LoudnessISO532_2.process\\(\\)` method."), + ): + N_bin, LN_bin, N_mon, LN_mon, Nprime_bin, Nprime_mon, fc = ( + loudness_computer.get_output_as_nparray() + ) + assert np.isnan(N_bin) + assert np.isnan(LN_bin) + assert len(N_mon) == 0 + assert len(LN_mon) == 0 + assert len(Nprime_bin) == 0 + assert len(Nprime_mon) == 0 + assert len(fc) == 0 + + loudness_computer.process() + + N_bin, LN_bin, N_mon, LN_mon, Nprime_bin, Nprime_mon, fc = ( + loudness_computer.get_output_as_nparray() + ) + assert N_bin == pytest.approx(EXP_BIN_LOUDNESS_DIOTIC_FREE_MIC) + assert LN_bin == pytest.approx(EXP_BIN_LOUDNESS_LEVEL_DIOTIC_FREE_MIC) + assert len(N_mon) == 1 + assert N_mon[0] == pytest.approx(EXP_MON_LOUDNESS_DIOTIC_FREE_MIC) + assert len(LN_mon) == 1 + assert LN_mon[0] == pytest.approx(EXP_MON_LOUDNESS_LEVEL_DIOTIC_FREE_MIC) + assert len(Nprime_bin) == EXP_ERB_LEN + assert Nprime_bin[0] == pytest.approx(EXP_BIN_SPECIFIC_LOUDNESS_DIOTIC_FREE_MIC_0) + assert Nprime_bin[45] == pytest.approx(EXP_BIN_SPECIFIC_LOUDNESS_DIOTIC_FREE_MIC_45) + assert Nprime_bin[98] == pytest.approx(EXP_BIN_SPECIFIC_LOUDNESS_DIOTIC_FREE_MIC_98) + assert len(Nprime_mon) == EXP_ERB_LEN + assert Nprime_mon[0] == pytest.approx(EXP_MON_SPECIFIC_LOUDNESS_DIOTIC_FREE_MIC_0) + assert Nprime_mon[45] == pytest.approx(EXP_MON_SPECIFIC_LOUDNESS_DIOTIC_FREE_MIC_45) + assert Nprime_mon[98] == pytest.approx(EXP_MON_SPECIFIC_LOUDNESS_DIOTIC_FREE_MIC_98) + assert len(fc) == 372 + assert fc[0] == pytest.approx(EXP_FREQ_0) + assert fc[45] == pytest.approx(EXP_FREQ_45) + assert fc[98] == pytest.approx(EXP_FREQ_98) + + +def test_loudness_iso_532_2_get_binaural_loudness_sone(): + """Test the get_binaural_loudness_sone method of the LoudnessISO532_2 class.""" + loudness_computer = LoudnessISO532_2() + + wav_loader = LoadWav(pytest.data_path_flute_nonUnitaryCalib_in_container) + wav_loader.process() + fc = wav_loader.get_output() + + loudness_computer.signal = fc[0] + + loudness_computer.process() + + N_bin = loudness_computer.get_binaural_loudness_sone() + assert N_bin == pytest.approx(EXP_BIN_LOUDNESS_DIOTIC_FREE_MIC) + + +def test_loudness_iso_532_2_get_binaural_loudness_level_phon(): + """Test the get_binaural_loudness_level_phon method of the LoudnessISO532_2 class.""" + loudness_computer = LoudnessISO532_2() + + wav_loader = LoadWav(pytest.data_path_flute_nonUnitaryCalib_in_container) + wav_loader.process() + fc = wav_loader.get_output() + + loudness_computer.signal = fc[0] + + loudness_computer.process() + + LN_bin = loudness_computer.get_binaural_loudness_level_phon() + assert LN_bin == pytest.approx(EXP_BIN_LOUDNESS_LEVEL_DIOTIC_FREE_MIC) + + +def test_loudness_iso_532_2_get_monaural_loudness_sone(): + """Test the get_monaural_loudness_sone method of the LoudnessISO532_2 class.""" + loudness_computer = LoudnessISO532_2() + + wav_loader = LoadWav(pytest.data_path_flute_nonUnitaryCalib_in_container) + wav_loader.process() + fc = wav_loader.get_output() + + loudness_computer.signal = fc[0] + + loudness_computer.process() + + N_mon = loudness_computer.get_monaural_loudness_sone() + assert len(N_mon) == 2 + assert N_mon[0] == pytest.approx(EXP_MON_LOUDNESS_DIOTIC_FREE_MIC) + assert N_mon[1] == pytest.approx(EXP_MON_LOUDNESS_DIOTIC_FREE_MIC) + + wav_loader.path_to_wav = pytest.data_path_Acceleration_stereo_nonUnitaryCalib + wav_loader.process() + fc = wav_loader.get_output() + + loudness_computer.signal = fc + + loudness_computer.process() + + N_mon = loudness_computer.get_monaural_loudness_sone() + assert len(N_mon) == 2 + assert N_mon[0] == pytest.approx(EXP_MON_LOUDNESS_DICHOTIC_FREE_MIC_LEFT) + assert N_mon[1] == pytest.approx(EXP_MON_LOUDNESS_DICHOTIC_FREE_MIC_RIGHT) + + +def test_loudness_iso_532_2_get_monaural_loudness_level_phon(): + """Test the get_monaural_loudness_level_phon method of the LoudnessISO532_2 class.""" + loudness_computer = LoudnessISO532_2() + + wav_loader = LoadWav(pytest.data_path_flute_nonUnitaryCalib_in_container) + wav_loader.process() + fc = wav_loader.get_output() + + loudness_computer.signal = fc[0] + + loudness_computer.process() + + LN_mon = loudness_computer.get_monaural_loudness_level_phon() + assert len(LN_mon) == 2 + assert LN_mon[0] == pytest.approx(EXP_MON_LOUDNESS_LEVEL_DIOTIC_FREE_MIC) + assert LN_mon[1] == pytest.approx(EXP_MON_LOUDNESS_LEVEL_DIOTIC_FREE_MIC) + + wav_loader.path_to_wav = pytest.data_path_Acceleration_stereo_nonUnitaryCalib + wav_loader.process() + fc = wav_loader.get_output() + + loudness_computer.signal = fc + + loudness_computer.process() + + LN_mon = loudness_computer.get_monaural_loudness_level_phon() + assert len(LN_mon) == 2 + assert LN_mon[0] == pytest.approx(EXP_MON_LOUDNESS_LEVEL_DICHOTIC_FREE_MIC_LEFT) + assert LN_mon[1] == pytest.approx(EXP_MON_LOUDNESS_LEVEL_DICHOTIC_FREE_MIC_RIGHT) + + +def test_loudness_iso_532_2_get_binaural_specific_loudness(): + """Test the get_binaural_specific_loudness method of the LoudnessISO532_2 class.""" + loudness_computer = LoudnessISO532_2() + + wav_loader = LoadWav(pytest.data_path_flute_nonUnitaryCalib_in_container) + wav_loader.process() + fc = wav_loader.get_output() + + loudness_computer.signal = fc[0] + + loudness_computer.process() + + Nprime_bin = loudness_computer.get_binaural_specific_loudness() + assert len(Nprime_bin) == EXP_ERB_LEN + assert Nprime_bin[0] == pytest.approx(EXP_BIN_SPECIFIC_LOUDNESS_DIOTIC_FREE_MIC_0) + assert Nprime_bin[45] == pytest.approx(EXP_BIN_SPECIFIC_LOUDNESS_DIOTIC_FREE_MIC_45) + assert Nprime_bin[98] == pytest.approx(EXP_BIN_SPECIFIC_LOUDNESS_DIOTIC_FREE_MIC_98) + + +def test_loudness_iso_532_2_get_monaural_specific_loudness(): + """Test the get_specific_loudness_sone method of the LoudnessISO532_2 class.""" + loudness_computer = LoudnessISO532_2() + + wav_loader = LoadWav(pytest.data_path_flute_nonUnitaryCalib_in_container) + wav_loader.process() + fc = wav_loader.get_output() + + loudness_computer.signal = fc[0] + + loudness_computer.process() + + Nprime_mon = loudness_computer.get_monaural_specific_loudness() + assert len(Nprime_mon) == 2 + assert len(Nprime_mon[0]) == EXP_ERB_LEN + assert Nprime_mon[0][0] == pytest.approx(EXP_MON_SPECIFIC_LOUDNESS_DIOTIC_FREE_MIC_0) + assert Nprime_mon[0][45] == pytest.approx(EXP_MON_SPECIFIC_LOUDNESS_DIOTIC_FREE_MIC_45) + assert Nprime_mon[0][98] == pytest.approx(EXP_MON_SPECIFIC_LOUDNESS_DIOTIC_FREE_MIC_98) + + wav_loader.path_to_wav = pytest.data_path_Acceleration_stereo_nonUnitaryCalib + wav_loader.process() + fc = wav_loader.get_output() + + loudness_computer.signal = fc + + loudness_computer.process() + + Nprime_mon = loudness_computer.get_monaural_specific_loudness() + assert len(Nprime_mon) == 2 + assert len(Nprime_mon[0]) == EXP_ERB_LEN + assert Nprime_mon[0][0] == pytest.approx(EXP_MON_SPECIFIC_LOUDNESS_DICHOTIC_FREE_MIC_L_0) + assert Nprime_mon[0][45] == pytest.approx(EXP_MON_SPECIFIC_LOUDNESS_DICHOTIC_FREE_MIC_L_45) + assert Nprime_mon[0][98] == pytest.approx(EXP_MON_SPECIFIC_LOUDNESS_DICHOTIC_FREE_MIC_L_98) + assert len(Nprime_mon[1]) == EXP_ERB_LEN + assert Nprime_mon[1][0] == pytest.approx(EXP_MON_SPECIFIC_LOUDNESS_DICHOTIC_FREE_MIC_R_0) + assert Nprime_mon[1][45] == pytest.approx(EXP_MON_SPECIFIC_LOUDNESS_DICHOTIC_FREE_MIC_R_45) + assert Nprime_mon[1][98] == pytest.approx(EXP_MON_SPECIFIC_LOUDNESS_DICHOTIC_FREE_MIC_R_98) + + +def test_loudness_iso_532_2_get_erb_center_frequencies(): + """Test the get_erb_center_frequencies method of the LoudnessISO532_2 class.""" + loudness_computer = LoudnessISO532_2() + + wav_loader = LoadWav(pytest.data_path_flute_nonUnitaryCalib_in_container) + wav_loader.process() + fc = wav_loader.get_output() + + loudness_computer.signal = fc[0] + + loudness_computer.process() + + fc = loudness_computer.get_erb_center_frequencies() + assert len(fc) == EXP_ERB_LEN + assert fc[0] == pytest.approx(EXP_FREQ_0) + assert fc[45] == pytest.approx(EXP_FREQ_45) + assert fc[98] == pytest.approx(EXP_FREQ_98) + + +def test_loudness_iso_532_2_get_erbn_numbers(): + """Test the get_erbn_numbers method of the LoudnessISO532_2 class.""" + loudness_computer = LoudnessISO532_2() + + wav_loader = LoadWav(pytest.data_path_flute_nonUnitaryCalib_in_container) + wav_loader.process() + fc = wav_loader.get_output() + + loudness_computer.signal = fc[0] + + loudness_computer.process() + + erb = loudness_computer.get_erbn_numbers() + assert len(erb) == EXP_ERB_LEN + assert erb[0] == pytest.approx(EXP_ERB_0) + assert erb[45] == pytest.approx(EXP_ERB_45) + assert erb[98] == pytest.approx(EXP_ERB_98) + + +@patch("matplotlib.pyplot.show") +def test_loudness_iso_532_2_plot(mock_show): + """Test the plot method of the LoudnessISO532_2 class.""" + loudness_computer = LoudnessISO532_2() + + wav_loader = LoadWav(pytest.data_path_flute_nonUnitaryCalib_in_container) + wav_loader.process() + fc = wav_loader.get_output() + + loudness_computer.signal = fc[0] + + loudness_computer.process() + + loudness_computer.plot() + + +def test_loudness_iso_532_2_plot_exceptions(): + """Test the exceptions of the plot method of the LoudnessISO532_2 class.""" + loudness_computer = LoudnessISO532_2() + + with pytest.raises( + PyAnsysSoundException, + match="Output is not processed yet. Use the `LoudnessISO532_2.process\\(\\)` method.", + ): + loudness_computer.plot()