+import numpy as np
+from typing import Tuple
+
+from libsoni.util.utils import fade_signal, smooth_weights, normalize_signal, pitch_to_frequency
+
+
+[docs]def generate_click(pitch: int = 69,
+
amplitude: float = 1.0,
+
tuning_frequency: float = 440.0,
+
click_fading_duration: float = 0.2,
+
fs: int = 22050) -> np.ndarray:
+
"""Generates a click signal.
+
+
Parameters
+
----------
+
pitch : int, default = 69
+
Pitch for colored click.
+
+
amplitude : float, default = 1.0
+
Amplitude of click signal.
+
+
click_fading_duration : float, default = 0.2
+
Fading duration of click signal, in seconds.
+
+
tuning_frequency : float, default = 440.0
+
Tuning frequency, in Hertz.
+
+
fs: int, default = 22050
+
Sampling rate, in samples per seconds.
+
+
Returns
+
-------
+
click : np.ndarray
+
Generated click signal.
+
"""
+
+
assert 0 <= pitch <= 127, f'Pitch is out of range [0,127].'
+
+
click_frequency = pitch_to_frequency(pitch=pitch, tuning_frequency=tuning_frequency)
+
angular_frequency = 2 * np.pi * click_frequency / fs
+
click = np.logspace(0, -10, num=int(fs * click_fading_duration), base=2.0)
+
click *= amplitude * np.sin(angular_frequency * np.arange(len(click)))
+
+
return click
+
+
+[docs]def generate_sinusoid(frequency: float = 440.0,
+
phase: float = 0.0,
+
amplitude: float = 1.0,
+
duration: float = 1.0,
+
fading_duration: float = 0.01,
+
fs: int = 22050) -> np.ndarray:
+
"""Generates sinusoid.
+
+
Parameters
+
----------
+
frequency: float, default: 440.0
+
Frequency of sinusoid, in Hertz.
+
+
phase: float, default: 0.0
+
Phase of sinusoid.
+
+
amplitude: float, default: 1.0
+
Amplitude of sinusoid.
+
+
duration: float, default: 1.0
+
Duration of generated signal, in seconds.
+
+
fading_duration: float, default: 0.01
+
Determines duration of fade-in and fade-out, in seconds.
+
+
fs: int, default = 22050
+
Sampling rate, in samples per seconds.
+
+
Returns
+
-------
+
sinusoid: np.ndarray
+
Generated sinusoid.
+
"""
+
sinusoid = amplitude * np.sin((2 * np.pi * frequency * (np.arange(int(duration * fs)) / fs)) + phase)
+
sinusoid = fade_signal(signal=sinusoid, fs=fs, fading_duration=fading_duration)
+
+
return sinusoid
+
+
+[docs]def generate_shepard_tone(pitch_class: int = 0,
+
pitch_range: Tuple[int, int] = (20, 108),
+
filter: bool = False,
+
f_center: float = 440.0,
+
octave_cutoff: int = 1,
+
gain: float = 1.0,
+
duration: float = 1.0,
+
tuning_frequency: float = 440,
+
fading_duration: float = 0.05,
+
fs: int = 22050) -> np.ndarray:
+
"""Generates shepard tone.
+
+
The sound can be changed either by the filter option or by the specified pitch-range.
+
Both options can also be used in combination.
+
Using the filter option shapes the spectrum like a bell curve centered around the center frequency,
+
while the octave cutoff determines at which octave the amplitude of the corresponding sinusoid is 0.5.
+
+
Parameters
+
----------
+
pitch_class: int, default: 0
+
Pitch class for shepard tone.
+
+
pitch_range: Tuple[int, int], default = [20,108]
+
Determines the pitch range to encounter for shepard tones.
+
+
filter: bool, default: False
+
Enables filtering of shepard tones.
+
+
f_center : float, default: 440.0
+
Determines filter center frequency, in Hertz.
+
+
octave_cutoff: int, default: 1
+
Determines the width of the filter.
+
+
gain: float, default: 1.0
+
Gain of shepard tone.
+
+
duration: float, default: 1.0
+
Determines duration of shepard tone, in seconds.
+
+
tuning_frequency: float, default: 440.0
+
Tuning frequency, in Hertz.
+
+
fading_duration: float, default: 0.01
+
Determines duration of fade-in and fade-out, in seconds.
+
+
fs: int, default = 22050
+
Sampling rate, in samples per seconds.
+
+
Returns
+
-------
+
shepard_tone: np.ndarray
+
Generated shepard tone.
+
"""
+
+
assert 0 <= pitch_class <= 11, f'Pitch class is out of range [0,11].'
+
assert all(0 <= p <= 127 for p in pitch_range), f'Pitch range has to be defined within [0,127].'
+
+
pitches = pitch_class + np.arange(11) * 12
+
mask = (pitches >= pitch_range[0]) & (pitches <= pitch_range[1])
+
shepard_frequencies = tuning_frequency * 2 ** ((pitches[mask] - 69) / 12)
+
shepard_tone = np.zeros(int(duration * fs))
+
+
if filter:
+
f_log = 2 * np.logspace(1, 4, 20000)
+
f_lin = np.linspace(20, 20000, 20000)
+
f_center_lin = np.argmin(np.abs(f_log - f_center))
+
weights = np.exp(- (f_lin - f_center_lin) ** 2 / (1.4427 * ((octave_cutoff * 2) * 1000) ** 2))
+
+
for shepard_frequency in shepard_frequencies:
+
shepard_tone += weights[np.argmin(np.abs(f_log - shepard_frequency))] * \
+
np.sin(2 * np.pi * shepard_frequency * np.arange(int(duration * fs)) / fs)
+
+
else:
+
for shepard_frequency in shepard_frequencies:
+
shepard_tone += np.sin(2 * np.pi * shepard_frequency * np.arange(int(duration * fs)) / fs)
+
+
shepard_tone = fade_signal(signal=shepard_tone, fs=fs, fading_duration=fading_duration)
+
shepard_tone = normalize_signal(shepard_tone) * gain
+
+
return shepard_tone
+
+
+[docs]def generate_tone_additive_synthesis(pitch: int = 69,
+
partials: np.ndarray = np.array([1]),
+
partials_amplitudes: np.ndarray = None,
+
partials_phase_offsets: np.ndarray = None,
+
gain: float = 1.0,
+
duration: float = 1.0,
+
tuning_frequency: float = 440,
+
fading_duration: float = 0.05,
+
fs: int = 22050) -> np.ndarray:
+
"""Generates tone signal using additive synthesis.
+
+
The sound can be customized using parameters partials, partials_amplitudes and partials_phase_offsets.
+
+
Parameters
+
----------
+
pitch: int, default = 69
+
Pitch of the generated tone.
+
+
partials: np.ndarray, default = [1]
+
Array containing the desired partials of the fundamental frequencies for sonification.
+
An array [1] leads to sonification with only the fundamental frequency,
+
while an array [1,2] leads to sonification with the fundamental frequency and twice the fundamental frequency.
+
+
partials_amplitudes: np.ndarray, default = None
+
Array containing the amplitudes for partials.
+
An array [1,0.5] causes the first partial to have amplitude 1,
+
while the second partial has amplitude 0.5.
+
When not defined, the amplitudes for all partials are set to 1.
+
+
partials_phase_offsets: np.ndarray, default = None
+
Array containing the phase offsets for partials.
+
When not defined, the phase offsets for all partials are set to 0.
+
+
gain: float, default = 1.0
+
Gain of generated tone.
+
+
duration: float, default: 1.0
+
Determines duration of shepard tone, given in seconds.
+
+
tuning_frequency: float, default: 440.0
+
Tuning frequency, in Hertz.
+
+
fading_duration: float, default: 0.01
+
Determines duration of fade-in and fade-out, given in seconds.
+
+
fs: int, default = 22050
+
Sampling rate, in samples per seconds.
+
+
Returns
+
-------
+
generated_tone: np.ndarray
+
Generated tone signal.
+
"""
+
+
assert 0 <= pitch <= 127, f'Pitch is out of range [0,127].'
+
+
partials_amplitudes = np.ones(len(partials)) if partials_amplitudes is None else partials_amplitudes
+
partials_phase_offsets = np.zeros(len(partials)) if partials_phase_offsets is None else partials_phase_offsets
+
+
assert len(partials) == len(partials_amplitudes) == len(partials_phase_offsets), \
+
f'Arrays partials, partials_amplitudes and partials_phase_offsets must be of equal length.'
+
+
generated_tone = np.zeros(int(duration * fs))
+
pitch_frequency = pitch_to_frequency(pitch=pitch, tuning_frequency=tuning_frequency)
+
+
for partial, partial_amplitude, partials_phase_offset in zip(partials, partials_amplitudes, partials_phase_offsets):
+
generated_tone += partial_amplitude * np.sin(2 * np.pi * pitch_frequency * partial * (np.arange(int(duration * fs)) / fs) + partials_phase_offset)
+
+
generated_tone = fade_signal(signal=generated_tone, fs=fs, fading_duration=fading_duration)
+
+
return generated_tone * gain
+
+
+[docs]def generate_tone_fm_synthesis(pitch: int = 69,
+
modulation_rate_relative: float = 0.0,
+
modulation_amplitude: float = 0.0,
+
gain: float = 1.0,
+
duration: float = 1.0,
+
tuning_frequency: float = 440.0,
+
fading_duration: float = 0.05,
+
fs: int = 22050) -> np.ndarray:
+
"""Generates tone signal using frequency modulation synthesis.
+
+
The sound can be customized using parameters modulation_rate_relative and modulation_amplitude.
+
+
Parameters
+
----------
+
pitch: int, default = 69
+
Pitch of the synthesized tone.
+
+
modulation_rate_relative: float, default = 0.0
+
Determines the modulation frequency as multiple or fraction of the frequency for the given pitch.
+
+
modulation_amplitude: float, default = 0.0
+
Determines the amount of modulation in the generated signal.
+
+
gain: float, default = 1.0
+
Gain for generated signal
+
+
duration: float, default: 1.0
+
Determines duration of shepard tone, given in seconds.
+
+
tuning_frequency: float, default: 440.0
+
Tuning frequency, in Hertz.
+
+
fading_duration: float, default: 0.01
+
Determines duration of fade-in and fade-out, given in seconds.
+
+
fs: int, default = 22050
+
Sampling rate, in samples per seconds.
+
+
Returns
+
-------
+
generated_tone: np.ndarray
+
Generated tone signal.
+
"""
+
+
assert 0 <= pitch <= 127, f'Pitch is out of range [0,127].'
+
+
pitch_frequency = pitch_to_frequency(pitch=pitch, tuning_frequency=tuning_frequency)
+
generated_tone = np.sin(2 * np.pi * pitch_frequency * (np.arange(int(duration * fs))) / fs + modulation_amplitude * np.sin(2 * np.pi * pitch_frequency * modulation_rate_relative * (np.arange(int(duration * fs)))))
+
generated_tone = gain * fade_signal(signal=generated_tone, fs=fs, fading_duration=fading_duration)
+
+
return generated_tone
+
+
+[docs]def generate_tone_wavetable(pitch: int = 69,
+
wavetable: np.ndarray = None,
+
gain: float = 1.0,
+
duration: float = 1.0,
+
tuning_frequency: float = 440.0,
+
fading_duration: float = 0.05,
+
fs: int = 22050) -> np.ndarray:
+
"""Generates tone using wavetable synthesis.
+
+
The sound depends on the given wavetable.
+
+
Parameters
+
----------
+
pitch: int, default = 69
+
Pitch of the synthesized tone.
+
+
wavetable: np.ndarray, default = None
+
Wavetable to be resampled.
+
+
gain: float, default = 1.0
+
Gain for generated signal
+
+
duration: float, default: 1.0
+
Determines duration of tone, given in seconds.
+
+
tuning_frequency: float, default: 440.0
+
Tuning frequency, in Hertz.
+
+
fading_duration: float, default: 0.01
+
Determines duration of fade-in and fade-out, in seconds.
+
+
fs: int, default = 22050
+
Sampling rate, in samples per seconds.
+
+
Returns
+
-------
+
generated_tone: np.ndarray
+
Generated signal
+
"""
+
+
assert 0 <= pitch <= 127, f'Pitch is out of range [0,127].'
+
+
generated_tone = []
+
pitch_frequency = pitch_to_frequency(pitch=pitch, tuning_frequency=tuning_frequency)
+
current_sample = 0
+
+
while len(generated_tone) < int(duration * fs):
+
current_sample += int(pitch_frequency)
+
current_sample = current_sample % wavetable.size
+
generated_tone.append(wavetable[current_sample])
+
current_sample += 1
+
+
generated_tone = np.array(generated_tone)
+
generated_tone = gain * fade_signal(signal=generated_tone, fs=fs, fading_duration=fading_duration)
+
+
return generated_tone
+
+
+[docs]def generate_tone_instantaneous_phase(frequency_vector: np.ndarray,
+
gain_vector: np.ndarray = None,
+
partials: np.ndarray = np.array([1]),
+
partials_amplitudes: np.ndarray = None,
+
partials_phase_offsets: np.ndarray = None,
+
fading_duration: float = 0.05,
+
fs: int = 22050) -> np.ndarray:
+
"""Generates signal out of instantaneous frequency.
+
+
The sound can be customized using parameters partials, partials_amplitudes and partials_phase_offsets.
+
+
Parameters
+
----------
+
frequency_vector: np.ndarray
+
Array containing sample-wise instantaneous frequencies.
+
+
gain_vector: np.ndarray, default = None
+
Array containing sample-wise gains.
+
+
partials: np.ndarray, default = [1]
+
An array containing the desired partials of the fundamental frequencies for sonification.
+
An array [1] leads to sonification with only the fundamental frequency core,
+
while an array [1,2] causes sonification with the fundamental frequency and twice the fundamental frequency.
+
+
partials_amplitudes: np.ndarray, default = [1]
+
Array containing the amplitudes for partials.
+
An array [1,0.5] causes the sinusoid with frequency core to have amplitude 1,
+
while the sinusoid with frequency 2*core has amplitude 0.5.
+
+
partials_phase_offsets: np.ndarray, default = [0]
+
Array containing the phase offsets for partials.
+
+
fading_duration: float, default: 0.01
+
Determines duration of fade-in and fade-out, given in seconds.
+
+
fs: int, default = 22050
+
Sampling rate, in samples per seconds.
+
+
Returns
+
-------
+
generated_tone: np.ndarray
+
Generated signal
+
"""
+
partials_amplitudes = np.ones(len(partials)) if partials_amplitudes is None else partials_amplitudes
+
partials_phase_offsets = np.zeros(len(partials)) if partials_phase_offsets is None else partials_phase_offsets
+
+
assert len(partials) == len(partials_amplitudes) == len(partials_phase_offsets), \
+
'Partials, Partials_amplitudes and Partials_phase_offsets must be of equal length.'
+
+
generated_tone = np.zeros_like(frequency_vector)
+
+
if gain_vector is None:
+
gain_vector = np.ones_like(frequency_vector)
+
+
else:
+
gain_vector = smooth_weights(weights=gain_vector, fading_samples=60)
+
+
phase = 0
+
phase_result = []
+
+
for frequency, gain in zip(frequency_vector, gain_vector):
+
phase_step = 2 * np.pi * frequency / fs
+
phase += phase_step
+
phase_result.append(phase)
+
+
phase_result = np.asarray(phase_result)
+
+
for partial, partial_amplitude, partials_phase_offset in zip(partials, partials_amplitudes, partials_phase_offsets):
+
generated_tone += np.sin((phase_result + partials_phase_offset) * partial) * partial_amplitude
+
+
generated_tone = generated_tone * gain_vector
+
generated_tone = fade_signal(signal=generated_tone, fs=fs, fading_duration=fading_duration)
+
+
return generated_tone
+