Articles Tagués ‘Enna’

h1

Quoi de neuf…

2010.07.03

Hello,

Pas beaucoup de nouvelles depuis passablement de temps. Je vais faire néanmoins un petit tour en bref des quelques activités sur lesquels j’ai travaillé. Mon activité sur les projets GeeXboX à baissé nettement depuis quelques semaines pour plusieurs raisons. J’ai eu 3 mois de service civil où j’ai qu’en même pu travailler dans mon domaine (l’informatique et plus précisément la programmation). Et de ces trois mois, sont nés deux petits projets hébergés sur les serveurs GeeXboX.

DaisyDuck & libduck

DaisyDuckJe vous invite à consulter le site internet. DaisyDuck est un lecteur de livre audio Daisy 2.02, basé sur Qt et libVLC. Il est multi-plateforme et distribué aussi bien pour Windows que pour Linux. Il pourrait également être adapté sans trop de problème à MacOSX. Mais pour des questions de temps, je me suis arrêté à la version Windows.

Il existe de "nombreux" (beaucoup de commerciaux aussi) programmes de lecteur de livre Daisy. Mais la grande différence avec DaisyDuck c’est tout d’abord libVLC. La plupart ne sont pas capables de lire une grande variété de format, et encore moins de protocole réseau. Même qu’en principe pouvoir lire n’importe quel format n’est pas un respect entier des spécifications. Par contre c’était le but de mon travail. Permettre la lecture de livre en ligne, ce que le logiciel est parfaitement capable de faire, et ça fonctionne à merveille. Merci à l’équipe de VideoLAN.

Le deuxième petit projet est donc libduck. Il est également présenté sur le site internet de DaisyDuck. Son but est de réaliser le "parsing" des fichiers Daisy 2.02.

Malheureusement je n’ai pas utilisé libplayer comme base à DaisyDuck. La raison principale est que libplayer n’est pas encore très utilisable (compilable) pour Windows pour différentes raisons techniques. Et faute de temps, je ne pouvais pas me permettre de travailler sur le port de libplayer dans mon temps destiné à mon service civil.

Valhalla

Bien que j’ai terminé le service civil depuis fin mai, il me fallait également trouver un emploi. Et depuis mi juin, j’effectue des trajets relativement longs par jour. Mon temps libre en soirée est devenu presque nul. Je me motive alors à travailler sur les projets GeeXboX, dans le train. Et faut bien l’avouer, c’est difficile d’avancer vite. Mais ça avance qu’en même, et sur libvalhalla cette fois ci.

J’ai commencé sérieusement à ajouter le support des langues pour les méta-données. Le patch devrait arriver très vite, peut être même demain. Par contre ce n’est pas encore complet. Je dois adapter les fonctions pour les sélections des méta-données afin de pouvoir filtrer sur les langues, il me faut aussi encore implémenter le support des grabbers multi-lingues. Ou tout du moins, pouvoir paramétrer les grabbers multi-lingues pour récupérer les données dans une langue spécifique. Au moment où j’écris ces lignes, j’ai uniquement ajouté le support des langues dans la base de donnée, et adapté les grabbers afin que l’information de langue soit correctement indiquée avec les meta-données. Par exemple, le grabber Allocine indique ainsi du "fr" pour ce qui est des résumés, catégories, etc,.. Les autres grabbers sont en principe en "en" et les données qui n’ont pas de langue (par exemple un codec, la taille du fichier ou alors la résolution) sont indiquées comme "undef". Toutes les méta-données récupérées par les "parsers" sont également en "undef".

Un autre projet dans lequel je veux me lancer sérieusement, c’est de pouvoir paralléliser le "downloader" et les "grabbers" pour un même fichier. Afin de récupérer les images un peu plus vite. Mais je ne veux pas rentrer dans ce genre d’explications avec ce poste.

libplayer

Aux alentours de mai, j’ai intégré le support de VDPAU directement dans libplayer. Alors non, il n’y a rien d’extraordinaire, ça concerne quelques lignes. L’idée c’est que pour pouvoir exécuter MPlayer correctement avec VDPAU, il faut connaître les caractéristiques du GPU. Et pour ça, il faut interroger la carte graphique. Avec GeeXboX, cela se faisait en dehors d’Enna/libplayer avec un script et un exécutable. Mais ceci n’est plus nécessaire, car maintenant libplayer fait cette tâche de manière transparente.

Enna

Nicolas à modifié en profondeur le VFS d’Enna. J’ai donc pris le temps de retravailler le browser Valhalla. Celui-ci est désormais mieux fait au niveau des chemins d’accès (je n’ai pas envie d’expliquer ça ici). Bref, en gros ce browser re-fonctionne avec le nouveau VFS mais il a qu’en même quelques petites régressions qui ne devraient pas être trop difficiles à corriger.

Le projet de "hardware"

Réaliser un set-top box GeeXboX est très nouveau et assez ambitieux. L’idée est de partir de zéro, de la schématique au PCB puis aux prototypes, pour finir avec la distribution GeeXboX/Enna optimisée au mieux pour le matériel. Le seul gros obstacle actuellement c’est la difficulté à avoir accès aux datasheets des composants intéressants (NDA nécessaires dans quasiment tous les cas). On vise bien sûr le full-HD avec si possible un petit plus par rapport aux autres boards. Tel que par exemple des ports miniPCIe. On a les ressources techniques, manque la doc :-). Ensuite il y aura forcément le problème du temps qui pourra être investi dans ce projet. Mais pour le moment je suis assez confiant.

Pour terminer (et pour arrêter de parler de moi)

Toolchain

Davide fait un gros boulot depuis plusieurs semaines. Il a pris une excellente initiative concernant le toolchain GeeXboX. Le but est d’avoir un toolchain basé sur opkg. Tout se construit via des paquetages, que se sois les dépendances pour la construction du toolchain (gcc, binutils, etc…) que les éléments du "target".

J’ai toujours aimé le toolchain GeeXboX pour sa simplicité. On entend souvent parler d’OpenWRT par exemple. Il est sûrement très bien, mais notre toolchain c’est notre identité. J’ai toujours été réticent à le voir se faire remplacer par une solution qui viendrait d’ailleurs. Alors un grand merci à Davide pour avoir fait ce gros job!

Le site web

Benjamin en avait marre du site web. Je le comprend tout à fait car j’ai toujours eu la flemme d’y faire des modifs. Faut être clair, chez GeeXboX en principe faut un peu toucher à tout. Mais parfois les tâches comme le site, pas grand monde à envie d’y mettre les mains. Alors Benjamin à basculer sur WordPress qui simplifie grandement la vie aussi bien pour la facilité que pour le design.

J’aimerais qu’en même dire un mot sur l’ancien site. Il avait une particularité en comparaison à beaucoup d’autres sites internet. Ce bon vieux site était entièrement réalisé en XML+XSL (un très bel exemple et très professionnel). Bien sûr les sources sont toujours bien au chaud, dans les dépots Mercurial.

Enna

Nicolas a fait diverses modifications sur Enna en plus du VFS. Néanmoins je n’ai pas encore pu compiler ces dernières modifs :-P. Faute de temps principalement, car je dois mettre à jour les EFL.

A bientôt,

Mathieu SCHROETER

h1

De "POSIX" à Windows

2010.02.19

Hello,

La sortie d’Enna au début janvier à réveiller des critiques de tous les genres. En principe (faut être honnête) elles ne m’intéressent pas spécialement. Tout d’abord je n’estime pas qu’il y ait de concurrence entre les logiciels libres. Beaucoup de projets s’inspirent d’autres projets et c’est normal. Et si quelqu’un désire une fonctionnalité spécifique il a plusieurs solutions. La première c’est d’utiliser le projet qui offre la fonctionnalité (non?). La seconde c’est de critiquer simplement le projet car une fonctionnalité évidente est absente. Se sont ces critiques là que j’ignore spécialement, car elles n’apportent rien.  Après vous avez des gens qui critiquent mais qui aident spontanément et ils sont toujours les bienvenue.

De "POSIX" à Windows

Une des critique facile est de dire qu’Enna ne fonctionne pas sous Windows et qu’XBMC par contre est multi-plateforme. Les gens qui le disent ont tendance à oublier (ou alors à ne pas du tout connaitre, même dans les grandes lignes) l’histoire d’XBMC. Et oui, à l’origine XBMC ne fonctionnait pas nativement sous Linux. Le port à pris du temps, et c’est le même problème quand il faut porter dans l’autre sens.

Pour en revenir à Enna, je n’ai aucun intérêt personnel à l’avoir sous Windows. Néanmoins il y a eu des progrès pour qu’un jour, Enna puisse fonctionner sous Windows. Pour quand? Je n’en sais rien et ça n’a aucune importance.

Concernant le titre, si j’ai mis POSIX entre guillemets c’est parce que tout n’est pas vraiment du POSIX. Certaines choses sont des extensions du GNU par exemple. Il y a des adaptations à faire aussi entre les systèmes qui se basent sur POSIX. Même entre les noyaux Linux et *BSD, voir même Hurd.

Par exemple libvalhalla fonctionne correctement sous les noyaux Linux et FreeBSD (je pense spécialement à Debian GNU/kFreeBSD), elle fonctionne aussi avec Hurd (testé avec Debian GNU/Hurd) à la différence que les priorités sur les threads ne sont pas gérées correctement. Chaque noyau à sa façon de faire des threads et ça demande de prendre en compte les cas particuliers.

J’ai volontairement omis de mentionner Mac OS X, ou plus précisément Darwin. Bien qu’Apple dit qu’il soit POSIX-compliant, il y a qu’en même au moins un cas particulier dans libvalhalla car ce n’est pas si POSIX que ça.

MinGW

Le meilleur moyen de réaliser des ports Windows est sans aucun doute MinGW. C’est une base GCC et le compilateur peut être natif Windows ou alors compilé pour une compilation croisée sous GNU/Linux (ou d’autres OS). En principe depuis GNU/Linux on peut cross-compiler aussi bien pour Windows que pour Darwin (c’est ainsi que les différentes versions du générateur d’ISO sont faites). Néanmoins, ça peut paraitre étonnant mais il est plus facile de créer un compilateur croisé pour Windows (merci au projet MinGW) que pour Darwin.

J’ai deux cross-compilateurs binaires pour Darwin8 (PPC et i686). Ils ont été créés il y a maintenant plusieurs années par un ancien membre de GeeXboX. Malheureusement il est parti avec les secrets de fabrication. Je n’ai jamais réussi à les reproduire depuis les sources (et ce n’est pas faute d’avoir essayé). Si quelqu’un à des pistes, elles m’intéressent grandement!

libgeexbox-win32

Avant d’espérer Enna sous Windows il faut bien sûr se concentrer sur les dépendances. Et ce qui nous intéresse ici c’est donc libnfo, libplayer et libvalhalla. Au moment où j’écris cet article, libnfo et libvalhalla sont "complètement" supportés sous Windows. Je vais reprendre quelques éléments intéressants qui ont posés des problèmes.

Notez les guillemets, car en ce qui concerne libvalhalla il reste un potentiel problème. Mais néanmoins la bibliothèque est utilisable.

Libvalhalla utilise des temporisations à différents endroits. Celles-ci sont réalisées à l’aide de variable-conditions/mutex. L’idée est d’avoir des temporisations interruptibles contrairement à des fonctions du type sleep(), usleep() ou nanosleep() (attention, je parle bien de temporisations interruptibles sans l’aide de signaux). La bibliothèque Pthreads de POSIX offre tout ce dont on a besoin. Ainsi libvalhalla et libplayer reposent complètement sur celle-ci. Mais ce n’est pas directement de Pthreads que je désire parler, mais du temps pour pouvoir espérer avoir des temporisations plus ou moins précises. Les fonctions pthreads utilisent la structure `struct timespec` qui en théorie offre un champ à la nanoseconde. Même si la valeur peut être juste au moment de la lecture de l’horloge, les appels de fonctions prennent de toutes façon des nanosecondes/microsecondes. Et même pour un système temps réel dur, c’est très difficile de jouer dans ces ordres de grandeurs. Les seuls applications pratiques où je me suis vraiment amusé à compter les nanosecondes c’est lorsque que je faisais du VHDL sur un bon vieux Xilinx.

