1#!/usr/bin/perl -w
2
3eval 'exec /usr/bin/perl -w -S $0 ${1+"$@"}'
4    if 0; # not running under some shell
5
6# If your Perl implementation resides somewhere other than
7# /usr/bin/perl, change the above line to reflect that.
8
9## Created:    <Sun Oct 15 02:57:05 EDT 2000>
10## Time-stamp: <2002-04-25 10:23:44 foof>
11## Author:     Alex Shinn <foof@debian.org>
12
13=head1 NAME
14
15pogg - Perl Ogg Vorbis Utility
16
17=head1 SYNOPSIS
18
19  pogg [play]  [options] [file1.ogg file2.ogg...]
20  pogg encode  [options] [file1.wav file2.wav...]
21  pogg decode  [options] [file1.ogg file2.ogg...]
22  pogg convert [options] [file1.mp3 file2.mp3...]
23  pogg comment [options] [file1.ogg file2.ogg...]
24
25=head1 DESCRIPTION
26
27pogg is a general purpose tool for playing, encoding (from .wav),
28decoding (to .wav), converting (from .mp3) and commenting Ogg Vorbis
29files.  External programs must be used to handle mp3 files, and
30currently pogg cannot write .ogg files (and thus cannot modify
31comments, encode to .ogg, or convert from mp3).
32
33=cut
34
35# Modules
36use strict;
37use Getopt::Long;
38use Ogg::Vorbis;
39use Ao;
40
41# Variables
42my $VERSION = '0.1';
43my $MODIFIED = '2000/10/23';
44
45# Config variables (external programs)
46my $mp3play = 'mpg123';
47my $mp3info = 'mp3info';
48my %mp3play_opts = ('buffer' => '-b',
49                    'verbose' => '-v',
50                    'quiet' => '-q');
51my %mp3info_opts = ('TITLE'  => '-t',
52                    'ARTIST' => '-a',
53                    'ALBUM'  => '-l',
54                    'GENRE'  => '-G');
55
56# Data variables
57my @devices = ();  # devices to play to
58my @list = ();     # List of sources to act on
59my $buffer;        # raw data buffer
60my $bitstream=0;   # current bitstream
61my $big_endian_p = 0;
62my $ogg = Ogg::Vorbis->new;  # Ogg Vorbis Stream
63
64# Separate option modes
65my %opts = ('bits' => 16,
66            'rate' => 44100,
67            'channels' => 2,
68            'buffer' => 4096);
69my @play_opts = ('help|h|?!','pogg-version|version|V!','verbose|v!',
70                 'quiet|q!','device|dev|d=s@','bits|b=i','rate|r=i',
71                 'channels|c=i','buffer|s=i','random|rand|R!',
72                 'sort|S=s');
73my @comment_opts = ('help|h|?!','pogg-version|V!','title|t:s',
74                    'VERSION|v:s','ALBUM|l:s','ARTIST|a:s',
75                    'ORGANIZATION|o:s','DESCRIPTION|D:s','GENRE|g:s',
76                    'DATE|d:s','LOCATION|p:s','COPYRIGHT|c:s',
77                    'comment|C:s%');
78my @opts = @play_opts;  # most commands use the play_opts
79my @known_comments = ('TITLE','VERSION','ALBUM','ARTIST','ORGANIZATION',
80                      'DESCRIPTION','GENRE','DATE','LOCATION','COPYRIGHT');
81
82# Get command (sets default devices, etc.)
83my $command = shift;
84if ($command eq 'play') {
85} elsif ($command eq 'encode') {
86  die "Sorry, encode not yet supported\n";
87} elsif ($command eq 'decode') {
88} elsif ($command eq 'convert') {
89  die "Sorry, convert not yet supported\n";
90} elsif ($command eq 'comment') {
91  @opts = @comment_opts;
92} else {
93  # No command specified... default to play and push back the arg.
94  unshift @ARGV, $command;
95  $command = 'play';
96}
97
98
99=head1 OPTIONS
100
101=over 4
102
103=item -h, --help
104
105Print a brief usage message.
106
107=item -v, --version
108
109Display version info.
110
111=item -d, --device DEVICE
112
113Add a device to play to.  The device may be any of oss, esd, alsa,
114irix or solaris as the output device, or wav to write to a wav file.
115You may specify additional options to a device by appending a colon
116and comma-separated list of key=value pairs.  Examples:
117
118  pogg file.ogg -d wav:file=output.wav
119  pogg file.ogg -d esd -d alsa:card=0,dev=1
120
121=item -b, --bits BITS
122
123Set the number of bits per sample (8 or 16, default 16).
124
125=item -r, --rate RATE
126
127Set the number of samples per second (default 44100).
128
129=item -c, --channels CHANNELS
130
131Set the number of channels (1 = mono, 2 = stereo).
132
133=item -s, --buffer SIZE
134
135Set buffer size (default 4096).
136
137=back
138
139=cut
140
141# Parse Options
142GetOptions(\%opts, @opts) or usage();
143usage() if $opts{help};
144if ($opts{version}) { version(); exit; }
145# Create buffer (just a string to store data)
146$buffer = 'x' x $opts{buffer};
147# If no files, read file list from stdin
148@list = @ARGV;
149unless (@list) {
150  while (<>) {
151    chomp;
152    # Modify to recognize external (http) resources
153    if (-e $_) {
154      unshift @list, $_;
155    }
156  }
157}
158# Sort or shuffle list (may have to wait until we have more info on
159# sources).  XXXX Add sort by genre, time, file type, etc.
160if ($opts{random} || (exists $opts{sort} &&
161                             ($opts{sort} =~ /^rand(om)?$/))) {
162  my $temp;
163  for (my $i=0; $i<@list; $i++) {
164    my $j = int(rand(@list));
165    $temp = $list[$i];
166    $list[$i] = $list[$j];
167    $list[$j] = $temp;
168  }
169} elsif ($opts{sort}) {
170  if ($opts{sort} =~ /order|given|default/) {
171    # default, don't sort
172  } elsif ($opts{sort} =~ /^(asc(end(ing)?)?|alpha)$/i) {
173    @list = sort @list;
174  } elsif ($opts{sort} =~ /^(de?sc(end(ing)?)?|omega)$/i) {
175    @list = sort {$b cmp $a} @list;
176  } else {
177    # unkown method, give warning and leave list alone
178    warn "unknown sort method: $opts{sort}\n";
179  }
180}
181# Handle special commands
182if ($command eq 'convert') {
183  do_convert();
184  exit;
185} elsif ($command eq 'comment') {
186  do_comments();
187  exit;
188} else {
189  # Configure default output device
190  unless ($opts{device}) {
191    if ($command eq 'play') {
192      $opts{device} = ['oss'];
193    } elsif ($command eq 'decode') {
194      $opts{device} = ['wav'];
195    }
196  }
197  # Open devices (change concept here... allow for default FILE.wav
198  # instead of output.wav).
199  open_devices();
200  play_files();
201  close_devices();
202}
203
204
205# Try to play all listed files
206# XXXX rewrite as play_list(@) for recursion
207sub play_files {
208  no strict qw(subs);
209  version() unless $opts{quiet};
210  foreach my $source (@list) {
211    print "\nPlaying $source\n" if ($opts{verbose});
212    if ($source =~ /\.ogg$/) {
213      # Open the .ogg
214      unless (open(INPUT, "< $source")) {
215        warn "couldn't open $source\n";
216        next;
217      }
218      if ($ogg->open(INPUT) < 0) {
219        warn "$source does not appear to be an Ogg Vorbis bitstream\n";
220        next;
221      } else {
222      # Play the .ogg
223        unless ($opts{quiet}) {
224          write_info($ogg);
225          print "\n";
226        }
227        my $t_time = $ogg->time_total;
228        my $t_min = int($t_time / 60);
229        my $t_sec = $t_time % 60;
230        while (1) {
231          my $bytes = $ogg->read($buffer, $opts{buffer}, $big_endian_p,
232                                 2, 1, $bitstream);
233          if ($bytes == 0) {
234            # EOS
235            last;
236          } elsif ($bytes < 0) {
237            # Error
238            warn "error in stream\n" unless $opts{quiet};
239          } else {
240            # Successful read, play on each device
241            foreach my $ao (@devices) {
242              $ao->play($buffer, $bytes);
243            }
244            # Print info if verbose
245            if ($opts{verbose}) {
246              my $c_time = $ogg->time_tell;
247              my $c_min = int($c_time / 60);
248              my $c_sec = $c_time - 60 * $c_min;
249              my $r_min = int(($t_time - $c_time) / 60);
250              my $r_sec = ($t_time - $c_time) - 60 * $r_min;
251              printf STDERR "\rTime: %02li:%05.2f [%02li:%05.2f] %.1f kbit/s  \r",
252                     $c_min, $c_sec, $r_min, $r_sec, $ogg->bitrate_instant / 1000;
253            }
254          }
255        }
256        printf STDERR "\r%s\r\n", ' ' x 60;
257      }
258    } elsif ($source =~ /\.mp3$/) {
259      # Translate our options into the mp3 player's options
260      my $command_line = $mp3play;
261      while (my ($k, $v) = each(%opts)) {
262        if (exists $mp3play_opts{$k}) {
263          $command_line .= " \"$mp3play_opts{$k}\" \"$v\"";
264        }
265      }
266      # Call the external player, let it handle warnings/errors.
267      print "$command_line \"$source\"\n" unless $opts{quiet};
268      `$command_line $source`;
269    } else {
270      warn "unknown format: $source\n";
271    }
272  }
273}
274
275
276# Convert between ogg and mp3
277sub do_convert {
278  no strict qw(subs);
279  foreach my $source (@list) {
280  }
281}
282
283
284# Display comments (XXXX add modify when we can write)
285sub do_comments {
286  no strict qw(subs);
287  foreach my $source (@list) {
288    print "Commenting $source\n" if ($opts{verbose});
289    if ($source =~ /\.ogg$/) {
290      # Comment .ogg
291      unless (open(INPUT, "< $source")) {
292        warn "couldn't open $source\n";
293        next;
294      }
295      if ($ogg->open(INPUT) < 0) {
296        warn "$source does not appear to be an Ogg Vorbis bitstream\n";
297        next;
298      } else {
299        # XXXX when available, add code to modify info
300        write_info($ogg);
301        $ogg->clear;
302        close(INPUT);
303      }
304    } elsif ($source =~ /\.mp3/) {
305      # Comment .mp3
306      my $command_line = $mp3info;
307      # Check known comments
308      foreach my $k (@known_comments) {
309        if ((exists $mp3info_opts{$k}) and (exists $opts{$k})) {
310          $command_line .= " \"$mp3info_opts{$k}\" \"$opts{$k}\"";
311        }
312      }
313      # Check for anything mp3info knows about that we don't
314      while (my ($k, $v) = each(%{$opts{comment}})) {
315        if (exists $mp3info_opts{$k}) {
316          $command_line .= " \"$mp3info_opts{$k}\" \"$v\"";
317        }
318      }
319      # Now run the command (die on error... we don't want to muck up
320      # multiple files).
321      open(OUTPUT, "$command_line $source |") ||
322          die "couldn't run \`$command_line $source \`\n";
323      while (<OUTPUT>) { print }
324      close(OUTPUT);
325    } else {
326      warn "unkown format: $source\n";
327    }
328  }
329}
330
331
332# Open device and place in global @devices list
333sub open_devices {
334  foreach my $dev (@{$opts{device}}) {
335    my $opt_string = '';
336    my %dev_opts = ();
337    if ($dev =~ /([^:]*):(.*)/) {
338      $dev = $1;
339      $opt_string = $2;
340      %dev_opts = split(/[,=]/, $opt_string);
341    }
342    my $dev_id = Ao::get_driver_id($dev);
343    my $ao = Ao::open($dev_id, $opts{bits}, $opts{rate},
344                      $opts{channels}, \%dev_opts) ||
345       die "error: couldn't open device $dev\n";
346    unshift @devices, $ao;
347  }
348}
349
350
351# Close devices in @devices list
352sub close_devices {
353  foreach my $ao (@devices) {
354    $ao->close;
355  }
356}
357
358
359sub write_info {
360  # Short, one line comment format like mp3info, followed by
361  # longer comments.
362  my %comments = %{$ogg->comment};
363  my $info = $ogg->info;
364  my $time = $ogg->time_total;
365  my $min = int($time / 60);
366  my $sec = $time % 60;
367  my $rate = int($ogg->bitrate / 1024);
368  # Try to print like mp3info, add album because it's nice to know
369  print "$comments{TITLE}($comments{ARTIST}, $comments{ALBUM})",
370        ", $rate kbit/s ", ($info->channels == 2)?'stereo':'mono',
371        " (${min}m ${sec}s)\n";
372  # Now print other known comments.
373  while (my ($k, $v) = each(%comments)) {
374    # XXXX Format nicer
375    if (grep {/^\Q$k\E$/} @known_comments) {
376      print "$k: $v\n" unless grep {/^\Q$k\E$/} ('TITLE','ARTIST','ALBUM');
377    } else {
378      warn "unknown comment: $k\n";
379    }
380  }
381}
382
383
384# Print a usage summary
385sub usage {
386  print <<EOF;
387usage: $0 [options] [command] [file ...]
388
389  -h, --help                 display this message
390  -v, --version              print version info
391  -d, --device DEVICE        add a device to play to
392  -b, --bits BITS            set bits per sample (8 or 16)
393  -r, --rate RATE            set samples per second (default 44100)
394  -c, --channels CHANNELS    set channels (1 = mono, 2 = stereo)
395  -s, --buffer SIZE          set default buffer size
396  -S, --sort METHOD          specify a sorting method
397  -R, --random               shuffle files (alias for --sort random)
398
399where COMMAND is any of play, encode, decode, convert or comment, and
400DEVICE is any of oss, esd, alsa, irix, solaris or wav.  The device may
401be followed by an optional colon with a comma separated list of
402key=value parameters, such as --device wav:file=output.wav.
403EOF
404  exit 0;
405}
406
407# Print version info
408sub version {
409  print "pogg $VERSION ($MODIFIED)\n";
410}
411
412
413=head1 AUTHOR
414
415Alex Shinn, foof@synthcode.com
416
417=head1 SEE ALSO
418
419Ogg::Vorbis(3pm), Ao(3pm), ogg123(1), oggenc(1), perl(1).
420
421=cut
422