TensorFlow in Max/MSP: un esempio pratico

A un certo punto dell’esplorazione del mondo del deep learning applicato alla musica e al sound design, a tutti i musicisti passa per la testa una domanda: è possibile utilizzare le reti neurali, magari attraverso il framework TensorFlow, in Max/MSP?

La risposta è fortunatamente sì, in particolare attraverso la versione JavaScript di TensorFlow, ovverosia TensorFlow.js. Ovviamente ci sono anche altri modi per mettere in comunicazione questi due ambienti, ad esempio scambiando i dati tra TensorFlow e Max attraverso il protocollo OSC, ma l’uso di Tensorflow.js permette a Tensorflow di essere integrato all’interno di Max, utilizzando le potenzialità di Node for Max. Ma andiamo per passi proponendo un esempio pratico, che permette di fare una predizione dei contenuti di un file audio (o di un qualsiasi contenuto di un buffer) sfruttando il modello YAMNet, una rete che è in grado di distinguere tra 521 categorie diverse di suoni.

La patch, che è possibile scaricare con il relativo progetto qui sotto, è basata su un buffer, i cui valori vengono inviati a uno script Node.js che usa la rete YAMNet per predire, a intervalli di 0.48 secondi, il suono principale rilevato in quel frammento di audio. Una volta che la predizione è stata effettuata, si può quindi riprodurre il contenuto del buffer e visualizzare via via che tipo di suono è il più rappresentato in quel momento.

Vediamo ora le varie fasi della costruzione della patch, con l’intento di spiegare il progetto e la logica dietro di esso più che quello di fare un tutorial passo passo.

Per prima cosa creiamo un buffer con relativa waveform e una serie di controlli per gestire alcuni aspetti del suo contenuto, che vedremo successivamente. In questa parte è presente l’unico oggetto esterno che non fa parte nativamente di Max: si tratta di bufresample~, che ricampiona l’audio a 16 kHz, dato che è questa la frequenza a cui lavora YAMNet. bufresample~ fa parte del pacchetto HISSTools, scaricabile direttamente dal gestore di pacchetti di Max/MSP.

TensorFlow in Max/MSP: creazione patch parte 1 - buffer e controlli associati

In secondo luogo prepariamo l’ambiente Node.js, creando un oggetto node.script in cui inseriremo questo codice, le cui funzioni presenti saranno viste a breve:

const Max = require('max-api');
const tf = require('@tensorflow/tfjs');
const fs = require('fs');
const modelUrl = 'https://tfhub.dev/google/tfjs-model/yamnet/tfjs/1';
var channels = 1;

buffer = [];
main_sounds = [];
let model;

async function init()
{
	model = await tf.loadGraphModel(modelUrl, { fromTFHub: true });
}

async function readbuf_async(file) 
{
	data = fs.readFileSync(file);
	buffer = [];
	
	for (o = 0; o < data.length - (4 * channels); o += 4 * channels)
	{
		buffer.push(data.readFloatBE(o));
	}
	
	await Max.post("Buffer filled!");
	
};

async function predict()
{	
	waveform = tf.tensor(buffer);
	const [scores, embeddings, spectrogram] = model.predict(waveform);
  	maxscore = scores.mean(axis=0).argMax();

	scores_length = scores.shape[0];
	
	main_sounds = [];
	
	arr = scores.arraySync();
	
	for (i = 0; i < scores_length; i++)
	{
		main_sounds[i] = arr[i].indexOf(Math.max.apply(Math, arr[i]));
	}
	
	await Max.post("Prediction done!");
	await Max.outlet(main_sounds);
}	

Max.addHandler('init', () => 
{
	init();
});

Max.addHandler('clearbuf', (sample) => 
{
	buffer = [];
});

Max.addHandler('setchans', (chans) => 
{
	channels = chans;
});

Max.addHandler('readbuf', (file) => 
{
	readbuf_async(file);
	
});

Max.addHandler('predict', () =>
{
	predict();
});

All’oggetto node.script vanno collegati questi messaggi:

  • script npm install @tensorflow/tfjs: per installare TensorFlow.js
  • script start: per avviare lo script, può essere utile anche creare un messaggio script stop per fermarlo
  • init: per richiamare l’omonima funzione che carica il modello YAMNet. Questo può essere collegato a script start e avviato dopo un certo tempo (per sicurezza nella patch è di 2 secondi), in modo da essere sicuri che l’ambiente Node sia stato nel frattempo avviato
  • predict: per richiamare l’omonima funzione che effettua la predizione
  • setchans $1: per indicare allo script di quanti canali è composto il buffer. Attenzione: per la predizione verrà usato solo il primo canale. L’argomento che prende setchans deriva dall’ultimo outlet dell’oggetto info~, che si può collegare o direttamente oppure tramite una coppia di send/receive
  • readbuf $1: per indicare allo script il percorso del file temporaneo su cui è stato salvato il buffer. L’argomento viene fornito da un messaggio che lo precede che a sua volta viene riempito da un breve script JavaScript (integrato in Max, con l’oggetto js) che recupera in automatico il percorso della patch:
autowatch = 1;
inlets = 1;
outlets = 1;

function bang()
{
	var patcher_dir = this.patcher.filepath.replace(patcher.name + ".maxpat", "");
	outlet(0, patcher_dir + "samplebuf.tmp");
}

Questo stesso script invierà il nome del file anche al messaggio samptype float32, writeraw $1, da collegare al buffer, in modo che da questo venga esportato un file temporaneo che viene poi letto da Node nella funzione readbuf_async (richiamata con il readbuf $1 visto poc’anzi).

TensorFlow in Max/MSP: script Node.js e funzioni correlate

Tornando allo script Node, questo ha un outlet che invia a un multislider le categorie predette dalla funzione predict: si tratta di una serie di numeri, tra 1 e 521, che indicano per ogni frammento di 480 millisecondi qual è il suono più rappresentativo in quel punto. Riproducendo quindi il file con un play~ il buffer, e incrementando un counter ogni 480 ms (attraverso un metro), sarà possibile reperire, in maniera sincronizzata con la riproduzione, l’indice della categoria sonora. Questa categoria è inviata a un coll che fa corrispondere a ogni numero il nome della categoria, in modo da poterlo visualizzare in tempo reale.

TensorFlow in Max/MSP

Con questa stessa impostazione di base è chiaramente possibile utilizzare altre reti neurali e anche addestrarne di nuove. Di principio è sufficiente installare TensorFlow.js in Node e creare le funzioni necessarie per scambiare i dati tra Max/MSP e l’ambiente Node, che è bene ricordare che, funzionando in un processo separato rispetto a Max, non è in grado di accedere direttamente a gran parte degli oggetti della patch (come ad esempio il contenuto di un buffer, contrariamente a quanto avviene per l’oggetto js). Un’altra cosa importante da segnalare è che dato che Node for Max lavora in modo asincrono e separato da Max, operazioni lunghe come ad esempio l’addestramento di una rete o la predizione su una serie di dati non bloccano l’esecuzione della patch, che può continuare a lavorare senza interruzioni.

Continue Reading