Bref.. passons.. Mon but est de pouvoir traiter des temporisations de plusieurs centaines de millisecondes. Ce qui est très facile avec un noyau Linux. La structure timespec évoquée précédemment se présente ainsi.

struct timespec {
  time_t sec;
  long int nsec;
}

Sous *BSD, Linux et Darwin il est très facile de la peupler. Concernant Mac OS X et son pseudo POSIX-compliant, le noyau Mach permet de récupérer une structure relativement semblable avec également des nanosecondes, mais la fonction POSIX clock_gettime() n’existe pas chez Apple. Que le champ nsec soit juste ou non ça n’a pas d’importance, pour autant qu’il ne soit pas faux dans les millisecondes. Finalement ces trois noyaux offrent les fonctions nécessaires et même plus. Mais on ne peut pas en dire autant de l’API Windows.

Une question de temps

GetSystemTime

Windows met à disposition des fonctions nommées GetSystemTime() et GetSystemTimeAsFileTime(). Elles sont sensées retourner une résolution à la milliseconde, respectivement à la centaine de nanoseconde. GetSystemTimeAsFileTime() est connu comme étant plus rapide que GetSystemTime(). Par contre cette fonction n’existe pas sous Windows CE et perd donc de son intérêt (dès le moment qu’on recherche la portabilité).

Voyez plutôt le résultat en pratique avec mon PC.

 WinXP                         GNU/Linux/Wine
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
GetSystemTime ()
 1266227465.000000000          1266233962.000000000
 1266227465.000000000          1266233962.000000000
 1266227465.000000000          1266233962.000000000
 1266227465.000000000          1266233962.000000000
 1266227465.000000000          1266233962.000000000
 - wait 1 ms
 1266227465.015000000          1266233962.001000000
 - wait 2 ms
 1266227465.031000000          1266233962.003000000
 - wait 3 ms
 1266227465.046000000          1266233962.006000000
 - wait 4 ms
 1266227465.062000000          1266233962.010000000
 - wait 5 ms
 1266227465.078000000          1266233962.015000000

A gauche il y a donc les résultats directement depuis Windows XP. A droite c’est le même programme mais exécuté à travers Wine (le même PC est utilisé). Les attentes de 1 à 5 ms sont réalisées simplement par la fonction Sleep() également mise à disposition par l’API Windows. Il est intéressant de noter que Windows n’arrive pas à descendre à la milliseconde avec un Sleep(1). Problème connu ceci dit…

A noter également que la fonction GetSystemTime() n’est pas des plus performante. Elle est reconnue comme étant peu propice à offrir réellement 1 ms de résolution. J’ai fais ainsi une seconde mesure avec 10’000 lectures du compteur, pour détecter la résolution effective.

Après plus de 8’000 lectures, Windows retourne vraiment 15 ms de plus que la lecture précédente.

 1266484241.000000000
 1266484241.000000000
 ... ~8000 fois ...
 1266484241.000000000
 1266484241.000000000
 1266484241.015000000
 1266484241.015000000
 1266484241.015000000
 1266484241.015000000

GetSystemTimeAsFileTime

J’ai donc refais les mêmes mesures mais avec GetSystemTimeAsFileTime() pour voir si on arrive à des meilleurs résultats. Le MSDN parle de 100 ns, on peut donc espérer une résolution utilisable à la milliseconde.

 WinXP                         GNU/Linux/Wine
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
GetSystemTimeAsFileTime ()
 1266227465.078125000          1266233962.015939000
 1266227465.078125000          1266233962.015941000
 1266227465.078125000          1266233962.015942000
 1266227465.078125000          1266233962.015944000
 1266227465.078125000          1266233962.015945000
 - wait 1 ms
 1266227465.093750000          1266233962.017012000
 - wait 2 ms
 1266227465.109375000          1266233962.019076000
 - wait 3 ms
 1266227465.125000000          1266233962.022139000
 - wait 4 ms
 1266227465.140625000          1266233962.026204000
 - wait 5 ms
 1266227465.156250000          1266233962.031268000

La première chose qui frappe ici, c’est que Windows semble donner que des valeurs multiples de 25. Donc d’une résolution de 25 us. On est encore relativement loin des 100 ns promis par le MSDN. Mais pour tester la vrai résolution, j’ai également fais tourner la lecture 10’000 fois.

On constate que Wine arrive à atteindre la microseconde. Néanmoins on n’a pas non plus la résolution de 100 ns. La raison est que Wine se base sur la fonction gettimeofday() qui sous les systèmes POSIX, ne donne pas une résolution meilleure que la microseconde. La structure est un timeval au lieu d’un timespec avec un champ usec au lieu de nsec.

Ici aussi, après environ 8’000 lectures, on constate une résolution d’exactement 109.375-93.75=15.625\,ms. C’est aussi mauvais qu’avant. Les microsecondes n’apportent absolument rien. Au début je me suis fais avoir car je pensais vraiment que les 25 us étaient atteints. Et bien que la fonction est sensée être plus rapide d’après mes recherches, en pratique (sous Windows XP), il n’y a pas de quoi en faire une montagne. Il a fallut presque le même nombre de lecture (un peu plus de 8000) pour environ 15 ms.

 1266484241.093750000
 1266484241.093750000
 ... ~8000 fois ...
 1266484241.093750000
 1266484241.093750000
 1266484241.109375000
 1266484241.109375000
 1266484241.109375000
 1266484241.109375000

Je pense qu’elle est considérée comme plus rapide car elle ne peuple pas une structure relativement complexe comme GetSystemTime (voir SYSTEMTIME). La structure utilisée avec la seconde fonction est FILETIME.

Finalement, comme première conclusion et pour garder la compatibilité avec Windows CE on peut utiliser GetSystemTime() sans regret.

clock_gettime

Il existe donc un moyen d’avoir une bien meilleur résolution. Le principe est d’utiliser l’horloge haute résolution (la TSC dans les processeurs x86) afin d’atteindre la nanoseconde. Pour ce faire, Windows met à disposition deux fonctions, QueryPerformanceFrequeny() conjointement avec QueryPerformanceCounter().

Le but final est de simuler la fonction clock_gettime() de POSIX qui permet d’atteindre une résolution de 1 ns.

La première fonction donne la fréquence de l’horloge haute résolution et la seconde donne le nombre de ticks depuis la mise en route. La fréquence donnée est toujours (à peu de chose près) un multiple de 1193182 Hz.

Le principe est donc de retrouver le tick qui correspond à un temps précis depuis EPOCH. Puis de retrouver le temps en divisant simplement le nombre de ticks par la fréquence. L’horloge étant au minimum cadencée à 1193182 Hz, on devrait avoir au moins une résolution de \frac{1}{1193182}=838.10\,ns.

 WinXP                         GNU/Linux/Wine
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 Freq: 3579545                 Freq: 1193182
 1266227465.156298915          1266233962.031381633
 1266227465.156301150          1266233962.031384147
 1266227465.156302826          1266233962.031385823
 1266227465.156304502          1266233962.031387499
 1266227465.156306178          1266233962.031389176
 - wait 1 ms
 1266227465.171900059          1266233962.032571728
 - wait 2 ms
 1266227465.187522995          1266233962.034622547
 - wait 3 ms
 1266227465.203157943          1266233962.037689975
 - wait 4 ms
 1266227465.218764954          1266233962.041754736
 - wait 5 ms
 1266227465.234392918          1266233962.046825211

A noter que Wine donne toujours la fréquence la plus basse. Cette fréquence normalement dépend du matériel, mais Wine se base sur Linux pour récupérer le temps. Ainsi la fréquence peut être arbitraire. Windows XP me donne par contre une fréquence pas tout à fait correcte. Comme je l’ai dis avant, celle-ci devrait être un multiple de 1193182, pourtant pour que ce multiple soit vrai, il faudrait alors 3579546 au lieu de 3579545. Je suppose que la fonction QueryPerformanceFrequeny() n’arrondit pas la valeur.

On trouve ici un pas de 1676/1677 ns. Aussi bien avec Windows qu’avec Wine. Ce qui est très bon. Le temps perdu vient désormais des appels de fonctions et non plus de l’imprécision des valeurs de temps.

On pourrait crier victoire, mais en réalité il y a encore un problème potentiel. L’horloge haute résolution est indépendante. Ce qui veut dire qu’elle va forcément diverger par rapport à l’horloge qui donne le "vrai" temps. Ainsi sur une longue période, l’erreur entre les deux va s’agrandir linéairement.

Étant donné que la résolution de GetSystemTimeAsFileTime() est trop imprécise pour de courtes mesures, il est nécessaire de faire des mesures sur plusieurs heures pour avoir des résultats significatifs.  Seul la résolution de 15.625 ms peut servir de référence et une telle divergence ne peut pas être détectée sur quelques minutes (à moins que QueryPerformanceCounter() et QueryPerformanceFrequeny() soient complètement faux). Je n’ai donc rien à vous montrer au sujet de ce potentiel problème de divergence.

La synchronisation

Bien que je ne connaisse pas encore la divergence entre les horloges, on peu légitimement se poser la question de la resynchronisation.

Le principe est d’utiliser une information qui est fiable. Et donc a priori c’est la seconde. L’idée est de récupérer le tick qui correspond au changement de seconde. Ensuite ce tick est converti en un temps depuis EPOCH (un temps absolu en seconde). On mémorise cette seconde pour toute la durée de vie du programme.

Dès que clock_gettime() est appelé, on regarde la valeur du compteur de l’horloge haute résolution, puis on la soustrait à la valeur qui correspond aux secondes du début. On a donc une différence de valeur du compteur. On la divise par la fréquence du compteur ce qui nous donne la différence de temps. On additionne ce nouveau temps avec les secondes du départ pour enfin avoir le temps en nanoseconde depuis EPOCH.

Le potentiel problème avec la synchronisation vient spécialement du fait d’utiliser la seconde comme référence. Si la synchronisation commence au début d’une nouvelle seconde, il faut attendre quasiment une seconde pour terminer la synchronisation. Ainsi actuellement dans le libvalhalla pour Windows, il n’y a pas de resynchronisation. En fonction du décalage entre l’horloge haute résolution et l’horloge du temps, les timers finissent par se rentrer dedans ou alors par devenir de plus en plus écartés. Le fait qu’ils divergent ou convergent dépend du matériel.

Je vois deux solutions pour le moment.

  1. Faire la resynchronisation en parallèle au reste du programme. Ainsi on peut continuer d’utiliser clock_gettime() avec la précédente synchronisation.
  2. Synchroniser sur GetSystemTimeAsFileTime() avec son pas de 15.625 ms. Mais la compatibilité avec Windows CE est perdue.

Il reste aussi à déterminer quand est-ce qu’il faut resynchroniser.

Un autre problème vient des changements de l’heure du système. Si cela arrive, actuellement libvalhalla aura toutes les temporisations faussées sous Windows.

Les Pthreads

Finalement, on peut se demander si tout cela vaut la peine. Les Pthreads pour Windows ont été conçus pour fonctionner sur un maximum de versions de Windows. Ainsi la référence de temps utilisée se fait via GetSystemTime(). Le clock_gettime() utilisé dans libvalhalla à deux raisons d’être. D’abord il sert à donner un temps absolu aux fonctions Pthreads, et il sert à faire les mesures de temps pour les statistiques. L’aberration dans tout ce travail sur un clock_gettime() pour Windows est simplement que le temps donné aux fonctions Pthreads est de bien meilleur résolution que la résolution du temps interne au Pthreads-win32  (il faudrait néanmoins que je vérifie ce point, je n’ai fais que survoler les sources de Pthreads-win32). Et avoir une résolution à la nanoseconde pour des statistiques n’apporte rien.

