#!/usr/bin/perl -w
# ripmake
#
# automatically configure transcode for various conversion tasks
#
# Written by Christian Vogelgsang <chris@lallafa.de>
# under the GNU Public License V2
#

# --- setup presets -----------------------------------------------------------
# output flavors
@flavors = ('avi','ogm','mkv','svcd','vcd','dvd');

# default codec for output flavor
%def_vcodec = ( 'avi' => 'xvid', 'ogm' => 'xvid', 'mkv' => 'xvid',
                'svcd' => 'mpeg2enc', 'vcd' => 'mpeg2enc',
                'dvd' => 'mpeg2enc');
%def_acodec = ( 'avi' => 'raw',  'ogm' => 'ogg', 'mkv' => 'ogg',
                'svcd' => 'toolame',  'vcd' => 'toolame',
                'dvd' => 'toolame');

# map flavor to default audio rate (kbps)
%def_aud_rate = ( 'avi' => 128, 'ogm' => 128, 'mkv' => 128,
                  'svcd' => 224, 'vcd' => 224,
                  'dvd' => 224 );

# map flavor to default audio sampling rate (Hz)
%def_aud_samp_rate = ( 'avi'  => 44100, 'ogm' => 44100, 'mkv' => 44100,
                       'svcd' => 44100, 'vcd' => 44100,
                       'dvd'  => 48000 );

# map flavor to mux overhead bitrate (kbps)
%mux_add_rate = ( 'avi' => 60, 'ogm' => 10, 'mkv' => 10,
                  'svcd' => 2, 'vcd' => 2,
                  'dvd' => 2);

# how many MB reserve for target format data
%cd_reserve = ( 'avi' => 0, 'ogm' => 0, 'mkv' => 0,
                'svcd' => 1, 'vcd' => 1, 'dvd' => 0 );

# how to scale user specified cd size to get raw bits
%cd_raw_scale = ( 'avi' => 1, 'ogm' => 1, 'mkv' => 1,
                  'svcd' => 2324/2048, 'vcd' => 2324/2048,
                  'dvd' => 1);

# default cd size in MB
%def_cd_size =  ( 'avi' => 700, 'ogm' => 700, 'mkv' => 700,
                  'svcd' => 700, 'vcd' => 700, 'dvd' => 4450 );
 
# frc (frame rate codes) (cloned from transcode)
@frame_rate_codes = ('illegal','23.976 NTSC','24.0', # 0 1 2
                     '25.0 PAL','29.97 NTSC','30.0',        # 3 4 5
                     '50.0','59.94','60.0',                 # 6 7 8
                     '1?','5?','10?',                       # 9 10 11
                     '12?','15?');

# --- define default options --------------------------------------------------
%options = (
  # --- input/output param ---
  'flavor' => 'avi',         # flavor for output
  'ovcodec' => 'auto',       # output tc video codec
  'oacodec' => 'auto',       # output tc audio codec
  'ivcodec' => 'auto',       # input tc video codec
  'iacodec' => 'auto',       # input tc audio codec
  'tc_vopt' => '',           # add transcode options to video processing
  'tc_aopt' => '',           # add transcode options to audio processing
  'ovc_opt' => '',           # options for the video output codec (passed via -F)
  # --- output cd setup ---
  'cd_size' => 0,            # size of CD/DVD in MB
  'cd_max' => 4,             # check this number of CDs
  'cd_force' => 0,           # force this number of CDs
  'cd_frac' => 1.0,          # fraction of cd_size used for rip
  # --- output audio setup ---
  'aud_track' => [],         # list of input source tracks to rip
  'aud_rate' => [],          # list of kbps rates for each audio track
  'aud_samp_rate' => [],     # list of sample rate for each audio track
  'aud_chan' => [],          # list of channels for each audio track
  'aud_vbr_qual' => [],      # list of vbr quality (0=no vbr, 1..9)
  # --- output subtitle setup ---
  'sub_render' => 1,         # render subtitles directly into the video stream
  'sub_track' => [],         # list of input subtitle tracks to rip
  # --- misc input flags ---
  'dvd_title' => '1',        # select a title in DVD input
  'tv_norm' => 'auto',       # pick an output tv norm (otherwise autodetect)
  'interlaced' => 0,         # input is interlaced
  'modavi_divx' => 'ffmpeg', # use this module for AVI/divx import
  'modavi_other'=> 'ffmpeg', # use this module for AVI/non-divx import
  'split_avi' => 0,          # split input avi for multi cd rips
  'in_frc' => -1,            # set input frameratecode (-1=autodetect)
  'in_asr' => [0,0],         # force input aspect ratio
  # --- misc output flags ---
  'anamorph' => 0,          # generate anamorph output (-1=autodetect)
  'high_quality' => 0,             # highest quality encoder settings
  'scl_tol' => 7,            # pixel tolerance when scaling
  'max_vrate' => 6000,       # maximum supported video rate
  'low_space' => 0,          # conserve space while ripping (deletes intermed.)
  # --- frame size determination ---
  'asrerr_max' => 2.5,       # maximum accepted aspect error (in percent)
  'size_force' => [0,0],     # force a specific output size
  'frame_mod' => 16,         # output frame size modulo
  'width_range' => [512,640],# output frame width search range
  'bpp_range' => [0.30,0.65],# minimum bits per pixel
  # --- sample image generation ---
  'sample_max' => 5,         # max number of sample images
  'sample_chapframe' => 16,  # use this frame as the sample image of a chapter
  'sample_begin' => 100,     # start frame for sample image extraction
  'sample_step' => 100,      # increment for next frame in sample image extr.
  'sample_recycle' => 1,     # do not regenerate or delete sample images
  # --- input clipping ---
  'clip_force' => '',        # force clip parameters
  'clip_user' => 0,          # user interactive clipping
  'clip_thres' => [80,100],  # clipping threshold for pgmfindclip
  'clip_tstep' => [10,10],   # stepping for threshold change
  'clip_bmodulo' => [1,1],   # pixel modulo of clipped border
  'clip_fmodulo' => [16,16], # pixel modulo of clipped target frame
  'clip_offset' => [0,0],    # pixel offset for search
  'clip_extra' => '',        # extra options for pgmfindclip
  # --- MPEG ---
  'mpeg_mplex' => 'mplex',   # multiplexer for MPEG (mplex or tcmplex)
  'chap_menu' => 0,          # create chapter menus for VCD/SVCD (>0 = entries)
  # --- VCD params (MPEG 1, 4:3) ---
  'vcd_conform' => 1,         # follow the VCD standard. otherwise use options below:
  'vcd_res' => [352,240,288], # screen resolution [w,h_ntsc,h_pal]
  'vcd_max_kbps' => 1374,     # maximum supported bitrate for whole stream
  'vcd_min_vbr_quant' => 0,   # minimum vbr quantizer (or 0=cbr, 2..31)
  'vcd_can_anamorph' => 0,    # supports anamoprh 16:9 encoding
  'vcd_aud_max_tracks' => 1,  # maximum supported audio channels
  'vcd_aud_rates' => [224],   # list of allowed audio kbps (first: default)
  'vcd_video_buf' => 0,       # video buffer for multiplexing (0=default)
  # --- SVCD params (MPEG 2, 4:3 or 16:9) ---
  'svcd_conform' => 1,        # follow the SVCD "standard". otherwise use options below:
  'svcd_res' => [480,480,576],# screen resolution [w,h_ntsc,h_pal]
  'svcd_max_kbps' => 2748,    # maximum supported bitrate for whole stream
  'svcd_min_vbr_quant' => 4,  # minimum vbr quantizer (or 0=cbr, 2..31)
  'svcd_can_anamorph' => 1,   # supports anamoprh 16:9 encoding
  'svcd_aud_max_tracks' => 2, # maximum supported audio channels
  'svcd_aud_rates' => [192,224], # list of allowed audio kbps (first: default)
  'svcd_video_buf' => 0,      # video buffer for multiplexing (0=default)
  # --- DVD params (MPEG 1/2, 4:3 or 16:9) ---
  'dvd_res' => [720,480,576], # screen resolution
  'dvd_max_kbps' => 9800,     # max full stream rate
  'dvd_min_vbr_quant' => 4,
  'dvd_can_anamorph' => 1,
  'dvd_aud_max_tracks' => 8,
  'dvd_aud_rates' => [192, 112, 128, 160, 224, 256, 320],
  'dvd_video_buf' => 0,
  # --- debug/control ---
  'debug_level' => 0,        # set debug level
  'probe_only' => 0          # probe only and then exit
);

# ========== MAIN CODE ========================================================
$ripmake ="ripmake: ".'$Revision: 1.39 $ $Date: 2004/09/12 18:13:53 $ '."\n";

# ----- adjust options -----
# try to read ~/.ripmakerc
my($rc_file) = "$ENV{'HOME'}/.ripmakerc";
if(-e $rc_file) {
  exit(1) if(!read_option_file(\%options,$rc_file));
}
# parse command line options
exit(1) if(!parse_options(\%options,\@ARGV));
# set project name
set_project(\%options);

# ----- open log file -----
$debug_level = $options{'debug_level'};
my($log_file) = "$options{'project'}.log";
open(my $log_handle,">$log_file") || die "Can't open log file: $log_file";
print_log($ripmake);

# ----- check options -----
exit(1) if(!check_options(\%options));
print_log("  debug mode: level $debug_level\n") if($debug_level>0);
dump_info("[5]--> Options",\%options) if($debug_level>=5);

# ----- check tool versions -----
exit(1) if(!check_tools(\%options));
print_log("\n");

# ----- set clean LANG, otherwise input probing will fail (e.g. on RH) -----
$LANG = $ENV{'LANG'} || "";
if ($LANG ne "C") {
  $ENV{'LANG'} = "C";
}

# ----- analyse input -----
%input = ();
$ok = probe_input(\%input,\%options);
dump_info("[5]--> Input info",\%input) if($debug_level>=5);
exit(1) if(!$ok);
print_log("\n");
exit 0 if($options{'probe_only'});

# ----- prepare output -----
%output = ();
$ok = prepare_output(\%input,\%output,\%options);
dump_info("[5]--> Output info",\%output) if($debug_level>=5);
exit(1) if(!$ok);
print_log("\n");

# ----- generate ripping makefile -----
exit(1) if(!generate_makefile(\%input,\%output,\%options));

# ----- close and rename log to project name -----
print_log("ready.\n");
close $log_handle;
exit(0);

########## SUBROUTINES ########################################################

# --- print_log(@strings) ---
# replacement for print command, outputs to log file, too
# returns: -
sub print_log {
  print @_;
  print $log_handle @_ if(defined $log_handle);
}

# --- run_cmd($cmd,$merge_stderr) ---
# run external command and receive output
# returns: stdout of command (if "merge_stderr" is set then stderr is added)
#          or "undef" if command failed
sub run_cmd {
  my($cmd) = shift;
  my($merge_stderr) = shift;

  # use a temporary file for stderr logging
  my($stderr_log);
  $stderr_log = "/tmp/log.$$" if(!$merge_stderr);

  # open pipe to command and read output
  my($all) = "$cmd 2>&1 |";
  $all = "$cmd 2>$stderr_log |" if(defined($stderr_log));
  my($ok) = open(CFH,$all);
  my($result);
  if($ok) {
    $result = join('',<CFH>);
    close(CFH);
  }

  # debug message [1]
  print_log("\n[1]--> external command: '$all' ($ok)\n") if($debug_level>=1);

  # log all output [2]
  if(!$ok || ($debug_level>=2)) {
    # print stdout
    if(defined($result)) {
      print_log("[2]--> output stdout begin:\n");
      print_log($result);
      print_log("[2]--> output stdout end.\n");
    }
    # print stderr
    if(defined($stderr_log) && -s $stderr_log) {
      print_log("[2]--> output stderr begin:\n");
      if(open(LOG2,$stderr_log)) {
        print_log(join('',<LOG2>));
        close LOG2;
      }
      print_log("[2]--> output stderr end.\n");
    }
  }

  # remove stderr log
  unlink($stderr_log) if(defined($stderr_log) && -e $stderr_log);

  # return undef or output
  if(!$ok) {
    return undef;
  } else {
    return $result;
  }
}

# --- dump_info($title,$hash_ref) ---
# verbose an information hash
# returns: -
sub dump_info {
  my($title) = shift;
  my($in)    = shift;

  print_log("$title begin:\n");
  foreach(sort(keys(%$in))) {
    my($value) = $$in{$_};
    print_log("  $_ = ");
    if( ref($value) eq 'ARRAY' ) {
      print_log("[".join(',',@$value)."]");
    } else {
      print_log($value);
    }
    print_log("\n");
  }
  print_log("$title end.\n");
}

# --- check_tools($opt) ---
# check if all external programs are available in the correct version
# returns: 0=error, 1=ok
sub check_tools {
  my($opt) = @_;

  # fetch transcode version
  my($tcver) = run_cmd("transcode 2>&1 | head -n 1",0);
  $tcver =~ m/v(\d+)\.(\d+)\.(\d+)/;
  if(!defined $1) {
    print_log("FATAL: can't fetch transcode version!\n");
    return 0;
  }
  my($tcvmaj) = $1;
  my($tcvmin) = $2;
  my($tcvmic) = $3;
  print_log("  transcode($tcvmaj.$tcvmin.$tcvmic),");
  # check version requirements: at least 0.6.x
  if(($tcvmaj==0)&&($tcvmin<6)&&($tcvmic<2)) {
    print_log("\nFATAL: please use at least transcode version 0.6!\n");
    return 0;
  }
  # check availability of pgmfindclip and chaplin
  my($has_pgmfindclip) = defined(run_cmd("pgmfindclip -h",1))?1:0;
  my($has_chaplin)     = defined(run_cmd("chaplin -h",1))?1:0;
  my($has_mpglen)      = defined(run_cmd("mpglen -h",1))?1:0;
  print_log( " pgmfindclip(".($has_pgmfindclip?'found':'MISSING')
            ."), chaplin(".($has_chaplin?'found':'MISSING')
            ."), mpglen(".($has_mpglen?'found':'MISSING')
            .")\n");
  if(!($has_pgmfindclip && $has_chaplin && $has_mpglen)) {
    print_log("FATAL: external tool missing!\n");
    return 0;
  }

  # check for display if interactive clipping is on
  if($$opt{'clip_user'}==1) {
    my($has_display) = defined(run_cmd("display -h",1))?1:0;
    print_log("  display:     ".($has_display?'found':'MISSING')."\n");
    if(!$has_display) {
      print_log("FATAL: external tool missing!\n");
      return 0;
    }
  }
  return 1;
}

# =============================================================================
# ========== options ==========================================================
# =============================================================================

# --- print_usage() -----------------------------------------------------------------
# print command help
# returns: -
sub print_usage {
  my($reason) = shift;
  print <<EOF;
$ripmake
  written by Christian Vogelgsang <chris\@lallafa.de>
  under the GNU Public License V2

print_usage:

  $0 [Options] <input-file/dir> <flavor>

Common Options:

  -t <num>     Select title in DVD mode             (default: 1,-1,1)
  -a <tn/tl>[,<br>][,<sr>][,<c>]
               Pick audio track num or language,    (e.g. -a en
               bitrate, sampling rate and channels.       -a 0,224
               Repeat for more audio tracks!              -a 0,128v
               Add 'v[mode]' to <br> for vbr encoding.    -a 1,192v4)
  -s <tn>      Add subtitle track                   (e.g. -s de
                                                          -s 2)
  -p <on/off>  Probe input only                     (default: 0=off)

Advanced Options:

  -n <tv_norm> Set output tv standard (fps)
               (none,pal,ntsc,ntscfilm)             (default: autodetect)
  -i <on/off>  Input signal is interlaced           (default: 0=off)

  -g <w>[,<h>] Force a target frame size            (default: autodetect)
  -c <num>     Force number of CDs                  (default: autodetect)
  -r <num>     Force clip range
  -u <on/off>  Do interactive clipping              (default: 0=off)
  -C <num>     Set size of a CD/DVD in MB           (default: 700=cd/4450=dvd)
  -S <frac>    Use only a fraction of CD size       (default: 1.0)
  -o <vcodec>[/<vc_opt>][,<acodec>]
               Choose codec for flavor              (default: derived)
  -f <vcodec>[,<acodec>]
               Force input codec                    (default: auto)

Expert Options:

  -x var=value Set an option variable               (help: "-x help")
  -X <file>    Read config options from file
  -d <level>   Enable debug output

EOF
  print "supported flavors: " . join(',',@flavors) . "\n";
  if(defined($reason)) {
    print "---> ERROR: $reason\n";
  }
}

