Ripons avec le Bash

Octobre 2002

Encore un article traitant de Bash ! Mais cette fois-ci nous allons prétexter un exemple précis pour parfaire nos connaissances. L'intérêt est donc double.

Nous allons donc élaborer un script entièrement en Bash dont le but sera d'extraire les pistes audio d'un CD. C'est ce que nous appelons, en "bon français", riper un CD. Cela provient du verbe anglais "to rip" an sens de "arracher".

Ensuite nous compresserons les données brutes obtenues dans un format libre (au hasard, Ogg Vorbis).

Rien de très compliqué en somme, pourquoi se casser la tête à faire un script ? Nous allons également interroger la base "freedb", version libre de "cddb" pour récupérer les titres des morceaux, de l'album et le nom de l'artiste.

Qu'est-ce que "cddb" ? Une base de données regroupant des informations sur la plupart des CD audio. Cette base est accessible par internet et l'on peut s'y connecter avec un programme client. Les informations disponibles sont le titre de l'album, le nom de l'artiste et le titre de chaque morceau. Ces informations ne se trouvent pas sur le CD audio. N'y figurent que le nombre de pistes, la position de chacune d'elles et le numéro d'identifiant du disque. C'est avec ces informations-là que nous pouvons obtenir les autres depuis "cddb".

Originellement "cddb", depuis connu sous le nom de "Gracenote", est un service proposé par la société Escient. Il se trouve que ledit service n'est pas libre, voire même contraignant (pléonasme ?). Un logiciel permettant l'accès à "Gracenote" ne doit pas permettre l'accès à une autre base du même type. De plus, il doit afficher le logo officiel de "Gracenote". Au fait, vous ai-je dit que nous allons réaliser un script s'exécutant en ligne de commandes ? Ah oui, c'est vrai, il y a le framebuffer... Ou encore l'art ascii.

Laissons "Gracenote" jouer au logo dans son coin. Très souvent, lorsqu'un service utile apparait sous une license propriétaire, le même service en version libre est développé en parallèle. C'est ainsi que "freedb" naquit.

Les outils de base

Bien sûr, ce n'est pas Bash lui-même qui va réaliser toutes les opérations menant du CD audio aux fichiers numériques compressés. Nous allons utiliser des outils spécifiques et Bash va les coordonner entre eux. Notez que ce script est également réalisable avec d'autres shells. Il suffit de l'adapter un peu.

Chaque outil que nous allons utiliser a donc une fonction spécifique, ce qui est l'un des piliers d'Unix et de ses dérivés : un programme, une fonction. Nous sommes donc aux antipodes des clients mail qui s'occupent aussi de l'envoi et la réception de mails, qui gèrent un agenda, qui sonne quand le café est chaud, etc.

Tout d'abord, le client cddb, l'outil permettant d'accéder à une base "de type" cddb et pas forcément Gracenote. Le programme nommé "cddbcmd" est contenu dans le paquet "cddb". Il permet d'envoyer une requête à la base distante. Un détail qui a son importance, vous devez disposer d'une connexion à Internet.

Cependant, vous devez fournir des informations à la base cddb afin que celle-ci vous renvoie ce que vous lui demandez. Ces informations sont incluses sur le CD audio et peuvent être obtenues à l'aide du programme "cd-discid".

Pour manipuler les chaînes de caractères retournées par ces deux outils nous utiliserons l'indispensable "sed".

Ensuite, il nous faut un logiciel capable d'extraire les pistes audio. Plusieurs choix s'offrent à nous : "cdda2wav" et "cdparanoia" pour les plus connus. Tous deux convertissent les pistes audio en fichiers de données brutes. Ces fichiers étant bruts, leur taille avoisine les 50 Mo pour un morceau de 5 minutes.

Enfin nous allons les compresser afin de diviser leur taille par dix avec une perte de qualité que seules les personnes équipées d'oreilles "éduquées" sont capables de détecter. Deux possibilités pour le choix du format de compression :

Bien entendu je vous invite à utiliser le format libre (lisible par xmms sans adjonction de plug-in). Et ceci indépendamment du fait que depuis le mois d'août Thomson fasse payer les licences d'utilisation pour la diffusion d'encodeurs et de décodeurs MP3 (voir les liens). Vous devez donc vous procurer le paquet "vorbis-tools" (existe pour Debian, Redhat/Mandrake ou en sources).

En pratique

