From 34aafc95b0d851b45421e50a3005bd0aac44d834 Mon Sep 17 00:00:00 2001 From: James Bunton Date: Mon, 10 Jun 2013 13:50:53 +1000 Subject: [PATCH] Added VHS encoding scripts --- hencode-recursive | 33 +++++ v4l-play | 25 ++++ v4l-rip | 33 +++++ video-transform | 325 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 416 insertions(+) create mode 100755 hencode-recursive create mode 100755 v4l-play create mode 100755 v4l-rip create mode 100755 video-transform diff --git a/hencode-recursive b/hencode-recursive new file mode 100755 index 0000000..61967d3 --- /dev/null +++ b/hencode-recursive @@ -0,0 +1,33 @@ +#!/bin/bash -e + +if [ -z "$1" -o -z "$2" ]; then + echo "Usage: $0 sourcedir destdir" + exit 1 +fi + +sourcedir="$(cd "$1" && pwd)" +destdir="$(cd "$2" && pwd)" + +cd "$sourcedir" +IFS=$(echo -en "\n") +for infile in $(find . -type f); do + if [ ! -r "$infile" ]; then + echo "Missing file $infile" + exit 1 + fi + outfile="${destdir}/$(echo "$infile" | sed 's/\.[a-zA-Z0-9]*$//').mp4" + if [ -e "$outfile" ]; then + echo "Skipping $infile" + continue + fi + mkdir -p "$(dirname "$outfile")" + HandBrakeCLI \ + --preset Universal \ + --quality 21 \ + --deinterlace \ + --loose-anamorphic \ + --crop 24:24:24:24 \ + --input "$infile" \ + --output "$outfile" +done + diff --git a/v4l-play b/v4l-play new file mode 100755 index 0000000..262055b --- /dev/null +++ b/v4l-play @@ -0,0 +1,25 @@ +#!/bin/bash + +mplayer \ +-nocache \ +-aspect 4:3 \ +tv:// \ +-tv \ +driver=v4l2:\ +width=720:height=576:\ +norm=pal:fps=25:\ +device=/dev/video0:input=0:\ +alsa:forceaudio:adevice=hw.0,0:immediatemode=0:\ +amode=0:forcechan=1:audiorate=48000 \ +\ +-vf \ +yadif \ + +###-vf \ +###yadif,\ +###boxblur=1:0,\ +###hue=0:2.5,\ +###denoise3d \ +###\ +###-af equalizer=-2:-2:-8:-8:3:3:3:3:0:-8 \ + diff --git a/v4l-rip b/v4l-rip new file mode 100755 index 0000000..18fe8fc --- /dev/null +++ b/v4l-rip @@ -0,0 +1,33 @@ +#!/bin/bash + +if [ -z "$1" ]; then + echo "Usage: $0 output.avi" + exit 1 +fi + +mencoder \ +tv:// \ +-tv \ +driver=v4l2:\ +width=720:height=576:\ +norm=pal:fps=25:\ +device=/dev/video0:input=0:\ +alsa:forceaudio:adevice=hw.0,0:immediatemode=0:\ +amode=0:forcechan=1:audiorate=48000 \ +\ +-force-avi-aspect 4/3 \ +\ +-vf \ +harddup \ +\ +-af \ +channels=1 \ +\ +-ovc lavc \ +-lavcopts vcodec=ffv1:ilme:ildct \ +-oac pcm \ +-o "$1" + + +# width=720:height=480: +# norm=pal-60:fps=30000/1001: diff --git a/video-transform b/video-transform new file mode 100755 index 0000000..53e5561 --- /dev/null +++ b/video-transform @@ -0,0 +1,325 @@ +#!/usr/bin/python + +from __future__ import division + +import os +import re +import shutil +import subprocess +import sys + +TMP_DIR = None +DEST_DIR = None +SOURCE_DIR = None +DRY_RUN = False # this will still delete caches + +VIDEO_FPS = 25 +AUDIO_SAMPLE_RATE = 48000 +ASPECT_RATIO = "4/3" + + +def mkarg(arg): + if re.match("^[a-zA-Z0-9\-\\.,/@_:=]*$", arg): + return arg + + if "'" not in arg: + return "'%s'" % arg + out = "\"" + for c in arg: + if c in "\\$\"`": + out += "\\" + out += c + out += "\"" + return out + +def fail(line_count, msg): + raise Exception(msg + " on line %d" % line_count) + +def convert_frame_to_sample(frame): + return frame * AUDIO_SAMPLE_RATE / VIDEO_FPS + +def run_cmd(cmd): + print "$", " ".join(map(mkarg, cmd)) + if DRY_RUN: + return + print + ret = subprocess.Popen(cmd).wait() + if ret != 0: + print >>sys.stderr, "Failed on command", cmd + raise Exception("Command returned non-zero: " + str(ret)) + +def read_file_contents(filename): + try: + f = open(filename) + data = f.read().strip() + f.close() + return data + except IOError: + return None + +def explode_video_to_png(source): + image_cache_desc = os.path.join(TMP_DIR, "image_cache.txt") + image_cache_dir = os.path.join(TMP_DIR, "image_cache") + + # Do nothing if the current cache is what we need + current_cache = read_file_contents(image_cache_desc) + if source == current_cache: + return image_cache_dir + + # Remove if necessary + if os.path.exists(image_cache_dir): +### print "Confirm removal of image cache:", current_cache +### ok = raw_input("(Y/n) ") +### if ok != "Y": +### print "Exiting..." +### sys.exit(2) + shutil.rmtree(image_cache_dir) + + cmd = [ + "mplayer", + "-vo", "png:outdir=%s" % image_cache_dir, + "-nosound", + "-noconsolecontrols", + "-noconfig", "user", + "-benchmark", + source, + ] + run_cmd(cmd) + + # Cache has been created, save the description + f = open(image_cache_desc, "w") + f.write(source) + f.close() + + return image_cache_dir + + +def explode_video_to_wav(source): + audio_cache_desc = os.path.join(TMP_DIR, "audio_cache.txt") + audio_cache_file = os.path.join(TMP_DIR, "audio_cache.wav") + + # Do nothing if the current cache is what we need + if source == read_file_contents(audio_cache_desc): + return audio_cache_file + + cmd = [ + "mencoder", + "-oac", "pcm", + "-ovc", "copy", + "-of", "rawaudio", + "-o", audio_cache_file + ".raw", + source, + ] + run_cmd(cmd) + + cmd = [ + "sox", + "-r", str(AUDIO_SAMPLE_RATE), "-b", "16", "-e", "signed-integer", + audio_cache_file + ".raw", + audio_cache_file, + ] + run_cmd(cmd) + + # Cache has been created, save the description + f = open(audio_cache_desc, "w") + f.write(source) + f.close() + + return audio_cache_file + +def apply_audio_effects(source, dest, crop_start, crop_end, audio_normalize): + cmd = [ + "sox", + source, + dest, + ] + if audio_normalize: + cmd += ["gain", "-n"] + if crop_start and crop_end: + c = convert_frame_to_sample + cmd += ["trim", "%ds" % c(crop_start), "%ds" % c(crop_end - crop_start)] + run_cmd(cmd) + +def apply_single_image_effects(source_file, dest_file, color_matrix): + cmd = [ + "convert", + source_file, + "-color-matrix", color_matrix, + dest_file, + ] + run_cmd(cmd) + +def apply_image_effects(source_dir, crop_start, crop_end, color_matrix): + dest_dir = os.path.join(TMP_DIR, "image_processed") + if os.path.exists(dest_dir): + shutil.rmtree(dest_dir) + os.mkdir(dest_dir) + + inframe = crop_start + outframe = 0 + while inframe <= crop_end: + source_file = os.path.join(source_dir, str(inframe+1).zfill(8) + ".png") + dest_file = os.path.join(dest_dir, str(outframe+1).zfill(8) + ".png") + if color_matrix: + apply_single_image_effects(source_file, dest_file, color_matrix) + else: + os.link(source_file, dest_file) + inframe += 1 + outframe += 1 + + return dest_dir + +def combine_audio_video(audio_file, image_dir, dest): + cmd = [ + "mencoder", + "mf://%s/*.png" % image_dir, + "-audiofile", audio_file, + "-force-avi-aspect", ASPECT_RATIO, + "-vf", "harddup", + "-af", "channels=1", + "-ovc", "lavc", + "-lavcopts", "vcodec=ffv1:ilme:ildct", + "-oac", "pcm", + "-o", dest, + ] + run_cmd(cmd) + + +class Job(object): + def __init__(self): + self.source = None + self.dest = None + self.crop_start = None + self.crop_end = None + self.color_matrix = None + self.audio_normalize = True + + def set_source(self, arg): + self.source = os.path.join(SOURCE_DIR, arg) + + def set_dest(self, arg): + self.dest = os.path.join(DEST_DIR, arg) + if not self.dest.endswith(".avi"): + self.dest += ".avi" + + def set_crop(self, arg): + a, b = arg.split("-") + self.crop_start = int(a) + self.crop_end = int(b) + + def set_colormatrix(self, arg): + [float(x) for x in arg.split(" ") if x] # check it's valid + self.color_matrix = arg + + def set_whitecolor(self, arg): + arg = arg.split(" ") + color = arg[0] + r = 0xff / int(color[0:2], 16) + g = 0xff / int(color[2:4], 16) + b = 0xff / int(color[4:6], 16) + # don't change the brightness + avg = (r + g + b) / 3 + if (avg - 1) > 0.02: + diff = avg - 1.0 + r -= diff + g -= diff + b -= diff + if len(arg) == 2: + brightness = float(arg[1]) + r *= brightness + g *= brightness + b *= brightness + self.set_colormatrix("%.3f 0 0 0 %.3f 0 0 0 %.3f" % (r, g, b)) + + def set_audionormalize(self, arg): + self.audio_normalize = int(arg) + + def validate(self, line_count, unique): + if self.dest in unique: + fail(line_count, "Non-unique output file: " + self.dest) + if self.source is None: + fail(line_count, "Missing source") + if self.dest is None: + fail(line_count, "Missing dest") + if not os.path.isfile(self.source): + fail(line_count, "Unable to find source: " + self.source) + + def is_done(self): + return os.path.isfile(self.dest) + + def run(self): + image_cache_dir = explode_video_to_png(self.source) + image_dir = apply_image_effects(image_cache_dir, self.crop_start, self.crop_end, self.color_matrix) + + audio_cache_file = explode_video_to_wav(self.source) + audio_file = os.path.join(TMP_DIR, "audio_processed.wav") + apply_audio_effects(audio_cache_file, audio_file, self.crop_start, self.crop_end, self.audio_normalize) + + combine_audio_video(audio_file, image_dir, self.dest+".tmp") + os.rename(self.dest+".tmp", self.dest) + + def __str__(self): + return "Job :: %s (%s)" % (self.dest, self.source) + +def main(frames): + jobs = [] + unique = set() + + f = open(frames) + + job = None + count = 0 + + def append_job(): + if job is None: + return + job.validate(count, unique) + if job.is_done(): + print "Skipping", job + else: + jobs.append(job) + + for line in f: + count += 1 + line = line.strip() + if line.startswith("#"): + continue + if not line: + if job is not None: + append_job() + job = None + continue + + if job is None: + job = Job() + cmd, arg = line.split(" ", 1) + f = getattr(job, "set_"+cmd, None) + if not f: + fail(count, "Invalid command: " + cmd) + try: + f(arg) + except Exception, e: + fail(count, str(e)) + + # trailing job... + append_job() + + # optimise image and audio cache usage, use the current cache first if it exists + current_image_cache = read_file_contents(os.path.join(TMP_DIR, "image_cache.txt")) + jobs.sort(key=lambda job: (job.source if job.source != current_image_cache else "", job.dest)) + for job in jobs: + print "\n\n\nStarted job:", job, "\n\n" + job.run() + +if __name__ == "__main__": + try: + frames = sys.argv[1] + SOURCE_DIR = sys.argv[2] + DEST_DIR = sys.argv[3] + TMP_DIR = sys.argv[4] + except IndexError: + print >>sys.stderr, "Usage: %s frames.txt source_dir dest_dir tmp_dir" % sys.argv[0] + sys.exit(1) + + main(frames) + -- 2.39.2