En continuant à naviguer sur mon site, vous consentez à ce que j'utilise des cookies pour collecter les statistiques de visites. En savoir plus

#Informatique Développement du site Utopic Radio - Partie 3 : Afficher une animation visuelle

On continue le dossier radio avec un article un peu plus mathématique. Ici, je détaille comme je suis parvenu à afficher une animation visuelle sur mon player quand la musique est jouée.

Tout d'abord, à quoi cette animation correspond-elle ? Oui, car elle ne sort pas de nulle part, et représente réellement quelque chose. Il s'agit de la répartition des fréquences émises par la musique diffusée. En clair, c'est un graphique représentant les basses à gauche, les mediums au milieu, et les aigus à droite.

Sur un vrai graphique, ça ressemble plus à quelque chose comme ça :

Commençons par le code HTML. Il est on ne peut plus simple !

<canvas id="fft"></canvas>

Vous mettez ça où vous voulez dans votre page (du moment que l'endroit où vous mettez cette balise soit convenable… dans le player par exemple). Pour le CSS, à vous de positionner le canvas comme bon vous semble, pour que la petite décoration soit affichée comme vous le souhaitez, mais retenez bien une chose : il y a la taille du canvas que l'on affiche, et la taille de la zone de dessin, qui sera déformée plus ou moins en fonction de ce que vous en faites en CSS. Dans notre cas, ça ne devrait pas être bien grave, mais c'est toujours bon à savoir quand on utilise un canvas.

On passe aux choses sérieuses : le javascript.

Comme pour le traitement de son, on va partir d'un contexte audio et créer un nœud d'analyse.

var context = new (AudioContext || webkitAudioContext)();
var source = context.createMediaElementSource(audio);
var analyser = context.createAnalyser();
var canvas = document.getElementById('fft');

Pour l'initialisation, j'ai toute une panoplie de variable que je détaille ci-dessous :

var drawingPrecision = 2 // Précision du tracé
    , originalWidth = 1024 // Taille du graphe FFT
    , fillStyle = 'white' // Couleur de remplissage
    , canvasWidth = originalWidth * drawingPrecision // Largeur de la zone de dessin
    , canvasHeight = 128 // Hauteur de la zone de dessin
    , ctx = canvas.getContext('2d') // En dessin aussi, on a un contexte…
    , fullScale = 256 // Valeur d'un entier représentant 0dB (en full-scale) (1)
    , logWantedScale = 20 // Une "décade" (2)
    , logOriginalScale = 10 // Une vraie décade (2)
    , logScale = logWantedScale / logOriginalScale // Valeur d'échelle logarithmique (2)
    , decades = 4 // Nombre de décades à afficher (2)
    , decadeWidth = canvasWidth / decades // Taille d'une décade (2)
;

Quelques précisions s'imposent :

  • (1) : en audio on travaille en "full-scale". Concrètement, cela implique que les valeurs avec lesquelles nous travailleront seront comprises entre 0dB pour le son le plus fort et -∞dB pour le son le plus fort. Informatiquement parlant, cela se représente un peu différement (avec des nombres entiers) : 0 représentera -∞dB et 255 représentera 0dB.
  • (2) : Les valeurs des différentes fréquences nous sont envoyées dans un tableau de 1024 données (variable "originalWidth"). À chaque donnée correspond une fréquence, le calcul est vite fait : avec un échantillonage du son à 44100Hz (soit 22050 par canal), on divise 22050 par 1024 et on peut voir que chaque donnée couvre une plage de 21.53Hz environ ; autrement dit, la première donnée représentera la puissance des fréquences entre 0 et 21.53Hz, la deuxième donnée correspondra aux fréquences comprises entre 21.53 et 43.07Hz, etc. Cette décomposition s'appelle une Transformée de Fourier. Je suis incapable de décrire mathématiquement comment ça fonctionne, donc retenez bien une chose : on décompose un signal reçu en fréquences, ce qui permet d'observer leur répartition dans le spectre audio. Il reste un problème : les fréquences basses et les fréquences mediums sont les plus significatives pour nous (donc celles qui vont le plus varier avec le temps), et le tableau qui nous est envoyée par le système est un tableau qui réparti équitablement les fréquences. Pour pouvoir représenter tout cela de manière plus "jolie", on va devoir faire quelques calculs logarithmiques que je détaille plus loin.

Maintenant qu'on a créé toutes les variables qui vont nous être utiles, on initialise tout ça :

var FFT_init = function() {
    canvas.width = canvasWidth;
    canvas.height = canvasHeight;
    source.connect(analyser);
    window.requestAnimationFrame(FFT_drawFFT);
};

Ici, on indique la taille de la zone de dessin du canvas (qui diffère de sa taille réelle), on connecte la source sonore au nœud d'analyse, et on demande au système d'effectuer le premier tracé grâce à la fonction FFT_drawFFT que voici :

var FFT_drawFFT = function() {
    window.requestAnimationFrame(FFT_drawFFT);
    var fbca = new Uint8Array(analyser.frequencyBinCount);
    analyser.getByteFrequencyData(fbca);

    ctx.clearRect(0, 0, canvasWidth, canvasHeight);
    ctx.fillStyle = fillStyle;
    ctx.beginPath();

    ctx.moveTo(0, canvasHeight);
    for (var k in fbca) {
        var v = fbca[k];
        var x, y;
        x = (k == 0 ? 0 : FFT_scaleLog(k));
        y = canvasHeight - (v * canvasHeight / fullScale);
        ctx.lineTo(x, y);
    }
    ctx.lineTo(canvasWidth, canvasHeight);
    ctx.closePath();

    ctx.fill();
};

Sitôt appelée, cette fonction redemande au système de mettre l'affichage à jour, pour pouvoir conserver une animation fluide. Ensuite, on récupère les données grâce aux lignes suivantes :

var fbca = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(fbca);

Je ne rentre pas dans les détails, mais grâce à cela, notre variable fbca deviendra notre tableau d'entiers que l'on a évoqué tout à l'heure.

ctx.clearRect(0, 0, canvasWidth, canvasHeight);
ctx.fillStyle = fillStyle;
ctx.beginPath();

On démarre le dessin. Pour cela, on trace un grand rectangle d'effacement sur toute la largeur et toute la hauteur de la zone. Ensuite, on indique quel "pinceau" utiliser et démarre un chemin de traçage.

ctx.moveTo(0, canvasHeight);
for (var k in fbca) {
    var v = fbca[k];
    var x, y;
    x = (k == 0 ? 0 : FFT_scaleLog(k));
    y = canvasHeight - (v * canvasHeight / fullScale);
    ctx.lineTo(x, y);
}

Dans la boucle ci-dessus, on place notre pinceau tout en bas et à gauche de notre zone de dessin, puis on boucle sur tout le tableau pour récupérer les différentes valeurs, et successivement, on va déplacer notre pinceau sur les différents points de la courbe, que l'on trace petit à petit.

Ici ces deux lignes doivent vous interpeler :

x = (k == 0 ? 0 : FFT_scaleLog(k));
y = canvasHeight - (v * canvasHeight / fullScale);

Détaillons d'abord le calcul de x. Si nous traçons notre premier point, alors on place automatiquement le crayon tout à gauche. Sinon, on va calculer la position du point x sur une échelle logarithmique à l'aide de la fonction FFT_scaleLog() que voici :

var FFT_scaleLog = function(x) {
    return Math.log10(x / logScale) * decadeWidth
};

Pas bien compliqué, n'est-ce pas ? Cette petite fonction va donc permettre de déplacer le crayon au bon endroit (sur l'axe horizontal) pour représenter une échelle logarithmique et non linéaire.

