#!/usr/bin/perl -w my $RCS_Id = '$Id: mp3cut.pl,v 1.13 2006/06/21 19:39:13 jv Exp $ '; # Skeleton for Getopt::Long. # Author : Johan Vromans # Created On : Sun Jul 13 14:30:33 2003 # Last Modified By: Johan Vromans # Last Modified On: Wed Jun 21 21:39:03 2006 # Update Count : 104 # Status : Unknown, Use with caution! ################ Common stuff ################ $VERSION = sprintf("%d.%02d", '$Revision: 1.13 $ ' =~ /: (\d+)\.(\d+)/); use strict; # Package name. my $my_package = 'Sciurix'; # Program name and version. my ($my_name, $my_version) = $RCS_Id =~ /: (.+).pl,v ([\d.]+)/; # Tack '*' if it is not checked in into RCS. $my_version .= '*' if length('$Locker: $ ') > 12; use FindBin; use lib $FindBin::Bin; ################ Command line parameters ################ my $id3tags = 0; # add ID3 tags my $verbose = 1; # more verbosity # Development options (not shown with --help). my $debug = 0; # debugging our $trace = 0; # trace (show process) my $test = 0; # test mode. # Process command line options. app_options(); # Post-processing. $trace |= ($debug || $test); ################ Presets ################ my $TMPDIR = $ENV{TMPDIR} || $ENV{TEMP} || '/usr/tmp'; ################ The Process ################ use MPEG::Audio::Frame 0.04; # use CueFile; # use MP3Tools qw(trackfile); foreach my $cue ( @ARGV ) { mp3cut($cue); } ################ Subroutines ################ sub mp3cut { my ($cue) = @_; $cue = new CueFile($cue); warn("Processing ", $cue->file, "\n") if $verbose; open (my $mpf, $cue->file) or die(($cue->file) . ": $!"); binmode($mpf); my $out; # name of output file my $outf; # file handle my $frames = 0; # frames written my $tlen = 0; # total time my $trk = 1; # first track my $track = $cue->track($trk); # get it my $split = $track->index; # get its index (should be zero!) warn("WARNING: First split is @ ".dpsecs($split).", not zero!\n") if $split; my $frame; while ( $frame = MPEG::Audio::Frame->read($mpf) ) { # Check if this frame still fits. $tlen += $frame->seconds; if ( $split == 0 || ($split > 0 && $tlen > $split) ) { # Close output, if any. if ( $outf ) { warn(" Track ", $trk-1, ": $out: $frames frames\n") if $trace; wrapup($outf, $out, $cue->track($trk-1)); } # New output file. $out = trackfile($track->track, $track->title, ".mp3"); printf STDERR (" %2d \@ %8s %s\n", $trk, dpsecs($split), $out) if $verbose; open ($outf, ">$out") || die("$outf: $!\n"); binmode($outf); # Reset and advance. $frames = 0; $trk++; $track = $cue->track($trk); $split = $track->index; } # Skip leading frames, if first index is not at 0.00. next unless $outf; # Copy frame data. print $outf $frame->asbin; $frames++; } # Close last output, if any. if ( $outf ) { warn(" Track ", $trk-1, ": $out: $frames frames\n") if $trace; wrapup($outf, $out, $cue->track($trk-1)); } } sub wrapup { my ($outf, $out, $track) = @_; return unless $outf; close($outf); return unless $id3tags; unless ( eval { require ID3Tag; } ) { warn("Cannot load package ID3Tag -- ID3 tag generation disabled\n"); $id3tags = 0; } my $tag = new ID3Tag($out); $tag->artist($track->artist); $tag->album($track->album); $tag->title($track->title); $tag->track($track->track); $tag->update; } sub dpsecs { my $secs = shift; my ($mm, $ss); $secs *= 100; $mm = int($secs / 6000); $secs %= 6000; $ss = int($secs / 100); $secs %= 100; sprintf ("%d:%02d:%02d", $mm, $ss, $secs); } ################ Copied from MP3Tools ################ use constant VFAT => 1; # Sanitize a filename for use on disk. sub fsanitize { local($_) = shift; tr{/}{;}; s/_+;/;/g; s/;_*/;_/g; if ( VFAT ) { s/:/_/g; s/["\`?!|]//g; s/\.$//g; } $_; } # Generate 'preferred filename' for a track. sub trackfile { my ($track, $title, $exttmpl) = @_; my $nn = ""; if ( defined $track ) { $nn .= $track; $nn = "0".$nn if length($nn) < 2; $nn .= "_" . join("_", split(' ',$title)); } else { $nn = join("_", split(' ',$title)); } $nn .= $1 if $exttmpl && $exttmpl =~ /(\.[^.]+)$/; fsanitize($nn); } ################ Command Line Options ################ use Getopt::Long 2.33; # will enable help/version sub app_options { GetOptions(ident => \&app_ident, 'verbose|v' => \$verbose, silent => sub { $verbose = 0 }, # application specific options go here id3tags => \$id3tags, # development options test => \$test, trace => \$trace, debug => \$debug) or Getopt::Long::HelpMessage(2); } sub app_ident { print STDOUT ("This is $my_package [$my_name $my_version]\n"); } ################ CueFile.pm -- Parse .cue files ################ # Author : Johan Vromans # Created On : Wed Jul 16 11:57:42 2003 # Merged : Wed Jul 30 11:38:20 2003 package CueFile; use strict; use warnings; sub new { my ($pkg, $file) = @_; my $self = bless {}, $pkg; require "shellwords.pl"; $self->{_file} = $file; my $fd; if ( ref($file) ) { # assume FILE, for testing $fd = $file; } else { open($fd, $file) or die("$file: $!\n"); } # By initializing track to self, initial settings for FILE, TITLE and so # on go to the cue object until we've seen a TRACK specification. my $track = $self; my $title; while ( <$fd> ) { my @w = shellwords($_); next unless @w; my $try = lc($w[0]); if ( $try eq "performer" ) { $track->{artist} = $w[1]; } elsif ( $try eq "title" ) { $track->{title} = $w[1]; } elsif ( $try eq "file" ) { $track->{file} = $w[1]; } elsif ( $try eq "track" && lc($w[2]) eq "audio" ) { $track = new CueFile::Track; $self->{tracks}->[$w[1]] = $track; $track->{track} = $w[1]; $track->{artist} = $self->{artist}; $track->{album} = $self->{title}; } elsif ( $try eq "index" ) { my $ts = $w[2]; # MM:SS:FF my @ts = split(/:/, $ts); my $secs = $ts[1]; $secs += 60 * $ts[0]; $secs += $ts[2] / 75 if $ts[2]; $track->{index} = $secs; } else { warn("$file: $_"); } } # Append a sentinel track object, for easy processing. $track = new CueFile::Track; push(@{$self->{tracks}}, $track); $track->{track} = @{$self->{tracks}} - 1; $track->{title} = "End Sentinel"; $track->{index} = -1; $self; } # CueFile methods. sub artist { my $self = shift; $self->{artist} } sub title { my $self = shift; $self->{title} } sub file { my $self = shift; $self->{file} } sub tracks { my $self = shift; scalar @{$self->{tracks}} - 2 } sub track { my ($self, $ix) = @_; $self->{tracks}->[$ix] } package CueFile::Track; sub new { my ($pkg) = @_; bless {}, $pkg; } # CueFile::Track methods. sub artist { my $self = shift; $self->{artist} } sub title { my $self = shift; $self->{title} } sub album { my $self = shift; $self->{album} } sub index { my $self = shift; $self->{index} } sub track { my $self = shift; $self->{track} } =begin testing package CueFile::Test; if ( !caller ) { my $c = new CueFile(\*DATA); use Data::Dumper; print Dumper($c); print $c->tracks, " tracks\n"; } __DATA__ PERFORMER "Pink Floyd" TITLE "Wish You Were Here" FILE "Wish_You_Were_Here.mp3" WAVE TRACK 01 AUDIO TITLE "Shine on You Crazy Diamond (Part I-V)" PERFORMER "Pink Floyd" INDEX 01 00:00:00 TRACK 02 AUDIO TITLE "Welcome to The Machine" PERFORMER "Pink Floyd" INDEX 01 13:32:67 TRACK 03 AUDIO TITLE "Have a Cigar" PERFORMER "Pink Floyd" INDEX 00 21:03:25 INDEX 01 21:06:17 TRACK 04 AUDIO TITLE "Wish You Were Here" PERFORMER "Pink Floyd" INDEX 01 26:30:47 TRACK 05 AUDIO TITLE "Shine on You Crazy Diamond (Part VI-IX)" PERFORMER "Pink Floyd" INDEX 01 31:46:57 =end testing =cut __END__ =head1 NAME mp3cut - split MP3 files according to cue sheets =head1 SYNOPSIS sample [options] [file ...] Options: --silent don't produce verbose messages --id3tags provide ID3 tags on the files --ident show identification --help brief help message --verbose verbose information =head1 OPTIONS =over 8 =item B<--id3tags> Provide ID3 tag information to the written MP3 files. This requires a yet unreleased additional package. Stay tuned. =item B<--verbose> More verbose information (default). =item B<--silent> Less verbose information. =item B<--version> Print a version identification to standard output and exits. =item B<--help> Print a brief help message to standard output and exits. =item B<--ident> Prints a program identification. =item I Input file(s), which must be valid cue sheets. =back =head1 DESCRIPTION B will read the given cue sheets and spits the associated mp3 files according to the information in the cue sheet. The name of the MP3 file will be taken from the cue sheet. All tracks will be written with names derived from the track title, with spaces changed to underscores, the track number prefixed, and ".mp3" added. Some iterations may be necessary to find satisfying cut points, but it is easy to adjust the cue file data manually. =head1 USING CDDB ENTRY DATA B does not understand CDDB entry data. A helper program, B can be used to generate a cue sheet from the CDDB info. Some iterations may be necessary to find satisfying cut points, and it is easier to correct a cue file than the CDDB info. =head1 REQUIREMENTS L 0.04 or later, except 0.07. =head1 DISCLAIMER Audio data can be copied incorrectly. Always check your resultant files. Never throw away the original data. USE AT YOUR OWN RISK. =head1 AUTHOR Johan Vromans =head1 COPYRIGHT This programs is Copyright 2003, 2006, Squirrel Consultancy. This program is free software; you can redistribute it and/or modify it under the terms of the Perl Artistic License or the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. =cut