[couverture de Linux Magazine 84]

Perles de Mongueurs (24)

Article publié dans Linux Magazine 84, juin 2006.

[+ del.icio.us] [+ Developers Zone] [+ Bookmarks.fr] [Digg this] [+ My Yahoo!]

La perle de ce mois-ci a été rédigée par Emmanuel Di Prétoro <manu@bjornoya.net>, un habitué du canal #perlfr (serveur irc.mongueurs.net).

Découper des MP3 avec Perl

Définition du besoin

Avec la mode des podcasts (« balladodiffusion » en bon français), il est de plus en plus facile et courant d'avoir des émissions sur son baladeur MP3. Certaines de ces émissions valent la peine de définir une politique de stockage, mais d'autres rentrent plus dans un registre de consommation. Pourtant, même ces dernières offrent des moments que l'on souhaite conserver. Par exemple, certains podcasts musicaux offrant des sessions acoustiques d'artistes connus, ce serait intéressant de pouvoir extraire ces quelques morceaux du podcast original. Évidemment, ce genre de manipulation est possible avec un grand nombre de logiciels, dont le plus connu est probablement Audacity. Malheureusement, la procédure est manuelle, et au final assez fastidieuse si vous devez le faire avec plusieurs fichiers par semaine. L'idéal serait d'avoir un script qui se chargerait de l'opération de découpe. Mais appeler le script à chaque fois que nécessaire n'est pas encore suffisant pour ma paresse : ce que je souhaiterais, c'est lancer le script sur un ensemble de fichiers MP3. Le plus simple pour moi est d'utiliser des fichiers CSV, ou un tableur (que ce soit Excel ou encore OpenCalc) afin de saisir les informations nécessaires à l'opération de découpage. En effet, le script ne sera pas utilisé uniquement par moi, mais également par des personnes un peu moins geek.

Maintenant que j'ai défini mes besoins, voyons ce que le CPAN peut m'offrir :

Voyons comment organiser tout cela.

MP3::Splitter

Utiliser MP3::Splitter est relativement simple puisque le module exporte deux fonctions nommées mp3plit() et mp3split_read(). La première permet de découper un fichier MP3, et la seconde offre la même fonctionnalité, mais prend ses données à partir d'un fichier. On pourrait penser que la seconde fonction remplit totalement les besoins décrits plus haut. Malheureusement, les informations prises dans le fichier s'appliquent uniquement au fichier MP3 passé en argument de la fonction. Or dans cecas, je souhaitais traiter plusieurs fichiers MP3 d'un seul coup. De plus, ces fonctions fonctionnent selon un mode assez peu naturel. Par exemple :

    mp3split('monemission.mp3', [ '00:00:01', 23 ]);

va découper le fichier MP3 à partir de la première seconde, et va créer un nouveau fichier d'une durée de 23 secondes. J'aurais préféré l'utilisation suivante :

    mp3split('monemission.mp3', [ '00:00:01', '00:00:24' ]);

c'est-à-dire de donner le début et la fin du morceau souhaité, ainsi la tâche consistant à calculer la durée du morceau échoue à l'ordinateur.

Spreadsheet::Read

Concernant ce module, le fonctionnement est également fort simple. Spreadsheet::Read exporte une fonction par défaut qui est ReadData(). Cette fonction prend en argument le nom du fichier, que ce fichier soit au format OpenOffice.org, Excel ou encore CSV.

	my $ref = ReadData("test.xls"); # Excel
	my $ref = ReadData("test.csv"); # CSV
	my $ref = ReadData("test.sxc"); # OpenOffice
	my $ref = ReadData("test.ods"); # OpenOffice

Une autre fonction utile dans le cas présent est la fonction rows(), mais elle n'est pas exportée par défaut, il est donc nécessaire de l'importer lors du chargement du module :

	use Spreadsheet::Read qw( rows ReadData );

J'ai constaté que lorsque j'importais la fonction rows(), il est nécessaire d'importer également la fonction ReadData(). Les fonctions peuvent s'employer de la manière suivante :

	my $file = ReadData("monfichier.ods");
	my @rows = rows($file);
	print join(", ", @rows), "\n";

Ce code affichera le contenu du fichier monfichier.ods ligne par ligne. Comme vous pouvez le constater, l'usage de ce module est fort simple.

En ce qui concerne le choix de la structure des fichiers, c'est-à-dire l'ordre des champs, nous avons retenu l'ordre suivant (donné sous forme du fichier CSV) :

    "Nom du fichier", "Nom du morceau", "Début du morceau", "Fin du morceau"

Voyons maintenant comment lier ces deux modules afin d'obtenir ce que nous souhaitons.

My::MP3::Splitter

Pour ajouter les fonctionnalités souhaitées, et les lier de manière simple, j'ai décidé de créer un objet regroupant le tout. Ainsi, avec un simple

    My::MP3::Splitter->new( "fichier.csv" )->run();

l'ensemble du traitement serait lancé.

Pour cela, il est encore nécessaire de régler quelques détails :

Le calcul de la durée

Pour effectuer ce calcul, il suffit simplement de réduire les expressions de début et de fin de morceau en seconde, et de soustraire la valeur obtenue pour la fin du morceau avec la valeur obtenue pour le début du morceau. De cette manière, l'information souhaitée est obtenue.