# --- parse_options($opt_ref,$argv_ref) ---------------------------------------
# fill in and overwrite the %options hash with command line options
# returns: 0=error, 1=ok
sub parse_options {
  my($opt)  = shift;
  my($argv) = shift;

  while($_=shift @$argv) {
    # --- option args begin with '-' ---
    if( m/^-(.)(.*)$/ ) {

      # fetch optional argument
      my($arg) = $2;
      $arg = shift @$argv if($2 eq '');

      # --- main options ---
      # t = dvd_title
      if($1 eq 't') {
        $$opt{'dvd_title'} = $arg;
      }
      # a = audio setup
      elsif($1 eq 'a') {
        my($trk,$rate,$samp_rate,$chan) = split(/,/,$arg);
        if(!defined($trk)) {
          print_usage("no track in -a given!");
          return 0;
        }
        # source track
        my($ref) = $$opt{'aud_track'};
        push @$ref,$trk;
        # rate
        # set rate 0 if none given - will be corrected in check_options
        my($vbr);
        if(!defined($rate)) {
          $rate = 0;
          $vbr = 0;
        } else {
          if($rate =~ m/^(\d+)v(\d+)?$/) {
            $rate = $1;
            $vbr = defined($2)?$2:5;
          } else {
            $vbr = 0;
          }
        }
        $ref = $$opt{'aud_rate'};
        push @$ref,$rate;
        $ref = $$opt{'aud_vbr_qual'};
        push @$ref,$vbr;
        # samp_rate - will be corrected in check_options
        $samp_rate = 0 if(!defined($samp_rate));
        $ref = $$opt{'aud_samp_rate'};
        push @$ref,$samp_rate;
        # channels - will be corrected in check_options
        $chan = 0 if(!defined($chan));
        $ref = $$opt{'aud_chan'};
        push @$ref,$chan;
      }
      # s = subtitle
      elsif($1 eq 's') {
        my($trk) = $arg;
        my($ref) = $$opt{'sub_track'};
        push @$ref,$trk;
      }
      # --- advanced options ---
      # n = tv_norm
      elsif($1 eq 'n') {
        $$opt{'tv_norm'} = $arg;
        if(($arg ne 'none')&&($arg ne 'pal')&&($arg ne 'ntsc')&&($arg ne 'ntscfilm')) {
          print_usage("unknown tv norm: $arg");
          return 0;
        }
      }
      # i = interlaced
      elsif($1 eq 'i') {
        $$opt{'interlaced'} = $arg;
        if(($arg != 0)&&($arg != 1)) {
          print_usage("wrong interlaced flag: $arg");
          return 0;
        }
      }
      # g = force geometry
      elsif($1 eq 'g') {
        my($w,$h) = split(/,/,$arg);
        if(!defined($w)) {
          print_usage("no width found in -g!");
          return 0;
        }
        if(!defined($h)) {
          $$opt{'size_force'} = [int($w),0];
        } else {
          $$opt{'size_force'} = [int($w),int($h)];
        }
      }
      # c = force num cd
      elsif($1 eq 'c') {
        $$opt{'cd_force'} = $arg;
      }
      # o = out_codec
      elsif($1 eq 'o') {
        my($ovcodec,$oacodec) = split(/,/,$arg);
        if(defined($ovcodec)) {
          my($ovc,$ovc_opt) = split(/\//,$ovcodec);
          $$opt{'ovcodec'} = $ovc if(defined($ovc));
          $$opt{'ovc_opt'} = $ovc_opt if(defined($ovc_opt));
        }
        $$opt{'oacodec'} = $oacodec if(defined $oacodec);
      }
      # C = cd size in MB
      elsif($1 eq 'C') {
        $$opt{'cd_size'} = $arg;
      }
      # S = fraction of cd_size
      elsif($1 eq 'S') {
        $$opt{'cd_frac'} = $arg;
      }
      # r = clip range
      elsif($1 eq 'r') {
        $$opt{'clip_force'} = $arg;
      }
      # u = interactive clipping
      elsif($1 eq 'u') {
        $$opt{'clip_user'} = $arg;
        &print_usage_exit("wrong user clipping flag: $arg")
          if(($arg != 0)&&($arg != 1));
      }
      # f = force input
      elsif($1 eq 'f') {
        my($ivcodec,$iacodec) = split(/,/,$arg);
        $$opt{'ivcodec'} = $ivcodec if(defined $ivcodec);
        $$opt{'iacodec'} = $iacodec if(defined $iacodec);
      }
      # --- profi options ---
      # x = extra var
      elsif($1 eq 'x') {
        # dump help
        if($arg eq 'help') {
          dump_info("Options",\%options);
          return 0;
        }
        # parse option
        if(!parse_option_line($opt,$arg,"command line")) {
          return 0;
        }
      }
      # X = read config file
      elsif($1 eq 'X') {
        if(!read_option_file($opt,$arg)) {
          print_log("ERROR: reading config from '$arg'\n");
           return 0;
        }
      }
      # d = debug
      elsif($1 eq 'd') {
        $$opt{'debug_level'} = $arg;
      }
      # p = probe only
      elsif($1 eq 'p') {
        $$opt{'probe_only'} = 1;
      }
      # unknown!
      else {
        print_usage("Unknown switch: $_");
        return 0;
      }
    } else {
      last;
    }
  }

  # --- fixed args: fetch input and flavor ---
  # input
  my($file) = $_;
  if(!defined($file)) {
    print_usage("give <input>!");
    return 0;
  }
  $file =~ s,/$,,;
  $$opt{'input'} = $file;

  if(!$$opt{'probe_only'}) {
    my($num_arg) = scalar @$argv;
    if($num_arg!=1) {
      print_usage("give <flavor>!");
      return 0;
    }
    # flavor
    $$opt{'flavor'} = $$argv[0];
  }
  return 1; # all went well
}

# --- read_option_file($$opt,$file) -------------------------------------------
# parse option values from a file
# returns: 1=ok, 0=failed
sub read_option_file {
  my($opt,$file) = @_;

  open my($fh),$file or return 0;
  my($line) = 0;
  while(<$fh>) {
    $line++;
    # normalize input line
    s/^\s+//;
    s/\s*=\s*/=/;
    # skip comment or empty lines
    next if(m/^#/ || m/^$/);
    chop;
    # really parse option
    return 0 if(!parse_option_line($opt,$_,"$file:$line"));
  }
  close $fh;
  return 1;
}

# --- parse_option_line($$opt,$line,$from) ------------------------------------
# parse an option value
# returns: 1=ok, 0=failed
sub parse_option_line {
  my($opt,$line,$from) = @_;
  my($var,$val) = split(/=/,$line);
  if(!defined($var) || !defined($val)) {
    print_log "$from: Syntax error in '$line'\n";
    return 0;
  }
  if(!defined($$opt{$var})) {
    print_log "$from: Unknown option: '$var' (see -x help)\n";
    return 0;
  }
  my($res);
  eval "\$res = $val";
  if(ref($res) ne ref($$opt{$var})) {
    print_log "$from: Wrong type of value for '$var': $val\n";
    return 0;
  }
  # asign value
  $$opt{$var} = $res;
  return 1;
}

# --- check_param($desc,$par,$list) -------------------------------------------
# check passed param if it is found in a list of parameters
# returns: -
sub check_param {
  my($desc) = shift;
  my($par)  = shift;
  my($list) = shift;

  my($found)=0;
  foreach(@$list) {
    return 1 if($_ eq $par);
  }
  print "Unsupported $desc parameter '$par' (in " . join(',',@$list) .")!\n";
  return 0;
}

# --- set_project($opt) -------------------------------------------------------
# set project name in options
# returns: -
sub set_project {
  my($opt) = shift;

  # --- set project ---
  # fetch input and normalize
  my($name) = $$opt{'input'};
  $name =~ s,.*/([^/]+)$,$1,;
  $name =~ s,\.[^.]+$,,;
  $name =~ s,\s+,_,g;
  $name = "output" if($name eq "");
  $$opt{'project'} = $name . '-' . $$opt{'flavor'};
}

# --- check_options($opt_ref) -------------------------------------------------
# perform some checks on the current state of the option hash
# returns: 0=error, 1=ok
sub check_options {
  my($opt) = shift;

  # --- check input ---
  my($input) = $$opt{'input'};
  # if its a dir then find real path
  if ( -d $input ) {
    $input = run_cmd("cd \"$input\" && pwd",0);
    chop $input;
    $$opt{'input'} = $input;
  }

  # --- check flavor ---
  my($flavor) = $$opt{'flavor'};
  if(!check_param('output flavor',$flavor,\@flavors)) {
    return 0;
  }

  # --- check cd_size ---
  if($$opt{'cd_size'}==0) {
    $$opt{'cd_size'} = $def_cd_size{$flavor};
  }
  
  # --- audio setup ---
  # add track 0 as default if none is given
  my($ref) = $$opt{'aud_track'};
  if(scalar @$ref == 0) {
    # default: add track 0 with default audio rate
    push @$ref,0;
    $ref = $$opt{'aud_rate'};
    push @$ref,$def_aud_rate{$flavor};
    $ref = $$opt{'aud_vbr_qual'};
    push @$ref,0;
    $ref = $$opt{'aud_samp_rate'};
    push @$ref,$def_aud_samp_rate{$flavor};
    $ref = $$opt{'aud_chan'};
    push @$ref,2;
  } else {
    # replace rate 0 values with default audio rate
    $ref = $$opt{'aud_rate'};
    foreach(@$ref) {
      $_ = $def_aud_rate{$flavor} if $_ == 0;
    }
    # replace samp_rate 0 values with default
    $ref = $$opt{'aud_samp_rate'};
    foreach(@$ref) {
      $_ = $def_aud_samp_rate{$flavor} if $_ == 0;
    }
    # replace channel 0 values with default
    $ref = $$opt{'aud_chan'};
    foreach(@$ref) {
      $_ = 2 if $_ == 0;
    }
  }
  $$opt{'aud_num'} = scalar @$ref;
  
  # --- subtitle setup ---
  my($sref) = $$opt{'sub_track'};
  $$opt{'sub_num'} = scalar @$sref;
  
  return 1;
}

# =============================================================================
# ========== probe_input($inp_ref,$opt_ref) ===================================
# =============================================================================
# fill input hash with information about the source
# returns: ok=1, error=0
sub probe_input {
  my($in)   = shift;
  my($opt)  = shift;

  # fetch source and optional title
  my($ifile) = $$opt{'input'};
  my($title) = $$opt{'dvd_title'};

  # call tcprobe
  print_log("Probing '$ifile' (title $title) with 'tcprobe'...\n");
  my($cmd) = "tcprobe -T $title -i \"$ifile\"";
  $_ = run_cmd($cmd,1);
  my(@lines) = split(/\n/);
  if(m/fail/) {
    print_log("ERROR: probing  with tcprobe failed!\n");
    return 0;
  }

  my($aud_total) = 0;
  my(@aud_samp_rate);
  my(@aud_info);
  my(@aud_chan);
  
  my($sub_total) = 0;
  my(@sub_type);

  # ----- DVD -----------------------------------------------------------------
  if(m,DVD image/device,) {
    # tc import codecs
    $$in{'vcodec'} = 'dvd';
    $$in{'acodec'} = 'dvd';

    # --- chapter analysis ---
    if(!m/(\d+) chapter\(s\)/) {
      print_log("  parse error: no chapters found!\n");
      return 0;
    }
    my($chaps) = $1;
    $$in{'chapters'} = $1;

    # start time for each chapter
    my(@chap_start);
    foreach(@lines) {
      if(m/Chapter \d+.*(\d\d:\d\d:\d\d\.\d\d\d)/) {
        push @chap_start,$1;
      }
    }
    $$in{'chap_start'} = \@chap_start;

    # --- find chapters for sample image extraction ---
    # sample chapters max
    my($max) = $$opt{'sample_max'};

    # build sample list
    my(@offset);
    my(@chap);
    my($i);

    # too few chapters -> search in first chapter only
    if($chaps<$max) {
      my($off) = $$opt{'sample_begin'};
      for($i=0;$i<$max;$i++) {
        push @chap,1; # always in first chapter
        push @offset,$off;
        $off += $$opt{'sample_step'};
      }
    } else {
      # clamp max range
      my($off) = $$opt{'sample_chapframe'};
      $max = $chaps-1 if($max > ($chaps-1));
      for($i=0;$i<$max;$i++) {
        push @offset,$off; # make sure to skip first (partial) gop
        push @chap,$i+2;   # chapter number (starting with 2)
      }
    }
    $$in{'sample_chaps'} = \@chap;
    $$in{'sample_offsets'} = \@offset;

    # --- audio/subtitle query ---
    foreach(@lines) {
      if(m/\(dvd_reader.c\) (.*) (\d+)kHz (\d+)Ch/) {
        push @aud_samp_rate,$2 * 1000;
        push @aud_chan,$3;
        my($info) = $1;
        $info =~ s/\s+/_/g;
        push @aud_info,$info;
        $aud_total++;
      }
      if(m/\(dvd_reader.c\) subtitle \d+=<(\w+)>/) {
        push @sub_type,$1;
        $sub_total++;
      }
    }
  }
  # ----- AVI -----------------------------------------------------------------
  elsif(m/RIFF data, AVI/) {
    # extract avi codec
    if(!m/codec=(\w+)/) {
      print_log("  parse error: no avi codec\n");
      return 0;
    }
    my($codec)=$1;

    # --- video codec ---
    # is it a DIVX file?
    if(($codec eq "DIV3")||
       ($codec eq "DIV4")||
       ($codec eq "DIV5")||
       ($codec eq "DIVX")||
       ($codec eq "divx")||
       ($codec eq "DX50")) {
      $$in{'vcodec'} = $$opt{'modavi_divx'};
    } elsif($codec eq "XVID") {
      $$in{'vcodec'} = "xvid";
    } else {
      $$in{'vcodec'} = $$opt{'modavi_other'};
    }
  }
  # ---- MPEG streams ---------------------------------------------------------
  elsif(m/MPEG|CDXA/) {
    # --- set codec ---
    $$in{'vcodec'} = "mpeg2";
    $$in{'acodec'} = "mp3";

    # we use mpglen for exact MPEG frame count
    print_log("  calling 'mpglen' to determine frame count... (this may take a while!)\n");
    my($frames) = run_cmd("mpglen -f \"$ifile\"",0);
    $$in{'frames'} = $frames;
  }

  # unknown input
  else {
    print_log("ERROR: no known input codec found!\n");
    return 0;
  }

  # ----- Common Setup --------------------------------------------------------
  # --- parse generic audio output of transcode ---
  if($aud_total == 0) {
    my($acodec) = 'auto';
    foreach(@lines) {
      if(m/audio track:.*-e (\d+),\d+,(\d+).*-n (\S+)/) {
        push @aud_samp_rate,$1;
        push @aud_info,$3;
        push @aud_chan,$2;
        $aud_total++;

        # pick right codec (assume all channels are the same)
        # PCM
        if($3 eq '0x01') {
          $acodec = 'avi';
        }
        # MP3
        elsif($3 eq '0x55') {
          $acodec = 'mp3';
        }
        # MP2
        elsif($3 eq '0x50') {
          $acodec = 'mp2';
        }
        # AC3
        elsif($3 eq '0x2000') {
          $acodec = 'ac3';
        }
        # WMA
        elsif($3 eq  '0x161') {
          $acodec = 'mplayer';
        }
      }
    }
    $$in{'acodec'} = $acodec;
  }

  # --- audio parameters ---
  $$in{'aud_total'} = $aud_total;
  $$in{'aud_samp_rate'} = \@aud_samp_rate;
  $$in{'aud_chan'} = \@aud_chan;
  $$in{'aud_info'} = \@aud_info;
  $aud_total = 0;
  print_log("  audio tracks:\n");
  foreach(@aud_samp_rate) {
    print_log("    ") if($aud_total % 2 == 0);
    print_log(sprintf "%02u=%2ukHz,%1uch(%s) ",
             $aud_total,
	     int($aud_samp_rate[$aud_total]/1000),
	     $aud_chan[$aud_total],
             $aud_info[$aud_total]);
    $aud_total++;
    print_log("\n") if($aud_total % 2 == 0);
  }
  print_log("\n") if($aud_total % 2 != 0);

  # --- subtitles ---
  $$in{'sub_total'} = $sub_total;
  $$in{'sub_type'} = \@sub_type;
  if($sub_total > 0) {
    print_log("  subtitles:\n");
    $sub_total = 0;
    foreach(@sub_type) {
      print_log("    ") if($sub_total % 8 == 0);
      print_log(sprintf("%02u=%s ",$sub_total,$_));
      $sub_total++;
      print_log("\n") if($sub_total % 8 == 0);
    }
    print_log("\n") if($sub_total % 8 != 0);
  }

  # --- video parameters ---
  # size
  if(!m/import frame size: -g (\d+)x(\d+)/) {
    print_log("  parse error: no frame size found!\n");
    return 0;
  }
  $$in{'size'} = [ $1,$2 ];

  # aspect
  my($asr_derived) = 0;
  my($asr_forced)  = 0;
  if($$opt{'in_asr'}[0]!=0) {
    $$in{'aspect'} = $$opt{'in_asr'};
    $asr_forced = 1;
  } elsif(!m/aspect ratio: (\d+):(\d+)/) {
    $$in{'aspect'} = $$in{'size'};
    $asr_derived = 1;
  } else {
    $$in{'aspect'} = [ $1,$2 ];
  }

  # frame rate
  if($$opt{'in_frc'} != -1) {
    # force frame rate
    $$in{'fps'} = $frame_rate_codes[$$opt{'in_frc'}];
    $$in{'frc'} = $$opt{'in_frc'};
  } else {
    if(!m/frame rate: -f ([\d\.]+) .* frc=(\d+)/) {
      print_log("  parse error: no frame rate/frc found!\n");
      return 0;
    }
    $$in{'fps'} = $1;
    $$in{'frc'} = $2;
  }

  # frames and duration in secs
  if(!defined($$in{'frames'})) {
    if(m/V: (\d+) frames, (\d+) sec/) {
      $$in{'frames'} = $1;
      $$in{'secs'} = $2;
    } elsif(m/length: (\d+) frames, frame_time=(\d+) msec/) {
      $$in{'frames'} = $1;
      $$in{'secs'} = $1 * $2 / 1000;
    } else {
      print_log("  parse error: no frames, secs!\n");
      return 0;
    }
  }
  if(!defined($$in{'secs'})) {
      $$in{'secs'} = $$in{'frames'} / $$in{'fps'};
  }

  # --- setup sample images if none is given ---
  if(!defined($$in{'sample_offsets'})) {
    $$in{'sample_offsets'} = [];
    $$in{'sample_chaps'} = [];
    my($off) = $$opt{'sample_begin'};
    my($chap)= $$in{'sample_chaps'};
    my($offs) = $$in{'sample_offsets'};
    for($i=0;$i<$$opt{'sample_max'};$i++) {
      push @$chap,1; # dummy chapter
      push @$offs,$off % $$in{'frames'};
      $off += $$opt{'sample_step'};
    }
  }

  # overwrite input codec?
  my($ivcodec) = $$opt{'ivcodec'};
  if($ivcodec ne 'auto') {
    print_log("  vcodec: '$ivcodec' (forced)\n");
    $$in{'vcodec'} = $ivcodec;
  } else {
    print_log("  vcodec: '$$in{'vcodec'}' (detected)\n");
  }
  my($iacodec) = $$opt{'iacodec'};
  if($iacodec ne 'auto') {
    print_log("  acodec: '$iacodec' (forced)\n");
    $$in{'acodec'} = $iacodec;
  } else {
    print_log("  acodec: '$$in{'acodec'}' (detected)\n");
  }

  # verbose
  my($size) = $$in{'size'};
  my($asr)  = $$in{'aspect'};
  $asr = $$asr[0] / $$asr[1];
  print_log(sprintf "  size:   %3dx%3d     aspect: %3.2g:1 %s\n",
           $$size[0],$$size[1],$asr,
           $asr_forced ? '(forced)' : $asr_derived ? '(from size)' : '(probed)');

  # play time
  my($secs) = $$in{'secs'};
  my($ms)   = int(($secs - int($secs)) * 1000);
  my($min)  = int($secs / 60);
  $secs -= $min * 60;
  my($hour) = int($min / 60);
  $min -= $hour * 60;
  print_log(sprintf "  frames: %06d    playtime: %02d:%02d:%02d.%03d\n",
           $$in{'frames'},$hour,$min,$secs,$ms);
  my($code) = $frame_rate_codes[$$in{'frc'}];
  print_log(sprintf("  fps:    %-14s frc: %s ($code)\n",$$in{'fps'},$$in{'frc'}));
  if($$in{'frames'} < 1000) {
    print_log("WARNING: movie is very short!! (maybe you selected the wrong title?)\n");
  }
    
  # --- do we need input clipping? ---
  # try to find a reasonable clip region
  return 1 if($$opt{'probe_only'});
  return find_clip(\%input,\%options)
}

# --- do_user_clip ------------------------------------------------------------
# adjust clipping with user feedback
# returns: -
sub do_user_clip {
  my($opt,$clip,$samples) = @_;

  print "  interactive clipping (enter '?' for help):\n";
  my(@bsamples) = map {/(.*)(\.pgm)/ && "$1-m$2" } @$samples;

  # init clip parameters
  my(@thres) = @{$$opt{'clip_thres'}};
  my(@step)  = @{$$opt{'clip_tstep'}};
  my(@bmod)  = @{$$opt{'clip_bmodulo'}};
  my(@fmod)  = @{$$opt{'clip_fmodulo'}};
  my(@offset)= @{$$opt{'clip_offset'}};
  my($extra) = $$opt{'clip_extra'};

  while(1) {
    # now call pgmfindclip
    my($opts) = "-b $bmod[0],$bmod[1] -f $fmod[0],$fmod[1] ";
    $opts .= "-t $thres[0],$thres[1] -o $offset[0],$offset[1] -w ";
    $opts .= $extra if($extra ne '');
    print_log("  searching clip region (pgmfindclip $opts): ");
    my($cmd) = "pgmfindclip $opts @$samples";
    $_ = run_cmd($cmd,0);
    if(!m/(\d+),(\d+),(\d+),(\d+)/) {
      print_log("\nFATAL: call to pgmfindclip failed???\n");
    } else {
      @$clip = ($1,$2,$3,$4);
      print_log("current clip: " . join(',',@$clip) . "\n");
    }

    # show result with ImageMagick's display
    print "[displaying]\r";
    run_cmd("display ".join(' ',@bsamples),0);

    # ----- handle user input -----
    my($quit) = 0;
    my($stay) = 1;
    while($stay) {
      $stay = 0;
      print "[enter cmd]> ";
      $_ = <STDIN>;
      # 'q' quit
      if(m/^q$/) {
        $quit = 1;
      }
      # 'x' adjust x threshold
      elsif(m/^x\s*([+-=])(\d*)$/) {
        my($inc) = ($2 eq '')?$step[0]:$2;
        if($1 eq '+') { $thres[0] += $inc; }
        elsif($1 eq '-') { $thres[0] -= $inc; }
        else { $thres[0] = $inc; }
      }
      # 'y' adjust y threshold
      elsif(m/^y\s*([+-=])(\d*)$/) {
        my($inc) = ($2 eq '')?$step[1]:$2;
        if($1 eq '+') { $thres[1] += $inc; }
        elsif($1 eq '-') { $thres[1] -= $inc; }
        else { $thres[1] = $inc; }
      }
      # 'b' border modulo
      elsif(m/^b\s*(\d+),(\d+)/) {
        $bmod[0] = $1;
        $bmod[1] = $2;
      }
      # 'f' border modulo
      elsif(m/^f\s*(\d+),(\d+)/) {
        $fmod[0] = $1;
        $fmod[1] = $2;
      }
      # 'o' offset
      elsif(m/^o\s*(\d+),(\d+)/) {
        $offset[0] = $1;
        $offset[1] = $2;
      }
      # 'e' extra options
      elsif(m/^e\s*(.*)/) {
        $extra = $1;
      }
      # else help
      else {
        print "x <+-=>[val]  change x threshold, e.g. x+ x-10 x=20\n";
        print "y <+-=>[val]  change y threshold\n";
        print "b <bx>,<by>   change border modulo\n";
        print "f <fx>,<fy>   change frame modulo\n";
        print "o <ox>,<oy>   change search offset\n";
        print "e <str>       add extra options to pgmfindclip\n";
        print "q             quit interactive clipping\n";
        $stay = 1;
      }
    }
    last if($quit);
  }
  foreach(@bsamples) {
    unlink($_);
  }
  print_log("  user clip result: " . join(',',@$clip) . "\n");
}

# --- find_clip($inp_ref,$opt_ref) --------------------------------------------
# adds a 'tc_clip' on success
# returns: 0=failed 1=ok
sub find_clip {
  my($in) = shift;
  my($opt) = shift;

  # the clip area
  my(@clip);
  print_log("Searching clipping values for input frames...\n");

  # --- use forced clip values ---
  my($force) = $$opt{'clip_force'};
  if($force ne '') {
    print_log("  using forced values: $force\n");
    @clip = split(/,/,$force);
    my($len) = scalar @clip;
    if($len==1) {
      push @clip,$clip[0],$clip[0],$clip[0];
    } elsif($len==2) {
      push @clip,$clip[0],$clip[1];
    } elsif($len==3) {
      push @clip,$clip[1];
    } elsif($len!=4) {
      print_log("ERROR: wrong clip parameters passed!\n");
      return 0;
    }
  }
  # --- automatic clip with pgmfindclip ---
  else {
    # --- generate a sample gray image for each vob file ---
    my($chaps)   = $$in{'sample_chaps'};
    my($offsets) = $$in{'sample_offsets'};
    my($num);
    my(@samples);
    my($sfile) = "sample000000.pgm"; # the file that transcode generates
    my($numchap) = scalar @$chaps;

    print_log("  generating $numchap sample images with transcode...\n");
    for($num=0;$num<$numchap;$num++) {
      # span a single image 'range'
      my($f) = $$offsets[$num];
      my($g) = $f+1;

      # input param
      my($ivcodec) = $$in{'vcodec'};
      my($input)   = "-x $ivcodec,null -i \"$$opt{'input'}\"";
      my($chap)    = $$chaps[$num];
      $input .= " -T $$opt{'dvd_title'},$chap" if($ivcodec eq 'dvd');

      # output name (project name without flavor)
      my($name) = $$opt{'project'};
      $name =~ s/-[^-]+$//;
      my($pgm_name) = sprintf("$name-SAMPLE-%02d.pgm",$num);
    
      # report frame
      if($ivcodec eq 'dvd') {
        print_log(sprintf("    chapter %02d/frame %03d: $pgm_name",$chap,$f));
      } else {
        print_log(sprintf("    frame %05d: $pgm_name",$f));
      }
      
      # call transcode only if file is missing or reuse mode is off
      if((!-s $pgm_name) || ($$opt{'sample_recycle'}==0)) {
        # call transcode
        $cmd = "transcode $input -o sample -y ppm -c $f-$g -z -K";
        my($log) = run_cmd($cmd,0);

        # check generated sample image
        if(!-s $sfile) {
          print_log("\nERROR: can't create sample file '$sfile'\n");
          return 0;
        }

        # rename to clipXX.pgm
        rename $sfile,"$pgm_name";
        print_log(" (extracted)\n");
      } else {
        print_log(" (recycled)\n");
      }

      # store sample images in list
      push(@samples,"$pgm_name");
    }
    # safety check
    if(scalar @samples == 0) {
      print_log("FATAL: no sample images found!!\n");
      return 0;
    }

    # --- find best clip window for all sample images ---
    if($$opt{'clip_user'} == 1) {
      # perform interactive clipping
      do_user_clip($opt,\@clip,\@samples);
    } else {
      # pgmfindclip parameters
      my($thres) = $$opt{'clip_thres'};
      my($bmod)  = $$opt{'clip_bmodulo'};
      my($fmod)  = $$opt{'clip_fmodulo'};
      my($offset)= $$opt{'clip_offset'};
      my($extra) = $$opt{'clip_extra'};

      # now call pgmfindclip
      my($opts) = "-b $$bmod[0],$$bmod[1] -f $$fmod[0],$$fmod[1] ";
      $opts .= "-t $$thres[0],$$thres[1] -o $$offset[0],$$offset[1] ";
      $opts .= $extra if($extra ne '');
      print_log("  finding clip region (pgmfindclip $opts): ");
      my($cmd) = "pgmfindclip $opts @samples";
      $_ = run_cmd($cmd,0);
      if(!m/(\d+),(\d+),(\d+),(\d+)/) {
        print_log("\nFATAL: call to pgmfindclip failed???\n");
        return 0;
      }
      @clip = ($1,$2,$3,$4);
      print_log(join(',',@clip) . "\n");
    }

    # --- remove temp files if not in debug mode --
    if($debug_level>0) {
      print_log("  DEBUG: keeping sample image files.\n");
    } else {
      if($$opt{'sample_recycle'}==0) {
        foreach(@samples) {
          unlink($_);
        }
      }
    }
  }

  # all zero?
  my($sum)=0;
  foreach(@clip) {
    $sum += $_;
  }

  # no, some clipping is required
  if($sum>0) {
    # store clip parameter
    print_log("  final clip: " . join(',',@clip) . "\n");
    $$in{'clip'} = \@clip;

    # store clip_size
    my($size) = $$in{'size'};
    my($w,$h);
    $w = $$size[0] - ($clip[1]+$clip[3]);
    $h = $$size[1] - ($clip[0]+$clip[2]);
    $$in{'clip_size'} = [$w,$h];

    # store new clip aspect
    my($aspect) = $$in{'aspect'};
    my($a_x) = $$size[1] * $$aspect[0] * $w;
    my($a_y) = $$size[0] * $$aspect[1] * $h;
    $$in{'clip_aspect'} = [$a_x,$a_y];
  } else {
    print_log("  no clipping required!\n");
  }
  return 1;
}

# =============================================================================
# ========== prepare_output($inp_ref,$out_ref,$opt_ref) =======================
# =============================================================================
# fill in all required values in the output hash
# return: 0=error, 1=ok
sub prepare_output {
  my($in)  = shift;
  my($out) = shift;
  my($opt) = shift;

  # fetch output flavor
  my($flavor) = $$opt{'flavor'};

  # ----- find output codecs ----
  my($ovcodec) = $$opt{'ovcodec'};
  my($oacodec) = $$opt{'oacodec'};
  $ovcodec = $def_vcodec{$flavor} if($ovcodec eq 'auto');
  $oacodec = $def_acodec{$flavor} if($oacodec eq 'auto');
  $$out{'vcodec'} = $ovcodec;
  $$out{'acodec'} = $oacodec;
  $$out{'vc_opt'} = $$opt{'ovc_opt'};

  # ----- setup output tv_norm (out fps, out frc) -----
  return 0 if(!prepare_tv_norm($in,$out,$opt));

  # ----- check and prepare output audio setup -----
  return 0 if(!prepare_audio($in,$out,$opt));

  # ----- check and prepare subtitles -----
  return 0 if(!prepare_subs($in,$out,$opt));

  # ----- calc vrates for range of cds (given the audio rate) -----
  return 0 if(!calc_vrates_per_num_cd($in,$out,$opt));

  # ----- flavor setup -----
  # -- vcd,svcd,dvd --
  if(($flavor eq 'vcd')||($flavor eq 'svcd')||($flavor eq 'dvd')) {
    return 0 if(!prepare_vcd_svcd_dvd($in,$out,$opt));
  }
  # -- avi/ogm/mkv --
  # given: aspect
  # find: size
  else {
    return 0 if(!find_output_size($in,$out,$opt));

    # set common options
    $$out{'can_letterbox'} = 0;
    $$out{'anamorph'} = 0;
    $$out{'pulldown'} = 0;
    $$out{'vbr'} = 1;
    $$out{'max_vrate'} = $$opt{'max_vrate'};
  }

  # ----- determine vrate for each cd -----
  if($$in{'vcodec'} eq 'dvd') {
    return 0 if(!recalc_vrate_for_chapter_sets($in,$out,$opt));
  } else {
    calc_ranges_for_each_cd($in,$out);
  }

  # ----- clamp video rates -----
  my($cd_vrates) = $$out{'cd_vrate'};
  $max_vrate = $$out{'max_vrate'};
  foreach(@$cd_vrates) {
    if(!$$out{'vbr'}) {
      print_log("  forcing fixed vrate $max_vrate (was $_)\n");
      $_ = $max_vrate;
    }
    if($_ > $max_vrate) {
      print_log("  clamping vrate $_ -> $max_vrate\n");
      $_ = $max_vrate;
    }
  }

  # --- verbose video output parameter ----------------------------------------
  print_log("\nDerived video output:\n");

  # video parameter
  print_log("  tv norm:  '$$out{'tv_norm'}'\n");
  print_log(sprintf "  fps:      %5.3f        frc: %d\n",$$out{'fps'},$$out{'frc'});
  print_log("  pulldown: " .($$out{'pulldown'}?'yes':'no ')
            ."      anamorph: ".($$out{'anamorph'}?'yes':'no')."\n");
  my($size)   = $$out{'size'};
  my($aspect) = $$out{'aspect'};
  $aspect = $$aspect[0]/$$aspect[1];
  print_log(sprintf "  size:     %dx%d    aspect: %3.2g:1\n",
           $$size[0],$$size[1],$aspect);
  my($cd_num) = $$out{'cd_num'};
  my($vrate)  = $$out{'cd_vrate'};
  print_log("  vrate:    " . ($$out{'vbr'}?'vbr ':'cbr '). join(', ',@$vrate)." kbps\n");

  # --- determine frame transformation ---
  # adds tc_trafo entry
  find_frame_trafo($in,$out,$opt);
  return 1;
}

# ----- prepare_vcd_svcd_dvd --------------------------------------------------
# setup output parameters suitable for vcd/svcd encoding
# returns: 1=ok 0=error
sub prepare_vcd_svcd_dvd
{
  my($in)  = shift;
  my($out) = shift;
  my($opt) = shift;

  # --- anamorph encoding suitable? ---
  my($anamorph) = $$opt{'anamorph'};
  if($anamorph==-1) {
    # autodetect: yes if asr at least 16:9
    my($in_asr) = $$in{'aspect'};
    $in_asr = $$in_asr[0] / $$in_asr[1];
    if($in_asr > 1.7) {
      $anamorph = 1;
    } else {
      $anamorph = 0;
    }
  }

  # --- setup flavor restrictions ---------------------------------------------
  my($tv_norm)   = $$out{'tv_norm'};
  my($vbr)       = 0;
  my($max_vrate) = $$opt{'max_vrate'};

  # --- need a valid tv norm! ---
  if($tv_norm eq 'none') {
    print "ERROR: tv norm 'none' not supported for VCD/SVCD/DVD flavor!\n";
    return 0;
  }

  # ----- dvd -----
  if($$opt{'flavor'} eq 'dvd') {
    $vbr       = ($$opt{'dvd_min_vbr_quant'}>0)?1:0;
    $max_vrate = $$opt{'dvd_max_kbps'} - $$opt{'aud_sum_rate'};

    # aspect? anamorph?
    if($anamorph && $$opt{'dvd_can_anamorph'}) {
      $$out{'aspect'} = [16,9];
      $$out{'anamorph'} = 1;
    } else {
      $$out{'aspect'} = [4,3];
      $$out{'anamorph'} = 0;
    }
    
    # frame size?
    my($res) = $$opt{'dvd_res'};
    $$out{'size'} = [$$res[0],$$res[$tv_norm eq 'pal' ? 2:1]];

    # -- 3:2 pulldown flag is required for NTSC film --
    $$out{'pulldown'} = ($$out{'frc'} == 1)?1:0; # ntscfilm pulldown
  }
  # ----- svcd -----
  elsif($$opt{'flavor'} eq 'svcd') {
    # -- follow the standard
    if($$opt{'svcd_conform'}==1) {
      # vbr, max vrate
      $vbr       = 1;
      $max_vrate = 2748 - $$out{'aud_sum_rate'};

      # 4:3 no anamorph
      $$out{'aspect'} = [4,3];
      $$out{'anamorph'} = 0;

      # fixed size
      if($tv_norm eq 'pal') {
        $$out{'size'} = [480,576];
      } else {
        $$out{'size'} = [480,480];
      }
    }
    # -- XSVCD
    else {
      # vbr? vrate?
      $vbr       = ($$opt{'svcd_min_vbr_quant'}>0)?1:0;
      $max_vrate = $$opt{'svcd_max_kbps'} - $$out{'aud_sum_rate'};

      # aspect? anamorph?
      if($anamorph && $$opt{'svcd_can_anamorph'}) {
        $$out{'aspect'} = [16,9];
        $$out{'anamorph'} = 1;
      } else {
        $$out{'aspect'} = [4,3];
        $$out{'anamorph'} = 0;
      }

      # frame size?
      my($res) = $$opt{'svcd_res'};
      $$out{'size'} = [$$res[0],$$res[$tv_norm eq 'pal' ? 2:1]];
    }

    # -- 3:2 pulldown flag is required for NTSC film --
    $$out{'pulldown'} = ($$out{'frc'} == 1)?1:0; # ntscfilm pulldown
  }
  # ----- vcd -----
  else {
    # -- follow the standard
    if($$opt{'vcd_conform'}==1) {
      # cbr, fixed rate
      $vbr       = 0;
      $max_vrate = 1150;

      # fixed 4:3 no anamorph
      $$out{'aspect'} = [4,3];
      $$out{'anamorph'} = 0;

      # fixed frame size
      if($tv_norm eq 'pal') {
        $$out{'size'} = [352,288];
      } else {
        $$out{'size'} = [352,240];
      }
    } else {
      # vbr? vrate?
      $vbr       = ($$opt{'vcd_min_vbr_quant'}>0)?1:0;
      $max_vrate = $$opt{'vcd_max_kbps'} - $$out{'aud_sum_rate'};

      # aspect? anamorph?
      if($anamorph && $$opt{'vcd_can_anamorph'}) {
        $$out{'aspect'} = [16,9];
        $$out{'anamorph'} = 1;
      } else {
        $$out{'aspect'} = [4,3];
        $$out{'anamorph'} = 0;
      }

      # frame size?
      my($res) = $$opt{'vcd_res'};
      $$out{'size'} = [$$res[0],$$res[$tv_norm eq 'pal' ? 2:1]];
    }

    # no 3:2 pulldown for MPEG1 available
    $$out{'pulldown'} = 0;
  }

  # ----- now determine #no cds -----
  find_num_cd($out,$opt,$max_vrate,$vbr);

  # ----- VCD/SVCD/DVD can use letterboxing -----
  $$out{'can_letterbox'} = 1;
  $$out{'vbr'}           = $vbr;
  $$out{'max_vrate'}     = $max_vrate;

  return 1;
}

# ------ calc_ranges_for_each_cd(out) ------------------------------------
# create the cd_vrate array and fill in the same value for each cd
# returns: -
sub calc_ranges_for_each_cd {
  my($in)  = shift;
  my($out) = shift;

  my($cd_num) = $$out{'cd_num'};
  my($vrate)  = $$out{'vid_rate'};
  my(@cd_vrate);
  # copy given vrate[cd_num] to all entries
  foreach(0 .. $cd_num-1) {
    push @cd_vrate,$$vrate[$cd_num-1];
  }
  $$out{'cd_vrate'} = \@cd_vrate;

  # calc cd_ranges
  my($total)  = $$in{'frames'};
  # frame rate conversion?
  if($$in{'fps'} != $$out{'fps'}) {
    $total = int($total * $$out{'fps'} / $$in{'fps'});
  }
  my(@cd_ranges);
  my($part) = $total / $cd_num;
  foreach(0 .. $cd_num-2) {
    $cd_ranges[$_] = sprintf("%06u-%06u",$_ * $part,($_+1)*$part);
  }
  $cd_ranges[$cd_num-1] = sprintf("%06u-%06u",($cd_num-1)*$part,$total);

  # verbose
  print_log("Split movie into frame ranges:\n");
  foreach(1 .. $cd_num) {
    print_log("  cd$_: range $cd_ranges[$_-1]\n");
  }
  
  $$out{'cd_ranges'} = \@cd_ranges;
}

# ----- recalc_vrate_for_chapter_sets($in,$out,$opt) --------------------------
# when ripping a dvd then each cd holds a region of chapters
# here we adjust the vrates for each cd so that the chapter range fits
# returns: 0=error, 1=ok
sub recalc_vrate_for_chapter_sets {
  my($in)  = shift;
  my($out) = shift;
  my($opt) = shift;

  print_log("Recalculating videorate for each DVD chapter set... (chaplin)\n");

  my($title) = $$opt{'dvd_title'};
  my($fps) = $$out{'fps'};

  # additional SVCD/VCD options for chaplin: XML + menus
  my($prj) = $$opt{'project'};
  my($xopt) = "-y $prj-pal.yuv"; # always create yuv palette
  my($flavor) = $$opt{'flavor'};
  my($chap_menu) = $$opt{'chap_menu'};
  if(($flavor eq 'svcd')||($flavor eq 'vcd')) {
    # XML creation
    $xopt .= sprintf(" -x \"$prj,%s,$prj-CD%%d.xml,$prj-CD%%d.mpg,$prj-CD%%d-menu%%d.mpg\"",
                    ($flavor eq 'svcd')?'svcd':'vcd');
    if($chap_menu>0) {
      # create chapter menus
      $xopt .= " -m $chap_menu -g $prj-menu.txt";
      print_log("  (creating chapter menu in '$prj-menu.txt' - please edit)\n");
    }
  }

  # pass requested number of chapter sets and call chaplin
  my($parts) = "-p $$out{'cd_num'}";
  my($cmd)="chaplin -d \"$$opt{'input'}\" -t $title -f $fps $xopt $parts";
  my($log) = run_cmd($cmd,0);
  my(@result) = split(/\n/,$log);
  
  # if creating menus then fetch the required menu MPG files
  if($chap_menu>0) {
    foreach(@result) {
      if(m/added menu '([^']+)/) {
	push @menu_mpgs,$1;
      }
    }
    $$out{'menu_mpgs'} = \@menu_mpgs;
  }

  # eval chaplin output
  my(@cd_chaps);
  my($max_chap)=0;
  my($total_frames)=0;
  my(@cd_vrate);
  foreach(@result) {
    if(m/^part.*chapters (\d+)-(\d+).*frames.*\+(\d+)/) {
      push @cd_chaps,"$1-$2";
      print_log("  chapters $1-$2: $3 frames\n");

      # recalc vrate for chapter set
      my($frames) = $3;
      my($cd_rate) = $$out{'cd_bits'} * $$out{'fps'} / ($frames * 1000);
      my($vr) = int($cd_rate - $$out{'novideo_rate'} - 0.5);
      push @cd_vrate,$vr;

      $max_chap = $2 if($2 > $max_chap);
      $total_frames += $frames;

      # recheck size
      my($trate) = $vr + $$out{'novideo_rate'};
      my($ncd_size) = int(0.5 + ($trate * $frames * 1000 /
                                 ($$out{'fps'} * 8 * 1024 * 1024)));

      print_log(sprintf "    video rate:   %4u kbps\n",$vr);
      print_log(sprintf "    total rate:   %4u kbps\n",$trate);
      print_log(sprintf "    cd size:      %4u MB\n",$ncd_size);
    }
  }
  if($max_chap==0) {
    print_log("FATAL: no chapters found in output of chaplin???\n");
    return 0;
  }
  $$out{'cd_chaps'}   = \@cd_chaps;
  $$out{'cd_vrate'}   = \@cd_vrate;
  print_log("  total frames:   $total_frames ($$in{'frames'} reported)\n");
  return 1;
}

