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 2 : Le traitement de son

Écouter une radio musicale sans traitement de son, c'est comme manger des frites sans sel. Ça se fait, mais c'est toujours mieux avec. Radionomy proposant un traitement de son très sommaire, il m'a donc fallu ruser pour obtenir un son acceptable sur ma radio, sans devoir surcharger mon serveur… et pour cela il existe une solution très simple, mais encore expérimentale pour les navigateurs Web.

1 - À notre disposition

Les navigateurs proposent depuis peu une formidable API Audio pour le web. Cela ne s'arrête pas à un player intégré qui permet de se passer d'Adobe Flash Player, non. L'API est bien plus puissante que cela et permet entre autre d'ajouter des effets (Réverbération, Compression, Flanger, …) sur les streams audios.

C'est cette partie de l'API qui va nous intéresser, et plus particulièrement à trois effets de traitement du signal : le filtre paramétrique, le compresseur, et le gain. Rien qu'avec ces trois petites choses-là, bien agencées, on peut réaliser des merveilles !

2 - Organiser notre traitement de son

Avant de coder, il va d'abord falloir organiser notre chaîne d'effets. Pour ma part, j'ai choisi d'organiser ma chaîne comme ceci :

Entrée → Gain → Compresseur Multibande → Bass Enhancer → Limiteur → Sortie

