Scripts MIDI-OUT avec Python, MidO et RtMidi (2/6)

Initiation à la programmation MIDI (Musical Instrument Digital Interface) en utilisant le trio « Python – MidO – RtMidi », avec focus sur MIDI-OUT

♦ Précision et Prérequis

MidO (Midi Objects) nous permet de travailler avec des messages MIDI directement en tant qu’objets Python. Nous commençons par « MIDI-OUT » car il est plus facile à mettre en œuvre et à traiter que « MIDI-IN ».

Dans cet article nous allons découvrir (que) quelques éléments de base pour envoyer des messages MIDI de MidO (avec RtMidi, son backend par défaut) vers les entrés MIDI de Qsynth/FluidSynth.
Pour de plus amples d’informations, reportez-vous aux documentations ad-hoc dans le lien ci-dessous.

• À lire en premier

• Installer si nécessaire, configurer les logiciels (Audio = Jack, Midi = Alsa, la fonte sonore), et vérifier qu’en appuyant (clavier PC ou souris) sur les touches de VMPK les notes jouées retentissent bien dans les hauts-parleurs de votre PC

  • Système d’exploitation libre GNU/Linux, ici Linux MINT 19.1 Cinnamon 64-bit
  • Python v3.6.7+
  • Python3-Mido v.1.2.7+
  • Python3-Rtmidi v.1.1.0+
  • IDLE Python v.3.6.7+
  • QjackClt + Jack
  • Qsynth + FluidSynth + au moins une Fonte sonore .sf2, par exemple FluidR3_GM.sf2
  • VMPK (Virtual Midi Piano Keyboard), de préférence v.0.7+ (intègre beaucoup de nouveautés)
  • Patchage

• Mappage des connexions MIDI Alsa et AUDIO Jack

Patchage permet de voir en un coup d’œil le mappage graphique complet pour les systèmes Audio et MIDI basés sur Jack et Alsa. C’est aussi un excellent outil de vérification et de dépannage. Il autorise même d’intervenir directement à la souris sur les connexions (ON/OFF) compatibles en cas de besoin.

Ci-dessous la configuration utilisée pour mes premiers articles sur la programmation MIDI / Python.

Un exemple d’utilisation de Patchage dans le cadre de l’écriture d’un script Python3 – MidO – RtMidi sous GNU/Linux

♦ Ports Midi-OUT de MidO (avec backend RtMidi)

import mido — Dans tous les cas, il faudra d’abord importer cette bibliothèque MidO dans Python !

>>> import mido
>>>

NB : Un message MidO est un objet Python avec des méthodes et des attributs. Les attributs varient en fonction du type de message. Les messages MidO sont livrés avec une vérification intégrée du type et des valeurs. Cela signifie que l’objet de message est toujours un message Midi valide (après vérification).

• Les 3 actions ci-dessous peuvent se faire soit dans une console (shell) Python soit dans un IDE Python comme IDLE.

De nombreuses autres possibilités de commandes existent aussi. Pour cela, lire la documentation de MidO.

◊ Lister les ports Midi-IN disponibles pour MidO-OUT – mido.get_output_names()

Un port Mido est un objet pouvant envoyer ou recevoir des messages Midi (ou les deux).
Sur mon ordinateur, MidO liste 10 entrées triées possibles dont 8 sont réellement utilisables par le programmeur. QjackCtl et Patchage sont en fait des baies de brassage modulaire graphique.

>>> print(mido.get_output_names())
['Client-128:qjackctl 128:0',
'FLUID Synth (QS-M1_FluidR3-GM):Synth input port (QS-M1_FluidR3-GM:0) 129:0',
'FLUID Synth (QS-M2_TofH-XG):Synth input port (QS-M2_TofH-XG:0) 130:0',
'FLUID Synth (QS-M3_SGM-GM):Synth input port (QS-M3_SGM-GM:0) 131:0',
'FLUID Synth (QS-M4_084MG-GM):Synth input port (QS-M4_084MG-GM:0) 132:0',
'FLUID Synth (QS-M5_Titanic200-GM):Synth input port (QS-M5_Titanic200-GM:0) 133:0',
'FLUID Synth (QS-M6_Crisis-GM):Synth input port (QS-M6_Crisis-GM:0) 134:0',
'Midi Through:Midi Through Port-0 14:0',
'Patchage:System Announcement Reciever 137:0',
'VMPK Input:in 135:0']
>>>