# --- prepare_audio($inp_ref,$out_ref,$opt_ref) -------------------------------
# setup and check audio relevant components for output
# returns: 0=error, 1=ok
sub prepare_audio {
  my($in)  = shift;
  my($out) = shift;
  my($opt) = shift;

  # fetch output flavor
  my($flavor) = $$opt{'flavor'};

  # ----- audio setup -----
  # check if all requested tracks are available in input
  my($aud_track)     = $$opt{'aud_track'};
  my($aud_num)       = $$opt{'aud_num'};
  my($aud_rate)      = $$opt{'aud_rate'};
  my($aud_samp_rate) = $$opt{'aud_samp_rate'};
  my($aud_chan)      = $$opt{'aud_chan'};
  my($inp_info)      = $$in{'aud_info'};
  my($inp_total)     = $$in{'aud_total'};

  # map audio language name to track number
  foreach(@$aud_track) {
    # check if language is given
    if(/^[a-zA-z]+$/) {
      my($lang) = $_;
      my($track) = -1;
      # try to find audio track for language
      foreach(0 .. $inp_total-1) {
        if($$inp_info[$_] =~ m/_${lang}_/i) {
          $track = $_;
          last;
        }
      }
      if($track == -1) {
        print_log "FATAL: Could not find audio track for language '$lang'\n";
        return 0;
      }
      $_ = $track;
    }
  }
  foreach(@$aud_track) {
    if($_ >= $inp_total) {
      print_log("ERROR: requested audio track $_ is not available in input!\n");
      return 0;
    }
  }

  # check vcd requirements
  if($flavor eq 'vcd') {
    # VCD standard
    if($$opt{'vcd_conform'}==1) {
      # maximum number of audio tracks
      if($aud_num>1) {
        print_log("ERROR: VCDs only supports one track!\n");
        return 0;
      }
      # only 224kbps
      if($$aud_rate[0]!=224) {
        print_log("ERROR: VCDs only support 224 kbps audio!\n");
        return 0;
      }
    }
    # XVCD
    else {
      # maximum number of audio tracks
      if($aud_num>$$opt{'vcd_aud_max_tracks'}) {
        print_log("ERROR: VCDs only support $$opt{'vcd_aud_max'} track(s)!\n");
        return 0;
      }
      # supported audio bitrate?
      my($rates) = $$opt{'vcd_aud_rates'};
      foreach(@$aud_rate) {
        my($rate) = $_;
        my($found) = 0;
        foreach(@$rates) {
          if($_ == $rate) {
            $found = 1;
            last;
            }
        }
        if($found == 0) {
          print_log("ERROR: audio rate $rate kbps no supported for VCDs\n");
          return 0;
        }
      }
    }
  }
  # check svcd requirements
  elsif($flavor eq 'svcd') {
    # standard SVCD
    if($$opt{'svcd_conform'}==1) {
      # at most 2 tracks
      if($aud_num>2) {
        print_log("ERROR: SVCDs only support 1 or 2 track(s)!\n");
        return 0;
      }
      # check bitrate
      foreach(@$aud_rate) {
        my($rate) = $_;
        if(($rate<32)||($rate>384)) {
          print_log("ERROR: SVCDs only support bitrate in [32;384]!\n");
          return 0;
        }
      }
    }
    # XSVCD
    else {
      # maximum number of audio tracks
      if($aud_num>$$opt{'svcd_aud_max_tracks'}) {
        print_log("ERROR: SVCDs only support $$opt{'svcd_aud_max'} track(s)!\n");
        return 0;
      }
      # supported audio bitrate?
      my($rates) = $$opt{'svcd_aud_rates'};
      foreach(@$aud_rate) {
        my($rate) = $_;
        my($found) = 0;
        foreach(@$rates) {
          if($_ == $rate) {
            $found = 1;
            last;
          }
        }
        if($found == 0) {
              print_log("ERROR: audio rate $rate kbps no supported for SVCDs\n");
          return 0;
        }
      }
    }
  }
  # check dvd requirements
  elsif($flavor eq 'dvd') {
    # maximum number of audio tracks
    if($aud_num>$$opt{'dvd_aud_max_tracks'}) {
      print_log("ERROR: DVDs only support $$opt{'dvd_aud_max'} track(s)!\n");
      return 0;
    }
    # supported audio bitrate?
    my($rates) = $$opt{'dvd_aud_rates'};
    foreach(@$aud_rate) {
      my($rate) = $_;
      my($found) = 0;
      foreach(@$rates) {
        if($_ == $rate) {
          $found = 1;
          last;
        }
      }
      if($found == 0) {
        print_log("ERROR: audio rate $rate kbps no supported for DVDs\n");
        return 0;
      }
    }
  }
  
  # calc total audio output rate
  my($aud_sum_rate) = 0;
  foreach(@$aud_rate) {
    $aud_sum_rate += $_;
  }
  $$out{'aud_sum_rate'} = $aud_sum_rate;

  # give audio output summary
  print_log("Requested audio output:\n");
  $aud_num = 0;
  foreach(@$aud_track) {
    print_log(sprintf "  audio%u: %5u kbps  %5u kHz  %u chan [src=#%u:%s]\n",
             $aud_num,
             $$aud_rate[$aud_num],
             $$aud_samp_rate[$aud_num],
             $$aud_chan[$aud_num],
             $_,$$inp_info[$_]);
    $aud_num++;
  }
  print_log(sprintf "  total:  %5u kbps\n",$aud_sum_rate);
  return 1;
}

