mercredi 5 août 2020

S'entraîner à lire les notes sur une portée avec Arduino

J'aime bien jouer du clavier, mais j'ai beaucoup de difficulté à jouer à vue, c'est à dire interpréter une partition de piano assez rapidement pour la jouer sans l'avoir préalablement mémorisée. Je me débrouille assez bien en clé de sol, mais en clé de fa, mes performances sont simplement pitoyables.



Dans l'espoir de devenir un meilleur musicien, j'ai mis au point un dispositif qui se branche dans un clavier MIDI. Une note s'affiche sur une portée, et je dois jouer cette note sur le clavier. Si j'appuie sur la bonne touche, je gagne un point, et une nouvelle note s'affiche. 



Si j'appuie sur la mauvaise touche, un petit clavier apparaît à l'écran pour m'indiquer quelle touche il faut appuyer et, bien entendu, je ne récolte aucun point. Je dois quand même appuyer sur la bonne touche afin de passer à la question suivante. De plus, juste après avoir joué "ma" note, le clavier joue la note que j'aurais dû jouer, ce qui me permet de comparer les deux notes de façon auditive.



Ce dispositif est constitué d'une carte Arduino Uno, d'un écran LCD PCD8544 (Nokia 5110) et d'une interface MIDI de fabrication maison qui est essentiellement constituée d'un octocoupleur, d'une diode de quelques résistances et de connecteurs DIN.




Le circuit

Je reproduis ici le schéma de mon interface MIDI, fabriquée en 2012... La partie "MIDI IN" est essentielle pour détecter la note jouée sur le clavier. La partie "MIDI OUT" permet à l'Arduino de faire jouer, par l'entremise du clavier, la note qu'il fallait jouer, ce qui aide l'utilisateur à constater s'il a joué la bonne note ou non.

Vous pouvez également vous procurer un module MIDI prêt à l'emploi (recherchez "MIDI Shield Arduino").

En ce qui concerne l'afficheur LCD, le circuit est assez complexe puisque j'utilise un circuit intégré 4050 afin d'abaisser à 3,3 V les signaux logiques générés par l'Arduino Uno. Voici le schéma, mais vous trouverez beaucoup plus d'information en consultant cet article consacré à l'utilisation d'un écran Nokia 5110 avec un Arduino.



Installation de la bibliothèque MIDI

J'ai utilisé la bibliothèque MIDI afin de simplifier au maximum la gestion de la communication entre l'Arduino et le clavier MIDI.


Installation des bibliothèques pour l'afficheur LCD

Pour l'affichage sur l'écran Nokia 5150, j'ai utilisé la bibliothèque Adafruit-PCD8544-Nokia-5110-LCD ainsi que la bibliothèque Adafruit-GFX.



Le sketch

Une part importante du sketch concerne la gestion de l'écran LCD. Au moyen d'un logiciel de dessin, j'ai créé quatre minuscules fichiers .bmp: la portée avec une clé de sol, la portée avec une clé de fa, le symbole bémol, et le symbole dièse.  J'ai ensuite chargé ces fichiers .bmp dans l'outil en ligne image2ccp afin de les transformer en information utilisable dans un sketch Arduino. Il s'agit des quatre constantes "cleDeSol", "cleDeFa", "bemol" et "diese" énoncées au début du programme.

Pour les autres éléments affichés sur l'écran, j'ai utilisé les outils de base de la bibliothèque GFX (rectangles pour dessiner le clavier, rectangle à coin arrondi pour les notes, etc.)

Comme son nom l'indique, la routine "nouvelleQuestion" est responsable de créer aléatoirement une nouvelle note à placer sur la portée. Cette nouvelle note est décrite au moyen de 3 variables: "clef" détermine si la portée est en clé de sol ou en clé de fa, "altération" détermine si la note est accompagnée d'un bémol ou d'une dièse, et "numéro" indique la position de la note sur la portée.

