#! /usr/bin/perl -w
#
# dv2ogg.pl -- convert dv to ogg theora, optionally filtering audio tracks.
#
# (C) 2006,2007 jw@suse.de Novell Inc.
# Distribute under GPLv2
# 
# 2006-11-17, jw, v0.1 	initial draft.
# 2006-11-20, jw, v0.2	next_mult_8() added.
#                       The implicit date in the default metafile name is a problem, 
#			when encoder scripts are called late at night. Use fixed name.
#                       Add meta info also to nosound tracks.
# 2006-11-21, jw, v0.3  added Gamma, Brightness, contrast controls.
# 2006-11-22, jw, v0.4  option -k added. Was default before.
# 2006-11-23, jw, v0.5  -v 6 is good enough for everything, was -v 8 before.
#                       No visual difference, but only 60% the size.
# 2007-01-26, jw, v0.6  -c -o -n options added. 
#                       'concat' is sequential, 'merge' is parallel.
# 2007-01-29, jw, v0.7  -Z option added, defaults improved.
# 2007-02-16, jw, v0.8  -f option added, HDV support with auto aspect.
#                       HDV cannot yet merge wav.
# 
##########################################
# yast -i vorbis-tools 
# -> oggenc
#
# yast -i libogg libvorbis libtheora libogg-devel libvorbis-devel libtheora-devel
# rpm -Uhv /mounts/dist/9.3-i386/suse/i586/libtheora.rpm
# rpm -Uhv /mounts/dist/9.3-i386/suse/i586/libtheora-devel.rpm
# rpm -Uhv /mounts/dist/9.3-i386/suse/i586/libogg.rpm
# rpm -Uhv /mounts/dist/9.3-i386/suse/i586/libogg-devel.rpm
#
# $ cd ~/src/video
# tar jxvf ffmpeg2theora-0.16.tar.bz2
# cd ffmpeg2theora-0.16
# patch < /suse/jw/patches/ffmpeg2theora-0.16-gamma.diff
# ./configure
# make
# -> ffmpeg2theora
#
# $ cd ~/src/video
# tar zxvf liboggz-0.9.4.tar.gz
# cd liboggz-0.9.4
# ./configure
# make
# -> ./src/tools/oggzmerge



use strict;
use POSIX;
use Data::Dumper;
$Data::Dumper::Terse = 1;
$Data::Dumper::Indent = 1;
$Data::Dumper::Sortkeys = 1;

my $version		= '0.8';
my $destdir		= 'ogg';
my $wavdir 		= 'wav';
my $editor 		= $ENV{EDITOR} || 'vi';
my $oggenc 		= 'oggenc --downmix';		# package vorbis-tools
my $ffmpeg2theora 	= 'ffmpeg2theora';
my $oggzmerge 		= 'oggzmerge';
my $verbose		= 1;
my $albumnamedef	= POSIX::strftime "%Y-%m-%d", localtime;
my $authornamedef	= $ENV{LOGNAME} || '';
my $titlefmtdef		= '%s';
my $metafile 		= "dv2ogg.data";
my $m_seen		= 0;
my $aspect_def		= '4:3';
my $aspect;
my $width 		= 384;
my $height 		= 0;
my $merge_wav		= 1;
my $noop		= 0;
my $gamma		= '1.2';
my $contrast		= '1.4';
my $brightness		= '0.0';
my $saturation		= '1.0';
my $sharpness		= 0;
my $keep_temp		= 0;
my $vid_quality		= 6;
my $au_quality		= 5;
my $au_samplerate	= 32000;
my $concat      	= 0;
my $concat_glue 	= ',';
my $editmeta		= 0;
my $dv_format;
my $outfilename;

push @ARGV, '-h' unless scalar @ARGV;

