11min.

Jouer de la musique dans le navigateur avec la Web Audio API

Parmi la multitude d’API natives proposées par les navigateurs en JavaScript, il en existe une qui vous offre la possibilité de jouer de la musique directement dans ceux-ci, sans utiliser de fichier audio. Il s’agit de la Web Audio API, qui peut sembler complexe au premier abord, surtout pour les personnes n’ayant pas de notions de musique.

Si vous souhaitez jouer de la musique sans enfreindre les droits d’auteur ou bien que vous êtes simplement curieux, nous vous proposons de vous aider à vous lancer dans cette API plutôt amusante. Dans cet article nous allons parler oscillateurs, fréquences et Rolling Stones. Rock on.

Section intitulée jouer-son-premier-sonJouer son premier son

Le premier élément dont nous aurons besoin pour jouer un son, c’est l’interface AudioContext, qui va nous permettre de créer les divers nœuds audio grâce auxquels nous pourrons jouer un son dans un premier temps, puis une mélodie. Les nœuds audio sont des éléments permettant de traiter l’audio de différentes manières, en contrôlant le volume par exemple. La documentation sur les nœuds audio est disponible sur MDN.

const audioContext = new AudioContext();

Une fois notre contexte initialisé, nous allons pouvoir lui ajouter notre premier nœud audio, l’Oscillateur.

Les oscillateurs sont des interfaces qui représentent une sonorité. Les oscillateurs jouent un son en fonction de l’oscillation en hertz que nous leur indiquons. Cette valeur en hertz peut être comprise entre 1 et environ 16 500. Nous vous conseillons cependant de vous limiter à des valeurs comprises entre 50 et 1000 environ, le son produit devenant très aigu au-delà de ce seuil. Pour notre exemple nous utiliserons une fréquence de 300.

const oscillator = new OscillatorNode(audioContext);
oscillator.frequency.value = 300;

Avant de pouvoir jouer notre premier son grâce à la méthode start() de notre oscillateur, il va nous rester une dernière étape à effectuer. Nous allons avoir besoin d’un autre nœud audio, le Gain. Ce nœud permet de connecter notre oscillateur à la sortie audio de l’utilisateur, en plus de permettre de modifier le son de notre oscillateur. La première modification que nous allons apporter, c’est de modifier le volume de l’oscillateur. En effet, le volume est par défaut assez élevé et l’expérience peut être particulièrement désagréable si vous utilisez un casque ou des écouteurs.

const gain = new GainNode(audioContext);
gain.connect(audioContext.destination);
gain.gain.value = 0.1;

Et voilà, nous pouvons désormais jouer notre son ! Nous le jouerons dans un EventListener puisque nous ne pouvons pas jouer de son tant que l’utilisateur n’a pas interagi avec la page. Notre code est maintenant le suivant :

const playMusic = function() {
   const audioContext = new AudioContext();

   const gain = new GainNode(audioContext);
   gain.connect(audioContext.destination);
   gain.gain.value = 0.1;

   const oscillator = new OscillatorNode(audioContext);
   oscillator.connect(gain);
   oscillator.frequency.value = 300;
   oscillator.start();
};

window.addEventListener('click', playMusic);

En cliquant sur le bouton, vous devriez entendre un son robotique. Pour arrêter ce son automatiquement, nous avons utilisé la méthode stop() en lui indiquant une durée en secondes :

oscillator.stop(1);

Notre son s’arrête maintenant après 1 seconde, et nous allons désormais pouvoir les enchaîner.

Section intitulée jouer-une-melodieJouer une mélodie

Pour jouer notre première succession de sons, nous allons utiliser un tableau de fréquences sur lequel nous allons itérer. Nous utiliserons le tableau de fréquences suivant :

[150, 320, 200, 650, 800, 75, 500, 400, 600]

Pour jouer toutes ces notes, nous allons avoir besoin d’autant d’oscillateurs et de gains qu’il y a de notes dans le tableau. Nous allons donc créer une nouvelle fonction playNote :

