Transformer un port série en carte son minimaliste

Pourquoi ? Parce que.

Image d'entête

par skywodd | | Licence (voir pied de page)

Catégories : Projets | Mots clefs : Musique Serial UART Hack


Dans ce mini projet, je vais vous montrer comment transformer un port série TTL en une carte son minimaliste avec seulement trois composants et un peu de code Python. L'utilité d'un tel projet ? Aucune, mais on va bien s'amuser !

Sommaire

Bonjour à toutes et à tous !

Transformer un port série en carte son minimaliste, voilà typiquement le genre d'idée farfelue complètement inutile auquel on peut aboutir quand un bricoleur s'ennuie.

J'avais fait un article sur ce sujet il y a quelques années, à l'époque du blog Skyduino (instant nostalgie). Au détour d'une recherche Google, je suis retombé sur cet article. En le relisant, je me suis dit qu'il serait intéressant de le remettre aux goûts du jour.

Posez votre cerveau sur la table un instant, cet article va lier l'inutile à l'agréable, au nom de la science.

La modulation par longueur d'impulsion, ou PWM pour les intimes

En électronique, il existe deux grandes écoles : l'analogique et le numérique.

Régulièrement, on se retrouve à devoir convertir des signaux analogiques en signaux numériques et inversement. Sauf que cela n'est pas si simple. Cela demande même pas mal de technique.

La PWM ("Pulse Width Modulation", ou "Modulation par longueur d'impulsion" en français) est une méthode de modulation de signal numérique qui permet de "convertir" un signal numérique en un "quasi" signal analogique.

PS Il serait plus juste de parler d'un "signal de contrôle analogique", car ce n'est pas un véritable signal analogique en sortie.

Ce genre de technique est très utile pour réguler la vitesse d’un moteur, moduler la luminosité d’une LED, ou – moyennant un haut-parleur – de faire des sons plus ou moins complexes.

Illustration d'un signal PWM

Illustration d'un signal PWM avec un rapport cyclique de 50%

Le principe de base de la modulation PWM est assez simple.

On dispose d’un signal logique, avec deux états possibles : "1" / HIGH / N volts ou "0" / LOW / 0 volt et une fréquence de modulation fixe. Cette fréquence est représentée par la période T sur le graphique ci-dessus. Pour rappel : Fréquence = 1 / Période.

La fréquence de modulation ne change pas, c’est uniquement le rapport temps haut (Thaut) / temps bas (Tbas) qui est variable. En faisant varier ce rapport, on fait varier la valeur moyenne du signal. Par extension, on fait donc aussi varier la tension moyenne (analogique) du signal.

N.B. En soi, un signal PWM n'est pas un signal analogique. C'est un signal numérique qui, sous certaines conditions, peut se comporter comme un signal analogique. Pour que cela soit le cas, il faut que le montage (ou le composant) recevant ce signal se comporte comme un montage intégrateur de tension.

Dans l’exemple ci-dessus, j’ai représenté un signal avec un rapport cyclique de 50% (Thaut = Tbas = 50% de T). Si ce signal était utilisé pour contrôler un moteur par exemple, il tournerait à la moitié de sa vitesse maximum.

Avec un signal PWM, ce qui compte c’est le rapport temps haut / temps bas. Si on mesurait la tension moyenne du signal ci-dessus avec un multimètre, on obtiendrait la moitié de la tension d'alimentation.

Du coup si l’on est malin, on peut détourner ce principe de modulation pour faire des sons, ou plus généralement n’importe quel signal analogique, moyennant l’usage d’un filtre passe-bas pour lisser le signal.

Il suffit ensuite de moduler le signal PWM à une fréquence définie, de préférence la plus élevée possible, à plusieurs MHz par exemple, pour obtenir un DAC low cost ("Digital to Analogic Converter", un convertisseur numérique analogique en français). Cela ne vaut pas un "vrai" convertisseur numérique analogique en terme de performance, mais suivant l'application, ça peut être tout à fait suffisant.