while (defined(my $arg = shift))
  {
    $verbose++				if $arg eq '-v';	
    $verbose = 0			if $arg eq '-q';	
    $destdir = shift			if $arg eq '-d';
    $wavdir = shift			if $arg eq '-w';
    dv_format(shift)			if $arg eq '-f';
    $m_seen = $albumnamedef = shift	if $arg eq '-l';
    $m_seen = $authornamedef = shift	if $arg eq '-a';
    $m_seen = $titlefmtdef = shift	if $arg eq '-t';
    $editmeta++				if $arg eq '-e';
    $metafile = shift			if $arg eq '-m';
    $aspect = '16:9',$height=0		if $arg =~ m{^-16};
    $width = shift			if $arg eq '-x';
    $height = shift			if $arg eq '-y';
    $gamma = shift			if $arg eq '-G';
    $contrast = shift			if $arg eq '-C';
    $sharpness = shift			if $arg eq '-S';
    $saturation = shift			if $arg eq '-Z';
    $vid_quality = shift		if $arg eq '-V';
    $au_quality = shift			if $arg eq '-A';
    $brightness = shift			if $arg eq '-B';
    $au_samplerate = shift		if $arg eq '-H';
    $keep_temp++ 			if $arg eq '-k';
    $noop++				if $arg eq '-n';
    $concat = 1         		if $arg eq '-c';
    $outfilename = shift 		if $arg eq '-o';

    last if $arg eq '--';

    $width =  next_mult_8($width, "width");

    unless ($arg =~ m{^-[frdqvwemtla1xynkcoGCBVASZ]})
      {
        unless ($arg =~ m{^-})
	  {
	    unshift @ARGV, $arg;
	    last;
	  }
        print STDERR qq{
dv2ogg $version
A converter for dv files into ogg theora.

$0 [options] tape.dv ...

Valid options are:

  -d destdir	Write .ogg files into destdir. Default: '$destdir'
  -w wavdir	Merge .wav files or -vorbis.ogg files from wavdir. Default: '$wavdir'
  -k            Keep temporary -nosound.ogg files. Default: remove them.

  -l albumn     Specify name of the album or event name. Default: '$albumnamedef'
  -a author	Specify name of author. Default: '$authornamedef'
  -t title	Specify title of this recording. Default: '$titlefmtdef'

  -e            Edit metadata file before converting.
  -m metafile   Use the named metadata file. Default '$metafile'
  -r            Raw mode. Do not merge anything.
  -16:9		Process 16:9 material. Default: $aspect_def
  -f hdv	Switch to HDV processing, implies 16:9.
  		Default: 'DV' unless input file suffix is .hdv or .hd

  -x width      Scale to given width (in pixels). Default: $width
  -y height     Scale to given width (in pixels). Default: $height
  -G gamma      Apply gamma correction. Default: $gamma
  -C contrast   Apply contrast correction. Default: $contrast
  -B brightness Apply brightness correction. Default: $brightness
  -S sharpness  Apply smoothing (0,1,2). Default: $sharpness
  -Z saturation Apply color saturation correction. Default: $saturation

  -V quality    ffmpeg2theora video quality. Default: $vid_quality
  -A quality    oggenc / ffmpeg2theora audio quality. Default: $au_quality
  -H samplerate oggenc / ffmpeg2theora audio sample rate. Default: $au_samplerate

  -c            Write one concatenated wav file from all listed dv files.
                The filename is a '$concat_glue'-separated list of the input file names
		unless -o is used to specify the name.
                Default: convert file by file.
  -o outfile    Specify the output filename. Implies -c. 
                destdir applies unless outfile starts with a '/'.

  -n            Do nothing, just print out commands.
  -v            Be more verbose. 
  -q            Be quiet.

Use 'dv2wav -d wav *.dv' to populate a wavdir with normalized audio tracks.
Optionally use postprocessing tools on these files, before running dv2ogg.
Files ending in -vorbis.ogg in wavdir are created if missing and used in subsequent runs.
Files ending in -nosound.ogg in destdir are temporary video files used for merging.
Use -w - to disable audio merging (takes the audio track directly from dv). 

};
        exit 0;
      }
  }

$merge_wav = 0 if ! $wavdir or $wavdir eq '-';

die "destdir $destdir does not exist.\n" unless -d $destdir; 
die "wavdir $wavdir does not exist.\n" if $merge_wav and ! -d $wavdir;

my $meta;
for my $dv (@ARGV)
  {
    my $name = $dv;
    $name =~ s{\.[^\./]*$}{};
    $name =~ s{^.*/}{};
    my $titledef = sprintf $titlefmtdef, $name;
    $meta->{$name} = { album => $albumnamedef, author => $authornamedef, title => $titledef, 
    	gamma => $gamma, contrast => $contrast, brightness => $brightness, sharpness => $sharpness, saturation => $saturation };
  }

if (!-f $metafile or (-s $metafile < 10))
  {
    open M, ">$metafile" or die "cannot write $metafile: $!";
    print M Dumper $meta;
    close M or die "could not write $metafile: $!";
  }
else
  {
    die "Metadata conflict:\ndelete $metafile OR use -e instead of -l, -a, -t options.\n" if $m_seen;
  }

runcmd("$editor '$metafile'") if $editmeta;

open IN, "<$metafile" or die "cannot read back $metafile: $!";
$meta = join '', <IN>;
close IN;
$meta = eval $meta;

