Rédigé par Whiteshoulders -

Synthèse vocale et JavaScript : Génération dynamique de sons

lire l'article

Le JavaScript offre beaucoup de possibilité, et avec les capacité d'un navigateur, on peut en faire beaucoup. J'ai décidé que parmi l'ensemble des possibilités, mon attention allait se focaliser sur la synthèses vocale.

Le but : réaliser un synthétiseur vocal simple, comme ceux des vieux comodore64.

Le première épisode de cette grande aventure humaine, riche en rebondissement et haute en couleurs portera sur la génération dynamique de sons en JavaScript.

Introduction

Pour pouvoir réaliser une application de synthèse vocale, il faut avant tout pouvoir générer un son.

Malheureusement, il est impossible actuellement de générer avec JavaScript un son joué directement par le navigateur. L'API Web Sound, en cours de développement devrais à terme le permettre, mais pour le moment c'est impossible.

Alors quoi ? On arrête l'article au bout de 5 lignes ? Non, car il existe une solution : générer un fichier son, puis de jouer ce fichier son. Les navigateurs récents en sont capables avec l’élément <audio>. On passe alors de la génération d'un son à la génération d'un fichier son.

Les fichiers son les plus simple à produire sont les fichiers wav. Il s'agit d'un format brut, non compressé. Et pour pouvoir produire ce type de fichier, il faut connaître son format.

Son et echantillonnage

Le son est une onde qui se transmet dans l'air, depuis une source (un haut parleur, un violon, un bouche) jusqu’à un récepteur (un micro, une oreille). L'onde se transmet sous la forme de variation de pression locale de l'air. Ce sont ces variations de pressions qui atteignent nos oreille, et font vibrer nos tympans. On peut décrire cette onde comme une fonction du temps, qui varie de manière continue.

L'informatique fonctionne de manière discrète. Toute les informations sont découpé en tranches, et codé sous forme binaire. Pour pouvoir décrire notre onde sonore, il va falloir la découper en tranche : c'est l'échantillonnage.

La largeur des tranches défini ce que l'on appel la fréquence d’échantillonnage. Plus les tranches sont étroite, plus on mesure précisément le signal. Si les tranches sont trop large, il peut arriver que l'on sous-échantillonne le signal.

La fréquence d'échantillonnage joue un rôle clef, puisqu'elle va permettre de savoir a quelle vitesse il faut lire les tranches pour retrouver le son d'origine.

A chaque tranche va correspondre l'amplitude de l'onde en ce point. Ce nombre va être codé dans un format binaire, sur un certain nombre de bit. C'est la résolution du son. Plus on alloue de bit pour coder chaque tranche, plus la résolution est grande, meilleur sera la qualité du son.

Format WAV

D'après la définition du format, un fichier wav est composé de plusieurs morceaux de données binaires. La première partie du fichier est le header, qui donne les information nécessaire à la lecture du reste du fichier, en particulier la fréquence d'échantillonnage.

Plus précisément, le fichier contient dans un premier temps un bloc de déclaration d'un fichier au format wave :

  • Une constante d'identification sur 4 octets : "RIFF"
  • La taille totale du fichier moins 8 octets codée sur 4 octets
  • Une constante de format sur 4 octets : "WAVE"

Viens ensuite un bloc décrivant le format audio (toutes les caractéristiques du son numérisé) :

  • Une constante identifiant le début du bloc sur 4 octets : "fmt "
  • La taille du bloc sur 4 octets
  • Une constant identifiant le format de stockage (PCM, par exemple) dans le fichier sur 2 octets
  • Le nombre de canaux
  • La fréquence d'échantillonnage sur 4 octets
  • Le nombre d'octets par seconde
  • Le nombre d'octets par bloc d'échantillonage
  • La résolution

Enfin, on trouve le bloc des données :

  • Une constante identifiant le début du bloc sur 4 octets : "data"
  • La taille du bloc de données moins 8 octets
  • Les données

Ecrire un wave en javascript

En respectant ce format, il est possible de générer le contenu du fichier. En utilisant les data URI, on peut facilement inclure le fichier ainsi écris dans le corps de la page, dans une balise audio.

Voici le code du module qui permet de retourner la version base64 de donnée sonores qu'il restera à générer :

 
var app = {};

