#!/usr/bin/perl -w

# If your Perl implementation resides somewhere other than
# /usr/bin/perl, change the above line to reflect that.

## Created:    <Sun Oct 15 02:57:05 EDT 2000>
## Time-stamp: <2002-04-25 10:23:44 foof>
## Author:     Alex Shinn <foof@debian.org>

=head1 NAME

pogg - Perl Ogg Vorbis Utility

=head1 SYNOPSIS

  pogg [play]  [options] [file1.ogg file2.ogg...]
  pogg encode  [options] [file1.wav file2.wav...]
  pogg decode  [options] [file1.ogg file2.ogg...]
  pogg convert [options] [file1.mp3 file2.mp3...]
  pogg comment [options] [file1.ogg file2.ogg...]

=head1 DESCRIPTION

pogg is a general purpose tool for playing, encoding (from .wav),
decoding (to .wav), converting (from .mp3) and commenting Ogg Vorbis
files.  External programs must be used to handle mp3 files, and
currently pogg cannot write .ogg files (and thus cannot modify
comments, encode to .ogg, or convert from mp3).

=cut

# Modules
use strict;
use Getopt::Long;
use Ogg::Vorbis;
use Ao;

# Variables
my $VERSION = '0.1';
my $MODIFIED = '2000/10/23';

# Config variables (external programs)
my $mp3play = 'mpg123';
my $mp3info = 'mp3info';
my %mp3play_opts = ('buffer' => '-b',
                    'verbose' => '-v',
                    'quiet' => '-q');
my %mp3info_opts = ('TITLE'  => '-t',
                    'ARTIST' => '-a',
                    'ALBUM'  => '-l',
                    'GENRE'  => '-G');

# Data variables
my @devices = ();  # devices to play to
my @list = ();     # List of sources to act on
my $buffer;        # raw data buffer
my $bitstream=0;   # current bitstream
my $big_endian_p = 0;
my $ogg = Ogg::Vorbis->new;  # Ogg Vorbis Stream

# Separate option modes
my %opts = ('bits' => 16,
            'rate' => 44100,
            'channels' => 2,
            'buffer' => 4096);
my @play_opts = ('help|h|?!','pogg-version|version|V!','verbose|v!',
                 'quiet|q!','device|dev|d=s@','bits|b=i','rate|r=i',
                 'channels|c=i','buffer|s=i','random|rand|R!',
                 'sort|S=s');
my @comment_opts = ('help|h|?!','pogg-version|V!','title|t:s',
                    'VERSION|v:s','ALBUM|l:s','ARTIST|a:s',
                    'ORGANIZATION|o:s','DESCRIPTION|D:s','GENRE|g:s',
                    'DATE|d:s','LOCATION|p:s','COPYRIGHT|c:s',
                    'comment|C:s%');
my @opts = @play_opts;  # most commands use the play_opts
my @known_comments = ('TITLE','VERSION','ALBUM','ARTIST','ORGANIZATION',
                      'DESCRIPTION','GENRE','DATE','LOCATION','COPYRIGHT');

# Get command (sets default devices, etc.)
my $command = shift;
if ($command eq 'play') {
} elsif ($command eq 'encode') {
  die "Sorry, encode not yet supported\n";
} elsif ($command eq 'decode') {
} elsif ($command eq 'convert') {
  die "Sorry, convert not yet supported\n";
} elsif ($command eq 'comment') {
  @opts = @comment_opts;
} else {
  # No command specified... default to play and push back the arg.
  unshift @ARGV, $command;
  $command = 'play';
}


=head1 OPTIONS

=over 4

=item -h, --help

Print a brief usage message.

=item -v, --version

Display version info.

=item -d, --device DEVICE

Add a device to play to.  The device may be any of oss, esd, alsa,
irix or solaris as the output device, or wav to write to a wav file.
You may specify additional options to a device by appending a colon
and comma-separated list of key=value pairs.  Examples:

  pogg file.ogg -d wav:file=output.wav
  pogg file.ogg -d esd -d alsa:card=0,dev=1

=item -b, --bits BITS

Set the number of bits per sample (8 or 16, default 16).

=item -r, --rate RATE

Set the number of samples per second (default 44100).

=item -c, --channels CHANNELS

Set the number of channels (1 = mono, 2 = stereo).

=item -s, --buffer SIZE

Set buffer size (default 4096).

=back

=cut