Astuce : S’assurer d’avoir lancé toutes les applications MIDI et si nécessaire d’avoir effectuer les branchements des équipements MIDI externes avant d’exécuter cette commande.

◊ Ouvrir des ports MidO-OUT – outport = mido.open_output(‘nom-du-port’)

Nous pouvons ouvrir des ports Midi en appelant l’une des méthodes d’ouverture, par exemple ci-dessous.
Nous choisissons dans la liste ci-dessus 2 Midi-IN (Qsynth-M1 et VMPK) pour y connecter RtMidi Output (MidO-OUT). Tous les messages Midi qui sortiront sur MidO-OUT seront envoyés à ces deux MIDI-IN. Nous pouvons le vérifier en agrandissant la fenêtre Patchage ou en explorant les onglets derrière le bouton Connecter de QjackCtl.

>>> outport = mido.open_output('FLUID Synth (QS-M1_FluidR3-GM):Synth input port (QS-M1_FluidR3-GM:0) 129:0')
>>> outport = mido.open_output('VMPK Input:in 135:0')
>>>

Astuce : Lorsque nous utilisons Linux/ALSA, les noms de port (IN & OUT) incluent le nom du client, le client ALSA et les numéros de port, par exemple ici :
‘FLUID Synth (QS-M1_FluidR3-GM):Synth input port (QS-M1_FluidR3-GM:0) 129:0’
Le client ALSA et les numéros de port (ici « 129:0 ») peuvent changer d’une session à l’autre, rendant difficile le codage en dur des noms de port ou leur utilisation dans des fichiers de configuration.
Pour contourner ce problème, le backend de RtMidi nous permet permet de laisser de côté le « numéro de port » du numéro de port / nom du client. Ces deux lignes ouvriront le port ci-dessus :
‘Synth input port (QS-M1_FluidR3-GM:0)’  (le plus court) -ou-
‘FLUID Synth (QS-M1_FluidR3-GM):Synth input port (QS-M1_FluidR3-GM:0)’.

◊ Utiliser des ports MidO-OUT – outport.send(mido.Message(‘note_on’, channel=val, note=val, velocity=val))

Nous pouvons envoyer des messages Midi au travers des ports physiques et/ou virtuels en appelant l’une des méthodes d’envoi, par exemple ci-dessous.
MidO envoie sur les 2 ports ci-dessus un message Midi note_on sur le canal 4 (0.. 15, défaut 0) avec la note n° 60 (0.. 127) ayant une vélocité de 100 (0.. 127, défaut 64), puis un message note_off.

>>> outport.send(mido.Message('note_on', channel=3, note=60, velocity=100))
>>> outport.send(mido.Message('note_off', channel=3, note=60))
>>>

Astuce : Parfois, les notes sont bloquées parce qu’une note_off n’a pas été envoyée – a été oubliée. Pour forcer l’arrêt de toutes les notes sur tous les canaux qui sonnent, nous pouvons appeler la commande :
    outport.panic()
Cela ne réinitialisera pas les contrôleurs. Contrairement à reset (), les notes ne seront pas désactivées normalement, mais s’arrêteront immédiatement sans tenir compte du temps de décroissance.

Système de portées musicales avec correspondance MIDI

Mémento correspondance notation musicale française/anglaise avec numéro de notes MIDI

♦ ENVOYER des messages MIDI avec MidO

Comme vous le constaterez en lisant la documentation de MidO, il y a plusieurs façons de faire les choses en fonction de ses besoins et de ses équipements. Ci-dessous nous prenons une voie parmi d’autres.

• JOUER la gamme chromatique de DO1 à DO7 (6 octaves)
Script simple Python3 – MidO avec IDLE Python

Nous allons utiliser le plus simple, la fonction while de Python pour créer une boucle.
  • Démarrage du script…
  • Importation de mido et time
  • Connexion du port MidO-OUT à Midi-IN de Qsynth/FluidSynth
  • Fixe valeur variable i (notes Midi)
  • Boucle while ascendante
    • Affiche le numéro de note jouée
    • Envoie la note i comprise entre 24 et 96
    • Incrémentation variable i
  • Après avoir joué toute la gamme chromatique Do1 à Do7
  • Le script se termine.