const audioContext = new AudioContext();
const partition = [150, 320, 200, 650, 800, 75, 500, 400, 600];

const playNote = function (frequency) {
   const gain = new GainNode(audioContext);
   gain.connect(audioContext.destination);
   gain.gain.value = 0.1;

   const oscillator = new OscillatorNode(audioContext);
   oscillator.connect(gain);
   oscillator.frequency.value = frequency;
   oscillator.start();
   oscillator.stop(1);
};

const playMusic = function () {
   partition.forEach(playNote);
};

window.addEventListener('click', playMusic);

Si nous essayons de jouer la musique, nous nous apercevons que cela ne fonctionne pas : tous les sons sont joués en même temps (nous vous épargnons le résultat, il est peu agréable) . Hors, nous voulons que JavaScript ne joue une note que lorsque la précédente s’est terminée. Nous allons pour cela devoir rendre notre fonction playNote bloquante et modifier légèrement la manière dont nous arrêtons nos oscillateurs :

const audioContext = new AudioContext();
const partition = [150, 320, 200, 650, 800, 75, 500, 400, 600];

const playNote = (frequency) =>
   new Promise((resolve) => {
       const gain = new GainNode(audioContext);
       gain.connect(audioContext.destination);
       gain.gain.value = 0.1;

       const oscillator = new OscillatorNode(audioContext);
       oscillator.connect(gain);
       oscillator.frequency.value = frequency;
       oscillator.start();

       setTimeout(() => {
           oscillator.stop();
           resolve();
       }, 1000);
   }
);

const playMusic = async function () {
   for (const frequency of partition) {
       await playNote(frequency);
   }
};

window.addEventListener('click', playMusic);

Afin de jouer nos notes consécutivement, nous avons donc rendu notre fonction playNote bloquante en lui faisant retourner la résolution d’une promesse après un temps fixé, 1 seconde ici. Notre méthode playMusic, quant à elle, doit await la résolution de cette promesse. Si nous relançons notre fonction, nous entendons bien une suite de sons. Nous allons désormais pouvoir essayer de faire une vraie musique !

Section intitulée jouer-de-la-musiqueJouer de la musique

Pour jouer une véritable musique, nous allons avoir besoin d’une véritable partition. Il nous faudra en effet connaître la fréquence correspondant à chaque note de la musique ainsi que la durée de chaque note. Notre choix de musique s’est porté sur Paint It, Black des Rolling Stones.

Pour cela, nous allons utiliser un site proposant des tablatures destinées aux débutants en piano. Les tablatures se présentent sous le format suivant :

5|--------c---d---c---------|
4|a---b---------------b---a-|

5|------------------d-------|
4|--a-------a-------d-------|

Le site propose un guide pour comprendre cette notation.

Les éléments qui nous intéressent, ce sont les tirets entre chaque lettre, ainsi que l’association des lettres avec le chiffre présent sur leur ligne.

Les tirets représentent chacun une unité de temps. Nous pourrons les utiliser afin de connaître la durée pendant laquelle nous voulons jouer la note à laquelle ils correspondent.

Les chiffres représentent l’octave des notes présentes leur ligne. Associés aux lettres, nous obtenons une note complète sous le format suivant : « 5c ». Pour convertir ces octaves et ces lettres en fréquence, nous pourrons nous appuyer sur un gist listant ces notes et donnant la fréquence correspondante. Les notes au format « E5 » représentent sur notre tablature les notes avec des lettres minuscules, les notes au format « E#5 » correspondent aux notes avec des lettres majuscules.

Notre musique est donc Paint it, Black des Rolling Stones. Nous ne jouerons pas l’intégralité de la musique ici, mais nous mettons en fin d’article à votre disposition un site de démo sur lequel la chanson complète est disponible.

La tablature que nous voulons jouer est la suivante, il s’agit d’un extrait du morceau :

4|------------e---F---g---a-|
4|--g---F---e---e---D---e---|
4|F---e---D-----------------|