# Parse Options
GetOptions(\%opts, @opts) or usage();
usage() if $opts{help};
if ($opts{version}) { version(); exit; }
# Create buffer (just a string to store data)
$buffer = 'x' x $opts{buffer};
# If no files, read file list from stdin
@list = @ARGV;
unless (@list) {
  while (<>) {
    chomp;
    # Modify to recognize external (http) resources
    if (-e $_) {
      unshift @list, $_;
    }
  }
}
# Sort or shuffle list (may have to wait until we have more info on
# sources).  XXXX Add sort by genre, time, file type, etc.
if ($opts{random} || (exists $opts{sort} &&
                             ($opts{sort} =~ /^rand(om)?$/))) {
  my $temp;
  for (my $i=0; $i<@list; $i++) {
    my $j = int(rand(@list));
    $temp = $list[$i];
    $list[$i] = $list[$j];
    $list[$j] = $temp;
  }
} elsif ($opts{sort}) {
  if ($opts{sort} =~ /order|given|default/) {
    # default, don't sort
  } elsif ($opts{sort} =~ /^(asc(end(ing)?)?|alpha)$/i) {
    @list = sort @list;
  } elsif ($opts{sort} =~ /^(de?sc(end(ing)?)?|omega)$/i) {
    @list = sort {$b cmp $a} @list;
  } else {
    # unkown method, give warning and leave list alone
    warn "unknown sort method: $opts{sort}\n";
  }
}
# Handle special commands
if ($command eq 'convert') {
  do_convert();
  exit;
} elsif ($command eq 'comment') {
  do_comments();
  exit;
} else {
  # Configure default output device
  unless ($opts{device}) {
    if ($command eq 'play') {
      $opts{device} = ['oss'];
    } elsif ($command eq 'decode') {
      $opts{device} = ['wav'];
    }
  }
  # Open devices (change concept here... allow for default FILE.wav
  # instead of output.wav).
  open_devices();
  play_files();
  close_devices();
}


# Try to play all listed files
# XXXX rewrite as play_list(@) for recursion
sub play_files {
  no strict qw(subs);
  version() unless $opts{quiet};
  foreach my $source (@list) {
    print "\nPlaying $source\n" if ($opts{verbose});
    if ($source =~ /\.ogg$/) {
      # Open the .ogg
      unless (open(INPUT, "< $source")) {
        warn "couldn't open $source\n";
        next;
      }
      if ($ogg->open(INPUT) < 0) {
        warn "$source does not appear to be an Ogg Vorbis bitstream\n";
        next;
      } else {
      # Play the .ogg
        unless ($opts{quiet}) {
          write_info($ogg);
          print "\n";
        }
        my $t_time = $ogg->time_total;
        my $t_min = int($t_time / 60);
        my $t_sec = $t_time % 60;
        while (1) {
          my $bytes = $ogg->read($buffer, $opts{buffer}, $big_endian_p,
                                 2, 1, $bitstream);
          if ($bytes == 0) {
            # EOS
            last;
          } elsif ($bytes < 0) {
            # Error
            warn "error in stream\n" unless $opts{quiet};
          } else {
            # Successful read, play on each device
            foreach my $ao (@devices) {
              $ao->play($buffer, $bytes);
            }
            # Print info if verbose
            if ($opts{verbose}) {
              my $c_time = $ogg->time_tell;
              my $c_min = int($c_time / 60);
              my $c_sec = $c_time - 60 * $c_min;
              my $r_min = int(($t_time - $c_time) / 60);
              my $r_sec = ($t_time - $c_time) - 60 * $r_min;
              printf STDERR "\rTime: %02li:%05.2f [%02li:%05.2f] %.1f kbit/s  \r",
                     $c_min, $c_sec, $r_min, $r_sec, $ogg->bitrate_instant / 1000;
            }
          }
        }
        printf STDERR "\r%s\r\n", ' ' x 60;
      }
    } elsif ($source =~ /\.mp3$/) {
      # Translate our options into the mp3 player's options
      my $command_line = $mp3play;
      while (my ($k, $v) = each(%opts)) {
        if (exists $mp3play_opts{$k}) {
          $command_line .= " \"$mp3play_opts{$k}\" \"$v\"";
        }
      }
      # Call the external player, let it handle warnings/errors.
      print "$command_line \"$source\"\n" unless $opts{quiet};
      `$command_line $source`;
    } else {
      warn "unknown format: $source\n";
    }
  }
}


# Convert between ogg and mp3
sub do_convert {
  no strict qw(subs);
  foreach my $source (@list) {
  }
}


