Progetto

MATLAB

Il progetto richiede l’analisi audio effettuata via MATLAB offline attraverso il MIRToolBox, reperibile gratuitamente a questo indirizzo, che fornisce degli ottimi strumenti di analisi dei segnali su matlab. Il tool si basa su una struttura dati personalizzata che deve essere tradotta in dato puro per poter essere manipolata con gli strumenti esterni al pacchetto, mentre ogni oggetto “miraudio” può essere manipolato in ogni modo con gli strumenti forniti. Per effettuare delle analisi complesse bisogna prima creare l’oggetto miraudio dal file audio:

files = dir('*.wav');
mir_a = miraudio(file.name,'Normal','Mono','Label',0);

Il tool riceve sia i files wav che in MP3 (solo su alcuni sistemi operativi) e li traduce in oggetti: da qui si può cambiare il campionamento della traccia, operare separazione di frames, analisi del pitch e pure studiare le features per machine learning.

Fs = 2048;
FsFb = 512;
Nch = 10;
mir_env = mirenvelope(mir_a,'Sampling',Fs); 
mir_a = miraudio(mir_a,'Sampling',FsFb);
mir_fb = mirfilterbank(mir_a,'NbChannels',Nch);

Per non eccedere come quantità di dati e rendere fruibile l’esperienza ho dovuto decimare i campioni ottenuti, quindi sottocampionare la traccia a seconda dell’analisi che dovevo fare. Un’altra operazione di cui voglio sgravare l’esecuzione è la normalizzazione dei campioni: portando i campioni in intervalli tra 0 e 1 o tra -1 e +1 semplifico le operazioni di scalatura che si applicheranno ai campioni per la generazione di eventi grafici:

env_norm = mirgetdata(temp_env);
env_norm = round(env_norm/max(abs(env_norm)),4);

fb_anorm = mirgetdata(temp_fbenv);
            fb_norm(:,:) = fb_anorm(:,1,:);
            for i = 1:Nch
                fb_max = max(fb_norm(:,i));
                fb_norm(:,i) = round(fb_norm(:,i)./fb_max,4);
            end
pitch = mirgetdata(temp_pitch);
            pitch = mean(pitch); % se ho più pitch diversi ne ho uno medio
            while(pitch > 440) 
                pitch = pitch - 440;
            end
            pitch = round((pitch/440)*255);

Una volta che ho le feature che voglio usare per la rappresentazione grafica, vado a creare una struttura dati accessibile dallo script: i file JSON sono delle strutture dati ben precise atte alla comunicazione di dati tra server e client. Da MATLAB è possibile esportare variabili, array, matrici e struct formattate bene secondo questo standard in un file .JSON

temp_s = struct('title', get(mir_a,'Label'),'Fs',Fs,'env',env_norm,'FsFb',FsFb,'Nch',Nch,'filterbank',fb_norm);
temp_name = sprintf('%s.json', tracks(file,:));
savejson('track',temp_s,temp_name);

Javascript: WEBGL

In HTML5 è stato definito un metodo per poter rappresentare graficamente eventi senza dover ricorrere a plugins esterni ed affidandosi alla capacità grafica del computer e del browser. Il codice viene scritto in Javascript e integrato nella pagina html grazie al tag <canvas>. In questa applicazione ho voluto sfruttare per la riproduzione audio le funzionalità del browser, usando il tag HTML5 <audio> per caricare le tracce da riprodurre: le prestazioni di esecuzione sono quindi legate alla capacità del browser e alla connessione per il download dell’audio.

Per semplificare l’organizzazione delle tracce ho scritto uno script di “loading” che mi organizza il caricamento asincrono dei dati di analisi al caricamento della pagina. Purtroppo il metodo di javascript “parseJSON()” non sembra funzionare con il caricamento asincrono del file, per questo ho ripiegato sul metodo di jQuery $.getJSON per recuperare le strutture dati generate da MATLAB. La libreria jQuery non verrà usata ulteriormente in questo progetto, se si riuscisse a sistemare il caricamento e il parsing dei files JSON se ne potrebbe fare a meno.

Il problema principale di un’applicazione audio/video è la sincronia tra gli elementi riprodotti e l’audio eseguito: come mantenere la coerenza? come possiamo accedere alle informazioni sonore? La semplice esecuzione grafica delle features estratte può essere risolta dal computer in pochi secondi ma vorremmo che ciò che viene rappresentato a video sia coerente con il campione audio eseguito in quell’istante. Javascript e HTML5 non forniscono dei metodi ufficiali per capire a che punto della canzone si è, bisogna accettare un minimo di imprecisione.

var timeNow = (Math.trunc(audio.currentTime*100))/100;
var sampleNow = Math.round(timeNow * Fs) - Fs;

