Tag Archive: Perl


I have an Nissan Altima with a BOSE radio that allows me to hook up an USB thumbdrive containing mp3 files. The problem is most of my audiobooks are in m4b format. Previously I’ve used tools like mp3splt and tried to split on ‘silence’ or timed increments (say 15 min) but I was getting mp3 files that would be split in midsentence and sometimes midword. It became very annoying after awhile.

So, I came up with a really simple script to convert an audiobook (m4b) into mp3 files splitting on the chapters. We are dependent on FFmpeg::Command and a modified FFprobe Perl module.

In the following example, we are converting a Ben Bova audiobook but we are going to specify to start the track numbering at “10″ because the 2nd file ended with track “9″.

jason@jason-Inspiron-1545 ~/bin $ ./test_mp4_info.pl -i "/home/jason/Audiobooks/Ben Bova/Mars/Mars 3.m4b" -o mp3 -a "Mars" -t 10
Converting "Mars 3.m4b" to "mp3/010 Mars.mp3"
  album: Mars
  artist: Ben Bova
  title: 010 – Mars
  genre: Audiobook
  track: 10
  … COMPLETE
Converting "Mars 3.m4b" to "mp3/011 Mars.mp3"
  album: Mars
  artist: Ben Bova
  title: 011 – Mars
  genre: Audiobook
  track: 11
  … COMPLETE
Converting "Mars 3.m4b" to "mp3/012 Mars.mp3"
  album: Mars
  artist: Ben Bova
  title: 012 – Mars
  genre: Audiobook
  track: 12
  … COMPLETE
Converting "Mars 3.m4b" to "mp3/013 Mars.mp3"
  album: Mars
  artist: Ben Bova
  title: 013 – Mars
  genre: Audiobook
  track: 13
  … COMPLETE
Converting "Mars 3.m4b" to "mp3/014 Mars.mp3"
  album: Mars
  artist: Ben Bova
  title: 014 – Mars
  genre: Audiobook
  track: 14
  … COMPLETE

Source code:

#!/usr/bin/perl

use strict;
use warnings;

use lib qw(/home/jason/bin);

use Getopt::Std;
use File::Basename;
use FFmpeg::Command;
use FFprobe;

$|++;

###############################
sub _encode_mp3 {
  my ($input_file, $output_dir, $album, $starting_track) = @_;

  my %tags = ();
  my $track_number;
  my $mp4 = FFprobe->probe_file($input_file);
  my $base_output_file = basename($input_file);
  $base_output_file =~ s/\.\w+$//;

  if (exists $mp4->{format}->{‘TAG:comment’}) {
    $tags{genre} = $mp4->{format}->{‘TAG:comment’};
    $tags{genre} =~ s/("’)//g;
  }

  if (exists $mp4->{format}->{‘TAG:genre’}) {
    $tags{genre} = $mp4->{format}->{‘TAG:genre’};
    $tags{genre} =~ s/("’)//g;
  }

  if (exists $mp4->{format}->{‘TAG:artist’}) {
    $tags{artist} = $mp4->{format}->{‘TAG:artist’};
    $tags{artist} =~ s/("’)//g;
  }

  if ($album) {
    $tags{album} = $album;
  } elsif (exists $mp4->{format}->{‘TAG:album’}) {
    $tags{album} = $mp4->{format}->{‘TAG:album’};
  }

  $tags{album} =~ s/("’)//g;
  $track_number = $starting_track if $starting_track;

  foreach my $chapter (sort keys %{$mp4->{chapters}}) {
    unless ($starting_track) {
      $track_number = $chapter;
    }

    my $output_file = sprintf "%s/%03d %s.mp3", $output_dir, $track_number, $tags{album};
    my $start = $mp4->{chapters}->{$chapter}->{start};
    my $duration = $mp4->{chapters}->{$chapter}->{end} - $start;
    my @options = ();

    if ($album) {
      $tags{title} = sprintf "%03d – %s", $track_number, $album;
    } else {
      if (exists $mp4->{format}->{‘TAG:title’}) {
        $tags{title} = sprintf "%03d – %s", $track_number, $mp4->{format}->{‘TAG:title’};
      } else {
        $tags{title} = sprintf "%03d – %s", $track_number, $base_output_file;
      }
    }

    $tags{title} =~ s/("’)//g;

    my $ffmpeg = FFmpeg::Command->new;

    $ffmpeg->input_options({
        file => $input_file,
     });

    $ffmpeg->output_options({
     ‘file’ => $output_file,
     ‘audio_codec’ => ‘libmp3lame’,
     ‘audio_bit_rate’ => 64,
     });

    printf "Converting \"%s\" to \"%s\"\n", basename($input_file), $output_file;

    foreach my $tag (keys %tags) {
      push @options, ‘-metadata’, $tag . "=" . $tags{$tag};
      printf "\t%s: %s\n", $tag, $tags{$tag};
    }

    push @options,
      ‘-metadata’ => ‘track=’ . $track_number,
      ‘-ss’ => $start,
      ‘-t’ => $duration;

    printf "\ttrack: %d\n", $track_number;

    $ffmpeg->options(
      @options
    );

    $ffmpeg->exec();
    print "\t… COMPLETE\n";
    $track_number++ if $starting_track;
  }
}
###############################

my %arg_options = ();
getopts(‘a:i:o:t:’, \%arg_options);

if ($arg_options{i} && $arg_options{o}) {
  my $input_file = $arg_options{i};
  my $output_dir = $arg_options{o};
  my $starting_track = $arg_options{t};
  my $album = $arg_options{a};

  if (-f $input_file && -d $output_dir) {
    _encode_mp3($input_file, $output_dir, $album, $starting_track);
  } else {
    warn ("Unable to find file: \"" . $input_file . "\"\n") unless -f $input_file;
    warn ("Unable to find dir: \"" . $output_dir . "\"\n") unless -f $output_dir;
  }
}

I have multiple audiobook files (m4b) that ffprobe is able to retrieve the chapters from just fine… except the chapter information is printed to stderr and never in the formatted (STDOUT) output. The Perl module FFprobe doesn’t handle the chapters so I submitted feature request #73803

Feature request is to format the chapter output.

jason@jason-Inspiron-1545 ~/bin $ ffprobe "/home/jason/Audiobooks/Ben Bova/Mars/Mars 1.m4b" 1>/dev/null
….
  libavutil    51.  7. 0 / 51.  7. 0
  libavcodec   53.  5. 0 / 53.  5. 0
  libavformat  53.  2. 0 / 53.  2. 0
  libavdevice  53.  0. 0 / 53.  0. 0
  libavfilter   2.  4. 0 /  2.  4. 0
  libswscale    2.  0. 0 /  2.  0. 0
  libpostproc  52.  0. 0 / 52.  0. 0
[mov,mp4,m4a,3gp,3g2,mj2 @ 0xddfac0] max_analyze_duration reached
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from ‘/home/jason/Audiobooks/Ben Bova/Mars/Mars 1.m4b’:
  Metadata:
    major_brand     : M4B
    minor_version   : 0
    compatible_brands: M4B mp42isom
    creation_time   : 2009-09-08 16:19:29
    album           : Mars
    artist          : Ben Bova
    genre           : Audiobook
  Duration: 03:51:23.41, start: 0.000000, bitrate: 81 kb/s
    Chapter #0.0: start 0.000000, end 2779.567914
    Metadata:
      title           : Mars – 01 of 24
    Chapter #0.1: start 2779.567914, end 5555.049161
    Metadata:
      title           : Mars – 02 of 24
    Chapter #0.2: start 5555.049161, end 8334.617075
    Metadata:
      title           : Mars – 03 of 24
    Chapter #0.3: start 8334.617075, end 11110.098322
    Metadata:
      title           : Mars – 04 of 24
    Chapter #0.4: start 11110.098322, end 13883.419864
    Metadata:
      title           : Mars – 05 of 24
    Stream #0.0(und): Audio: aac, 44100 Hz, stereo, s16, 80 kb/s
    Metadata:
      creation_time   : 2009-09-08 16:19:29
    Stream #0.1(eng): Subtitle: text / 0×74786574
    Metadata:
      creation_time   : 2009-09-08 17:31:00
Unsupported codec with id 94213 for input stream 1
jason@jason-Inspiron-1545 ~/bin $

patch to add m4b chapter support:

82c82
< my ($tree, $branch, $tag, $stream);

>     my ($tree, $branch, $tag, $stream, $chapter);
100c100,108
< }

>   } elsif ($line =~ m/Chapter \#(\d+\.*\d+): start (\d+\.*\d+)\, end (\d+\.*\d+)/i) {
>       my ($start, $end) = ($2, $3);
>       $chapter = $1;
>       $chapter =~ s/\.//g;
>       $chapter =~ s/^0+(\d)/$1/;
>
>       $$tree{chapters}{$chapter} = { start => $start, end => $end };
>     } elsif ($line =~ /title\s+: (.+)$/) {
>       $$tree{chapters}{$chapter}{title} = $1;
101a110
>   }

Run multiple versions of Perl from your home directory using Perlbrew!

One thing to know:  Perl won’t compile if you have an encrypted home directory. :( Bug has been fixed but not exactly how and it hasn’t been put into the Ubuntu updates yet.
Thanks goes to Kang-min Liu for creating Perlbrew!!!! :)

The recommended way to install perlbrew is to run these statements in your shell:

    curl -LO http://xrl.us/perlbrew
    chmod +x perlbrew
    ./perlbrew install

After that, perlbrew installs itself to ~/perl5/perlbrew/bin, and you should follow the instruction on screen to setup your .bashrc or .cshrc to put it in your PATH.

Perl Books online

Modern Perl  by chromaticModern Perl by chromatic (FREE ebook!)

and

The Perl Language Reference Manual (for Perl version 5.12.1)

The Perl Language Reference Manual (for Perl version 5.12.1)

If you’re getting the “Can’t locate Catalyst/Engine/HTTP/Restarter.pm” error message, it is very likely you’re running a recent version of Catalyst:

jason@catalyst:~/catalyst-book-code/Chapter_3/LolCatalyst-Lite$ script/lolcatalyst_lite_server.pl -r
Can‘t locate Catalyst/Engine/HTTP/Restarter.pm in @INC (@INC contains: /home/jason/catalyst-book-code/Chapter_3/LolCatalyst-Lite/script/../lib /etc/perl /usr/local/lib/perl/5.10.1 /usr/local/share/perl/5.10.1 /usr/lib/perl5 /usr/share/perl5 /usr/lib/perl/5.10 /usr/share/perl/5.10 /usr/local/lib/site_perl .). at /usr/lib/perl5/Class/MOP.pm line 116
        Class::MOP::load_first_existing_class(‘
Catalyst::Engine::HTTP::Restarter‘) called at /usr/lib/perl5/Class/MOP.pm line 121
        Class::MOP::load_class(‘
Catalyst::Engine::HTTP::Restarter‘) called at /usr/share/perl5/Catalyst.pm line 2634
        Catalyst::setup_engine(‘
LolCatalyst::Lite‘, undef) called at /usr/share/perl5/Catalyst.pm line 1081
        Catalyst::setup(‘
LolCatalyst::Lite‘) called at /home/jason/catalyst-book-code/Chapter_3/LolCatalyst-Lite/script/../lib/LolCatalyst/Lite.pm line 34
        require LolCatalyst/Lite.pm called at script/lolcatalyst_lite_server.pl line 55
Compilation failed in require at script/lolcatalyst_lite_server.pl line 55.

The problem is that Catalyst::Engine::HTTP::Restarter within Catalyst::Engine was replaced by Catalyst::Restarter within the Catalyst::Devel package. How to fix your application to use the updated development web server? Very easy. Rerun catalyst.pl with the “-scripts”, to rebuild just the scripts in the script directory, and “-force” to overwrite any files therein:

jason@catalyst:~/catalyst-book-code/Chapter_3$ catalyst.pl -scripts -force LolCatalyst::Lite
 exists "LolCatalyst-Lite/script/lolcatalyst_lite_cgi.pl"
created "LolCatalyst-Lite/script/lolcatalyst_lite_cgi.pl"
 exists "LolCatalyst-Lite/script/lolcatalyst_lite_fastcgi.pl"
created "LolCatalyst-Lite/script/lolcatalyst_lite_fastcgi.pl"
 exists "LolCatalyst-Lite/script/lolcatalyst_lite_server.pl"
created "LolCatalyst-Lite/script/lolcatalyst_lite_server.pl"
 exists "LolCatalyst-Lite/script/lolcatalyst_lite_test.pl"
created "LolCatalyst-Lite/script/lolcatalyst_lite_test.pl"
 exists "LolCatalyst-Lite/script/lolcatalyst_lite_create.pl"
created "LolCatalyst-Lite/script/lolcatalyst_lite_create.pl"

If you’re interested in learning the Catalyst Web Framework (Perl based), I highly recommend The Definitive Guide to Catalyst: Writing Extensible, Scalable and Maintainable Perl–Based Web Applications.