Article publié dans Linux Magazine 84, juin 2006.
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
).
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 :
MP3::Splitter
se chargera de la découpe des fichiers MP3,
Spreadsheet::Read
se chargera de lire les informations dans mon
fichier tableur afin d'opérer les découpes.
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.
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 afin de pouvoir utiliser mp3split()
de la manière
souhaitée ;
le renommage de fichier, en effet, par défaut, mp3split()
renomme un
fichier exemple.mp3 en 01_exemple.mp3.
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.
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' ] );
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"; }
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.
Envoyez vos perles à perles@mongueurs.net
, elles seront peut-être
publiées dans un prochain numéro de Linux Magazine.
Copyright © Les Mongueurs de Perl, 2001-2011
pour le site.
Les auteurs conservent le copyright de leurs articles.