Scripts MIDI-PLAY-WRITE avec Python, MidO et RtMidi (3/6)

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

♦ Précision et Prérequis

MidO (Midi Objects) nous permet de travailler avec des messages MIDI directement en tant qu’objets Python. Après avoir vu « MIDI-OUT » dans un précédent article, ici nous continuons avec « MIDI-PLAY » et « MIDI-WRITE », et dans un prochain article nous finirons par « MIDI-IN ».

Dans cet article nous allons découvrir (que) quelques éléments de base pour jouer et écrire des fichiers MIDI avec MidO, et 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 les deux liens ci-dessous.

• À lire en premier

♦ MIDI-PLAY de MidO

Les objets MidiFile peuvent être utilisés pour lire (read), écrire (write) et jouer (play back) le contenu des fichiers MIDI.

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 un fichier .mid

Nous reprenons des éléments utilisés dans mon précédent article 2/6 et nous y ajoutons ceux spécifiques à la lecture d’un Fichier MIDI Standard (SMF – Standard MIDI Files). La spécification Fichiers MIDI standard définit comment enregistrer une séquence de messages MIDI dans un fichier (.mid) afin qu’ils puissent être lus dans le bon ordre et au bon moment.

Le script -MIDO Play Midi File- dans un éditeur de texte

# *** LECTURE d'un FICHIER MIDI ***
print("===== JOUE un Fichier MIDI... =====")
import mido  # importe la bibliothèque MidO qui gère aussi RtMidi
import time  # importe le module Time Python

# ouvre un port OUT-MidO et le connecte à IN QS-M2 Qsynth/FluidSynth
port = mido.open_output('Synth input port (QS-M2_TofH-XG:0)')

# chemin absolu vers le fichier .mid, ici "blackvelvet.mid"
mid = mido.MidiFile('/home/joe/Python-Projects/Mido-Scripts/blackvelvet.mid')

# affiche chemin fichier Midi + son type + nb de pistes + nb de messages dans fichier
print("=>", mid, "...\n... ...")

# calcul + affiche la durée de lecture du fichier Midi en h:m:s
print("=> Durée de lecture =", time.strftime('%Hh:%Mm:%Ss', time.gmtime(mid.length)))
print("=> Lecture en cours...")

for msg in mid.play():  # boucle de lecture du fichier Midi
    port.send(msg)      # envoi fichier Midi port MidO-OUT vers IN QS-M2 Qsynth/FS

port.close()  # ferme proprement le port Midi
print("=> Fichier MIDI lu... ARRÊT !")

Astuce : Nous pouvons obtenir le temps total de lecture en secondes d’un fichier MIDI en accédant à la propriété length de MidO : mid.length. Puis, il suffit de le convertir en H:M:S.
Ceci n’est pris en charge que pour les fichiers MIDI de type 0 et 1. L’accès à length sur un fichier de type 2 génère ValueError car il est impossible de calculer le temps de lecture d’un fichier asynchrone.
Voir ci-dessous les explications sur les Types 0, 1 et 2 des SMF (Standard MIDI File).

La sortie du script -MIDO Play Midi File- dans 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_Play-Midi-File.py ========
===== JOUE un Fichier MIDI... =====
=> <midi file '/home/joe/Python-Projects/Mido-Scripts/blackvelvet.mid'
    type 1, 11 tracks, 9347 messages> ...
... ...
=> Durée de lecture = 00h:04m:21s
=> Lecture en cours...
... ...
=> Fichier MIDI lu... ARRÊT !
>>> 

Remarque : Il existe trois types de fichiers MIDI standard (SMF) :
Type 0 (piste unique) : tous les messages sont sauvegardés dans une seule piste. Ce type est souvent utilisé par les claviers-synthétiseurs et autres claviers numériques avec séquenceur.
Type 1 (synchrone) : les messages sont sauvegardés dans plusieurs pistes et toutes les pistes commencent en même temps. Ce type est le plus fréquemment utilisé par les séquenceurs et STAN (DAW) logiciels.
Type 2 (asynchrone) : les messages sont sauvegardés dans plusieurs pistes et chaque piste est indépendante des autres. Ce type est très rarement utilisé en pur MIDI.

•  •  •  •  •  •  •  •  •  •
Remix de Black Velvet chantée par Alannah Myles – 1989 (sous distribution GNU/Linux)

Fichier MIDI ~ 30 Ko lu par le script Python MidO -et-
enregistré en temps réel avec Audacity (Audio Jack) dans fichier audio OGG ~ 4,3 Mo

Python
MidO
Fichier
30 Ko
MidO
backend
Synthétiseur
logiciel
Fonte
sonore
Audio
num.
Enregistreur
numérique
Fichier
4,3 Mo
Script .mid RtMidi Qsynth / FS .sf2 Jack Audacity .ogg
•  •  •  •  •  •  •  •  •  •