# --- ($inp_ref,$out_ref,$opt_ref) --------------------------
# setup and check subtitles for output
# returns: 0=error, 1=ok
sub prepare_subs {
  my($in)  = shift;
  my($out) = shift;
  my($opt) = shift;

  # fetch output flavor
  my($flavor) = $$opt{'flavor'};

  # check if all requested tracks are available in input
  my($sub_track)     = $$opt{'sub_track'};
  my($sub_num)       = $$opt{'sub_num'};
  my($inp_type)      = $$in{'sub_type'};
  my($inp_total)     = $$in{'sub_total'};

  # nothing to do
  return 1 if($sub_num == 0);

  # map audio language name to track number
  foreach(@$sub_track) {
    # check if language is given
    if(/^[a-zA-z]+$/) {
      my($lang) = $_;
      my($track) = -1;
      # try to find subtitle track for language
      foreach(0 .. $inp_total-1) {
        if($$inp_type[$_] =~ m/${lang}/i) {
          $track = $_;
          last;
        }
      }
      if($track == -1) {
        print_log "FATAL: Could not find subtitle track for language '$lang'\n";
        return 0;
      }
      $_ = $track;
    }
  }
  foreach(@$sub_track) {
    if($_ >= $inp_total) {
      print_log("ERROR: requested subtitle track $_ is not available in input!\n");
      return 0;
    }
  }

  # we can render only one sub title
  if(($$opt{'sub_render'})&&($sub_num > 1)) {
    print_log("ERROR: you can only render a single subtitle!!\n");
    return 0;
  }

  # check s/vcd requirements
  if(($flavor eq 'vcd')||($flavor eq 'svcd')) {
    if($sub_num > 1) {
      print_log("ERROR: only a single subtitle supported for S/VCD!\n");
      return 0;
    }
    if(!$$opt{'sub_render'}) {
      print_log("ERROR: S/VCD need rendered subtitles!\n");
      return 0;
    }
  }
  
  # give subtitle summary
  print_log("Subtitle setup:\n");
  $sub_num = 0;
  foreach(@$sub_track) {
    my($src_trk) = $$sub_track[$sub_num];
    print_log(sprintf("  sub%u:     %s [src=#%u]\n",
                      $sub_num,$$inp_type[$src_trk],$src_trk));
    $sub_num++;
  }    
  return 1;
}