Un des seul intérêt restant c’est donc le petit défi que ça représente.

J’hésite à enlever tout le code relatif à QueryPerformanceCounter() pour n’utiliser que GetSystemTime() avec sa misérable résolution. Ou alors rajouter un test sur la fonction GetSystemTimeAsFileTime() pour la préférer à GetSystemTime() si elle existe. Tout ces problèmes me rappel toujours un peu plus pourquoi Windows à un noyau  qui n’a rien de plus que les autres. Mais qui au contraire, ne créer que des problèmes supplémentaires.

Speedhack

Je profite de cet article pour présenter les speedhacks (ces logiciels de triches permettant par exemple de se déplacer plus vite dans un jeu, très prisé à l’époque sur Counter-Strike).

Si j’en parle ici c’est qu’ils reposent sur les fonctions de l’horloge haute résolution, et plus précisément QueryPerformanceCounter(). Il y a un peu plus d’un an, j’avais écris un article à ce sujet que vous pouvez lire à cette adresse. J’en ai profité pour y faire deux trois améliorations et corrections.

A bientôt,

Mathieu SCHROETER

h1

Autour de la compilation d’Enna

2009.12.17

Hello,

je tiens juste à donner quelques précisions pour les personnes qui désirent compiler Enna, et donc également les bibliothèques rattachées. Cet article n’est pas un tutoriel. Si vous ne savez pas comment vous y prendre, il vaut mieux rechercher l’information ailleurs.

Faites aussi un tour à ces adresses (en anglais / in english) :
http://enna.geexbox.org/developers.html
http://captainigloo.wordpress.com/2009/12/21/enna-compilation-on-ubuntu-3/

Evas

Comme vous le savez, Enna se base sur les EFL. Je vous conseil fortement d’utiliser les snapshots ne serait-ce au moins pour ne pas que vous reportiez des "bugs" juste parce que les APIs des EFL auraient trop évoluées. En partant du principe que vous savez compiler les EFL, je vous rappel de ne pas oublier le flag ––enable–gl–x11 avec Evas, si vous avez l’accélération 3D avec votre PC. Les performances graphiques d’Enna sont nettement meilleurs. Toujours concernant Evas, il y a une petite modification dans les sources qui vous permettra d’avoir des textures anti-aliasées. Dans le cas contraire, Enna est plus beau (mais plus lent) en X11 Software plutôt qu’en OpenGL. Il faut tout simplement modifier les définitions GL_NEAREST en GL_LINEAR dans toutes les sources.

cd ~/e17_src/evas
sed -i "s/GL_NEAREST/GL_LINEAR/g" \
    `grep -Rl GL_NEAREST . | grep "\.c$"`

Libgeexbox

Libgeexbox est une manière naïve de parler de toutes les bibliothèques que nous développons en parallèle à la distribution et à Enna. Depuis quelques temps, des releases sont apparues pour libnfo, libplayer et libvalhalla. Il faut savoir que se sont les toutes premières releases! Là où je veux en venir c’est qu’il était totalement justifié de toujours se baser sur les versions de développement pour compiler Enna. Par exemple le premier import de libplayer date de 2006 et c’était une habitude que de faire un hg pull -u régulièrement. Mais maintenant que les versions 1.0.0 sont disponibles, veuillez s.v.p. vous baser uniquement sur celles-ci (ou sur n’importe quelles nouvelles releases de ces libs dans le futur).

Il y a deux solutions, en passant sur les sites web respectifs des projets (les liens sont disponibles à droite des articles de ce blog) ou alors avec Mercurial. Mais dans ce cas veuillez préciser la version de la dernière release. Tel que par exemple:

hg clone -r v1.0.0 http://hg.geexbox.org/libvalhalla

Si vous chargez la devel (donc le "tip") vous ne pourrez pas compiler Enna ou alors vous avez de la chance :-). Depuis qu’il y a les releases je me permets de casser les APIs des bibliothèques parce que je suis un éternel insatisfait (je rigole…) et ça faisait longtemps que je voulais faire un peu de ménage dans certaines en-têtes publiques.

Et après l’installation des libgeexbox, n’oubliez pas de faire un `sudo ldconfig`. Question que le chargeur de programme ait les nouvelles bibliothèques de référencées.

libvalhalla

Quand vous exécutez le configure de libvalhalla, jetez un œil aux informations retournées avant de faire bêtement un `make`. Le configure désactive les éléments en fonction des bibliothèques qu’il ne trouve pas. Par exemple, si vous n’avez pas la libcurl-dev, le support des grabbers est complètement désactivé mais cela n’empêche pas la compilation.

Admettons que vous avez toutes les libs nécessaires et que vous voyez certains grabbers de désactivés. Je pense par exemple à lyricsfly. Ce n’est pas un bug. Lyricsfly n’est pas utilisable car la clef de l’API pour le webservice était provisoire. Ce grabber est donc désactivé par défaut. Si vous utilisez ––enable–grabbers pour être sûr d’activer tous les grabbers vous n’aurez rien à gagner. Vous ferez perdre du temps à libvalhalla sur lyricsfly qui ne retournera jamais rien (je parle de lyricsfly, mais ça peut être d’autres à l’avenir).

A noter également que dès que vous forcez les grabbers ou seulement quelques grabbers, leurs dépendances deviennent obligatoires. Par exemple si vous faites ––enable–grabber–ffmpeg vous forcez la dépendance sur libavcodec. Si vous avez vraiment libavcodec et que ça échoue, c’est simplement que votre version est trop ancienne. Par exemple, le libavcodec que vous trouvez avec Ubuntu Karmic ne supporte pas la fonction av_lockmgr_register() qui garanti l’utilisation des codecs à être multi-thread safe.

libplayer

Concernant libplayer, c’est la dépendance indirecte avec MPlayer qui est la plus importante. Assurez-vous que votre MPlayer est en anglais uniquement. Libplayer peut détecter les MPlayer incompatibles jusqu’à un certain point. En fonction de la manière dont MPlayer a été compilé, libplayer ne peut pas savoir s’il est en anglais ou non et va donc l’utiliser (pour les curieux, je parle de la variable d’environnement LINGUAS à la compilation d’MPlayer; libplayer ne détecte la langue que si celle-ci est passée avec ––language–msg= ou ––language=).

Démarrer Enna

Contrairement à certains tutoriels sur Enna, il n’est pas nécessaire de copier le fichier d’exemple enna.cfg qui se trouve à la racine des sources, dans le dossier ~/.enna. Parce que ce fichier est automatiquement créé au premier démarrage de l’interface. Et allez jeter un œil au contenu. Si vous voulez que libvalhalla puisse faire son travail, vaut mieux lui dire où il doit scanner. Dans le cas contraire 100% des fichiers seront traités en ondemand (je vous laisser chercher dans ce blog si vous ne comprenez pas).

Et maintenant que vous avez l’OpenGL, vous remarquerez assez vite qu’en le spécifiant dans ~/.enna/enna.cfg ça ne change pas grand chose. Pire que cela, ça ne change absolument rien ;-).

Pour l’instant il faudra le faire à la main depuis un terminal, avec:

ELM_ENGINE=gl enna

A bientôt,

Mathieu SCHROETER

h1

Le "decrapifier" de libvalhalla

2009.12.02

Hello,

sous ce nom barbare se cache un modeste et petit mécanisme pour nettoyer les noms des fichiers. Il faut savoir qu’avec des conteneurs audio et vidéo de bonne qualité, le titre du média est généralement stocké dans une méta-donnée "title". En général ce titre est propre et peut être utilisé tel quel pour une recherche d’information sur internet. Dans notre contexte, il est utilisé en général par les "grabbers" pour rechercher les données, paroles des chansons, descriptions, liste des acteurs, etc, … Malheureusement, on traine encore des boulets au niveau conteneur multimédia.

Un petit bond en arrière

Je me rappel encore très bien, à l’époque de Windows 3.1/3.11 d’avoir installé le composant optionnel "Video for Windows". Celui-ci rendait possible la lecture de petites vidéos dans des conteneurs AVI (entre autres). J’avais les yeux qui brillaient quand je voyais ces mini-films défiler. Pourtant aujourd’hui on se plaint quand une vidéo n’est même pas en 720p. D’un autre côté quand on voit le lecteur Flash qui bouffe toute la puissance CPU avec des vidéos de mauvaise qualité, y a de quoi se poser des questions par rapport à ce qui se faisait déjà il y a 15 ans en arrière. Voyez par exemple la vidéo suivante:

Cliquez dessus pour la lire! (si vous le pouvez :-P)

C’est un petit film qui date des alentours de 1997 (peut être même 1996), et qui est enregistré au format FLI. C’est un format d’animation d’image dans la même idée que le GIF. Ce petit film est cadencé à 14 images/sec avec une définition d’image de 320×200 px. Il peut y avoir également de la compression avec ce format, mais ce n’est pas aussi sophistiqué que du MPEG. Là où je veux en venir, c’est que cette vidéo est lisible de manière fluide sur un Smaky 130, qui correspond à un processeur Motorola 68030 (fixé à 25 MHz pour cet ordinateur). C’est d’un Smaky que j’ai récupéré ce film à l’aide d’un outil que je m’étais amusé à faire http://home.gna.org/fosfat/. La grande partie des logiciels Smaky sont écrits en CALM (en calme aussi), qui est une sorte de langage d’abstraction sur les assembleurs (une notation assembleur indépendante du fabricant). Il y a donc de forte chance que le Smaky utilise un logiciel écrit non pas en C/Pascal, mais en CALM pour arriver à lire cette animation. Ainsi le Flash me donne vraiment mal au ventre quand je le vois suer avec un Athlon X2 64.

Decrapifier

Bref, pour en revenir au "decrapifier", j’ai abordé les fichiers AVI. Ce conteneur développé à l’origine par Microsoft a aujourd’hui des défauts, et malheureusement on le retrouve encore bien trop souvent (on ne peut pas en vouloir à l’AVI, il est vieux). Quoiqu’il en soit, il n’y a pas de méta-donnée "title" la dedans.

Le "decrapifier" va alors se charger d’éliminer tous les caractères peu propice à aider à effectuer des recherches via les "grabbers". A noter également que lorsqu’un titre est nettoyé, c’est aussi une bonne chose pour l’utilisateur. Le principe est très simple et efficace dans la plupart des cas. Pour l’illustrer, prenons un nom de fichier inutilisable pour une recherche (et bien trop courant).

"{XvID-LOL}.Elephant.-.Dreams.s02e10_(DVDRip)_Etach.avi"
  1. Le nom est nettoyé de tous ses caractères dont le code ASCII est inclus dans les 7 premiers bits (128 premiers caractères). L’ASCII 7 bits à l’avantage d’être présent dans (presque) tous les codages de caractères dont l’unicode. L’idée est de simplement remplacer chacun de ces caractères par un espace, en prenant soins de supprimer l’extension du fichier (sauf exception pour l’apostrophe, les espaces et les caractères alpha-numériques qui sont conservés).
    "XvID LOL  Elephant   Dreams s02e10  DVDRip  Etach"
  2. Une liste noir de mots clefs va permettre d’éliminer tout ce qui ne nous intéresse pas pour la recherche. Les valeurs par défaut avec Enna (fichier ~/.enna/enna.cfg) sont:

    0tv, 1080p, 2hd, 720p, ac3, booya, caph, crimson, ctu, dimension, divx, dot, dsr, dvdrip, dvdscr, e7, etach, fov, fqm, hdq, hdtv, lol, mainevent, notv, pdtv, proper, pushercrew, repack, reseed, screencam, screener, sys, vtv, x264, xor, xvid, SExEP, sSEeEP

    Cette liste est donc éditable à souhait. Il y a deux mots clefs particulier (en fin de liste) que je vais expliquer dans un deuxième temps. Tous les autres mots clefs sont écrits en minuscules car le système n’est pas sensible aux majuscules en temps normal. On peut donc voir le résultat sous la forme suivante:

    "          Elephant   Dreams s02e10               "
  3. Mais le résultat précédent, bien que beaucoup plus intéressant, ne correspond pas à la réalité. Le mot clef "sSEeEP" présent dans la liste va permettre de gérer le s02e10. L’idée est de récupérer également des informations utiles comme la saison et le numéro d’épisode. Le vrai résultat est donc:
    "          Elephant   Dreams                      "
    

    Add new metadata "season", value: "2"
    Add new metadata "episode", value: "10"

  4. La dernière étape est de supprimer tous les espaces blancs superflus. Pour finalement avoir une chaîne de caractère sous cette forme:
    "Elephant Dreams"