Il metodo “currentTime” mi fornisce a quale secondo di riproduzione siamo della traccia correntemente eseguita dal browser: è un metodo molto poco preciso e asincrono che potrebbe portare a risultati erronei per l’analisi in tempo reale. La precisione è garantita al centesimo di secondo (ma spesso anche solo al decimo). Per eliminare dati spuri che vengono proposti ho deciso di trocanre le misurazioni al centesimo di secondo, successivamente per capire a quale campione di esecuzione siamo, moltiplico il frammento temporale per la frequena di campionamento: la sottrazione mi serve per poter avere la rappresentazione grafica coordinata con l’esecuzione audio. Normalmente vengono usati dei metodi di analisi in tempo reale per questo tipo di applicazioni, basandosi su buffer temporanei in modo da non dover avere una struttura dati complessa e pesante ma potersi basare solo su quello che effettivamente è in riproduzione in quel momento. I metodi per questo tipo di analisi vengono forniti da Web Audio integrato in HTML5.

  1. Linea d’Onda

    Per la prima rappresentazione richiesta, ho sfruttato il metodo “line” di jvascript, che disegna una linea

    canvasCtx.lineWidth = 2;
    canvasCtx.strokeStyle = 'rgb(0,0,0)';
    canvasCtx.beginPath();var sliceWidth = WIDTH * 1.0 / Fs;
    var x = 0;
    for(var i = 0; i < Fs; i++) {
        if ( timeNow == 0) {
            var v = 2;
        } else {
            var v = parseFloat(track.env[i+sampleNow]);
            v = (v * -2) + 2;
        }
        var y = v * HEIGHT/2;
        if(i === 0) {
            canvasCtx.moveTo(x, y);
        } else {
            canvasCtx.lineTo(x, y);
        }
        x += sliceWidth;
    }
    canvasCtx.lineTo(canvas.width, y);
    canvasCtx.stroke();
  2. Spettrogramma 2D

    In questo caso andiamo a colorare dei rettangoli, proporzionalmente al numero di campioni e di canali, per ogni canale  ad ogni campione in base alla sua intensità. La scala cromatica va dal blu intenso per valori molto bassi al giallo per valori alti, passando dal verdino. Essendo i colori in base 256 è normale  che ci sia dell’aliasing tra i dati.

    var pxWidth = Math.trunc((WIDTH / ( trackLength * FsFb))*1000)/1000;
    var pxHeight = HEIGHT / chCount;
    var xNext = Math.trunc(audio.currentTime * FsFb) * pxWidth;
    function draw() {
        drawVisual = requestAnimationFrame(draw);
    //  Gestione del tempo di riproduzione
        var timeNow = (Math.trunc(audio.currentTime*100))/100;
    //  Campione attuale:
        var sampleNow = Math.round(timeNow * FsFb);
        var pxColor = 0;
        var xNow = xNext;
        xNext = sampleNow * pxWidth;
        var y = 0;
        for(var i = 0; i < chCount; i++) {
            if(timeNow == 0){
            // situazione iniziale, cavas pulito.
                canvasCtx.fillStyle = 'rgb(248,248,248)';
                canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
                    } else {
                pxColor = Math.trunc(parseFloat(track.filterbank[sampleNow][i])*255);
                    }
            //  colore in base all'intensità
            if(pxColor <= 128){
                var colorR = 0;
                var colorG = pxColor*2;
                    } else {
                var colorR = (pxColor-127)*2;
                var colorG = 255;
                    }
            var colorB = 255 - pxColor;
            canvasCtx.fillStyle = 'rgb('+colorR+','+colorG+','+colorB+')';
            canvasCtx.fillRect(xNow, y, xNext-xNow, pxHeight);
            y += pxHeight;
                }
                   }
    
  3. Istogramma Spettrale

    Semplificazione, in quanto rappresento soltato i valori attuali dei campioni. L’altezza è proporzionale all’intesità sul canale.

    var barWidth = (WIDTH / chCount) - 1;
    var barHeight;
    var valueSample;
    var x = 0;
    for(var i = 0; i < chCount; i++) {
        if(timeNow == 0){
            valueSample = 0.0; // valore minimo in entrata
        } else {
            valueSample = parseFloat(track.filterbank[sampleNow][i]);
        }
        if(valueSample <= 0.500){ // da blu, valore basso, a verdino 
            var colorR = 0;
            var colorG = Math.trunc(valueSample*255)*2;
        } else { 
            var colorR = Math.trunc((valueSample-0.5)*255)*2;
            var colorG = 255;
        }
        var colorB = Math.trunc(255-valueSample*255); // scalo su tutto il blu indipendentemente.
        canvasCtx.fillStyle = 'rgb('+colorR+','+colorG+','+colorB+')';
        barHeight = valueSample * HEIGHT;
        canvasCtx.fillRect(x,HEIGHT - barHeight, barWidth , barHeight);
        x += barWidth + 1;
    }
  4. Rappresentazione Personalizzata

    Per questa rappresentazione ho usato le librerie THREE.js e il codice è troppo lungo e scomodo da leggere per essere messo qui, potete analizzare il codice javascript contenuto nel file app.js .

Questo progetto mi ha permesso di dare uno sguardo alla grafica in tempo reale web, come ci si muove e quali possibilità si abbiano, spero che queste pagine possano essere d’aiuto per il futuro.

Rispondi

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.