Fork me on GitHub

Alexis BRENON

Projet: Deezer Record

Article publié le 7 février 2016, c'était un dimanche.

La joie du programmeur, c'est de trouver des solutions aux problèmes qu'il rencontre. En voilà un exemple typique. Pour ma part, j'avais quelques ennuis avec la synchronisation hors-ligne de mes playlists Deezer.

À chaque problème sa solution

D'emblée, je ne reviendrai pas ici sur les raisons de mon choix de Deezer face à ses concurrents comme Spotify, SoundCloud, Google Music & co. pour la simple et bonne raison que de nombreux facteurs subjectifs rentrent en jeu. Dans l'ensemble, je suis plutôt satisfait de mon expérience avec Deezer, que ce soit l'interface utilisateur ou le catalogue musical. Mais voilà, j'ai deux problèmes.

Primo, un problème quotidien. Il y a quelques mois de cela (avant Noël en fait), j'avais un smart phone d'un certain âge, un Wiko Cink Peax (la première version, le vieux quoi). Acheté en juin 2013, il allait sur ses 2 ans et demi, ce qui en fait presque un centenaire en âge smart phone. Et malgré une soi-disant Compatibilité avec votre appareil, la synchronisation hors-ligne sur mon appareil ne marchait pas très bien. Sur une playlist de plus de 200 titres, je n'en n'avait qu'une trentaine, qui se lisaient quand ils le voulaient bien.

À ceux qui sont déjà en train de penser que j'avais qu'à changer de téléphone pour en avoir un plus récent, je leur rétorquerai que malgré un téléphone tout neuf que je me suis offert pour Noël (Un OnePlus X pour ceux que ça intéresse), le problème de synchronisation s'est réglé mais pas celui de la lecture.

Mon second problème est plus circonstanciel. Il s'avère que je me suis récemment marié. Et autant un mariage sans musique était totalement inconcevable, autant avoir un DJ qui passait des musiques que je n'aime pas en parlant avec un auto-tune me tentait que moyennement. D'un commun accord, avec l'heureuse élue, nous avons donc décider de créer de bonne playlists Deezer, que l'on aurait plus qu'à faire tourner pendant la soirée.

Mais voilà, le lieu de la réception n'a pas de connexion internet. Il va donc nous falloir synchroniser toutes ces chansons, mais sur mon ordi impossible, la synchronisation ne fonctionne pas sous Linux (même sous Ubuntu)... Or, avec ma manie de convertir tout le monde au Linux, ça devient de plus en plus compliqué de trouver une machine Windows dans mon entourage.

Après quelques recherches, on trouve des âmes charitables prêts à nous prêter une tablette, sous Android ou Windows. Voilà au moins une solution de secours. Mais entre temps, j'ai découvert Mixxx et je me suis dit que ce serait vraiment pas mal d'avoir les musiques en local sur mon PC.

Nous sommes alors début juin, j'ai 4 mois pour trouver une solution.

La stack audio Linux

Disclaimer : je m'excuse par avance de toutes les digressions qui pourrait être faite dans cette partie de l'article. Je vais expliquer ce que j'ai compris du fonctionnement audio Linux, mais je suis loin d'être un expert. N'hésitez pas à faire des remarques dans les commentaires, je tâcherai de corriger en conséquence.

Le fonctionnement de l'audio sous Linux, et je suppose sur d'autres systèmes également, est assez simple. Le principe repose sur 4 couches :

  • Une couche matériel : les haut-parleurs, casques, etc. qui ne font que retranscrire un signal électrique.
  • Un driver qui s'occupe de transcrire un signal numérique en entrée, en un signal analogique/électrique en sortie compréhensible par le matériel. Sous Linux c'est généralement ALSA qui s'occupe de ça.
  • Une application qui génère un signal audio numérique. Typiquement, n'importe quelle application qui fait du bruit : l'OS quand il fait des bruits systèmes, un lecteur audio, un navigateur internet, etc.

Voilà, c'est aussi simple que ça. Mais pour ceux qui ont un peu suivi, je vous parle de 4 couches et n'en cite que 3. C'est bien, je vois que vous êtes attentifs.