Les motifs

Deux motifs ont été présentés brièvement. En réalité il y en a trois.

  • NUM (indique un nombre non signé et entier)
  • SE (indique un nombre non signé et entier qui représente une saison)
  • EP (indique un nombre non signé et entier qui représente un épisode)

Il y a quelques règles à prendre en compte lorsqu’on les utilise. Tout d’abord, ce type de mot clef est sensible aux majuscules. Par exemple, SSEEEP n’est pas égale à sSEeEP. Ensuite NUM, ne retournant aucune valeur, il peut être utilisé plusieurs fois dans un même mot clef.

Prenons l’exemple d’Elephant Dreams. Si le mot clef était sNUMeNUM, le résultat final serait exactement le même qu’au point 4. Néanmoins, aucune nouvelle méta-donnée seraient insérées. Cela peut donc être utile dans certains cas de figure où on trouverait des numéros inutiles. Il ne faut pas non plus abuser de ce genre de mots clefs. En voulant supprimer tous les numéros, on risquerait de ne plus avoir de titre (je pense au film "2012" par exemple).

Concernant SE et EP, ils ne doivent être utilisés qu’une seule fois par mot clef. Mais il y a qu’en même quelques libertés comme par exemple les mots clefs suivants sont parfaitement possibles: SeasonSE, EpisodeEP. Il n’est donc pas obligatoire d’avoir toujours SE et EP dans le même mot clef. Une autre considération concerne le nombre de saisons et d’épisodes pour un seul et même fichier. Imaginons un autre exemple complètement absurde:

"Mes Vacances (02x100) -s55e10-"

Si les mots clefs SExEP et sSEeEP sont présents dans la liste noir, le résultat final est donc bien:

"Mes Vacances"

Mais en plus il y a 4 nouvelles méta-données :

Add new metadata "season", value: "2"
Add new metadata "episode", value: "100"
Add new metadata "season", value: "55"
Add new metadata "episode", value: "10"

Pour terminer

Je ne tiens pas à donner des explications techniques sur le fonctionnement. Le gros du travail se fait via la magie de sscanf(). Le choix de NUM, SE et EP n’est pas non plus arbitraire. C’est uniquement par souci de commodité. En interne, le NUM est remplacé par %*u, les SE et EP sont remplacés par %u. Cela évite de devoir jouer l’accordéon avec les chaînes de caractères. Pour des détails, lisez le MAN de sscanf.

A bientôt,

Mathieu SCHROETER

h1

Un (tout petit) tour d’horizon

2009.11.23

Yop,

un tout petit tour d’horizon s’impose. Le travail sur (et autour de) Enna fait son petit bonhomme de chemin. Le design était sans cesse modifié mais depuis quelques semaines on a enfin quelque chose de relativement stable. Personnellement je n’ai pas beaucoup travaillé sur Enna car je continue à me concentrer sur libvalhalla. Il y a encore deux régressions de Enna qui sont dues à l’utilisation de libvalhalla.

Je parle de régressions, bien qu’il n’y a jamais eu de sortie officielle et stable. Il faut comprendre par là qu’avec toutes les modifications qui ont été apportées à Enna, de temps en temps des fonctionnalités disparaissent simplement parce qu’elle devraient être gérées d’une autre manière.

La première régression que je veux souligner est celle de pouvoir reprendre la lecture d’un film à l’endroit où il a été interrompu. Dit comme ça, il n’y a rien d’exceptionnel. Mais "à l’époque", l’enregistrement se faisait à l’aide de Eet qui était également utilisé pour stocker les méta-données des fichiers audio/vidéos. Depuis l’arrivée de libvalhalla et donc d’une base de donnée SQLite, Eet n’avait plus beaucoup de sens et cette partie a été simplement supprimée d’Enna. Mais la lib ne permet pas l’écriture depuis son API publique pour plusieurs raisons. Tout d’abord Valhalla ce n’est pas une bibliothèque pour gérer une base de donnée, mais c’est avant tout un scanner de fichiers, des "parsers" et des "grabbers". Les données sont récupérées par elle automatiquement. Il est donc possible de les lire depuis l’API publique. Lorsque des données doivent être modifiées, elles le sont depuis les fichiers (tags ID3 par exemple), ou alors depuis les site internets qui fournissent des informations (les grabbers, ImDB, Amazon, etc, …).

Mais pour répondre à certains besoins comme la sauvegarde de la position d’un film, il est nécessaire de reconsidérer cette question sur libvalhalla. J’ai donc commencé à travailler sur cet aspect il y a peu de temps. Il y aura ainsi une information qui permettra d’identifier des données venant de l’extérieur (parser, grabbers, …) et des données venant de l’intérieur. Pour mieux comprendre pourquoi il est nécessaire de faire des séparations, l’exemple d’un champ "playcount" est bien adapté. Le "playcount" (donc le nombre de fois qu’un fichier est lu), dépend uniquement de l’utilisateur d’Enna. Si le fichier en question est modifié (les tags par exemple), ce champ doit rester intacte ce qui n’est pas possible actuellement.

La deuxième régression concerne les "snapshots" (ou "fanarts"). Ils servent à avoir une image illustrant une video. Par exemple les images suivantes présentent les résultats pour quatre "trailers" où des données intéressantes ont pu être récupérées par les "grabbers" (j’ai récupéré les "trailers" sur le site d’Apple, merci au blogger en lien <ici> et <ici> pour l’astuce).


Dans tous ces exemples les images de fond ont pu être téléchargées sur le site TheMovieDB.org. Mais il peut arriver qu’aucune image ne soient disponibles et donc une alternative est nécessaire. Actuellement il y a une image de fond par défaut pour ces cas de figure, mais le but est de créer un "snapshot" directement avec le contenu de la vidéo. Cette fonctionnalité était disponible à l’époque où libvalhalla n’existait pas, et libplayer faisait office d’intermédiaire pour récupérer l’image à 20% de la vidéo. L’idée désormais est donc d’utiliser le "grabber" FFmpeg (ajouté il y a peu de temps) pour extraire l’image. Bien sûr, il ne faut pas l’extraire dans les cas où TheMovieDB contient déjà un "snapshot" de bien meilleur qualité.

Il y bien entendu toujours un panneau d’informations pour les vidéos. Il va encore probablement subir des modifications, et donc rien de ce qui est montré dans ces "screenshots" ne peut être considéré comme définitif.

Il y a encore d’autres choses qui ont évoluées aussi bien au niveau de l’audio que de la vidéo, mais mon objectif  pour ce billet est uniquement de parler des deux régressions. Du côté de libvalhalla, les "grabbers" tels que Amazon, Allocine et LyricWiki re-fonctionnent normalement (espérons que se sois pour encore longtemps). A part ça, une grande partie des modifications sont internes tel que de l’optimisation et des corrections de bugs par exemple.

A bientôt,

Mathieu SCHROETER

h1

Un bug particulier avec Enna/libplayer

2009.07.17

Hello,

comme je l’ai plus d’une fois expliqué, la vidéo sous Enna passe par libplayer. Et depuis quelque temps, Ben et Nico m’ont à nouveau sensibilisé sur un problème d’aspect avec les vidéos.
Le plus simple des aspects correspond à une stupide fraction:

\textbf{aspect}=\frac{\textbf{largeur}}{\textbf{hauteur}}

Mais celle-ci n’est pas toujours vraie, ainsi l’aspect peut être directement enregistré en tant que propriété dans le fichier vidéo. Un exemple courant étant les films DVD. La définition de l’image est de 720×576 (PAL) néanmoins l’aspect peut être du 4:3, 16:9 ou du cinémascope 2.39:1. Pourtant l’image est toujours enregistrée afin d’utiliser un maximum de surface et donc les pixels ne sont pas carrés. Considérer le pixel carré est le dernier recours au cas où la vidéo ne donnerait aucune information sur son aspect.

Le bug…

EnnaLe problème rencontré part Ben et Nico n’était pas reproductible chez moi. Et étant principalement responsable de libplayer depuis toutes les modifications que j’ai apporté, et donc connaissant de fond en comble sa structure et son fonctionnement actuel, il est forcément de mon devoir que de corriger les problèmes connus.
Quand ils utilisaient libplayer avec Enna, ils avaient certaines vidéos qui s’affichaient avec un aspect de 1.00. Ce qui engendre donc une image parfaitement carrée. S’ils testaient cette même vidéo avec le logiciel test-player qui utilise libplayer, l’aspect était correcte. Là où ça devenait encore plus étrange, c’est quand j’ai testé la même vidéo que Nico et j’avais un aspect correct aussi bien sous Enna que sous test-player. Je ne savais donc pas comment reproduire le bug, même que j’avais de plus en plus de doute sur l’existence de ce bug. Non pas que je n’ai pas confiance en leurs manipulations (surtout qu’ils sont deux à être victime de ce problème) mais plutôt que je fais toujours une grande quantité de testes lorsque j’ajoute ou modifie des fonctionnalités dans la bibliothèque. Les problèmes d’aspect je les ai particulièrement travaillé quand j’ai rajouté le support de la navigation DVD pour les wrappers MPlayer et xine. Car il y a des conversions de coordonnées à effectuer pour que la souris agissent au bon endroit sur la surface de l’image et cela en tenant compte de l’aspect et des offsets en x et y.

Nico m’a alors transmis les logs Enna lorsque le problème survient et j’ai pu identifier le problème indirectement. libplayer écrit dans ses logs certaines informations sur les modifications qu’il effectue sur la fenêtre vidéo. Il apparait que la ligne qui indique l’aspect de l’image utilisait une virgule pour séparer les décimales. Sur mon ordinateur cette même ligne ne s’écrit pas avec une virgule, mais avec un point.

Un problème de LOCALE

libplayer utilise abondamment la fonction atof() pour la conversion des valeurs réelles transmissent par MPlayer. Une de ces valeurs est l’aspect. MPlayer retourne la valeur dans un champ ID_VIDEO_ASPECT=1.85 par exemple. Et ce champ a toujours un point comme délimiteur dans le cas d’un MPlayer utilisé avec libplayer (MPlayer doit toujours être en anglais). Là où ça devient un problème c’est le comportement de la fonction atof(). En fonction de la LOCALE du système, elle considère que le délimiteur est une virgule et non un point. De ce fait, (et surtout parce que la fonction atof() ne peut pas échouer) l’aspect lu n’était pas 1.85, mais 1. atof() ignorait tous les caractères depuis le point.

Bien qu’étant de langue francophone, en Suisse-Romande nous n’avons pas du tout la même LOCALE que la France. Notre clavier est également différent (disposition QWERTZ). Nous utilisons le point comme séparateur de décimale et non la virgule et la LOCALE est "fr_CH". En France, la LOCALE est "fr_FR" et le séparateur de décimale est une virgule. Ceci explique pourquoi je ne pouvais pas reproduire l’erreur. Qui pense aux LOCALEs quand il y a un bug sur l’aspect des vidéos? Et bien je tâcherais d’y penser à partir de maintenant.
Quoi qu’il en soit, de nombreux français devaient avoir des problème, mais je n’ai pas eu d’autres retours que les autres développeurs. Je suppose que les éventuels utilisateurs d’Enna se sont dit qu’étant donné qu’aucune version stable existe, ça finirait par être fixé pour la finale? Sachez qu’il y a un site où vous pouvez rapporter les problèmes: enna.geexbox.org, vérifiez si le bug est déjà listé et dans le cas contraire n’hésitez pas à vous inscrire et à soumettre un rapport de bug.