Capture d’écran du script -MIDO Play Midi File- en action

Le script ne prévoit pas, pour l’instant, l’envoi des messages MIDI vers le clavier virtuel VMPK. Nous verrons cela plus tard. Mais… grâce à Patchage, il est possible d’intervenir visuellement et facilement sur les connexions Audio/MIDI du système.
Il suffit de tirer à la souris une connexion MIDI compatible entre RtMidi Ouput et VMPK Input, et dans VMPK de cocher les cases Activer l’entrée MIDI et Mode MIDI Omni (réception sur les 16 canaux). Et voila, les notes jouées du fichier MIDI s’afficheront dynamiquement sur VMPK.
De plus, si nous utilisons au moins sa version 0.7, chaque canal (instrument) aura sa propre couleur. C’est plutôt sympathique, voire éducatif, que d’écouter un fichier MIDI et de voir en temps réel le jeu dynamique (intensité couleur proportionnelle à la vélocité) de l’artiste à l’écran. N’est-ce pas !
Nous aurions aussi pu utiliser autant de claviers virtuels que de canaux dans le fichier MIDI, en désactivant le Mode MIDI Omni dans VMPK, puis en lançant et disposant à l’écran autant d’occurrences de VMPK que nécessaire. Magique !

Un exemple de configuration utilisée dans le cadre d’un script « Play Midi File » en Python3 – MidO – RtMidi sous GNU/Linux

La version de VMPK (Virtual Midi Piano Keyboard) actuellement disponible dans la Logithèque de Linux Mint n’est que la 0.4.0 et qui commence à dater très fort (juin 2011 !). Pour disposer de VMPK 0.7+, il faudra le télécharger directement depuis le site de son développeur, pour Linux qu’en 64-bit au format AppImage (logiciel Tout-en-un (AIO) avec toutes ses dépendances).
– Linux (64 bit): vmpk-0.7.1-linux-x86_64.AppImage (24 MB) as of December 2018

Une fois téléchargé sur notre ordinateur, validons la case Autoriser l’exécution du fichier comme un programme dans les Propriétés-Permission du fichier, puis un double-clic sur le fichier lance directement l’application VMPK. Nous pouvons aussi créer une entrée dans le Menu Principal de Linux Mint en ouvrant Cinnamon Menu Editor, puis :
– Applications / Son et vidéo => clic sur bouton + Nouvel élément qui ouvre Launcher Properties
. Name: Virtual Midi Piano Keyboard √ (ou autre)
. Command: /home/joe/AppImages/vmpk-0.7.1-linux-x86_64.AppImage √ (fonction d’où nous avons enregistré le fichier)
. Comment: VMPK – un générateur et récepteur d’événements MIDI (ou autre)
. Pour finir, clic sur le bouton √ Valider. Et voila !

♦ MIDI-WRITE de MidO

Nous pouvons créer un nouveau fichier toujours en appelant MidiFile sans l’argument du nom de fichier. Le fichier peut ensuite être enregistré en appelant la méthode save ().

• ÉCRIRE (dans) un fichier .mid

La classe MidiTrack est une sous-classe de la liste, nous pouvons donc utiliser toutes les méthodes habituelles. Tous les messages doivent être marqués avec le temps delta (en ticks). Un delta est le temps d’attente avant le message suivant. S’il n’y a pas de message ‘end_of_track’ à la fin d’une piste, un message sera quand même écrit.

Le script -MIDO Write Midi File- dans un éditeur de texte

# ÉCRITURE d'un fichier MIDI
# GAMME CHROMATIQUE UP sur 6 octaves (Do1 à Do7) + 73 Program Changes GM
import mido    # importe bibliothèque Mido
from mido import Message, MidiFile, MidiTrack  # importe modules depuis Mido
import time    # importe le module Time Python
import random  # importe le module Random Python

mid = MidiFile()          # Nous pouvons créer un nouveau fichier en appelant MidiFile
track = MidiTrack()       # sans l’argument du nom de fichier. Le fichier peut ensuite
mid.tracks.append(track)  # être enregistré à la fin en appelant la méthode save()

n = 24  # initialisation variable n avec note 24 = Do1
p = 0   # initialisation variable p avec program 0 = Grand Piano

while n < 97:  # boucle notes à jouer dans la gamme Chromatique 24 à 96 / Do1 à Do7
    alea = random.randrange(1,8)  # générateur valeurs aléatoires entre 1 et 7
    # affiche les numéros de note / de program-instrument joués
    print("Note", n, "- Instrument", p, "- alea", alea)
    track.append(Message('program_change', program=p, time=0))  # n. program=instrument
    track.append(Message('note_on', note=n, velocity=100, time=32))
    track.append(Message('note_off', note=n, velocity=67, time=128 *alea))
    n = n +1  # incrémentation i (note)
    p = p +1  # incrémentation j (program-instrument)

