Source code for libsoni.core.methods

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