Laboratoire 5

Systèmes embarqués temps réel (GIF-3004)

Objectifs

Ce laboratoire vise les objectifs suivants :

  1. Gérer la communication et la synchronisation temps réel entre deux systèmes embarqués distincts;
  2. Se familiariser avec la gestion du son sous Linux en temps réel;
  3. Utiliser les pipes nommés correctement pour communiquer entre les processus;
  4. Observer le compromis entre latence et débit;
  5. S'adapter aux limitations et variations intrinsèques d'un lien sans fil (Bluetooth);
  6. Mettre en commun les connaissances vues au fil du cours pour créer un système temps réel complet.

Préparation et outils nécessaires

Installation des librairies logicielles nécessaires

Dans ce laboratoire, vous aurez besoin des librairies supplémentaires suivantes :

  • libvorbis (pour l'encodage OGG)
  • bluez (pour la communication Bluetooth)

Ces librairies peuvent être installées en utilisant la commande suivante :

sudo apt-get update && sudo apt-get install bluez libvorbisenc2 libvorbis-dev

Attention : apt-get update est utilisé ici pour rafraîchir la liste des paquets. Vous ne devez pas effectuer un apt-get upgrade, au risque de remplacer votre noyau par un noyau non temps réel.

Également, n'oubliez pas de transférer les fichiers d'en-tête et les librairies de libvorbis dans le sysroot de votre station de travail. Les librairies sont situées sur le Raspberry Pi dans /usr/lib/arm-linux-gnueabihf/ et se nomment libvorbis et libvorbisenc (attention de ne pas copier seulement des liens symboliques!); les fichiers d'en-tête sont situés dans /usr/include/vorbis. Finalement, ajustez les propriétés du projet sous Eclipse pour qu'il lie les fichiers produits à libvorbis et libvorbisenc (comme pour curl dans le laboratoire 2, par exemple). Vous n'avez pas besoin de transférer quoi que ce soit lié à bluez puisque nous n'utiliserons pas directement cette librairie dans les fichiers C.

Liaison Bluetooth

Pour ce laboratoire, vous devrez donc utiliser deux Raspberry Pi, qui communiqueront entre eux par Bluetooth. L'un d'entre eux sera le serveur (celui qui est branché à la source audio et qui envoie le son) et l'autre sera le client (celui qui reçoit le son et auquel on branche les haut-parleurs ou écouteurs). La procédure de configuration, pour chacun des Raspberry Pi, est la suivante :

Les étapes suivantes sont à exécuter sur les deux Raspberry Pi :

  1. Lancez le daemon de gestion Bluetooth avec la commande sudo systemctl start bluetooth; notez que vous pouvez lancer automatiquement le daemon à chaque démarrage, en exécutant sudo systemctl enable bluetooth.
  2. Assurez-vous que le périphérique Bluetooth est prêt à recevoir/émettre : sudo hciconfig hci0 up

Les étapes suivantes sont à exécuter sur le Raspberry Pi SERVEUR :

  1. Exécutez la commande sudo hciconfig hci0 piscan qui rendra visible le périphérique Bluetooth aux autres Raspberry Pi. Ce ne serait pas forcément une erreur de le faire sur les clients, mais cela ne sert à rien et ajouterait beaucoup au « bruit » sur les canaux Bluetooth, rendant la procédure de connexion malaisée lorsque beaucoup d'équipes sont présentes dans le même local.
  2. Exécutez la commande sudo hciconfig hci0 name 'MonNom', en remplaçant MonNom par le nom que vous voulez donner à votre serveur. Synchronisez-vous avec les autres équipes (vous n'avez heureusement pas besoin de mutex pour cela) afin d'assurer que votre nom soit unique, puisqu'il s'agira de l'unique élément permettant d'identifier votre Raspberry Pi et de le différencier de ceux des autres équipes. Notez que si vous réexécutez la commande piscan, vous devrez également réexécuter celle-ci.
  3. Exécutez la commande sudo rfcomm -r listen [CHEMIN] 1 &. Remplacez [CHEMIN] par le chemin d'accès au pseudo fichier qui vous permettra de lire et écrire sur la connexion Bluetooth. Traditionnellement, les fichiers se nomment /dev/rfcommXX est un nombre entre 0 et 9. N'oubliez pas le dernier caractère & : il permet de lancer la commande en arrière-plan.

Les étapes suivantes sont à exécuter sur le Raspberry Pi CLIENT, après avoir exécuté les étapes demandées sur le serveur :

  1. Exécutez la commande hcitool scan. Cela devrait vous donner une sortie de ce genre :

    pi@raspberrypi:~ $ hcitool scan
    Scanning ...
        B8:8D:12:19:EB:F1       nollaum
        94:DB:C9:B2:88:76       Kalia
        B8:27:EB:1B:9F:6B       MonRaspberryPiAvecUnNomUniqueCestImportant
    Repérez la ligne contenant le nom unique que vous avez choisi plus tôt. Si celle-ci n'apparaît pas, attendez quelques secondes et exécutez un scan de nouveau; tout comme les réseaux Wi-Fi, le Bluetooth peut parfois observer certains problèmes transitoires. Notez l'adresse qui est liée à ce nom. Une fois que vous avez récupéré l'adresse du serveur, vous n'aurez plus à refaire hcitool scan puisque cette adresse est unique et ne changera jamais.
  2. Exécutez la commande sudo rfcomm -r connect [CHEMIN] [ADRESSE] 1 & en remplaçant [ADRESSE] par l'adresse récupérée à l'étape précédente et [CHEMIN] par le chemin du pseudo fichier qui sera créé. Encore une fois, n'oubliez pas le dernier caractère &.

Une fois ces étapes effectuées, chacun de vos Raspberry Pi devrait avoir accès à un fichier dans lequel il peut lire ou écrire. Écrire dans ce fichier transmet les données sur l'autre Raspberry Pi lorsque ce dernier fait une lecture : autrement dit, si vous écrivez sans arrêt sans lire de l'autre côté, vous remplirez la RAM du serveur avec des données en attente de transmission!

Attention : par défaut, la commande piscan ne rend le Raspberry Pi visible que pendant 180 secondes. C'est dire que 3 minutes après avoir exécuté la commande le rendant visible, il redeviendra invisible. Si vous trouvez ce délai trop court, vous pouvez modifier la variable DiscoverableTimeout dans /etc/bluetooth/main.conf.

Après un redémarrage, seules les dernières étapes (celles invoquant rfcomm) sont à refaire, si vous vous rappelez de l'adresse du Raspberry Pi serveur.

Énoncé

Quelques exemples de code sont disponibles sur le dépôt Git suivant : https://bitbucket.org/GIF3004/laboratoire5. Notez que contrairement aux laboratoires précédents, nous ne fournissons pas d'ébauche de code. C'est à vous de décider de l'architecture de votre système et de le programmer en conséquence.

Dans ce laboratoire, vous devrez implémenter un système de transmission sonore fonctionnant en temps réel, capable de retransmettre le flux sonore d'une guitare électrique à un haut-parleur, sans utiliser de fils et en ajoutant éventuellement des effets sonores (filtrage, distorsion, etc.). De manière générale, le concept global ressemble beaucoup à celui du laboratoire 3. Dans les deux cas, une source est lue par un processus et transmise dans une chaîne de processus la modifiant, avant d'arriver à la dernière étape où elle est rendue visible/audible. Les différences les plus importantes sont les suivantes :

  1. Vous devez transmettre le flux sonore sans fil entre les deux Raspberry Pi, en utilisant une interface Bluetooth.
  2. Contrairement au laboratoire 3, il ne peut y avoir ici qu'une seule source et une seule sortie (il n'y a donc pas de multiplexage).
  3. La vidéo utilisée dans le laboratoire 3 contient des éléments atomiques : les trames du vidéo. Dans le cas du son, ce genre de division « naturelle » n'existe pas vraiment, et vous devrez vous-mêmes déterminer un compromis sur la taille des blocs à utiliser.
  4. Contrairement à la lecture d'un vidéo, où un délai d'affichage un peu trop long entre deux trames ne se remarque pas tellement s'il est isolé, un « blanc » dans le flux sonore s'entend immédiatement. Au delà de la latence, vous devez donc à tout prix éviter les coupures.

La figure suivante montre une vue globale de ce que vous devrez réaliser. Bien entendu, le nombre de processus peut varier en fonction des requêtes de l'utilisateur.

Acquisition du son à partir de Alsa

Afin de lire le son à partir de l'entrée micro, nous allons utiliser la librairie Alsa, qui constitue le standard de facto de la gestion audio sous Linux. Le fichier alsa_acquisition.c, disponible dans le dépôt de code, fournit un exemple d'utilisation d'Alsa dans ce contexte. Notez que si vous n'avez pas une carte d'acquisition sous la main, vous pouvez simuler une entrée microphone ou ligne en utilisant le module aloop de Alsa. Ce module permet de connecter une source sonore (par exemple un fichier WAV que vous aurez préalablement téléchargé sur le Raspberry Pi) comme s'il s'agissait d'un périphérique réel. Pour l'utiliser, chargez d'abord le module correspondant : sudo modprobe snd-aloop. Par la suite, vous pourrez observer qu'un nouveau périphérique est apparu dans la liste fournie par aplay -l. Repérez les identifiants device et subdevice de ce pseudo-périphérique. Lancez par la suite le programme aplay sur ce périphérique :

aplay -D hw:DEV,SUBDEV monfichier.wav

avec, bien évidemment, DEV et SUBDEV remplacés par le numéro de votre périphérique. Une fois cela fait, vous pouvez lancer votre programme d'acquisition, en lui fournissant le même périphérique de loopback. De cette manière, vous pouvez tester votre code en toutes circonstances, bien que les tests faits dans ce cas ne tiennent pas compte de la latence induite par la carte son USB.

Nous vous fournissons deux fichiers WAV (le premier et le second) d'une trentaine de secondes chacun d'une guitare électrique "clair" (sans effets appliqués). Notez que ces fichiers sont stéréo, alors que la carte d'acquisiton ne possède qu'un canal d'enregistrement (mono). Vous pouvez sélectionner uniquement un canal de ces fichiers (le gauche) ou alors moyenner les deux canaux. Ces extraits proviennent de karoryfesamples et sont disponibles sous licence d'utilisation non commerciale. Vous pouvez évidemment utiliser vos propres fichiers.

Traitement du son et pipeline de communication

Une fois l'audio acquit, vous devez pouvoir lui appliquer divers effets avant de l'envoyer au Raspberry Pi de lecture. Les effets que vous devez implémenter énumérés ici. Nous fournissons également un exemple de chaque effet une fois appliqué sur le fichier suivant.

Tous ces filtres travaillent sur des échantillons (samples). Un échantillon est un point de mesure sur le signal sonore. Une fréquence d'échantillonnage de 44.1 kHz est par exemple usuelle (tout comme 48 kHz), ce qui correspond donc à 44 100 échantillons par seconde. Dans les figures suivantes, x[i] et y[i] correspondent respectivement à la valeur de l'échantillon i du signal d'entrée et de sortie.

Important : l'implémentation proposée de chaque effet utilise des opérations mathématiques simples et leur mise en place devrait être simple. Soyez toutefois très prudent en ce qui concerne le débordement : par défaut, les échantillons sonores sont représentés sous la forme d'entiers de 2 octets (type short). Il est très facile de dépasser les valeurs limites de ce type et les résultats seront alors incorrects.

Filtrage passe-bas

Un filtrage passe-bas consiste à retirer les fréquences du signal dépassant un certain seuil. Il existe beaucoup de façons de réaliser un tel filtre, mais une des plus simples, dans le cas qui nous occupe, est de combiner l'échantillon courant du signal d'entrée avec l'échantillon précédent du signal de sortie. Cela atténue l'effet des variations rapides, produisant ainsi un effet qui retire les hautes fréquences du signal. Schématiquement, ce traitement peut être visualisé comme suit :

Le facteur alpha permet de choisir la fréquence de coupure du filtre. Une explication détaillée du lien entre la fréquence de coupure et cette constante est disponible sur cette page (en anglais). Lorsque filtré par un filtre passe-bas, le fichier sonore présenté plus haut peut par exemple ressembler à ceci.

Filtrage passe-haut

Le filtrage passe-haut est l'inverse du filtre passe-bas. Comme pour ce dernier, il existe beaucoup de manières de réaliser un filtre passe-haut, mais nous utiliserons encore une des plus simples, présentée schématiquement dans la figure suivante :

où le facteur alpha contrôle ici aussi la fréquence de coupure du filtre. Une explication mathématique détaillée de l'approche est disponible sur cette page (en anglais). Lorsque passé dans un filtre passe-haut, le fichier sonore original devrait ressembler à ceci.

Délai (réverbération)

Le délai (écho) est similaire au filtre passe-bas, mais utilise les échantillons de sortie nettement en arrière par rapport à l'échantillon actuel. Cela occasionne un effet d'écho, dont le fonctionnement est schématisé dans la figure suivante :

Dans ce cas-ci, deux paramètres peuvent être choisis, soit le facteur alpha, qui contrôle la force de l'effet, et le délai N, qui détermine la période de l'écho. Un exemple d'ajout de délai peut être écouté dans ce fichier.

Distorsion / Amplification

La distorsion peut être modélisée par une simple multiplication par un paramètre alpha fixe suivie d'un seuillage (clip). Le fait de couper les pics des sons forts produit un grand nombre d'harmoniques qui sont perçues comme de la distorsion. On notera incidemment que c'est exactement le même mécanisme qu'un changement de volume, à ceci près que le changement de volume ne devrait pas saturer (et donc le seuillage ne devrait presque pas être nécessaire).

Une amélioration permettant de produire un effet de distorsion un peu plus réaliste est de compresser la plage dynamique en passant le signal dans une fonction tangente hyperbolique (tanh) après la multiplication. Cela réduit le "grésillement" et améliore l'effet. Une fois la distorsion appliquée, le fichier original pourrait devenir le suivant : notez que cet enregistrement n'est pas très adapté à l'ajout de distorsion, puisque la plupart des notes sont très courtes. Un bon exemple de l'effet recherché (avec des notes tenues plus longtemps) est donné par la dernière note de la pièce.

Aspects généraux et communication

Vous devez, pour chacun de ces effets, offrir un moyen de les paramétrer sur la ligne de commande. La plupart des effets n'ont qu'un seul paramètre, alpha, mais la réverbération possède également un second paramètre. Notez que bien que si les types d'effets énumérés plus haut sont obligatoires, l'implémentation reste à votre discrétion : vous n'avez pas forcément à suivre les suggestions présentées ici quant à la manière d'implémenter chaque effet. Par ailleurs, ces effets peuvent modifier le volume du son, ce qui n'est pas un problème, il suffit de compenser par une amplification subséquente.

Tout comme dans le laboratoire 3, chaque processus doit pouvoir faire partie d'une chaîne de programmes. Toutefois, à la différence du laboratoire 3, vous n'utilisez pas les espaces mémoire partagés, mais l'autre structure de communication usuelle dans les systèmes temps réel, les pipes nommés. Ces structures sont très proches des pipes que vous avez utilisés dans le laboratoire 2, mais sont liées à un pseudo-fichier dans le système, ce qui leur permet d'exister indépendamment des processus qui les créent. Par ailleurs, leur implémentation est optimisée dans linux-rt afin d'éviter tout délai inutile. Leur création et utilisation est relativement similaire à celle d'un fichier, si ce n'est qu'il faut utiliser la fonction spéciale mkfifo() avant d'ouvrir le fichier à proprement parler. Notez que contrairement aux zones mémoire partagées, vous n'avez pas besoin de mécanisme de synchronisation avec les pipes nommés.

Transmission via Bluetooth

Si vous avez bien suivi la section Préparation, vous devriez être en possession de deux Raspberry Pi pouvant communiquer entre eux en utilisant un lien série via Bluetooth. Ce lien s'utilise très simplement, en lisant ou écrivant le fichier créé par rfcomm. Cependant, si vous essayez d'envoyer directement l'audio, vous constaterez deux problèmes gênants :

  1. Le débit du lien série est beaucoup trop faible. Obtenir au-delà de 30 ko/s est déjà un tour de force et nécessite une distance très faible entre les deux Raspberry Pi. Dans ces conditions, il est impensable d'envoyer un signal sonore non modifié, qui nécessite au moins 100 ko/s de bande passante...
  2. Les latences de réception des paquets sont elles aussi très variables. Cela est dû à la structure du Bluetooth lui-même, qui attend qu'un paquet soit demandé avant de l'envoyer effectivement. Aussi, si bien souvent la latence est compatible avec un traitement sonore, des pauses de plusieurs centaines de millisecondes peuvent se produire relativement régulièrement, ce qui est un problème pour l'audio.

En ce qui concerne le premier problème, nous devrons encoder l'audio dans un format compressé afin de limiter sa bande passante à une valeur compatible avec la vitesse du lien Bluetooth. Nous utiliserons OGG Vorbis, qui est un format audio reconnu et dont l'implémentation est libre, mais d'autres formats auraient également pu être utilisés. Si cela règle le problème de la vitesse insuffisante du Bluetooth, cette solution ajoute néanmoins une couche de complexité puisque tout audio sortant du Raspberry Pi d'acquisition devra être encodée, alors que tout signal reçu par le Raspberry Pi de lecture devra être décodé à la volée. Le fichier vorbis_encodeur.c, disponible sur le dépôt, présente un exemple d'utilisateur de l'encodeur.

Pour ce qui est du second problème, nous allons utiliser une stratégie s'appuyant sur le fait qu'en moyenne, la latence est correcte : ce sont les pics de latence qui doivent être éliminés. Nous allons utiliser un tampon pour limiter ces pics de latence. Du côté serveur, rien à changer : ce dernier envoie encore aussi rapidement que possible les paquets contenant l'audio compressé. Cependant, du côté client, un thread supplémentaire devra être créé : ce thread ne fera que lire les données le plus vite possible et les stocker dans un tampon. En moyenne, ce tampon sera rempli plus vite qu'il ne sera vidé, puisque la vitesse moyenne du Bluetooth est supérieure à la vitesse à laquelle les données sont lues. Lorsqu'une longue pause se produit, ce tampon sera graduellement vidé, évitant ainsi au processus envoyant le son sur les haut-parleurs de manquer de données et de produire de disgracieux craquements. Le thread de lecture et le processus principal gérant le décodage et l'envoi aux haut-parleurs devront bien évidemment être synchronisés.

Décodage et lecture du son en utilisant Alsa

Pour le décodage, nous vous conseillons d'utiliser le fichier stb_vorbis.c, qui est un décodeur Vorbis tenant dans un seul fichier C. Son utilisation est beaucoup plus simple que la librairie de décodage officielle. Un exemple de son utilisation est fourni dans le fichier vorbis_decodeur.c. Notez que vous êtes libre d'utiliser une autre librairie si vous préférez.

Une fois l'audio décodé, il faut le passer à Alsa pour qu'il le traite et l'envoie sur les hauts-parleurs ou écouteurs connectés au Raspberry Pi. Voyez le fichier alsa_lecture.c, disponible dans le dépôt Git, qui donne un exemple de comment passer l'audio à Alsa. Notez que du côté client, vous n'avez pas à supporter l'ajout d'effets sonores, ceux-ci pouvant être ajoutés par le serveur avant l'envoi au client : l'idée derrière cette architecture est également de pouvoir facilement multiplier le nombre de clients avec le minimum de changements possibles. Bien que vous n'ayez pas à supporter l'envoi à des clients multiples, un tel design rend l'ajout de cette fonction aisé.

Gestion du débit et compromis débit/latence

Nous mettons à votre disposition au PLT-0105 un ordinateur qui vous permet de tester la latence totale de votre système. Vous n'avez qu'à brancher la sortie audio de l'ordinateur dans votre entrée ligne et la sortie de votre second Raspberry Pi dans l'entrée micro de l'ordinateur, sans ajouter d'effets dans votre chaîne de traitement (autrement dit, le programme d'acquisition doit être directement lié au programme encodeur). Cet ordinateur envoie sans arrêt un signal spécial lui permettant de calculer le délai entre son entrée et sa sortie et l'affiche à l'écran. Si le programme affiche Aucun signal détecté, c'est qu'il ne réussit pas à retrouver le signal qu'il a envoyé dans ce que vous lui retournez : vérifiez le bon fonctionnement de votre système.

Comme il a été mentionné au début de l'énoncé, un flux audio ne possède pas de déliminations internes claires. Vous pouvez « couper » ce flux en morceaux de 64 octets ou de 64 Ko, sans que rien ne change conceptuellement. Toutefois, pour la communication, ce choix fait toute la différence : utiliser une petite taille réduit la latence (les données n'ont pas à « attendre » après celles qui suivent avant d'être envoyées), mais réduit aussi le débit, puisque les paquets Bluetooth ne sont alors pas utilisés à leur plein potentiel. Inversement, utiliser une grande taille optimise le débit, puisque la taille des paquets Bluetooth devient négligeable face à la taille envoyée, mais augmente la latence, car le premier octet ne peut être envoyé avant que tous les autres ne soient eux aussi disponibles.

Il est donc crucial de choisir un bon compromis entre débit et latence. Vous devrez pour cela expérimenter plusieurs configurations et déterminer celle donnant les meilleurs résultats. Notez que vous aurez également, lorsque vous modifiez le débit de transmission, à changer de manière cohérente le débit que vous demandez à l'encodeur OGG Vorbis.

Modalités d'évaluation

Ce travail doit être réalisé en équipe de deux, la charge de travail étant à répartir équitablement entre les deux membres de l'équipe. Aucun rapport n'est à remettre, mais les deux équipiers doivent être en mesure d'expliquer leur approche et de démontrer le bon fonctionnement de votre pilote. Cette évaluation sera faite lors de la séance de laboratoire du 28 avril 2017, et les deux équipiers doivent y être présents. Ce travail compte pour 15% de la note totale du cours.

Le barême d'évaluation détaillé sera le suivant (laboratoire noté sur 20 points) :

  • (8 pts) Fonctionnement général du système, avec un lien sans fil Bluetooth et transmission du son sans craquements;
  • (3 pts) Atteinte d'un compromis débit/latence raisonnable, cohérent avec le débit demandé à l'encodeur Vorbis et justifiable par les étudiants;
  • (2 pts) Utilisation correcte de pipes nommés pour communiquer entre les différents modules;
  • (2 pts) Effets sonores (distorsion, réverbération et filtrage) fonctionnels;
  • (3 pts) Architecture globale du système conforme à un système temps réel;
  • (2 pts) Explications et réponses aux questions par les étudiants sur l'approche utilisée et leur code.

Ressources et lectures connexes