You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

205 lines
6.1 KiB

#!/usr/bin/env perl
use strict;
use warnings;
use lib 'lib';
use utf8;
use Getopt::Long;
use Subtitle::Format::SSA;
use Subtitle::TimePoint;
use Subtitle::Utils ':timing';
my @points;
sub usage {
my ($msg) = @_;
print $msg, "\n" if $msg;
print <<USAGE;
Usage: ssa-retime <mode> [<options>] -i <file> [ -o <file>]
Modes are:
* framerate Recalculate timing for different fps
* shift Shift timing in given points
* drift Adjust timing of whole file by time points
Common options:
-h This help.
-i <file> Input file. (mandatory).
-o <file> Output file. Default: write to stdout.
-v Be verbose.
Specific options for 'framerate' mode:
-f <float> Source framerate. Default: 25.0 fps.
-F <float> Target framerate.
Specific options for 'shift' mode:
-m <mode> How to apply shift by points:
* seq -- apply timeshifts one by one to end of file (default)
* rst -- every next point overrides previous
-p <time>/<time> Point of fixup & time shift in it. Option can be
specified more than once. (see man for details)
Both args must be in form [+-][[h:]m:]s[.ms]
Specific options for 'drift' mode:
-p <time>/<time> Point of fixup & time shift in it. Option can be
specified more than once. (see man for details)
Both args must be in form [+-][[h:]m:]s[.ms]
USAGE
exit 1;
}
sub add_point {
my ($opt, $val) = @_;
my $p = Subtitle::TimePoint->new;
my $err = $p->parse($val);
die "$err\n" if $err;
push @points, $p;
@points = sort { $a->time <=> $b->time } @points;
}
########### init ###################
$| = 1;
unless (@ARGV and $ARGV[0] =~ m{^(framerate|shift|drift)$}oi) {
usage("You should set mode by first arg");
}
my $mode = shift @ARGV;
my %opts = ( loglevel => 0, inrate => 25.0, amode => 'seq');
GetOptions(
'h|help' => \&usage,
'v|verbose+' => \$opts{loglevel},
'i|infile=s' => \$opts{infile},
'o|outfile=s' => \$opts{outfile},
'm|amode=s' => \$opts{amode},
'f|inrate=f' => \$opts{inrate},
'F|outrate=f' => \$opts{outrate},
'p|point=s' => \&add_point,
);
if ($mode eq 'framerate') {
$opts{outrate}
or die "You should set '-F' option in this mode\n";
($opts{inrate} != 0.0 and $opts{outrate} != 0.0)
or die "Framerate can't be zero\n";
} elsif ($mode eq 'shift') {
scalar(@points)
or die "You should specify at least one timepoint\n";
($opts{amode} eq 'seq' or $opts{amode} eq 'rst')
or die "Value of -m option should be 'seq' or 'rst'\n";
} elsif ($mode eq 'drift') {
scalar(@points)
or die "You should specify at least one timepoint\n";
} else {
usage();
}
$opts{infile}
or die "No input file\n";
my $ssa = Subtitle::Format::SSA->new(debug => ($opts{loglevel} >= 2 ? 1 : 0));
unless ($ssa->from_file($opts{infile})) {
foreach my $line (@{ $ssa->{log} }) {
print $line, "\n";
}
exit 1;
}
if ($mode eq 'framerate') {
my $mod = $opts{inrate} / $opts{outrate};
foreach my $e (@{ $ssa->events }) {
$e->t_start($e->t_start * $mod);
$e->t_start($e->t_end * $mod);
}
} elsif ($mode eq 'shift') {
if ($opts{amode} eq 'seq') {
while (my $p = shift @points) {
foreach my $e (@{ $ssa->events }) {
next if $p->time > $e->t_start;
$e->t_start($e->t_start + $p->shift);
$e->t_end ($e->t_end + $p->shift);
} # foreach @events
} # while @points
} elsif ($opts{amode} eq 'rst') {
while (my $p = shift @points) {
# use time of next point as upper limit for current
my $end = @points ? $points[0]->time : 0;
foreach my $e (@{ $ssa->events }) {
next if $p->time > $e->t_start; # too early
next if $end and $end <= $e->t_start; # too late
$e->t_start($e->t_start + $p->shift);
$e->t_end ($e->t_end + $p->shift);
} # foreach @events
} # while @points
}
} elsif ($mode eq 'drift') {
if ($points[0]->time >= 0.2) {
# add starting point
my $p = Subtitle::TimePoint->new(time => 0.0, shift => 0.0);
unshift @points, $p;
}
# add endpoint, if needed
my $maxtime = 0.0;
foreach my $e (@{ $ssa->events }) {
next unless $e->t_end > $maxtime;
$maxtime = $e->t_end;
}
if ($maxtime > $points[-1]->time) {
my $p = Subtitle::TimePoint->new(time => $maxtime, shift => 0.0);
push @points, $p;
}
if ($opts{loglevel} >= 1) {
# dump points list
print "Final points list:\n";
foreach my $p (@points) {
printf "- %d:%02d:%02d.%03d ~ %.3f\n", make_timing($p->time), $p->shift;
}
print "-" x 30, "\n";
}
# video: 40s 47s 64s
# |-------*---*------*---------|
# ^ ^ ^-- b (-3.0s)
# | `--------- t ( ?.?s)
# `------------- a ( 0.5s)
# ----------------------------------------------------
# pct = (47.0 - 40.0) / (64.0 - 40.0) => 0.29 (29%)
# abs = (0.5 ~~ -3.0) => -3.5
# mod = -3.5 * 0.29 => -1.01
# t += 0.5 + -1.01 => -0.51s
# ----------------------------------------------------
# real retime
$ssa->sort_events;
foreach my $e (@{ $ssa->events }) {
while ($points[1]->time <= $e->t_start) {
if ($opts{loglevel} >= 2) {
printf "move timepoints: %d:%02d:%02d.%03d (endpoint) <= %d:%02d:%02d.%03d (event)\n",
make_timing($points[1]->time), make_timing($e->t_start);
}
shift @points;
}
my ($a, $b) = @points;
my $pct = ($e->t_start - $a->time) / ($b->time - $a->time);
my $abs = ($a->shift > $b->shift)
? ($a->shift - $b->shift)
: ($b->shift - $a->shift);
my $mod = ($a->shift > $b->shift)
? ($a->shift - $abs * $pct)
: ($a->shift + $abs * $pct);
$e->t_start($e->t_start + $mod);
$e->t_end ($e->t_end + $mod);
next unless $opts{loglevel} >= 2;
printf "- %d:%02d:%02d.%03d ~ %.3f\n", make_timing($e->t_start), $mod;
}
}
if ($opts{outfile}) {
print $ssa->to_file($opts{outfile});
} else {
print $ssa->to_string;
}
exit 0;