Sommaire
- Résumé de l'épisode précédent
- Les Rapports de bugs
- Utiliser systemd plutôt que des scripts shell
- Un peu de sécurité
- Les problèmes de latence
- Modifier le volume directement sur la batterie
- Jouons aussi de la musique
- Pourquoi pas à partir d'une clé USB ?
- Le temps de boot
- Conclusion
Cher lecteur,
Résumé de l'épisode précédent
En cette journée de deuil national suite à la bronsonisation de Jean-Philippe Smet, je me permets de refaire un journal sur la musique.
La semaine dernière, je t'ai expliqué dans ce journal que j'essayais de construire un boitier permettant de transformer une batterie du jeu vidéo Rock Band en batterie électronique pour enfant.
Bien que le système était opérationnel, il restait des axes d'amélioration. Je viens donc te présenter les nouveautés.
Les Rapports de bugs
Durant mon développement, je suis tombé sur deux bugs, sur les deux outils que j'utilisais. J'ai donc ouvert deux tickets dans les bug trackers correspondants :
- Hydrogen refuse d'émettre du son si les évènements midi arrivent trop tôt au lancement.
- L'id USB de la batterie Rock Band 1 pour Wii est faux dans rbdrum2midi.
Bien qu'ils n'aient pas soulevé les foules, ils sont présents pour les générations futures d'utilisateurs.
Utiliser systemd plutôt que des scripts shell
Dans la version précédente, lorsque je détectais le branchement de la batterie, au travers d'une rule udev, je lançais un script shell démarrant mes processus. Il y avait également une autre règle pour le débranchement de la batterie. Voila les règles en question :
ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="1bad", ATTR{idProduct}=="0005", RUN+="/opt/drumset/launchusb.sh"
ACTION=="remove", SUBSYSTEM=="usb", ENV{PRODUCT}=="1bad/5/1000", RUN+="/opt/drumset/removeusb.sh"
Cela me force à gérer les logs, la synchronisation entre les 2 processus… tout à la main dans mes scripts shell.
En lisant la documentation de udev et systemd, je me suis aperçu qu'il était possible de démarrer des services systemd directement à partir de udev. C'est à mon avis beaucoup plus propre.
Les services systemd
Commençons donc à écrire les services systemd. Dans mon cas, je vois bien deux services, un qui analyse la batterie pour émettre des messages midi (rbdrum2midi) et un autre (hydrogen) qui lit les messages midi et émet le son. Il y a une dépendance forte entre ces deux services. Le deuxième nécessite obligatoirement le premier et ne peut être lancer qu'après celui-ci. Si le premier est arrêté, il faut aussi que j'arrête le deuxième. D'après la documentation de systemd, on doit utiliser la dépendance BindTo
et After
.
Je crée donc mon premier service dans le fichier /etc/systemd/system/rb2midi.service
:
[Unit]
Description=Rockband USB to Midi Service
[Service]
Type=simple
ExecStartPre=/usr/bin/play /opt/drumset/start.wav
ExecStart=/usr/bin/rbdrum2midi -rb1
TimeoutStopSec=100
Rien de bien compliqué ici. C'est un service de type simple
qui tourne en fond de tache. Je souhaite qu'avant de lancer celui-ci, un son soit jouer sur le casque pour prévenir l'utilisateur qu'il peut commencer à jouer (ligne ExecStartPre
), et la commande à lancer est /usr/bin/rbdrum2midi -rb1
.
Le deuxième service se trouve dans le fichier /etc/systemd/system/hydrogen.service
[Unit]
Description=Hydrogen sound server
BindsTo=rb2midi.service
After=rb2midi.service
[Service]
Type=simple
ExecStart=/usr/bin/h2cli -d alsa -s /opt/drumset/default.h2song
TimeoutStopSec=100
C'est quasiment la même chose avec les dépendances supplémentaires. J'ai aussi rajouté une chanson en paramètre de h2cli pour pouvoir charger des paramètres des sons des instruments que j'ai un peu tuné pour être sûr que le son dans le casque ne puisse jamais être trop fort.
Pour accéder aux logs du service, il suffit d'exécuter journalctl -u hydrogen
ou journalctl -u rb2midi
.
L'intégration avec udev
D'après la doc de udev, c'est plutôt simple, il suffit de replacer mes deux règles précédentes par celle-ci :
ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="1bad", ATTR{idProduct}=="0005", TAG+="systemd", ENV{SYSTEMD_WANTS}="hydrogen.service"
Quand la batterie est branchée, un nouveau device est créé dans systemd avec une dépendance de type WANTS
vers mon service hydrogen
. Je teste, et cela fonctionne tout seul. La dépendance est bien gérée, et je vois qu'au branchement j'ai le jingle joué, puis rbdrum2midi qui est lancé et enfin hydrogen. Yes, tout est gagné ?
Non, pas tout à fait, quand je débranche la batterie, cela se passe moins bien. Vu qu'il y a une dépendance de type WANTS
entre le device et le service, si je débranche le device, le service n'est pas arrété. Arg !
Bon, je ne suis pas le premier à rencontrer ce problème, et je tombe sur une bonne explication avec deux solutions. La première semble simple, il faut rajouter StopWhenUnneeded=yes
sur mes deux services pour leur demander de s'arrêter quand on a plus besoin d'eux. Pas de chance, l'option n'a pas encore géré par la version de systemd livrée avec armbian. La deuxième est moins propre : il suffit de lancer et arrêter les services à la main avec systemctl
. Partons au final sur celle-ci :
ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="1bad", ATTR{idProduct}=="0005", RUN+="/bin/systemctl --no-block start hydrogen"
ACTION=="remove", SUBSYSTEM=="usb", ENV{PRODUCT}=="1bad/5/1000", RUN+="/bin/systemctl --no-block stop rb2midi"
Et cela fonctionne. Je peux débrancher et rebrancher la batterie, tout est OK. Yes !!
Un peu de sécurité
Même si le boitier n'est relié qu'au réseau que quand je développe, un peu de sécurité ne ferait pas de mal. Actuellement, mes deux services tournent en tant que root
, c'est un peu dommage. Je crée donc un user appelé drumset
qui appartient au group audio
pour pouvoir jouer du son. Il faut aussi que cet utilisateur puisse accéder à la batterie. Cela semble possible avec les règles udev.
Mes nouvelles règles sont donc :
ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="1bad", ATTR{idProduct}=="0005", GROUP="audio", MODE="0664", RUN+="/bin/systemctl --no-block start hydrogen"
ACTION=="remove", SUBSYSTEM=="usb", ENV{PRODUCT}=="1bad/5/1000", RUN+="/bin/systemctl --no-block stop rb2midi"
Et j'ai rajouté User=drumset
dans la définition de mes deux services. Et cela juste fonctionne. Comme quoi, systemd c'est bien pour ce genre de choses.
Les problèmes de latence
Lors de mon précédent journal, on m'a fait remarqué à juste titre que la vidéo montrait des problèmes de latence. Je ne m'en étais pas rendu compte sur le moment, mais c'était flagrant.
Mon premier réflexe a été d'utiliser jack. Mais j'ai vraiment galéré. Le serveur jackd refuse de se connecter directement au device physique au travers de alsa, la version de jackd livrée avec armbian n'est pas compatible avec hydrogen… Au final, j'ai renoncé.
Néanmoins, cela m'a appris des choses sur la latence audio :
- Celle-ci semble directement lié à la fréquence d'échantillonnage, le nombre de frames gérées à chaque période (et donc le temps entre chaque traitement), et la taille des buffers.
- Mettre des nombres trop bas peut être contre-productif, car trop couteux en cpu et il est possible que celui-ci n'ait pas le temps de générer les données à temps (cf. XRUN).
- Il est possible de configurer alsa pour avoir une bonne latence.
- La configuration audio par défaut pour armbian sur un orangepi zero est catastrophique.
J'ai donc décidé de mettre les mains dans la config audio d’alsa. En plus d'essayer de diminuer la latence, j'ai rajouté un mixer pour que deux processus puissent accéder au son en même temps (oui, il n'y en avait pas par défaut).
J'ai donc créé un fichier /etc/asound.conf
avec ce contenu :
pcm.dmix0 {
type dmix
ipc_key 673138
ipc_key_add_uid false # let multiple users share
ipc_perm 0666 # IPC permissions for multi-user sharing (octal, default 0600)
slave {
pcm "hw:0,0"
rate 44100
period_time 0
period_size 256
buffer_size 512
}
bindings {
0 0
1 1
}
}
pcm.!default {
type plug
slave.pcm "dmix0"
}
ctl.!default {
type hw
card 0
}
D'après ce site, cela doit correspondre à une latence d'environ 12 ms, mais les captures audio me montrent un chiffre plus proche de 30 ms, mais j'ai du mal à vraiment l'évaluer et encore moins de ressentir la différence. Mais dans tous les cas, c'est mieux qu'avant et acceptable pour des enfants qui jouent.
J'ai quelques xrun, mais cela reste rare, et cela ne s'entend pas trop. Il a fallu aussi modifier la configuration de hydrogen pour utiliser la sortie alsa dmix0
plutôt que hw:0,0
.
Modifier le volume directement sur la batterie
C'est une demande de remontant du field et donc très importante. Quand les enfants jouent avec un casque, ils ne peuvent pas changer le volume. Or la batterie possède deux boutons "+" et "-". Il a donc fallu comprendre comment ces boutons fonctionnaient. C'est très simple, rbdrum2midi possède un mode verbeux qui afficher tous les bytes reçus par le device usb. j'ai pu voir que ces boutons correspondaient aux bits 1 et 2 du deuxième byte reçu. J'ai donc modifié rbdrum2midi :
- Pour rajouter deux fonctions volumeup et volumedown dans le driver alsa.
- Coder ces deux fonctions grâce à l'API C alsa et aux fonctions
snd_mixer_selem_get_playback_volume
etsnd_mixer_selem_set_playback_volume
. - Appeler ces fonctions lorsque je détecte l'appui des touches. Le code de rbdrum2midi était suffisamment bien faite pour que ce soit très simple.
J'ai peur par contre que ce contexte d'utilisation soit très spécifique et qu'il n'y ait pas d'utilité à proposer un patch au projet. Surtout que je ne sais pas comment fonctionnent les autres batteries USB. C'est dommage.
Jouons aussi de la musique
Mon autre utilisatrice (ma femme) m'a aussi fait part d'une idée d'évolution. cela pourrait être bien, lorsque l'on veut s'entrainer, de pouvoir jouer en même temps une chanson en fond sonore. Et pouvoir contrôler celle-ci directement à partir de la croix directionnelle disponible sur la batterie.
Cela a fait tout de suite tilt dans ma tête. Il y a un outil parfaitement adapté pour cela : Music Player Daemon ou mpd. Pour ceux qui ne connaissent pas, c'est un serveur de son contrôlable de beaucoup de manières différentes. Il est très souvent utiliser dans des projets embarqués. Cela tombe bien, il est disponible de base dans armbian. Je décide de retoucher un peu la configuration par défaut pour :
- Lui dire d'aller chercher les chansons dans
/media
- Interdire le contrôle à distance pour des raisons de sécurité.
- D'utiliser le device alsa
dmix0
- De ne pas utiliser le contrôle du volume d'alsa, mais un mixer software. Le but ici, est de permettre d'avoir un son mpd plutôt faible par rapport au son de la batterie (70% du volume me semble pas mal après tests). Et le volume global de la batterie et de la musique se fait toujours sur la batterie avec le mixer alsa.
Cela se fait très facilement dans le fichier /etc/mpd.conf
:
audio_output {
type "alsa"
name "Alsa dmix"
device "dmix0"
mixer_type "software"
}
L'installation du package a créé un service systemd qui est lancé par défaut. Rien à retoucher ici.
Il faut donc maintenant contrôler mpd au travers de la croix directionnelle. Les conventions que j'ai choisies sont les suivantes :
- Touche haut : analyse du répertoire, création d'une playlist contenant toutes les chansons, démarrage de la première chanson.
- Touche droite : chanson suivante.
- Touche gauche : chanson précédente.
- Touche bas : pause/retirer la pause.
Le plus simple ici pour contrôler mpd était de modifier rbdrum2midi pour capturer les appuis de la croix directionnelle et puis de lancer la commande mpc
avec les bons paramètres pour faire l'action associé. Cela n'est surement pas le plus performant de lancer une commande à partir du code c, mais quand on change les chansons la latence des drums n'est surement pas importante.
Au final, cela donne les commandes suivantes :
- Touche haut :
mpc clear; mpc update; mpc ls|mpc add; mpc repeat on; mpc play
- Touche droite :
mpc next
- Touche gauche :
mpc prev
- Touche bas :
mpc play
oumpc pause
J'ai donc créé un répertoire /media/songs
qui contient quelques mp3 et ogg. Et cela fonctionne super. J'en profite pour modifier le service rb2midi pour rajouter une règle demandant d'arrêter la lecture dans mpd quand on arrête le service. C'est très simple :
ExecStop=/usr/bin/mpc stop
Pourquoi pas à partir d'une clé USB ?
Le fait de devoir déposer les chansons sur la carte sd était un peu gênant. Sachant qu'il me reste deux ports USB inutilisés, pourquoi ne pas s'en servir ? Idéalement, il faudrait que lorsque l'utilisateur insère sa clé, elle soit montée automatiquement et soit accessible à mpd. Idem lorsqu'il la retire.
Après quelques recherches, je m'aperçois que l'outil usbmount fait exactement cela. Quand tu insères ta clé, elle est montée dans /media/usbX (X entre 0 et 7) et est donc dans la bibliothèque de mpd. J'édite néanmoins la configuration pour rajouter le support du ntfs, et modifier les droits des répertoires pour que l'utilisateur mpd
puisse y avoir accès.
FILESYSTEMS="vfat ext2 ext3 ext4 hfsplus ntfs"
MOUNTOPTIONS="sync,noexec,nodev,noatime,nodiratime,uid=999,gid=29,umask=0022"
Et encore tout fonctionne niquel. la seule chose est que quand tu insères une nouvelle clé, il faut appuyer sur la touche du haut pour forcer un rescan des répertoires. Sachant que cela n'arrive souvent qu'une fois dans ta session, ce n'est pas très dérangeant.
Le temps de boot
Lors de la première version, je me plaignais des temps de boot. Il fallait compter 40 secondes avant de pouvoir utiliser la batterie. J'ai donc décidé de passer du temps dessus pour gagner quelques secondes.
Au final, trois commandes systemd sont très utiles : systemd-analyze
, systemd-analyze blame
et systemd-analyze critical-chain
. La première donne le temps de boot, la deuxième les services les plus lents à démarrer, et la troisième donne la chaine de démarrage des services la plus lente.
En commençant, je me suis aperçu que les services NetworkManager et NetworkManager-wait-online étaient les plus lents. Pour le premier, j'ai décidé de désactiver le wifi (en retirant le chargement du module) et en passant en ip fixe plutôt qu'en dhcp. Le but du boitier n'est d'être relié qu'au réseau quand je troubleshoote, donc dans un environnement maitrisé. Pour le deuxième, sachant que la connexion réseau est optionnelle, j'ai décidé de le désactiver : systemctl disable NetworkManager-wait-online.service
. J'ai également désactivé le service resolvconf pour la même raison.
Ensuite, deux services ont attiré mon attention : loadcpufreq
et cpufrequtils
. Ils permettent d'adapter la fréquence des cœurs en fonction de la charge cpu. Ils sont chainés et prennent à eux deux 5 secondes. Néanmoins, les désactiver me faisait augmenter trop la température des cpu pour un environnement sans ventilateur ni ailettes de refroidissement. On va donc les réactiver. J'ai fait aussi un peu de ménage dans les paquets installés.
Au final, j'arrive à cela :
root@orangepizero:~/rbdrum2midi_test/src# systemd-analyze
Startup finished in 4.557s (kernel) + 17.743s (userspace) = 22.300s
root@orangepizero:~/rbdrum2midi_test/src# systemd-analyze critical-chain
The time after the unit is active or started is printed after the "@" character.
The time the unit takes to start is printed after the "+" character.
graphical.target @17.282s
multi-user.target @17.264s
sysfsutils.service @16.972s +269ms
cpufrequtils.service @14.892s +1.959s
loadcpufreq.service @10.313s +3.279s
basic.target @10.182s
sockets.target @10.162s
dbus.socket @10.143s
sysinit.target @10.103s
systemd-update-utmp.service @9.833s +248ms
systemd-tmpfiles-setup.service @9.443s +298ms
systemd-journal-flush.service @8.593s +718ms
systemd-journald.service @8.263s +213ms
log2ram.service @6.903s +1.228s
var-log.mount @7.104s
local-fs-pre.target @6.142s
systemd-tmpfiles-setup-dev.service @4.463s +1.199s
kmod-static-nodes.service @2.053s +1.489s
systemd-journald.socket @1.602s
-.mount @1.113s
system.slice @1.263s
-.slice @1.113s
La batterie est utilisable au bout de 25 secondes environ. C'est pas super, mais c'est le mieux que j'ai pu. J'ai aussi l'impression que le passage de mes deux processus en services systemd semble aussi avoir amélioré les choses.
Si vous avez des idées pour encore aller plus loin, n'hésitez pas à m'en faire part. J'avais essayé par exemple de booter à partir d'une image suspend-to-disk, mais cette option ne semble pas activée dans le noyau que j'utilise.
Conclusion
Je pense que je suis arrivé au bout de ce projet. La seule petite chose qui me reste à faire je pense, est de générer une image disque "light" (inférieure à 2Go) de la carte sd pour la rendre disponible à tous. Les remontés de bug ont été faits, et les modifications de rbdrum2midi sont tellement liés à ce projet que je ne pense pas qu'ils intéresseront quelqu'un.
De plus, cela m'a permis de découvrir certains domaines que je ne connaissais pas du tout : udev, les dépendances de systemd, les temps de boot, la latence audio… C'est que du bénéfice.
Ma famille a adhéré de suite à cette batterie et au final, ils l'utilisent plus que la vraie batterie à côté. J'ai aussi récupéré quelques mp3 de chansons simples à faire la batterie, sans la batterie : cela permet d'apprendre facilement. J'ai aussi rajouté un adaptateur pour pouvoir brancher la boîte directement sur l'ampli de la guitare électrique afin de pouvoir faire du bruit si l'on a envie.
Si vous avez un avis sur le projet, des questions ou des idées d'évolution, n'hésitez pas à m'en faire part dans les commentaires. Je cherche aussi un endroit où publier l'image, qu'est ce qui a de mieux ? github ? Comment cela se passe pour citer les third parties ? Je ne pense pas que l'on peut mettre une licence sur de la configuration de projet tiers…
# Mini-optimisation
Posté par Kerro . Évalué à 3.
Pour cpufrequtils et loadcpufreq tu peux gagner un chouillème en ne les lançant qu'une fois le système démarré.
https://wiki.archlinux.org/index.php/Systemd/Timers
[^] # Re: Mini-optimisation
Posté par TilK . Évalué à 2.
En effet, c'est une piste intéressante. Je testerais demain.
[^] # Re: Mini-optimisation
Posté par Anonyme . Évalué à 2.
Quelques pistes supplémentaires pour la vitesse de démarrage :
De ce que j'ai lu sur leur forum, les gars de armbian tentent de limiter la consommation maximale pour éviter dans les moments de pics que l'alimentation soit sous-dimensionnée et que la baisse de tension fasse planter le SOC. Donc si tu sais que ton alim est suffisamment grosse et fiable par rapport à ce que tu y branches, tu peux passer outre ces réglages conservateurs.
Et avec le Allwinner H3 ils ont aussi dû créer un système de limitation de température (thermal throttling) car il n'y a pas de sécurité interne au SOC par rapport au risque de cramer le silicium. Donc si c'est bien à cause de ça, tu peux ajouter un radiateur. Apparemment une simple bande de cuivre autocollant en guise de dissipateur fait l'affaire mais attention aux courts-circuits.
Enfin, il y a toujours la qualité de la carte SD qui peut ralentir la mise en route. Étonnamment, au delà de la classe 6 il n'y a plus de débits garantis sur les petits fichiers donc ça peut être très mauvais selon le modèle.
# Commentaire supprimé
Posté par Anonyme . Évalué à 6.
Ce commentaire a été supprimé par l’équipe de modération.
[^] # Re: Overkill?
Posté par TilK . Évalué à 3.
Salut,
J'ai parfaitement conscience que mon utilisation d'hydrogen est ridicule par rapport à ses capacités. Par contre, cette solution possède quand même des avantages :
# systemd sans règle udev
Posté par Glandos . Évalué à 1.
Personnellement, je me suis fait un interrupteur de wifi, avec une clé USB branchée sur mon point d'accès. Il y a deux services,
hostapd.service
etrfkill-by-card.service
. Le premier gère le point d'accès en lui-même, le deuxième me permet de couper les ondes, vraiment.Et le deuxième:
Et comme ça, j'ai pas de règle udev. Le nom de l'unité device peut être connu avec
systemctl -t device
lorsque le périphérique est branché.[^] # Re: systemd sans règle udev
Posté par TilK . Évalué à 3.
En fait, le device n'apparait pas dans la liste de
systemctl
:Il n'y est pas non plus dans la liste si je rajoute un
--all
. De ce que j'ai compris, pour certains équipement, ils n'apparaissent que s'il y a une rule udev rajoutantTAG+="systemd"
. Dans mon cas, de plus, le chemin du device dans l'arbre de udev dépendant du port usb dans lequel je l'avais branché. Il fallait donc prévoir tous les cas.Suivre le flux des commentaires
Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.