print("Note", 60, "- Instrument", 64, "- Final touch Soprano Sax")
track.append(Message('program_change', program=64, time=0))  # Final touch Soprano Sax
track.append(Message('note_on', note=60, velocity=127, time=128))
track.append(Message('note_off', note=60, velocity=67, time=1024))

mid.save('MIDO_Write-Midi-File.mid')  # enregistre le tout dans ce fichier Midi
print("=> Fichier MIDI sauvegardé !\n", mid, "...")  # affiche info fichier Midi

Remarque : L’attribut time est utilisé de différentes manières :
– dans une piste, c’est le temps delta en ticks. Cela doit être un entier,
– dans les messages générés par play(), il s’agit du temps delta en secondes (temps écoulé depuis le dernier message renvoyé), et
– dans certaines méthodes (important uniquement pour les développeurs), il est utilisé pour le temps absolu en ticks ou en secondes.

La sortie du script -MIDO Write Midi File- dans 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_Write-Midi-File.py =======
Note 24 - Instrument 0 - alea 5
Note 25 - Instrument 1 - alea 6
Note 26 - Instrument 2 - alea 4
Note 27 - Instrument 3 - alea 2
Note 28 - Instrument 4 - alea 2
Note 29 - Instrument 5 - alea 4
Note 30 - Instrument 6 - alea 4
... ... ... ... ... ... ... ...
Note 90 - Instrument 66 - alea 1
Note 91 - Instrument 67 - alea 4
Note 92 - Instrument 68 - alea 5
Note 93 - Instrument 69 - alea 5
Note 94 - Instrument 70 - alea 7
Note 95 - Instrument 71 - alea 5
Note 96 - Instrument 72 - alea 2
Note 60 - Instrument 64 - Final touch Soprano Sax
=> Fichier MIDI sauvegardé !
 <midi file None type 1, 1 tracks, 222 messages> ...
>>> 

Précision : La synchronisation dans les fichiers MIDI est centrée sur les ticks et les battements (tempo en BPM). Un temps est la même chose qu’une noire (dans une mesure chiffrée 4/4, la noire vaut un temps, soit le quart de la mesure). Les battements sont divisés en ticks, la plus petite unité de temps en MIDI.
Contrairement à la musique physique, le tempo MIDI n’est pas défini en battements par minute, mais en microsecondes par battement. Le tempo par défaut est de 500.000 microsecondes par battement, soit 120 battements par minute (BPM). Le méta-message ‘set_tempo’ peut être utilisé pour changer de tempo pendant une chanson. Nous pouvons utiliser bpm2tempo() et tempo2bpm() pour convertir vers et depuis battements par minute. Notons que tempo2bpm() peut renvoyer un nombre à virgule flottante.

•  •  •  •  •  •  •  •  •  •
La sortie du script sur les hauts-parleurs de mon ordinateur pour une musique toujours cacophonique (ce sont toujours des tests, quoi que)

Fichier MIDI ~ 1 Ko crée avec le script Python MidO -puis-
lu et converti directement par VLC en fichier audio OGG ~ 700 Ko

•  •  •  •  •  •  •  •  •  •

• ÉCRIRE une petite composition dans un fichier .mid

Pour compléter mon précédent exemple, nous allons y ajouter une petite composition dont nous avons transformé la partition musicale en valeurs numériques que MidO peut gérer. Comme toujours, cet exemple n’est qu’une manière de faire parmi d’autres.

Le script -MIDO Write Nocturne Composition File- dans un éditeur de texte

print("PLAY NOCTURNE - Silent Voice ")
print("=============================")
import mido    # importe bibliothèque Mido
from mido import Message, MidiFile, MidiTrack  # importe modules depuis Mido
import time    # importe le module Time Python

mid = MidiFile()          # Nous pouvons créer un nouveau fichier en appelant MidiFile
track = MidiTrack()       # sans l’argument du nom de fichier. Le fichier peut ensuite
mid.tracks.append(track)  # être enregistré à la fin en appelant la méthode save()

# La partition convertie en valeurs numériques, noctn = notes et noctd = durée notes
noctn = [74,71,67,69,71,67,69,74,71,67,69,67,71,69,74,72,71,69,67,69,72,71,69,74,71,69,67,69,71,67,74]
noctd = [1.0,1.0,2.0,1.0,0.5,0.5,2.0,1.0,1.0,2.0,1.0,0.5,0.5,2.0,1.0,1.0,1.0,0.5,0.5,1.0,1.0,1.0,1.0,0.5,0.5,0.5,0.5,1.0,1.0,2.0,2.0]
hauteur = [0,-12,0,+12,0]  # choix des octaves à jouer, 12 = 1 octave et 0 = original

