Skip to content

Commit

Permalink
feat: sound composer's source audio (#169)
Browse files Browse the repository at this point in the history
Co-authored-by: pyansys-ci-bot <[email protected]>
  • Loading branch information
ansaminard and pyansys-ci-bot authored Nov 28, 2024
1 parent 4b14ea1 commit 331c862
Show file tree
Hide file tree
Showing 9 changed files with 156,512 additions and 4 deletions.
1 change: 1 addition & 0 deletions doc/changelog.d/169.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
feat: sound composer's source audio
1 change: 1 addition & 0 deletions doc/source/api/sound_composer.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Sound composer
:toctree: _autosummary

SourceSpectrum
SourceAudio
SourceControlSpectrum
SourceControlTime
SpectrumSynthesisMethods
2 changes: 2 additions & 0 deletions src/ansys/sound/core/sound_composer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from ._sound_composer_parent import SoundComposerParent
from ._source_control_parent import SourceControlParent, SpectrumSynthesisMethods
from ._source_parent import SourceParent
from .source_audio import SourceAudio
from .source_control_spectrum import SourceControlSpectrum
from .source_control_time import SourceControlTime
from .source_spectrum import SourceSpectrum
Expand All @@ -39,5 +40,6 @@
"SpectrumSynthesisMethods",
"SourceSpectrum",
"SourceControlSpectrum",
"SourceAudio",
"SourceControlTime",
)
225 changes: 225 additions & 0 deletions src/ansys/sound/core/sound_composer/source_audio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
# 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 audio source."""
import warnings

from ansys.dpf.core import Field, Operator
from matplotlib import pyplot as plt
import numpy as np

from ansys.sound.core.signal_utilities import LoadWav, Resample

from .._pyansys_sound import PyAnsysSoundException, PyAnsysSoundWarning
from ._source_parent import SourceParent

ID_LOAD_FROM_TEXT = "load_sound_samples_from_txt"


class SourceAudio(SourceParent):
"""Sound Composer's audio source class.
This class creates an audio source for the Sound Composer. An audio source is simply made of a
sound signal, that is, sound samples over time, in Pa, that is meant to be played, as is
(unless resampling is necessary), along with other Sound Composer sources. The audio source can
be loaded from a WAV file or a text file.
"""

def __init__(self, file: str = ""):
"""Class instantiation takes the following parameters.
Parameters
----------
file : str, default: ""
Path to the audio file (WAV format or text format with header
`AnsysSound_SoundSamples`).
"""
super().__init__()

# Define DPF Sound operators.
self.__operator_load = Operator(ID_LOAD_FROM_TEXT)

# Load the audio file, if specified.
if len(file) > 0:
if file.endswith(".wav"):
self.load_from_wave_file(file)
else:
self.load_from_text_file(file)
else:
self.source_audio_data = None

def __str__(self) -> str:
"""Return the string representation of the object."""
if self.source_audio_data is not None:
support_data = self.source_audio_data.time_freq_support.time_frequencies.data

if len(support_data) < 1:
str_duration = "N/A"
else:
str_duration = f"{support_data[-1]:.1f} s"

if len(support_data) < 2:
str_fs = "N/A"
else:
str_fs = f"{1/(support_data[1] - support_data[0]):.1f} Hz"

str_source = (
f"'{self.source_audio_data.name}'\n"
f"\tDuration: {str_duration}\n"
f"\tSampling frequency: {str_fs}"
)
else:
str_source = "Not set"

return f"Audio source: {str_source}"

@property
def source_audio_data(self) -> Field:
"""Audio source data, as a DPF field.
Samples over time, in Pa, as a DPF field.
"""
return self.__source_audio_data

@source_audio_data.setter
def source_audio_data(self, source_audio_data: Field):
"""Set the audio source data, from a DPF field."""
if not (isinstance(source_audio_data, Field) or source_audio_data is None):
raise PyAnsysSoundException(
"Specified audio source data must be provided as a DPF field."
)
self.__source_audio_data = source_audio_data

def load_from_wave_file(self, file: str):
"""Load the audio source data from a WAV file.
Parameters
----------
file : str
Path to the WAV file.
"""
loader = LoadWav(file)
loader.process()
self.source_audio_data = loader.get_output()[0]

def load_from_text_file(self, file: str):
"""Load the audio source data from a text file.
Parameters
----------
file : str
Path to the text file containing the samples over time. Supported files shall
have the same text format (with the AnsysSound_SoundSamples header) as 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_audio_data = self.__operator_load.get_output(0, "field")

def process(self, sampling_frequency: float = 44100.0):
"""Generate the sound of the audio source.
The generated sound simply corresponds to the audio source data, resampled if necessary.
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 self.source_audio_data is None:
raise PyAnsysSoundException(
f"Source's audio data is not set. Use "
f"``{__class__.__name__}.source_audio_data`` or method "
f"``{__class__.__name__}.load_source_audio_from_text()``."
)

# Check sampling frequency, and resample if necessary.
support_data = self.source_audio_data.time_freq_support.time_frequencies.data
if np.round(1 / (support_data[1] - support_data[0]), 1) != np.round(sampling_frequency, 1):
resampler = Resample(
signal=self.source_audio_data, new_sampling_frequency=sampling_frequency
)
resampler.process()
self._output = resampler.get_output()
else:
self._output = self.source_audio_data

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 as a NumPy array.
"""
output = self.get_output()

if output == None:
return np.array([])

return np.array(output.data)

def plot(self):
"""Plot the generated sound 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)
name = output.name
if len(name) > 0:
plt.title(name)
else:
plt.title("Sound generated from the audio source")
plt.xlabel("Time (s)")
plt.ylabel("Amplitude (Pa)")
plt.grid(True)
plt.show()
6 changes: 3 additions & 3 deletions src/ansys/sound/core/sound_composer/source_spectrum.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ def __init__(self, file_source: str = "", source_control: SourceControlSpectrum
Parameters
----------
file_source : str, default ""
file_source : str, default: ""
Path to the spectrum file.
source_control : SourceControlSpectrum, default None
source_control : SourceControlSpectrum, default: None
Source control to use when generating the sound from this source.
"""
super().__init__()
Expand Down Expand Up @@ -172,7 +172,7 @@ def process(self, sampling_frequency: float = 44100.0):
Parameters
----------
sampling_frequency : float, default 44100.0
sampling_frequency : float, default: 44100.0
Sampling frequency of the generated sound in Hz.
"""
if sampling_frequency <= 0.0:
Expand Down
3 changes: 3 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ def pytest_configure():
pytest.data_path_sound_composer_spectrum_source_in_container = (
"C:\\data\\AnsysSound_Spectrum_v3_-_nominal_-_dBSPLperHz_2024R2_20241121.txt"
)
pytest.data_path_flute_nonUnitaryCalib_as_txt_in_container = (
"C:\\data\\flute_nonUnitaryCalib_as_text_2024R2_20241125.txt"
)
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"

Expand Down
Loading

0 comments on commit 331c862

Please sign in to comment.