L'équivalent matériel de la stack
Crédit : Flickr

En fait, la quatrième couche émerge d'une limitation des drivers. Un driver audio ne peut gérer qu'une seule entrée. Donc lorsque votre lecteur musical joue votre musique préférée, votre navigateur est incapable de jouer quoique ce soit. Le driver va gentiment lui dire d'attendre son tour. Si cette situation parait assez plaisante puisqu'elle nous éviterait d'entendre toutes ces musiques venant de popups ou de webmasters peu scrupuleux, elle s'avère problématique dans pas mal de cas quand même. Pour régler ce problème, une couche logicielle vient s'intercaler entre les applications et le driver, c'est le serveur audio.

Le rôle du serveur audio est assez simple. Lui sait gérer plusieurs entrées. Il se contente alors de récupérer l'audio provenant de toutes les applications qui le souhaitent, multiplexer le tout et envoyer un seul et unique flux de données vers le driver.

Sous Linux, on compte principalement deux serveurs audio, PulseAudio qui est majoritairement utilisé sur les systèmes classiques, et Jack qui est particulièrement reconnu dans le monde de la Musique Assistée par Ordinateur (MAO) sous Linux pour sa modularité et sa capacité temps réel.

L'avantage du serveur audio, c'est que TOUS les flux audio de votre système passe par lui, et qu'il ne vient généralement pas seul.

Enregistrement d'un flux audio avec PulseAudio

L'objectif principal du serveur audio, comme je le disais, c'est de multiplexer les différentes sources sonores. Mais n'importe quel serveur un peu avancé, comme peuvent l'être Pulse et Jack, permettent bien plus. Un simple exemple est le contrôle du volume de chaque flux. Si vous êtes sur un système classique comme Ubuntu, vous devriez pouvoir ouvrir votre gestionnaire audio, aller dans l'onglet Lecture ou Applications où vous devriez voir la liste des applications en train de jouer quelque chose avec pour chacune d'elle un curseur. Ce curseur permet de modifier le volume des flux indépendamment les uns des autres.

Mais alors s'il est possible de contrôler les flux indépendamment, est-il possible de faire plus ? Eh bien oui. PulseAudio est fourni avec quelques utilitaires qui permettent notamment d'enregistrer directement un flux sur son disque.

J'aurais pu m'arrêter là en me disant : Bah voilà, j'ai juste à lancer Deezer dans un navigateur et enregistrer son flux de sortie. Mais voilà, le problème c'est que j'aurai obtenu un seul gros fichier contenant les 200 chansons dans l'ordre de lecture. Pas vraiment ce que je veux.

Mon objectif, c'est alors d'obtenir le flux audio de Deezer, et de détecter les changements de chansons pour créer un fichier par musique. Si au passage je pouvais obtenir le nom de l'artiste et de la piste, ça me permettrait de renommer les fichiers correctement et de rajouter quelques tags dans le fichier final.

Pour ça, on va se servir des modules PulseAudio pour extraire le flux d'une seule et unique application. Il s'avère ensuite que la plupart des sites diffusant de l'audio, Deezer, Youtube, Spotify modifient le titre du navigateur en fonction du fichier en cours de lecture. On va pouvoir utiliser cette information pour détecter le changement de chanson et en extraire les infos de la piste courante. Tout ça en Python, parce que j'aime bien Python.

Architecture et codage

L'architecture de base du projet était assez simple. D'un côté, je récupère l'audio et l'enregistre dans un fichier. De l'autre je regarde toute les demies secondes si le titre du navigateur a changé (avec des utilitaires comme xprop ou xwininfo) et si c'est le cas, je change tout simplement de fichier dans lequel j'écris.