Dans le cadre de cet article, pour générer des sons comme le ferait une vraie carte son, il suffit de changer à intervalle régulier la valeur du rapport cyclique, par exemple à 44.1KHz. C’est grâce à cette astuce qu’il est possible de faire de la musique "chiptune" ou encore de jouer des petits morceaux de musique avec un microcontrôleur sans "vraie" sortie analogique.

Générer un signal PWM avec un port série

On peut générer des sons en utilisant un signal PWM, soit. Cependant, le sujet de cet article est la transformation d'un port série en carte son minimaliste. Or, un port série ne génère pas de signaux PWM ! Ou peut être bien que si en fait …

Illustration du format d'une trame série TTL

Illustration du format d'une trame série TTL

Vu à l’oscilloscope une "trame" (un octet) d'un signal série ressemble au graphique ci-dessus.

Au début se trouve un "bit de start", toujours à "0", puis 8 bits de données (suivant la configuration du port série, il peut y avoir 5, 6, 7 voir 9 bits) et pour finir 1 (ou 2) "bits de stop", toujours à "1".

PS Suivant la configuration du port série, il est aussi possible d'avoir un "bit de parité" avant le ou les bits de stop pour détecter une erreur de communication. Dans les faits, le bit de parité est souvent désactivé, car peu efficace pour détecter une erreur.

Le bit de start permet la synchronisation du port série côté récepteur. Le bit de stop permet la détection d'une "erreur de frame" (problème d'horloge / de synchronisation). De plus, quand le signal est au repos, le port série reste à "1". Cela permet de détecter un "break", soit une coupure dans la communication (un héritage des premières communications télégraphiques filaires).

En y regardant de plus près, on se rend compte que sans le bit de start et de stop on dispose de 8 "temps" modifiables à volonté. Sans ces bits de start et de stop on pourrait donc générer à la main un signal PWM sans difficulté juste en envoyant les octets adéquats via le port série.

Illustration du principe de génération de signaux PWM avec un port série

Illustration du principe de génération de signaux PWM avec un port série

La solution à ce problème consiste à simplement inverser le signal du port série avec un peu d'électronique.

Ce qui nous empêche d’utiliser le signal série comme un bête signal PWM est le fait que la trame série commence par un niveau "0" et se fini par un niveau "1". C’est tout l’inverse d’un signal PWM qui commence par un niveau "1" puis se finit par un niveau "0".

Si on inverse le signal, le problème ne se pose plus. On perd deux temps puisque non modifiable, mais les 8 temps restants du signal PWM sont contrôlables. Il reste cependant un (gros) problème à résoudre : comment générer une série d’octets qui une fois transmis via le port série font que celui-ci devient une sortie PWM. C'est ce que l'on va étudier dans le prochain chapitre.

N.B. On parle ici d’une sortie PWM avec une très faible précision. Avec 8 temps contrôlables par période, cela représente un signal PWM avec une résolution de 3 bits (2 ^ 3 = 8). Pour comparaison, une sortie PWM sur un microcontrôleur bas de gamme a une résolution de 8 bits (soit 256 temps par période). Une carte son standard à une sortie sur 16 bits, voir 24 bits pour une carte son de qualité studio. Il ne faudra donc pas s’attendre à une qualité audio type blueray avec le montage présenté un peu plus bas.

Le montage

Le montage de ce projet se compose de seulement trois composants (ok, quatre en réalité, mais le quatrième est optionnel) que l'on laisse généralement trainer dans un tiroir au fond de son atelier.

Photographie du matériel nécessaire à la réalisation du montage de lecture de fichiers audio via un port série

Matériel nécessaire