Correction

Pour y remédier il suffit donc de changer de LOCALE avec libplayer. Mais il n’y a pas de raison apparente de changer la LOCALE pour toute la bibliothèque. Ainsi il suffit de réécrire une fonction atof() qui s’exécute dans une autre LOCALE. En principe un programme utilise la LOCALE "C". Qui est celle par défaut. Enna étant internationalisé, la LOCALE change selon la langue. Ceci explique donc pourquoi le bug apparaissait sous Enna mais pas sous test-player.

En principe, la LOCALE se change avec la fonction setlocale(). Mais avant de dire youpie il faut garder à l’esprit que libplayer utilise abondamment les threads. Lorsque la fonction atof() est appelée, Enna peut être en train de faire autre chose. Et cet autre chose peut très bien être influencé par la LOCALE en cours. Le fait d’utiliser la fonction setlocale() modifie la LOCALE pour tout le processus.
Il est donc nécessaire d’avoir recours à une fonction qui ne modifie la LOCALE que sur le thread où atof() est exécuté. Cette fonction s’appelle uselocale(), le nouveau atof() se présente donc ainsi (patch libplayer):

#define _GNU_SOURCE
#include <locale.h>
#include <stdlib.h>

double
my_atof (const char *nptr)
{
  double res;
  locale_t new_locale, prev_locale;

  new_locale = newlocale (LC_NUMERIC_MASK, "C", NULL);
  prev_locale = uselocale (new_locale);
  res = atof (nptr);
  uselocale (prev_locale);
  freelocale (new_locale);

  return res;
}

Afin de simplifier la fonction et d’éviter atof() qui a une réputation d’obsolescence je l’ai remplacé par strtod_l(). Du même coup il est possible d’éliminer uselocale() et donc de ne plus changer la LOCALE du thread.

#define _GNU_SOURCE
#include <locale.h>
#include <stdlib.h>

double
my_atof (const char *nptr)
{
  double res;
  locale_t new_locale;

  new_locale = newlocale (LC_NUMERIC_MASK, "C", NULL);
  res = strtod_l (nptr, NULL, new_locale);
  freelocale (new_locale);

  return res;
}

On notera que strtod() peut retourner HUGE_VAL contrairement à atof().

A bientôt,

Mathieu SCHROETER

h1

Valhalla: les nouvelles fonctions de sélection

2009.05.31

Hello,

depuis peu, les modifications évoquées dans le billet précédent ont été réalisées. L’API a été modifiée pour toutes les fonctions qui permettent d’effectuer des sélections sur la base de donnée. Ce blog étant aussi en quelque sorte un carnet de laboratoire, je vais donc y expliquer le fonctionnement de ces fonctions ainsi que la manière de les utiliser. A terme, ces informations figureront directement dans la documentation (en anglais) de libvalhalla. Il est judicieux de consulter la documentation Doxygen déjà présente dans l’en-tête public (valhalla.h) qui peut être généré via un paramètre du script "configure".

Cet article est long et très spécifique à libvalhalla du côté développeur uniquement. Si vous êtes un simple utilisateur d’Enna, vous ne verrez pas vraiment de différence par rapport à avant car les modifications sont majoritairement internes.

Il y a trois fonctions publiques et chacune est complémentaire aux autres.

int valhalla_db_metalist_get (
  valhalla_t *handle,
  valhalla_db_item_t *search,
  valhalla_db_restrict_t *restriction,
  int (*result_cb) (void *data,
                    valhalla_db_metares_t *res),
  void *data);

int valhalla_db_filelist_get (
  valhalla_t *handle,
  valhalla_file_type_t filetype,
  valhalla_db_restrict_t *restriction,
  int (*result_cb) (void *data,
                    valhalla_db_fileres_t *res),
  void *data);

int valhalla_db_file_get (
  valhalla_t *handle,
  int64_t id, const char *path,
  valhalla_db_restrict_t *restriction,
  valhalla_db_filemeta_t **res);

bird

valhalla_db_metalist_get

Utilisation simple

Elle permet de récupérer une liste de metadata. Cela veut dire, par exemple une liste d’albums, d’auteurs, d’artistes, de genres; peu importe le nom, vous pouvez récupérer n’importe quelle liste. Le résultat se présente toujours sous la forme du nom du champ (comme "author") avec une valeur et un groupe. Sous la forme d’un tableau, cela donnerait quelque chose comme:

meta id meta name data id data value group
2 author 1665 Dido ENTITIES
2 author 456 Ennio Morricone ENTITIES
2 author 1257 John Williams ENTITIES
2 author 1224 Linkin Park ENTITIES

Dans cet exemple, uniquement les auteurs sont présents, mais il est également possible de demander une liste de metadata selon un groupe et non selon un nom de meta ou un id. Vous pouvez typiquement demander la liste de toutes les metadata qui appartiennent au group ENTITIES. De ce fait, vous trouverez la liste de tous les auteurs, artistes, compositeurs, studios, etc,…

Sans rentrer encore dans les détails du code, une telle liste se récupère de cette manière:

valhalla_db_item_t s = VALHALLA_DB_SEARCH_TEXT ("author", ENTITIES);
valhalla_db_metalist_get (handle, &s, NULL, callback, NULL);

Et sans expliquer en détail la fonction, l’argument "handle" correspond au pointeur sur votre instance de valhalla, "s"  (pour search) défini ce que vous cherchez, le troisième argument permet de spécifier des restrictions (qui sont expliquées dans le chapitre suivant), le callback est la fonction dans laquelle chaque ligne du résultat est envoyée et le dernier argument est un pointeur void qui peut être récupéré dans le callback.

Les macros  VALHALLA_DB_SEARCH_XXX sont présentes pour simplifier l’utilisation de libvalhalla. Elles ne sont pas indispensables. En réalité, elle ne font qu’attribuer les valeurs au bon endroit dans la structure valhalla_db_item_t. Actuellement il existe trois macros différentes:

VALHALLA_DB_SEARCH_ID(meta_id, group)
VALHALLA_DB_SEARCH_TEXT(meta_text, group)
VALHALLA_DB_SEARCH_GRP(group)

Vous pouvez donc chercher une liste de meta en fonction de l’id, du texte, ou alors vous pouvez récupérer plusieurs noms de metadata différents dans le cas d’une recherche selon un groupe.

Utilisation étendue

Récupérer une liste comme expliqué dans le chapitre précédent est la fonctionnalité la plus simple offerte par cette fonction. Il va de soit qu’il faut pouvoir donner quelques restrictions sur la liste retournée. Il est courant de ne pas vouloir une liste complète. Un exemple parlant concerne la metadata "album". Il est intéressant de pouvoir récupérer tous les albums d’un auteur en particulier. Et c’est ici qu’interviennent les restrictions. Elles permettent d’inclure ou d’exclure certains résultats. D’un point de vue SQL, se sont des sous-requêtes qui limitent les résultats à leur ensemble, ou alors à tous les résultats différents de leur ensemble.

Voici un premier exemple. Vous voulez tous les albums de Dido:

valhalla_db_item_t s = VALHALLA_DB_SEARCH_TEXT ("album", TITLES);
valhalla_db_restrict_t r1 = VALHALLA_DB_RESTRICT_STR (IN, "author", "Dido");
valhalla_db_metalist_get (handle, &s, &r1, callback, NULL);

Le résultat sera un tableau de la même forme que vue précédemment, mais qui contiendra uniquement les albums de Dido.

meta id meta name data id data value group
3 album 1666 Life For Rent TITLES
3 album 1669 No Angel TITLES
3 album 1725 Safe Trip Home TITLES

Cette requête est déjà bien plus sophistiquée, car vous pouvez imaginer de donner ce genre de restriction pour n’importe quel metadata. Un autre exemple, vous pourriez lister tous les albums d’une certaine année. Il n’y a aucune limite sur les choix des restrictions. Mais ceci n’est pas suffisant, et il y a donc des possibilités un peu plus complexes afin d’avoir des résultats plus spécifiques.

Tout d’abord, dans l’exemple précédent, la macro VALHALLA_DB_RESTRICT est utilisée avec un premier argument "IN". Il est possible d’inverser la logique en y inscrivant "NOTIN" à la place. Le résultat deviendrait alors la liste de tous les albums qui existent dans la base de donnée, sauf ceux de Dido.

Utilisation avancée

L’utilisation étendue permet déjà d’effectuer de nombreuse recherches intéressantes. Néanmoins pour les cas les plus précis, elle ne suffit pas. Il est nécessaire parfois de cumuler les restrictions pour affiner le résultats, ou tout simplement, pour avoir des résultats un peu plus exotiques. Ainsi les restrictions sont des listes chaînées, et il est possible de les lier très facilement. Commençons par un exemple, nous désirons la liste de tous les albums de Dido qui ne sont pas de 2003.

valhalla_db_item_t s = VALHALLA_DB_SEARCH_TEXT ("album", TITLES);
valhalla_db_restrict_t r1 = VALHALLA_DB_RESTRICT_STR (IN, "author", "Dido");
valhalla_db_restrict_t r2 = VALHALLA_DB_RESTRICT_STR (NOTIN, "year", "2003");
VALHALLA_DB_RESTRICT_LINK (r2, r1);
valhalla_db_metalist_get (handle, &s, &r1, callback, NULL);

Une nouvelle macro est donc introduite. Elle permet de lier très simplement les restrictions ensemble. La macro doit être comprise de cette manière. "link from r2 to r1", soit lier de r2 à r1. Ainsi c’est r1 qui doit être spécifié dans la fonction valhalla_db_metalist_get(). Il n’y a aucune limite sur le nombre de liens possibles (mise à part qu’une requête ne doit pas dépasser 4096 caractères), néanmoins il ne faut pas tenter de lier toutes les restrictions sur la même car seul le dernier lien sera considéré. Avec trois restrictions, la bonne manière de procéder est donc:

valhalla_db_item_t s = VALHALLA_DB_SEARCH_TEXT ("album", TITLES);
valhalla_db_restrict_t r1 = VALHALLA_DB_RESTRICT_STR (IN, "author", "Linkin Park");
valhalla_db_restrict_t r2 = VALHALLA_DB_RESTRICT_STR (NOTIN, "year", "2003");
valhalla_db_restrict_t r3 = VALHALLA_DB_RESTRICT_STR (NOTIN, "genre", "Alternative");
VALHALLA_DB_RESTRICT_LINK (r3, r2);
VALHALLA_DB_RESTRICT_LINK (r2, r1);
valhalla_db_metalist_get (handle, &s, &r1, callback, NULL);

Ici, la liste concernera donc tous les albums de Linkin Park, qui ne sont pas de 2003 et qui ne sont pas du rock/metal alternatif. Okay, la liste ne risque pas d’être bien longue avec cet exemple :-P, mais ce qui compte c’est de montrer le principe. Les restrictions sont liées de la manière suivante: de r3 à r2 et de r2 à r1. En code ça donnerait donc:

r2.next = &r3;
r1.next = &r2;

Le code ci-dessus peut très bien remplacer la macro si vous préférez.

Il y a encore une macro que je n’ai pas vraiment présenté. Pour l’argument de recherche de la fonction, il est possible d’indiquer uniquement un groupe (comme je l’ai mentionné plus haut dans ce billet). Si le groupe est mit à NIL, il y aura toutes les metadata qui seront listées, autrement il n’y aura que les metadata liées au groupe.

Un dernier exemple qui retourne absolument toutes les metadata liées à Dido pour son album Life For Rent.