$oggenc .= ' -Q' unless $verbose;

my $count = scalar @ARGV;
my @basenames = map { my $a = $_; $a =~ s{\.[^\./]*$}{}; $a =~ s{^.*/}{}; $a } @ARGV;
$concat = 1 if defined $outfilename;

for my $dv (@ARGV)
  {
    my $dvf = $dv_format;
    unless (defined $dvf)
      {
        $dvf = ($dv =~ m{\.hdv?}i) ? 'hdv' : 'dv';
      }

    my $asp = $aspect;
    unless (defined $asp)
      {
        $asp = ($dvf eq 'hdv') ? '16:9' : $aspect_def;
      }

    my $hgt = $height;
    unless ($hgt)
      {
	## derive $hgt from $width using $asp.
	$hgt = next_mult_8(($asp eq '16:9') ? int($width*9/16) : int($width*3/4));
      }
    $hgt =  next_mult_8($hgt, "height");

    $count--;
    my $name = $basenames[0];

    # take the meta info from the first file, even if we concat.
    my $m = $meta->{$name};
    die "oops, no metainformation in $metafile for $name\n" unless $m;
    die "oops, no album in $metafile for $name\n" unless $m->{album};
    die "oops, no author in $metafile for $name\n" unless $m->{author};
    die "oops, no title in $metafile for $name\n" unless $m->{title};

    my $G = $m->{gamma} || $gamma;
    my $C = $m->{contrast} || $contrast;
    my $B = $m->{brightness} || $brightness;
    my $S = $m->{sharpness} || $sharpness;
    my $Z = $m->{saturation} || $saturation;

    my $outname = "$destdir/$name-$width.ogg";
    if ($concat)
      {
        $name = join ',', @basenames;
	if (defined $outfilename)
	  {
	    $outname = $outfilename;
	    $outname .= ".ogg" unless $outname =~ m{\.\w+$};
	    $outname = "$destdir/" . $outname unless $outname =~ m{^/};

	    $name = $outfilename;
	    $name =~ s{\.\w+$}{};
	    $name =~ s{.*/}{};
	  }
	$dv = join "' '", @ARGV;

	# ffmpeg2theora does not take multiple input files, but dv-files are cat-able.
	$ffmpeg2theora = "cat '$dv' | $ffmpeg2theora";
	$dv = '-';
      }

    my $f2t_opt = "-v $vid_quality -x $width -y $hgt --aspect $asp -G $G -C $C -B $B -S $S";
    $f2t_opt .= " -Z $Z";	# ffmpeg2theora-0.17 with satur patch.
    $f2t_opt .= " --artist '$m->{author}' --title '$m->{title}' --location '$m->{album}'";
    if ($merge_wav)
      {
        my $aout = "$wavdir/$name-vorbis.ogg";
	my $ain = "$wavdir/$name.wav";

        runcmd("$oggenc -q $au_quality --resample $au_samplerate -t '$m->{title}' -a '$m->{author}' -l '$m->{album}' -o '$aout' '$ain'")
	  if ! -s $aout or -M $aout > -M $ain;

        my $vout = "$destdir/$name-$width-nosound.ogg";
        runcmd("$ffmpeg2theora -f $dvf $f2t_opt --nosound -o '$vout' '$dv'");
        runcmd("$oggzmerge -o '$outname' '$vout' '$aout'");
        unlink $vout unless $keep_temp;
      }
    else
      {
        runcmd("$ffmpeg2theora -f $dvf $f2t_opt -a $au_quality -H $au_samplerate -o '$outname' '$dv'");
      }
    last if $concat;

    print "$count files to go...\n" if $count and $verbose;
  }
exit 0;

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

sub runcmd
{
  my ($cmd) = @_;
  print "$cmd\n" if $verbose;
  return if $noop;
  system $cmd and die "$cmd: failed: $@ $!";
}

sub next_mult_8
{
  my ($val, $name) = @_;
  my $r = 8 * int(($val+4)/8);
  return $r unless $name;

  my $d = $r - $val;
  print "Note: $name adjusted by $d\n" if $d && $verbose;
  return $r;
}

sub dv_format
{
  my ($fmt) = @_;
  if ($fmt =~ m{^dv$}i)
    {
      $dv_format = 'dv';
    }
  elsif ($fmt =~ m{^hdv?$}i)
    {
      $dv_format = 'hdv';
      $aspect = '16:9';
      $height = 0;
    }
  else
    {
      die "unknown dv_format '$fmt'. Try 'dv' or 'hdv'\n";
    }
}