# Display comments (XXXX add modify when we can write)
sub do_comments {
  no strict qw(subs);
  foreach my $source (@list) {
    print "Commenting $source\n" if ($opts{verbose});
    if ($source =~ /\.ogg$/) {
      # Comment .ogg
      unless (open(INPUT, "< $source")) {
        warn "couldn't open $source\n";
        next;
      }
      if ($ogg->open(INPUT) < 0) {
        warn "$source does not appear to be an Ogg Vorbis bitstream\n";
        next;
      } else {
        # XXXX when available, add code to modify info
        write_info($ogg);
        $ogg->clear;
        close(INPUT);
      }
    } elsif ($source =~ /\.mp3/) {
      # Comment .mp3
      my $command_line = $mp3info;
      # Check known comments
      foreach my $k (@known_comments) {
        if ((exists $mp3info_opts{$k}) and (exists $opts{$k})) {
          $command_line .= " \"$mp3info_opts{$k}\" \"$opts{$k}\"";
        }
      }
      # Check for anything mp3info knows about that we don't
      while (my ($k, $v) = each(%{$opts{comment}})) {
        if (exists $mp3info_opts{$k}) {
          $command_line .= " \"$mp3info_opts{$k}\" \"$v\"";
        }
      }
      # Now run the command (die on error... we don't want to muck up
      # multiple files).
      open(OUTPUT, "$command_line $source |") ||
          die "couldn't run \`$command_line $source \`\n";
      while (<OUTPUT>) { print }
      close(OUTPUT);
    } else {
      warn "unkown format: $source\n";
    }
  }
}


# Open device and place in global @devices list
sub open_devices {
  foreach my $dev (@{$opts{device}}) {
    my $opt_string = '';
    my %dev_opts = ();
    if ($dev =~ /([^:]*):(.*)/) {
      $dev = $1;
      $opt_string = $2;
      %dev_opts = split(/[,=]/, $opt_string);
    }
    my $dev_id = Ao::get_driver_id($dev);
    my $ao = Ao::open($dev_id, $opts{bits}, $opts{rate},
                      $opts{channels}, \%dev_opts) ||
       die "error: couldn't open device $dev\n";
    unshift @devices, $ao;
  }
}


# Close devices in @devices list
sub close_devices {
  foreach my $ao (@devices) {
    $ao->close;
  }
}


sub write_info {
  # Short, one line comment format like mp3info, followed by
  # longer comments.
  my %comments = %{$ogg->comment};
  my $info = $ogg->info;
  my $time = $ogg->time_total;
  my $min = int($time / 60);
  my $sec = $time % 60;
  my $rate = int($ogg->bitrate / 1024);
  # Try to print like mp3info, add album because it's nice to know
  print "$comments{TITLE}($comments{ARTIST}, $comments{ALBUM})",
        ", $rate kbit/s ", ($info->channels == 2)?'stereo':'mono',
        " (${min}m ${sec}s)\n";
  # Now print other known comments.
  while (my ($k, $v) = each(%comments)) {
    # XXXX Format nicer
    if (grep {/^\Q$k\E$/} @known_comments) {
      print "$k: $v\n" unless grep {/^\Q$k\E$/} ('TITLE','ARTIST','ALBUM');
    } else {
      warn "unknown comment: $k\n";
    }
  }
}


# Print a usage summary
sub usage {
  print <<EOF;
usage: $0 [options] [command] [file ...]

  -h, --help                 display this message
  -v, --version              print version info
  -d, --device DEVICE        add a device to play to
  -b, --bits BITS            set bits per sample (8 or 16)
  -r, --rate RATE            set samples per second (default 44100)
  -c, --channels CHANNELS    set channels (1 = mono, 2 = stereo)
  -s, --buffer SIZE          set default buffer size
  -S, --sort METHOD          specify a sorting method
  -R, --random               shuffle files (alias for --sort random)

where COMMAND is any of play, encode, decode, convert or comment, and
DEVICE is any of oss, esd, alsa, irix, solaris or wav.  The device may
be followed by an optional colon with a comma separated list of
key=value parameters, such as --device wav:file=output.wav.
EOF
  exit 0;
}

# Print version info
sub version {
  print "pogg $VERSION ($MODIFIED)\n";
}


=head1 AUTHOR

Alex Shinn, foof@synthcode.com

=head1 SEE ALSO

Ogg::Vorbis(3pm), Ao(3pm), ogg123(1), oggenc(1), perl(1).

=cut