Pour jouer cet extrait, notre code va devoir introduire un nouvel élément : la durée des notes. Pour cela, notre fonction playNote recevra désormais un objet comprenant une fréquence et une durée, et résoudra sa promesse après un délai correspondant à la durée de la note. Voici notre nouvelle fonction :

const playNote = ({ frequency, beat }) =>
   new Promise((resolve) => {
       const gain = new GainNode(audioContext);
       gain.connect(audioContext.destination);
       gain.gain.value = 0.1;

       const oscillator = new OscillatorNode(audioContext);
       oscillator.connect(gain);
       oscillator.frequency.value = frequency;
       oscillator.start();

       const noteDuration = beat * 0.18;

       setTimeout(() => {
           oscillator.stop();
           resolve();
       }, noteDuration * 1000);
   }
);

Le paramètre frequency provient désormais de notre tableau de correspondance, tandis que le paramètre beat est le nombre de tirets suivants chacune des lettres des partitions. Nous multiplions ce chiffre par 0.18 de manière arbitraire, il est possible de choisir une autre valeur et d’obtenir une mélodie agréable. Toutefois, nous ne voulons pas qu’un tiret représente une seconde comme précédemment, cela rend les notes beaucoup trop longues. N’hésitez pas à jouer avec cette valeur afin de trouver le rendu qui vous convient !

Maintenant que notre fonction est prête à jouer des notes de durées variables, il nous reste à construire nos objets. Notre variable partition, un tableau de nombres, va devoir évoluer en un tableau d’objets.

Nous allons devoir effectuer un travail très fastidieux, à savoir la construction à la main de nos objets. Pour cela, nous devrons en effet prendre chaque note de notre partition, lui adjoindre l’octave qui lui correspond, chercher dans notre tableau de correspondance la bonne fréquence et l’ajouter sous la clé frequency. Enfin, il nous faudra compter le nombre de tirets suivant cette note et ajouter le total sous la clé beat. Une fois ce travail effectué, notre variable partition est la suivante :

const partition = [
 {frequency: 329.63, beat: 3}, // E4
 {frequency: 369.99, beat: 3}, // F#4
 {frequency: 392, beat: 3}, // G4
 {frequency: 440, beat: 3}, // A4
 //
 {frequency: 392, beat: 3}, // G4
 {frequency: 369.99, beat: 3}, // F#4
 {frequency: 329.63, beat: 3}, // E4
 {frequency: 329.63, beat: 3}, // E4
 {frequency: 311.13, beat: 3}, // D#4
 {frequency: 329.63, beat: 3}, // E4
 //
 {frequency: 369.99, beat: 3}, // F#4
 {frequency: 329.63, beat: 3}, // E4
 {frequency: 311.13, beat: 17}, // D#4
];

(Bien sûr ce travail de traduction est automatisable, voir notre site de démo !).

Si nous essayons de jouer cette partition, vous devriez entendre une mélodie plutôt connue ! Toutefois, bien que reconnaissable, cette mélodie n’est pas particulièrement agréable à l’oreille : le bruit est un peu étouffé, et entre chaque note nous pouvons entendre un son désagréable, une sorte de « clic ». Nous allons améliorer cela afin d’avoir un meilleur rendu. Nous pouvons premièrement changer notre type d’oscillation. Par défaut, le type d’oscillation est le type sine, mais il en existe 3 autres : square, triangle, et sawtooth. Il est aussi possible de créer son propre type d’oscillation. En ce qui nous concerne, le type ayant reçu nos faveurs est le type triangle. Nous ajoutons donc cette ligne à notre fonction playNote :

oscillator.type = 'triangle';

Enfin, pour supprimer le « clic » entre chaque note, nous allons utiliser le gain pour faire en sorte que le volume des notes commence et se termine progressivement, et non pas abruptement comme actuellement. En plus de supprimer ce son désagréable, cela permet aux notes de sembler plus fidèles à des notes réelles : une note ne commence pas immédiatement, et ne se termine pas immédiatement non plus.

