DSPy¶
Indice¶
Introduzione Indice
Questo Notebook vuole fornire un percorso didattico per illustrare l'applicazione del linguaggio Python nel realizzare la tecniche fondamentali per la sintesi, elaborazione ed analisi del suono.
Lo scopo è un approfondimento teorico su alcuni concetti che stanno alla base dell'informatica musicale e non applicativo o performativo, per questo motivo tutti i suoni saranno sintetizzati in tempo differito e memorizzati in sound files che potranno essere importabili in una qualsiasi delle principali DAW.
Alcuni dei concetti e strumenti informatici persenti sono tratti da Think DSP di Allen B. Downey - O'Reilly media.
Prerequisiti Indice
- Familiarità con la programmazione in Python (METTI LINK AL NOTEBOOK).
- Familiarità con il paradigma di programmazione orientata agli oggetti (METTI LINK AL NOTEBOOK).
- Familiarità con i principali concetti legati all'informatica musicale e alla programmazione di strumenti virtuali (campioni, rata di campionamento, inviluppoi, segnali di controllo, etc.).
- Familiarità con concetti base di matematica inclusi i numeri complessi.
Installazione Indice
Scarichiamo ed installiamo:
- Python (Anaconda distribution)
- VScode (Incluso con l'Anaconda distribution)
Saranno utilizzate inoltre le seguenti librerie (se si utilizza Anaconda sono già tutte presenti nella distribuzione, altrimenti bisogna scaricarle).
import copy
import math
import numpy as np
import random
import scipy
import scipy.stats
import scipy.fftpack
import subprocess
import warnings
from wave import open as open_wave
from scipy.io import wavfile
import matplotlib.pyplot as plt
try:
from IPython.display import Audio
except:
warnings.warn(
"Non posso importare IPython.display; " "Wave.make_audio() non funzionerà."
)
Risorse in rete Indice
Primo promemoria (dominio del tempo)Indice
Il suono è formato da piccole variazioni della pressione atmosferica che si propagano nel tempo e nello spazio (onda sonora).
Un segnale audio rappresenta dunque una grandezza che varia nel tempo.
Le variazioni di pressione atmosferica possono essere misurate da un microfono che le trasduce in segnali elettrici (variazione di tensione).
I segnali elettrici (continui) possono essere campionati attraverso un dispositivo (adc) che li trasforma in segnali digitali (discreti).
Un segnale audio digitale è dunque rappresentato attraverso una sequenza di valori equispaziati nel tempo (numeric stream).
Questi valori si chiamano campioni (samples) e definiscono le ampiezze istantanee del segnale.
Per convenzione sono compresi tra +1.0 e -1.0.
Gli istanti di tempo in cui sono misurati i campioni si chiamano frames.
I termini samples e frames a volte sono impiegati come sinonimi.
Possiamo sintetizzare un segnale audio digitale calcolando attraverso una funzione d'onda i valori dei campioni (ys) per ogni frame consecutivo (ts).
I valori ottenuti (discreti) possono essere inviati a un dispositivo (dac) che li trasforma in segnale elettrico (continuo) il quale può fornire l'energia necessaria alla generazione del movimento della membrana di un altoparlante che a sua volta produce variazioni di pressione atmosferica (suono).
La quantità di campioni misurati o generati in un secondo si chiama rata di campionamento (sample rate o frame rate).
In Python possiamo memorizzare questi valori sotto forma di numpy.ndarray che ci facilitano la computazione di operazioni matematiche su tutti gli elementi della lista senza la necessità di iterarli.
PI2 = math.pi*2 # Calcola 2pi dalla funzione del modulo math
framerate = 11025
nf = np.arange(0,100, 1) # Ganera gli indici dei frames np.arange(inizio,fine,passo)
ts = nf / framerate # Array di frames
ys = np.sin(PI2 * 111 * ts + 0) # Array di campioni (funzione d'onda della sinusoide)
print(nf)
print(ts)
plt.plot(ys, 'b:')
[ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99] [0.00000000e+00 9.07029478e-05 1.81405896e-04 2.72108844e-04 3.62811791e-04 4.53514739e-04 5.44217687e-04 6.34920635e-04 7.25623583e-04 8.16326531e-04 9.07029478e-04 9.97732426e-04 1.08843537e-03 1.17913832e-03 1.26984127e-03 1.36054422e-03 1.45124717e-03 1.54195011e-03 1.63265306e-03 1.72335601e-03 1.81405896e-03 1.90476190e-03 1.99546485e-03 2.08616780e-03 2.17687075e-03 2.26757370e-03 2.35827664e-03 2.44897959e-03 2.53968254e-03 2.63038549e-03 2.72108844e-03 2.81179138e-03 2.90249433e-03 2.99319728e-03 3.08390023e-03 3.17460317e-03 3.26530612e-03 3.35600907e-03 3.44671202e-03 3.53741497e-03 3.62811791e-03 3.71882086e-03 3.80952381e-03 3.90022676e-03 3.99092971e-03 4.08163265e-03 4.17233560e-03 4.26303855e-03 4.35374150e-03 4.44444444e-03 4.53514739e-03 4.62585034e-03 4.71655329e-03 4.80725624e-03 4.89795918e-03 4.98866213e-03 5.07936508e-03 5.17006803e-03 5.26077098e-03 5.35147392e-03 5.44217687e-03 5.53287982e-03 5.62358277e-03 5.71428571e-03 5.80498866e-03 5.89569161e-03 5.98639456e-03 6.07709751e-03 6.16780045e-03 6.25850340e-03 6.34920635e-03 6.43990930e-03 6.53061224e-03 6.62131519e-03 6.71201814e-03 6.80272109e-03 6.89342404e-03 6.98412698e-03 7.07482993e-03 7.16553288e-03 7.25623583e-03 7.34693878e-03 7.43764172e-03 7.52834467e-03 7.61904762e-03 7.70975057e-03 7.80045351e-03 7.89115646e-03 7.98185941e-03 8.07256236e-03 8.16326531e-03 8.25396825e-03 8.34467120e-03 8.43537415e-03 8.52607710e-03 8.61678005e-03 8.70748299e-03 8.79818594e-03 8.88888889e-03 8.97959184e-03]
[<matplotlib.lines.Line2D at 0x127d1e5d0>]
Architettura informaticaIndice
Secondo quanto ricordato nel promemoria cominciamo a formalizzare un insieme di classi (modulo) seguendo il paradigma della programmazione orientata agli oggetti (OOP).
N.B. Nel corso del notebook le classi saranno di volta volta aggiornate con nuove funzionalità e sostituiranno le versioni precedenti. Il modulo nella sua versione finale può essere scaricato a questo link (METTI LINK!!!!!!!!).
WaveIndice
Cominciamo col definire la classe Wave che rappresenta una forma d'onda generica discreta e accetta come proprietà:
- una collezione di campioni (ys).
- una collezione di frames (ts).
- una rata di campionamento (framerate).
class Wave:
"""
Rappresenta una forma d'onda generica discreta
"""
def __init__(self, ys, ts=None, framerate=None):
"""
ys: collezione di campioni (valori y)
ts: collezione di frames (valori x)
framerate: rata di campionamento
"""
self.ys = np.asanyarray(ys) # converte qualsiasi tipo di collezione
# in un ndarray
self.framerate = framerate if framerate is not None else 11025 # framerate di default = 11025
if ts is None: # Se non specifichiamo i frames
self.ts = np.arange(len(ys)) / self.framerate # li calcola dal numero di campioni
else:
self.ts = np.asanyarray(ts)
def __len__(self): # Riporta il numero di frames (dunder method)
return len(self.ys)
@property # Riporta il valore del primo frame
def start(self):
return self.ts[0]
@property # Riporta li valore dell'ultimo frame
def end(self):
return self.ts[-1]
@property
def duration(self): # Calcola e riporta la durata in secondi (float)
return len(self.ys) / self.framerate
def plot(self,curva="linear",**kvargs): # Se ys sono numeri complessi, visualizza la parte reale.
plt.ylim([-1, 1])
plt.yticks([-1,0,1])
plt.grid(True)
#plt.tight_layout()
plt.xscale(curva)
plt.yscale(curva)
plt.plot(self.ts, np.real(self.ys), **kvargs) # options
def plot_vlines(self,curva="linear",**kvargs): # Plot con linee verticali
plt.ylim([-1, 1])
plt.yticks([-1,0,1])
plt.grid(True)
#plt.tight_layout()
plt.xscale(curva)
plt.yscale(curva)
plt.vlines(self.ts, 0, self.ys)
plt.plot(self.ts, np.real(self.ys), **kvargs)
Notiamo l'utilizzo di np.asanyarray() che accetta come argomento qualsiasi tipo di collezione (tuple, list, dict, etc.) ed esegue il casting in np.ndarray().
Notiamo l'attributo curva dei plot che definisce la visualizzazione degli assi lineare (default) o logaritmica.
Notiamo l'attributo **kwargs che definisce gli stili del plot.
Un introduzione a pyplot la troviamo qui.
Per verificare le funzionalità utilizziamo come proprietà gli stessi parametri già espressi alla fine del promemoria.
PI2 = math.pi*2 # Calcola 2pi dalla funzione del modulo math
framerate = 11025
nf = np.arange(0,100, 1) # Ganera gli indici dei frames np.arange(inizio,fine,passo)
ts = nf / framerate # Array di frames
ys = np.sin(PI2 * 111 * ts + 0) # Array di campioni (funzione d'onda della sinusoide)
a = Wave(ys,ts,framerate) # Istanza
print('len:', len(a)) # Capacità
print('start:', a.start) # Getters (Decoratori)
print('end:', a.end)
print('dur:', a.duration)
len: 100 start: 0.0 end: 0.008979591836734694 dur: 0.009070294784580499
a.plot(color="red",linewidth=1,linestyle='dotted') # Plot normale
a.plot_vlines(color="red",linewidth=1,linestyle='dotted') # Plot con barre verticali
SignalIndice
Definiamo ora una SuperClasse chiamata Signal che provvederà a fornire funzionalità comuni ai diversi tipi di segnale.
- generare un plot del segnale (per le forme d'onda periodiche visualizza 3 cicli di default).
- generare un'istanza di Wave.
class Signal:
"""
Rappresenta un segnale nel tempo.
"""
# ==================================== Questo metodo sarà spiegato nel paragrafo sulla sintesi additiva
def __add__(self, other):
"""Somma due segnali.
other: Signal
returns: Signal
"""
if other == 0:
return self
return SumSignal(self, other) # Richiama la classe SumSignal()
__radd__ = __add__
# ====================================
@property # Decoratore
def period(self): # Restituisce il periodo in secondi
"""
principalmente per i plot dei segnali
returns: secondi - float
"""
return 0.1
def plot(self, framerate=11025): # Genera un Plot del segnale
duration = self.period * 3 # Default 3 periodi
wave = self.make_wave(duration, start=0, framerate=framerate)
wave.plot()
def make_wave(self, duration=1, start=0, framerate=11025): # Crea un'istanza di Wave
"""
duration: float seconds
start: float seconds
framerate: int frames per second
returns: Wave
"""
n = round(duration * framerate) # Durata in frames
ts = start + np.arange(n) / framerate # Genera ts (come nel codice precedente)
ys = self.evaluate(ts) # Valuterà le singole funzioni d'onda definite nelle sottoclassi
return Wave(ys, ts, framerate=framerate)
Notiamo alla penultima linea la presenza di evaluate(ts) che richiama una funzione vuota.
Questa sarà definita di volta in volta nelle sottoclassi a seconda della funzione d'onda di generazione dei valori ys.
Il metodo make_wave() restituisce un'istanza della classe Wave.
SinusoidIndice
Definiamo ora una sottoclasse chiamata Sinusoid() che eredita i metodi di Signal() e genera ys e ts al suo interno a seconda delle proprietà che specifichiamo.
PI2 = math.pi*2
class Sinusoid(Signal):
"""Rappresenta un segnale sinusoidale"""
def __init__(self, freq=440, amp=1.0, offset=0, func=np.sin):
"""
freq: float frequency in Hz
amp: float amplitude, 1.0 is nominal max
offset: float phase offset in radians
func: function that maps phase to amplitude
"""
self.freq = freq
self.amp = amp
self.offset = offset
self.func = func
@property
def period(self): # Restituisce il periodo in secondi
return 1.0 / self.freq
def evaluate(self, ts): # Definisce la funzione evaluate che viene richiamata
ts = np.asarray(ts) # dalla SuperClasse Signal
phases = PI2 * self.freq * ts + self.offset
ys = self.amp * self.func(phases)
return ys
a = Sinusoid(600, 0.6, 0) # Istanza di Sinusoid (freq amp fase)
print(a.period) # Periodo in secondi
a.plot() # Plot di Signal
0.0016666666666666668
Possiamo generare un oggettto Wave e invocare i suoi metodi.
b = a.make_wave(a.period * 2) # Crea un'istanza di Wave - Durata in secondi
b.plot_vlines(color="red",linewidth=1,linestyle='dotted') # Plot con barre verticali (invocato su Wave)
Definiamo alcune funzionalità che potrebbero tornare utili aggiungendo metodi alla classe Wave.
- wave.shift() - slitta la forma d'onda a sinistra o a destra nel tempo (secondi).
- wave.roll() - slitta la forma d'onda di un numero di campioni.
- wave.truncate() - taglia la forma d'onda dopo un numero di campioni.
- wave.zero_pad() - aggiunge alla fine una sequenza di zeri.
def zero_pad(array, n):
"""
Estende un Array con zeros.
array: numpy array
n: lunghezza finale
returns: new NumPy array
"""
res = np.zeros(n)
res[: len(array)] = array
return res
class Wave:
"""
Rappresenta una forma d'onda generica discreta
"""
def __init__(self, ys, ts=None, framerate=None):
"""
ys: collezione di campioni (valori y)
ts: collezione di frames (valori x)
framerate: rata di campionamento
"""
self.ys = np.asanyarray(ys)
self.framerate = framerate if framerate is not None else 11025
if ts is None:
self.ts = np.arange(len(ys)) / self.framerate
else:
self.ts = np.asanyarray(ts)
def __len__(self):
return len(self.ys)
@property
def start(self):
return self.ts[0]
@property
def end(self):
return self.ts[-1]
@property
def duration(self):
return len(self.ys) / self.framerate
def plot(self,curva="linear",**kvargs):
plt.ylim([-1, 1])
plt.yticks([-1,0,1])
plt.grid(True)
#plt.tight_layout()
plt.xscale(curva)
plt.yscale(curva)
plt.plot(self.ts, np.real(self.ys), **kvargs)
def plot_vlines(self,curva="linear",**kvargs):
plt.ylim([-1, 1])
plt.yticks([-1,0,1])
plt.grid(True)
#plt.tight_layout()
plt.xscale(curva)
plt.yscale(curva)
plt.vlines(self.ts, 0, self.ys)
plt.plot(self.ts, np.real(self.ys), **kvargs)
def shift(self, shift):
"""
Slitta a destra o sinistra nel tempo
shift: float time shift
"""
self.ts += shift
def roll(self, roll):
"""
Shifta di n campione la forma d'onda (defasaggio)
"""
self.ys = np.roll(self.ys, roll)
def truncate(self, n):
"""
Tronca la forma d'onda dopo n campioni
n: integer index
"""
self.ys = self.ys[:n]
self.ts = self.ts[:n]
def zero_pad(self, n):
"""
Stabilita una lunghezza in campioni aggiunge n zero alla fine
n: integer index
"""
self.ys = zero_pad(self.ys, n) # Richiama la funzione esterna
self.ts = self.start + np.arange(n) / self.framerate
a = Sinusoid(1, 0.6, 0) # Istanza di Sinusoid (freq amp fase)
b = a.make_wave(a.period * 3) # Crea un'istanza di Wave - Durata in secondi
b.shift(0.2) # Secondi - Aggiunge del tempo (anche negativo)
a.plot() # Blu
b.plot() # Arancio
b = a.make_wave(a.period * 3)
b.roll(4000) # Campioni - non aggiunge del tempo (defasaggio)
a.plot() # Blu
b.plot() # Arancio
b = a.make_wave(a.period * 3)
b.truncate(6000) # Campioni
a.plot() # Blu
b.plot() # Arancio
b = a.make_wave(a.period * 3)
b.zero_pad(b.framerate*5) # Size finale dell'array (aggiunge zeri alla fine)
a.plot()
b.plot() # Arancio
Scrivere un soundfileIndice
Possiamo salvare un oggetto Wave sotto forma di sound file (in formato .wav).
- definiamo la funzione normalize che normalizza i valori ys a un ampiezza massima.
- definiamo la funzione quantize che quantizza (approssima) i valori ys al formato informatico utilizzato.
- definiamo la classe WavFileWriter che possiamo richiamare invocando il metodo write all'interno della classe Wave.
def normalize(ys, amp=1.0):
"""Normalizza i valori ys a +amp or -amp.
ys: wave array
amp: ampiezza massima alla quale normalizzare i valori
returns: wave array
"""
high, low = abs(max(ys)), abs(min(ys))
return amp * ys / max(high, low)
def quantize(ys, bound, dtype):
"""Maps the waveform to quanta.
ys: wave array
bound: maximum amplitude
dtype: numpy data type of the result
returns: segnale quantizzato
"""
if max(ys) > 1 or min(ys) < -1:
warnings.warn("Warning: devi normalizzare!")
ys = normalize(ys) # Se i valori eccedono +/-1 invoca la funzione normalizza
zs = (ys * bound).astype(dtype)
return zs
class WavFileWriter:
"""Scrive un file WAV."""
def __init__(self, filename="sound.wav", framerate=11025):
"""
filename: string
framerate: samples per second
"""
self.filename = filename
self.framerate = framerate
self.nchannels = 1
self.sampwidth = 2 # profondità di quantizzazione (bytes)
self.bits = self.sampwidth * 8 # numero di bits
self.bound = 2 ** (self.bits - 1) - 1 # Numero di livelli
self.fmt = "h" # formato
self.dtype = np.int16 # formato
self.fp = open_wave(self.filename, "w") # alias dalla libreria wave.open()
self.fp.setnchannels(self.nchannels)
self.fp.setsampwidth(self.sampwidth)
self.fp.setframerate(self.framerate)
def write(self, wave):
"""
wave: istanza di Wave
"""
zs = wave.quantize(self.bound, self.dtype) # Richiama la funzione quantize
self.fp.writeframes(zs.tobytes())
def close(self):
self.fp.close()
Aggiungiamo due metodi alla classe Wave.
- write() che richiama la classe WavFileWriter.
- normalize() che valuta la funzione corrispondente.
class Wave:
"""
Rappresenta una forma d'onda generica discreta
"""
def __init__(self, ys, ts=None, framerate=None):
"""
ys: collezione di campioni (valori y)
ts: collezione di frames (valori x)
framerate: rata di campionamento
"""
self.ys = np.asanyarray(ys)
self.framerate = framerate if framerate is not None else 11025
if ts is None:
self.ts = np.arange(len(ys)) / self.framerate
else:
self.ts = np.asanyarray(ts)
def __len__(self):
return len(self.ys)
@property
def start(self):
return self.ts[0]
@property
def end(self):
return self.ts[-1]
@property
def duration(self):
return len(self.ys) / self.framerate
def plot(self,curva="linear",**kvargs):
plt.ylim([-1, 1])
plt.yticks([-1,0,1])
plt.grid(True)
#plt.tight_layout()
plt.xscale(curva)
plt.yscale(curva)
plt.plot(self.ts, np.real(self.ys), **kvargs)
def plot_vlines(self,curva="linear",**kvargs):
plt.ylim([-1, 1])
plt.yticks([-1,0,1])
plt.grid(True)
#plt.tight_layout()
plt.xscale(curva)
plt.yscale(curva)
plt.vlines(self.ts, 0, self.ys)
plt.plot(self.ts, np.real(self.ys), **kvargs)
def shift(self, shift):
self.ts += shift
def roll(self, roll):
self.ys = np.roll(self.ys, roll)
def truncate(self, n):
self.ys = self.ys[:n]
self.ts = self.ts[:n]
def zero_pad(self, n):
self.ys = zero_pad(self.ys, n)
self.ts = self.start + np.arange(n) / self.framerate
def quantize(self, bound, dtype): # Quantizza i valori al formato corretto
"""
Rounding dinamico
bound: ampiezza massima
dtype: numpy data type or string
returns: segnale quantizzato
"""
return quantize(self.ys, bound, dtype)
def normalize(self, amp=1.0):
"""
amp: float ampiezza
"""
self.ys = normalize(self.ys, amp=amp)
def write(self, filename="sound.wav"): # Proprietà come stringa
"""
Richima la classe WavFileWriter
"""
print("Writing", filename) # Monitor visivo
wfile = WavFileWriter(filename, self.framerate) # Richiama la classe
wfile.write(self) # Genera il file wav
wfile.close() # Lo chiude
a = Sinusoid(1500, 1, 0) # Istanza di Sinusoid (freq amp fase)
b = a.make_wave(1) # Istanza di Wave - Durata in secondi
b.write('suoni/soundfile.wav') # Scrive il soundfile
Writing suoni/soundfile.wav
Ora possiamo leggere ed eseguire il soundfile con una qualsiasi DAW oppure direttamente dal codice.
Eseguire un soundfileIndice
Possiamo eseguire un soundfile dal codice in due modi:
- Aprendo da terminale il player di sistema (per MAC è afplay).
- Creando un oggetto Audio della libreria IPYthon.
Nel primo caso prima definiamo la funzione play_wave che valuteremo successivamente invocando un metodo specifico da aggiungere alla classe Wave.
Nel secondo caso scriviamo direttamente il metodo.
def play_wave(filename="sound.wav", player="afplay"):
"""Esegue un file wav.
filename: string
player: il nome della daw da utilizzare per eseguire il file
"""
cmd = "%s %s" % (player, filename) # comando da eseguire dalla shell (terminale)
popen = subprocess.Popen(cmd, shell=True)
popen.communicate()
class Wave:
"""
Rappresenta una forma d'onda generica discreta
"""
def __init__(self, ys, ts=None, framerate=None):
"""
ys: collezione di campioni (valori y)
ts: collezione di frames (valori x)
framerate: rata di campionamento
"""
self.ys = np.asanyarray(ys)
self.framerate = framerate if framerate is not None else 11025
if ts is None:
self.ts = np.arange(len(ys)) / self.framerate
else:
self.ts = np.asanyarray(ts)
def __len__(self):
return len(self.ys)
@property
def start(self):
return self.ts[0]
@property
def end(self):
return self.ts[-1]
@property
def duration(self):
return len(self.ys) / self.framerate
def plot(self,curva="linear",**kvargs):
plt.ylim([-1, 1])
plt.yticks([-1,0,1])
plt.grid(True)
#plt.tight_layout()
plt.xscale(curva)
plt.yscale(curva)
plt.plot(self.ts, np.real(self.ys), **kvargs)
def plot_vlines(self,curva="linear",**kvargs):
plt.ylim([-1, 1])
plt.yticks([-1,0,1])
plt.grid(True)
#plt.tight_layout()
plt.xscale(curva)
plt.yscale(curva)
plt.vlines(self.ts, 0, self.ys)
plt.plot(self.ts, np.real(self.ys), **kvargs)
def shift(self, shift):
self.ts += shift
def roll(self, roll):
self.ys = np.roll(self.ys, roll)
def truncate(self, n):
self.ys = self.ys[:n]
self.ts = self.ts[:n]
def zero_pad(self, n):
self.ys = zero_pad(self.ys, n)
self.ts = self.start + np.arange(n) / self.framerate
def quantize(self, bound, dtype):
return quantize(self.ys, bound, dtype)
def normalize(self, amp=1.0):
self.ys = normalize(self.ys, amp=amp)
def write(self, filename="sound.wav"):
print("Writing", filename)
wfile = WavFileWriter(filename, self.framerate)
wfile.write(self)
wfile.close()
def play(self, filename="sound.wav"):
"""
filename: string
"""
self.write(filename) # scrive il file
play_wave(filename) # valuta la funzione
def make_audio(self):
"""
Crea un oggetto Audio di IPython.
"""
audio = Audio(data=self.ys.real, rate=self.framerate)
return audio
a = Sinusoid(500, 1, 0) # Istanza di Sinusoid (freq amp fase)
b = a.make_wave(1) # Istanza di Wave - Durata in secondi
b.play('suoni/soundfile.wav') # Legge con afplay
Writing suoni/soundfile.wav
b.make_audio() # Crea oggetto Audio di IPython
Importare un soundfileIndice
Possiamo generare automaticamente un'istanza di Wave importando un file .wav.
Definiamo la funzione read_wave che estrae i valori ys dal sound file grazie ad alcune funzionalità della libreria wave e li usa per generare un'istanza dell'oggetto *Wave.
def read_wave(filename="sound.wav"):
"""
filename: string
returns: Wave
"""
fp = open_wave(filename, "r") # Dalla libreria wave
nchannels = fp.getnchannels()
nframes = fp.getnframes()
sampwidth = fp.getsampwidth()
framerate = fp.getframerate()
z_str = fp.readframes(nframes)
fp.close()
dtype_map = {1: np.int8, 2: np.int16, 3: "special", 4: np.int32} # Dict di formati informatici
if sampwidth not in dtype_map: # Se il formato non è riconosciuto
raise ValueError("formato %d sconosciuto" % sampwidth) # messaggio di errore
if sampwidth == 3: # Se è una stringa
xs = np.fromstring(z_str, dtype=np.int8).astype(np.int32) # la converte in int32
ys = (xs[2::3] * 256 + xs[1::3]) * 256 + xs[0::3]
else:
ys = np.frombuffer(z_str, dtype=dtype_map[sampwidth]) # altrimenti sceglie
# specificato
if nchannels == 2: # Se è stereo prende solo il
ys = ys[::2] # Primo canale
wave = Wave(ys, framerate=framerate) # ts è ricavato da ys in Wave
wave.normalize()
return wave
a = read_wave('suoni/voce.wav') # genera un'istanza di Wave dal sound file
a.framerate
44100
a.plot(color="black",linewidth=1)
a.play('suoni/voce.wav')
Writing suoni/voce.wav
Estrarre un segmentoIndice
Quando generiamo un'istanza di wave da un sound file possiamo avere la necessità di estrarre un frammento e restituire una nuova istanza di Wave con il frammento selezionato.
Vediamo queli operazioni sono necessarie.
- stabilire un inizio e una durata del frammento da estrarre in secondi.
- definire un metodo per recuperare il valore degli indici dell'Array corrispondenti a inizio e fine.
- definire un metodo che realizzi uno slicing sull'Array ys.
- definire un metodo che generi una nuova istanza di Wave che contiene il frammento.
class Wave:
"""
Rappresenta una forma d'onda generica discreta
"""
def __init__(self, ys, ts=None, framerate=None):
"""
ys: collezione di campioni (valori y)
ts: collezione di frames (valori x)
framerate: rata di campionamento
"""
self.ys = np.asanyarray(ys)
self.framerate = framerate if framerate is not None else 11025
if ts is None:
self.ts = np.arange(len(ys)) / self.framerate
else:
self.ts = np.asanyarray(ts)
def __len__(self):
return len(self.ys)
@property
def start(self):
return self.ts[0]
@property
def end(self):
return self.ts[-1]
@property
def duration(self):
return len(self.ys) / self.framerate
def plot(self,curva="linear",**kvargs):
plt.ylim([-1, 1])
plt.yticks([-1,0,1])
plt.grid(True)
#plt.tight_layout()
plt.xscale(curva)
plt.yscale(curva)
plt.plot(self.ts, np.real(self.ys), **kvargs)
def plot_vlines(self,curva="linear",**kvargs):
plt.ylim([-1, 1])
plt.yticks([-1,0,1])
plt.grid(True)
#plt.tight_layout()
plt.xscale(curva)
plt.yscale(curva)
plt.vlines(self.ts, 0, self.ys)
plt.plot(self.ts, np.real(self.ys), **kvargs)
def shift(self, shift):
self.ts += shift
def roll(self, roll):
self.ys = np.roll(self.ys, roll)
def truncate(self, n):
self.ys = self.ys[:n]
self.ts = self.ts[:n]
def zero_pad(self, n):
self.ys = zero_pad(self.ys, n)
self.ts = self.start + np.arange(n) / self.framerate
def quantize(self, bound, dtype):
return quantize(self.ys, bound, dtype)
def normalize(self, amp=1.0):
self.ys = normalize(self.ys, amp=amp)
def write(self, filename="sound.wav"):
print("Writing", filename)
wfile = WavFileWriter(filename, self.framerate)
wfile.write(self)
wfile.close()
def play(self, filename="sound.wav"):
self.write(filename) # scrive il file
play_wave(filename) # valuta la funzione
def make_audio(self):
audio = Audio(data=self.ys.real, rate=self.framerate)
return audio
def copy(self):
"""
Returns: una copia dell'istanza
"""
return copy.deepcopy(self)
def find_index(self, t):
"""
Calcola l'indice corrispondente
a un tempo dato in secondi
return: index int
"""
n = len(self) # numero di campioni
start = self.start # inizio in secondi
end = self.end # fine in secondi
i = round((n - 1) * (t - start) / (end - start))
return int(i)
def segment(self, start=None, duration=None):
"""
Estrae un frammento.
start: float inizio in secondi
duration: float durata in secondi
returns: Wave
"""
if start is None: # se non è specificato inizio vai all'indice 0
start = self.ts[0]
i = 0
else: # altrimenti calcola l'indice dell'inizio
i = self.find_index(start)
# calcola l'indice della fine
# (inizio + durata)
j = None if duration is None else self.find_index(start + duration)
return self.slice(i, j) # valuta la funzione successiva...
def slice(self, i, j):
"""
Esegue uno slicing sugli Array di Wave
e genera una copia.
i: primo slice index
j: ultimo slice index
"""
ys = self.ys[i:j].copy()
ts = self.ts[i:j].copy()
return Wave(ys, ts, self.framerate)
a = read_wave('suoni/voce.wav') # Genera un'istanza di Wave dal sound file
b = a.segment(2.2,0.1) # Estrae un frammento e genera una nuova istanza di Wave
b.plot(color="red",linewidth=1)
b.play('suoni/frag.wav')
Writing suoni/frag.wav
Inviluppi d'ampiezzaIndice
In informatica musicale possiamo modificare l'ampiezza di un segnale principalmente in tre modi:
- riscalarla moltiplicando tutti i valori dei campioni (ys) per un valore costante compreso tra 0.0 e 1.0.
- definire un inviluppo d'ampiezza ovvero moltiplicare tutti i valori dei campioni (ys) per un valore che cambia nel tempo sempre compreso tra 0.0 e 1.0.
- definire una finestra di pochi campioni come inviluppo d'ampiezza attraverso una funzione (hanning. hamming, welch, etc.).
Come vedremo ognuna di queste modalità avrà una propria applicazione in contesti differenti.
Definiamo tre metodi all'interno della classe Wave.
- scale() - riscalaggio costante.
- window() - inviluppo d'ampiezza.
- hamming() - windowing per analisi e risintesi.
class Wave:
"""
Rappresenta una forma d'onda generica discreta
"""
def __init__(self, ys, ts=None, framerate=None):
"""
ys: collezione di campioni (valori y)
ts: collezione di frames (valori x)
framerate: rata di campionamento
"""
self.ys = np.asanyarray(ys)
self.framerate = framerate if framerate is not None else 11025
if ts is None:
self.ts = np.arange(len(ys)) / self.framerate
else:
self.ts = np.asanyarray(ts)
def __len__(self):
return len(self.ys)
@property
def start(self):
return self.ts[0]
@property
def end(self):
return self.ts[-1]
@property
def duration(self):
return len(self.ys) / self.framerate
def plot(self,curva="linear",**kvargs):
plt.ylim([-1, 1])
plt.yticks([-1,0,1])
plt.grid(True)
#plt.tight_layout()
plt.xscale(curva)
plt.yscale(curva)
plt.plot(self.ts, np.real(self.ys), **kvargs)
def plot_vlines(self,curva="linear",**kvargs):
plt.ylim([-1, 1])
plt.yticks([-1,0,1])
plt.grid(True)
#plt.tight_layout()
plt.xscale(curva)
plt.yscale(curva)
plt.vlines(self.ts, 0, self.ys)
plt.plot(self.ts, np.real(self.ys), **kvargs)
def shift(self, shift):
self.ts += shift
def roll(self, roll):
self.ys = np.roll(self.ys, roll)
def truncate(self, n):
self.ys = self.ys[:n]
self.ts = self.ts[:n]
def zero_pad(self, n):
self.ys = zero_pad(self.ys, n)
self.ts = self.start + np.arange(n) / self.framerate
def quantize(self, bound, dtype):
return quantize(self.ys, bound, dtype)
def normalize(self, amp=1.0):
self.ys = normalize(self.ys, amp=amp)
def write(self, filename="sound.wav"):
print("Writing", filename)
wfile = WavFileWriter(filename, self.framerate)
wfile.write(self)
wfile.close()
def play(self, filename="sound.wav"):
self.write(filename)
play_wave(filename)
def make_audio(self):
audio = Audio(data=self.ys.real, rate=self.framerate)
return audio
def copy(self):
return copy.deepcopy(self)
def find_index(self, t):
n = len(self)
start = self.start
end = self.end
i = round((n - 1) * (t - start) / (end - start))
return int(i)
def segment(self, start=None, duration=None):
if start is None:
start = self.ts[0]
i = 0
else:
i = self.find_index(start)
j = None if duration is None else self.find_index(start + duration)
return self.slice(i, j)
def slice(self, i, j):
ys = self.ys[i:j].copy()
ts = self.ts[i:j].copy()
return Wave(ys, ts, self.framerate)
def scale(self, factor):
"""
factor: fattore di riscalaggio (tra 0 e 1)
"""
self.ys *= factor # Moltiplica e aggiorna l'array
def window(self, window):
"""
window: sequenza di fattori di riscalaggio
della stessa lunghezza di self.ys
"""
self.ys *= window
def hamming(self):
"""
Applica una Hamming window al segnale.
"""
self.ys *= np.hamming(len(self.ys))
scale()Indice
a = read_wave('suoni/voce.wav')
b = a.segment(2.2,0.1)
b.scale(0.3) # riscala
b.plot(color="red",linewidth=1)
b.play('suoni/frag.wav')
Writing suoni/frag.wav
window()Indice
Per questo medodo dobbiamo definire esternamente un'Array di valori della stessa lunghezza di ys.
a = read_wave('suoni/voce.wav')
b = a.segment(2.2,0.5)
n = b.duration # Otteniamo la durata in secondi
t = len(b) # Otteniamo la capacità in campioni
print(n)
print(t)
0.5 22050
linen()
Definiamo una funzione per generare un inviluppo trapezoidale/triangolare.
Fadein e fadeout sono indicati sotto forma di lista e in tempo relativo, ovvero come fattori di moltiplicazione della durata totale.
def linen(duration=1, fade=[0.1,0.3], framerate=11025):
"""
duration: float durata totale in secondi
fade: float tempo di [fade in, fade out] somma <= 1.0
framerate:int rata di campionamento
returns: np.ndarray
"""
n = int(duration * framerate) # durata in frames
k1 = int(fade[0] * n) # tempo di fadein in frames
k2 = int(fade[1] * n) # tempo di fadeout in frames
w1 = np.linspace(0, 1, k1) # fade in
w2 = np.ones(n - (k1 + k2)) # sostegno
w3 = np.linspace(1, 0, k2) # fade out
window = np.concatenate((w1, w2, w3))
return window
e = linen(0.5, [0.1,0.1], 44100)
plt.plot(e,'r:') # Plot
print(len(e)) # lunghezza in frames
22050
Passiamo la proprietà all'istanza di Wave invocando il metodo window().
a = read_wave('suoni/voce.wav')
b = a.segment(2.2,0.4)
dur = b.duration # Otteniamo la durata in secondi
fade = [0.1,0.1]
rate = b.framerate # Otteniamo la framerate
e = linen(dur, fade, rate)
b.window(e)
b.plot(color="red",linewidth=1)
b.play('suoni/frag.wav')
Writing suoni/frag.wav
fade()
Definiamo una versione di linen() dove possiamo specificare un solo tempo di fade in valori assoluti (secondi).
def fade(duration=1, fade=0.02, framerate=11025):
"""
duration: float durata totale in secondi
fade: float tempo del fade in secondi
framerate:int rata di campionamento
returns: np.ndarray
"""
n = int(duration * framerate) # durata in frames
k1 = int(fade * framerate) # tempo di fadein in frames
w1 = np.linspace(0, 1, k1) # fade in
w2 = np.ones(n - (k1 * 2)) # sostegno
w3 = np.linspace(1, 0, k1) # fade out
window = np.concatenate((w1, w2, w3))
return window
e = fade(0.5, 0.02, 44100)
plt.plot(e,'r:') # Plot
print(len(e)) # lunghezza in frames
22050