Aumentare le “possibilità sonore” offerte da un cajon, permettendo la riproduzione di campioni tramite l’utilizzo di un Raspberry Pi3 e due microfoni MEMS.

Obbiettivi e descrizione

L’obbiettivo di questo progetto è quello di aumentare le “possibilità sonore” offerte da un cajon, uno strumento musicale.

Al giorno d’oggi il cajon viene utilizzato in contesti di musica moderna dove spesso vi è la necessità di avere anche altre tipologie di suoni (un tamburello, un clap, un wind chimes..), ecco la definizione di “cajon aumentato”. Quello che vogliamo fare è dotare il cajon di una tecnologia economica in grado di offrire al musicista la possibilità di aggiungere due suoni (campioni) per espandere il suo ventaglio sonoro.

Il cajon continuerà a essere suonato al “solito modo”, ma quando il musicista suonerà la faccia destra o sinistra dello strumento verranno riprodotti i campioni rispettivi.

L’idea di base è quella di raccogliere i segnali provenienti dal cajon con l’ausilio di microfoni ed elabolarli in tempo reale per decidere se provengono dai lati oppure dalla parte frontale che d’ora in poi chiamaremo tapa. Nel primo caso vogliamo riprodurre il campione corrispondente alla facciata sinistra o destra (left o right), mentre nel secondo non dobbiamo riprodurre alcun suono extra.

Hardware

Ci siamo avvalsi dell’utilizzo di un microcomputer e di due microfoni MEMS, rispettivamente:

Abbiamo collegato i microfoni al Raspberry seguento il seguente schema

I pin dei microfoni sono stati collegati ai pin del raspeberry in questo modo:

Ecco il risultato finale:

Come si può notare dalle immagini soprastanti, i microfoni sono stati dapprima incastrati in una specie di gomma e poi racchiusi in una scatolina piena di cotone. Questo è stato necessario per non far andare i microfoni in saturazione. Infine essi sono stati posizionati internamente al cajon, nello specifico appoggiati alle facciate laterali.

Idea

I tre punti cruciali del progetto sono:

  • identificare gli onset ovvero gli istanti nei quali il cajon viene colpito
  • decidere la provenienza del colpo:
    • tapa
    • sinistro
    • destro
  • riprodurre il campione rispettivo (sinistro o destro) “in tempo reale” e gestire il mix tra i due qualora fosse necessario

È da sottolineare come tutte le scelte ai punti sopra devono essere fatte in “tempo reale”. Questo aspetto ha condizionato molte scelte implementative prese durante la realizzazione del progetto.

Per la rilevazione degli onset ci siamo avvalsi di: inviluppo e derivata.

Inviluppo

Per il calcolo dell’inviluppo è stata usata questa formula:

env[LEFT] = max(abs(left), env[LEFT] * ALPHA)
env[RIGHT] = max(abs(right), env[RIGHT] * ALPHA)

dove

LEFT = 0 e RIGHT = 1 sono due costanti.

ALPHA è una costante che determina l’importanza dell’inviluppo calcolato fino ad un dato istante, rispetto al nuovo campione.

env è un array di lunghezza 2 dove memorizziamo l’inviluppo relativo a LEFT e RIGHT.

left e right sono due varibili, dove ad ogni iterazione del ciclo si trovano il campione sinistro e destro appena letti.

max e abs sono due funzioni che calcolano rispettivamenti il massimo e il valore assoluto.

Nell’immagine sottostante potete vedere il canale sinistro di una porzione di segnale registrato dal cajon con il suo relativo inviluppo:

Derivata

Per il calcolo della derivata ci siamo avvalsi di un buffer circolare di dimensione 24. Per ogni campione è stata effettuata una differenza tra l’inviluppo attuale e l’inviluppo calcolato 24 campioni indietro:

diff = env - buffer[buffer_idx]

dove

diff è un array di lunghezza 2 dove memorizziamo la differenza tra l’inviluppo corrente e quello calcolato 24 campioni indietro.

buffer_idx è una variabile di ciclo che va da 0 a 23.

buffer è un buffer circolare ovvero una matrice di dimensione 24 * 2 dove ad ogni iterazione si trovano gli ultimi 24 valori dell’inviluppo.

