From 3fa7b353544e6af72385b440e560c995139a98fb Mon Sep 17 00:00:00 2001 From: Antoine Minard <165933065+ansaminard@users.noreply.github.com> Date: Wed, 11 Dec 2024 09:17:18 +0100 Subject: [PATCH] feat: source broadband noise for sound composer (#172) Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> --- doc/changelog.d/172.added.md | 1 + doc/source/api/sound_composer.rst | 1 + .../sound/core/sound_composer/__init__.py | 4 +- .../sound_composer/source_broadband_noise.py | 336 +++++++++++++ .../core/sound_composer/source_spectrum.py | 4 +- tests/conftest.py | 6 + .../AnsysSound_BBN dBSPL OCTAVE Constants.txt | 14 + ...NARROWBAND v2_40values_2024R2_20241128.txt | 25 + ...t_sound_composer_source_broadband_noise.py | 456 ++++++++++++++++++ .../test_sound_composer_source_spectrum.py | 2 +- 10 files changed, 845 insertions(+), 4 deletions(-) create mode 100644 doc/changelog.d/172.added.md create mode 100644 src/ansys/sound/core/sound_composer/source_broadband_noise.py create mode 100644 tests/data/AnsysSound_BBN dBSPL OCTAVE Constants.txt create mode 100644 tests/data/AnsysSound_BBN dBSPLperHz NARROWBAND v2_40values_2024R2_20241128.txt create mode 100644 tests/tests_sound_composer/test_sound_composer_source_broadband_noise.py diff --git a/doc/changelog.d/172.added.md b/doc/changelog.d/172.added.md new file mode 100644 index 000000000..381bb912e --- /dev/null +++ b/doc/changelog.d/172.added.md @@ -0,0 +1 @@ +feat: source broadband noise for sound composer \ No newline at end of file diff --git a/doc/source/api/sound_composer.rst b/doc/source/api/sound_composer.rst index 739a4692d..d67f36943 100644 --- a/doc/source/api/sound_composer.rst +++ b/doc/source/api/sound_composer.rst @@ -7,6 +7,7 @@ Sound composer :toctree: _autosummary SourceSpectrum + SourceBroadbandNoise SourceAudio SourceControlSpectrum SourceControlTime diff --git a/src/ansys/sound/core/sound_composer/__init__.py b/src/ansys/sound/core/sound_composer/__init__.py index 6482d92bf..cae58e491 100644 --- a/src/ansys/sound/core/sound_composer/__init__.py +++ b/src/ansys/sound/core/sound_composer/__init__.py @@ -29,6 +29,7 @@ from ._source_control_parent import SourceControlParent, SpectrumSynthesisMethods from ._source_parent import SourceParent from .source_audio import SourceAudio +from .source_broadband_noise import SourceBroadbandNoise from .source_control_spectrum import SourceControlSpectrum from .source_control_time import SourceControlTime from .source_spectrum import SourceSpectrum @@ -40,6 +41,7 @@ "SpectrumSynthesisMethods", "SourceSpectrum", "SourceControlSpectrum", - "SourceAudio", + "SourceBroadbandNoise", "SourceControlTime", + "SourceAudio", ) diff --git a/src/ansys/sound/core/sound_composer/source_broadband_noise.py b/src/ansys/sound/core/sound_composer/source_broadband_noise.py new file mode 100644 index 000000000..f01f2abe3 --- /dev/null +++ b/src/ansys/sound/core/sound_composer/source_broadband_noise.py @@ -0,0 +1,336 @@ +# Copyright (C) 2023 - 2024 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. + +"""Sound Composer's broadband noise source.""" +import warnings + +from ansys.dpf.core import Field, FieldsContainer, Operator +from matplotlib import pyplot as plt +import numpy as np + +from .._pyansys_sound import PyAnsysSoundException, PyAnsysSoundWarning +from ._source_parent import SourceParent +from .source_control_time import SourceControlTime + +ID_COMPUTE_LOAD_SOURCE_BBN = "sound_composer_load_source_bbn" +ID_COMPUTE_GENERATE_SOUND_BBN = "sound_composer_generate_sound_bbn" + + +class SourceBroadbandNoise(SourceParent): + """Sound Composer's broadband noise source class. + + This class creates a broadband noise source for the Sound Composer. A broadband noise source is + used to generate a sound signal from a given broadband noise and its source control. The + broadband noise consists of a series of noise spectra, each corresponding to a control + parameter value. The source control contains the control parameter values over time. + """ + + def __init__(self, file: str = "", source_control: SourceControlTime = None): + """Class instantiation takes the following parameters. + + Parameters + ---------- + file : str, default: "" + Path to the broadband noise file. Supported files are text files with the header + `AnsysSound_BBN`. If left empty, the broadband noise source is not loaded. + source_control : SourceControlTime, default: None + Source control, consisting of the control parameter values over time, to use when + generating the sound from this source. + """ + super().__init__() + self.source_control = source_control + + # Define DPF Sound operators. + self.__operator_load = Operator(ID_COMPUTE_LOAD_SOURCE_BBN) + self.__operator_generate = Operator(ID_COMPUTE_GENERATE_SOUND_BBN) + + if len(file) > 0: + self.load_source_bbn(file) + else: + self.source_bbn = None + + def __str__(self) -> str: + """Return the string representation of the object.""" + # Source info. + if self.source_bbn is not None: + spectrum_type, delta_f, control_name, control_unit, control_values = ( + self.__extract_bbn_info() + ) + + # Source name. + str_name = self.source_bbn.name + if str_name is None: + str_name = "" + + # Spectrum type. TODO: reactivate if/else when quantity_type is available in python. + # if spectrum_type == "NARROWBAND": + # str_type = f"{spectrum_type} (DeltaF: {delta_f:.1f} Hz)" + # else: + # str_type = spectrum_type + str_type = spectrum_type + + # Spectrum control values. + control_values = np.round(control_values, 1) + if len(control_values) > 10: + str_values = f"{str(control_values[:5])[:-1]} ... {str(control_values[-5:])[1:]}" + else: + str_values = str(control_values) + + str_source = ( + f"'{str_name}'\n" + f"\tSpectrum type: {str_type}\n" + f"\tSpectrum count: {len(self.source_bbn)}\n" + f"\tControl parameter: {control_name}, {control_unit}\n" + f"\t\t{str_values}" + ) + else: + str_source = "Not set" + + # Source control info. + if self.is_source_control_valid(): + str_source_control = ( + f"{self.source_control.control.name}\n" + f"\tMin: {self.source_control.control.data.min()}\n" + f"\tMax: {self.source_control.control.data.max()}\n" + f"\tDuration: " + f"{self.source_control.control.time_freq_support.time_frequencies.data[-1]} s" + ) + else: + str_source_control = "Not set" + + return f"Broadband noise source: {str_source}\nSource control: {str_source_control}" + + @property + def source_control(self) -> SourceControlTime: + """Broadband noise source control. + + Contains the control parameter values over time. + """ + return self.__source_control + + @source_control.setter + def source_control(self, source_control: SourceControlTime): + """Set the source control.""" + if not (isinstance(source_control, SourceControlTime) or source_control is None): + raise PyAnsysSoundException( + "Specified source control object must be of type ``SourceControlTime``." + ) + self.__source_control = source_control + + @property + def source_bbn(self) -> FieldsContainer: + """Broadband noise source data, as a DPF fields container. + + The broadband noise source data consists of a series of spectra, each corresponding to a + control parameter value. Spectra can be narrowband (in dB or dB/Hz), octave-band levels, or + 1/3-octave-band levels. + """ + return self.__source_bbn + + @source_bbn.setter + def source_bbn(self, source_bbn: FieldsContainer): + """Set the broadband noise source data, from a DPF fields container.""" + if source_bbn is not None: + if not isinstance(source_bbn, FieldsContainer): + raise PyAnsysSoundException( + "Specified broadband noise source must be provided as a DPF fields container." + ) + + if len(source_bbn) < 1: + raise PyAnsysSoundException( + "Specified broadband noise source must contain at least one spectrum." + ) + + for spectrum in source_bbn: + if len(spectrum.data) < 1: + raise PyAnsysSoundException( + "Each spectrum in the specified broadband noise source must contain at " + "least one element." + ) + + support_data = source_bbn.get_support("control_parameter_1") + support_properties = support_data.available_field_supported_properties() + support_values = support_data.field_support_by_property(support_properties[0]) + if len(support_values) != len(source_bbn): + raise PyAnsysSoundException( + "Broadband noise source must contain as many spectra as the number of values " + "in the associated control parameter (in the provided DPF fields container, " + "the number of fields should be the same as the number of values in the fields " + "container support)." + ) + + self.__source_bbn = source_bbn + + def is_source_control_valid(self) -> bool: + """Source control verification function. + + Check if the source control is set. + + Returns + ------- + bool + True if the source control is set. + """ + return self.source_control is not None and self.source_control.control is not None + + def load_source_bbn(self, file: str): + """Load the broadband noise source data from a file. + + Parameters + ---------- + file : str + Path to the broadband noise file. Supported files have the same text format (with the + `AnsysSound_BBN` header) as that which is supported by Ansys Sound SAS. + """ + # Set operator inputs. + self.__operator_load.connect(0, file) + + # Run the operator. + self.__operator_load.run() + + # Get the loaded sound power level parameters. + self.source_bbn = self.__operator_load.get_output(0, "fields_container") + + def process(self, sampling_frequency: float = 44100.0): + """Generate the sound of the broadband noise source. + + This method generates the sound of the broadband noise source, using the current broadband + noise data and source control. + + Parameters + ---------- + sampling_frequency : float, default: 44100.0 + Sampling frequency of the generated sound in Hz. + """ + if sampling_frequency <= 0.0: + raise PyAnsysSoundException("Sampling frequency must be strictly positive.") + + if not self.is_source_control_valid(): + raise PyAnsysSoundException( + "Broadband noise source control is not set. " + f"Use ``{__class__.__name__}.source_control``." + ) + + if self.source_bbn is None: + raise PyAnsysSoundException( + f"Broadband noise source data is not set. Use ``{__class__.__name__}.source_bbn`` " + f"or method ``{__class__.__name__}.load_source_bbn()``." + ) + + # Set operator inputs. + self.__operator_generate.connect(0, self.source_bbn) + self.__operator_generate.connect(1, self.source_control.control) + self.__operator_generate.connect(2, sampling_frequency) + + # Run the operator. + self.__operator_generate.run() + + # Get the loaded sound power level parameters. + self._output = self.__operator_generate.get_output(0, "field") + + def get_output(self) -> Field: + """Get the generated sound as a DPF field. + + Returns + ------- + Field + Generated sound as a DPF field. + """ + if self._output == None: + warnings.warn( + PyAnsysSoundWarning( + "Output is not processed yet. " + f"Use the ``{__class__.__name__}.process()`` method." + ) + ) + return self._output + + def get_output_as_nparray(self) -> np.ndarray: + """Get the generated sound as a NumPy array. + + Returns + ------- + numpy.ndarray + Generated sound (signal samples in Pa) as a NumPy array. + """ + output = self.get_output() + + return np.array(output.data if output is not None else []) + + def plot(self): + """Plot the resulting signal in a figure.""" + if self._output == None: + raise PyAnsysSoundException( + f"Output is not processed yet. Use the '{__class__.__name__}.process()' method." + ) + output = self.get_output() + + time_data = output.time_freq_support.time_frequencies.data + + plt.plot(time_data, output.data) + plt.title(output.name if len(output.name) > 0 else "Signal from broadband noise source") + plt.xlabel("Time (s)") + plt.ylabel("Amplitude (Pa)") + plt.grid(True) + plt.show() + + def __extract_bbn_info(self) -> tuple[str, float, str, str, list[float]]: + """Extract the broadband noise source information. + + Returns + ------- + tuple[str, float, str, str, list[float]] + Broadband noise source information, consisting of the following elements: + First element is the spectrum type ('NARROWBAND', 'OCTAVE1:1', or 'OCTAVE1:3'). + + Second element is the spectrum frequency resolution in Hz (only if spectrum type is + 'NARROWBAND', 0.0 otherwise). + + Third element is the control parameter name. + + Sixth element is the control parameter unit. + + Seventh element is the control parameter values. + """ + if self.source_bbn is None: + return ("", 0.0, "", "", []) + + # Spectrum info. + # TODO: for now quantity_type can't be accessed in python. When it is, the line below + # should be uncommented, and replace the one after. + # spectrum_type = self.source_bbn[0].field_definition.quantity_type + spectrum_type = "Not available" + frequencies = self.source_bbn[0].time_freq_support.time_frequencies.data + if len(frequencies) > 1: + delta_f = frequencies[1] - frequencies[0] + else: + delta_f = 0.0 + + # Control parameter info. + support_ids = list(self.source_bbn.get_label_space(0).keys()) + control_data = self.source_bbn.get_support(support_ids[0]) + parameter_ids = control_data.available_field_supported_properties() + control_unit = parameter_ids[0] + control_name = control_data.field_support_by_property(control_unit).name + control_values = list(control_data.field_support_by_property(control_unit).data) + + return spectrum_type, delta_f, control_name, control_unit, control_values diff --git a/src/ansys/sound/core/sound_composer/source_spectrum.py b/src/ansys/sound/core/sound_composer/source_spectrum.py index 199972578..e56735504 100644 --- a/src/ansys/sound/core/sound_composer/source_spectrum.py +++ b/src/ansys/sound/core/sound_composer/source_spectrum.py @@ -240,14 +240,14 @@ def plot(self): """Plot the resulting signal in a figure.""" if self._output == None: raise PyAnsysSoundException( - "Output is not processed yet. Use the 'SourceSpectrum.process()' method." + f"Output is not processed yet. Use the '{__class__.__name__}.process()' method." ) output = self.get_output() time_data = output.time_freq_support.time_frequencies.data plt.plot(time_data, output.data) - plt.title("Signal from spectrum source") + plt.title(output.name if len(output.name) > 0 else "Signal from spectrum source") plt.xlabel("Time (s)") plt.ylabel("Amplitude (Pa)") plt.grid(True) diff --git a/tests/conftest.py b/tests/conftest.py index 240b2937b..5ae3d7d6e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -62,6 +62,12 @@ def pytest_configure(): ) pytest.data_path_rpm_profile_as_wav_in_container = "C:\\data\\RPM_profile_2024R2_20241126.wav" pytest.data_path_rpm_profile_as_txt_in_container = "C:\\data\\RPM_profile_2024R2_20241126.txt" + pytest.data_path_sound_composer_bbn_source_in_container = ( + "C:\\data\\AnsysSound_BBN dBSPL OCTAVE Constants.txt" + ) + pytest.data_path_sound_composer_bbn_source_40_values_in_container = ( + "C:\\data\\AnsysSound_BBN dBSPLperHz NARROWBAND v2_40values_2024R2_20241128.txt" + ) @pytest.fixture(scope="session") diff --git a/tests/data/AnsysSound_BBN dBSPL OCTAVE Constants.txt b/tests/data/AnsysSound_BBN dBSPL OCTAVE Constants.txt new file mode 100644 index 000000000..1459e9c0a --- /dev/null +++ b/tests/data/AnsysSound_BBN dBSPL OCTAVE Constants.txt @@ -0,0 +1,14 @@ +AnsysSound_BBN 2 +dBSPL OCTAVE1:1 +"Speed of wind" +"m/s" 1 2 5.3 10.5 27.778 +32 41 41 41 41 41 +63 41 41 41 41 41 +125 41 41 41 41 41 +250 41 41 41 41 41 +500 41 41 41 41 41 +1000 41 41 41 41 41 +2000 41 41 41 41 41 +4000 41 41 41 41 41 +8000 41 41 41 41 41 +16000 41 41 41 41 41 diff --git a/tests/data/AnsysSound_BBN dBSPLperHz NARROWBAND v2_40values_2024R2_20241128.txt b/tests/data/AnsysSound_BBN dBSPLperHz NARROWBAND v2_40values_2024R2_20241128.txt new file mode 100644 index 000000000..65178dc16 --- /dev/null +++ b/tests/data/AnsysSound_BBN dBSPLperHz NARROWBAND v2_40values_2024R2_20241128.txt @@ -0,0 +1,25 @@ +AnsysSound_BBN 2 +dBSPL/Hz NARROWBAND +"Speed of wind" +"m/s" 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 +0 41.21603362 47.52701254 36.06128595 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 45.8003136 +10 42.93945075 47.0708236 38.68769626 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 47.48655183 +20 44.82649589 47.56715844 41.39835452 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 49.32089886 +30 46.8692157 49.32860282 44.16070719 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 51.31474922 +40 49.33045728 52.25354777 47.25466204 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 53.67350099 +50 52.36549207 56.0807908 50.82499308 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 56.60580539 +60 56.22576795 60.77962391 55.0635153 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 60.32876147 +70 62.63715434 67.87776267 61.79138396 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 66.5601447 +80 73.87458909 79.65403526 73.24339818 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 77.5784609 +90 63.47679683 69.61241841 62.93428805 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 66.90519794 +100 58.59715194 64.89646591 57.97949768 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 61.68138968 +110 56.1001839 62.26141762 55.16199337 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 58.74016148 +120 54.57625997 60.46300715 53.1622226 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 56.52074411 +130 53.91157713 59.25125519 51.93979964 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 55.34244173 +140 54.10997024 58.13375122 50.80938005 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 54.81487034 +150 55.49287902 56.87660446 49.69211747 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 55.32395755 +160 60.59947466 53.07667186 48.14088021 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 59.57354639 +170 58.18953195 59.90786389 52.6697731 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 58.06071394 +180 52.10287814 58.10637407 50.51704724 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 52.7059746 +190 53.25691252 57.66810649 50.06821359 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 53.61619791 +200 56.88392507 57.7715642 49.95132201 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 56.30426054 diff --git a/tests/tests_sound_composer/test_sound_composer_source_broadband_noise.py b/tests/tests_sound_composer/test_sound_composer_source_broadband_noise.py new file mode 100644 index 000000000..c4962e4b6 --- /dev/null +++ b/tests/tests_sound_composer/test_sound_composer_source_broadband_noise.py @@ -0,0 +1,456 @@ +# Copyright (C) 2023 - 2024 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, + TimeFreqSupport, + fields_container_factory, + fields_factory, + locations, +) +import numpy as np +import pytest + +from ansys.sound.core._pyansys_sound import PyAnsysSoundException, PyAnsysSoundWarning +from ansys.sound.core.sound_composer import SourceBroadbandNoise, SourceControlTime +from ansys.sound.core.spectral_processing import PowerSpectralDensity + +REF_ACOUSTIC_POWER = 4e-10 + +EXP_LEVEL_OCTAVE_BAND = 41.0 +EXP_SPECTRUM_DATA03 = 5.0357002692180686e-06 +EXP_STR_NOT_SET = "Broadband noise source: Not set\nSource control: Not set" +EXP_STR_ALL_SET = ( + "Broadband noise source: ''\n" + "\tSpectrum type: Not available\n" + "\tSpectrum count: 5\n" + "\tControl parameter: Speed of wind, m/s\n" + "\t\t[ 1. 2. 5.3 10.5 27.8]" + "\nSource control: \n" + "\tMin: 1.0\n" + "\tMax: 10.0\n" + "\tDuration: 3.0 s" +) +EXP_STR_ALL_SET_40_VALUES = ( + "Broadband noise source: ''\n" + "\tSpectrum type: Not available\n" + "\tSpectrum count: 40\n" + "\tControl parameter: Speed of wind, m/s\n" + "\t\t[1. 2. 3. 4. 5. ... 36. 37. 38. 39. 40.]" + "\nSource control: \n" + "\tMin: 1.0\n" + "\tMax: 10.0\n" + "\tDuration: 3.0 s" +) + + +def test_source_broadband_noise_instantiation_no_arg(dpf_sound_test_server): + """Test SourceBroadbandNoise instantiation without arguments.""" + source_bbn_obj = SourceBroadbandNoise() + assert isinstance(source_bbn_obj, SourceBroadbandNoise) + assert source_bbn_obj.source_bbn is None + + +def test_source_broadband_noise_instantiation_file_arg(dpf_sound_test_server): + """Test SourceBroadbandNoise instantiation with file argument.""" + source_bbn_obj = SourceBroadbandNoise( + file=pytest.data_path_sound_composer_bbn_source_in_container + ) + assert isinstance(source_bbn_obj, SourceBroadbandNoise) + assert source_bbn_obj.source_bbn is not None + + +def test_source_broadband_noise___str___not_set(dpf_sound_test_server): + """Test SourceBroadbandNoise __str__ method when nothing is set.""" + source_bbn_obj = SourceBroadbandNoise() + assert str(source_bbn_obj) == EXP_STR_NOT_SET + + +def test_source_broadband_noise___str___all_set(dpf_sound_test_server): + """Test SourceBroadbandNoise __str__ method when all data are set.""" + # Create a field to use in a SourceControlTime object. + f_source_control = fields_factory.create_scalar_field( + num_entities=1, location=locations.time_freq + ) + f_source_control.append([1, 3, 6, 10], 1) + support = TimeFreqSupport() + f_time = fields_factory.create_scalar_field(num_entities=1, location=locations.time_freq) + f_time.append([0, 1, 2, 3], 1) + support.time_frequencies = f_time + f_source_control.time_freq_support = support + + # Create a SourceControlTime object. + source_control = SourceControlTime() + source_control.control = f_source_control + + # Create a SourceBroadbandNoise object using source file with less than 30 values and created + # source control. + source_bbn_obj = SourceBroadbandNoise( + pytest.data_path_sound_composer_bbn_source_in_container, + source_control, + ) + assert str(source_bbn_obj) == EXP_STR_ALL_SET + + # Replace source file with one with more than 10 values. + source_bbn_obj.load_source_bbn( + pytest.data_path_sound_composer_bbn_source_40_values_in_container + ) + assert str(source_bbn_obj) == EXP_STR_ALL_SET_40_VALUES + + +def test_source_broadband_noise_properties(dpf_sound_test_server): + """Test SourceBroadbandNoise properties.""" + source_bbn_obj = SourceBroadbandNoise() + + # Test source_control property. + source_bbn_obj.source_control = SourceControlTime() + assert isinstance(source_bbn_obj.source_control, SourceControlTime) + + # Test source_bbn property. + # Create a second object and then reuse its source_bbn property. + source_bbn_obj_tmp = SourceBroadbandNoise() + source_bbn_obj_tmp.load_source_bbn(pytest.data_path_sound_composer_bbn_source_in_container) + bbn_fieldscontainer = source_bbn_obj_tmp.source_bbn + source_bbn_obj.source_bbn = bbn_fieldscontainer + assert isinstance(source_bbn_obj.source_bbn, FieldsContainer) + + +def test_source_broadband_noise_properties_exceptions(dpf_sound_test_server): + """Test SourceBroadbandNoise properties' exceptions.""" + source_bbn_obj = SourceBroadbandNoise() + + # Test source_control setter exception (str instead of SourceControlTime). + with pytest.raises( + PyAnsysSoundException, + match="Specified source control object must be of type ``SourceControlTime``.", + ): + source_bbn_obj.source_control = "InvalidType" + + # Test source_bbn setter exception 1 (str instead a Field). + with pytest.raises( + PyAnsysSoundException, + match="Specified broadband noise source must be provided as a DPF fields container.", + ): + source_bbn_obj.source_bbn = "InvalidType" + + # Test source_bbn setter exception 2 (less than 1 spectrum). + fc_source_bbn = FieldsContainer() + with pytest.raises( + PyAnsysSoundException, + match="Specified broadband noise source must contain at least one spectrum.", + ): + source_bbn_obj.source_bbn = fc_source_bbn + + # Test source_bbn setter exception 3 (empty spectrum). + fc_source_bbn = fields_container_factory.over_time_freq_fields_container([Field()]) + with pytest.raises( + PyAnsysSoundException, + match=( + "Each spectrum in the specified broadband noise source must contain at least one " + "element." + ), + ): + source_bbn_obj.source_bbn = fc_source_bbn + + # Test source_bbn setter exception 4 (empty bbn source's control data). + # For this, we use a valid dataset, and then remove the control data. + source_bbn_obj = SourceBroadbandNoise() + source_bbn_obj.load_source_bbn(pytest.data_path_sound_composer_bbn_source_in_container) + support_data = source_bbn_obj.source_bbn.get_support("control_parameter_1") + support_properties = support_data.available_field_supported_properties() + support_values = support_data.field_support_by_property(support_properties[0]) + support_values.data = [] + fc_source_bbn = source_bbn_obj.source_bbn + with pytest.raises( + PyAnsysSoundException, + match=( + "Broadband noise source must contain as many spectra as the number of values in the " + "associated control parameter \\(in the provided DPF fields container, the number of " + "fields should be the same as the number of values in the fields container support\\)." + ), + ): + source_bbn_obj.source_bbn = fc_source_bbn + + +def test_source_broadband_noise_is_source_control_valid(dpf_sound_test_server): + """Test SourceBroadbandNoise is_source_control_valid method.""" + source_bbn_obj = SourceBroadbandNoise() + + # Test is_source_control_valid method (attribute not set). + assert source_bbn_obj.is_source_control_valid() is False + + # Test is_source_control_valid method (attribute set, but attribute's field not set). + source_control_obj = SourceControlTime() + source_bbn_obj.source_control = source_control_obj + assert source_bbn_obj.is_source_control_valid() is False + + # Test is_source_control_valid method (all set). + f_source_control = fields_factory.create_scalar_field( + num_entities=1, location=locations.time_freq + ) + f_source_control.append([1.0, 2.0, 3.0, 4.0, 5.0], 1) + source_bbn_obj.source_control.control = f_source_control + assert source_bbn_obj.is_source_control_valid() is True + + +def test_source_specrum_load_source_bbn(dpf_sound_test_server): + """Test SourceBroadbandNoise load_source_bbn method.""" + source_bbn_obj = SourceBroadbandNoise() + source_bbn_obj.load_source_bbn(pytest.data_path_sound_composer_bbn_source_in_container) + assert isinstance(source_bbn_obj.source_bbn, FieldsContainer) + assert source_bbn_obj.source_bbn[0].data[3] == pytest.approx(EXP_SPECTRUM_DATA03) + + +def test_source_broadband_noise_process(dpf_sound_test_server): + """Test SourceBroadbandNoise process method.""" + # Create a field to use in a SourceControlTime object. + f_source_control = fields_factory.create_scalar_field( + num_entities=1, location=locations.time_freq + ) + f_source_control.append([1, 3, 6, 10], 1) + support = TimeFreqSupport() + f_time = fields_factory.create_scalar_field(num_entities=1, location=locations.time_freq) + f_time.append([0, 1, 2, 3], 1) + support.time_frequencies = f_time + f_source_control.time_freq_support = support + + # Create a SourceControlTime object. + source_control_obj = SourceControlTime() + source_control_obj.control = f_source_control + + source_bbn_obj = SourceBroadbandNoise( + pytest.data_path_sound_composer_bbn_source_in_container, + source_control_obj, + ) + source_bbn_obj.process() + assert source_bbn_obj._output is not None + + +def test_source_broadband_noise_process_exceptions(dpf_sound_test_server): + """Test SourceBroadbandNoise process method exceptions.""" + # Test process method exception1 (missing control). + source_bbn_obj = SourceBroadbandNoise(pytest.data_path_sound_composer_bbn_source_in_container) + with pytest.raises( + PyAnsysSoundException, + match=( + "Broadband noise source control is not set. " + "Use ``SourceBroadbandNoise.source_control``." + ), + ): + source_bbn_obj.process() + + # Test process method exception2 (missing bbn source data). + source_bbn_obj.source_bbn = None + f_source_control = fields_factory.create_scalar_field( + num_entities=1, location=locations.time_freq + ) + f_source_control.append([1.0, 2.0, 3.0, 4.0, 5.0], 1) + source_control_obj = SourceControlTime() + source_control_obj.control = f_source_control + source_bbn_obj.source_control = source_control_obj + with pytest.raises( + PyAnsysSoundException, + match=( + "Broadband noise source data is not set. Use ``SourceBroadbandNoise.source_bbn`` " + "or method ``SourceBroadbandNoise.load_source_bbn\\(\\)``." + ), + ): + source_bbn_obj.process() + + # Test process method exception3 (invalid sampling frequency value). + source_bbn_obj.load_source_bbn(pytest.data_path_sound_composer_bbn_source_in_container) + with pytest.raises( + PyAnsysSoundException, match="Sampling frequency must be strictly positive." + ): + source_bbn_obj.process(sampling_frequency=0.0) + + +def test_source_broadband_noise_get_output(dpf_sound_test_server): + """Test SourceBroadbandNoise get_output method.""" + # Create a field to use in a SourceControlTime object. + f_source_control = fields_factory.create_scalar_field( + num_entities=1, location=locations.time_freq + ) + f_source_control.append([1, 3, 6, 10], 1) + support = TimeFreqSupport() + f_time = fields_factory.create_scalar_field(num_entities=1, location=locations.time_freq) + f_time.append([0, 1, 2, 3], 1) + support.time_frequencies = f_time + f_source_control.time_freq_support = support + + # Create a SourceControlTime object. + source_control_obj = SourceControlTime() + source_control_obj.control = f_source_control + + source_bbn_obj = SourceBroadbandNoise( + pytest.data_path_sound_composer_bbn_source_in_container, + source_control_obj, + ) + source_bbn_obj.process(sampling_frequency=44100.0) + f_output = source_bbn_obj.get_output() + assert isinstance(f_output, Field) + assert len(f_output.data) / 44100.0 == pytest.approx(3.0) + + # Compute the power spectral density over the output signal. + psd = PowerSpectralDensity( + input_signal=f_output, + fft_size=8192, + window_type="HANN", + window_length=8192, + overlap=0.75, + ) + psd.process() + psd_squared, psd_freq = psd.get_PSD_squared_linear_as_nparray() + delat_f = psd_freq[1] - psd_freq[0] + + # Check the sound power level in the octave bands centered at 250, 1000 and 4000 Hz. + # Due to the non-deterministic nature of the produced signal, tolerance is set to 1 dB. + psd_squared_band = psd_squared[ + (psd_freq >= 250 * 2 ** (-1 / 2)) & (psd_freq < 250 * 2 ** (1 / 2)) + ] + level = 10 * np.log10(psd_squared_band.sum() * delat_f / REF_ACOUSTIC_POWER) + assert level == pytest.approx(EXP_LEVEL_OCTAVE_BAND, abs=1.0) + + psd_squared_band = psd_squared[ + (psd_freq >= 1000 * 2 ** (-1 / 2)) & (psd_freq < 1000 * 2 ** (1 / 2)) + ] + level = 10 * np.log10(psd_squared_band.sum() * delat_f / REF_ACOUSTIC_POWER) + assert level == pytest.approx(EXP_LEVEL_OCTAVE_BAND, abs=1.0) + + psd_squared_band = psd_squared[ + (psd_freq >= 4000 * 2 ** (-1 / 2)) & (psd_freq < 4000 * 2 ** (1 / 2)) + ] + level = 10 * np.log10(psd_squared_band.sum() * delat_f / REF_ACOUSTIC_POWER) + assert level == pytest.approx(EXP_LEVEL_OCTAVE_BAND, abs=1.0) + + +def test_source_broadband_noise_get_output_unprocessed(dpf_sound_test_server): + """Test SourceBroadbandNoise get_output method's exception.""" + source_bbn_obj = SourceBroadbandNoise() + with pytest.warns( + PyAnsysSoundWarning, + match="Output is not processed yet. Use the ``SourceBroadbandNoise.process\\(\\)`` method.", + ): + f_output = source_bbn_obj.get_output() + assert f_output is None + + +def test_source_broadband_noise_get_output_as_nparray(dpf_sound_test_server): + """Test SourceBroadbandNoise get_output_as_nparray method.""" + # Create a field to use in a SourceControlTime object. + f_source_control = fields_factory.create_scalar_field( + num_entities=1, location=locations.time_freq + ) + f_source_control.append([1, 3, 6, 10], 1) + support = TimeFreqSupport() + f_time = fields_factory.create_scalar_field(num_entities=1, location=locations.time_freq) + f_time.append([0, 1, 2, 3], 1) + support.time_frequencies = f_time + f_source_control.time_freq_support = support + + # Create a SourceControlTime object. + source_control_obj = SourceControlTime() + source_control_obj.control = f_source_control + + source_bbn_obj = SourceBroadbandNoise( + pytest.data_path_sound_composer_bbn_source_in_container, + source_control_obj, + ) + source_bbn_obj.process(sampling_frequency=44100.0) + output_nparray = source_bbn_obj.get_output_as_nparray() + assert isinstance(output_nparray, np.ndarray) + assert len(output_nparray) / 44100.0 == pytest.approx(3.0) + + +def test_source_broadband_noise_get_output_as_nparray_unprocessed(dpf_sound_test_server): + """Test SourceBroadbandNoise get_output_as_nparray method's exception.""" + source_bbn_obj = SourceBroadbandNoise() + with pytest.warns( + PyAnsysSoundWarning, + match="Output is not processed yet. Use the ``SourceBroadbandNoise.process\\(\\)`` method.", + ): + output_nparray = source_bbn_obj.get_output_as_nparray() + assert len(output_nparray) == 0 + + +@patch("matplotlib.pyplot.show") +def test_source_broadband_noise_plot(mock_show, dpf_sound_test_server): + """Test SourceBroadbandNoise plot method.""" + # Create a field to use in a SourceControlTime object. + f_source_control = fields_factory.create_scalar_field( + num_entities=1, location=locations.time_freq + ) + f_source_control.append([1, 3, 6, 10], 1) + support = TimeFreqSupport() + f_time = fields_factory.create_scalar_field(num_entities=1, location=locations.time_freq) + f_time.append([0, 1, 2, 3], 1) + support.time_frequencies = f_time + f_source_control.time_freq_support = support + + # Create a SourceControlTime object. + source_control_obj = SourceControlTime() + source_control_obj.control = f_source_control + + source_bbn_obj = SourceBroadbandNoise( + pytest.data_path_sound_composer_bbn_source_in_container, + source_control_obj, + ) + source_bbn_obj.process() + source_bbn_obj.plot() + + +def test_source_broadband_noise_plot_exceptions(dpf_sound_test_server): + """Test SourceBroadbandNoise plot method's exception.""" + source_bbn_obj = SourceBroadbandNoise() + with pytest.raises( + PyAnsysSoundException, + match="Output is not processed yet. Use the 'SourceBroadbandNoise.process\\(\\)' method.", + ): + source_bbn_obj.plot() + + +def test_source_broadband_noise___extract_bbn_info(dpf_sound_test_server): + """Test SourceBroadbandNoise __extract_bbn_info method.""" + source_bbn_obj = SourceBroadbandNoise() + assert source_bbn_obj._SourceBroadbandNoise__extract_bbn_info() == ("", 0.0, "", "", []) + + source_bbn_obj.load_source_bbn(pytest.data_path_sound_composer_bbn_source_in_container) + assert source_bbn_obj._SourceBroadbandNoise__extract_bbn_info() == ( + "Not available", + 31.0, + "Speed of wind", + "m/s", + [1.0, 2.0, 5.300000190734863, 10.5, 27.777999877929688], + ) + + # Test with empty control support (delta_f not applicable). + source_bbn_obj.source_bbn[0].time_freq_support.time_frequencies.data = [] + assert source_bbn_obj._SourceBroadbandNoise__extract_bbn_info() == ( + "Not available", + 0.0, + "Speed of wind", + "m/s", + [1.0, 2.0, 5.300000190734863, 10.5, 27.777999877929688], + ) diff --git a/tests/tests_sound_composer/test_sound_composer_source_spectrum.py b/tests/tests_sound_composer/test_sound_composer_source_spectrum.py index e3d1d95ad..a1a864966 100644 --- a/tests/tests_sound_composer/test_sound_composer_source_spectrum.py +++ b/tests/tests_sound_composer/test_sound_composer_source_spectrum.py @@ -232,7 +232,7 @@ def test_source_spectrum_get_output(dpf_sound_test_server): ) source_spectrum.process(sampling_frequency=44100.0) - # Checkout output type and sampling frequency. + # Check output type and sampling frequency. output_signal = source_spectrum.get_output() time = output_signal.time_freq_support.time_frequencies.data fs = 1.0 / (time[1] - time[0])