x = 0  #  initialisation index x lecture de gauche à droite liste "hauteur"
for i in hauteur:  # boucle octave à jouer par rapport aux notes d'origine
    delta = hauteur[x]  # nb d'octaves à ajouter ou soustraire exprimé par tranche de 12 notes
    print("   => HAUTEUR =", delta,"notes...")  # affiche nb notes en + ou -
    x = x +1  # incrémentation index x pour hauteur octave (de 0 à nb dans liste "hauteur")

    y = 0  # initialisation index y lecture de gauche à droite listes "noctn" et "noctd"
    for j in noctn:  # boucle notes à jouer dans noctn (notes partition)
        track.append(Message('program_change', program=64, time=0))  # n. program=instrument
        track.append(Message('note_on', note = noctn[y] +delta, velocity = 100, time = 32))
        print("Nocturne note #", noctn[y],"- Durée =", (noctd[y]), "- time =", int(256 *noctd[y]))
        track.append(Message('note_off', note = noctn[y] +delta, velocity = 67, time = int(256 *noctd[y])))
        y = y +1  # incrémentation index y pour couple note/durée (de 0 à nb dans liste "noctn")

track.append(Message('note_on', note = 62, velocity = 100, time = 32))  # touche final
track.append(Message('note_off', note = 62, velocity = 67, time = 1024))

mid.save('MIDO_Write-Nocturne-Composition-File.mid')  # enregistre le tout dans ce fichier Midi
print("=> Fichier MIDI sauvegardé", mid, "...")  # affiche info fichier Midi
print("C'EST FINI !")

Dans le script ci-dessus, les listes noctn = [notes] et noctd = [durées] (lignes 12-13) sont la traduction en valeurs numériques de la petite partition musicale à jouer. Les notes utilisent directement la numérotation MIDI (0.. 127) et les durées ont été converties, croche = 0,5 s, noire = 1 s et blanche = 2 secondes. La liste hauteur = [valeurs multiple de 12] (ligne 14) permet de choisir l’octave auquel le morceau sera joué.
Nous réutilisons le principe de la boucle for/for imbriquée que nous avions vu dans l’article précédent. Le premier for (ligne 17) gère hauteur et le deuxième for (ligne 23) gère noctn/noctd, sachant qu’ici noctn/noctd doivent avoir exactement le même nombre d’éléments.

La sortie du script -MIDO Write Nocturne Composition File- dans 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_PLAY-Nocturne-Composition.py ==
PLAY NOCTURNE - Silent Voice 
=============================
   => HAUTEUR = 0 notes...
Nocturne note # 74 - Durée = 1.0 - time = 256
Nocturne note # 71 - Durée = 1.0 - time = 256
Nocturne note # 67 - Durée = 2.0 - time = 512
   ...   ...   ...
Nocturne note # 74 - Durée = 2.0 - time = 512
   => HAUTEUR = -12 notes...
Nocturne note # 74 - Durée = 1.0 - time = 256
   ...   ...   ...
Nocturne note # 74 - Durée = 2.0 - time = 512
   => HAUTEUR = 0 notes...
Nocturne note # 74 - Durée = 1.0 - time = 256
   ...   ...   ...
Nocturne note # 74 - Durée = 2.0 - time = 512
   => HAUTEUR = 12 notes...
Nocturne note # 74 - Durée = 1.0 - time = 256
   ...   ...   ...
Nocturne note # 74 - Durée = 2.0 - time = 512
   => HAUTEUR = 0 notes...
Nocturne note # 74 - Durée = 1.0 - time = 256
Nocturne note # 71 - Durée = 1.0 - time = 256
Nocturne note # 67 - Durée = 2.0 - time = 512
Nocturne note # 69 - Durée = 1.0 - time = 256
Nocturne note # 71 - Durée = 0.5 - time = 128
Nocturne note # 67 - Durée = 0.5 - time = 128

   ...   ...   ...
Nocturne note # 67 - Durée = 2.0 - time = 512
Nocturne note # 74 - Durée = 2.0 - time = 512
=> Fichier MIDI sauvegardé <midi file None type 1, 1 tracks, 467 messages> ...
C'EST FINI !
>>> 
•  •  •  •  •  •  •  •  •  •
La sortie du script sur les hauts-parleurs de mon ordinateur pour une musique enfin structurée

Fichier MIDI ~ 2 Ko crée avec le script Python MidO -puis-
lu et converti directement par VLC en fichier audio OGG ~ 800 Ko

•  •  •  •  •  •  •  •  •  •

♦ Focus sur MIDI-PLOT

Avant de nous intéresser aux Scripts MIDI-IN avec Python, MidO et RtMidi (5/6), nous allons explorer quelques possibilités ludiques de représentation graphique du MIDI sous Python.

⇒ Lire la suite : Scripts MIDI-PLOT avec Python, MidO et RtMidi (4/6)