# GAMME CHROMATIQUE ASCENDANTE/UP (12 notes/octave) sur 6 octaves (Do1 à Do7)
import mido    # importe bibliothèque MidO (MIDI Objects) qui gère directement RtMidi
import time    # importe module Time Python

# Ouvre un port Midi-OUT de MidO => un port Midi-IN de Qsynth/FluidSynth
outport = mido.open_output('FLUID Synth (QS-M1_FluidR3-GM):Synth input port (QS-M1_FluidR3-GM:0) 129:0')

i = 24  # initialisation variable i pour note 24 = Do1
while i < 97:  # boucle notes à jouer dans la gamme Chromatique 24 à 96 / Do1 à Do7
    print("Note =", i)  # affiche numéro de note jouée
    outport.send(mido.Message('note_on', note=i, velocity=100))
    time.sleep(0.3)  # durée d'environ 1/3 seconde
    outport.send(mido.Message('note_off', note=i))
    i = i +1  # incrémentation de l'index i (note jouée)

Remarque : Ni channel ni program n’ont été définis dans ce script. Ce sont les valeurs par défaut qui sont utilisées par MidO, à savoir channel = 0 et program = 0.

• JOUER un accord DO3 sur les 128 instruments GM (General Midi)
Script Python3 – MidO avec IDLE Python

Nous continuons d’utiliser la fonction while de Python pour créer une boucle et nous y ajoutons le message program_change pour contrôler le canal et les instruments.
  • Démarrage du script…
  • Importation de mido, time et random
  • Connexion du port MidO-OUT à Midi-IN de Qsynth/FluidSynth
  • Fixe valeur aux deux variables i (instrument Midi) et ch (canal Midi)
  • Boucle while ascendante ou descendante en fonction du sens choisi
    • Génération d’un nombre aléatoire compris entre 1 et 3 pour la durée des notes
    • Affiche n° de l’instrument et son canal Midi obtenu avec program_change (channel et program)
    • Envoie 4 notes do3-mi-sol-do4 (ON/OFF) avec durées variables
    • Incrémentation ou décrémentation variable i en fonction du choix de départ
  • Après avoir joué l’accord Do3 sur les 128 instruments GM
  • Le script se termine.

Le script 128 Instruments GM dans un éditeur de texte

Ligne 14 : petite incohérence coloration syntaxique à la fin de ligne.
« # instrument GM 0…127 + durée aléatoire » devrait être tout en bleu car c’est un commentaire !

print("JOUE les 128 instruments GM ordre croissant ou décroissant en accord Do3")
print("========================================================================")
import mido    # importe bibliothèque MidO (MIDI Objects) qui gère directement RtMidi
import time    # importe module Time Python
import random  # importe module Random Python

# Ouvre un port Midi-OUT de MidO => un port Midi-IN de Qsynth/FluidSynth
outport = mido.open_output('FLUID Synth (QS-M1_FluidR3-GM):Synth input port (QS-M1_FluidR3-GM:0) 129:0')

i = 0   # 1) variable i=0 pour instrument Up (ou i=127 pour Down)
ch = 1  # variable ch=0 à 15 choix du canal Midi - ici 1 correspond au canal 2 Qsynth
while i<128:  # 2) boucle accord Do3 pour les 128 instruments GM avec i<128 (ou i>=0)
    alea = random.randrange(1,4) # générateur valeurs aléatoires entre 1 et 3
    print("=> Instrument #",i,"- Canal #",ch, end="  ") # instrument GM 0...127 + durée aléatoire
    outport.send(mido.Message('program_change', channel=ch, program=i)) # canal/programme
    outport.send(mido.Message('note_on', channel=ch, note=48, velocity=90))
    time.sleep(alea *0.2)  # durée aléatoire pondérée pour donner semblant de vie
    outport.send(mido.Message('note_off', channel=ch, note=48))
    outport.send(mido.Message('note_on', channel=ch, note=52, velocity=102))
    time.sleep(alea *0.1)  # durée aléatoire...
    outport.send(mido.Message('note_off', channel=ch, note=52))
    outport.send(mido.Message('note_on', channel=ch, note=55, velocity=114))
    time.sleep(alea *0.1)  # durée aléatoire...
    outport.send(mido.Message('note_off', channel=ch, note=55))
    outport.send(mido.Message('note_on', channel=ch, note=60, velocity=127))
    time.sleep(alea *0.3)  # durée aléatoire de 0.3, 0.6 ou 0.9 seconde
    outport.send(mido.Message('note_off', channel=ch, note=60))
    print("suivant...")
    i = i +1  # 3) up +1 (ou down -1) du compteur i - voir ci-dessus 1) & 2)