Ça c'était l'architecture de base... Qui s'est vite révélée insuffisante. Plusieurs problèmes ont émergé.

  • Le plus évident, c'est que la coupure entre deux chansons est parfois très courte et l'intervalle d'une demi seconde faisait que le début de certaines chansons était dans le même fichier que la fin de la chanson d'avant.
  • Le deuxième, c'est qu'à l'époque, je ne connaissais pas encore les modules PulseAudio, et mon flux d'entrée était le même que celui reçu par le driver (j'utilisais un utilitaire ALSA pour récupérer le flux). Aucun souci jusqu'à ce que je reçoive un mail. Le petit bruit de notification en plein milieu de sa musique d'ouverture de bal, ça le fait moyen...
  • Troisièmement, cette version du projet avait tendance à perdre du signal lorsque le processeur surchargeait un peu.

J'ai ensuite rapidement migré vers l'utilisation des modules PulseAudio, ce qui éliminait déjà tous les bruits superflus. Toutes les autres applications continueront de fonctionner comme si de rien n'était sans impacter le flux que je veux enregistrer. La contrepartie, c'est que l'on entend plus le flux enregistrer, il faut se fier à l'application. (En vrai, il est possible de dupliquer le flux pour le rediriger à la fois vers l'enregistreur et vers le driver, mais ça a tendance à faire laguer très fortement l'audio, qui en devient inutilisable). Pour corriger le problème de latence de détection de changement de chanson, j'ai augmenté la fréquence de vérification du titre de la fenêtre, ce qui n'a fait qu'empirer les pertes de données. Il fallait que je trouve autre chose.

C'est à partir d'ici que j'ai commencé à implémenter une fonction de détection de silence dans le flux audio. L'idée est assez simple, au changement de chanson, il y a quasiment toujours un silence (sauf cas particulier de certains albums, mais d'une ce n'était pas mon cas et de deux, je pense que même dans ce cas-là, il y a une micro coupure due au chargement du fichier). Suffit alors de détecter ce silence et d'en déduire le changement de chanson. Rien de bien compliqué.

Quand je tombe sur un bout de code qui ne sert à rien
Crédit : Les joies du code

Bon, je vous avoue, à ce point-là, je ne comprends plus complètement mon code. Certains trucs me paraissent complètement idiots... Dans l'ensemble, l'idée était bonne mais il s'avère que je lançais une recherche de silence toute les demies secondes. Niveau optimisation on a vu mieux ! En plus, il existe des chansons avec des passages silencieux au milieu ce qui détruit complètement mon approche. Je l'ai donc modifié un peu pour lancer une recherche de silence seulement au voisinage du changement de chanson (vérifié environ toutes les secondes).

On s'approche de quelque chose d'utilisable. Par la suite, le principe général n'a guère changé, il s'agit surtout de ré-écriture de manière à faire un code, plus efficace, plus maintenable et plus évolutif.

Aujourd'hui le code se base sur plusieurs briques :

  • Un AppInspector qui s'occupe de vérifier les changements de piste et de déterminer l'artiste et le titre de la chanson. C'est aussi lui qui détermine la fin de l'enregistrement (lorsque toutes les pistes ont été enregistrées). Il est relativement basique, mais supporte la plupart des sites comme Deezer, Spotify ou Youtube.
  • Un streamloader qui limite la perte de données. La majorité des pertes de données dans les précédentes versions venaient du fait que l'application ne gérait pas les données aussi vite qu'elles arrivaient. Ce thread s'occupe de charger les données en mémoire aussi vite que possible. En gros, soit on ne perd aucune donnée, soit l'application plante à cause d'une stack overflow.
  • Un manager PulseAudio, qui sert principalement d'API.
  • Un SongWriter qui va chercher les silences au changement de piste dans les données chargées par le streamloader et écrire les données d'UNE chanson sur le disque au format brut.
  • Un encodeur, qui va convertir le fichier brut en un fichier lisible par les lecteurs classiques. Il y en a 2 d'implémentés pour l'instant, un encodeur MP3 et un encodeur FLAC.

Le tout s'est révélé assez satisfaisant puisque je n'ai eu aucun problème de musique notable à mon mariage. En revanche sur des lecteurs plus particuliers (téléphone, autoradio), il s'avère que certains morceaux au format MP3 sautent. C'est un problème que je cherche à régler.

Si cet outil vous intéresse, n'hésitez pas à le télécharger et à vous en servir, je suis prêt à répondre à toutes vos questions. J'attends vos retours.

Musicalement,
Alexis

N'hésitez pas à réagir

Via Disqus ...

... ou Facebook