Pour effectuer la conversion entre l'expression des bornes du morceau, je me suis contenté de lire le code utilisé par MP3::Splitter et je l'ai adapté au besoin de ce script. Cette fonction utilise intensivement des regexps et donc, peut être assez compliquée à comprendre. Néanmoins, par facilité, je l'ai conservée. C'est sans aucun doute une des choses à améliorer dans une version ultérieure de ce script.

Le renommage

Pour le renommage, j'ai d'abord pensé à une solution compliquée, pour me rendre compte finalement que la fonction mp3split() offre également la possibilité de définir une fonction permettant de changer la méthode de nommage des fichiers. Dans le cas présent, il suffit simplement d'utiliser comme valeur de retour le nom du fichier choisi pour le morceau, par exemple :

    mp3split(
        'test.mp3',
        { name_callback => sub { 'monmorceau.mp3' } },
        [ '00h01m00', '00h25m00' ]
    );

Le script final

Finalement, notre script est assez simple puisqu'il ressemble à ce qui suit :

    #!/usr/bin/perl

    use strict;
    use warnings;
    use Getopt::Long;

    package My::MP3::Splitter;

    use MP3::Splitter;
    use Spreadsheet::Read qw( ReadData rows );
    use Carp;

    sub new {
        my $class = shift;
        my $self = bless {}, $class;
        $self->{input_file} = shift if scalar @_ >= 1; # on vérifie si
                               # l'utilisateur a passé un
                               # paramètre lors de la
                               # création de l'objet
    }

    sub _process_input_file {
        my $self = shift;
        if ( -e $self->{input_file} ) {
            my $mp3_files = ReadData( $self->{input_file} );
            my @files     = rows($mp3_files->[1]);
        
            shift @files;    # par souci de documentation, la première ligne des
                             # fichiers traités est ignorée, permettant ainsi
                             # d'indiquer le type de données attendu
        
            foreach my $row (@files) {
                                            # on passe si...
                next if $row->[0] eq "";    # - cellule vide
                next if not -e $row->[0];   # - le fichier MP3 n'existe pas
                next if scalar @{$row} < 4; # - pas assez d'information

                $self->_split_file(@{$row});	    
            }
        }
        else {
            croak "Le fichier $self->{input_file} n'existe pas...";
        }
    }

    sub _split_file {
        my ($self, $mp3_file, $new_file, $begin_part, $end_part) = @_;
        my $duration = $self->_compute_duration($begin_part, $end_part);
        mp3split($mp3_file, { name_callback => sub { $new_file } },  [ $begin_part, $duration ]);
    }

    sub _compute_duration {
        my ( $self, $begin, $end ) = @_;
        my ( $b_hour, $b_min, $b_sec )
            = $begin
            =~ /^(?:([\d.]+)(?:h|:(?=.*[m:])))?(?:([\d.]+)[m:])?(?:([\d.]+)s?)?$/;
        for ( $b_hour, $b_min, $b_sec ) {
            next unless defined $_;
            /^(\d+\.?|\d*\.\d+)$/;
        }
        my $begin_total
            = ( $b_hour || 0 ) * 3600 + ( $b_min || 0 ) * 60 + ( $b_sec || 0 );
        my ( $e_hour, $e_min, $e_sec )
            = $end
            =~ /^(?:([\d.]+)(?:h|:(?=.*[m:])))?(?:([\d.]+)[m:])?(?:([\d.]+)s?)?$/;
        for ( $e_hour, $e_min, $e_sec ) {
            next unless defined $_;
            /^(\d+\.?|\d*\.\d+)$/;
        }
        my $end_total
            = ( $e_hour || 0 ) * 3600 + ( $e_min || 0 ) * 60 + ( $e_sec || 0 );
        return $end_total > $begin_total ? $end_total - $begin_total : 0;
    }

    sub run {
        my ($self) = shift;
        if (scalar @_ >= 1) {
        $self->{input_file} = shift; # on vérifie si l'utilisateur a spécifié
                         # un paramètre à la fonction, et le cas
                         # échéant, on se prépare à traiter ce
                         # fichier  
        } else {
            if (not defined $self->{input_file}) {
                croak "No input file...\n"; # on gère le cas où aucun fichier à
                        # traiter n'a été spécifié. Que ce
                        # soit lors de la création de l'objet,
                        # ou lors de l'appel de la méthode
            }
        }
        $self->_process_input_file();
    }

    package main;

    my %conf;
    GetOptions( \%conf, "input=s" );
    usage() if not exists $conf{input};
    My::MP3::Splitter->new( $conf{input} )->run();

    sub usage {
        die "$0 --input file, or $0 -i file\n";
    }

Conclusion

Voilà, j'ai maintenant la possibilité d'extraire des morceaux de mes fichiers MP3. Évidemment, je pourrais encore améliorer les services que peut me rendre ce script, par exemple, en ajoutant des champs dans le fichier CSV, je pourrais ajouter des informations ID3 aux fichiers MP3 créés, mais je laisse la réalisation de cette idée au lecteur, ou à une soirée prochaine.

À vous !

Envoyez vos perles à perles@mongueurs.net, elles seront peut-être publiées dans un prochain numéro de Linux Magazine.

[IE7, par Dean Edwards] [Validation du HTML] [Validation du CSS]