(function() {

app.utility = {

    /* js port of PHP function pack */
    pack : function (fmt) {
        var output = '';

        var argi = 1;
        for (var i = 0; i < fmt.length; i++) {
            var c = fmt.charAt(i);
            var arg = arguments[argi];
            argi++;

            switch (c) {
                case "a":
                    output += arg[0] + "\0";
                    break;
                case "A":
                    output += arg[0] + " ";
                    break;
                case "C":
                case "c":
                    output += String.fromCharCode(arg);
                    break;
                case "n":
                    output += String.fromCharCode(
                        (arg >> 8) & 255, 
                        arg & 255
                    );
                    break;
                case "v":
                    output += String.fromCharCode(
                        arg & 255, 
                        (arg >> 8) & 255
                    );
                    break;
                case "N":
                    output += String.fromCharCode(
                        (arg >> 24) & 255, 
                        (arg >> 16) & 255, 
                        (arg >> 8) & 255, 
                        arg & 255
                    );
                    break;
                case "V":
                    output += String.fromCharCode(
                        arg & 255, 
                        (arg >> 8) & 255, 
                        (arg >> 16) & 255, 
                        (arg >> 24) & 255
                    );
                    break;
                case "x":
                    argi--;
                    output += "\0";
                    break;
                default:
                    throw new Error("Unknown pack format '"+c+"'");
            }
        }

        return output;
    }

}

/* Classic sound config : mono wav, 44100 Hz, 16 bit depth */
app.config = {
    channels : 1,
    sampleRate : 44100,
    bitsPerSample : 16,
}

}());

(function(){

var pack = app.utility.pack;
var cf = app.config;

// constructor
app.wave = function (_data) {
    this.data = _data;
};

app.wave.prototype = {

    // Generate the wave content by concatenation, and encode it to base64.
    generate : function () {
        var chunk1 = this.makeChunk1(); // format chunk
        var chunk2 = this.makeChunk2(); // data chunk
        var header = this.makeHeader(chunk1.length, chunk2.length);
        var out = header + chunk1 + chunk2;
        return "data:audio/wav;base64," + btoa(out);
    },

    // Generate the content of the audio format chunk
    makeChunk1 : function () {
        var chunk1 = [
            "fmt ",
            pack("V", 16), // Chunk length for PCM
            pack("v", 1), // linear PCM
            pack("v", cf.channels),
            pack("V", cf.sampleRate),
            pack("V", cf.sampleRate * cf.channels * cf.bitsPerSample / 8), // ByteRate
            pack(
                "v", 
                cf.channels * cf.bitsPerSample / 8
            ), // BlockAlign
            pack("v", cf.bitsPerSample)
        ];
        return chunk1.join('');
    },

    // Generate the content of the data chunk
    makeChunk2 : function () {
        var data = this.data;

        var chunk2 = [
            "data", // chunk ID
            pack("V", data.samples * cf.channels * cf.bitsPerSample / 8), // Chunk length
            data.raw
        ];
        return chunk2.join('');
    },

    // Generate the header chunk
    makeHeader : function () {
        var data = this.data;

        var dataSize = data.samples * cf.channels * cf.bitsPerSample / 8;
        var block1 = (8 + 16);
        var block2 = (8 + dataSize);
        var header = [
            "RIFF",
            pack(
            "V", 4 + block1 + block2), // total lenght
            "WAVE"
        ];
        return header.join('');
    }

};

}());  

Démonstration

On va générer les données d'un fichier wav à partir d'une simple sinusoïde à 440Hz. Un beau la bien pur.

 
var frequency = 440;
var duration = 1;
var volume = Math.floor(65535 / 2);

var raw = []; var samples = 0;

for(var i = 0; i <= app.config.sampleRate * duration; i++) {

for(var c = 0; c &lt; app.config.channels; c++) {
    var v = volume * Math.sin(2 * Math.PI * frequency * i/app.config.sampleRate);
    raw.push(app.utility.pack("v", v));
    samples++;
}

} var data = {raw: raw.join(''), samples: samples};

var wave = new app.wave(data); var base64wave = wave.generate(); var audio = document.getElementById("demo"); var source = document.createElement("source"); source.setAttribute("src", base64wave); audio.appendChild(source);  

Le résultat du code, c'est cette balise audio, avec un la de très belle facture, d'une seconde.

Conclusion

Maintenant que l'on est capable d'écrire n'importe quelle fichier audio pour le faire lire par le browser, il ne "reste plus" qu'a générer les données sonores correspondant à une voix de synthèse.

La prochaine étape sera de générer des voyelles, avant de passer aux consonnes, puis à un convertisseur de texte en phonèmes.

3 commentaires

avatar #1 TL a dit :

T'aimes ça les beaux la bien pur, sale nazi. Eugéniste ! Dieudonniste !

avatar #2 TL a dit :

T'as le droit de ne pas afficher ces deux commentaires, puisque tu uses de la modération comme un nazi.

avatar #3 Whiteshoulders a dit :

@TL : Ché né gomprend pas ce déverlement dé haine à mon égard. Fraiment.

Écrire un commentaire

Vérification anti-spam :
Quelle est la dernière lettre du mot ltltl ?