print("TERMINÉ !")

Astuce : Dans le script ci-dessus, la progression des 128 instruments GM est ascendante de 0 à 127 et le canal de transmission est 1 (en fait 2 pour Qsynth/FluidSynth qui démarre à 1).
Pour avoir la progression GM descendante de 127 à 0, il suffit de modifier :
ligne 10 : i = 0  en  i = 127  -et-  ligne 12 : while i < 128:  en  while i >= 0:  -et-  ligne 29 : i = i +1  en  i = i -1
Et pour changer de canal, il suffit de modifier :
ligne 11 : ch = 1  en  ch = de 0 à 15
Puis de mettre à jour les quelques commentaires qui vont avec.

•  •  •  •  •  •  •  •  •  •
La sortie du script sur les hauts-parleurs de mon ordinateur pour une musique plutôt cacophonique (ce sont des tests, quoi que) enregistrée live

Fichier audio OGG ~ 2,8 Mo, script enregistré live avec Audacity (Audio Jack)

•  •  •  •  •  •  •  •  •  •

La sortie du script sur une console Python

Python 3.6.7 (default, Oct 22 2018, 11:32:17) 
[GCC 8.2.0] on linux
Type "help", "copyright", "credits" or "license()" for more information.
>>>
=== RESTART: /home/joe/Python-Projects/Mido-Scripts/MIDO-OUT_Play-128-Instruments.py ===
JOUE les 128 instruments GM ordre croissant ou décroissant en accord Do3
========================================================================
=> Instrument # 0 - Canal # 1  suivant...
=> Instrument # 1 - Canal # 1  suivant...
=> Instrument # 2 - Canal # 1  suivant...
=> Instrument # 3 - Canal # 1  suivant...
=> Instrument # 4 - Canal # 1  suivant...
=> Instrument # 5 - Canal # 1  suivant...
    ... ... ... ... ... ... ...
=> Instrument # 123 - Canal # 1  suivant...
=> Instrument # 124 - Canal # 1  suivant...
=> Instrument # 125 - Canal # 1  suivant...
=> Instrument # 126 - Canal # 1  suivant...
=> Instrument # 127 - Canal # 1  suivant...
TERMINÉ !
>>>

Capture d’écran du script 128 Instruments GM en action

  • Nous pouvons vérifier le bon déroulement du script de plusieurs manières :
    • Affichage dans le Shell Python du texte défini
    • Affichage dans la fenêtre Qsynth Canaux de la bonne sélection de la fonte sonore, de la progression du n° de programme et du nom de l’instrument sur le bon canal 2
    • Clignotement de la Led verte sous Entrée pour le canal 2 à chaque réception de notes MIDI

• JOUER la gamme DO majeure de Do1 à Do7 (6 octaves)
(ou autres gammes juste avec quelques modifications)
Script Python3 – MidO avec IDLE Python

Nous allons maintenant utiliser la fonction for/for de Python pour créer une boucle imbriquée.
  • Démarrage du script…
  • Importation de mido, time et random
  • Connexion du port MidO-OUT à Midi-IN de Qsynth/FluidSynth
  • Fixe valeur aux deux variables octaverange (notes/octave) et octavenote (note départ), et définit notescale (7 notes gamme majeure)
  • Boucle for ascendante (octave)
    • Affiche le n° octave
    • Initialisation index x (lecture liste notescale)
    • Boucle for ascendante (notes) imbriquée dans la précédente
      • Envoie note_on des 7 notes dans l’octave en cours
      • Génération d’un nombre aléatoire compris entre 1 et 3 pour la durée des notes
      • Affiche le n° note et sa durée sur une même ligne
      • Envoie note_off
      • Incrémentation de l’index x (liste note gamme majeure)
  • Envoie note 96 (do7) (ON/OFF)…
  • Le script se termine.