# --- calc_vrates_per_num_cd($inp,$out,$opt) ----------------------------------
# calc max vrate available on one, two or more discs if audio+mux rate is given
# returns: 0=error, 1=ok
sub calc_vrates_per_num_cd {
  my($in)  = shift;
  my($out) = shift;
  my($opt) = shift;

  # fetch output flavor
  my($flavor) = $$opt{'flavor'};

  print_log("Calculating video output rate for CD sets...\n");

  # --- bits on a cd ---
  # MB available on a CD
  my($cd_size) = int(($$opt{'cd_size'} - $cd_reserve{$flavor})
                     * $cd_raw_scale{$flavor} * $$opt{'cd_frac'});
  if($cd_size<1) {
    print_log("ERROR: no space on CD left ($cd_size MB!)\n");
    return 0;
  }
  $$out{'cd_size'} = $cd_size;
  # Bits available on a CD
  my($cd_bits) = $cd_size * 1024 * 1024 * 8;
  $$out{'cd_bits'} = $cd_bits;

  # av rate on a CD in kbps
  my($cd_rate) = int(($cd_bits * $$out{'fps'} / ($$in{'frames'} * 1000))-0.5);

  print_log(sprintf "  space for movie:    %4u MB = $cd_bits bits\n",$cd_size);
  print_log(sprintf "  total audio rate:   %4u kbps\n",$$out{'aud_sum_rate'});
  print_log(sprintf "  required mux rate:  %4u kbps\n",$mux_add_rate{$flavor});

  # fixed add on to video rate
  my($novideo_rate) = $$out{'aud_sum_rate'} + $mux_add_rate{$flavor};
  $$out{'novideo_rate'} = $novideo_rate;

  # --- video bitrate for each cd num ---
  my(@vrate,@fbits);
  my($max_vrate) = $$opt{'max_vrate'};
  for($i=0;$i<$$opt{'cd_max'};$i++) {
    $j = $i+1;
    my($vid_rate) = int(($j*$cd_rate - $novideo_rate)-0.5);
    if($vid_rate<100) {
      print_log("ERROR: video bitrate $vid_rate kbps too low!\n");
      return 0;
    }
    if($vid_rate>$max_vrate) {
      if($j < $$opt{'cd_max'}) {
        print_log("     [restricting max CDs to $j; vrate already $vid_rate!]\n");
        $$opt{'cd_max'} = $j;
        # correct force cd
        $$opt{'cd_force'} = $j if($$opt{'cd_force'} > $j);
      }
    }
    my($frame_bits) = int(($vid_rate * 1000 / $$out{'fps'})-0.5);
    print_log(sprintf "  %u cd video rate:    %4u kbps   %6u bits per frame\n",
             $j,$vid_rate,$frame_bits);
    push @vrate,$vid_rate;
    push @fbits,$frame_bits;
  }
  $$out{'vid_rate'} = \@vrate;
  $$out{'frame_bits'} = \@fbits;
  return 1;
}

# --- prepare_tv_norm($in,$out,$opt) ------------------------------------------
# setup output tv norm. either given or derive from input frc
# returns: 0=error, 1=ok
sub prepare_tv_norm {
  my($in)  = shift;
  my($out) = shift;
  my($opt) = shift;

  my($tv_norm,$fps,$frc);

  # --- determine tv_norm, fps and frc for output ---
  $tv_norm = $$opt{'tv_norm'};
  if($tv_norm eq 'auto') {
    # automatic detect from input frc code:
    my($frc)=$$in{'frc'};
    if($frc == 3) {
      $tv_norm = 'pal';
    } elsif($frc == 4) {
      $tv_norm = 'ntsc';
    } elsif($frc == 1) {
      $tv_norm = 'ntscfilm';
    } else {
      $tv_norm = 'none';
    }
  }

  # --- now set output fps and frc according to norm ---
  if($tv_norm eq 'pal') {
    $fps = 25.0;
    $frc = 3;
  } elsif($tv_norm eq 'ntsc') {
    $fps = 29.97;
    $frc = 4;
  } elsif($tv_norm eq 'ntscfilm') {
    $fps = 23.976;
    $frc = 1;
  } else {
    $fps = $$in{'fps'};
    $frc = $$in{'frc'};
  }

  # set output
  $$out{'tv_norm'} = $tv_norm;
  $$out{'fps'}     = $fps;
  $$out{'frc'}     = $frc;

  return 1;
}

# --- calc_frame_height($width,$height,$ratio,$modulo) ------------------------
# calculate a frame size that respects the given modulo (AVI export)
# and has the smallest ratio error
# returns: ($new_width,$new_height)
sub calc_frame_height {
  my($width,$ratio,$modulo) = @_;

  my($blocks,$height);
  my($h1,$r1,$d1,$h2,$r2,$d2);

  # calc real height with aspect
  $height = $width / $ratio;
  # round to nearest modulo value
  $blocks = int($height / $modulo);
  $h1 = $blocks * $modulo;
  $h2 = ($blocks + 1) * $modulo;
  $r1 = $width / $h1;
  $r2 = $width / $h2;
  $d1 = abs($r1-$ratio)/$ratio;
  $d2 = abs($r2-$ratio)/$ratio;
  # check both ratio errors and chose better one
  if($d1 < $d2) {
    return ($h1,$d1);
  } else {
    return ($h2,$d2);
  }
}

# --- enum_frames($opt_ref,$ratio) --------------------------------------------
# generate a list of possible output frame sizes (AVI export)
# returns: array of "$w,$h" or empty
sub enum_frames {
  my($opt)   = shift;
  my($ratio) = shift;

  my($w,$h,$d);
  my(@res);
  my($mod) = $$opt{'frame_mod'};
  my($range) = $$opt{'width_range'};
  my($min) = $$range[0];
  my($max) = $$range[1];
  my($asrerr) = $$opt{'asrerr_max'};

  # loop over all possible widths
  print_log("  enumerating framesize from $min to $max, modulo $mod\n");
  print_log("    pick entries with aspect ratio error <= $asrerr%\n");
  for($w=$min;$w<=$max;$w+=$mod) {
    ($h,$d) = calc_frame_height($w,$ratio,$mod);
    # convert error into percent
    $d *= 100;
    # skip resolution if error is too large
    print_log(sprintf "    %dx%d:     %4.2g %%",$w,$h,$d);
    if($d>$asrerr) {
      print_log("  too large\n");
    } else {
      print_log("  OK\n");
      push(@res,"$w,$h");
    }
  }

  # no width found
  if(scalar @res == 0) {
    print_log("ERROR: No suitable resolutions found! (increase asr error?)\n");
  }
  return @res;
}

# --- find_output_size($inp_ref,$out_ref,$opt_ref) ----------------------------
# automatically determine size of display with square pixels (AVI export)
# returns: 0=failed, 1=ok
sub find_output_size {
  my($in)  = shift;
  my($out) = shift;
  my($opt) = shift;

  print_log("Searching suitable output size for square pixels display...\n");

  # first get aspect for target
  my($aspect);
  if(defined($$in{'clip_aspect'})) {
    $aspect = $$in{'clip_aspect'};
  } else {
    $aspect = $$in{'aspect'};
  }

  # --- determine output frame size ---
  my(@nres);
  my($size) = $$opt{'size_force'};
  my($w,$h);
  $w = $$size[0];
  $h = $$size[1];
  if(($w!=0)&&($h!=0)) {
    print_log("  using forced size ${w}x$h\n");
    push @res,"$w,$h";
  }
  else {
    # --- enumerate possible target frames ---
    # enum possible target sizes, but smaller or equal than source
    my(@eres) = enum_frames($opt,$$aspect[0]/$$aspect[1]);
    return 0 if(scalar(@eres)==0);

    # pick by width
    if($w!=0) {
      foreach(@eres) {
        ($ew,$eh) = split(/,/);
        if($w==$ew) {
          push @res,$_;
          print_log("  picked by given width: $_\n");
        }
      }
    }
    # pick by height
    elsif($h!=0) {
      foreach(@eres) {
        ($ew,$eh) = split(/,/);
        if($h==$eh) {
          push @res,$_;
          print_log("  picked by given height: $_\n");
        }
      }
    }
    # otherwise use enumerated sizes
    @res = @eres if(scalar @res == 0);
  }

  # --- find an optimal number of CDs ---
  print_log("Find optimal number of CDs for encoding...\n");

  my($cd_num)     = $$opt{'cd_force'};
  my($vid_rate)   = $$out{'vid_rate'};

  # force number of CDs and only a single resolution available
  if(($cd_num!=0)&&(scalar @res == 1)) {
    ($w,$h) = split(/,/,$res[0]);
    print_log("  using forced #CDs: $cd_num and frame size ${w}x$h\n");
  }
  # search
  else {
    my($bpprange)   = $$opt{'bpp_range'};
    my($bppmin)     = $$bpprange[0];
    my($bppmax)     = $$bpprange[1];
    my($frame_bits) = $$out{'frame_bits'};
    my($cd_max)     = $$opt{'cd_max'};
    my($cd_begin)   = 0;
    my($cd_end)     = $cd_max;

    # shrink search region if a cd number is forced
    if($cd_num!=0) {
      $cd_begin = $cd_end = $cd_num-1;
    }

    # init hit array for each cd
    print_log("  listing frame size x #CDs in bpp (avg. bits per pixel)\n");
    print_log("    pick entries with bpp in [$bppmin;$bppmax]\n");
    print_log("        #CD:  ");
    my(@found);
    for($cd=0;$cd<$cd_max;$cd++) {
      print_log(sprintf "     %02d   ",$cd+1);
      push @found,[];
    }
    print_log("\n");
    print_log("     vrates:  ");
    for($cd=0;$cd<$cd_max;$cd++) {
      print_log(sprintf "   %4d   ",$$vid_rate[$cd]);
      push @found,[];
    }
    print_log("\n");

    # --- loop through all sizes ---
    my($total)=0;
    foreach(@res) {
      ($w,$h) = split /,/;
      print_log(sprintf("    %dx%d:   ",$w,$h));
      my($size) = $w * $h;
      my($cd);
      for($cd=0;$cd<$cd_max;$cd++) {
        my($bpp) = $$frame_bits[$cd] / $size;
        print_log(sprintf("%6.3g",$bpp));

        # check only in region
        if(($cd>=$cd_begin)&&($cd<=$cd_end)) {
          # is in bpp interval?
          if(($bppmin<=$bpp)&&($bpp<=$bppmax)) {
            # bpp fits in range
            my($tab) = $found[$cd];
            push @$tab,"$w,$h";
            $total++;
            print_log("**  ");
          } else {
            print_log("    ");
          }
        } else {
          print_log("--  ");
        }
      }
      print_log("\n");
    }
    if($total==0) {
      print_log("ERROR: found NO suitable resolution! (use -g and/or -c)\n");
      return 0;
    }
    print "  found $total suitable resolution(s)\n";

    # --- find lowest number of CDs and largest size ---
    for($cd=0;$cd<$cd_max;$cd++) {
      my($tab) = $found[$cd];
      my($len) = scalar @$tab;
      if($len > 0) {
        $cd_num = $cd+1;
        ($w,$h) = split(/,/,$$tab[$len-1]);
        last;
      }
    }
  }

  # store all values
  my($vrate) = $$vid_rate[$cd_num-1];
  print_log("  using #$cd_num CD, vrate: $vrate kbps, frame size: ${w}x$h\n");
  $$out{'cd_num'} = $cd_num;
  $$out{'size'}   = [$w,$h];
  $$out{'aspect'} = $aspect;
  return 1;
}

# --- find_num_cd($out_ref,$opt_ref,$rate,$is_vbr) -----------------------------
# find a suitable number of CDs by judging the comparing the resulting vrate
# with the given value (used for VCD/SVCD/DVD)
# returns: -
sub find_num_cd {
  my($out)  = shift;
  my($opt)  = shift;
  my($rate) = shift; # given rate
  my($vbr)  = shift; # is vbr mode?

  print_log("Determining number of CDs for given bitrate...\n");
  print_log(sprintf "  %s output vrate:   $rate kbps\n",($vbr)?'max':'fixed');

  my($cd_max) = $$opt{'cd_max'};
  my($cd_num) = $$opt{'cd_force'};
  my($vrate) = $$out{'vid_rate'};
  print_log("  avg. vrate per #cd: ".join(', ',@$vrate)
           ." kbps\n  -> will use #cd:    ");

  # force cd number
  if($cd_num>0) {
    print_log("$cd_num (forced)\n");
    $$out{'cd_num'} = $cd_num;
  }
  # search for cd number
  else {
    my($cd);
    # vbr: pick the largest number of cds where cd rate <= max rate
    if($vbr) {
      for($cd=$cd_max;$cd>=1;$cd--) {
        if($$vrate[$cd-1]<=$rate) {
          $$out{'cd_num'} = $cd;
          last;
        }
      }
      $$out{'cd_num'} = 1 if($cd==0);
    }
    # cbr: pick the smalles number of cds where cd rate >= fixed rate
    else {
      for($cd=1;$cd<=$cd_max;$cd++) {
        if($$vrate[$cd-1]>=$rate) {
          $$out{'cd_num'} = $cd;
          last;
        }
      }
      $$out{'cd_num'} = $cd_max if($cd==$cd_max+1);
    }
    print_log("$$out{'cd_num'} (= $$vrate[$$out{'cd_num'}-1] kbps)\n");
  }
}

# --- shrink_input($inp_ref,$shrink_x,$shrink_y) ------------------------------
# enlarge the current clipping region of input by sx and sy
# and center the resulting frame
# returns: -
sub shrink_input {
  my($in) = shift;
  my($sx) = shift;
  my($sy) = shift;

  my($size);
  if(defined($$in{'clip_size'})) {
    $size = $$in{'clip_size'};
  } else {
    $size = $$in{'size'};
  }

  # reduce size
  my($in_w) = $$size[0];
  my($in_h) = $$size[1];
  $in_w -= $sx;
  $in_h -= $sx;

  # modify input size and aspect
  $$in{'clip_size'}   = [$in_w,$in_h];
  $$in{'clip_aspect'} = [$in_w,$in_h];

  # now change clip
  my($x) = int($sx/2);
  my($y) = int($sy/2);
  if(defined($$in{'clip'})) {
    my($t,$l,$b,$r);
    my($clip) = $$in{'clip'};
    ($t,$l,$b,$r) = @$clip;
    $t += $y;
    $b += $sy - $y;
    $l += $x;
    $r += $sx - $x;
    $$in{'clip'}=[$t,$l,$b,$r];
  } else {
    $$in{'clip'}=[$y,$x,$sy-$y,$sx-$x];
  }
}

