hav1w/hav1w.py

110 lines
3.5 KiB
Python

import argparse
import os
import pathlib
import shlex
import subprocess
import yaml
# Hannes' AV1 wrapper script
# Version 2.0.1
# This is a good guide for optimizing AV1 parameters with the AOM reference encoder:
# https://www.reddit.com/r/AV1/comments/t59j32/encoder_tuning_part_4_a_2nd_generation_guide_to/
parser = argparse.ArgumentParser(prog="hav1w")
parser.add_argument("input")
parser.add_argument("output")
parser.add_argument("preset")
parser.add_argument("--crf")
parser.add_argument("--ba")
parser.add_argument("--alayout")
parser.add_argument("--cpu")
parser.add_argument("--mf")
parser.add_argument("--dnl")
parser.add_argument("--arnr-strength")
parser.add_argument("--crop")
parser.add_argument("--no10bit", action="store_true")
parser.add_argument("--dry", action="store_true")
args = parser.parse_args()
preset_path = os.path.join(pathlib.Path(__file__).parent.resolve(), "av1_presets.yml")
with open(preset_path, "r") as stream:
presets = yaml.safe_load(stream)
preset = presets[args.preset]
if args.crf is not None:
preset["crf"] = args.crf
if args.ba is not None:
preset["ba"] = args.ba
if args.alayout is not None:
preset["alayout"] = args.alayout
if args.cpu is not None:
preset["cpu"] = args.cpu
if args.mf is not None:
preset["mf"] = args.mf
if args.dnl is not None:
preset["dnl"] = args.dnl
if args.arnr_strength is not None:
preset["arnr_strength"] = args.arnr_strength
cmd = ["ffmpeg", "-i", args.input, "-c:v", "libaom-av1", "-c:a", "libopus", "-c:s", "copy", "-map", "0"]
# AV1 Settings
# Constant rate factor, determines target quality
cmd += ["-crf", preset["crf"], "-b:v", "0"]
# Set minimum keyframe interval to 12 to prevent placing too many keyframes... just in case
cmd += ["-keyint_min", "12"]
# Set lookahead to 48 frames
# Reddit post says it's worth it...
cmd += ["-lag-in-frames", "48"]
# Temporal filtering parameters
# Set arnr_strength to 0 for animation and low variance stuff, 1 for content with more variance and fast motion scenes
cmd += ["-arnr-strength", preset["arnr_strength"], "-arnr-max-frames", "3"]
# Disable deltaq-mode for higher fidelity
aom_params = ["deltaq-mode=0"]
# Enable quantization matrices
# The Reddit post told me to do so, no matter what :P
aom_params += ["enable-qm=1"]
# Grain synthesis
# Set to -1 for disabling grain synthesis: makes sense for animation content (animes, ...) as
# a) this is not a strength of the aom encoder
# b) animation content usually does not have a lot of noise
#
# If you need to do grain synthesis on animation content, maybe SVT-AV1 is a better choice?
# And read this: https://www.reddit.com/r/AV1/comments/n4si96/encoder_tuning_part_3_av1_grain_synthesis_how_it/
# TEMPORARILY DISABLED DUE TO https://bugs.chromium.org/p/aomedia/issues/detail?id=2768
preset["dnl"] = "-1"
if preset["dnl"] != "-1":
aom_params += ["enable-dnl-denoising=0", "denoise-noise-level=" + preset["dnl"]]
cmd += ["-aom-params", ":".join(aom_params)]
if not args.no10bit: # Encode to 10 bit per default
cmd += ["-pix_fmt", "yuv420p10le"]
if args.crop is not None:
cmd += ["-vf", "crop=" + args.crop]
# Encoding efficiency settings (tiles also affect decoding performance)
cmd += ["-cpu-used", preset["cpu"], "-row-mt", "1", "-tiles", "2x2"]
# Audio/Opus settings
cmd += ["-b:a", preset["ba"], "-af", f"aformat=channel_layouts={preset['alayout']}", "-mapping_family", preset["mf"]]
cmd += [args.output]
if args.dry:
print(" ".join(list(map(lambda x:shlex.quote(x), cmd))))
exit(0)
p = subprocess.run(cmd)
exit(p.returncode)