Pour ce faire, plutôt que de mettre le volume à 0.1 de manière permanente, nous allons utiliser 2 méthodes disponibles sur notre gain afin de donner une progression au volume : setValueAtTime et linearRampToValueAtTime. La première nous permettra d’indiquer que le volume doit commencer à 0, et la seconde permettra de faire progressivement monter le volume avant de le faire redescendre progressivement à également. Voici le code avec lequel nous remplaçons notre précédent gain.gain.value = 0.1 :

gain.gain.setValueAtTime(0, audioContext.currentTime);
gain.gain.linearRampToValueAtTime(0.15, audioContext.currentTime + 0.01);
gain.gain.linearRampToValueAtTime(0, audioContext.currentTime + noteDuration - 0.01);

Nous utilisons ici la propriété currentTime de notre audioContext afin d’indiquer à quel moment la modification du volume doit s’effectuer. Avec setValueAtTime, cette propriété permet de mettre le volume à 0 immédiatement. Avec linearRampToValueAtTime, elle permet de modifier progressivement le volume jusqu’à une valeur de 0.15 après 0.01 secondes, puis de le refaire passer à 0 juste avant la fin de la note. Cette valeur de 0.01 secondes peut sembler faible, et on pourrait l’imaginer imperceptible. C’est en réalité déjà une valeur importante : une valeur de 0.1, par exemple, hache complètement les notes. Notre mélodie joue désormais correctement et agréablement sa partition, notre objectif est atteint ! Avant de conclure, ajoutons à notre code le support pour les notes jouées simultanément. Pour des raisons de simplicité, nous ne les avons pas abordées jusque-là, mais certaines tablatures jouent plusieurs notes en même temps. Dans notre « partition » JavaScript, ces notes ressemblent à cela :

const multiNotes = [{frequency: 329.63, beat: 3}, {frequency: 392, beat: 3}];

Notre fonction, plutôt que de recevoir un objet { frequency, beat } recevra donc désormais un tableau de ces objets.

Voici notre fonction finale, tenant compte de cette nouveauté :

const playNote = (keys) =>
   new Promise((resolve) => {
       const oscillators = keys.map(({ frequency, beat }) => {
           const noteDuration = beat * 0.18;

           const gain = new GainNode(audioContext);
           gain.connect(audioContext.destination);
           gain.gain.setValueAtTime(0, audioContext.currentTime);
           gain.gain.linearRampToValueAtTime(0.15, audioContext.currentTime + 0.01);
           gain.gain.linearRampToValueAtTime(0, audioContext.currentTime + noteDuration - 0.01);

           const oscillator = new OscillatorNode(audioContext);
           oscillator.connect(gain);
           oscillator.type = 'triangle';
           oscillator.frequency.value = frequency;

           return { oscillator, noteDuration };
       });

       oscillators.forEach(({ oscillator }) => oscillator.start(audioContext.currentTime));

       setTimeout(() => {
           oscillators.forEach(({ oscillator }) => {
               oscillator.stop(0);
           });

           resolve();
       }, oscillators[0].noteDuration * 1000);
   });

N’oubliez pas de créer votre audioContext préalablement, ainsi que la méthode playMusic, inchangée !

Afin que vous puissiez tester facilement cette sympathique API, nous avons mis à votre disposition un site de démo qui vous permet de lancer directement des chansons présélectionnées par nos soins. Vous aurez aussi l’occasion de vous rendre sur le blog de piano que nous utilisons, afin d’y sélectionner une tablature et de la jouer directement sur le site de démo, sans devoir écrire la partition à la main. Notre parser se charge du travail pénible :).

Enfin, si vous êtes intéressés par cette API, vous pourrez également récupérer la tablature au format JSON. Pour l’utiliser sur vos projets vous n’aurez plus qu’à la parser et à l’utiliser avec l’implémentation vue dans cet article. Le code de notre site de démo est quant à lui accessible sur GitHub.

Bonne écoute !

Commentaires et discussions

Ces clients ont profité de notre expertise