Ce projet n'est pas totalement nouveau: c'est une amélioration d'un projet que j'avais réalisé 3 ans plus tôt.
Ce que l'ancienne version pouvait déjà faire:
- envoyer par l'entremise d'un câble MIDI des messages "note on" et "note off" correspondant aux notes appuyées par le musicien.
- augmenter ou diminuer l'octave de la note jouée, grâce à une paire de boutons poussoir ("octave up" et "octave down"), question de compenser la faible étendue du clavier utilisé (3 octaves...).
- modifier le programme, c'est à dire la sonorité de l'instrument (piano, trompette, etc.) au moyen d'une autre paire de bouton
Cette nouvelle version comporte les améliorations suivantes:
- Ajout d'un registre à décalage HC165, afin d'économiser quelques entrées sur l'Arduino.
- Grâce aux pins libérées sur l'Arduino, ajout d'un afficheur LCD.
- Ajout d'un dispositif anti-rebond pour les boutons qui permettent le changement d'octave et le changement de programme.
- Ajout d'un potentiomètre permettant le contrôle de la vélocité (le volume sonore).
- Ajout d'un joystick permettant de contrôler le "pitch bend".
Le clavier:

Mon clavier n'est pas sensible à la vélocité de la touche: il réagira de la même façon peu importe que le musicien enfonce la touche tout doucement, ou brusquement. C'est un inconvénient pour le musicien (ça donne une musique moins expressive) mais d'un point de vue technique, ça va nous faciliter les choses.
Les touches d'un clavier sont connectées de façon à former une matrice, qui permet de détecter la ou les touches enfoncées sans qu'il soit nécessaire que chaque touche soit reliée de façon indépendante à son propre circuit. Grâce à cette matrice, je peux tester chacune des 37 touches du clavier au moyen de 16 connecteurs seulement. Ces connecteurs sont visibles sur la photo ci-dessous, dans le coin supérieur gauche du clavier.
Au moyen d'un multimètre, j'ai pu constater que, lorsque j'appuie sur la touche "sol" la plus grave du clavier, le connecteur "Sol" sur la photo ci-dessous est mis en contact avec le connecteur "Octave 1". Si j'appuie sur la touche "sol" située un octave plus haut sur le clavier, le connecteur "Sol" sera maintenant en contact avec le connecteur "Octave 2", et ainsi de suite. Il n'y a qu'une note sur le 4e octave: FA (mon clavier commence par un FA, et se termine également par un FA). Notez bien qu'il s'agit de la description de la matrice du clavier que j'ai utilisé: il y a de très fortes chances que le clavier que vous utiliserez ait une matrice différente.
Le schéma ci-dessous illustre la matrice pour les touches "sol" et "la" du premier octave, et pour les touches "sol" et "la" du deuxième octave. Pour plus de clarté, je n'ai pas illustré la totalité des 37 touches... Comme vous pouvez le constater, chaque touche du clavier est accompagnée d'une diode.
Balayage et décodage de la matrice du clavier
Il faut maintenant faire en sorte que l'Arduino soit en mesure de déterminer si chacune des 37 touches du clavier est enfoncée ou non.
Jetez un coup d'oeil sur le schéma de circuit ci-dessous. J'ai ajouté une résistance de 100 kΩ à chacun des deux connecteurs "octave 1" et "octave 2". Une extrémité de cette résistance est reliée à une sortie de l'Arduino (qui peut être réglée à 0 ou 5 V), alors que l'autre extrémité de la même résistance est reliée à une entrée de l'Arduino (qui peut vérifier si la tension à cet endroit est de 0 ou de 5 V). En bas du circuit, les connecteurs "sol" et "la" sont également branchées à leur propre sortie de l'Arduino (pour l'instant, 6 pins de l'Arduino sont donc nécessaires pour lire ces 4 touches du clavier: nous réglerons ce problème plus tard).
Pour qu'un courant circule dans une des branches de ce circuit, deux conditions doivent être respectées:
- la touche correspondante (qui agit comme interrupteur) doit être enfoncée.
- la tension doit être plus élevée à l'anode de la diode qu'à sa cathode (en d'autres mots, la tension sur les connecteurs situés en haut sur le schéma doit être plus grande que la tension sur les connecteurs situés en bas).
Supposons que je veux vérifier si la touche "sol 1" est enfoncée ou non: je règle l'entrée "octave 1" à 5 volts, l'entrée "octave 2" à 0 volt, l'entrée "sol" à 0 volt, et l'entrée "la" à 5 volts.
Si la touche "sol 1" est enfoncée, un courant circule dans la première diode à gauche. Si on néglige la chute de potentiel dans la diode, "sortie octave 1" se trouve au même potentiel que "entrée sol", donc 0 V. Si la touche "sol 1" n'est pas enfoncée, aucun courant ne circule dans la résistance et "sortie octave 1" se trouve au même potentiel que "entrée octave 1", donc 5 V.
L'état des autres touches n'a pas d'influence sur le résultat: aucun courant ne circulera dans la deuxième diode peu importe que vous enfonciez "sol 2" ou non, puisque le potentiel pour cette touche est de 0 volt en haut et en bas (donc aucune différence de potentiel). Même chose pour la touche "la 1" (0 volt en haut, 5 volts et en bas, mais la diode empêche la circulation d'un courant dans cette direction) et la touche "la 2" (5 volts en haut, 5 volts en bas).
Ensuite, on veut vérifier l'état de la touche "la 1": on règle donc l'entrée "octave 1" à 5 volts, l'entrée "octave 2" à 0 volt, l'entrée "sol" à 5 volts, et l'entrée "la" à 0 volts, et on mesure la tension à la sortie "octave 1".
Pour vérifier l'état de la touche "sol 2", on règle l'entrée "octave 1" à 0 volts, l'entrée "octave 2" à 5 volts, l'entrée "sol" à 0 volt, et l'entrée "la" à 5 volts.
..et on continue comme ça pour chacune des 37 touches du clavier! Ça peut sembler long, mais un microcontrôleur comme l'Arduino peut prendre toutes ces mesures très rapidement, en une fraction de seconde.
Pour économiser les entrées et les sorties de l'Arduino: des registres à décalage
Comme je l'ai mentionné un peu plus haut, la technique de balayage du clavier est gourmande en entrées et sorties: il y a 12 notes différentes, et 4 octaves (avec une entrée et une sortie pour chaque octave), donc un total de 20 entrées/sorties...c'est plus que ce que comporte notre Arduino Uno (d'autant plus que nous voulons aussi y brancher des boutons, des potentiomètres et un afficheur LCD!).
La solution: des registres à décalage. Pour nos 16 sorties (12 notes, 4 octaves), nous utiliserons 2 registres à décalage HC595, alors que pour nos 4 entrées, nous utiliserons un registre à décalage HC165 (puisque ce circuit intégré peut accommoder 8 entrées, nous y brancherons également 4 boutons poussoirs: les deux boutons poussoirs permettant de changer d'octave, et les deux boutons poussoirs permettant de changer le programme).
Si vous désirez plus d'informations sur le principe de fonctionnement de ces deux registres à décalage, je vous réfère à ces deux articles: celui sur le HC595 et celui sur le HC165.
Voici comment chaque registre a été connecté au clavier et à l'Arduino:
Le premier HC595:
Le deuxième HC595:
Le HC165:
Dispositif anti-rebond:
On veut que le numéro du programme augmente de 1 chaque fois qu'on appuie sur le bouton "program up", et qu'il diminue de 1 chaque fois qu'on appuie sur le bouton "program down". Toutefois, en absence de dispositif anti-rebond, il est fréquent que le nombre augmente de 2 ou 3 après une seule pression du bouton. J'ai donc mis en pratique les connaissances exposées dans cet article afin d'éliminer les rebonds, au moyen de bascules de Schmitt (CD40106),
Mouais...ça fait beaucoup de fils à brancher, comme vous pouvez le constater...
L'afficheur LCD, le contrôle de volume, le pitch bend et la sortie MIDI
L'afficheur LCD sera particulièrement pratique pour naviguer parmi les programmes.
Il affichera donc le numéro de programme, la vélocité et le numéro de l'octave.
Les connexions de l'afficheur sont indiquées sur l'illustration ci-dessous (le potentiomètre sert à régler le contraste).
Il faut aussi brancher un potentiomètre à l'entrée A0: ce potentiomètre permettra de régler le volume sonore (la vélocité, si on utilise le vocabulaire MIDI), et un deuxième potentiomètre à l'entrée A1, qui contrôlera le pitch bend (il s'agit en fait d'un joystick, pour que le potentiomètre retourne automatiquement à sa position d'équilibre aussitôt qu'on le relâche).
Et bien sûr, un jack MIDI femelle branché à la pin 1 (TX) de l'Arduino permettra d'acheminer les messages MIDI générés par notre clavier vers un autre dispositif.
Le sketch.
Finalement, voici le sketch (tout de même assez long puisqu'il y a beaucoup de périphériques à gérer):
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/************************************************************** | |
* Clavier MIDI | |
* Ce sketch permet de transformer un vieux clavier | |
* d'orgue électronique en un clavier MIDI. | |
* http://electroniqueamateur.blogspot.com/2015/06/clavier-midi-arduino-version-20.html | |
* Juin 2015, Yves Pelletier | |
**************************************************************/ | |
#include <LiquidCrystal.h> | |
// les pins du HC595 | |
#define latchPin595 7 // Pin 12 du 74HC595 (Latch) | |
#define clockPin595 6 // Pin 11 du 74HC595 (Clock) | |
#define dataPin595 5 // Pin 14 du 74HC595 (Data) | |
// les pins du HC165 | |
#define parallelLoadPin165 3 // Pin 1 du 74HC165 | |
#define clockPin165 2 // Pin 2 du 74HC165 | |
#define dataPin165 4 // Pin 9 du 74HC165 | |
// les pins de l'afficheur LCD | |
LiquidCrystal lcd(13, 12, 11, 10, 9, 8); | |
int octave1In= 7; // pin du hc165 qui reçoit le signal pour le 1er octave | |
int octave2In = 6; // pin du hc165 qui reçoit le signal pour le 2e octave | |
int octave3In = 5; // pin du hc165 qui reçoit le signal pour le 3e octave | |
int octave4In = 4; // pin du hc165 qui reçoit le signal pour le 4e octave | |
int pinOctaveDown = 0; // pin du hc165 du bouton pour augmenter l'octave | |
int pinOctaveUp = 1; // pin du hc165 du bouton pour diminuer l'octave | |
int pinProgrammeDown = 2; // pin du hc165 du bouton pour diminuer le programme | |
int pinProgrammeUp = 3; // pin du hc165 du bouton pour augmenter le programme | |
int valeurs165[8]; // état des entrées du HC165 | |
int etat_des_touches[37]; // 1 quand la touche est enfoncée, 0 sinon | |
int octave = 0; //valeur initiale par défaut de l'octave | |
int octaveDownReady = 1; //vrai quand le bouton n'est pas enfoncé | |
int octaveUpReady = 1; // vrai que le bouton n'est pas enfoncé | |
int programme = 0; // numéro du programme (instrument) | |
int programmeDownReady = 1; //vrai quand le bouton n'est pas enfoncé | |
int programmeUpReady = 1; //vrai quand le bouton n'est pas enfoncé | |
unsigned long debutProgrammeDown; // temps auquel on a appuyé sur le bouton | |
unsigned long debutProgrammeUp; // temps auquel on a appuyé sur le bouton | |
int velocite; // volume sonore, controlé par pententiometre | |
void setup() { | |
// initialisation de l'afficheur | |
lcd.begin(16, 2); | |
// préparation du HC595 | |
pinMode(latchPin595, OUTPUT); | |
pinMode(clockPin595, OUTPUT); | |
pinMode(dataPin595, OUTPUT); | |
// préparation du HC165 | |
pinMode(parallelLoadPin165, OUTPUT); | |
digitalWrite(parallelLoadPin165, HIGH); | |
pinMode(clockPin165, OUTPUT); | |
digitalWrite(clockPin165, LOW); | |
pinMode(dataPin165, INPUT); | |
Serial.begin(31250); //pour transmission MIDI | |
// aucune touche n'est enfoncée au départ | |
for (int i=0; i<=36; i++) | |
{ | |
etat_des_touches[i]=0; | |
} | |
ProgChange(0,programme); | |
} | |
void loop() { | |
// on vérifie toutes les touches du clavier; | |
scanClavier(); | |
// mise a jour du LCD | |
affichage(); | |
} | |
/* Routine qui affiche les parametres actuels sur le LCD */ | |
void affichage (void){ | |
lcd.clear(); | |
lcd.setCursor(0, 0); | |
lcd.print("Prog:"); | |
lcd.print(programme); | |
lcd.setCursor(8,0); | |
lcd.print("Vel:"); | |
lcd.print(velocite); | |
lcd.setCursor(0,1); //deuxieme ligne | |
lcd.print("Oct:"); | |
lcd.print(octave); | |
} | |
/* Routine qui vérifie une par une chaque touche du clavier | |
pour vérifier si elle est enfoncée ou non. | |
On en profite aussi pour vérifier les autres controles: | |
boutons, potentiometre, joystick */ | |
void scanClavier() { | |
velocite = analogRead(A0)/8; // mesure du potentiometre de volume | |
lecture165(); // on lit les 8 tensions mesurées par le hc165 | |
// faut-il changer de programme? | |
if (valeurs165[pinProgrammeDown]) // bouton programme down enfoncé | |
{ | |
if ((programmeDownReady) && (programme > 0)) | |
{ | |
programmeDownReady = 0; | |
programme = programme - 1; | |
ProgChange(0x00,programme); | |
debutProgrammeDown = millis(); | |
} | |
if (((millis()-debutProgrammeDown) > 1000) && (programme > 0)) //bouton enfoncé depuis 1 second: avance rapide | |
{ | |
programme--; | |
ProgChange(0x00,programme); | |
delay(50); | |
} | |
} | |
else // bouton programme down n'est pas enfoncé | |
{ | |
programmeDownReady = 1; | |
} | |
if (valeurs165[pinProgrammeUp]) // bouton programme up enfoncé | |
{ | |
if ((programmeUpReady) && (programme < 127)) | |
{ | |
programmeUpReady = 0; | |
programme = programme + 1; | |
ProgChange(0x00,programme); | |
debutProgrammeUp = millis(); | |
} | |
if (((millis()-debutProgrammeUp) > 1000) && (programme < 127)) | |
{ | |
programme++; | |
ProgChange(0x00,programme); | |
delay(50); | |
} | |
} | |
else // bouton octave up n'est pas enfoncé | |
{ | |
programmeUpReady = 1; | |
} | |
// faut-il changer d'octave? | |
if (valeurs165[pinOctaveDown]) // bouton octave down enfoncé | |
{ | |
if ((octaveDownReady) && (octave > -2)) | |
{ | |
octaveDownReady = 0; | |
octave = octave - 1; | |
} | |
} | |
else // bouton octave down n'est pas enfoncé | |
{ | |
octaveDownReady = 1; | |
} | |
if (valeurs165[pinOctaveUp]) // bouton octave up enfoncé | |
{ | |
if ((octaveUpReady) && (octave < 2)) | |
{ | |
octaveUpReady = 0; | |
octave = octave + 1; | |
} | |
} | |
else // bouton octave up n'est pas enfoncé | |
{ | |
octaveUpReady = 1; | |
} | |
byte registreA, registreB, registreBinitial; | |
int laNote = 0; // numéro de la note détectée | |
// Testons les touches FA1 a DO1 | |
registreA = B11111110; | |
registreB = B11110001; | |
for (int i=0; i <= 7; i++){ | |
shiftOut(registreB,registreA); | |
lecture165(); | |
if (!valeurs165[octave1In]) | |
{ | |
comparaison(laNote,1); | |
} | |
else | |
{ | |
comparaison(laNote,0); | |
} | |
registreA = (registreA << 1) + 1; | |
laNote++; | |
} | |
// Testons D0#1 a MI1 | |
registreA = B11111111; | |
registreBinitial = B11111110; | |
for (int i=0; i <= 3; i++){ | |
registreB = (registreBinitial << 4) | B00000010; | |
shiftOut(registreB,registreA); | |
lecture165(); | |
if (!valeurs165[octave2In]) | |
{ | |
comparaison(laNote,1); | |
} | |
else | |
{ | |
comparaison(laNote,0); | |
} | |
registreBinitial = (registreBinitial << 1) + 1; | |
laNote++; | |
} | |
// Testons les touches FA2 a DO2 | |
registreA = B11111110; | |
registreB = B11110010; | |
for (int i=0; i <= 7; i++){ | |
shiftOut(registreB,registreA); | |
lecture165(); | |
if (!valeurs165[octave2In]) | |
{ | |
comparaison(laNote,1); | |
} | |
else | |
{ | |
comparaison(laNote,0); | |
} | |
registreA = (registreA << 1) + 1; | |
laNote++; | |
} | |
// Testons D0#2 a MI2 | |
registreA = B11111111; | |
registreBinitial = B11111110; | |
for (int i=0; i <= 3; i++){ | |
registreB = (registreBinitial << 4) | B00000100; | |
shiftOut(registreB,registreA); | |
lecture165(); | |
if (!valeurs165[octave3In]) | |
{ | |
comparaison(laNote,1); | |
} | |
else | |
{ | |
comparaison(laNote,0); | |
} | |
registreBinitial = (registreBinitial << 1) + 1; | |
laNote++; | |
} | |
// Testons les touches FA3 a DO3 | |
registreA = B11111110; | |
registreB = B11110100; | |
for (int i=0; i <= 7; i++){ | |
shiftOut(registreB,registreA); | |
lecture165(); | |
if (!valeurs165[octave3In]) | |
{ | |
comparaison(laNote,1); | |
} | |
else | |
{ | |
comparaison(laNote,0); | |
} | |
registreA = (registreA << 1) + 1; | |
laNote++; | |
} | |
// Testons D0#3 a MI3 | |
registreA = B11111111; | |
registreBinitial = B11111110; | |
for (int i=0; i <= 3; i++){ | |
registreB = (registreBinitial << 4) | B00001000; | |
shiftOut(registreB,registreA); | |
lecture165(); | |
if (!valeurs165[octave4In]) | |
{ | |
comparaison(laNote,1); | |
} | |
else | |
{ | |
comparaison(laNote,0); | |
} | |
registreBinitial = (registreBinitial << 1) + 1; | |
laNote++; | |
} | |
//Testons la touche FA4 | |
shiftOut(B11111000,B11111110); | |
lecture165(); | |
if (!valeurs165[octave4In]) | |
{ | |
comparaison(laNote,1); | |
} | |
else | |
{ | |
comparaison(laNote,0); | |
} | |
} | |
// on vérifie si l'état de la touche a changé et, si c'est le cas, | |
// on envoie le message MIDI approprié | |
void comparaison(short laNote, short etat){ | |
if (etat) // touche enfoncé©e | |
{ | |
if (!etat_des_touches[laNote]) { //elle ne l'était pas auparavant | |
etat_des_touches[laNote]=1; | |
noteOn(0x90, 0x29+laNote+12*octave, velocite); | |
} | |
} | |
else // touche non enfoncée | |
{ | |
if (etat_des_touches[laNote]) { //elle ne l'était pas auparavant | |
etat_des_touches[laNote]=0; | |
noteOn(0x90, 0x29+laNote+12*octave, 0x00); | |
} | |
} | |
} | |
// gestion du HC595 | |
void shiftOut(byte dataOut1,byte dataOut2) { | |
boolean pinState; | |
digitalWrite(latchPin595, LOW); | |
digitalWrite(dataPin595, LOW); | |
digitalWrite(clockPin595, LOW); | |
for (int i=0; i<=7; i++) { | |
digitalWrite(clockPin595, LOW); | |
if ( dataOut1 & (1 << i) ) { | |
pinState = HIGH; | |
} | |
else { | |
pinState = LOW; | |
} | |
digitalWrite(dataPin595, pinState); | |
digitalWrite(clockPin595, HIGH); | |
digitalWrite(dataPin595, LOW); | |
} | |
digitalWrite(clockPin595, LOW); | |
digitalWrite(dataPin595, LOW); | |
digitalWrite(clockPin595, LOW); | |
for (int i=0; i<=7; i++) { | |
digitalWrite(clockPin595, LOW); | |
if ( dataOut2 & (1 << i) ) { | |
pinState = HIGH; | |
} | |
else { | |
pinState = LOW; | |
} | |
digitalWrite(dataPin595, pinState); | |
digitalWrite(clockPin595, HIGH); | |
digitalWrite(dataPin595, LOW); | |
} | |
digitalWrite(clockPin595, LOW); | |
digitalWrite(latchPin595, HIGH); | |
} | |
// Lecture des broches du HC165 | |
void lecture165() | |
{ | |
// on met la pin 1 du HC165 à 0 V pour enregistrer l'état de ses 8 entrées | |
digitalWrite(parallelLoadPin165, LOW); | |
digitalWrite(parallelLoadPin165, HIGH); | |
for (int i=0; i <= 7; i++){ | |
// on note l'état de la sortie (pin 9) du HC165 | |
valeurs165[7-i] = digitalRead(dataPin165); | |
// et on envoi un front montant sur la pin 2 pour décaler les valeurs | |
digitalWrite(clockPin165, HIGH); | |
digitalWrite(clockPin165, LOW); | |
} | |
} | |
void PitchWheelChange(int value) { | |
unsigned int change = 0x2000 + value; // 0x2000 == No Change | |
unsigned char low = change & 0x7F; // Low 7 bits | |
unsigned char high = (change >> 7) & 0x7F; // High 7 bits | |
noteOn(0xE0, low, high); | |
} | |
void noteOn(int cmd, int pitch, int velocity) { | |
Serial.write(cmd); | |
Serial.write(pitch); | |
Serial.write(velocity); | |
} | |
void ProgChange(int channel, int prognumber) { | |
Serial.write(0xC0+channel); | |
Serial.write(prognumber); | |
} | |
Je n'avais pas un besoin immédiat d'ajouter une façon de changer le numéro du canal MIDI, mais ça peut facilement être ajouté (il y a encore de la place pour deux boutons supplémentaires sur le CD40106, et ces boutons pourraient être branchés sur les entrées A2 et A3 de l'Arduino).
Yves Pelletier (Twitter, Facebook)
Bonjour, cet exposé m'a beaucoup intéressé car électronicien de métier je cherchais une solution pour midifier un vieux piano électrique de la fin des années 70...cela dit je me demande comment faire pour intégrer également la détection dynamique dans le programme...
RépondreSupprimerbonjour
RépondreSupprimerPourquoi ne pas utiliser directement un atmega 256
plus cher certes ,mais cablage plus simple?