valhalla_db_item_t s = VALHALLA_DB_SEARCH_GRP (NIL);
valhalla_db_restrict_t r1 = VALHALLA_DB_RESTRICT_STR (IN, "author", "Dido");
valhalla_db_restrict_t r2 = VALHALLA_DB_RESTRICT_STR (IN, "album", "Life For Rent");
VALHALLA_DB_RESTRICT_LINK (r2, r1);
valhalla_db_metalist_get (handle, &s, &r1, callback, NULL);
meta id meta name data id data value group
6 track 19 1 ORGANIZATIONAL
6 track 225 10 ORGANIZATIONAL
6 track 229 11 ORGANIZATIONAL
6 track 242 12 ORGANIZATIONAL
6 track 92 2 ORGANIZATIONAL
5 year 50 2003 TEMPORAL
6 track 202 3 ORGANIZATIONAL
6 track 101 4 ORGANIZATIONAL
6 track 223 5 ORGANIZATIONAL
6 track 96 6 ORGANIZATIONAL
6 track 231 7 ORGANIZATIONAL
6 track 196 8 ORGANIZATIONAL
6 track 187 9 ORGANIZATIONAL
1 title 1673 BonusTrack TITLES
2 author 1665 Dido ENTITIES
1 title 1668 Do You Have A Little Time TITLES
1 title 1672 Dont Leave Home TITLES
1 title 1666 Life For Rent TITLES
3 album 1666 Life For Rent TITLES
1 title 1678 Marys In India TITLES
4 genre 104 Pop CLASSIFICATION
1 title 1670 Sand In My Shoes TITLES
1 title 1669 See The Sun TITLES
1 title 1674 See You When Youre 40 TITLES
1 title 1675 Stoned TITLES
1 title 1671 This Land Is Mine TITLES
1 title 1664 White Flag TITLES
1 title 1667 Who Makes You Feel TITLES

Et d’un point de vue SQL

La requête SQL qui effectue les sélections est créée en fonction des arguments de la fonction. Elle se construit comme des lego qui s’imbriquent les uns dans les autres. D’une manière général, sa forme est:

SELECT meta.meta_id, data.data_id, meta.meta_name, data.data_value
FROM (
 data INNER JOIN assoc_file_metadata AS assoc
 ON data.data_id = assoc.data_id
) INNER JOIN meta
ON assoc.meta_id = meta.meta_id
-- Les conditions sont optionnelles
WHERE
  -- Une sous-requête apparait par le biais des restrictions.
  -- Il y a autant de sous-requêtes que de restrictions du type IN, NOT IN.
  assoc.file_id <IN|NOT IN> (
    SELECT assoc.file_id
    FROM (
      data INNER JOIN assoc_file_metadata AS assoc
      ON data.data_id = assoc.data_id
    ) INNER JOIN meta
    ON assoc.meta_id = meta.meta_id
    -- A choix, il peut y avoir un ID ou du texte.
    WHERE meta.<meta_id|meta_name> = <ID|"TEXT"> AND data.<data_id|data_value> = <ID|"TEXT">
  )
  -- L'argument search indique quel meta et/ou quel groupe doit être listé
  AND meta.<meta_id|meta_name> = <ID|"TEXT">
  AND assoc._grp_id = <ID>
GROUP BY assoc.meta_id, assoc.data_id
ORDER BY data.data_value;

Sa longueur varie fortement selon le nombre de conditions. Il est possible de visualiser les requêtes générées par Valhalla en utilisant la verbosité maximale de la bibliothèque.

bird

valhalla_db_filelist_get

Utilisation simple

La récupération d’une liste de fichiers ressemble à celle pour les metadata. La différence vient principalement du deuxième argument qui fait référence au type de fichier (audio, video, etc, …). Ce qui limite ensuite la liste se sont les restrictions. L’utilisation étant exactement la même, je vais aller droit au but avec des exemples.

Pour lister la totalité des fichiers référencés dans la base de donnée, rien de plus simple:

valhalla_db_filelist_get (handle, VALHALLA_FILE_TYPE_NULL, NULL, callback, NULL);

Le fait d’indiquer un type de fichier NULL, permet d’ignorer le type. Le résultat de cette requête correspond à un tableau de ce genre qui contiendrait la totalité de l’arborescence:

id path filetype
101 /home/foo/bar/dido/life_for_rent/sand_in_my_shoes.m4a AUDIO
102 /home/foo/bar/dido/life_for_rent/white_flag.m4a AUDIO
103 /home/foo/bar/dido/life_for_rent/who_makes_you_feel.m4a AUDIO

Avec les restrictions

Il est ensuite facile de faire du tri avec les restrictions, de la même manière que pour la fonction précédente. Par exemple, pour lister tous les fichiers d’Ennio Morricone qui ne sont référencés dans aucun album.

valhalla_db_restrict_t r1 = VALHALLA_DB_RESTRICT_STR (IN, "author", "Ennio Morricone");
valhalla_db_restrict_t r2 = VALHALLA_DB_RESTRICT_STR (NOTIN, "album", NULL);
VALHALLA_DB_RESTRICT_LINK (r2, r1);
valhalla_db_filelist_get (handle, VALHALLA_FILE_TYPE_NULL, &r1, callback, NULL);

Et ci-dessous, un autre exemple qui correspond à la catégorie "Non Classé" dans Enna. Tous les fichiers qui ne sont ni dans un album, ni lié à un author et dont le genre est indéfini.

valhalla_db_restrict_t r1 = VALHALLA_DB_RESTRICT_STR (NOTIN, "album", NULL);
valhalla_db_restrict_t r2 = VALHALLA_DB_RESTRICT_STR (NOTIN, "author", NULL);
valhalla_db_restrict_t r3 = VALHALLA_DB_RESTRICT_STR (NOTIN, "genre", NULL);
VALHALLA_DB_RESTRICT_LINK (r3, r2);
VALHALLA_DB_RESTRICT_LINK (r2, r1);
valhalla_db_filelist_get (handle, VALHALLA_FILE_TYPE_NULL, &r1, callback, NULL);

Facile, non?

A noter que ce n’est pas encore totalement optimal, car le groupe ne peut pas être donné avec les restrictions. Ainsi n’importe quel metadata de n’importe quel groupe est considérée dans l’inclusion comme l’exclusion.

Et d’un point de vue SQL

La aussi, la requête ressemble beaucoup à la précédente avec des différences au niveau de ce qui est sélectionné et dans quelle table.

SELECT file_id, file_path, _type_id
FROM file AS assoc
-- Les conditions sont optionnelles
WHERE
  -- Une sous-requête apparait par le biais des restrictions.
  -- Il y a autant de sous-requêtes que de restrictions du type IN, NOT IN.
  assoc.file_id <IN|NOT IN> (
    SELECT assoc.file_id
    FROM (
      data INNER JOIN assoc_file_metadata AS assoc
      ON data.data_id = assoc.data_id
    ) INNER JOIN meta
    ON assoc.meta_id = meta.meta_id
    -- A choix, il peut y avoir un ID ou du texte.
    WHERE meta.<meta_id|meta_name> = <ID|"TEXT"> AND data.<data_id|data_value> = <ID|"TEXT">
  )
  -- Le type n'est pas inscrit dans la requête si la valeur est NULL.
  AND _type_id = <ID>
ORDER BY file_id;

Si dans la clause FROM, la table "file" créer un alias "assoc", c’est uniquement pour une question de factorisation dans le code par rapport à la sous-requête.

bird

valhalla_db_file_get

Maintenant que l’on a vu comment récupérer des listes de metadata ainsi que des listes de fichiers, il est important de pouvoir aussi récupérer des metadata en fonction d’un fichier spécifique. Cette possibilité n’existait pas avec l’ancien modèle car les fichiers étaient toujours retournés dans une liste. Ici il est question d’un seul fichier à la fois. Il faut également être conscient que cette fonction est liée uniquement à la base de donnée, si un fichier non référencé dans la base mais existant quelque part est donné en argument, aucun résultat ne sera retourné (cette possibilité existera quand j’implémenterai le "scan on-demand").

Les arguments sont donc l’id du fichier, le chemin du fichier, des restrictions et le pointeur sur la structure de résultat. Il n’y a pas de callback pour l’utilisateur car le résultat est limité à un seul fichier (en réalité il y a un callback interne qui récupère toutes les metadata car le vrai résultat est sur plusieurs lignes).

Les metadata sont disponibles dans une structure chaînée. Je dois également introduire une nouvelle possibilité des restrictions. Mais tout d’abord, voici un exemple:

valhalla_db_filemeta_t *metadata = NULL;
valhalla_db_restrict_t r1 = VALHALLA_DB_RESTRICT_STR (EQUAL, "track", NULL);
valhalla_db_restrict_t r2 = VALHALLA_DB_RESTRICT_STR (EQUAL, "title", NULL);
VALHALLA_DB_RESTRICT_LINK (r2, r1);
valhalla_db_file_get (handle, 0, "/home/foo/bar/dido/life_for_rent/white_flag.m4a", &r1, &metadata);
/* ... */
VALHALLA_DB_FILEMETA_FREE (metadata);

L’argument EQUAL dans la macro VALHALLA_DB_RESTRICT sert à limiter les résultats à des metadata précises. Sans les restrictions, la structure metadata contiendrait absolument toutes les metadata. Mais dans cet exemple, la structure contient uniquement le track et le titre. A noter qu’utiliser IN ou NOTIN ici ça n’a aucun sens. De même qu’utiliser EQUAL pour les deux fonctions précédentes (voir la conclusion).

Il faut être conscient que plusieurs tracks et plusieurs titres peuvent être retournés pour un même fichier. Car rien n’exclus que certains noms de metadata n’existent pas dans différents groupes. Et comme je l’ai dis plus haut, il n’est pour le moment pas possible de limiter une restriction à un groupe. Néanmoins le groupe est un champ de la structure valhalla_db_filemeta_t, il est donc possible de faire un tri à postériori.

Après utilisation des metadata, il ne faut pas oublier de les libérer avec VALHALLA_DB_FILEMETA_FREE().

Et d’un point de vue SQL

SELECT file.file_id, assoc._grp_id, meta.meta_id, data.data_id, meta.meta_name, data.data_value
FROM ((
    file INNER JOIN assoc_file_metadata AS assoc
    ON file.file_id = assoc.file_id
  ) INNER JOIN data
  ON data.data_id = assoc.data_id
) INNER JOIN meta
ON assoc.meta_id = meta.meta_id
-- Les conditions sont optionnelles
WHERE
  -- Une condition apparait par le biais des restrictions.
  -- Il y a autant de conditions que de restrictions du type EQUAL.
  -- A choix, il peut y avoir un ID ou du texte. Chaque restriction
  -- est séparée par un OR.
  (
    (meta.<meta_id|meta_name> = <ID|"TEXT"> AND data.<data_id|data_value> = <ID|"TEXT">)
    <OR> ...
  )
  -- Il faut l'id d'un fichier ou un path
  AND file.<file_id|file_path> = <ID|"PATH">
;

Cette requête retourne autant de lignes qu’il y a de metadata. Mais pour l’utilisateur, tout est transparent car les résultats sont tous retournés dans une structure chaînée.

En conclusion

Les possibilités de ce nouveau système sont très larges par rapport à l’ancien. Bien qu’il y ait encore quelques manques pour pouvoir spécifier des groupes avec les restrictions, il est possible de retrouver les données avec peu de lignes de code et simplement, tout en ayant des requêtes relativement complexes qui se créer en interne. Il y aura forcément des modifications à l’avenir, je tâcherais de garder à jour cet article.

Et pour en revenir aux restrictions et à l’argument qui peut prendre la valeur IN, NOTIN ou EQUAL, il faut comprendre que c’est avant tout une question de factorisation en interne. IN et NOTIN devraient être utilisés uniquement avec les deux premières fonctions présentées qui récupèrent des listes, et EQUAL devrait être utilisé uniquement pour la récupération des metadata d’un fichier. Je vais continuer à travailler sur ces éléments dans les jours à venir afin d’éviter des confusions.