Nell’immagine sottostante potete vedere il canale sinistro di una porzione di segnale registrato dal cajon, il suo inviluppo e derivata:

Left/Right, FFT e rapporto

Per determinare la presenza di un onset ci siamo avvalsi dell’utilizzo di una soglia. Quando una delle due derivate supera la soglia memorizziamo se si è trattato del canale left o right basandoci sulla “temporizzazione” del colpo, ovvero se il colpo è stato captato prima dal microfono sinistro allora vuol dire che è stato dato sulla faccia sinistro, viceversa su quella destra. Subito dopo mettiamo da parte i 1024 campioni successivi, relativi al canale corrispondente, prima di verificare nuovamente la presenza di un onset.

Raccolti i 1024 passiamo al calcolo della FFT e del PSD che sarà necessario per determinare se l’onset rilevato è frontale o laterale.

Qui in basso potete vedere i psd delle due zone che ci interessa poter distinguere:

  • frontale
  • laterale
Frontale
Laterale

Si nota come i PSD dei colpi frontali sono molto diversi da quelli laterali. Dopo svariati esperimenti abbiamo scelto quelle che secondo noi sono le frequenze (ovvero indici del vettore corrispondente al PSD) significative da utilizzare per identificare la provenienza del colpo:

  • fascia verde: 86 - 172 Hz
  • fascia rossa: 258 - 344 Hz

Utilizzando la formula sottostante e confrontando il risultato ratio con una soglia siamo stati in grando di identificare la provenienza del colpo:

ratio = np.sum(psd[1:3]) / np.sum(psd[5:7])

Ora non resta che “suonare” il campione corrispondente.

Software

Python

Python è risultato il linguaggio di programmazione più adatto in fase prototipale e con le giuste aggiunte è stato performante anche nella realizzazione del progetto.

Numpy

Numpy è il principale pacchetto usato nel calcolo scientifico in Python. Alcune delle sue principali caratteristiche sono:

  • array N-dimensionali,
  • broadcasting delle funzioni,
  • tool per integrare codice C/C++ e Fortran,
  • funzioni per algebra lineare, trasformata di Fourier.

Cython

Cython è una estensione del linguaggio Python che permette di scrivere estensioni in C per Python. Esso ha le potenzialità di Python ma supporta ad esempio le chiamate a funzioni in C o la dichiarazione dei tipi sulle variabili. Questo permette al compilatore di generare del codice C veramente efficiente partendo da quello Cython.

Premix e Multithread

  • Premix

    Se il campione destro e sinistro vengono colpiti ad una distanza temporale tale per cui il primo dei due non è ancora finito quando l’altro inizia è necessario far suonare insieme i due campioni, ovvero effettuare un operazione di mix. L’operazione di mix consiste nel sommare i due vettori corrispondenti ai campioni da suonare, dimezzandone l’ampiezza. Il mix è dispendioso in termini di tempo ed è per questo che abbiamo scelto di creare una matrice di liste che chiamaremo matrice di premix. Con premix indichiamo l’operazione di mix sopra descritta fatta all’avvio del programma, prima che il musicista inizi a suonare, motivo per il quale utilizziamo premix al posto di mix. Come prima operazione abbiamo diviso i due campioni da suonare in fette grandi 512 campioni. Queste fette sono delle liste e nel caso in cui l’ultima ha lunghezza < 512, sono stati aggiunti degli zeri.

    Nella posizione [i,j] della matrice di premix si trova la fetta i-esima del campione destro mixata con la fetta j-esima del campione sinistro. La riga zeresima è riservata alle fette del solo campione sinistro, mentre la colonna zeresima a quelle del campione destro.

  • Multithread

    Per rispettare il più possibile il requisito del real-time ci siamo avvalsi dell’utilizzo dei Thread utilizzando il modulo di python threading. Nello specifico sono presenti due thread:

    • uno che ascolta: MAIN THREAD
    • uno che suona: PLAYER THREAD

    I due thread sono sincronizzati tramite un evento (una sorta di semaforo).

Workflow

Nell’immagine sottostante potete vedere il workflow del software, dove i box in arancione rappresentano le porzioni di codice scritte in Cython.

Dimostrazione

Ecco una dimostrazione del risultato conseguito