Pour réaliser ce montage, il va nous falloir :

  • Un module USB-série TTL (et son câble USB),

  • Un transistor PNP quelconque (à ne pas confondre avec un transistor NPN), comme un classique BC557 par exemple

  • Une résistance de 1K ohms, code couleur : marron / noir / rouge,

  • Une diode 1N4004 ou 1N4007 (optionnelle),

  • Une plaque d'essai et des fils pour câbler notre montage.

Vue prototypage du montage de lecture de fichiers audio via un port série

Vue prototypage du montage

Vue schématique du montage de lecture de fichiers audio via un port série

Vue schématique du montage

Pour commencer le montage, il va falloir relier la sortie du module USB-série à une extrémité de la résistance de 1K ohms, puis l'autre extrémité à la base du transistor PNP (la broche du milieu sur un BC557).

Ensuite, il faut relier l'alimentation du module USB-série à l'émetteur du transistor (la broche de droite quand vous avez le transistor en face de vous) et le collecteur du transistor à une des broches du haut-parleur.

Photographie du montage de lecture de fichiers audio via un port série

Le montage fini

Pour finir, il convient de relier la seconde broche du haut-parleur à la masse du module USB-série et de placer la diode entre les bornes du haut-parleur, avec l'anode (le côté sans la barre blanche) du côté de la masse du module USB-série.

Pourquoi un transistor PNP ?

Généralement, on utilise un transistor NPN, qui devient passant quand un courant est appliqué sur la base du transistor.

Mais dans notre cas, on souhaite inverser le signal. On a donc besoin d'un transistor qui devient passant quand il n'y a pas de courant sur la base. C'est la définition même d'un transistor PNP.

Le code

Le montage c'est fait, passons au code.

Le code n'a pas grand-chose à faire en réalité :

  • Il doit prendre en entrée un fichier audio et le lire,

  • Il doit ensuite effectuer un traitement sur chaque échantillon audio à l'intérieur du fichier audio,

  • Pour finir, il doit envoyer le résultat sur le port série.

Le code ci-dessous est réalisé en Python 3, mon langage de programmation préféré pour ce genre de projet.

1
2
3
import wave
import argparse
import serial

Le code a besoin de trois bibliothèques de code, dont deux standards :

  • "wave" pour la lecture de fichier audio ".wav",

  • "argparse" pour la gestion des arguments en ligne de commande,

  • "serial" pour la communication via le port série (à installer manuellement).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def resample(x):
    """ Converti l'échantillon audio x sur 8 bits en un signal PWM sur 3 bits (avec inversion). """
    if x >= 224: return 0b00000000
    if x >= 195: return 0b10000000
    if x >= 168: return 0b11000000
    if x >= 140: return 0b11100000
    if x >= 112: return 0b11110000
    if x >= 84:  return 0b11111000
    if x >= 56:  return 0b11111100
    if x >= 28:  return 0b11111110
    return 0b11111111

La première fonction à étudier est la fonction qui prend un échantillon audio (sur 8 bits) en paramètre et donne en sortie une valeur PWM (inversée) sur 3 bits.

La fonction est constituée d'une cascade de if permettant de renvoyer la valeur adéquate en fonction de l'échantillon audio. On remarquera que les "1" et les "0" sont inversés. C'est normal, puisqu'il y a une inversion du signal en sortie du port série.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# Taille du buffer pour l'envoi via le port série (permet l'arrêt du script via CTRL-C)
STREAM_BUFFER_SIZE = 1024


def chunks(l, n):
    """ Découpe le buffer l en morceau de n octets. """
    for i in range(0, len(l), n):
        yield l[i : i + n]