Pour le calcul de la coordonée y, on fait ici un simple produit en croix pour trouver la hauteur du point, et comme notre axe des Y est orienté vers le bas (on est dans un repère fait de pixels, pas comme en mathématiques), on effectue une simple soustraction par la hauteur de notre zone de dessin pour trouver la position réelle de notre point, puis que nous voulons afficher un graphique qui s'étend vers le haut.

Ce n'est pas plus compliqué que cela ! Il vous reste une dernière chose à faire…

FFT_init();

… lancer la machine !

Voici donc le code en entier que vous êtes tout à fait libre et en droit de pomper :

var context = new (AudioContext || webkitAudioContext)();
var source = context.createMediaElementSource(audio);
source.connect(context.destination);

var drawingPrecision = 2
    , originalWidth = 1024
    , fillStyle = 'white'
    , analyser = context.createAnalyser()
    , canvasWidth = originalWidth * drawingPrecision
    , canvasHeight = 128
    , canvas = document.getElementById('canvas')
    , ctx = canvas.getContext('2d')
    , fullScale = 256
    , logWantedScale = 20
    , logOriginalScale = 10
    , logScale = logWantedScale / logOriginalScale
    , decades = 4
    , decadeWidth = canvasWidth / decades
;

var FFT_init = function() {
    canvas.width = canvasWidth;
    canvas.height = canvasHeight;
    source.connect(analyser);
    window.requestAnimationFrame(FFT_drawFFT);
};

var FFT_scaleLog = function(x) {
    return Math.log10(x / logScale) * decadeWidth
};

var FFT_drawFFT = function() {
    window.requestAnimationFrame(FFT_drawFFT);
    var fbca = new Uint8Array(analyser.frequencyBinCount);
    analyser.getByteFrequencyData(fbca);

    ctx.clearRect(0, 0, canvasWidth, canvasHeight);
    ctx.fillStyle = fillStyle;
    ctx.beginPath();

    ctx.moveTo(0, canvasHeight);
    for (var k in fbca) {
        var v = fbca[k];
        var x, y;
        x = (k == 0 ? 0 : FFT_scaleLog(k));
        y = canvasHeight - (v * canvasHeight / fullScale);
        ctx.lineTo(x, y);
    }
    ctx.lineTo(canvasWidth, canvasHeight);
    ctx.closePath();

    ctx.fill();
};

FFT_init();

Je vous recommande bien évidemment de tester tous ces développements sur des pages à part avant de les intégrer à vos sites, bien entendu.

Pour information, dans mon article précédent, j'évoquais le codage à la main d'un compresseur acoustique. Sachez que c'est en cours… J'ai fini par trouver une source intéressante, et je cherche encore un moyen de bien l'intégrer. J'en appelle toujours aux experts en la matière : mon but étant de faire un contrôle du gain automatique, et la manière la plus simple que je vois est de faire appel à un compresseur avec, en sidechain, le même signal auquel on a appliqué un coupe-bas.

Rédigé le .

Commentaires

comments powered by Disqus