Lorsque l'utilisateur appuie sur une touche du clavier, il faut vérifier si la note qu'il a généré correspond à celle qui est affichée sur le portée; c'est fait dans la fonction "compareNotes", grâce à deux tableaux qui établissent une relation entre chaque position sur la portée et son numéro de note MIDI (ce sont les variables notes_fa et notes_sol).

-
/*********************************************************************
Gadget MIDI pour améliorer la lecture des notes sur une portée.
Une note s'affiche sur une portée sur un écran Nokia, et on
doit la jouer sur un clavier MIDI..
Pour plus d'infos:
https://electroniqueamateur.blogspot.com/2020/08/sentrainer-lire-les-notes-sur-une.html
*********************************************************************/
#include <MIDI.h> // https://github.com/FortySevenEffects/arduino_midi_library
#include <SPI.h> // Pour l'afficheur
#include <Adafruit_GFX.h> // https://github.com/adafruit/Adafruit-GFX-Library
#include <Adafruit_PCD8544.h> // https://github.com/adafruit/Adafruit-PCD8544-Nokia-5110-LCD-library
MIDI_CREATE_DEFAULT_INSTANCE();
Adafruit_PCD8544 display = Adafruit_PCD8544(5, 4, 3);
// D/C broche 5,CSE ou CS broche 4, RST broche 3
// mages bitmap éalisées avec l'outil en ligne http://javl.github.io/image2cpp/
// la portée en clé de sol
const unsigned char cleDeSol [] PROGMEM = {
// 'cledesol', 45x36px
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00,
0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11,
0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00,
0x1f, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00,
0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x28,
0x00, 0x00, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc8, 0x00, 0x00, 0x00, 0x00,
0x1f, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x01, 0x9f, 0x00, 0x00, 0x00, 0x00, 0x01, 0xb5, 0x80, 0x00,
0x00, 0x00, 0x01, 0xa4, 0x40, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x01, 0x84,
0x40, 0x00, 0x00, 0x00, 0x00, 0xc4, 0x80, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x00,
0x1f, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00,
0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf2, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf6,
0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
// la portée en clé de fa
const unsigned char cleDeFa [] PROGMEM = {
// 'cledefa', 45x36px
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x1f, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x03, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x06, 0x39, 0x00, 0x00,
0x00, 0x00, 0x07, 0x98, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x03, 0x8c,
0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00,
0x1f, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00,
0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x03, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x1f, 0xff, 0xff, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
// symbol bémol
const unsigned char bemol [] PROGMEM = {
// 'bemol', 4x7px
0x80, 0x80, 0xa0, 0xd0, 0x90, 0xa0, 0xc0
};
// symbole dièse
const unsigned char diese [] PROGMEM = {
// 'dièse', 7x7px
0x14, 0x14, 0x7e, 0x28, 0xfc, 0x50, 0x50
};
// numéro de note midi associé à chaque position de la portée en clé de fa (numero 0 = MIDI 33, etc.)
const int notes_fa[] = {33, 35, 36, 38, 40, 41, 43, 45, 47, 48, 50, 52, 53, 55, 57, 59, 60, 62, 64, 65, 67, 69};
// numéro de note midi associé à chaque position de la portée en clé de sol(numero 0 = MIDI 53, etc.)
const int notes_sol[] = {53, 55, 57, 59, 60, 62, 64, 65, 67, 69, 71, 72, 74, 76, 77, 79, 81, 83, 84, 86, 88, 89};
// variables globales
int numero; //position de la note sur la portée: 0 à 21
int alteration; // 0: rien 1: bémol 2: dièse
int clef; // 0: sol 1: fa
int erreur = 0; // 1 si notre dernière réponse était fausse
int pointage = 0; // nombre de bonnes réponses depuit le début
int total = 0; // nombre de questions posées depuis le début
int bonneNote; // le numéro MIDI de la note qu'il faut jouer
void dessineNote() {
const int marge = 0; // nombre de pixels à gauche avant de commencer à dessiner
display.clearDisplay();
// on dessine la portée
if (clef) { //clef de fa
display.drawBitmap(marge, 8, cleDeFa, 45, 36, BLACK);
}
else { // clef de sol
display.drawBitmap(marge, 8, cleDeSol, 45, 36, BLACK);
}
//on dessine la note
display.drawRoundRect(marge + 26, 42 - 2 * numero, 8, 5, 4, BLACK);
// s'il y a lieu, on dessine les petits traits supplémentaires
// note à l'extérieur de la portée
if (numero == 0) {
display.drawLine(marge + 24, 44, marge + 34, 44, BLACK);
}
if (numero < 3) {
display.drawLine(marge + 24, 40, marge + 34, 40, BLACK);
}
if (numero < 5) {
display.drawLine(marge + 24, 36, marge + 34, 36, BLACK);
}
if (numero > 15) {
display.drawLine(marge + 24, 12, marge + 34, 12, BLACK);
}
if (numero > 17) {
display.drawLine(marge + 24, 8, marge + 34, 8, BLACK);
}
if (numero > 19) {
display.drawLine(marge + 24, 4, marge + 34, 4, BLACK);
}
// on dessine l'altération, s'il y a lieu
if (alteration == 1) { // bemol
display.drawBitmap(marge + 19, 41 - 2 * numero, bemol, 4, 7, BLACK);
}
if (alteration == 2) { // dièse
display.drawBitmap(marge + 18, 42 - 2 * numero, diese, 7, 7, BLACK);
}
// si notre dernière tentative était mauvaise, on affiche un clavier qui montre
// comment jouer la note.
if (erreur) {
dessineClavier();
}
// on affiche le pointage
display.setCursor(46, 8); // coordonnées du point de départ du texte
display.setTextColor(BLACK);
display.setTextSize(1); // taille par défaut
display.print(String(pointage));
display.print("/");
display.print(String(total));
display.display();
}
void dessineClavier() {
const int origx = 48;
const int origy = 28;
const int largeurBlanche = 5;
// les touches blanches
for (int i = 0; i <= 6; i++) {
display.drawRect(origx + i * largeurBlanche, origy, largeurBlanche , 12, BLACK);
}
//les touches noires
for (int i = 0; i <= 1; i++) {
display.fillRect(origx + largeurBlanche / 2 + 1 + i * largeurBlanche, origy, 4 , 7, BLACK);
}
for (int i = 0; i <= 2; i++) {
display.fillRect(origx + 7 * largeurBlanche / 2 + 1 + i * largeurBlanche, origy, 4 , 7, BLACK);
}
// le repère qui indique la touche à enfoncer:
switch (bonneNote % 12) {
case 0: // do
display.fillCircle(origx + largeurBlanche / 2 , origy + 14, 2, BLACK);
break;
case 1: // do# réb
display.fillCircle(origx + largeurBlanche , origy - 4, 2, BLACK);
break;
case 2: // ré
display.fillCircle(origx + 3 * largeurBlanche / 2 , origy + 14, 2, BLACK);
break;
case 3: // ré# mib
display.fillCircle(origx + 2 * largeurBlanche , origy - 4, 2, BLACK);
break;
case 4: // mi
display.fillCircle(origx + 5 * largeurBlanche / 2 , origy + 14, 2, BLACK);
break;
case 5: // fa
display.fillCircle(origx + 7 * largeurBlanche / 2 , origy + 14, 2, BLACK);
break;
case 6: // fa# solb
display.fillCircle(origx + 4 * largeurBlanche , origy - 4, 2, BLACK);
break;
case 7: // sol
display.fillCircle(origx + 9 * largeurBlanche / 2 , origy + 14, 2, BLACK);
break;
case 8: // sol# lab
display.fillCircle(origx + 5 * largeurBlanche , origy - 4, 2, BLACK);
break;
case 9: // la
display.fillCircle(origx + 11 * largeurBlanche / 2 , origy + 14, 2, BLACK);
break;
case 10: // la# sib
display.fillCircle(origx + 6 * largeurBlanche , origy - 4, 2, BLACK);
break;
case 11: // si
display.fillCircle(origx + 13 * largeurBlanche / 2 , origy + 14, 2, BLACK);
break;
}
display.display();
}
// on choisit au hasard la nouvelle note à jouer
void nouvelleQuestion() {
numero = random(22); // position de la note sur la portée (0 à 21);
alteration = random(3); // 0: rien 1: bemol 2: dièse
clef = random(2); // 0: clé de sol 1: clé de fa
// ajustement: on ne veut pas de mi dièse, de fa bémol, de si dièse et de do bémol
if (alteration == 1) { // bémol
if (clef) { // clé de fa
if ((numero % 7 == 2) || (numero % 7 == 5) ) { // do ou fa
alteration = 0; // on enlève le bémol.
}
}
else { // clé de sol
if ((numero % 7 == 0) || (numero % 7 == 4) ) { // do ou fa
alteration = 0; // on enlève le bémol.
}
}
}
if (alteration == 2) { // dièse
if (clef) { // clé de fa
if ((numero % 7 == 1) || (numero % 7 == 4) ) { // si ou mi
alteration = 0; // on enlève la dièse
}
}
else { // clé de sol
if ((numero % 7 == 3) || (numero % 7 == 6) ) { // si ou mi
alteration = 0; // on enlève la dièse
}
}
}
dessineNote(); // affichage de la question
}
// on compare la note jouée avec celle qu'il fallait jouer
int compareNotes(byte pitch) {
// on convertit la variable numero (position sur la portée) en numéro de note MIDI
if (clef) { // clé de fa
bonneNote = notes_fa[numero];
}
else { // clé de sol
bonneNote = notes_sol[numero];
}
if (alteration == 1) { // bémol
bonneNote = bonneNote - 1;
}
if (alteration == 2) { // dièse
bonneNote = bonneNote + 1;
}
// on joue la bonne note
delay(200); // petit délai pour séparer la note jouée de la note corrigée
MIDI.sendNoteOff(pitch, 0, 1);
MIDI.sendNoteOn(bonneNote, 100, 1);
delay(100);
MIDI.sendNoteOff(bonneNote, 0, 1);
return (bonneNote % 12 == pitch % 12); // on ne vérifie pas si c'est le bon octave
/* Si vous désirez que la note soit acceptée uniquement si elle est jouée dans
le bon octave, utilisez plutôt:
return (bonneNote == pitch);
*/
}
// éxécuté lorsque l'utilisateur appuie sur une touche
void handleNoteOn(byte channel, byte pitch, byte velocity)
{
if (erreur == 0) {
total = total + 1;
}
if (compareNotes(pitch)) { // bonne réponse
// on additionne 1 au pointage
if (erreur == 0) {
pointage = pointage + 1;
}
erreur = 0;
// on fait apparaître une nouvelle question
nouvelleQuestion();
}
else { // mauvaise réponse
erreur = 1;
dessineNote();
}
}
void setup() {
// initialisation de l'écran
display.begin(); // initialisation de l'afficheur
display.setContrast(60); // réglage du contraste (40-60) variable selon votre écran
display.clearDisplay(); // ça efface à la fois le buffer et l'écran
display.display();
// initialisation MIDI
MIDI.setHandleNoteOn(handleNoteOn);
MIDI.begin(MIDI_CHANNEL_OMNI);
// initialisation du générateur de nombres aléatoires
randomSeed(analogRead(0));
// affichage de la première question
nouvelleQuestion();
}
void loop() {
MIDI.read(); // on vérifie si une note est jouée sur le clavier.
}
view raw MIDI_portee.ino hosted with ❤ by GitHub
-

À lire aussi:


Yves Pelletier (TwitterFacebook)

1 commentaire:

  1. Bonjour monsieur Pelletier.
    Cela fait quelques temps que je parcours vos sujets, essentiellement sur Arduino et l'électronique, et je suis épaté par votre volonté de partage de connaissances.
    Toujours bien présenté et illustré et dans un français impeccable.
    Mille mercis à vous pour tout ça.

    RépondreSupprimer