# --- find_frame_trafo($inp_ref,$out_ref,$opt_ref) ----------------------------
# given: input frame size, aspect and output frame size, aspect
# find: place input frame with correct aspect into output frame
#       add letterbox if necessary
# returns: -
sub find_frame_trafo {
  my($in)  = shift;
  my($out) = shift;
  my($opt) = shift;

  print_log("Input->output frame transformation...\n");

  # --- fetch parameters ------------------------------------------------------
  my($in_asr,$in_size);
  # use clipped size
  if(defined($$in{'clip_aspect'})) {
    $in_asr  = $$in{'clip_aspect'};
    $in_size = $$in{'clip_size'};
  } else {
    $in_asr  = $$in{'aspect'};
    $in_size = $$in{'size'};
  }
  my($out_asr)  = $$out{'aspect'};
  my($out_size) = $$out{'size'};
  my($in_w)   = $$in_size[0];
  my($in_h)   = $$in_size[1];
  my($out_w)  = $$out_size[0];
  my($out_h)  = $$out_size[1];
  my($in_ax)  = $$in_asr[0];
  my($in_ay)  = $$in_asr[1];
  my($out_ax) = $$out_asr[0];
  my($out_ay) = $$out_asr[1];
  my($can_lb)  = $$out{'can_letterbox'};
  # pixel tolerance: we may shrink input in each direction to use fast rescale
  my($tol) = $$opt{'scl_tol'};

  # --- verbose parameter ---
  my($in_aspect)  = $in_ax  / $in_ay;
  my($out_aspect) = $out_ax / $out_ay;
  print_log(sprintf "  input:   %3dx%3d    asr: %3.2g:1\n",
           $in_w,$in_h,$in_aspect);
  print_log(sprintf "  output:  %3dx%3d    asr: %3.2g:1\n",
           $out_w,$out_h,$out_aspect);

  # --- adjust target height if we need aspect correction ---------------------
  my($out_rh) = $out_h;

  # if input and output aspects only differ < asrerr_max -> same aspect
  # OLD: if($in_ax * $out_ay == $in_ay * $out_ax) {
  if(abs($in_aspect-$out_aspect)<($$opt{'asrerr_max'}*0.01)) {
    print_log("  output height:   $out_rh (same aspect)\n");
  } elsif($can_lb) {
    # real output frame width (when assuming square pixels)
    my($out_rw) = $out_ax * $out_h / $out_ay;
    # real output frame height when assuming input aspect
    $out_rh = $in_ay * $out_rw / $in_ax;
    # round to output modulo
    my($frame_mod) = $$opt{'frame_mod'};
    $out_rh = int($out_rh / $frame_mod) * $frame_mod;
    print_log("  output height:   $out_rh (aspect correction)\n");
    if($out_rh>$out_h) {
      print_log("  WARNING: asr height > output height -> clipped\n");
      $out_rh = $out_h;
    }
  }

  # --- check scale mode ------------------------------------------------------
  # 0=nothing  1=only shrink  2=only enlarge  3=shr x enl y  4=enl x shr y
  my($diff_x) = $out_w  - $in_w;
  my($diff_y) = $out_rh - $in_h;
  my($mode);
  if(($diff_x==0)&&($diff_y==0)) {
    $mode = 0;
  } else {
    my($sign) = $diff_x * $diff_y;
    if($sign < 0) {
      # both: shrink and enlarge
      if($diff_x<0) {
        $mode=3;
      } else {
        $mode=4;
      }
    } else {
      if(($diff_x>0)||($diff_y>0)) {
        $mode=2;
      } else {
        $mode=1;
      }
    }
  }
  my(@smode) = ("same size","only shrink","only enlarge",
                "x-shrink y-enlarge","x-enlarge y-shrink");
  print_log("  scale mode:      $smode[$mode]\n");

  # --- find modulo for scale mode --------------------------------------------
  my($modulo);
  if($mode==0) {
    # no scaling required
    $modulo=0;
  } else {
    $modulo=1;
    # check valid modulos for fast operation
    foreach(32,16,8) {
      my($mod_x) = abs($diff_x) % $_;
      my($mod_y) = abs($diff_y) % $_;
      # accept parameter in tolerance range
      if(($mod_x<=$tol)&&($mod_y<=$tol)) {

        # check resulting image size
        my($new_w) = $in_w + int($diff_x / $_) * $_;
        my($new_h) = $in_h + int($diff_y / $_) * $_;
        # new image size fullfills modulo
        if(($new_w % $_ == 0)&&($new_h % $_ == 0)) {

          # we need to change image
          if(($mod_x>0)||($mod_y>0)) {
            # change input image!!!
            shrink_input($in,$mod_x,$mod_y);
            print_log("  (reduced input size by $mod_x,$mod_y -> modulo $_)\n");
            # correct differences between input and output frame size
            $diff_x += ($diff_x>0)?$mod_x:-$mod_x;
            $diff_y += ($diff_y>0)?$mod_y:-$mod_y;
          }
          $modulo = $_;
          last;
        }
      }
    }
  }
  # verbose
  if($modulo>0) {
    print_log("  scale modulo:    $modulo = " . ($modulo>1?'fast':'slow')."\n");
  }

  # --- first add clip operation ----------------------------------------------
  $opt = "";
  if(defined($$in{'clip'})) {
    my($clip) = $$in{'clip'};
    $opt = "-j " . join(',',@$clip) . " ";
  }

  # --- find tc command for scale operation -----------------------------------
  # now perform scale with tc
  if($modulo==0) {
    # nothing to do
  }
  elsif($modulo==1) {
    $opt = "-Z ${out_w}x$out_rh ";
  }
  else {
    my($num_x) = int($diff_x / $modulo);
    my($num_y) = int($diff_y / $modulo);
    # shrink component
    if(($num_x<0)||($num_y<0)) {
      $opt .= "-B "
        . ($num_y<0?abs($num_y):'0') . ","
        . ($num_x<0?abs($num_x):'0') . ",$modulo ";
    }
    # enlarge component
    if(($num_x>0)||($num_y>0)) {
      $opt .= "-X "
        . ($num_y>0?$num_y:'0') . ","
        . ($num_x>0?$num_x:'0') . ",$modulo ";
    }
  }

  # --- add letterbox ---------------------------------------------------------
  my($blackbar) = $out_h - $out_rh;
  if($blackbar > 0) {
    my($top) = $blackbar/2;
    my($bot) = $blackbar - $top;
    print_log("  add letterbox:   top=$top, bottom=$bot\n");
    $opt .= "-Y -$top,0,-$bot,0 ";
  }

  # store final frame transformation for transcode
  print_log("  transcode param: $opt\n");
  $$out{'tc_trafo'} = $opt;
}

# =============================================================================
# ========== generate_makefile($inp_ref,$out_ref,$opt_ref) ====================
# =============================================================================
# returns: 0=ok, 1=error
sub generate_makefile {
  my($in)  = shift;
  my($out) = shift;
  my($opt) = shift;
  my($i,$j);

  print_log("Generating makefile...\n");

  # --- create makefile ---
  my($project)= $$opt{'project'};
  my($mkfile) = "$project.mak";
  print_log("  file:   '$mkfile'\n");
  if(!open(FH,">$mkfile")) {
    print_log("ERROR: opening makefile $mkfile!\n");
    return 0;
  }
  # --- write banner ---
  print FH "# rip *makefile*: $mkfile\n";
  print FH "# generated by $ripmake\n";
  print FH "# --- config ---\n";

  # --- output make variables ---
  print_log("    emitting make variables...\n");
  emit_var_input($in,$out,$opt);
  emit_var_output($out,$opt);
  emit_var_acodec($in,$out,$opt);
  emit_var_vcodec($in,$out,$opt);
  emit_var_subs($in,$out,$opt);

  # --- begin rules ---
  $project = "\$(TARGET)";
  my($vcodec) = $$out{'vcodec'};
  my($vc_opt) = $$out{'vc_opt'};
  my($acodec) = $$out{'acodec'};
  my($flavor)  = $$opt{'flavor'};
  print_log("  flavor: '$flavor'\n  vcodec: '$vcodec'  options: '$vc_opt'\n  acodec: '$acodec'\n");

  # --- determine stages for makefile ---
  my(%make);
  setup_make($in,$out,$opt,\%make);
  dump_info("[5]--> Make",\%make) if($debug_level>=5);

  # --- rules ---
  print_log("    writing rules...\n");
  print_log("  global: all clean\n");
  print FH "\n# ----- MAIN RULES -----\n"
    . "all:   sample\n"
    . "clean: clean-sample clean-rip\n";
  emit_sample_rules(\%make);
  emit_rip_rules(\%make);

  # --- close file and ready --
  close(FH);
  return 1;
}

# ========== VARIABLE STUFF ===================================================

# --- emit_var_input($in,$out,$opt) -------------------------------------------
# emit input specific variables in makefile
# returns: -
sub emit_var_input {
  my($in)  = shift;
  my($out) = shift;
  my($opt) = shift;

  # ---- input ----
  print FH "# generic input parameters\n";
  my($source) = $$opt{'input'};
  $source =~ s/ /\\ /g;
  print FH "SOURCE =$source\n";
  print FH "IVCODEC=$$in{'vcodec'}\n";
  print FH "IACODEC=$$in{'acodec'}\n";
  print FH "INPUTV =-x \$(IVCODEC),null\n";
  print FH "INPUTA =-x null,\$(IACODEC)\n";
  print FH "INPUTAV=-x \$(IVCODEC),\$(IACODEC)\n";

  # ---- input ranges and sources for each cd ----
  print FH "\n# input ranges and sources for each target cd\n";
  my($range) = int($$out{'fps'}*60); # a minute sample
  my($cd_num) = $$out{'cd_num'};
  print FH "RANGE_SAMPLE=0-$range\n";

  # dvd mode - direct select with chapters
  if($$in{'vcodec'} eq 'dvd') {
    print FH "TITLE=$$opt{'dvd_title'}\n";
    print FH "ANGLE=1\n";

    # DVD sample
    my($sample_chap) = int($$in{'chapters'} / 2);
    $sample_chap = 1 if($sample_chap==0);
    print FH "CHAP_SAMPLE=$sample_chap\n";

    # DVD chapter range for each CD
    my($cd_chaps) = $$out{'cd_chaps'};
    foreach(1..$cd_num) {
      my($j) = $_-1;
      print FH "CHAP_CD$_=$$cd_chaps[$j]\n";
    }

    # sources
    print FH "SRC_SAMPLE=\$(SOURCE)\n";
    foreach(1..$cd_num) {
      print FH "SRC_CD$_=\$(SOURCE)\n";
    }

    # construct transcode selector
    print FH "TSEL_SAMPLE=-i \$(SRC_SAMPLE)"
      . " -T \$(TITLE),\$(CHAP_SAMPLE),\$(ANGLE)"
      . " -c \$(RANGE_SAMPLE)\n";
    foreach(1..$cd_num) {
      print FH "TSEL_CD$_=-i \$(SRC_CD$_)"
        . " -T \$(TITLE),\$(CHAP_CD$_),\$(ANGLE)\n";
    }
  }
  # avi input mode
  else {
    # ranges
    my($cd_ranges) = $$out{'cd_ranges'};
    foreach(1..$cd_num) {
      print FH "RANGE_CD$_=$$cd_ranges[$_-1]\n";
    }

    if($cd_num>1) {
      # sources
      my($mid) = int($cd_num/2)+1;
      if($$opt{'split_avi'}) {
        print FH "SRC_SAMPLE=\$(TARGET)-SRC-CD$mid.avi\n";
      } else {
        print FH "SRC_SAMPLE=\$(SOURCE)\n";
      }
      foreach(1..$cd_num) {
        if($$opt{'split_avi'}) {
          print FH "SRC_CD$_=\$(TARGET)-SRC-CD$_.avi\n";
        } else {
          print FH "SRC_CD$_=\$(SOURCE)\n";
        }
      }
    } else {
      # single cd source/selectors
      print FH "SRC_SAMPLE=\$(SOURCE)\n"
        . "SRC_CD1=\$(SOURCE)\n";
    }
    # selectors
    print FH "TSEL_SAMPLE=-i \$(SRC_SAMPLE) -c \$(RANGE_SAMPLE)\n";
    foreach(1..$cd_num) {
      if($$opt{'split_avi'}) {
        print FH "TSEL_CD$_=-i \$(SRC_CD$_)\n";
      } else {
        print FH "TSEL_CD$_=-i \$(SRC_CD$_) -c \$(RANGE_CD$_)\n";
      }
    }
  }
}

# --- emit_var_output($out,$opt) ----------------------------------------------
# emit output specific variables in makefile
# returns: -
sub emit_var_output {
  my($out) = shift;
  my($opt) = shift;

  # ---- video parameter ----
  print FH "\n# video param\n";
  print FH "TRAFO=$$out{'tc_trafo'}\n";
  my($vkbps) = $$out{'cd_vrate'};
  my($cd_num) = $$out{'cd_num'};
  foreach(1..$cd_num) {
    my($j) = $_-1;
    my($sum) = $$vkbps[$j] + $$out{'aud_sum_rate'};
    print FH "VRATE_CD$_=$$vkbps[$j]\n";
    print FH "SRATE_CD$_=$sum\n";
  }
  my($sum) = $$vkbps[0] + $$out{'aud_sum_rate'};
  print FH "VRATE_SAMPLE=$$vkbps[0]\n";
  print FH "SRATE_SAMPLE=$sum\n";

  # ---- audio parameter ----
  my($aud_track) = $$opt{'aud_track'};
  my($aud_rate)  = $$opt{'aud_rate'};
  my($aud_num)   = $$opt{'aud_num'};
  my($aud_vbr)   = $$opt{'aud_vbr_qual'};
  print FH "\n# audio param\n";
  for($i=0;$i<$aud_num;$i++) {
    print FH "ATRACK_T$i=$$aud_track[$i]\n";
    print FH "ARATE_T$i=$$aud_rate[$i]\n";
    print FH "AVBR_T$i=" . (($$aud_vbr[$i]>0)?1:0) . "\n";
    print FH "AVBR_QUAL_T$i=" . (($$aud_vbr[$i]>0)?$$aud_vbr[$i]:5) . "\n";
  }

  # ---- output ----
  print FH "\n# output\n";
  print FH "TARGET  =$$opt{'project'}\n";
  print FH "OVCODEC =$$out{'vcodec'}\n";
  print FH "OACODEC =$$out{'acodec'}\n";
  print FH "OUTPUTV =-y \$(OVCODEC),null\n";
  print FH "OUTPUTA =-y null,\$(OACODEC)\n";
  print FH "OUTPUTAV=-y \$(OVCODEC),\$(OACODEC)\n";

  # ----- output extensions -----
  print FH "\n# output extensions\n";
  # --- video extension
  my($flavor) = $$opt{'flavor'};
  if($flavor eq 'vcd') {
    print FH "OVC_EXT=.m1v\n";
  } elsif(($flavor eq 'svcd')||($flavor eq 'dvd')){
    print FH "OVC_EXT=.m2v\n";
  } else {
    print FH "OVC_EXT=-v.avi\n";
  }
  # --- audio extension
  my($oac) = $$out{'acodec'};
  if($oac eq 'mp2enc') {
    print FH "OAC_EXT=.mpa\n";
  } elsif($oac eq 'toolame') {
    print FH "OAC_EXT=.mp2\n";
  } elsif($oac eq 'raw') {
    print FH "OAC_EXT=.avi\n";
  } elsif($oac eq 'ogg') {
    print FH "OAC_EXT=.ogg\n";
  }
  # --- mux extension
  if(($flavor eq 'svcd')||($flavor eq 'vcd')) {
    print FH "MUX_EXT=.mpg\n";
    print FH "BIN_EXT=.bin\n";
    print FH "CUE_EXT=.cue\n";
  }
  elsif($flavor eq 'dvd') {
    print FH "MUX_EXT=.mpg\n";
    print FH "IMG_EXT=.img\n";
  }
  elsif($flavor eq 'avi') {
    print FH "MUX_EXT=.avi\n";
  }
  elsif($flavor eq 'ogm') {
    print FH "MUX_EXT=.ogm\n";
  }
  elsif($flavor eq 'mkv') {
    print FH "MUX_EXT=.mkv\n";
  }

  # --- filters ---
  print FH "\n# user transcode options for video/audio\n";
  if($$opt{'tc_vopt'} ne '') {
    print FH "V_OPT=$$opt{'tc_vopt'}\n";
  } else {
    print FH "# use V_OPT to add video options\n";
  }
  if($$opt{'tx_aopt'} ne '') {
    print FH "A_OPT=$$opt{'tc_aopt'}\n";
  } else {
    print FH "# use A_OPT to add audio options\n";
  }
    
  # --- menu mpgs ---
  my($menu_mpgs) = $$out{'menu_mpgs'};
  if(defined($menu_mpgs)) {
    print FH "\n# mpeg files for menus\n"
      . "MENU_MPGS=" . join(' ',@$menu_mpgs) . "\n"
      . "GENMENU_OPTS=-c\n";
  }
}

