dimanche 14 avril 2019

Synthèse sonore avec Mozzi et Arduino (2)

Dans le billet précédent, nous avons vu comment produire un son avec Mozzi. Cette fois, nous allons  écrire un sketch qui joue une mélodie. Ensuite, nous améliorerons progressivement ce sketch en y ajoutant de la polyphonie, puis une enveloppe ADSR.

Jouer une mélodie: la classe EventDelay

Nous voulons donc, dans un premier temps, écrire un sketch qui jouera automatiquement une mélodie. L'algorithme sera donc : jouer la première note, attendre un peu, jouer la deuxième note, attendre un peu, etc.

Mais attention: la bonne vieille fonction "delay()" est désactivée dans Mozzi. Il faut plutôt utiliser la classe EventDelay, ce qui implique de démarrer le chronométrage ("start()"), puis de vérifier périodiquement si le délai est écoulé ou non ("ready()").

Vous en avez une illustration dans le sketch ci-dessous, qui joue de façon répétitive une suite de 12 notes au rythme de 4 notes par seconde.

Pour utiliser des délais dans le sketch, il faut d'abord inclure le fichier "EventDelay.h". C'est fait à la ligne 8 du sketch.

On doit ensuite définir un objet de type "EventDelay": c'est ce que j'ai fait à la ligne 14, je l'ai baptisé "attente".

À la ligne 26, je démarre un temps d'attente de 250 millisecondes grâce à la commande "attente.start(duree);".

Ensuite, à l'intérieur d'updateControl(), je vérifie si le délai est écoulé; ça commence à la ligne 31 du sketch ("if (attente.ready())"). Cette condition deviendra vraie 250 millisecondes après le démarrage du délai.

Lorsque les 250 millisecondes sont écoulées, les lignes 32 à 38 sont exécutées: elles consistent à modifier la valeur de la fréquence de l'oscillateur (ligne 32), à incrémenter la variable "compteur" qui indique le rang de la note à jouer (ligne 33), et à redémarrer le chronomètre pour un nouveau délai de 250 millisecondes (ligne 38).

/* *********************************************************
On joue une mélodie avec Mozzi.
Démonstration de la classe EventDelay
Plus d'infos:
https://electroniqueamateur.blogspot.com/2019/04/synthese-sonore-avec-mozzi-et-arduino-2.html
***********************************************************/
#include <MozziGuts.h>
#include <Oscil.h>
#include <EventDelay.h> // fichier nécessaire pour utiliser des délais.
#include <tables/saw2048_int8.h>
#define CONTROL_RATE 64
Oscil <SAW2048_NUM_CELLS, AUDIO_RATE> onde(SAW2048_DATA);
EventDelay attente;
const int duree = 250; // durée d'une note, en millisecondes
// fréquence de chaque note jouée
#define nombreDeNotes 12 // nombre de notes à jouer
const float notes[nombreDeNotes] = {65.41, 138.59, 220.00, 77.78, 164.81, 261.63, 92.50, 196.00, 311.13, 110.00, 233.08, 369.99
};
int compteur = 0; // numéro de la note à jouer
void setup() {
startMozzi(CONTROL_RATE);
attente.start(duree);
}
void updateControl() {
if (attente.ready()) { // c'est le temps de jouer une nouvelle note
onde.setFreq(notes[compteur]);
compteur++;
if (compteur == nombreDeNotes)
{
compteur = 0;
}
attente.start(duree); // on repart le chrono
}
}
int updateAudio() {
return onde.next();
}
void loop() {
audioHook();
}

Jouer des accords (polyphonie)

Rien ne nous oblige à nous limiter à jouer une note à la fois. Le sketch ci-dessous est très similaire au précédent, sauf que nous utilisons 3 oscillateurs afin de jouer trois notes simultanément.