def stream_audio_file(filename, serial_port):
    """ Lit le fichier audio spécifié via le port série spécifié en utilisant des données formatées pour générer un signal PWM rudimentaire. """

    # Ouvre le fichier audio
    with wave.open(filename, 'rb') as fi:

        # Vérifie la compatibilité du format de fichier
        assert fi.getnchannels() == 1, "Only mono wave files are supported"
        assert fi.getsampwidth() == 1, "Only 8bits PCM wave files are supported"

        # Converti les données audio en données série PWM
        print("Converting audio file to PWM serial data ...")
        nb_frames = fi.getnframes()
        framerate = fi.getframerate()
        print('Source file "{}" is {} frames long, sampled at {} Hertz'.format(filename, nb_frames, framerate))
        data = bytes(map(resample, fi.readframes(nb_frames)))
    
    # Ouvre le port série et envoi les données
    serial_baudrate = framerate * 10  # 8N1: 1 Start, 8 Data, 1 Stop
    print('Setting serial port "{}" in mode 8N1, at {} bps ...'.format(serial_port, serial_baudrate))
    with serial.Serial(port=serial_port,
                       baudrate=serial_baudrate,
                       bytesize=serial.EIGHTBITS,
                       parity=serial.PARITY_NONE,
                       stopbits=serial.STOPBITS_ONE) as serial_com:
        print("Streaming data ... Use CTRL+C to stop.")
        for chunk in chunks(data, STREAM_BUFFER_SIZE):
            serial_com.write(chunk)
            serial_com.flush()

Vient ensuite la fonction principale qui ouvre le fichier audio, vérifie plusieurs paramètres de celui-ci et exécute le traitement des échantillons audio.

Une fois le traitement exécuté, le port série est ouvert et les données sont transmises par paquets de STREAM_BUFFER_SIZE octets pour permettre à l'utilisateur d'arrêter le script avec le raccourci clavier CTRL + C si nécessaire.

La conversion des données est réalisée au moyen de la fonction map() qui permet d'exécuter une fonction sur chaque élément d'un tableau et de la fonction bytes() qui permet de créer un tableau d'octets à partir d'une séquence d'octets indépendants.

N.B. Pour que la lecture se fasse à la bonne vitesse, il est nécessaire de configurer la vitesse du port série à 10 fois la fréquence d'échantillonnage du fichier audio. Cela est obligatoire, car 1 octet sur le port série = 10 bits = 1 échantillon audio. Tous les modules USB-série ne supportent pas de telles vitesses "non standard".

1
2
3
4
5
6
7
8
# Interface en ligne de commande
if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Stream an audio wave file over a serial port using PWM-like serial data.')
    parser.add_argument('filename', metavar='<WAVE FILE>', help='The input audio wave file (must be a mono 8 bits PCM file).')
    parser.add_argument('serialport', metavar='<SERIAL PORT>', help='The output serial port.')

    args = parser.parse_args()
    stream_audio_file(args.filename, args.serialport)

Le dernier morceau de code gère la partie ligne de commande et permet l'affichage d'un message d'aide si les paramètres fournis ne sont pas corrects. Cette partie n'est pas très intéressante donc je ne vais pas en parler plus que cela.

Le code complet avec commentaires :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
"""
Lit un fichier audio .wav via un port série en utilisant des données formatées pour générer un signal PWM rudimentaire.
"""

import wave
import argparse
import serial


# Taille du buffer pour l'envoi via le port série (permet l'arrêt du script via CTRL-C)
STREAM_BUFFER_SIZE = 1024


def chunks(l, n):
    """ Découpe le buffer l en morceau de n octets. """
    for i in range(0, len(l), n):
        yield l[i : i + n]


def resample(x):
    """ Converti l'échantillon audio x sur 8 bits en un signal PWM sur 3 bits (avec inversion). """
    if x >= 224: return 0b00000000
    if x >= 195: return 0b10000000
    if x >= 168: return 0b11000000
    if x >= 140: return 0b11100000
    if x >= 112: return 0b11110000
    if x >= 84:  return 0b11111000
    if x >= 56:  return 0b11111100
    if x >= 28:  return 0b11111110
    return 0b11111111