# --- emit_var_acodec($in,$out,$opt) ------------------------------------------
# setup parameter make variables for audio codec
# returns: -
sub emit_var_acodec {
  my($in)  = shift;
  my($out) = shift;
  my($opt) = shift;

  # fetch input and output sample rate
  my(@aud_isamp_rate);
  my(@aud_osamp_rate);
  my(@aud_isamp_chan);
  my($aud_track) = $$opt{'aud_track'};
  my($aud_num)   = $$opt{'aud_num'};
  for($i=0;$i<$aud_num;$i++) {
    my($iref) = $$in{'aud_samp_rate'};
    push @aud_isamp_rate,$$iref[$$aud_track[$i]];
    my($oref) = $$opt{'aud_samp_rate'};
    push @aud_osamp_rate,$$oref[$i];
    my($icr) = $$in{'aud_chan'};
    push @aud_isamp_chan,$$icr[$$aud_track[$i]];
  }

  # audio codec params
  print FH "\n# acodec params\n";
  for($i=0;$i<$aud_num;$i++) {
    print FH "# channel $i\n";
    print FH "CODERA_T$i=-a \$(ATRACK_T$i) "
      . " -b \$(ARATE_T$i),\$(AVBR_T$i),\$(AVBR_QUAL_T$i)";
    if($aud_isamp_rate[$i]!=$aud_osamp_rate[$i]) {
      my($mod) = '';
      $mod = "-J resample" if ($$out{'acodec'} ne 'toolame');
      print FH " -E $aud_osamp_rate[$i] --a52_dolby_off $mod";
    }
    print FH "\n";

    # audio scaling options
    if($aud_isamp_chan[$i]!=1) {
      print FH "# auto scaling\n";
      print FH "SCL_T${i}_SAMPLE=\$(TARGET)-SAMPLE-a$i.scl\n";
      print FH "SCL_T${i}_SAMPLE_CREATE=-J astat=\$(SCL_T${i}_SAMPLE)\n";
      print FH "SCL_T${i}_SAMPLE_USE=-s `cat \$(SCL_T${i}_SAMPLE)`\n";
      foreach(1..$$out{'cd_num'}) {
        print FH "SCL_T${i}_CD$_=\$(TARGET)-CD$_-a$i.scl\n";
        print FH "SCL_T${i}_CD${_}_CREATE=-J astat=\$(SCL_T${i}_CD$_)\n";
        print FH "SCL_T${i}_CD${_}_USE=-s `cat \$(SCL_T${i}_CD$_)`\n";
      }
    }
  }
}

# --- emit_var_vcodec($in,$out,$opt) ------------------------------------------
# setup parameter make variables for video codec
# returns: -
sub emit_var_vcodec {
  my($in)  = shift;
  my($out) = shift;
  my($opt) = shift;

  # generic video setup
  print FH "\n# vcodec params\n";
  print FH "IN_FPS=$$in{'fps'}\n";
  print FH "IN_FRC=$$in{'frc'}\n";
  print FH "OUT_FPS=$$out{'fps'}\n";
  print FH "OUT_FRC=$$out{'frc'}\n";
  print FH "CODERV=\$(TRAFO) -V -f \$(IN_FPS),\$(IN_FRC) --export_fps \$(OUT_FPS),\$(OUT_FRC)\n";
  print FH "CODERV+= --pulldown\n" if($$out{'pulldown'}==1);
  print FH "ifneq \"\$(IN_FRC)\" \"\$(OUT_FRC)\"\n"
    . " CODERV+= -Jfps\n"
    . "endif\n";
  print FH "CODERV+= -F \"$$out{'vc_opt'}\"\n" if($$out{'vc_opt'} ne '');

  # ---- codec specific audio/video params -----
  # pick a deinterlacer
  my($interlaced) = $$opt{'interlaced'};
  my($deinter) = '';
  $deinter = '-I 3' if($interlaced);

  # avi
  my($flavor) = $$opt{'flavor'};
  if(($flavor eq 'avi')||($flavor eq 'ogm')||($flavor eq 'mkv')) {
    my($extra)='';
    if($deinter ne '') {
      print FH "# avi output\n";
      print FH "CODERV+=$deinter\n";
    }
  }
  # svcd,vcd
  else {
    # output aspect code
    my($asr) = 2;
    $asr = 3 if($$out{'anamorph'}==1);

    # ----- mpeg2enc encoder -----
    my($mode);
    if($$out{'vcodec'} eq 'mpeg2enc') {
      print FH "# mpeg2enc\n";
      my($extra)='';

      # add sequence headers every gop
      # WORKAROUND: forbid splitting
      my($flags)="-s -S 9999";

      # hq encoder settings
      if($$opt{'high_quality'}) {
        $flags .= " -4 1 -2 1";
      }

      # special vcd stuff
      if($flavor eq 'vcd') {
        if($$opt{'vcd_conform'}==1) {
          $mode=1
        } else {
          # XVCD
          $mode=2;
          if($$opt{'vcd_min_vbr_quant'}>0) {
            $flags .= " -q $$opt{'vcd_min_vbr_quant'}";
          }
          if($$opt{'vcd_video_buf'}>0) {
            $flags .= " -V $$opt{'vcd_video_buf'}";
          }
        }
        if($interlaced==1) {
          $extra .= $deinter; # use deinterlacer
        }
      }
      # special svcd stuff
      elsif($flavor eq 'svcd') {
        if($$opt{'svcd_conform'}==1) {
          # HACK: use mode 5 here, too otherwise bitrate selection won't work! 
          $mode=5;
        } else {
          # XSVCD
          $mode=5;
          if($$opt{'svcd_min_vbr_quant'}>0) {
            $flags .= " -q $$opt{'svcd_min_vbr_quant'}";
          }
          if($$opt{'svcd_video_buf'}>0) {
            $flags .= " -V $$opt{'svcd_video_buf'}";
          }
        }
        if($interlaced==1) {
          $flags = '-I1'; # encode interlaced directly
        }
      }
      # special DVD stuff
      elsif($flavor eq 'dvd') {
        # DVD MPEG 2 for dvdauthor
        $mode = 8;
        if($$opt{'dvd_min_vbr_quant'}>0) {
          $flags .= " -q $$opt{'dvd_min_vbr_quant'}";
        }
        if($$opt{'dvd_video_buf'}>0) {
          $flags .= " -V $$opt{'dvd_video_buf'}";
        }
        if($interlaced==1) {
          $flags = '-I1'; # encode interlaced directly
        }
      }

      # build coder options
      print FH "CODERV+=-F \"$mode,$flags\" --export_asr $asr $extra\n";
    }

    # ----- bbmpeg - not supported very well ;) -----
    else {
      print FH "# mpeg\n";
      if($flavor eq 'vcd') { $mode='v'; }
      else { $mode='s'; }
      print FH "CODERV+=-F \"$mode\" --export_asr $asr $deinter\n";
    }
  }
}

# ========== RULE STUFF =======================================================

# --- setup_make($in,$out,$opt,$make) -----------------------------------------
# build $make hash with values required for makefile generation
# returns: -
sub setup_make {
  my($in)  = shift;
  my($out) = shift;
  my($opt) = shift;
  my($mak) = shift;

  my($flavor) = $$opt{'flavor'};

  # does flavor need mastering
  my($do_master) = (($flavor eq 'vcd')||($flavor eq 'svcd')||($flavor eq 'dvd'));

  # if the input is not dvd then split it up
  my($do_split) = (($$in{'vcodec'} ne 'dvd')&&($$out{'cd_num'}>1))?
    $$opt{'split_avi'}:0;

  # set values
  $$mak{'do_master'} = $do_master;
  $$mak{'do_split'}  = $do_split;

  # derive values for makefile
  $$mak{'aud_num'}   = $$opt{'aud_num'};
  $$mak{'cd_num'}    = $$out{'cd_num'};
  $$mak{'flavor'}    = $$opt{'flavor'};
  $$mak{'cd_ranges'} = $$out{'cd_ranges'} if(defined($$out{'cd_ranges'}));
  $$mak{'xml'}       = ($$in{'vcodec'} eq 'dvd')?1:0;
  $$mak{'menu'}      = $$opt{'chap_menu'};
  $$mak{'slim'}      = $$opt{'low_space'};

  # vcd/svcd/dvd setup
  $$mak{'vcd_std'}   =  $$opt{'vcd_conform'};
  $$mak{'vcd_vbr'}   = ($$opt{'vcd_min_vbr_quant'}>0)?1:0;
  $$mak{'vcd_buf'}   =  $$opt{'vcd_video_buf'};
  $$mak{'svcd_std'}  =  $$opt{'svcd_conform'};
  $$mak{'svcd_vbr'}  = ($$opt{'svcd_min_vbr_quant'}>0)?1:0;
  $$mak{'svcd_buf'}  =  $$opt{'svcd_video_buf'};
  $$mak{'dvd_vbr'}   = ($$opt{'dvd_min_vbr_quant'}>0)?1:0;
  $$mak{'dvd_buf'}   =  $$opt{'dvd_video_buf'};
  $$mak{'mpeg_mplex'}=  $$opt{'mpeg_mplex'};

  # subs setup
  $$mak{'sub_num'}   = $$opt{'sub_num'};
  $$mak{'sub_dvd'}   = $$in{'vcodec'} eq 'dvd';
}

# --- get_video_name($mak,$tgt) -----------------------------------------------
# return the name of the generated video file for a target
sub get_video_name {
  my($mak) = shift;
  my($tgt) = shift;
  return "\$(TARGET)-$tgt\$(OVC_EXT)";
}

# --- get_audio_name($mak,$tgt,$chan) -----------------------------------------
# return the name of the generated audio file for a target
sub get_audio_name {
  my($mak)  = shift;
  my($tgt)  = shift;
  my($chan) = shift;
  return "\$(TARGET)-$tgt-a$chan\$(OAC_EXT)";
}

# --- get_all_audio_names($mak,$tgt) ------------------------------------------
# return all generated audio names
sub get_all_audio_names {
  my($mak) = shift;
  my($tgt) = shift;

  my($j);
  my(@names);
  for($j=0;$j<$$mak{'aud_num'};$j++) {
    my($name) = get_audio_name($mak,$tgt,$j);
    push @names,$name if(defined($name));
  }
  return join(' ',@names);
}

# --- get_mux_name($mak,$tgt) -------------------------------------------------
# return the filename after multiplexing
sub get_mux_name {
  my($mak) = shift;
  my($tgt) = shift;
  return "\$(TARGET)-$tgt\$(MUX_EXT)";
}

# --- get_master_name($mak,$tgt) ----------------------------------------------
# return the filenames generated after mastering
sub get_master_name {
  my($mak) = shift;
  my($tgt) = shift;
  
  if($$mak{'flavor'} eq 'dvd') {
    return "\$(TARGET)-$tgt\$(IMG_EXT)";
  } else {
    return "\$(TARGET)-$tgt\$(BIN_EXT) \$(TARGET)-$tgt\$(CUE_EXT)";
  }
}

# --- get_final_name ----------------------------------------------------------
# returns: filenames of rip result separated by spaces
sub get_final_name {
  my($mak)    = shift;
  my($tgt)    = shift;
  my($master) = shift;
  my($names);
  # return the filename after mastering
  if($master) {
    $names = get_master_name($mak,$tgt);
    return $names if(defined($names));
  }
  # return the filename after muxing
  $names = get_mux_name($mak,$tgt);
  return $names;
}

# --- emit_sample_rules($mak) -------------------------------------------------
sub emit_sample_rules {
  my($mak) = shift;

  # get the resulting name
  my($final) = get_final_name($mak,"SAMPLE",0);
  print_log("  sample: sample clean-sample\n");

  # have subtitles?
  my($have_subs) = ($$mak{'sub_num'}>0);
  my($sub_rules) = ($have_subs)?"sub-SAMPLE":"";

  # write sample rule line
  print FH "\n# ========== SAMPLE ==========\n";
  print FH "# render a one minute sample of the movie\n";
  print FH "sample: $sub_rules mux-SAMPLE\n";
  print FH "\t\@echo \"--- sample ready! ---\"\n";
  print FH "\t\@ls -l \"$final\"\n";
  print FH "\t\@echo  $final\n";

  # cleanup rule
  print FH "\nclean-sample: "
    . "clean-video-SAMPLE clean-audio-SAMPLE clean-mux-SAMPLE\n";

  # write video rule
  write_video_rule($mak,"SAMPLE");
  write_audio_rules($mak,"SAMPLE");
  write_mux_rule($mak,"SAMPLE");
  write_sub_rule($mak,"SAMPLE") if($have_subs);
}

# --- emit_rip_rules($mak) ----------------------------------------------------
# returns: -
sub emit_rip_rules {
  my($mak) = shift;

  # have subtitles?
  my($have_subs) = ($$mak{'sub_num'}>0);
  # have menu?
  my($have_menu) = ($$mak{'menu'}>0);

  # get result names
  my($cd_num) = $$mak{'cd_num'};
  my(@names);
  my(@rips);
  my(@crips);

  # add (global9 menu creation
  my($global)  = "";
  my($cglobal) = "";
  if($have_menu) {
    $global  = "menu";
    $cglobal = "clean-menu";
  }
  
  # now add rules for each CD
  foreach(1..$cd_num) {
    push @rips,"rip-CD$_";
    push @crips,"clean-rip-CD$_";
    push @names,get_final_name($mak,"CD$_",$$mak{'do_master'});
  }
  my($rips)  = join(' ',@rips);
  my($crips) = join(' ',@crips);
  my($names) = join(' ',@names);
  
  # main rule
  print_log("     rip: rip $rips clean-rip $crips\n");
  print FH "\n# ========== RIP RULES ==========\n"
    ."\nrip: $global $rips\n"
    . "\t\@echo \"--- rip ready! ---\"\n"
    . "\t\@ls -l $names\n"
    . "\t\@echo \"$names\"\n"
    ."\nclean-rip: $cglobal $crips\n";

  # CD rip rules
  foreach(1..$cd_num) {
    my($j) = $_ - 1;
    my($subs) = ($have_subs)?"sub-CD$_":"";
    my($csubs)= ($have_subs)?"clean-sub-CD$_":"";
    
    if($$mak{'do_master'}) {
      print FH "\n$rips[$j]: $subs master-CD$_\n"
        . "\n$crips[$j]: $csubs clean-video-CD$_ clean-audio-CD$_ clean-mux-CD$_ clean-master-CD$_\n";
    } else {
      print FH "\n$rips[$j]: $subs mux-CD$_\n"
        . "\n$crips[$j]: $csubs clean-video-CD$_ clean-audio-CD$_ clean-mux-CD$_\n";
    }
  }

  # split source avi if requested
  write_split_rule($mak) if($$mak{'do_split'});

  # write menu generation rule
  write_menu_rule($mak) if($have_menu);

  # write rules for each cd
  foreach(1..$cd_num) {
    write_video_rule($mak,"CD$_");
    write_audio_rules($mak,"CD$_");
    write_mux_rule($mak,"CD$_");
    write_mastering_rule($mak,"CD$_") if($$mak{'do_master'});
    write_sub_rule($mak,"CD$_") if($have_subs);
  }
}

# --- write_split_rule($mak,$tgt) ---------------------------------------------
# split an input AVI file into CD chunks
# returns: -
sub write_split_rule {
  my($mak) = shift;
  my($cd_num)    = $$mak{'cd_num'};
  my($cd_ranges) = $$mak{'cd_ranges'};

  # create names of splitted AVIs
  my(@names);
  foreach(1..$cd_num) {
    push @names,"\$(TARGET)-SRC-CD$_.avi";
  }

  # split input AVI with avisplit
  # NOTE: unfortunately this does not work always !!
  print FH "\n# ----- splitting AVI source -----\n";
  foreach(1..$cd_num) {
    print FH "\nsplit-SRC-CD$_ $names[$_-1]: \$(INPUT)\n"
      . "\t\@echo \"--- split-SRC-CD$_ ---\"\n"
      . "\tavisplit -s 0 -t \$(RANGE_CD$_) \\\n"
      . "\t\t-i \$(SOURCE) \\\n"
      . "\t\t-o temp\n"
      . "\t\tmv temp-0000 $names[$_-1]\n";
  }
}