/************************************************************
On joue une suite d'accords en additionnant le signal de
3 oscillateurs.
Plus d'infos:
https://electroniqueamateur.blogspot.com/2019/04/synthese-sonore-avec-mozzi-et-arduino-2.html
***************************************************************/
#include <MozziGuts.h>
#include <Oscil.h>
#include <EventDelay.h>
#include <tables/square_no_alias_2048_int8.h>
Oscil <SQUARE_NO_ALIAS_2048_NUM_CELLS, AUDIO_RATE> oscil1(SQUARE_NO_ALIAS_2048_DATA);
Oscil <SQUARE_NO_ALIAS_2048_NUM_CELLS, AUDIO_RATE> oscil2(SQUARE_NO_ALIAS_2048_DATA);
Oscil <SQUARE_NO_ALIAS_2048_NUM_CELLS, AUDIO_RATE> oscil3(SQUARE_NO_ALIAS_2048_DATA);
#define CONTROL_RATE 64
EventDelay attente;
#define NOMBRE_D_ACCORDS 4
const float lesAccords[NOMBRE_D_ACCORDS][3] = {
{261.63, 329.63, 392.00}, // accord de do majeur
{220.00, 261.63, 349.23}, // accord de fa majeur
{246.94, 293.66, 392.00}, // accord de sol majeur
{220.00, 261.63, 349.23} // accord de fa majeur
};
int compteur = 0;
void setup() {
startMozzi(CONTROL_RATE);
attente.set(1500); // on définit la durée du délai
attente.start();
}
void updateControl() {
if (attente.ready()) { // c'est le temps de jouer un nouvel accord
// on règle les 3 oscillateurs à leur nouvelle fréquence
oscil1.setFreq(lesAccords[compteur][0]);
oscil2.setFreq(lesAccords[compteur][1]);
oscil3.setFreq(lesAccords[compteur][2]);
compteur++;
if (compteur == NOMBRE_D_ACCORDS) {
compteur = 0;
}
attente.start(); // on repart en neuf.
}
}
int updateAudio() {
return ((oscil1.next() + oscil2.next() + oscil3.next()) >> 2);
}
void loop() {
audioHook();
}


Enveloppe ADSR

Nos deux programmes précédents donnent un résultat qui manque un peu d'expression, puisque chaque note (ou accord) est joué avec un volume sonore égal du début à la fin.  Pour améliorer les choses, nous allons maintenant définir une enveloppe ADSR qui nous permettra de modifier le volume pendant l'exécution de la note.

