import numpy as np
from typing import Tuple
from ..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 (np.float32) [shape=(M, )]
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 (np.float32) [shape=(M, )]
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 (np.float32) [shape=(M, )]
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 (np.float32 / np.float64) [shape=(N, )], 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 (np.float32 / np.float64) [shape=(N, )], 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 (np.float32 / np.float64) [shape=(N, )], 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 (np.float32 / np.float64) [shape=(M, )]
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):
if (partial * pitch_frequency) >= (fs / 2):
# skip partials that would create aliasing
continue
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) * gain
return generated_tone
[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 (np.float32 / np.float64) [shape=(M, )]
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 (np.float32 / np.float64) [shape=(N, )], 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 (np.float32 / np.float64) [shape=(M, )]
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 (np.float32 / np.float64) [shape=(N, )]
Array containing sample-wise instantaneous frequencies.
gain_vector: np.ndarray (np.float32 / np.float64) [shape=(N, )], default = None
Array containing sample-wise gains.
partials: np.ndarray (np.float32 / np.float64) [shape=(N, )], 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 (np.float32 / np.float64) [shape=(N, )], 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 (np.float32 / np.float64) [shape=(N, )], 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 (np.float32 / np.float64) [shape=(M, )]
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
if not (len(partials) == len(partials_amplitudes) == len(partials_phase_offsets)):
raise ValueError('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 = np.cumsum(2 * np.pi * frequency_vector / fs)
for partial, partial_amplitude, partials_phase_offset in zip(partials, partials_amplitudes, partials_phase_offsets):
# mute partials that would create aliasing
# (due to the time-varying nature of the frequency, this is done sample-wise)
instant_ampl = np.ones_like(phase) * partial_amplitude
instant_ampl[np.where((partial * frequency_vector) >= (fs / 2))] = 0
generated_tone += np.sin((phase + partials_phase_offset) * partial) * instant_ampl
generated_tone = generated_tone * gain_vector
generated_tone = fade_signal(signal=generated_tone, fs=fs, fading_duration=fading_duration)
return generated_tone