# --- write_video_rule($mak,$tgt) ---------------------------------------------
# write a rule for generating a video stream
# and the audio scale value for the first channel
# returns: -
sub write_video_rule {
  my($mak) = shift;
  my($tgt) = shift; # target: SAMPLE, CD1, CD2, MAIN ...

  my($name)   = get_video_name($mak,$tgt);
  my($flavor) = $$mak{'flavor'};
  my($log)    = "";

  print FH "\n# ----- video rules of $tgt -----\n"
    . "\nvideo-$tgt: $name\n";

  # command for subtitle integration
  my($subcmd) = "\$(RENDER_SUBS)";
  
  # ----- avi,ogm,mkv - render video into an avi container
  if(($flavor eq 'avi')||($flavor eq 'ogm')||($flavor eq 'mkv')) {
    # the 2 pass log file
    $log = "\$(TARGET)-$tgt-v.log";

    # --- pass1 : video analyse + audio 0 scale
    print FH "\nvideo-pass1-$tgt $log: \$(SRC_$tgt)\n";
    print FH "\t\@echo \"--- video-pass1-$tgt ---\"\n";
    print FH "\ttranscode \$(TSEL_$tgt) \$(INPUTAV) \$(OUTPUTV) \\\n"
      . "\t\t\$(CODERV) \\\n"
      . "\t\t\$(CODERA_T0) \\\n"
      . "\t\t-w \$(VRATE_$tgt) \$(V_OPT) \\\n"
      . "\t\t-R 1,$log \\\n"
      . "\t\t\$(SCL_T0_${tgt}_CREATE) \\\n"
      . "\t\t$subcmd\n";

    # --- pass2 : video render + audio 0 render
    print FH "\nvideo-pass2-$tgt $name: $log \$(SRC_$tgt) \$(SCL_T0_$tgt)\n";
    print FH "\t\@echo \"--- video-pass2-$tgt ---\"\n";
    print FH "\ttranscode \$(TSEL_$tgt) \$(INPUTV) \$(OUTPUTV) \\\n"
      . "\t\t\$(CODERV) \\\n"
      . "\t\t-o $name -w \$(VRATE_$tgt) \$(V_OPT) \\\n"
      . "\t\t-R 2,$log \\\n"
      . "\t\t$subcmd\n";
  }
  # ----- svcd,vcd,dvd
  else {
    print FH "\n$name: \$(SRC_$tgt)\n";
    print FH "\t\@echo \"--- video-$tgt ---\"\n";
    print FH "\ttranscode \$(TSEL_$tgt) \$(INPUTAV) \$(OUTPUTV) \\\n"
      . "\t\t\$(CODERV) \\\n"
      . "\t\t\$(CODERA_T0) \\\n"
      . "\t\t-o \$(TARGET)-$tgt\\\n"
      . "\t\t-w \$(VRATE_$tgt) \$(V_OPT) \\\n"
      . "\t\t\$(SCL_T0_${tgt}_CREATE) \\\n"
      . "\t\t$subcmd\n";
  }

  # cleanup rule
  print FH "\nclean-video-$tgt:\n"
    . "\t-rm -f $name $log\n";
}

# --- write_audio_rules($mak,$addon) ------------------------------------------
# write scale calc and audio render rules for all requested channels
# returns: -
sub write_audio_rules {
  my($mak) = shift;
  my($tgt) = shift; # target: SAMPLE, CD1, CD2, MAIN...

  my($flavor) = $$mak{'flavor'};

  # main audio rule
  print FH "\n# ----- audio rules for $tgt -----\n"
    . "\naudio-$tgt: " 
    . join(' ',map { "audio$_-$tgt" } (0.. $$mak{'aud_num'}-1)) . "\n";

  # cleanup rule
  print FH "\nclean-audio-$tgt: " . 
    join(' ',map { "clean-audio$_-$tgt" } (0.. $$mak{'aud_num'}-1)) . "\n";

  my($j);
  # channel loop
  for($j=0;$j<$$mak{'aud_num'};$j++) {
    my($scl)   = "\$(TARGET)-$tgt-a$j.scl";
    my($audio) = get_audio_name($mak,$tgt,$j);

    # main channel audio rule
    print FH "\naudio$j-$tgt: $audio\n";

    # first fetch the audio scale value
    print FH "\n$scl: \$(SRC_$tgt)\n"
      . "\t\@echo \"--- audio$j-scl-$tgt ---\"\n"
      . "\ttranscode \$(TSEL_$tgt) \$(INPUTA) \\\n"
      . "\t\t\$(CODERA_T$j) \$(A_OPT) \\\n"
      . "\t\t\$(SCL_T${j}_${tgt}_CREATE)\n";

    # render audio track
    print FH "\n$audio: \$(SCL_T${j}_$tgt) \$(SRC_$tgt)\n"
      . "\t\@echo \"--- audio$j-$tgt ---\"\n";

    # audio render commands
    # WORKAROUND for raw+avi container + mp2/mpa-no-extension workaround
    print FH "ifeq (\$(OACODEC),raw)\n"
      . "\ttranscode \$(TSEL_$tgt) \$(INPUTA) -y raw \\\n"
      . "\t\t\$(CODERA_T$j) \$(A_OPT) \\\n"
      . "\t\t\$(SCL_T${j}_${tgt}_USE) \\\n"
      . "\t\t-g 0x0 -o $audio\n"
      . "else\n"
      . "\ttranscode \$(TSEL_$tgt) \$(INPUTA) \$(OUTPUTA) \\\n"
      . "\t\t\$(CODERA_T$j) \$(A_OPT) \\\n"
      . "\t\t\$(SCL_T${j}_${tgt}_USE) \\\n"
      . "\t\t\-m `echo $audio | sed -e 's/\\.mp.\$\$//'`\n"
      . "endif\n";

    # cleanup rule
    print FH "\nclean-audio$j-$tgt:\n"
      . "\t-rm -f $scl $audio\n";
  }
}

# --- write_mux_rule($mak,$tgt) -----------------------------------------------
# multiplexing
# returns: -
sub write_mux_rule {
  my($mak) = shift;
  my($tgt) = shift;

  my($flavor)  = $$mak{'flavor'};
  my($aud_num) = $$mak{'aud_num'};
  my($name)    = get_mux_name($mak,"$tgt");
  my($video)   = get_video_name($mak,$tgt);
  my($audio)   = get_all_audio_names($mak,"$tgt");

  print FH "\n# ----- multiplexing rule of $tgt -----\n";
  # conserve space in slim mode
  if($$mak{'slim'}) {
    print FH "\nmux-$tgt: $name clean-video-$tgt clean-audio-$tgt\n"
      . "\n$name: video-$tgt audio-$tgt\n";
  } else {
    print FH "\nmux-$tgt: $name\n"
      . "\n$name: $video $audio\n";
  }
  print FH "\t\@echo \"--- mux-$tgt ---\"\n";

  # ----- avi
  if($flavor eq 'avi') {
    # mux all audio tracks by pairwise avimerge calls
    my($a);
    my($input) = $video;
    for($a=0;$a<$aud_num;$a++) {
      my($output);
      if($a == $aud_num-1) {
        $output = $name;
      } else {
        $output = "\$(TARGET)-$tgt-m" . $a . ".avi";
      }
      my($afile) = "\$(TARGET)-$tgt-a" . $a . ".avi";
      print FH "\tavimerge -i $input \\\n"
        . "\t\t -p $afile \\\n"
        . "\t\t -o $output\n";
      print FH "\trm $input\n" if($input ne $video);
      $input = $output;
    }
  }
  # ----- ogm
  elsif($flavor eq 'ogm') {
    print FH "\togmmerge -o $name \\\n"
      . "\t\t$video \\\n"
      . "\t\t$audio\n";
  }
  # ----- mkv
  elsif($flavor eq 'mkv') {
    print FH "\tmkvmerge -o $name \\\n"
      . "\t\t$video \\\n"
      . "\t\t$audio\n";
  }
  # ----- svcd,vcd,dvd
  else {
    if($$mak{'mpeg_mplex'} eq 'mplex') {
      # --- mplex support ---
      my($extra) = '';
      my($mode)  = 0;
      
      # dvd
      if($flavor eq 'dvd') {
        $mode = 8; # MPEG2 DVD
        $extra  = "-r \$(SRATE_$tgt)";
        $extra .= ' -V' if($$mak{'dvd_vbr'}); 
        $extra .= " -b $$mak{'dvd_buf'}" if($$mak{'dvd_buf'}>0);
      }
      # svcd
      elsif($flavor eq 'svcd') {
        if($$mak{'svcd_std'}==1) {
                $mode = 4; # standard SVCD
        } else {
          $mode = 5; # user mode SVCD
          $extra  = "-r \$(SRATE_$tgt)";
          $extra .= ' -V' if($$mak{'svcd_vbr'});
          $extra .= " -b $$mak{'svcd_buf'}" if($$mak{'svcd_buf'}>0);
        }
      } 
      # vcd
      else {
        if($$mak{'vcd_std'}==1) {
          $mode = 1; # standard VCD
        } else {
          $mode = 2; # user mode vcd
          $extra  = "-r \$(SRATE_$tgt)";
          $extra .= ' -V' if($$mak{'vcd_vbr'});
          $extra .= " -b $$mak{'vcd_buf'}" if($$mak{'vcd_buf'}>0);
        }
      }
      print FH "\tmplex -f $mode $extra\\\n"
        . "\t\t-o $name \\\n"
        . "\t\t$video\\\n"
        . "\t\t$audio\n";
    } else {
      # --- tcmplex support ---
      my(@auds) = split(/\s+/,$audio);
      my($mode);
      if($flavor eq 'svcd') {
        $mode = "-m s";
      } else {
        $mode = "-m 1";
      }
      my($aud) = '';
      $aud .= "-p $auds[0] " if(defined($auds[0]));
      $aud .= "-s $auds[1]" if(defined($auds[1]));
      print FH "\ttcmplex $mode\\\n"
                   . "\t\t-o $name\\\n"
             . "\t\t-i $video\\\n"
             . "\t\t$aud\n";
    }
  }

  # cleanup rule
  print FH "\nclean-mux-$tgt:\n"
    . "\t-rm -f $name\n";
}

# --- write_mastering_rule($mak,$tgt) -----------------------------------------
# build SVCD/VCD disc images
# returns: -
sub write_mastering_rule {
  my($mak) = shift;
  my($tgt) = shift;

  my($mux)    = get_mux_name($mak,$tgt);
  my($master) = get_master_name($mak,$tgt);
  my($flavor) = $$mak{'flavor'};

  # part numbers starting from 1!
  my($out) = "-c \$(TARGET)-$tgt.cue \\\n\t\t  -b \$(TARGET)-$tgt.bin";

  print FH "\n# ----- mastering rule of $tgt -----\n";
  # conserver space in slim mode
  if($$mak{'slim'}) {
    print FH "\nmaster-$tgt: $master clean-mux-$tgt\n"
      . "\n$master: mux-$tgt\n"
  } else {
    print FH "\nmaster-$tgt: $master\n"
      . "\n$master: $mux\n";
  }
  print FH "\t\@echo \"--- master-$tgt ---\"\n";

  # --- dvd ---
  my($dvddir);
  if($flavor eq 'dvd') {
    $dvddir = "\$(TARGET)-$tgt.imgdir";
    print FH "\tdvdauthor -o $dvddir $mux\n";
    print FH "\tdvdauthor -T -o $dvddir\n";
    print FH "\tmkisofs -dvd-video -o $master $dvddir\n";
  }
  # --- vcd+svcd --- 
  else {
    # XML is used for DVD input (created by chaplin)
    if($$mak{'xml'}) {
      print FH "\tvcdxbuild $out \\\n\t\t  \$(TARGET)-$tgt.xml\n";
    } else {
      my($opts) = "";
      if($$mak{'flavor'} eq 'svcd') {
        $opts .= "-t svcd --update-scan-offsets";
      } 
      print FH "\tvcdimager $opts $out \\\n\t\t$mux\n";
    }
  }
    
  # cleanup rule
  print FH "\nclean-master-$tgt:\n"
    . "\t-rm -f $master\n";
  if($dvddir ne '') {
    print FH "\trm -rf $dvddir\n";
  }
}

# --- write_menu_rule($mak) ---------------------------------------------------
# create chapter menus with a global call to chaplin-genmenu
# returns: -
sub write_menu_rule {
  my($mak) = shift;

  print FH "\n# ----- menu creation -----\n";
  print FH "\nmenu: \$(MENU_MPGS)\n"
    . "\n\$(MENU_MPGS): \$(TARGET)-menu.txt\n"
    . "\t\@echo \"--- chapter menu creation ---\"\n"
    . "\tchaplin-genmenu \$(GENMENU_OPTS) \$(TARGET)-menu.txt\n";
  print FH "\nclean-menu:\n"
    . "\trm -f \$(MENU_MPGS)\n";
}

#
# ===== subtitle stuff =======================================================
#

# --- emit_var_subs ----------------------------------------------------------
sub emit_var_subs {
  my($in)  = shift;
  my($out) = shift;
  my($opt) = shift;

  # ---- subtitle parameter ----
  my($sub_track) = $$opt{'sub_track'};
  my($sub_num)   = $$opt{'sub_num'};
  return if($sub_num == 0);
  
  print FH "\n# --- subtitle params ---\n";
  
  # ----- DVD subs -----
  if($$in{'vcodec'} eq 'dvd') {
    print FH "# dvd sub channels\n";
    my($num) = 0;
    foreach(@$sub_track) {
      print FH sprintf("SUBID_T%d = %u\n",$num,$_);
      print FH sprintf("SUBCHN_T%d = 0x%x\n",$num,$_ + 0x20);
      $num++;
    }
    
    print FH "# dvd chapter begin\n";
    my($chap_start) = $$in{'chap_start'};
    $num = 1;
    foreach(@$chap_start) {
      print FH sprintf("CHAP%d_START = %s\n",$num,$_);
      $num++;
    }
    
    # DVD burn in subs - currently only one track
    print FH "# burn-in subtitles\n";
    print FH "RENDER_SUBS=-J extsub=\"\$(SUBID_T0)\"\n";
  }
}

# --- write_sub_rule($mak,$tgt) ------------------------------------------
sub write_sub_rule {
  my($mak) = shift;
  my($tgt) = shift;
  
  my($sub_num) = $$mak{'sub_num'};
  my($sub_dvd) = $$mak{'sub_dvd'};
  return if($sub_num==0);
 
  # common rule for all subs: process each subtitle
  print FH "\n# ----- subtitle rules for $tgt -----\n";
  print FH "\nsub-$tgt: ";
  foreach(0 .. $sub_num-1) {
    print FH "sub$_-$tgt ";
  }
  print FH "\n\nclean-sub-$tgt: ";
  foreach(0 .. $sub_num-1) {
    print FH "clean-sub$_-$tgt ";
  }
  print FH "\n";
  
  # ----- gen DVD subs -----
  if($sub_dvd) {
    # now for each sub title
    foreach(0 .. $sub_num-1) {
      # we use a working directory for each subtitle
      my($dir) = "\$(TARGET)-$tgt.sub$_";
      my($ppml) = "$dir/sub.ppml";
      my($srtx) = "$dir/sub.srtx";

      # the subtitle generation rule      
      print FH "\n# --- subtitle #$_ for $tgt ---\n";
      print FH "\nsub$_-$tgt: #$ppml\n";

      print FH "\n$srtx: $dir\n"
        . "\ttccat -i \$(SRC_$tgt) -T \$(TITLE),\$(CHAP_$tgt),\$(ANGLE) | \\\n"
        . "\ttcextract -x ps1 -t vob -a \$(SUBCHN_T$_) | \\\n"
        . "\tsubtitle2pgm -c 255,0,128,128 -o $dir/sub\n"
        . "\tfor a in $dir/*.pgm ; do b=`echo \$\$a | sed -e 's/pgm/ppm/'`; convert \$\$a \$\$b ; done\n";
      print FH "\n$dir:\n\tmkdir $dir\n";
      print FH "\n$ppml: $srtx\n"
        . "\tsrtx2ppml -o \$(CHAP\$(CHAP_$tgt)_START) -f \$(OUT_FPS) < $srtx > $ppml\n";
    
      # clean up rule
      print FH "\nclean-sub$_-$tgt:\n"
        . "#\trm -rf $dir\n";
    }
  }   
}