A bientôt,

Mathieu SCHROETER

h1

Quoi de neuf dans la Valhalla

2009.05.09

OdinAu pays du dieu Odin, il est temps de ne plus limiter l’entrée de la Valhalla uniquement aux guerriers valeureux. Mais soyons fous et acceptons tout le monde, et peu importe leurs caractéristiques (cf. Valhalla Scanner).

Je travail ainsi à la refonte complète de la structure de la base de donnée. Pour rappel, le problème majeur vient du manque de souplesse à l’insertion des méta-données. Il n’est pas possible d’y insérer des informations autres que le titre, l’année, le numéro de piste, l’auteur, l’album et le genre. De nombreux fichiers offrants bien d’autres informations il est donc indispensable de revoir le modèle complètement. Mais qui dit nouveau modèle, dit aussi nouveau code et nouvelle API. La conséquence de cette réécriture provoquera indéniablement une fracture avec le module actuellement présent dans Enna et avec les bases de données générées par le système actuel. En date de cet article, je n’ai pas encore écris les modifications mais j’ai néanmoins fais les réflexions nécessaires. Et se sont ces réflexions que je vais partager ici "en partie :-)".

Nouveau modèle

L’idée est de se détacher des noms donnés au méta-données. Tel que "genre", "author" et "album" par exemple. Ce ne sont finalement que des noms et devraient donc être traités comme de simple donnée dans la base de donnée. Il n’y aura donc plus de table nommée "genre", etc. Mais une table regroupant les noms, et une seconde table regroupant les valeurs et gardant indirectement une relation entre le nom et la valeur.

Un tel système permet d’insérer une infinité de noms différents de méta-données et d’éviter en même temps les duplications. Il est important d’assurer un minimum de redondance. Si un genre est inséré, il ne faudrait pas qu’il soit réinséré trois fois si trois nouvelles données nommées "genre" seraient envoyées par le scanner. De même pour les valeurs, la valeur "rock" par exemple devrait apparaitre qu’une seule fois dans la base de donnée. Ainsi tous les fichiers qui feraient référence à "genre"->"rock" seraient tous liés à la même information. Ce qui est le cas avec l’ancien modèle, et il est important de conserver cette caractéristique pour une question de bon-sens et de qualité.

Mais rendre le modèle complètement générique apporte un nouveau problème. Il devient difficile de retrouver les informations qui concernent un même thème. Imaginez un fichier qui utilise le nom "genre" et un autre type de fichier qui utilise le nom "style". Peut être que ces deux fichiers sont de la musique et que les deux contiennent la valeur "rock". Mais une fois nous avons "genre"->"rock" et une fois nous avons "style"->"rock".
Pour y remédier, il peut être judicieux de créer une troisième table qui permettrait de regrouper les paires nom->valeur sous des noms d’ensemble. Cette table se nommerait donc "group" et contiendrait une liste (pouvant être étendue si nécessaire) de différents groupes possibles. Voici une première liste de groupes (qui va encore évoluer):

Miscellaneous, Entity, Title, Classification, Personal, Temporal, Spacial, Commercial, Legal, Technical, Identifier, Musical, Contact, Organizational

Typiquement, par rapport à l’exemple précédent pour "genre" et "style", il serait judicieux de regrouper ces paires dans le groupe "Classification". Ainsi il serait facile de ressortir toutes les paires par groupe pour créer des listes de genre et de style en étant sûr que le nom "genre" ne fait pas référence à autre chose. On pourrait très bien imaginer que pour certains formats de fichier, ce soit des noms français et que ça ferait référence au genre (sexe) et non au style de musique. Il faudrait donc pouvoir assigner un autre groupe pour ces particularités. Ainsi, lors de l’insertion des méta-données d’un fichier, il sera obligatoire de chaque fois spécifier le groupe. Mais dans les cas "inconnu", il sera toujours possible d’utiliser le groupe "Miscellaneous".

Ça sera donc aux parsers de dire quel paire va dans quel groupe. Cela peut paraître un peu contraignant, car ça demande quelques réflexions sur le fonctionnement des parsers. Mais afin de pouvoir récupérer de la manière la plus exhaustives possible des informations depuis l’extérieur (comme Enna), il est indispensable de les regrouper correctement. Avec l’ancien système, ce n’est pas possible et c’est donc un plus que j’estime intéressant. Sans oublier que le nom des méta-données pourra également être récupéré depuis l’extérieur, et donc un tri pourra se faire sur les "genre" et sur les "style", etc,.. si nécessaire. Et bien sûr, il sera qu’en même possible de récupérer tous les "genre" sans tenir compte des groupes.
(Quand je parle de "l’extérieur", je parle donc des accès effectués depuis l’API et des valeurs récupérées. Je parle également toujours du genre, mais c’est un exemple.. l’idée est la même pour toutes les méta-données.)

Diagramme Entité/Relation

Le diagramme E/R ci-dessous résume assez bien à quoi va ressembler la base de donnée. Il se peut qu’il évolue encore, mais la forme générale (les relations) ne devraient pas trop bouger.

Valhalla E/R

D’habitude je préfère travailler avec des diagrammes MERISE, mais le logiciel Dia est mieux adapté pour du E/R. Pour les non-adeptes des E/R, le rectangle/losange "is defined by" correspond à une table d’allocation ayant pour clef primaire (file_id, meta_id et data_id).

But final

Cette petite analyse du modèle est un premier pas vers une future modification de Valhalla, dans laquelle il sera possible d’y introduire directement les grabbers d’Enna (les grabbers sont les modules qui permettent de récupérer des informations et des images via Allocine, IMdB, etc,..).

Il y a quelques indications dans ce document (voir aussi ici), mais j’en parlerais plus en détail dans un nouvel article, dès que j’en aurais terminé avec le nouveau modèle et son implémentation.

A bientôt,

Mathieu SCHROETER

h1

Valhalla Scanner

2009.03.14

Hello,

Un petit nouveau a fait son entrée dans les dépôts de GeeXboX. C’est un projet qui est né suite à quelques expériences négatives avec une autre bibliothèque. Elle m’avait alors motivé à en écrire une afin d’y apporter une vision un peu différente. Le but de Valhalla (ou libvalhalla), est très simple, elle va scanner des emplacements sur votre disque dur (par exemple) afin d’y trouver des fichiers audio/video pour les référencer dans une base de données. Il est alors facile ensuite d’y effectuer des requêtes permettant de générer des listes d’artistes, d’albums, etc,…

Mais tout d’abord.. pourquoi Valhalla?

Dans la mythologie nordique, la Valhöll (ou Walhalla, Valhalla, Valhalle), demeure-des-occis, est le lieu où les guerriers valeureux sont amenés. C’est sur les champs de bataille que des vierges guerrières (pour les celts) ou des Valkyries (pour les germains), cherchent et récupèrent les hommes les plus braves et les plus valeureux afin de les ramener dans Ásgard, où Odin les attend pour les préparer à la bataille finale, le Ragnarök.
Dans la Valhöll(palais de Thor), les guerriers alors nommés Einherjar sont heureux : ils combattent, se tuent, renaissent pour encore se pourfendre dans un champ clos.

Valhöll. (2009, mars 10). Wikipédia, l’encyclopédie libre. Page consultée le 17:42, mars 12, 2009 à partir de http://fr.wikipedia.org/w/index.php?title=Valh%C3%B6ll&oldid=38802776.

Je cherchais un nom qui sort de l’ordinaire tout en gardant un sens par rapport à la fonctionnalité de cette bibliothèque. Si la définition de La Valhalla ci-dessus ne vous a pas convaincu, imaginez que les scanners sont les Valkyries, que les braves sont les fichiers audio/video, et qu’elles les emmènent à la Valhalla (dans la base de donnée). Ceux-ci mourant et renaissant sans cesse dans un champ clos (les fichiers pouvant être rescannés en boucle afin d’y repérer les modifications).

(A noter que Valgrind est la porte qui mène à La Valhalla)

Fonctionnement

SQLite

Small. Fast. Reliable.

Valhalla se base sur deux bibliothèques principales. Tout d’abord il y a la libavformat (de FFmpeg) qui permet de récupérer les méta-données, puis SQLite qui s’occupe de gérer la base de donnée. Si ces libs ont été choisies c’est pour de bonnes raisons. Comme je l’ai dis plus haut, libvalhalla est née suit à des expériences avec une autre lib. En premier lieu, cela concerne le moyen pour récupérer les méta-données. Sans rentrer dans des détails inutiles, pour se faire il est nécessaire de passer par des démuxeurs et en créer peut avoir un avantage certain sur la rapidité. Néanmoins beaucoup de travail serait nécessaire pour gérer la plupart des formats de fichier, et c’est ici qu’intervient FFmpeg. Cette bibliothèque étant la plus aboutie, elle supporte énormément de formats. Il est donc facile de récupérer les méta-données sans se soucier du "comment". La deuxième raison est purement technique. Certains aspects dans la construction de l’autre lib ne me plaisaient pas (à tord ou à raison).
Concernant SQLite, c’est un excellent outil (et très léger et rapide) de gestion de base de données.

Architecture

Avant d’aborder l’architecture en elle-même j’aimerais expliquer mes choix. Tout d’abord Valhalla repose sur les threads. Parce que j’aime travailler avec les threads même si de nombreuses personnes estiment que les "threads are evil". Ensuite il y a une raison liée à l’aspect pratique et à l’aspect vitesse. D’un point de vue pratique, les threads sont un moyen facile de créer des tâches parallèles et aussi de créer des tâches de fond. Il est intéressant dans le cas d’Enna, de pouvoir l’utiliser sans être bloqué par Valhalla.

J’entends certaines personnes me parler des fork(), alors oui c’est également un moyen.. mais franchement, communiquer entre des sous-processus c’est pénible et nettement moins efficace que de le faire directement entre deux threads qui partagent le même espace mémoire. Les fork() doivent être utilisés si le contexte le demande. Tel que c’est le cas dans libplayer pour travailler avec MPlayer.

L’aspect vitesse est également intéressant car aujourd’hui il est courant de trouver des ordinateurs avec au moins 2 cores. Alors quitte à faire travailler les démuxeurs sur les deux (ou plus), je vous montrerais des petites statistiques plus loin.

La figure ci-dessous présente une vision simplifiée de l’architecture.

Valhalla Internals

Il y a trois parties principales. En vert, le scanner s’occupe de chercher tous les fichiers qui respectent certains critères relatifs à leur nom. Puis chaque référence aux fichiers est envoyée à la partie jaune (Database) qui va vérifier si le fichier existe déjà dans la base de donnée ou s’il est nouveau. Si des modifications sont nécessaires, alors il est envoyé à la partie orange (Parser) afin que les méta-données soient récupérées. Le parser renvoit les données à la Database qui va alors insérer les modifications adéquates dans la base de données à travers SQLite. Chaque bloc représenté dans la figure est un thread. La partie orange peut contenir plusieurs parsers et donc plusieurs threads.

FFmpeg

FFmpeg

A complete, cross-platform solution for audio and video.

Les parsers utilisent la bibliothèque libavformat d’FFmpeg pour réaliser l’extraction des méta-données. Il y a cependant un problème de vitesse. libavformat fait appel à des fonctions de "probe" afin d’identifier le format du fichier pour lui assigner le bon démuxeur. Et dans certains cas (fichiers altérés?), le "probe" prend beaucoup de temps car il y a beaucoup de démuxeurs et qu’il est parfois nécessaire de tester plusieurs fois un même fichier (en avançant dans les données) avant que la lib puisse deviner lequel est le bon.

L’idée est donc de proposer un format directement à libavformat au lieu de le laisser procéder tout seul à la détection. Le moyen utilisé est très simple et efficace. Il est rare que le suffixe d’un fichier ne correspond pas au bon format. La première étape est donc de récupérer le nom du format qui est lié au suffixe (à l’extension). Typiquement, si les fichiers sont des .mp3, le nom est tout simplement "mp3". Voir les détails dans les sources.