def stream_audio_file(filename, serial_port):
    """ Lit le fichier audio spécifié via le port série spécifié en utilisant des données formatées pour générer un signal PWM rudimentaire. """

    # Ouvre le fichier audio
    with wave.open(filename, 'rb') as fi:

        # Vérifie la compatibilité du format de fichier
        assert fi.getnchannels() == 1, "Only mono wave files are supported"
        assert fi.getsampwidth() == 1, "Only 8bits PCM wave files are supported"

        # Converti les données audio en données série PWM
        print("Converting audio file to PWM serial data ...")
        nb_frames = fi.getnframes()
        framerate = fi.getframerate()
        print('Source file "{}" is {} frames long, sampled at {} Hertz'.format(filename, nb_frames, framerate))
        data = bytes(map(resample, fi.readframes(nb_frames)))
    
    # Ouvre le port série et envoi les données
    serial_baudrate = framerate * 10  # 8N1: 1 Start, 8 Data, 1 Stop
    print('Setting serial port "{}" in mode 8N1, at {} bps ...'.format(serial_port, serial_baudrate))
    with serial.Serial(port=serial_port,
                       baudrate=serial_baudrate,
                       bytesize=serial.EIGHTBITS,
                       parity=serial.PARITY_NONE,
                       stopbits=serial.STOPBITS_ONE) as serial_com:
        print("Streaming data ... Use CTRL+C to stop.")
        for chunk in chunks(data, STREAM_BUFFER_SIZE):
            serial_com.write(chunk)
            serial_com.flush()


# Interface en ligne de commande
if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Stream an audio wave file over a serial port using PWM-like serial data.')
    parser.add_argument('filename', metavar='<WAVE FILE>', help='The input audio wave file (must be a mono 8 bits PCM file).')
    parser.add_argument('serialport', metavar='<SERIAL PORT>', help='The output serial port.')

    args = parser.parse_args()
    stream_audio_file(args.filename, args.serialport)

L'extrait de code ci-dessus est disponible en téléchargement sur cette page.

Le résultat

Particulièrement horrible à l'oreille pour être franc. Ça reste cependant assez impressionnant pour un signal audio avec une résolution de seulement 3 bits.

On reconnait facilement la musique et les paroles, mais le son contient beaucoup de bruit. J'ai tenté de faire un enregistrement audio pour illustrer ce chapitre, mais cela ne rendait pas bien du tout.

Si vous êtes curieux, je vous laisse faire le test par vous même. Le montage est simple et vous verrez, le résultat est "intéressant" ;)

Les plus courageux pourront tenter d'ajouter un filtre passe-bas et un amplificateur pour améliorer le signal. Pour ma part, je préfère garder le circuit simple.

Annexe : Comment générer un fichier .wav avec audacity

Voici la procédure pour générer un fichier audio .wav, mono canal, encodé en PCM 8bits, avec Audacity :

  • Étape 1 : Importez un fichier audio dans le logiciel (fichier mp3, flac, ce que vous voulez) en utilisant le menu "Fichier", puis "Importer", puis "Audio".

  • Étape 2 : Passez d'un fichier stéréo à mono (si besoin) en cliquant sur le menu "Pistes", puis "Piste stéréo vers mono".

  • Étape 3 : Modifiez la fréquence d’échantillonnage du fichier (si besoin) en modifiant la fréquence dans la zone de texte "Projet à : XXXXX" dans le bandeau en bas de la fenêtre. J'utilise une fréquence de 44100Hz par défaut.

  • Étape 4 : Exportez le projet en format .wav en cliquant sur le menu "Fichiers", puis "Exporter".

  • Étape 5 : Choisir "Autre type de fichier non compressé", puis cliquer sur le bouton "Options" et choisir les options : "Entête WAV (Microsoft)" et "Encodage Unsigned 8 bit PCM".

  • Étape 6 : Enregistrer et voilà le fichier est prêt !

Conclusion

Ce mini projet est désormais terminé.

Si ce mini projet vous a plu, n'hésitez pas à le commenter sur le forum, à le partager sur les réseaux sociaux et à soutenir le site si cela vous fait plaisir.