(Chaîne d'effet librement inspirée de la défunte version gratuite de SoundSolution)

Détaillons un peu tout cela, car je vous ai dit que nous n'avions que trois effets à notre disposition. Le compresseur multibande n'est jamais qu'un composé de plusieurs effets :

Entrée → Filtre → Compresseur → Gain → Sortie

Ce schéma est à répéter autant de fois qu'il y a de bandes désirées pour le compresseur multibandes, en ayant bien en tête que les entrées sont toutes branchées sur la même source audio, et que les sorties viendront se mixer entre elles pour faire une seule et unique sortie.

Quant au Bass Enhancer, il s'agit d'un simple filtre ; et le Limiteur est un compresseur au ratio infini (ou presque), à l'attaque et la relâche très rapide.

3 - Effectuer et enregistrer les réglages

Alors là, ce n'est pas une partie de plaisir. Vous avez trois solutions :

  • Soit vous mettez toutes vos données de traitement de son dans un fichier à part, vous faites le système qui va permettre de traduire ces données en traitement dans le navigateur, et vous actualisez votre page à chaque changement (ce qu'il faut faire à terme).
  • Soit vous mettez tout dans votre page et vous utilisez un framework comme AngularJS pour effectuer les réglages dynamiquement (ce que j'ai fait au tout début).

Je vais d'abord rapidement expliquer la deuxième solution : AngularJS est un framework qui à l'énorme avantage de plutôt bien gérer le côté dynamique du MVC : on change le contenu d'un champ de formulaire → la donnée est immédiatement mise à jour ; on change la donnée → le champ de formulaire est mis à jour. C'est magique !

Bref, là où ça nous sera bien utile, c'est que l'on pourra, grâce à ce mécanisme, régler finement notre traitement sans avoir à recharger la page dès qu'un changement est effectué. Je vous laisse vous informer sur le fonctionnement d'Angular, il existe même de très bons tutos pour s'initier.

Venons-en au fait : vous avez effectué tous vos réglages et vous souhaitez les mettre dans un fichier à part, pour ne pas avoir à toucher à votre script de manière systématique.

Il vous faudra choisir deux choses : le format de sérialisation de vos données, et la manière dont vous les organiserez. Pour ma part, j'ai fait simple. J'ai choisi le format YAML, pour son côté très peu verbeux et très puissant à la fois.

Voici donc mon fichier de configuration de traitement de son :

---
- type: gain
  settings:
    gain: 2
- type: multiband
  settings:
    bands:
    - centerFrequency: 170
      attack: 0.06
      release: 0.23
      ratio: 5
      threshold: -26.3
      gain: 0.67
    - centerFrequency: 1000
      attack: 0.04
      release: 0.42
      ratio: 5
      threshold: -21.5
      gain: 0.73
    - centerFrequency: 3200
      attack: 0.03
      release: 1
      ratio: 4
      threshold: -19.8
      gain: 0.63
    - centerFrequency: 7200
      attack: 0.03
      release: 1.5
      ratio: 3
      threshold: -23.2
      gain: 0.61
    - centerFrequency: 12000
      attack: 0.03
      release: 2.7
      ratio: 3
      threshold: -22.4
      gain: 0.71
- type: filter
  settings:
    type: peaking
    frequency: 130
    q: 0.5
    gain: 1.5
- type: compressor
  settings:
    attack: 0.001
    release: 0.01
    ratio: 20000
    threshold: -0.1

On peut déjà apercevoir la chaîne d'effet que je vous ai décrite : ici les effets seront ajoutés dans l'ordre où ils sont déclarés dans le fichier.

Pour pouvoir le lire depuis mon script JS (puisque l'API Audio pour le Web est avant tout faite pour être utilisée en JS), on va coder une petite moulinette qui va traduire ces données en JSON.

<?php
echo json_encode(yaml_parse(file_get_contents(__DIR__.'/data/soundProcessing.yml')));

Attention : ici, la fonction yaml_parse() n'est disponible que si vous avez installé le plugin YAML pour PHP.

L'avantage de ce petit fichier est qu'à l'avenir, on pourra mettre des contrôles sur le HTTP_REFERER pour éviter qu'il ne se fasse pomper par d'autres serveurs et sites Internet.

4 - Traitement de son en javascript, dans le vif du sujet !

Vous l'aurez deviné, nous allons faire appel à ce fichier à l'aide d'une requête AJAX, et initialiser le traitement de son ensuite avec les données reçues.

À ce stade, il n'est plus nécessaire d'utiliser AngularJS, mais pour ma part je l'ai conservé car il m'a rendu bien des services, et me sert pour d'autres fonctionnalités du site qu'on détaillera plus tard.

Je vous donne le code (débarrassé de pas mal de choses qui ne nous serviront pas ici), et on l'explique après :

$scope.audio = document.getElementById('audio');
$scope.audio.volume = 0;
$scope.audio.play();

angularHttpPost($http, '/public/sp.php', {}, function(data) {
    var FXs = {
        gain: function(ctx, input, settings) {
            var fx = ctx.createGain();
            fx.gain.value = settings.gain;
            input.connect(fx);
            return fx;
        },
        filter: function(ctx, input, settings) {
            var fx = ctx.createBiquadFilter();
            fx.type = settings.type;
            fx.frequency.value = settings.frequency;
            fx.Q.value = settings.q;
            fx.gain.value = settings.gain;
            input.connect(fx);
            return fx;
        },
        compressor: function(ctx, input, settings) {
            var fx = ctx.createDynamicsCompressor();
            fx.attack.value = settings.attack;
            fx.release.value = settings.release;
            fx.ratio.value = settings.ratio;
            fx.threshold.value = settings.threshold;
            input.connect(fx);
            return fx;
        },
        monoband: function(ctx, input, settings) {
            var filter = FXs.filter(ctx, input, {
                type: 'bandpass',
                frequency: settings.centerFrequency,
                q: 0.5,
                gain: 1.0
            });
            var compressor = FXs.compressor(ctx, filter, {
                attack: settings.attack,
                release: settings.release,
                ratio: settings.ratio,
                threshold: settings.threshold
            });
            var gain = FXs.gain(ctx, compressor, {
                gain: settings.gain
            });
            return gain;
        },
        multiband: function(ctx, input, settings) {
            var bands = [];
            var joiner = ctx.createGain();
            for (var b in settings.bands) {
                var band = settings.bands[b];
                var monoband = FXs.monoband(ctx, input, band);
                monoband.connect(joiner);
            }
            return joiner;
        }
    };

    var context = new (AudioContext || webkitAudioContext)();
    var source = context.createMediaElementSource($scope.audio);

    var input = source;
    for (var e in data) {
        var effect = data[e];
        input = FXs[effect.type](context, input, effect.settings);
    }
    input.connect(context.destination);
});

Dans l'ordre :

$scope.audio = document.getElementById('audio');
$scope.audio.volume = 0;
$scope.audio.play();

D'abord, on cherche l'élément audio déclaré dans notre code HTML comme ceci :

<audio id="audio" src="http://url/de/mon/flux.mp3"></audio>

Notez qu'on met le volume à 0 et qu'on joue le stream immédiatement. Pourquoi ? Parce que cela permettra à l'auditeur de pouvoir écouter la webradio dès qu'il cliquera sur le bouton de lecture, sans devoir attendre le chargement des premiers buffers en mémoire. Il y a également un autre avantage qui vous concerne, vous, producteur de radio, que je vous laisserai le soin de deviner :)

Rappel important : on n'impose JAMAIS à un visiteur d'écouter votre radio sans qu'il en ait explicitement fait la demande (en cliquant sur le bouton play par exemple), pour deux raisons : la première étant qu'en général un visiteur vient d'abord voir votre site avant d'écouter votre radio, et il devra chercher dans 90% des cas le bouton Stop… c'est pas vraiment ce que vous souhaitez. Ensuite, imaginez que votre auditeur a 14 ans, il est 23h, il veut écouter discrètement votre superbe libre antenne mais est censé dormir. Il va sur votre site pour écouter la radio, mais avait oublié que plus tôt dans la journée, il s'était défoulé en écoutant son tube préféré à fond dans sa chambre… et là, c'est le drame : toute la maison est réveillée ! Plus sérieusement, vous risquez réellement d'assourdir une personne qui a un casque sur les oreilles, qui avait dû monter le son à 100%, parce que la radio concurrente qu'elle écoutait juste avant vous a des problèmes et diffuse à un volume très faible ; quand cette personne va ouvrir votre site… c'est le drame pour ses oreilles !

Revenons-en à nos moutons. Observons la ligne qui suit :

angularHttpPost($http, '/public/sp.php', {}, function(data) {
    // …
});

La fonction angularHttpPost() permet juste de simplifier l'écriture de la requête AJAX, en utilisant la méthode POST. On fait donc ici appel à notre script de tout à l'heure, qui permet de convertir les données YAML en JSON.

La fonction callback est appelée une fois la requête terminée, il est donc évident que nous allons placer tout notre code source concernant le traitement de son à l'intérieur.

Nous allons détailler la déclaration des effets après. En attendant, on continue avec :

var context = new (AudioContext || webkitAudioContext)();
var source = context.createMediaElementSource($scope.audio);

Ici, on initialise le contexte audio en faisant appel à l'objet adéquat, ce qui explique l'instanciation un peu bizarre mais tout à fait correcte de l'objet AudioContext.

Note : l'API AudioContext étant encore expérimentale, j'ai dû faire toute une panoplie de contrôles pour n'autoriser l'exécution du traitement de son qu'avec Google Chrome sur ordinateur de bureau. Les autres navigateurs et les autres supports (mobiles, tablettes, …) ne sont pas encore pris en charge… ça viendra avec le temps. D'où la nécessité de traiter une majorité de cas compatibles, pour que le plus de personnes possible puisse profiter du traitement de son.

Le contexte audio va nous permettre d'avoir à disposition un contexte de développement. C'est en quelque sorte la combinaison entre une boîte à outils et une usine spécialisés dans l'audio pour les navigateurs.

La première chose que l'on fait, une fois le contexte créé, c'est de créer une nouvelle source de données audio à l'aide de la méthode context.createMediaElementSource() qui prend un objet Audio en paramètre.

var input = source;
for (var e in data) {
    var effect = data[e];
    input = FXs[effect.type](context, input, effect.settings);
}
input.connect(context.destination);

Les sources (dont on vient de parler) et les effets sont considérés comme des nœuds que l'on va relier entre eux. Si on reprend notre chaîne d'effets de tout à l'heure :

Entrée → Gain → Compresseur Multibande → Bass Enhancer → Limiteur → Sortie

Ici, chaque terme est un nœud, et chaque flèche est une liaison indiquant dans quel sens l'audio navigue.

Pour modéliser cette chaîne, on va donc itérer sur le tableau JSON reçu grâce à la requête AJAX pour créer tous nos effets et les relier entre eux. C'est ce que fait notre boucle for() : on récupère les effets un par un, et on appelle la fonction associée (que nous détaillons après), en envoyant les réglages associés.

La dernière ligne correspond au dernier branchement : on connecte le dernier effet créé à la destination, ce qui va permettre à l'audio de ressortir et d'arriver jusqu'à nos enceintes.

Vous l'aurez compris les connexions se font selon le modèle suivant :

noeudAudioEntree.connect(noeudAudioSortie);

Ne reste plus qu'à détailler nos fonctions d'effets. On va commencer par la plus simple, le gain :

var FXs = {
    gain: function(ctx, input, settings) {
        var fx = ctx.createGain();
        fx.gain.value = settings.gain;
        input.connect(fx);
        return fx;
    },
    // …
}

Première étape : on demande au contexte de créer un nouveau nœud qui sera notre effet de gain. Deuxième étape : on affecte la valeur. Troisième étape, on connecte notre entrée au nœud de gain, et on renvoie ce nouveau nœud.

Ce nouveau nœud est réutilisé comme nœud d'entrée pour l'effet suivant dans notre boucle for() de tout à l'heure : d'où les connexions successives.

Le filtre paramétrique et le compresseur sont des effets que je nomme comme "élémentaires", car ils ne combinent pas plusieurs effets. Ils sont construits sur le même modèle que le gain. Ce qui n'est pas le cas de "monoband".

monoband: function(ctx, input, settings) {
    var filter = FXs.filter(ctx, input, {
        type: 'bandpass',
        frequency: settings.centerFrequency,
        q: 0.5,
        gain: 1.0
    });
    var compressor = FXs.compressor(ctx, filter, {
        attack: settings.attack,
        release: settings.release,
        ratio: settings.ratio,
        threshold: settings.threshold
    });
    var gain = FXs.gain(ctx, compressor, {
        gain: settings.gain
    });
    return gain;
},

Comme vous pouvez le constater, on retrouve notre chaîne d'effets au sein d'une seule bande : un filtre, un compresseur, et un gain. Au moment de la création de chacun d'eux, on utilisera le même procédé que dans notre boucle for() : celui de passer le nœud précédent au nœud nouvellement créé.

La fonction monoband ne sera pas appelée autre part que dans la fonction d'effet multiband :

multiband: function(ctx, input, settings) {
    var bands = [];
    var joiner = ctx.createGain();
    for (var b in settings.bands) {
        var band = settings.bands[b];
        var monoband = FXs.monoband(ctx, input, band);
        monoband.connect(joiner);
    }
    return joiner;
}

Dans cet effet, il y a deux choses à voir, la première étant qu'on a une nouvelle boucle for() pour itérer sur toutes les bandes qui toutes sont déclarés de la même manière dans nos données de réglages. La deuxième chose à voir étant que chaque bande est connecté au même nœud de sortie, le "joiner" qui n'est ni plus ni moins qu'un gain à +0dB. C'est ce nœud qu'on retourne en sortie.

5 - En conclusion

Vous avez maintenant toutes les billes pour coder votre propre traitement de son. Je vous propose ici une manière de l'implémenter, mais libre à vous de créer des chaînes d'effets bien plus complexes pour répondre à vos besoins.

Gardez bien en tête plusieurs choses :

  • Il vous faut un moyen de stocker votre traitement de son sans devoir systématiquement toucher à vos sources dès que vous voulez opérer un changement ;
  • Le traitement de son est une chaîne d'effets élémentaires combinés ensemble. Attention, quand vous coderez votre traitement, vous pourrez facilement avoir un son affreux ou pas de son du tout. Veillez donc à bien implémenter votre chaîne d'effets ;
  • L'API Audio pour le Web est encore expérimentale et n'est pas supportée par tous les navigateurs. Veillez à laisser la possibilité à vos auditeurs d'écouter votre radio sans traitement s'ils n'utilisent pas de navigateur suffisamment récent.
  • Comme le traitement de son est effectué par le navigateur, tous les réglages sont accessibles au public (et plus particulièrement aux développeurs avertis), donc il ne vous sera pas possible de les conserver de manière privée juste pour vous.
  • Ce traitement est proposé de manière temporaire, en attendant que Radionomy daigne proposer un traitement de son digne de nom :)

Pour ma part, je suis toujours à la recherche d'un algorithme de compresseur avec sidechain pour l'implémenter en javascript ensuite. L'intérêt étant, au final, de créer un effet supplémentaire dans ma chaîne : le contrôle du gain automatique, aujourd'hui réalisé par le traitement Radionomy.

Pourquoi le faire moi-même ? Parce que Radionomy se contente d'un simple compresseur, qui peut vite donner des impressions de son "aspiré" sur les musiques avec des basses très prononcées. Dans la musique actuelle, les fréquences basses sont, dans plus de 90% des cas, utilisées pour marquer le rythme, un simple compresseur derrière en pâtira, alors qu'un compresseur avec sidechain pourra filtrer les basses sur le signal de référence, pour compresser le tout avec un signal beaucoup plus stable : ce qui veut dire, beaucoup moins d'aspiration !

La suite au prochain article…

Rédigé le .

Commentaires

comments powered by Disqus