"ADSR" est l'acronyme pour Attack, Decay, Sustain et Release, quatre phases qui se succèdent pendant l'exécution d'une note.

  • L'attaque (attack) est la première phase; il s'agit du temps pendant lequel le volume de la note augmente progressivement d'une valeur nulle jusqu'à la valeur maximale. Pour un son percussif, on utilise une attaque courte (le son atteint instantanément son volume maximal), alors qu'une attaque longue donnera un résultat beaucoup plus doux (le volume augmente lentement au début de la note).
  • La chute (decay) est le temps pendant lequel le volume de la note diminue afin de passer de la valeur maximale (atteinte à la fin de l'attaque) jusqu'à une valeur un peu plus faible.
  • L'entretien (sustain) est le temps pendant lequel le volume de la note demeure constant.
  • L'extinction (release) est l'étape finale, pendant laquelle le volume de la note diminue progressivement jusqu'à devenir nul.

Le sketch ci-dessous joue 5 fois la même note en utilisant chaque fois une enveloppe dont les paramètres sont différents.

Cette fois, il est important d'inclure le fichier ADSR.h; c'est fait à la ligne numéro 10.

À la ligne 31, j'ai créé un objet de type ADSR que j'ai baptisé "enveloppe".

Les caractéristiques de l'enveloppe sont réglées aux lignes 44 et 47.

"setADLevels(niveau_attaque, niveau_chute)" (ligne 44) permet de régler le volume sonore atteint à la fin de l'attaque et le volume qui sera maintenu constant pendant la phase d'entretien. Les deux paramètres peuvent prendre n'importe quelle valeur entre 0 et 255.

À la ligne 47, "setTimes(durée_attaque, durée_chute, durée_entretien, durée_extinction)" permet de définir, en millisecondes, la durée de chacune des 4 phases de l'enveloppe. Il semble nécessaire d'éviter les durées inférieures à 20 ms, qui génèrent parfois des résultats indésirables.

À la ligne 60, "update()" met l'enveloppe à jour.

Finalement, la ligne 66 retourne la multiplication de notre enveloppe et de la note jouée par l'oscillateur principal. Il faut diviser par 256 (">> 8") pour que le résultat demeure à l'intérieur des limites requises.

Vous devriez entendre 5 notes qui ne diffèrent que par les paramètres de leur enveloppe.

/****************************************************************
Démonstration de l'utilisation d'une enveloppe (ADSL)
avec mozzi. La même note est jouée 5 fois avec une enveloppe
différente.
https://electroniqueamateur.blogspot.com/2019/04/synthese-sonore-avec-mozzi-et-arduino-2.html
***************************************************************/
#include <MozziGuts.h>
#include <Oscil.h>
#include <EventDelay.h>
#include <ADSR.h> // fichier nécessaire pour générer une enveloppe
#include <tables/saw2048_int8.h>
#define CONTROL_RATE 64
#define NOMBRE_DE_NOTES 5
// format du tableau: niveau_attaque, niveau_chute, temps_attaque, temps_chute, temps_entretien, temps_extinction, fréquence
// les durées sont en ms: éviter les valeurs de 20 ou moins, qui risquent d'être interprétées comme 0.
const int lesNotes[NOMBRE_DE_NOTES][7] = {
{255, 255, 30, 30, 50, 30, 300}, // un court blip
{255, 255, 30, 30, 2000, 30, 300}, // un son de niveau constant
{255, 255, 2000, 30, 30, 30, 300}, // une attaque lente
{255, 255, 30, 30, 50, 2000, 300}, // percussif, avec une longue extinction
{255, 150, 500, 500, 500, 500, 300} // les 4 phases de durée égale.
};
unsigned int compteur = 0; // indique laquelle des 5 notes il faut jouer
Oscil <SAW2048_NUM_CELLS, AUDIO_RATE> oscillateur(SAW2048_DATA);
EventDelay attente; // le délai entre deux notes consécutives
ADSR <CONTROL_RATE, AUDIO_RATE> envelope; // notre enveloppe
void setup() {
attente.set(3000); // une note jouée toutes les 3 secondes
startMozzi(CONTROL_RATE);
}
void updateControl() {
if (attente.ready()) { // c'est le temps de jouer une nouvelle note
// réglage des volumes de l'enveloppe
envelope.setADLevels(lesNotes[compteur][0], lesNotes[compteur][1]);
// réglage des durées de l'enveloppe en millisecondes.
envelope.setTimes(lesNotes[compteur][2], lesNotes[compteur][3], lesNotes[compteur][4], lesNotes[compteur][5]);
oscillateur.setFreq(lesNotes[compteur][6]); // fréquence de la note
envelope.noteOn(); // on joue la note
compteur = compteur + 1;
if (compteur == NOMBRE_DE_NOTES) {
compteur = 0;
}
attente.start(); // on redémarre le chrono
}
envelope.update(); // mise à jour de l'enveloppe
}
int updateAudio() {
//multiplication des 2 signaux et division par 256
return (int) (envelope.next() * oscillateur.next()) >> 8;
}
void loop() {
audioHook();
}


Résultat final

Pour terminer, voici un autre sketch qui joue une suite d'accords mais, cette fois, je leur applique une enveloppe ADSL (définie dans setUp()). Sans l'enveloppe, ça sonnait un peu comme un orgue. Maintenant, c'est plus proche d'un accordéon...

/***************************************************************
On joue une suite d'accords en leur appliquant une enveloppe.
Plus d'infos:
https://electroniqueamateur.blogspot.com/2019/04/synthese-sonore-avec-mozzi-et-arduino-2.html
****************************************************************/
#include <MozziGuts.h>
#include <Oscil.h>
#include <EventDelay.h>
#include <ADSR.h>
#include <tables/square_no_alias_2048_int8.h>
#define CONTROL_RATE 64
Oscil <SQUARE_NO_ALIAS_2048_NUM_CELLS, AUDIO_RATE> oscil1(SQUARE_NO_ALIAS_2048_DATA);
Oscil <SQUARE_NO_ALIAS_2048_NUM_CELLS, AUDIO_RATE> oscil2(SQUARE_NO_ALIAS_2048_DATA);
Oscil <SQUARE_NO_ALIAS_2048_NUM_CELLS, AUDIO_RATE> oscil3(SQUARE_NO_ALIAS_2048_DATA);
EventDelay Delay;
ADSR <CONTROL_RATE, AUDIO_RATE> envelope; // notre enveloppe
#define NOMBRE_D_ACCORDS 4
const float lesAccords[NOMBRE_D_ACCORDS][3] = {
{261.63, 329.63, 392.00}, // accord de do majeur
{246.94, 293.66, 392.00}, // accord de sol majeur
{220.00, 261.63, 349.23}, // accord de fa majeur
{246.94, 293.66, 392.00} // accord de sol majeur
};
int compteur = 0; // numéro de l'accord à jouer
void setup() {
startMozzi(CONTROL_RATE);
// réglage des volumes de l'enveloppe
envelope.setADLevels(255, 200);
// réglage des durées de l'enveloppe en millisecondes.
envelope.setTimes(1000, 500, 500, 1000);
Delay.set(3000); // durée de chaque accord
Delay.start();
}
void updateControl() {
if (Delay.ready()) {
oscil1.setFreq(lesAccords[compteur][0]);
oscil2.setFreq(lesAccords[compteur][1]);
oscil3.setFreq(lesAccords[compteur][2]);
envelope.noteOn();
compteur++;
if (compteur == NOMBRE_D_ACCORDS) {
compteur = 0;
}
Delay.start();
}
envelope.update();
}
int updateAudio() {
return ((envelope.next() * ((oscil1.next() + oscil2.next() + oscil3.next()) >> 2)) >> 8);
}
void loop() {
audioHook();
}


Yves Pelletier   (TwitterFacebook)

1 commentaire:

  1. Excellente introduction à Mozzi et ses grands principes. D'un point de vue pédagogique, pour une bonne compréhension des décalages >>2, peut-être aurait-il mieux valu utiliser 4 oscillateurs de polyphonie ? Même si musicalement ce n'est pas idéal...
    En tout cas bravo, tout est dit dans vos articles.

    RépondreSupprimer