Source code for muprocdurham.sound

#  Copyright (c) 2024 Robert Lieck

import IPython.display
import numpy as np
from scipy.io.wavfile import write as write_wav
import librosa
import matplotlib.pyplot as plt


sampling_rate = 44000


[docs] def normalise_wave(wave, max_amp=0.95): wave /= abs(wave).max() wave *= max_amp
[docs] def fade_wave(wave, time=0.01, start=True, end=True): # ramp of correct length fade_vals = np.linspace(0, 1, int(np.ceil(time * sampling_rate))) if start: wave[:len(fade_vals)] *= fade_vals if end: wave[-len(fade_vals):] *= np.flip(fade_vals)
[docs] def render(wave, normalise=True, fade=True): wave = wave.copy() if normalise is True: normalise = dict() if normalise is not False: normalise_wave(wave, **normalise) if fade is True: fade = dict() if fade is not False: fade_wave(wave, **fade) return wave
[docs] def save(wave, file_name, normalise=True, fade=True): wave = render(wave=wave, normalise=normalise, fade=fade) # convert to 16bit integer wave = np.int16(wave * (np.iinfo(np.int16).max - 1)) write_wav(file_name, sampling_rate, wave)
[docs] def load(file): x, _ = librosa.load(file, sr=sampling_rate) return x
[docs] def audio(wave, fade=True): wave = render(wave=wave, normalise=False, fade=fade) IPython.display.display(IPython.display.Audio(data=wave, rate=sampling_rate))
[docs] def audio_add(wave, start_time, audio=None, min_total_time=0): offset = int(start_time * sampling_rate) # extend audio if required required_length = max(offset + len(wave), int(min_total_time * sampling_rate)) if audio is None or len(audio) < required_length: new_audio = np.zeros(required_length) if audio is not None: new_audio[:len(audio)] = audio audio = new_audio # add audio audio[offset:offset + len(wave)] += wave return audio
[docs] def sound(func, phases=0., duration=1.): # time vector time = np.arange(0, duration, 1 / sampling_rate) # array of unit-angle steps (corresponding to frequency of 1Hz) angle_steps = np.full_like(time, duration * 2 * np.pi / len(time)) # get frequencies and amplitudes over time if callable(func): freq_amps = func(time) else: freq_amps = func if not isinstance(freq_amps, tuple): freqs = freq_amps amps = 1. else: freqs, amps = freq_amps freqs = np.atleast_2d(freqs) amps = np.atleast_2d(amps) # effective change of angle for different frequencies (time-dependent) angle_steps = angle_steps[:, None] * freqs # actual angle at given time corresponds to the accumulated angle steps angles = np.cumsum(angle_steps, axis=0) + phases # generate oscillations, multiply by amplitudes and sum up return (amps * np.sin(angles)).sum(axis=1)
[docs] def spectrogram(wave, ylim=None, figsize=(14, 5), **kwargs): X = librosa.stft(wave) Xdb = librosa.amplitude_to_db(abs(X)) Xdb += Xdb.min() plt.figure(figsize=figsize) kwargs = {**dict(sr=sampling_rate, x_axis='time', y_axis='hz'), **dict(kwargs)} librosa.display.specshow(Xdb, **kwargs) if ylim is not None: plt.ylim(None, ylim) plt.show()
[docs] def harmonic_tone(f0, decay=1, n=20, **kwargs): return sound(lambda time: ( [f0 * i for i in range(1, n + 1)], [np.exp(-i*decay) for i in range(0, n)], ), **kwargs)