name = suffix_fmt_guess (file);
if (name)
  fmt = av_find_input_format (name);

La deuxième étape est un petit peu plus compliquée. Bien que l’on a désormais une idée sur le démuxeur, il est exclus de l’utiliser tel quel. Si un format est forcé à l’ouverture du fichier, aucun test n’est réalisé par libavformat. Ainsi, une fonction de probe() a été écrite pour tester le démuxeur. L’avantage cette fois vient du fait qu’un seul démuxeur est testé, et non plus tous les démuxeurs de libavformat. Le gain en vitesse est impressionnant (j’ai constaté un gain de plus d’un facteur dix avec cette technique et un peu plus de 1000 MP3/OGG).

if (fmt)
{
  int score = parser_probe (fmt, file);
  if (!score) /* Bad score? */
    fmt = NULL;
}

Si vous êtes intéressé par la fonction parser_probe(), je vous invite à directement la consulter dans les sources. En résumé, la fonction est inspirée de libavformat/utils.c::av_open_input_file(), et dépend fortement de l’implémentation interne de libavformat en ce qui concerne les scores retournés par les fonctions de probe().

Les priorités

Si vous désirez définir des priorités non temps réelles à des threads, voici l’astuce:

#include <unistd.h>
#include <sys/resource.h>
#include <sys/syscall.h>

static inline void
my_setpriority (int prio)
{
  pid_t pid = syscall (__NR_gettid);
  setpriority (PRIO_PROCESS, pid, prio);
}

La fonction gettid() n’existe pas dans la glibc, il est donc obligatoire de passer par un appel système. Et sans utiliser setpriority(), il n’est pas possible à ma connaissance de baisser la priorité en étant un simple utilisateur.

Défaut de jeunesse

Malheureusement un défaut relativement important a été soulevé par Aurélien (un des leader du projet GeeXboX et développeur de FFmpeg). Les méta-données disponibles dans les fichiers audio/video peuvent être très différentes d’un format à l’autre. Valhalla est limité dans le nombre de meta-données supportées. Une future mise à jour majeure devrait apporter beaucoup plus de souplesse pour la gestion des méta-données. A priori cela ne semble pas si compliqué, mais si l’on considère le fait que la base de données suit un modèle relationnel, il est difficile d’être complètement générique.

Je n’ai pas encore pris le temps d’étudier toutes les possibilités pour y remédier. Mais ça fait partie de mes priorités pour Valhalla.

Enna

Mon but principal est d’utiliser Valhalla dans Enna. Ainsi un nouveau module à fait son apparition et permet de retrouver de manière thématique les albums, les artistes, les genres ainsi que les fichiers non-classés. Le module est également paramétrable depuis le fichier ~/.enna/enna.cfg pour surtout y indiquer les répertoires à scanner.

[valhalla]
path=file:///home/foo/music
path=file:///home/bar/music
verbosity=info
parser_number=2
commit_interval=128
# <=0 for infinite
scan_loop=-1
# time [sec] for sleeping between loops
scan_sleep=900
# 0: normal, -20: higher, 19 lower
scan_priority=19

Quelques paramètres méritent certaines explications. Tout d’abord vous pouvez indiquer autant de "path" que vous le désirez. Les paths ne sont pas scannés en parallèle, mais séquentiellement. Il n’y a pas beaucoup d’avantage de scanner  plusieurs répertoires en parallèle car les accès au disque ne seront pas plus rapide.
Vous pouvez bien sûr changer la verbosité de la bibliothèque. L’intérêt est limité pour de simples utilisateurs, mais ça peut être utile dans des cas de debuggage.

Nombre de parsers

L’option "parser_number" indique le nombre de parsers à créer. Par défaut, le nombre maximum est de 8. Ce chiffre peut être changé à la compilation de Valhalla en passant le define PARSER_NB_MAX=# dans le CFLAGS. Mais il faut garder à l’esprit qu’il est inutile de définir beaucoup de parsers car ça n’augmentera en rien la vitesse.

Intervalle entre les commits

L’option "commit_interval" permet de dire chaque combien de fichiers, les données sont réellement inscrites dans la base de données. Moins cette valeur est grande, et moins le scan sera rapide car SQLite augmentera le nombre d’écritures sur le disque dur. J’ai constaté un gain de ~20 en passant d’une valeur de 1 à une valeur de 128. Si vous définissez une valeur plus grande, le gain ne sera plus tellement visible et les données s’accumuleront dans la RAM. Le fait de mettre une petite valeur est également plus sûr, car en cas de plantage, plus de données seront écrites. C’est donc aussi une question de bon sens.

Bouclage du scan et temps entre les scans

L’option "scan_loop" indique le nombre de bouclage qui doivent être effectués pendant qu’Enna est exécuté. L’intérêt est de détecter les modifications et les nouveaux fichiers. En mettant une valeur de 1, le scan se fera uniquement au démarrage d’Enna. Par contre avec une valeur plus petite ou égale à 0, le scan se fera chaque "scan_sleep" secondes. Vous pouvez bien sûr donner une valeur supérieure tel que 2, 3, etc,.. pour définir exactement le nombre de bouclages désiré.

Priorité du scan

L’option "scan_priority" permet de garantir que le scanner, le gestionnaire de la base de données ainsi que les parsers, ne puissent pas monopoliser les ressources de votre système. Le fait de donner une valeur de 19, garanti que Valhalla aura la priorité la plus basse et son comportement sera bien une tâche de fond. Par défaut un utilisateur à une priorité de 0, et tous les programmes exécutés ont une priorité de 0 (vous ne pouvez donc donner que des priorités inférieures (valeurs positives ou égales)). Si vous n’avez pas les droits appropriés et que vous donnez une priorité de -1 à -20, Valhalla gardera une priorité de 0. Par contre, si vous êtes root, alors vous pouvez mettre n’importe quel priorité. (Il n’est pas recommandé de mettre une priorité supérieure ou égale à Enna à moins d’affecter directement la lecture des fichiers et l’interface.)

La suite

Comme je l’ai expliqué plus haut, il reste beaucoup de travail pour rendre la bibliothèque plus générique envers les méta-données. Mais il reste également du travail au niveau de l’API, en ce qui concerne les fonctions pour sélectionner les données dans la base de données, et ces fonctions dépendent directement des méta-données.

Bien sûr, il est possible d’effectuer des requêtes sur la base de données sans passer par Valhalla. Mais dans ce cas il faut être conscient qu’il peut y avoir des problèmes de parallélisme. SQLite utilise fcntl() pour bloquer l’accès au fichier le temps des modifications. Mais sur certains systèmes de fichier, cela est buggé (voir les explications  ici).

Quelques modifications mineures sont également prévues, comme l’ajout de filtres sur les paths, pour par exemple ignorer les fichiers et dossiers cachés. Et également un moyen de récupérer l’information si un fichier contient des flux audio, video ou audio+video.

Une mise à jour concernera également l’ajout d’informations sur la version du fichier de base de données et la version de Valhalla. Car actuellement, si des modifications structurelles sont effectuées sur la base de données, Valhalla ne fait aucun contrôle sur le fichier qui a été créé par une ancienne version. Ce qui peut provoquer des erreurs plus ou moins imprévisibles lors de l’exécution d’un scan. Le temps que j’implémente cette partie, il est judicieux d’effacer le fichier ~/.enna/valhalla_music.db lors d’une mise à jour de Valhalla.

Télécharger

Pour télécharger Valhalla, rien de plus simple:

hg clone http://hg.geexbox.org/libvalhalla

La compilation et l’installation se fait comme pour n’importe quel logiciel.

./configure && make && sudo make install

Pensez aussi à faire un `sudo ldconfig` pour être sûr que la lib soit référencée pour le chargeur de programme.

Il est également important (indispensable) d’utiliser une version récente d’FFmpeg!

Quelques statistiques

Comme promis, voici quelques statistiques liées à la vitesse du scan. Le premier scan est toujours le plus long. La vitesse dépend principalement de la vitesse d’accès du média sur lequel se trouve les fichiers. Puis en second lieu, elle dépend également du cache effectué par le système de fichier. Mes quelques tests ont été fait sur une partitions EXT3 avec principalement des fichiers MP3 (dont des corrompus), des fichiers OGG, WMA et M4A. (le fichier de base de données est bien sûr effacé entre chaque essai). Il y a 1090 fichiers et mon système est dual-core.

Premier scan (sans cache)

Run: parser=2 loop=1 wait=0 priority=19 commit-int=128
Time elapsing: 24675.591000 msec, 24.675591 sec

Quelques scans (même config + cache)

Time elapsing: 1049.802000 msec, 1.049802 sec
Time elapsing: 1183.110000 msec, 1.183110 sec
Time elapsing: 1066.583000 msec, 1.066583 sec

La vitesse a énormément changée et pourtant la bibliothèque à fait exactement le même job. Le cache du système de fichier a un effet très important.

Avec un seul parser (1 parser + cache)

Run: parser=1 loop=1 wait=0 priority=19 commit-int=128
Time elapsing: 1500.876000 msec, 1.500876 sec
Time elapsing: 1436.085000 msec, 1.436085 sec
Time elapsing: 1637.296000 msec, 1.637296 sec

On note en moyenne 1.10 sec pour 2 parsers et 1.53 sec pour 1 parser. Vous me direz que ça n’a rien d’exceptionnel et vous avez tout a fait raison. La différence est de 430 ms entre les deux moyennes. Mais en réfléchissant un peu, cela représente qu’en même un gain de ~40%.
Ces statistiques ne sont pas très exhaustives car 1090 fichiers ce n’est pas grand chose. Il faudrait également effectuer beaucoup plus de tests pour avoir une statistique relevante. Mon but était juste de vous montrer que le nombre de parsers ne fait pas de miracle. Et qu’avoir plus d’un parser lors du premier scan ne changera quasiment rien. Mais si vous avez beaucoup plus de fichiers que moi, (des dizaines de milliers) l’impacte sera bien plus grand.

Avec un commit-int à 1 (+ cache)

Run: parser=2 loop=1 wait=0 priority=19 commit-int=1
Time elapsing: 20299.520000 msec, 20.299520 sec
Time elapsing: 18831.318000 msec, 18.831318 sec
Time elapsing: 19063.652000 msec, 19.063652 sec

Voyez comme l’intervalle entre les commits effectués par SQLite sont très important. Passer de 1.5 sec à 20 sec n’est pas du tout négligeable.

Avec un commit-int à 1024 (+ cache)

Run: parser=2 loop=1 wait=0 priority=19 commit-int=1024
Time elapsing: 935.655000 msec, 0.935655 sec
Time elapsing: 1018.052000 msec, 1.018052 sec
Time elapsing: 953.042000 msec, 0.953042 sec

Il est intéressant de noter qu’avec un intervalle de 1024, Valhalla arrive à descendre en dessous de la seconde. Maintenant vous comprendrez qu’il est difficile de trouver une configuration parfaite pour tout le monde. Mais le gain entre un intervalle de 128 ou de 1024 n’a plus beaucoup d’impact. Sans compter que 128 est plus sûr dans le cas d’un crash.

(Tous les tests ont été effectué avec l’outil test-valhalla directement disponible depuis les sources.)

Conclusion

Il reste beaucoup de travail à faire, mais vous pouvez utiliser cette bibliothèque correctement dans l’état actuel. Les performances sont à mon avis très bonnes et le nombre de parsers sera sûrement plus significatif dans le futur, car certaines améliorations auront forcément un impact sur la vitesse de récupération des méta-données. Même sur un système mono-processeur (mono-core), il est judicieux de garder 2 parsers. Parfois certains fichiers sont relativement altérés et difficiles à parser. Le fait de garder un deuxième parser assure un bon enchaînement si l’un d’eux perd du temps.

A bientôt,

Mathieu SCHROETER

Suivre

Recevez les nouvelles publications par mail.

%d bloggers like this: