Tag Archive: mp3


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;
  }
}

Yeah, it’s Who Better Than Me by Phil Collins with the Turk and Tarzan names changed but it covers just about any two closely aged siblings IMHO:

Miriam
You’re one of a kind, I can’t explain it.
You’re kind of cool, in a wonderful way.
Though you’re weird, you can make it.
And who better than me to teach you.
Who better then me to set you on your way. (Mhm)

This could take some hangin’ in there
Though with persuasion I can take you on up
Make you grow up, beside the others
And who better than me to lead you
Who better than me to take you all the way (Hey hey hey yeah)

Asher & Miriam
Struggling along for years and years

Miriam
Until I came along for you
Now its all comin’ together

Asher & Miriam
And together will see this through
You for me and me for you


Asher
I can learn, I can listen
I know there’s something
Deep inside but
I need assistance to go the distance

Miriam
And who better than me

Asher
To teach me

Miriam
Who better than me

Asher
To tell me all you know

Miriam

Who better than me

Asher
You reach me

Miriam
Who better than me

Asher & Miriam
To show them all we know!

Yesterday, I posted Howto: Convert your mp3 tags (id3v2 to id3v1) so your Playstation 3 can play your MP3s! and it worked fine but there was one little problem with it.

When we processed the files, namely running eye3D, we did so synchronously. Essentially, the file notification came in from the Linux kernel and we processed the file at that time. This may be an issue of overflowing the inotify queue within the Linux kernel if there are a lot of files to process.

A better solution would be to add the file to an internal queue and process the files in a sub process using POE::Wheel::Run. Of course we will limit the number of sub processes :)

#!/usr/bin/perl

use strict;
use warnings;

use File::Basename;
use File::Find ();
use Getopt::Std;
use Linux::Inotify2;
use POE qw( Kernel Session Wheel::Run );

$|++;

#######################################
#######################################

our @found_dirs;
our $max_concurrent_tasks;

sub watch_add_dir {
 my ($heap_ref, $session, $dir_name) = @_;

 ##############
 # Watch this directory with a call back
 #  to the watch_hdlr() subroutine via
 #  a message to the POE system
 ##############
 $heap_ref->{inotify}->watch($dir_name, IN_CREATE|IN_CLOSE_WRITE, $session->postback("watch_hdlr"));
 print " Watching directory $dir_name\n";
}

sub watch_hdlr {
 my ($kernel, $heap, $session, $event) = ( $_[KERNEL], $_[HEAP], $_[SESSION], $_[ARG1][0] );

 my $name = $event->fullname;
 my $short_name = $event->name;

 ##############
 # We can receive many many notifications
 #  for a file.  If we’ve already processed
 #  the file, do nothing.
 ##############
 unless ($heap->{inotify}{files}{$name}) {

  ##############
  # If a new directory is added, we need
  #  to watch that directory too.
  ##############
  if ($event->IN_CREATE && -d $name) {
   print "New directory: $name\n";
   watch_add_dir($heap, $session, $name);
   } elsif ($event->IN_CLOSE_WRITE) {

   ##############
   # When a file descriptor that was opened for
   #  ’writing’ is closed, then process that
   #  file it was being written to.  We’re
   #  assuming that the file is complete at this
   #  point as the operation will be a copy into
   #  the watched directory
   ##############
   my $ext = ( fileparse($name, ‘\..*’) )[2];

   if (lc($ext) eq ‘.mp3′) {
    ##############
    # Add the file to the file process queue
    ##############
    push @{ $heap->{task}{task_files} }, $name;

    ##############
    # Mark that we have processed the file.  If
    #  we don’t we will end up processing the file
    #  in an infinite loop because we are modifying
    #  the files.
    ##############
    $heap->{inotify}{files}{$name} = 1;

    ##############
    # Yield to "task_next_file" through so
    #  that we can process files in the queue.
    ##############
    $kernel->yield("task_next_file");
   }

   $heap->{inotify}{files}{$name} = 1;
  }
 }

 ##############
 # While possible, it is highly unlikely that we will
 #  overflow the notification buffers within the Linux
 #  kernel.  If so, we should report that.
 ##############
  print "events for $name have been lost\n" if $event->IN_Q_OVERFLOW;
}

sub task_next_file {
 my ($kernel, $heap) = @_[ KERNEL, HEAP ];

 ##############
 # Process the files in the queue up
 #  to the $max_concurrent_tasks at
 #  once.  Any extras will be processed
 #  when a file (task) completes.
 ##############
 while ( keys( %{ $heap->{task} } ) < $max_concurrent_tasks ) {
  my $next_task_file = shift @{ $heap->{task}{task_files} };

  ##############
  # If the $next_task_file is empty, then we can safely
  #  ignore it.
  ##############
  last unless defined $next_task_file;

  ##############
  # Use POE::Wheel::Run to fire off the
  #  file processing using a sub process
  #  to the process_file() subroutine
  ##############
  my $task = POE::Wheel::Run->new (
    Program => sub { process_file($next_task_file) },
    StdoutEvent => "task_output",
    CloseEvent => "task_done",
   );

  ##############
  # Update the session with the task
  #  information and the kernel with
  #  the SIG_CHILD handler.  These are
  #  necessary for the task to execute.
  ##############
  $heap->{task}->{ $task->ID } = $task;
  $kernel->sig_child( $task->PID, "sig_child");
 }
}

sub process_file {
 my $file = shift;

 print "  Processed \"$file\"\n";

 ##############
 # Use the eyeD3 package to convert
 #  the mp3 id3v2/3/4 to id3v1.  If
 #  eyeD3 fails, we don’t really care. :)
 ##############
 my $cmd_output = `eyeD3 –to-v1.1 "$file"`;
 $cmd_output = `eyeD3 –remove-v2 "$file"`;
}

sub find_wanted {
 my $object = $File::Find::name;

 if (-d $object) {
  push @found_dirs, $object;
 }
}

#######################################
#######################################
#######################################

my %arg_options;
my $watch_dir;

getopts(‘d:t:’, \%arg_options);

if ($arg_options{d} && -d $arg_options{d}) {
 $watch_dir = $arg_options{d};

 if ($arg_options{t} && $arg_options{t} =~ /^\d+/) {
  $max_concurrent_tasks = $arg_options{t};
 } else {
  $max_concurrent_tasks = 2;
 }

 ##############
 # We need to watch all existing sub directories
 #  so we will find them and add them to the
 #  @found_dirs array to be added to the watched
 #  directories when we create the Inotify object
 ##############
 File::Find::find({wanted => \&find_wanted}, $watch_dir);

 POE::Session->create
  ( inline_states =>
   { _start => sub {
     my $inotify_FH;

     ##############
     # alias this particular POE session to
     #  ’notify’ so we can easily reference
     #  it later if needed
     ##############
     $_[KERNEL]->alias_set(‘notify’);

     ##############
     # Create the Linux::INotify object
     ##############
     $_[HEAP]{inotify} = new Linux::Inotify2
      or die "Unable to create new inotify object: $!";

     ##############
     # Add the preexisting directories to
     #  be watched from the @found_dirs array
     ##############
     foreach my $dir (@found_dirs) {
      watch_add_dir($_[HEAP], $_[SESSION], $dir);
     }

     ##############
     # We need to create a hash in the "notify"
     #  POE session so we can determine if we’ve
     #  processed a file already
     ##############
     $_[HEAP]{inotify}{files} = {};

     ##############
     # The Inotify notifications are received
     #  on a file descriptor.  We need to read
     #  from it when there is something to be
     #  read
     ##############
     open $inotify_FH, "< &=" . $_[HEAP]{inotify}->fileno
      or die "Can’t fdopen: $!\n";

     ##############
     # Inform POE to poll the file descriptor
     ##############
     $_[KERNEL]->select_read( $inotify_FH, "inotify_poll" );
    },
    inotify_poll => sub {
     $_[HEAP]{inotify}->poll;
    },
    watch_hdlr => \&watch_hdlr,

    ##############
    # Process the next file in the queue
    ##############
    task_next_file => \&task_next_file,

    ##############
    # print the output of the job
    ##############
    task_output => sub {
     my $result = $_[ARG0];

     print "$result\n";
    },

    ##############
    # When we are done with a file, go process the
    #  next file if there is one waiting
    ##############
    task_done => sub {
     my ($kernel, $heap, $task_id) = @_[ KERNEL, HEAP, ARG0 ];

     delete $heap->{task}{$task_id};
     $kernel->yield("task_next_file");
    },
    sig_child => sub {
     my ($heap, $pid) = @_[ HEAP, ARG1 ];

     my $details = delete $heap->{$pid};
    },
   },
 );

 POE::Kernel->run();
}

exit 0;

Example output:

ps3_mp3_converter.pl -d /home/jfroebe/j
 Watching directory /home/jfroebe/j
 Watching directory /home/jfroebe/j/bin
 Watching directory /home/jfroebe/j/doc
 Watching directory /home/jfroebe/j/java
 Watching directory /home/jfroebe/j/lib
 Watching directory /home/jfroebe/j/j
 Watching directory /home/jfroebe/j/j/tmp
 Watching directory /home/jfroebe/j/sdk
 Watching directory /home/jfroebe/j/sdk/demo
 Watching directory /home/jfroebe/j/sdk/include
New directory: /home/jfroebe/j/Earth Final Conflict Soundtrack
 Watching directory /home/jfroebe/j/Earth Final Conflict Soundtrack
  Processed "/home/jfroebe/j/Earth Final Conflict Soundtrack/01 Main Title.mp3"
  Processed "/home/jfroebe/j/Earth Final Conflict Soundtrack/02 The Scret of Strandhill-Redemption.mp3"
  Processed "/home/jfroebe/j/Earth Final Conflict Soundtrack/03 Old Flame.mp3"
  Processed "/home/jfroebe/j/Earth Final Conflict Soundtrack/04 Defector.mp3"
  Processed "/home/jfroebe/j/Earth Final Conflict Soundtrack/05 Decidion.mp3"
  Processed "/home/jfroebe/j/Earth Final Conflict Soundtrack/06 Float Like a Butterfly.mp3"
  Processed "/home/jfroebe/j/Earth Final Conflict Soundtrack/07 Sandoval’s Run.mp3"
  Processed "/home/jfroebe/j/Earth Final Conflict Soundtrack/08 Bliss.mp3"
  Processed "/home/jfroebe/j/Earth Final Conflict Soundtrack/09 If You Could Read My Mind.mp3"
  Processed "/home/jfroebe/j/Earth Final Conflict Soundtrack/10 Lilli.mp3"
  Processed "/home/jfroebe/j/Earth Final Conflict Soundtrack/11 Law and Order.mp3"
  Processed "/home/jfroebe/j/Earth Final Conflict Soundtrack/12 Atavus.mp3"
  Processed "/home/jfroebe/j/Earth Final Conflict Soundtrack/13 Between Heaven and Hell.mp3"
  Processed "/home/jfroebe/j/Earth Final Conflict Soundtrack/14 Sleepers.mp3"
  Processed "/home/jfroebe/j/Earth Final Conflict Soundtrack/15 Dimensions.mp3"
  Processed "/home/jfroebe/j/Earth Final Conflict Soundtrack/16 Moonscape.mp3"
  Processed "/home/jfroebe/j/Earth Final Conflict Soundtrack/17 Isabel.mp3"
  Processed "/home/jfroebe/j/Earth Final Conflict Soundtrack/18 The Gauntlet.mp3"
  Processed "/home/jfroebe/j/Earth Final Conflict Soundtrack/19 Second Chances.mp3"
  Processed "/home/jfroebe/j/Earth Final Conflict Soundtrack/20 One Man’s Castle.mp3"
  Processed "/home/jfroebe/j/Earth Final Conflict Soundtrack/21 Payback.mp3"
  Processed "/home/jfroebe/j/Earth Final Conflict Soundtrack/22 Truth.mp3"
  Processed "/home/jfroebe/j/Earth Final Conflict Soundtrack/23 Déjà Vu.mp3"
  Processed "/home/jfroebe/j/Earth Final Conflict Soundtrack/24 Crossfire.mp3"
  Processed "/home/jfroebe/j/Earth Final Conflict Soundtrack/25 Volunteers-End Credits.mp3"
  1. Run the converter on your media server: ps3_mp3_converter.pl -d {directory}
  2. Copy your mp3 collection wherever you told ps3_mp3_converter.pl to run in.
#!/usr/bin/perl

use strict;
use warnings;

use File::Basename;
use File::Find ();
use Getopt::Std;
use Linux::Inotify2;
use POE;

$|++;

#######################################
#######################################

our @found_dirs;

sub watch_add_dir {
 my ($heap_ref, $session, $dir_name) = @_;

 $heap_ref->{inotify}->watch($dir_name, IN_CREATE|IN_CLOSE_WRITE, $session->postback("watch_hdlr"));
 print " Watching directory $dir_name\n";
}

sub watch_hdlr {
 my ($heap_ref, $session, $event) = ( $_[HEAP], $_[SESSION], $_[ARG1][0] );

 my $name = $event->fullname;
 my $short_name = $event->name;

 unless ($_[HEAP]{inotify}{files}{$name}) {
  if ($event->IN_CREATE && -d $name) {
   print "New directory: $name\n";
   watch_add_dir($heap_ref, $session, $name);
  } elsif ($event->IN_CLOSE_WRITE) {
   my $ext = ( fileparse($name, ‘\..*’) )[2];

   if (lc($ext) eq ‘.mp3′) {
    print "-"x20 . "\n";
    print "$name:\n";

    my $cmd_output = `eyeD3 –to-v1.1 "$name"`;
    $cmd_output = `eyeD3 –remove-v2 "$name"`;
   }

   $_[HEAP]{inotify}{files}{$name} = 1;
  }
 }

 print "events for $name have been lost\n" if $event->IN_Q_OVERFLOW;
}

sub find_wanted {
 my $object = $File::Find::name;

 if (-d $object) {
  push @found_dirs, $object;
 }
}

#######################################
#######################################
#######################################

my %arg_options;
my $watch_dir;

getopts(‘d:’, \%arg_options);

if ($arg_options{d} && -d $arg_options{d}) {
 $watch_dir = $arg_options{d};
 File::Find::find({wanted => \&find_wanted}, $watch_dir);

 POE::Session->create
  ( inline_states =>
   { _start => sub {
     my $inotify_FH;

     $_[KERNEL]->alias_set(‘notify’);
     $_[HEAP]{inotify} = new Linux::Inotify2
      or die "Unable to create new inotify object: $!";

     foreach my $dir (@found_dirs) {
      watch_add_dir($_[HEAP], $_[SESSION], $dir);
     }

     $_[HEAP]{inotify}{files} = {};

     open $inotify_FH, "< &=" . $_[HEAP]{inotify}->fileno
     or die "Can’t fdopen: $!\n";

     $_[KERNEL]->select_read( $inotify_FH, "inotify_poll" );
   },
   inotify_poll => sub {
    $_[HEAP]{inotify}->poll;
   },
    watch_hdlr => \&watch_hdlr,
   },
 );

 POE::Kernel->run();
}

exit 0;

5-in-1 White Charger/Holder/Fm Transmitter/LCD/Audio for 2.5mm and 3.5 mm/Usb Charger

I was more than a little skeptical at this iPod/mp3 player/cell phone/usb charger & FM transmitter.  There weren’t any reviews that I could find whatsoever on this contraption.  After looking around and seeing other combo units at about $90, I figured, that I would get one of these and worst case, I’d be out $20.

Well, it arrived today when I was working at home.  Of course, I had to take a little time and try it out.  Since I’ve been sick all week, I’m more than a little stir crazy to get out of the apartment even if it was just a jaunt to Starbucks and back.

Hooking it up was simple:

  • slide the side grips apart
  • insert ipod
  • attach ipod cable to bottom of ipod – it is the only cable to attach :)
  • plug into cigarette lighter

To turn it on, you hold down the power button for about 3 – 5 seconds and tune the fm transmitter to the desired channel.

That’s it.  The only downside I’ve found is that the FM tuner doesn’t retain the channel frequency so you will have to tune the FM channel everytime you turn it on.  I consider this a minor downside and one that I can live with.

Hooking up other mp3 players is done by attaching the usb cable to the mp3 player and the audio cable to the mp3 player.   Both cables are provided.  Charging a cell phone will work as long as you have the (not provided) usb <=> cell phone cable.