print("Gamme DO Majeure UP (7 notes/octave) sur 6 octaves (Do1 à Do7)")
print("==============================================================")
import mido    # importe bibliothèque MidO (MIDI Objects) qui gère directement RtMidi
import time    # importe module Time Python
import random  # importe module Random Python

# Ouvre un port Midi-OUT de MidO => un port Midi-IN de Qsynth/FluidSynth
outport = mido.open_output('Synth input port (QS-M1_FluidR3-GM:0)')

print("==> Boucle FOR/FOR imbriquée avec RANGE <==")
octaverange = 12  # 12 notes/octave
octavenote = 23   # Tonalité DO Maj. - astuce prog. note départ adaptée = SI0 (DO1 - 1)
notescale = [1,3,5,6,8,10,12]  # liste notes gamme Majeure (octave)

for i in range(2,8):  # boucle Échelle musicale (octave up de 2 à 7)
    octavenote = (octaverange * i) -1  # octave réel de 1 à 6
    print("Octave =", i -1)  # affiche i-1 = n° octave réel joué de 1 à 6

    x = 0  # initialisation index (val. gauche) lecture de la liste "notescale"
    for j in notescale:  # boucle notes à jouer dans la Gamme musicale (par octave)
        outport.send(mido.Message('note_on', note=(octavenote + notescale[x])))
        alea = random.randrange(1,4)  # générateur valeurs aléatoires entre 1 et 3
        time.sleep(alea *0.3)  # durée aléatoire 0.3 à 0.9s pour donner un peu de vie
        print("  Note Up DO Majeure =", (octavenote + notescale[x]), end=" - ")
        print("Durée =", round((alea * 0.3), 1), "s"),  # Affiche 1 ligne pour les 2 print
        outport.send(mido.Message('note_off', note=(octavenote + notescale[x])))
        x = x +1  # incrémentation de l'index x (de 0 à nb dans liste "notescale")

outport.send(mido.Message('note_on', note=(octavenote +13)))
print("Dernière note Up DO Majeure =", (octavenote +13))  # note = 96 (Do7)
time.sleep(1)  # pause d'une seconde
outport.send(mido.Message('note_off', note=(octavenote +13)))
Astuce : Pour changer de Gamme musicale dans le script ci-dessus, il suffit de modifier la ligne 13 notescale = [.., .., .., …] avec les degrés ad-hoc et les quelques commentaires qui vont avec, par exemple :
#========================================
# Correspondance entre les principaux systèmes de Notation Musicale en Europe occidentale
# - Français   Do   Ré   Mi   Fa   Sol  La   Si   Do
# - Italien    do   re   mi   fa   sol  la   si   do
# - Anglais    C    D    E    F    G    A    B    C
# - Allemand   C    D    E    F    G    A    H    C
# avec 1 octave = 12 * 1/2 tons / note# = +1/2 ton et noteb = -1/2 ton
# d'où Do# = Réb / Dob = Si / Mi# = Fa / Fab = Mi / etc.
# d'où C#  = Db  / Cb  = B  / E#  = F  / Fb  = E  / etc.
#========================================
# Gamme Chromatique : Do - Do# - Ré - Ré# - Mi - Fa - Fa# - Sol - Sol# - La - La# - Si - Do
#           n° note : 1     2    3     4    5    6     7     8     9     10   11    12   13
#========================================
# Gamme DO Majeure : Do - Ré - Mi - Fa - Sol - La - Si - Do
#          n° note : 1    3    5    6     8    10   12   13
#========================================
# Gamme DO Mineure : Do - Ré - Mib - Fa - Sol - Lab - Sib - Do
#          n° note : 1    3     4    6     8     9    11    13
#========================================

♦ Focus sur MIDI-PLAY-WRITE

MidO (Midi Objects) nous permet de travailler avec des messages MIDI directement en tant qu’objets Python.

Nous allons continuer avec « MIDI-PLAY » et « MIDI-WRITE »…

⇒ Lire la suite : Scripts MIDI-PLAY-WRITE avec Python, MidO et RtMidi (3/6)