Chiudi gli occhi mentre qualcuno suona una nota: sapresti dire se è un flauto o una tromba? Quasi certamente sì, anche se la nota è la stessa. Ma come fai? La frequenza è identica, il volume pure. Quello che ti permette di distinguerli si chiama timbro — ed è esattamente ciò che insegneremo a riconoscere a una macchina in questo progetto.
Questo è un tutorial-progetto: partiamo dall’onda sonora grezza e arriviamo a un classificatore che riconosce lo strumento. Lungo la strada tocchiamo tutta la filiera del machine learning applicato all’audio: sintesi del segnale, estrazione di feature, addestramento e valutazione. E per non farti scaricare gigabyte di dataset, gli strumenti ce li costruiamo da soli con NumPy.
Il timbro è una questione di armoniche
Se non ricordi cos’è un’onda sonora, ti basta questo: è una vibrazione dell’aria che possiamo rappresentare come una sequenza di numeri (i campioni), tipicamente 22050 o 44100 al secondo. Una nota «pura» è una sinusoide a una certa frequenza: il La del diapason vibra a 440 Hz.
Ma nessuno strumento reale produce una sinusoide pura. Ogni strumento aggiunge armoniche: multipli interi della frequenza fondamentale, ognuno con la propria intensità. Pensa a un caffè: la «frequenza fondamentale» è il caffè stesso, le armoniche sono zucchero, latte, cacao. Due tazze possono contenere lo stesso caffè ed avere gusti completamente diversi: è la ricetta delle proporzioni a fare il timbro. Il flauto è quasi solo fondamentale (un espresso liscio), il clarinetto privilegia le armoniche dispari, gli ottoni sono ricchi su tutto lo spettro.
Riconoscere uno strumento non significa sentire la nota: significa riconoscere la ricetta delle sue armoniche.
Il classificatore, quindi, non deve guardare l’onda campione per campione (decine di migliaia di numeri, quasi tutti ridondanti): deve guardare un riassunto compatto della ricetta spettrale. Quel riassunto ha un nome: MFCC.
MFCC: la carta d’identità del suono
I Mel-Frequency Cepstral Coefficients sono una dozzina abbondante di numeri che descrivono la forma dello spettro così come la percepisce l’orecchio umano (la scala mel comprime le alte frequenze, dove siamo meno sensibili). Non serve capire la matematica per usarli: ti basta sapere che due suoni con timbro simile hanno MFCC simili, e che sono da decenni le feature standard per parlato e musica. La libreria librosa li calcola in una riga.
La filiera del progetto: librosa comprime ogni suono in 13 coefficienti MFCC, e in quello spazio la SVM traccia il confine tra gli strumenti.
Prepara l’ambiente
Python 3.12, un ambiente virtuale, tre librerie. Con uv:
uv venv && source .venv/bin/activate
uv pip install numpy librosa scikit-learn
# In alternativa con venv classico:
# python3.12 -m venv .venv && source .venv/bin/activate
# pip install numpy librosa scikit-learn
Apri VS Code o PyCharm e crea strumenti.py. Il progetto gira interamente in CPU, in pochi secondi.
Il codice completo: sintesi, feature, classificazione
Sintetizziamo tre «strumenti» con ricette di armoniche diverse — flauto (quasi pura fondamentale), clarinetto (armoniche dispari), ottone (spettro ricco) — su note e volumi casuali, così il classificatore deve imparare il timbro e non la nota. Se non ricordi cos’è una SVM, ti basta sapere che cerca il confine che separa le classi con il margine più ampio possibile.
# strumenti.py — riconoscere strumenti dal timbro
import numpy as np
import librosa
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
from sklearn.metrics import classification_report
SR = 22050 # frequenza di campionamento (campioni al secondo)
DURATA = 1.0 # ogni suono dura 1 secondo
rng = np.random.default_rng(42)
def sintetizza(f0, ricetta):
"""Genera 1 secondo di suono: fondamentale f0 + armoniche pesate."""
t = np.linspace(0, DURATA, int(SR * DURATA), endpoint=False)
onda = sum(peso * np.sin(2 * np.pi * f0 * k * t)
for k, peso in ricetta.items())
# inviluppo: attacco e rilascio morbidi, come un suono reale
inviluppo = np.minimum(1, np.minimum(t / 0.05, (DURATA - t) / 0.2))
onda = onda * inviluppo
return (onda / np.abs(onda).max()).astype(np.float32)
# La "ricetta" delle armoniche: {numero armonica: peso}
RICETTE = {
"flauto": {1: 1.0, 2: 0.15, 3: 0.05}, # quasi puro
"clarinetto": {1: 1.0, 3: 0.75, 5: 0.5, 7: 0.3}, # dispari
"ottone": {1: 1.0, 2: 0.9, 3: 0.8, 4: 0.6, 5: 0.5}, # ricco
}
def estrai_feature(onda):
"""Da 22050 campioni a 13 numeri: la media dei MFCC nel tempo."""
mfcc = librosa.feature.mfcc(y=onda, sr=SR, n_mfcc=13)
return mfcc.mean(axis=1) # media sui frame temporali
# Costruiamo il dataset: 60 suoni per strumento, note e volumi casuali
X, y = [], []
for nome, ricetta in RICETTE.items():
for _ in range(60):
f0 = rng.uniform(200, 800) # nota casuale (Hz)
onda = sintetizza(f0, ricetta)
onda += rng.normal(0, 0.01, onda.size) # un filo di rumore
X.append(estrai_feature(onda.astype(np.float32)))
y.append(nome)
X, y = np.array(X), np.array(y)
# Train/test split stratificato: mai valutare sui dati di training
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, random_state=42, stratify=y
)
# SVM con kernel RBF. Lo scaling PRIMA della SVM e' obbligatorio:
# le SVM ragionano in distanze, e feature su scale diverse le accecano.
modello = make_pipeline(StandardScaler(), SVC(kernel="rbf", C=10))
modello.fit(X_train, y_train)
print(classification_report(y_test, modello.predict(X_test)))
# Prova finale: una nota MAI vista, 523 Hz (un Do), suonata "da ottone"
nuovo = estrai_feature(sintetizza(523, RICETTE["ottone"]))
print("Predizione:", modello.predict([nuovo])[0]) # atteso: ottone
Esegui python strumenti.py: dovresti vedere precisione e recall vicine a 1.00 su tutte e tre le classi, e la nota di prova riconosciuta come ottone. Nota il punto cruciale: la SVM non ha mai visto quella nota a quella frequenza. Ha imparato la ricetta delle armoniche, non le note — esattamente come il tuo orecchio.
Perché funziona così bene (e quando non funzionerebbe)
Onestà intellettuale, come sempre: i nostri suoni sintetici sono un mondo pulito. Ogni strumento ha una ricetta fissa, niente riverbero, niente vibrato, niente registrazioni col telefono in un bar. Con audio reale le cose si complicano:
- Lo stesso strumento cambia timbro tra registro grave e acuto, tra piano e forte, tra microfoni diversi.
- La media dei MFCC butta via l’evoluzione temporale: l’attacco di una nota (i primi 50 ms) è uno degli indizi più forti per l’orecchio umano, e noi lo stiamo schiacciando in una media.
- Con più strumenti che suonano insieme il problema cambia natura, e servono reti neurali addestrate su grandi dataset.
Ma attenzione a non buttare la baseline: per compiti mono-strumento con audio decente, MFCC + SVM resta un punto di partenza serio, si addestra in secondi e non richiede GPU. Se la baseline ti dà il 95%, forse la rete neurale non ti serve. Se ti dà il 60%, ora sai cosa deve battere.
Prova tu
- Livello 1: aggiungi un quarto strumento alle
RICETTE— per esempio un «organo» con armoniche pari e dispari tutte forti — e verifica se la SVM lo distingue dall’ottone. Se fa fatica, prova ad aumentaren_mfccda 13 a 20. - Livello 2: passa all’audio vero. Scarica il dataset IRMAS (registrazioni reali etichettate per strumento), carica i file con
librosa.load()al posto disintetizza()e riaddestra la stessa identica pipeline. Confronta le metriche con il mondo pulito di questo tutorial: la differenza è la lezione più importante del progetto.
Per approfondire: la documentazione di librosa ha un’ottima galleria di esempi su feature audio e spettrogrammi, e il libro Fundamentals of Music Processing di Meinard Müller è il riferimento accademico per capire davvero cosa c’è dietro MFCC, chroma e compagnia. Se poi vuoi vedere dove arriva lo stato dell’arte, cerca il paper di OpenAI su Whisper: anche il riconoscimento del parlato parte dagli stessi spettrogrammi che hai usato oggi.
I commenti sono riservati agli iscritti.
Accedi per commentare