Le script que nous allons réaliser, va lui-même créer un répertoire dont la syntaxe est : "Artiste - Titre de l'album". Ces deux informations sont obtenues à l'aide d'une requête de type "query" à freedb. Pour formuler une telle requête, il faut fournir, dans l'ordre, l'identifiant du CD, le nombre de pistes, la position (en secondes) de chacune des pistes par rapport au début et la durée totale en secondes. Ces informations sont disponibles sur le CD audio et données par "cd-discid" prêtes à être utilisées pour la requête.

Vérifications

Comme nous utilisons plusieurs logiciels externes à Bash, il est préférable de tester leur présence avant de les utiliser. Il nous faut pour chacun une variable contenant le nom et le chemin complet du programme.

discidcmd=/usr/bin/cd-discid;
cddbcmd=/usr/bin/cddbcmd;
cdtowav=/usr/bin/cdparanoia;
wavtoogg=/usr/bin/oggenc;

Nous utilisons ensuite une structure conditionnelle afin de tester la présence (et par la même occasion, l'attribut d'exécution) de chacun des programmes. Une expression conditionnelle se présente toujours de la même façon. Elle est entourée d'une paire de crochets et d'une paire d'espaces (après le crochet ouvrant et avant le fermant). Pour tester l'existence d'un fichier nous utilisons un opérateur unaire (un seul paramètre). L'opérateur "-e" a cet usage : [ -e fichier] renvoie vrai si fichier existe. L'opérateur "-x" teste en plus si le fichier a l'attribut d'exécution. Afin d'inverser le résultat du test (vrai si le fichier est manquant ou non exécutable) il faut rajouter un point d'exclamation au début de l'expression (veillez à ce qu'il soit entouré d'une paire d'espaces sinon sa signification est différente).

Voici donc notre procédure de test :

erreur=0;
if [ ! -x ${discidcmd} ];
then
    echo "${discidcmd} not found !";
    erreur=1;
fi;

etc. (voir script complet en fin d'article)

if [ $erreur == 1 ];
then
    echo "Please install missing software";
    exit 1;
fi;

Identité du CD

A présent nous définissons une variable avec le nom de périphérique de votre lecteur cdrom :

discdev=/dev/cdrom;

Attention, vous devez avoir le droit de lecture sur le périphérique pointé par le lien symbolique /dev/cdrom. Si ce n'est pas le cas, rajoutez le droit en lecture pour tous, ou rajoutez votre utilisateur dans le groupe du périphérique ("disk" ou "cdrom" selon les cas).

Ensuite nous extrayons les données du CD audio. Attention, ce sont des apostrophes inversées. Il s'agit d'une substitution du nom de la commande par son résultat. Cela signifie que la commande est appelée et ici le résultat et non l'intitulé est retourné dans la variable discidres.

discidres=`${discidcmd} ${discdev}`;

La syntaxe de cd-discid impose de donner le nom du périphérique où se trouve votre galette argentée. En général il s'agit de /dev/cdrom.

Nous récupérons la description du CD audio dans la chaîne discidres. Nous l'utilisons ensuite dans la première requête à freedb.

infostr=`${cddbcmd} cddb query $discidres`;

Il existe une autre syntaxe pour substituer des commandes : encadrer le nom de la commande avec une paire de parenthèses et précéder le tout d'un dollar. Ainsi, les deux lignes précédentes peuvent être simplifiées en une seule :

infostr=$(${cddbcmd} cddb query `${discidcmd} ${discdev}`);
ou encore
infostr=$(${cddbcmd} cddb query $(${discidcmd} ${discdev}));

A la suite de quoi nous obtenons dans l'ordre, la catégorie cddb dans laquelle est classée le disque, l'identifiant du CD. Puis le nom de l'artiste et le titre de l'album séparés par un slash ("/").

$ cddbcmd cddb query `cd-discid /dev/cdrom`
rock a50d650d Nightwish / Angels fall first

Mais il arrive que le disque soit classé dans plusieurs catégories. Donc, nous recevons dans la chaîne infostr plusieurs lignes concaténées.

$ cddbcmd cddb query `cd-discid /dev/cdrom`
misc b10d7a0d Alanis Morissette / Jagged Little Pill
rock b10d7a0d Alanis Morissette / Jagged Little Pill
newage b10d7a0d Alanis Morissette / Jagged Little Pill
Or nous considérons dans la suite du script que infostr ne contient qu'une ligne. Il faut donc s'assurer d'extraire systématiquement la première ligne. Pour cela, nous utilisons infostr comme un tableau dont chaque case contient une ligne du résultat renvoyé par la requête cddb. Ainsi, nous n'avons plus qu'à considérer la première ligne.

Il existe plusieurs façons de construire un tableau avec Bash. Celle que nous utilisons consiste à entourer la liste des éléments d'une paire de parenthèses.

infostr=($(${cddbcmd} cddb query `${discidcmd} ${discdev}`));

Ici Bash sépare chaque élément du résultat de la commande et les place chacun dans une case du tableau. Comment Bash sépare-t-il les éléments ? Il utilise une variable interne, IFS contenant un ou plusieurs caractères de séparation. IFS signifie "Internal Field Separator" soit en français, "séparateur de champs interne". Chaque fois qu'un caractère d'IFS est rencontré dans la chaîne résultante, Bash crée un nouvel élément. Par défaut IFS contient une espace, une tabulation et un saut de ligne. Nous devons le redéfinir avant d'appeler la commande précédente afin que les éléments soient séparés au niveau des fins de ligne :

IFS=$'\n';

Il ne nous reste alors plus qu'à analyser la première ligne et en isoler chaque élément dans un autre tableau. Il faut bien sûr modifier l'IFS afin que l'espace soit un séparateur.

IFS=' ';
infotab=(${infostr[0]});

Attention à ne pas oublier les accolades. Elles doivent être employées à chaque fois qu'il y a ambiguïté, sinon tout le temps. Le symbole dollar "$" est utilisé pour remplacer le nom d'une variable par sa valeur. Par exemple, "$i" donne la valeur de "i". Une variable peut-être composée de plusieurs caractères (le premier ne doit être ni un chiffre, ni un caractère spécial, les suivants peuvent être des chiffres). De plus, il peut arriver qu'un nom de variable soit directement suivi par des caractères n'entrant pas dans sa composition. Il faut alors le délimiter avec une paire d'accolades (cet exemple n'entre pas dans la composition de notre script) :

convert image${i}.gif image${i}.png

Mais dans notre cas (infostr[0]) c'est le contraire. Ce sont les caractères "[0]" qu'il faut inclure dans le nom de la variable. Comme en langage C, "infostr" est le nom du tableau et "infostr[0]" représente le premier élément. Vous devez donc toujours utiliser des accolades lorsque vous manipulez les éléments d'un tableau.

Une fois cela fait, on a, dans le tableau infotab, les informations obtenues de freedb. La catégorie et l'identifiant (que l'on a déjà mais il est bon de l'isoler dans une variable pour la suite) sont aisés à extraire :

categ=${infotab[0]};
discid=${infotab[1]};

Artiste et album

Le reste du tableau contient de manière découpée le nom de l'artiste et le titre de l'album ainsi qu'un slash. Si l'on reprend l'exemple du CD de Nightwish, cela donne :

$ echo "|${infotab[2]}|${infotab[3]}|...|${infotab[6]}|"
|Nightwish|/|Angels|fall|first|

Il nous faut reconstituer les deux chaînes de caractères artist et album. Pour reconstituer une chaîne à partir des éléments d'un tableau il faut utiliser la syntaxe suivante :

chaine="${infotab[@]}";

Cette expression développe chacun des membres en les séparant par des espaces. Il y a aussi la syntaxe avec une astérisque à la place de l'arobase, la différence vient du fait que les membres sont séparés par le premier caractère de la chaîne "IFS". Voici un exemple où le caractère de séparation est l'underscore :

$ IFS='_'
$ echo "${infotab[*]}"
rock_a50d650d_Nightwish_/_Angels_fall_first

Les informations qui nous intéressent ne commencent qu'à partir du troisième élément. Nous allons donc extraire le sous-tableau qui nous intéresse. La syntaxe générique est : ${paramètre:début:longueur}. Elle permet d'extraire de la chaîne paramètre la sous-chaîne commençant au caractère début et contenant longueur caractères. Les valeurs début et longueur sont des expressions arithmétiques. A moins que leur valeur ne soit un nombre positif et seul, vous devez indiquer qu'il s'agit d'une expression arithmétique avec la syntaxe $((expression arithmétique)). Par exemple ${chaine:$((-2)):1} nous donne l'avant-dernier caractère de chaine. Lorsque le paramètre représente l'ensemble des éléments d'un tableau, les expressions arithmétiques s'appliquent sur ces mêmes éléments et non plus sur des caractères. Notre problème était d'extraire tous les éléments d'un tableau à partir du troisième. Il nous suffit de récupérer dans une chaîne la valeur de ${infotab[@]:2}.

Mais nous pouvons faire encore plus fin. La chaîne retournée précédemment est constituée du nom de l'artiste et du titre de l'album séparés par un slash. Nous allons donc utiliser un tableau intermédiaire pour stocker nos informations proprement séparées.

IFS='/';
dtitle=(${infotab[@]:2});

Et ensuite, on récupère nos deux variables.

artist=${dtitle[0]:0:$((${#dtitle[0]}-1))};
album=${dtitle[1]:1};

Vous remarquerez les quelques ajustements. En effet, lorsque nous avons séparé les deux éléments que nous voulons récupérer, nous l'avons fait avec le caractère "/" comme séparateur. Or il subsiste deux espaces qui peuvent nous géner par la suite. Le premier se situe à la fin du nom de l'artiste et le second au début du titre de l'album. Soit autour du slash escamoté.

Si la ligne concernant le titre de l'album vous semble limpide, l'autre peut vous paraître plus trouble. Il faut peut-être préciser que la syntaxe ${#chaine} permet de connaître la longueur en caractères de chaine. Comme nous le verrons un peu plus loin, si chaine représente tous les éléments d'un tableau, alors on obtient le nombre de ces éléments.

Une information ne se trouve pas dans la base cddb, du moins pas assez précisément. C'est le genre de musique. Sur freedb par exemple on ne trouve que les catégories suivantes :

$ cddbcmd cddb lscat                                  
blues
classical
country
data
folk
jazz
misc
newage
reggae
rock
soundtrack

En fait, la catégorie n'a pas vocation à renseigner d'un genre de musique (surtout "misc"). Nous allons offrir la possibilité à l'utilisateur de spécifier un genre au moment où il appellera le script (en paramètre donc). Si aucun paramètre n'est donné on utilise la catégorie. Le premier paramètre d'un script Bash est représenté par "$1".

La syntaxe ${paramètre:-mot} teste si "paramètre" est inexistant ou nul. Dans ce cas l'écriture est remplacée par "mot". Utilisons cela dans notre script :

genre=${1:-$categ};

Ce qui peut se traduire par : si $1 est donné $genre vaut $1, sinon il vaut $categ.

Titres des pistes

Nous allons maintenant récupérer les titres des différents morceaux. Ces informations ne sont disponibles que dans la base cddb. Nous allons donc envoyer une nouvelle requête, de type "read" cette fois-ci. Nous devons donner comme informations la catégorie dans laquelle est classé le disque (il arrive que deux disques différents aient le même numéro d'identifiant, on les différencie sur la catégorie) ainsi que l'identifiant. Faisons un essai hors script :

$ cddbcmd cddb read rock a50d650d
[...]
DISCID=a50d650d
DTITLE=Nightwish / Angels fall first
TTITLE0=Elvenpath
TTITLE1=Beauty And The Beast
[...]

Un certain nombre d'informations est renvoyé. D'abord un récapitulatif de ce que donne la commande cd-discid. Ensuite, plus intéressant, les lignes ci-dessus. Le titre de chaque morceau est préfixé par "TTITLE<X>=" avec "<X>" le numéro du morceau (attention, cela commence à zéro).

Il n'y a plus qu'à récupérer simplement les lignes qui nous intéressent dans un tableau où chaque élément est un titre. Pour cela (et pour beaucoup d'autres choses) sed est notre ami. Nous lui demandons d'épurer les lignes commençant par "TTITLE" de ce qu'elles contiennent jusqu'au signe égal, puis d'afficher ces lignes et uniquement ces lignes.

Voici la commande sed :

sed -n -e s/^TTITLE.*=//p

L'option "-n" supprime la sortie par défaut, c'est à dire l'intégralité du fichier qu'il traite. L'option "-e" exécute la commande qui suit. Il s'agit ici d'une substitution de l'expression régulière "TTITLE.*=" par rien du tout. "" signifie le début de la ligne. ".*" signifie n'importe quel caractère en zéro, un ou plusieurs exemplaires. Nous rajoutons la commande "p" qui permet l'affichage des lignes modifiées. Et tout ceci rentre directement dans un tableau.

IFS=$'\n';
track=(`${cddbcmd} cddb read ${categ} ${discid} | \
		    sed -n -e s/^TTITLE.*=//p`);

On peut maintenant compter le nombre de pistes (même si on l'avait déjà) :

ntracks=${#track[*]};

A présent nous avons toutes les données nécessaires.

Il y a comme une odeur de café

Commençons par créer un répertoire dans lequel se trouveront les fichiers musicaux.

mkdir "$artist - $album";
cd "$artist - $album";

Et ensuite, à l'intérieur d'une boucle, nous allons faire tout le travail d'extraction, compression sur chacune des pistes audio.

cdtowavparams="";
wavtooggparams="-b 192";

i=1;
while [ $i -le $ntracks ];
do
    trackname="`printf %.2d $i` - ${track[$((i-1))]}"
    $cdtowav $cdtowavparams $i "$trackname";
    $wavtoogg $wavtooggparams         \
        --artist "$artist"            \
        --genre "$genre"              \
        --tracknum "$i"               \
        --title "${track[$((i-1))]}"  \
        --album "$album"              \
        "$trackname" -o "${trackname}.ogg";
    rm "$trackname";
    i=$((i+1));
done

La boucle va tourner tant que "$i" est inférieur ou égal ("-le" pour "less or equal") à "$ntracks".

La commande "printf" a un comportement semblable à la fonction "printf()" en C. Le premier paramètre définit le format dans lequel est affiché le second paramètre. Ici, "$i" est traité comme un entier (d pour entier en base décimale, par opposition à hexadécimale ou octale) avec une précision forcée à 2 chiffres (.2).

Ainsi la variable trackname contient le nom final du fichier (sans l'extension "ogg") au format : numéro de piste - titre.

L'appel à cdparanoia extrait la piste $i dans un fichier brut du nom de $trackname. Il est au format "wave". De même que cd-discid, cdparanoia utilise directement le périphérique /dev/cdrom. Encore une fois vous devez pouvoir le lire.

Ensuite, nous compressons ce fichier brut au format Ogg Vorbis en donnant toutes les informations (ce que l'on appelle les "tags"). L'option "-b" spécifie le débit moyen de données (en anglais "bitrate"). Contrairement au format MP3 où cette valeur est fixe, l'encodage vorbis l'adapte en fonction de la quantité de données afin de limiter leur perte. L'option "-o" spéficie un nom de fichier de sortie. En effet, si le tag "title" est spécifié, oggenc s'en sert pour nommer le fichier si aucun n'est donné. Si nous voulons que le numéro apparaisse dans le nom de fichier, nous devons le préciser.

Après cela nous pouvons effacer le fichier musical brut.

Ensuite, vous pouvez exécuter le script (après l'avoir rendu exécutable par "chmod +x") et aller vous préparer un café.

L'intégralité de cdrip

#!/bin/bash

cddbcmd=/usr/bin/cddbcmd;
discidcmd=/usr/bin/cd-discid;
discdev=/dev/cdrom;
cdtowav=/usr/bin/cdparanoia;
cdtowavparams="";
wavtoogg=/usr/bin/oggenc;
wavtooggparams="-b 192";

erreur=0;
if [ ! -x ${discidcmd} ];
then
    echo "${discidcmd} not found !";
    erreur=1;
fi;

if [ ! -x ${cddbcmd} ];
then
    echo "${cddbcmd} not found !";
    erreur=1;
fi;

if [ ! -x ${cdtowav} ];
then
    echo "${cdtowav} not found !";
    erreur=1;
fi;

if [ ! -x ${wavtoogg} ];
then
    echo "${wavtoogg} not found !";
    erreur=1;
fi;

if [ $erreur == 1 ];
then
    echo "Please install missing software";
    exit 1;
fi;

IFS=$'\n';
infostr=($(${cddbcmd} cddb query `${discidcmd} ${discdev}`));

IFS=' ';
infotab=(${infostr[0]});

categ=${infotab[0]};
discid=${infotab[1]};

IFS='/';
dtitle=(${infotab[@]:2});

artist=${dtitle[0]:0:$((${#dtitle[0]}-1))};
album=${dtitle[1]:1};

genre=${1:-$categ};

IFS=$'\n';
track=(`${cddbcmd} cddb read ${categ} ${discid} | \
    sed -n -e s/^TTITLE.*=//p`);
ntracks=${#track[*]};

mkdir "$artist - $album";
cd "$artist - $album";

i=1;
while [ $i -le $ntracks ];
do
    trackname="`printf %.2d $i` - ${track[$((i-1))]}"
    echo "${trackname}";
    $cdtowav $cdtowavparams $i "$trackname";
    $wavtoogg $wavtooggparams         \
        --artist "$artist"            \
        --genre "$genre"              \
        --tracknum "$i"               \
        --title "${track[$((i-1))]}"  \
        --album "$album"              \
        "$trackname" -o "${trackname}.ogg";
    rm "$trackname";
    i=$((i+1));
done

 

Dimitri ROBERT
<dimitri-robert CHEZ wanadoo.fr>

Glossaire

Requête
dans le domaine des bases de données il s'agit d'une commande permettant d'obtenir des données suivant des critères précis. Par exemple, demander à une base gérant les transports ferroviaires "tous les trains partant de Marseille entre 14h et 16h" est une requête.

Liens