V4L capturing/script

From LinuxTVWiki
Jump to: navigation, search

This script is designed for use with the V4L capturing page. It should be enough for most use cases, but search this page for CMD_ if you'd rather take the commands and do it yourself.

Installation

  • Save the script: copy the block into a file in your home directory called v4l-capture.sh
  • Make the script executable: run chmod o+x ~/v4l-capture.sh on the command-line
  • Initialise the script: ~/v4l-capture.sh --init
  • Configure the script: open ~/.v4l-capturerc in an editor
  • Go through the V4L capturing instructions, setting values in the configuration file based on environment variables in the instructions
  • Measure your audio noise: close your configuration file, pause a tape or tune to a silent TV channel, then run ~/v4l-capture.sh --profile
  • See the changes: reopen ~/.v4l-capturerc and make sure there's a huge line full of numbers at the bottom

Ordinary usage

  • Get the duration: Fast-forward/rewind a tape and write down how long it is (e.g. a 6-hour tape would be written as 06:00:00)
  • Encode a file: Run ~/v4l-capture.sh --encode <name-of-output-directory> <how-long-it-is>
    1. If you don't specify a duration, you can finish encoding with ctrl+c, or with q and enter
  • Review the file: run ~/v4l-capture.sh --review <name-of-output-directory>
    1. Use ctrl, shift, alt and your mouse wheel to scroll through the video
    2. Backwards frame-seek won't always work before the video has finished recording
  • Configure the transcode script: open <name-of-output-directory>/<name-of-output-directory>.sh
    1. Try different settings to see what you like
    2. Set the start/end times from the numbers in the bottom-left of the review file
    3. Scrolling around in review mode will continually copy the review file's timestamps - paste them and edit the microsecond count to match the time in the bottom-left
    4. Transcode: run <name-of-output-directory>/<name-of-output-directory>.sh on the command-line

Note: timestamps differ slightly between the review file and the accurate copy. The review file has a fixed framerate so you can scroll around in it during playback, the accurate copy has a variable framerate so you can measure it during transcoding. Accurate timestamps are displayed in the review video itself to make transcoding easier.

The script

#!/bin/bash
#
# Encode/transcode a video - see http://linuxtv.org/wiki/index.php/V4L_capturing
#
# Approximate system requirements for the default settings:
# * about 10GB disk space for every hour of the initial recording
# * about 1-2GB disk space for every hour of transcoded recordings
# * dual 1.5GHz processor
#
# V4L Capture Script - encode and transcode video
# Written between 2015 and 2020 by Andrew Sayers <v4l-capture-script@pileofstuff.org>
# To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. This software is distributed without any warranty.
# You should have received a copy of the CC0 Public Domain Dedication along with this software. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.

HELP_MESSAGE="Usage: $0 --init
       $0 --profile
       $0 --encode <directory> [duration]
       $0 --review <directory>

Record a video into a directory (one directory per video).

    --init      create an initial ~/.v4l-capturerc
                please edit this file before your first recording

    --profile   update ~/.v4l-capturerc with your system's default noise profile
                pause a tape or tune to a silent channel for the best profile

    --encode    create a faithful recording in the specified directory
                Specify a duration to finish recording after that amount of time

    --review    play a special copy of the video, designed to help find segment boundaries
                (you can run this from a second window while encoding)

Once you've encoded a file, the script in the same directory will explain how to transcode.
"

CONFIGURATION='#
# CONFIGURATION FOR V4L CAPTURE SCRIPT
# See http://linuxtv.org/wiki/index.php/V4L_capturing
#

#
# VARIABLES YOU NEED TO EDIT
# See the wiki page for details about finding these values
#

# Set these based on your hardware/location:
TV_NORM="PAL" # (search Wikipedia for the exact norm in your country)

AUDIO_DEVICE="hw:CARD=SAA7134,DEV=0"

VIDEO_DEVICE="/dev/video0"
VIDEO_INPUT="1"
VIDEO_CONTROLS="brightness=128,contrast=64,saturation=64,hue=0"

# PAL video is approximately 720x576 resolution.  VHS tapes have about half the horizontal quality, but this post convinced me to encode at 720x576 anyway:
# http://forum.videohelp.com/threads/215570-Sensible-resolution-for-VHS-captures?p=1244415#post1244415
VIDEO_CAPABILITIES="video/x-raw, format=UYVY, framerate=25/1, width=720, height=576"
AUDIO_CAPABILITIES="audio/x-raw, rate=32000, channels=2"

# Encode settings:
ENCODE_VIDEO_FORMAT="libx264"
ENCODE_AUDIO_FORMAT="flac"
ENCODE_MUXER_FORMAT="matroska"
ENCODE_VIDEO_OPTIONS="-preset ultrafast -x264opts crf=18:keyint=50:min-keyint=5 -pix_fmt yuv422p" # change "18" to "0" for true lossless video
ENCODE_AUDIO_OPTIONS=""
ENCODE_MUXER_OPTIONS=""
ENCODE_EXTENSION="mkv"

# Transcode settings:
TRANSCODE_VIDEO_FORMAT="libx264"
TRANSCODE_AUDIO_FORMAT="libmp3lame"
TRANSCODE_MUXER_FORMAT="matroska"
TRANSCODE_VIDEO_OPTIONS="-flags +ilme+ildct -preset veryslow -crf 22 -pix_fmt yuv422p" # increase/decrease "22" for smaller files/better quality
TRANSCODE_AUDIO_OPTIONS="-b:a 256k" # for some reason, ffmpeg desyncs audio and video if "-q:a" is used instead of "-b:a"
TRANSCODE_MUXER_OPTIONS=""

#
# COMMON TRANSCODE SETTINGS
#

# For systems that do not automatically handle audio/video initialisation times:
DEFAULT_AUDIO_DELAY="0"

# Reducing noise:
DEFAULT_NOISE_REDUCTION="0.21"

# remove overscan:
DEFAULT_VIDEO_FILTERS="crop=(iw-10):(ih-14):3:0, pad=iw+10:ih+14:(ow-iw)/2:(oh-ih)/2"

# make all files are about as loud as each other (note: this filter uses sox instead of ffmpeg - see the man page for details):
DEFAULT_AUDIO_FILTERS="norm -1"

# Once you have set the above, record a silent source (e.g. a paused tape or silent TV channel)
# then call '"$0"' --profile to build a default noise profile
DEFAULT_NOISE_PROFILE=""
'

#
# CONFIGURATION SECTION
#

CONFIG_SCRIPT="$HOME/.v4l-capturerc"
[ -e "$CONFIG_SCRIPT" ] && source "$CONFIG_SCRIPT"

#
# OPTIONS FOR THE webm FILE:
# Detects several events that should help find segment boundaries
#

ENCODE_WEBM_VIDEO_OPTS="-crf 10 -b:v 512K -cpu-used 16 -g 20"
ENCODE_WEBM_AUDIO_OPTS=

# build the main image:
ENCODE_WEBM_FILTER_MAIN_IMAGE="
[0:v]
 crop=(iw-14):(ih-14):7:0,
 blackdetect=d=0.4,
 cropdetect=limit=14:round=16:reset=1,
 scale=352:(ih*352/iw),
 drawtext=text='%{pts\\:hms}': x=50: y=main_h-50: fontsize=24: fontcolor=white: borderw=3,
 pad=360:(ih+(ih*72/iw)+100):(ow-iw)/2,
 setpts=PREV_INPTS,
 setpts=PREV_INPTS [main_image]
"
# detect silence:
ENCODE_WEBM_FILTER_DETECT_SILENCE="[0:a] silencedetect=d=2:n=-25dB; [0:a] asplit=1 [wa]"

# build the waveform.  This visualisation doesn't let us align waveforms precisely to frames, so we cheat:
# pick a ridiculously high framerate, knowing all but the closest one to a video frame will be thrown away,
# then duplicate frame contents into nearby frames so whichever frames are picked contain about a video frame's worth of data
ENCODE_WEBM_FILTER_WAVEFORM_IMAGES=4 # reduce this number to save a little CPU during encoding
ENCODE_WEBM_FILTER_WAVEFORM="
[0:a] showwaves=s=$(( 72 / ENCODE_WEBM_FILTER_WAVEFORM_IMAGES ))x100:rate=$(( 25 * ENCODE_WEBM_FILTER_WAVEFORM_IMAGES )), split [waveform0][waveform1];
[waveform0] pad=72:ih [waveforms0];
$(
  for (( IMAGE=1; IMAGE < ENCODE_WEBM_FILTER_WAVEFORM_IMAGES-1; ++IMAGE ))
  do echo "[waveform$IMAGE] setpts=PREV_INPTS, split [waveform+$IMAGE][waveform$((IMAGE+1))]; [waveforms$((IMAGE-1))][waveform+$IMAGE] overlay=overlay_w*$IMAGE [waveforms$((IMAGE))];"
done )
[waveform$((ENCODE_WEBM_FILTER_WAVEFORM_IMAGES-1))]
 setpts=PREV_INPTS [waveform+$((ENCODE_WEBM_FILTER_WAVEFORM_IMAGES-1))];
 [waveforms$((ENCODE_WEBM_FILTER_WAVEFORM_IMAGES-2))][waveform+$((ENCODE_WEBM_FILTER_WAVEFORM_IMAGES-1))] overlay=overlay_w*$((ENCODE_WEBM_FILTER_WAVEFORM_IMAGES-1)) [waveform]
"

# build the small image:
ENCODE_WEBM_FILTER_SMALL_IMAGE="[0:v] scale=72:(ih*72/iw), pad=72:ih+100 [small_image]"

# combine image and waveform into film strip view:
ENCODE_WEBM_FILTER_FILM_STRIP="
[small_image][waveform] overlay=0:(main_h-overlay_h) [film_strip+0];
[film_strip+0]                    split [film_strip+1][to_combine+0]; [main_image][to_combine+0] overlay=72*0:(main_h-overlay_h) [combined+0];
[film_strip+1] setpts=PREV_INPTS, split [film_strip+2][to_combine+1]; [combined+0][to_combine+1] overlay=72*1:(main_h-overlay_h) [combined+1];
[film_strip+2] setpts=PREV_INPTS, split [film_strip+3][to_combine+2]; [combined+1][to_combine+2] overlay=72*2:(main_h-overlay_h) [combined+2];
[film_strip+3] setpts=PREV_INPTS, split [film_strip+4][to_combine+3]; [combined+2][to_combine+3] overlay=72*3:(main_h-overlay_h) [combined+3];
[film_strip+4] setpts=PREV_INPTS, split [film_strip+5][to_combine+4]; [combined+3][to_combine+4] overlay=72*4:(main_h-overlay_h) [combined+4];
[film_strip+5] setpts=PREV_INPTS                      [to_combine+5]; [combined+4][to_combine+5] overlay=72*5:(main_h-overlay_h) [wv]
"

ENCODE_WEBM_FILTER="
$ENCODE_WEBM_FILTER_MAIN_IMAGE;
$ENCODE_WEBM_FILTER_DETECT_SILENCE;
$ENCODE_WEBM_FILTER_WAVEFORM;
$ENCODE_WEBM_FILTER_SMALL_IMAGE;
$ENCODE_WEBM_FILTER_FILM_STRIP
"

#
# UTILITY FUNCTIONS
# You should only need to edit these if you're making significant changes to the way the script works
#

pluralise() {
    case "$1" in
        ""|0) return
            ;;
        1) echo "$1 $2, "
            ;;
        *) echo "$1 ${2}s, "
            ;;
    esac
}

timed_progress() {
    PADDING="$( echo -n "$MESSAGE" | tr -c '' ' ' )"
    CURRENT_TIME_MS="$2"
    TIME_REMAINING="$(( ( $(date +%s) - $START_TIME ) * ( $TOTAL_TIME_MS - $CURRENT_TIME_MS ) / $CURRENT_TIME_MS ))"
    HOURS_REMAINING=$(( $TIME_REMAINING / 3600 ))
    MINUTES_REMAINING=$(( ( $TIME_REMAINING - $HOURS_REMAINING*3600 ) / 60 ))
    SECONDS_REMAINING=$((   $TIME_REMAINING - $HOURS_REMAINING*3600 - $MINUTES_REMAINING*60 ))
    HOURS_REMAINING="$(   pluralise   $HOURS_REMAINING hour   )"
    MINUTES_REMAINING="$( pluralise $MINUTES_REMAINING minute )"
    SECONDS_REMAINING="$( pluralise $SECONDS_REMAINING second )"
    MESSAGE_REMAINING="$( echo "$HOURS_REMAINING$MINUTES_REMAINING$SECONDS_REMAINING" | sed -e 's/, $//' -e 's/\(.*\),/\1 and/' )"
    MESSAGE="$1 $(( 100 * CURRENT_TIME_MS / TOTAL_TIME_MS ))% ETA: $( date +%X -d "$TIME_REMAINING seconds" ) (about $MESSAGE_REMAINING)"
    MESSAGE="$( date +%c ) $MESSAGE" >&2
    echo -n $'\r'"$PADDING"$'\r'"$MESSAGE" >&2
}

ffmpeg_progress() {

    MESSAGE="$1..."

    echo -n $'\r'"$( date +%c ) $MESSAGE" >&2
    if [ -z "$TOTAL_TIME_MS" -o "$TOTAL_TIME_MS" = 0 ]
    then
        while IFS== read PARAMETER VALUE
        do
            if [ "$PARAMETER" = out_time ]
            then
                PADDING="$( echo -n "$MESSAGE" | tr -c '' ' ' )"
                MESSAGE="$( date +%c ) $1 ${VALUE/%???????/}" >&2
                echo -n $'\r'"$PADDING"$'\r'"$MESSAGE" >&2
            elif [ "$PARAMETER" = progress -a "$VALUE" = end ]
            then
                echo -n $'\r'"$( echo -n "$MESSAGE" | tr -c '' ' ' )"$'\r' >&2
                return
            fi
        done
    else
        while IFS== read PARAMETER VALUE
        do
            if [ "$PARAMETER" = out_time_ms ] && [ -n "$VALUE" -a "$VALUE" != 0 ]
            then
                timed_progress "$1" "$VALUE"
            elif [ "$PARAMETER" = progress -a "$VALUE" = end ]
            then
                echo -n $'\r'"$( echo -n "$MESSAGE" | tr -c '' ' ' )"$'\r' >&2
                return
            fi
        done
    fi

}

sox_progress() {

    MESSAGE="$1..."

    echo -n $'\r'"$( date +%c ) $MESSAGE" >&2
    read -d $'\r'
    while read -d $'\r' PERCENT TIME EXTRA
    do
        if [ "$TIME" = "00:00:00.00" ]
        then
            echo -n $'\r'"$( echo -n "$MESSAGE" | tr -c '' ' ' )"$'\r' >&2
        elif [ -n "$TIME" ]
        then
            timed_progress "$1" "$( parse_time "$TIME" )000"
        fi
    done
}

# convert 00:00:00.000 to a count in milliseconds
parse_time() {
    echo $((
        $(date -d "1970-01-01T${1}Z" +%s )*1000
        + $(
            echo "$1" | sed \
                -e 's/.*\.\([0-9]\)$/\100/' \
                -e 's/.*\.\([0-9][0-9]\)$/\10/' \
                -e 's/.*\.\([0-9][0-9][0-9]\)$/\1/' \
                -e '/^[0-9][0-9][0-9]$/! s/.*/0/' \
                -e 's/^0*\([0-9]\)/\1/'
        )
    ))
}

ms_to_s() {
    case "$1" in
        .  ) echo ".00$1" ;;
        .. ) echo ".0$1" ;;
        ...) echo ".$1" ;;
        *  ) echo "$1" | sed -e 's/\(.*\)\(...\)$/\1.\2/'
    esac
}

# get the full name of the script's directory
set_directory() {
    if [ -z "$1" ]
    then
        echo "$HELP_MESSAGE"
        exit 1
    else
        DIRECTORY="$( readlink -f "$1" )"
        FILE="$DIRECTORY/$( basename "$DIRECTORY" )"
    fi
}

# actual commands that do something interesting:
CMD_GST="gst-launch-1.0"
CMD_FFMPEG="nice -n +20 ffmpeg -loglevel 23"
CMD_FFPLAY="ffplay"
CMD_SOX="nice -n +20 sox"
CMD_MPV="mpv"

#
# MAIN LOOP
#

case "$1" in

    -i|--i|--in|--ini|--init)
        if [ -e "$CONFIG_SCRIPT" ]
        then
            echo "Please delete $CONFIG_SCRIPT if you want to recreate it"
        else
            echo "$CONFIGURATION" > "$CONFIG_SCRIPT"
            echo "Now edit $( tput setaf 4 )$CONFIG_SCRIPT$( tput sgr0 ) to match your system"
        fi
    ;;

    -p|--p|--pr|--pro|--prof|--profi|--profil|--profile)
        sed -i "$CONFIG_SCRIPT" -e '/^DEFAULT_NOISE_PROFILE=.*/d'
        echo "DEFAULT_NOISE_PROFILE='$( "$CMD_GST" -q alsasrc device="$AUDIO_DEVICE" ! wavenc ! fdsink | $CMD_SOX -t wav - -n trim 0 1 noiseprof | tr '\n' '\t' )'" >> "$CONFIG_SCRIPT"
        echo "Updated $CONFIG_SCRIPT with default noise profile"
    ;;

    -e|--e|--en|--enc|--enco|--encod|--encode)

        # Build a pipeline with sources being encoded as MPEG4 video and FLAC audio, then being muxed into a Matroska container.
        # FLAC and Matroska are used during encoding to ensure we don't lose much data between passes

        set_directory "$2"
        mkdir -p -- "$DIRECTORY" || exit

        if [ -z "$3" ]
        then
            DURATION_OPTIONS=
            TOTAL_TIME_MS=
        else
            DURATION_OPTIONS="-t $3"
            TOTAL_TIME_MS="$( parse_time "$3" )000"
        fi

        if [ -e "$FILE.$ENCODE_EXTENSION" ]
        then
            echo "Please delete the old $FILE.$ENCODE_EXTENSION before making a new recording"
            exit 1
        fi

        cat <<EOF > "$FILE.sh"
#!$0 --transcode
#
# The original $( basename "$FILE" ).$ENCODE_EXTENSION accurately represents the source.
# If you would like to get rid of imperfections in the source (e.g.
# splitting it into segments), edit then run this file.

#
# ORIGINAL FILE
#
# This is the original file:
original "$( basename "$FILE" ).$ENCODE_EXTENSION"


#
# AUDIO DELAY
#
# If you need to manually synchronise audio and video,
# set this to the duration in seconds (can be fractional):
#
audio_delay ${AUDIO_DELAY:-0.0}


#
# SYSTEM NOISE REDUCTION
#
# Your system will add a small amount of noise to any recording.
# You can undo this by specifying the amount of noise reduction
# (the default value is usually fine), and can optionally specify
# a time in the video to use for the noise profile.

# Uncomment the following line if you have configured a default
# noise profile with \`--profile\`:
#reduce_noise $DEFAULT_NOISE_REDUCTION

# If this video starts with silence, you can use that instead of
# the default noise profile.
#reduce_noise $DEFAULT_NOISE_REDUCTION 00:00:00-00:00:01

# Uncomment the following line to disable noise reduction:
#no_reduce_noise


#
# LOCAL SETTINGS
#
# You might want to override some settings, for example to filter
# videos based on the quality of this specific source.
#

TRANSCODE_VIDEO_FILTERS="$DEFAULT_VIDEO_FILTERS"

# See https://ffmpeg.org/ffmpeg-filters.html for information,
# Here are some examples - remove the leading '#' to make one work:

# lightly denoise a video - useful for good-quality VHS tapes:
#TRANSCODE_VIDEO_FILTERS="il=d:d:d, pp=tn, hqdn3d, il=i:i:i, $DEFAULT_VIDEO_FILTERS"
# significantly denoise interlaced video - useful for poor-quality VHS tapes:
#TRANSCODE_VIDEO_FILTERS="il=d:d:d, pp=tn, hqdn3d=luma_spatial=6:2:luma_tmp=20, il=i:i:i, $DEFAULT_VIDEO_FILTERS"
# significantly reduce "snow" (video noise) - not yet released as of ffmpeg 2.7:
#TRANSCODE_VIDEO_FILTERS="il=d:d:d, removegrain=9:0:0:0, pp=tn, il=i:i:i, $DEFAULT_VIDEO_FILTERS"

# Halve the picture size after applying all other video filters:
#TRANSCODE_VIDEO_FILTERS="\$TRANSCODE_VIDEO_FILTERS,scale=in_w/2:in_h/2"

# VHS LP recordings produce tape hiss above about 4KHz.
# This adds an audio filter to silence all noise above that range with minimal effect below it:
#TRANSCODE_AUDIO_FREQUENCY_RANGE="0 4000"
# add any other filters you like:
TRANSCODE_AUDIO_FILTERS="$DEFAULT_AUDIO_FILTERS"


#
# SEGMENTS
#
# You can split a video into one or more files.  To create a segment,
# add a line like this:
#
# segment "name of output file.avi" start-end start-end ...
#
# each "start-end" block specifies part of the file used in the segment.
# You can use this to e.g. split adverts out of a TV program
#

# Here are some examples - remove the leading '#' to make one work:

# create a file from the whole file:
#segment "videos/$( basename "$FILE" ).mkv"

# remove ad breaks from a half-hour program:
#segment "videos/$( basename "$FILE" ).mkv" 00:00:30-00:15:00 00:19:00-00:28:00

# split into two parts just over and hour:
#segment "videos/$( basename "$FILE" ) part 1.mkv" 00:00:00-01:00:05
#segment "videos/$( basename "$FILE" ) part 2.mkv" 00:59:55-01:00:05

# Advanced examples:
#
# show what a segment would look like with your video filters:
#play_segment "videos/$( basename "$FILE" ).mkv" 00:01:00-00:02:00
# build the audio and metadata files for a segment, but not the video:
#prepare_segment "videos/$( basename "$FILE" ) part 1.mkv" 00:01:00-00:02:00


# Create a playlist with all the segments above
# (adding multiple playlists will create 
playlist "$( basename "$FILE" ).m3u"
EOF
        chmod 755 "$FILE.sh"

        [ -n "$VIDEO_INPUT"    ] && v4l2-ctl --device="$VIDEO_DEVICE" --set-input "$VIDEO_INPUT" > >( grep -v '^Video input set to' )
        [ -n "$VIDEO_CONTROLS" ] && v4l2-ctl --device="$VIDEO_DEVICE" --set-ctrl "$VIDEO_CONTROLS"

        SUBTITLE_ID=1
        format_subtitle() {
            START="$( date -u +"%H:%M:%S,${1/*./}" -d @$1 )"
            END="$(   date -u +"%H:%M:%S,${2/*./}" -d @$2 )"
            MESSAGE="$3"
            echo "$((SUBTITLE_ID++))"
            echo "$START --> $END"
            echo "$MESSAGE"
            echo
        }

        process_ffmpeg_output() {
            sed -un \
                -e 's/.*Parsed_cropdetect.*t:0*\([0-9.]*\) crop=[-0-9]*:\([0-9]*\).*/crop \1 \2/p' \
                -e 's/.*silencedetect .* silence_start: \(.*\)$/silence_start \1/p' \
                -e 's/.*silencedetect .* silence_end: \([0-9.]*\).*/silence_end \1/p' \
                -e 's/.*blackdetect .* black_start:\([0-9.]*\) black_end:\([0-9.]*\).*/black \1 \2/p' \
                | {
                    format_subtitle 0 10 "Subtitles indicate possible ad breaks."
                    declare -A CROPS=()
                    CROP_START_TIME=0
                    CROP_END_TIME=0
                    CROP_COUNT=0
                    OLD_CROP=
                    while read MESSAGE_TYPE ARG1 ARG2
                    do
                        case "$MESSAGE_TYPE" in
                            silence_start)
                                if [ "${ARG1:0:1}" = "-" ]
                                then SILENCE_START="0"
                                else SILENCE_START="$ARG1"
                                fi
                                ;;
                            silence_end)
                                format_subtitle "$SILENCE_START" "$ARG1" "silence"
                                ;;
                            black)
                                format_subtitle "$ARG1" "$ARG2" "darkness"
                                ;;
                            crop) # need some more complex logic to build a useful message
                                if [ $(( ++CROP_COUNT )) -gt 250 ]
                                then
                                    MOST_FREQUENT=
                                    MOST_FREQUENCY=0
                                    for CROP in "${!CROPS[@]}"
                                    do
                                        if [ "${CROPS[$CROP]}" -gt "$MOST_FREQUENCY" ]
                                        then
                                            MOST_FREQUENT="${CROP:1}"
                                            MOST_FREQUENCY="${CROPS[$CROP]}"
                                        fi
                                    done
                                    if [ "$OLD_CROP" != "$MOST_FREQUENT" ]
                                    then
                                        if [ -n "$OLD_CROP" ]
                                        then
                                            CROP_MESSAGE="$( echo "$OLD_CROP" | sed -e 's/:/x/' -e 's/:/+/g' )"
                                            format_subtitle "$CROP_START_TIME" "$CROP_END_TIME" "image is $CROP_MESSAGE pixels high"
                                        fi
                                        CROP_START_TIME="$CROP_END_TIME"
                                    fi
                                    CROP_END_TIME="$ARG1"
                                    declare -A CROPS=()
                                    CROP_COUNT=0
                                    OLD_CROP="$MOST_FREQUENT"
                                fi
                                CROPS["x$ARG2"]="$(( CROPS["x$ARG2"] + 1 ))"
                                ;;
                        esac
                    done
                } > "$FILE.txt"
        }

        date +"%c $( tput setaf 4 )$FILE$( tput sgr0 ) started"
        START_TIME="$( date +%s )"
        # Encoding command:
        $CMD_FFMPEG \
            \
            -loglevel 32 \
            \
            $DURATION_OPTIONS \
            \
            -i <(
                $CMD_GST -q \
                    v4l2src device="$VIDEO_DEVICE" do-timestamp=true norm="$TV_NORM" pixel-aspect-ratio=1 \
                        ! $VIDEO_CAPABILITIES \
                        ! queue max-size-buffers=0 max-size-time=0 max-size-bytes=0 \
                        ! mux. \
                    alsasrc device="$AUDIO_DEVICE" do-timestamp=true \
                        ! $AUDIO_CAPABILITIES \
                        ! queue max-size-buffers=0 max-size-time=0 max-size-bytes=0 \
                        ! mux. \
                    matroskamux name=mux \
                        ! queue max-size-buffers=0 max-size-time=0 max-size-bytes=0 \
                        ! fdsink fd=1
            ) \
            \
            -filter_complex "$ENCODE_WEBM_FILTER" \
            \
            -c:v "$ENCODE_VIDEO_FORMAT" $ENCODE_VIDEO_OPTIONS \
            -c:a "$ENCODE_AUDIO_FORMAT" $ENCODE_AUDIO_OPTIONS \
            -f   "$ENCODE_MUXER_FORMAT" $ENCODE_MUXER_OPTIONS \
            "file:$FILE.$ENCODE_EXTENSION" \
            \
            -map [wv] -c:v libvpx    $ENCODE_WEBM_VIDEO_OPTS \
            -map [wa] -c:a libvorbis $ENCODE_WEBM_AUDIO_OPTS \
            -f webm \
            "file:$FILE.webm" \
            \
            -progress >( ffmpeg_progress "$( tput setaf 4 )$FILE$( tput sgr0 )" ) \
            \
            2> >( process_ffmpeg_output )

        echo >&2
        date +"%c $FILE.$ENCODE_EXTENSION finished"

        echo "Now see $( tput setaf 4 )$FILE.sh$( tput sgr0 )"
    ;;

    -r|--r|--re|--rev|--revi|--revie|--review)
        set_directory "$2"
        [ -d "$DIRECTORY/mpv-config" ] || mkdir "$DIRECTORY/mpv-config"

        cat > "$DIRECTORY/mpv-config/input.conf" <<END
Shift+MOUSE_BTN3 frame_step
Shift+MOUSE_BTN4 frame_back_step

Ctrl+Shift+MOUSE_BTN3 seek  0.5
Ctrl+Shift+MOUSE_BTN4 seek -0.5

MOUSE_BTN3 seek  5
MOUSE_BTN4 seek -5

Ctrl+MOUSE_BTN3 seek  200 - exact
Ctrl+MOUSE_BTN4 seek -200 - exact

Alt+MOUSE_BTN3 sub_seek  1
Alt+MOUSE_BTN4 sub_seek -1

END

        PREV_TIME=
        # Review command:
        $CMD_MPV --keep-open --ontop --pause --config-dir="$DIRECTORY/mpv-config" --term-status-msg 'TIME ${=time-pos}'$'\n' "$FILE.webm" 2>&1 | \
            while read TITLE TIME
            do
                if [ "$TITLE" = "TIME" -a "$TIME" != "$PREV_TIME" ]
                then
                    PREV_TIME="$TIME"
                    TIME="$( date --utc +"%H:%M:%S.${TIME/*./}" -d "@$TIME" )"
                    TIME="${TIME%000}"
                    echo -n "$TIME" | xclip
                fi
            done

        rm -rf "$DIRECTORY/mpv-config"
        ;;

    --transcode)

        # we use ffmpeg and sox here, as they have better tools

        HAVE_CREATED_SEGMENTS=
        PLAYLIST=$'#EXTM3U\n'

        HAVE_NOISE_PROFILE=true

        AUDIO_BITRATE="$( echo "$AUDIO_CAPABILITIES" | sed -ne 's/.*rate=\([0-9]*\).*/\1/p' )"
        if [ -z "$AUDIO_BITRATE" ]
        then
            echo "Please specify a rate in AUDIO_CAPABILITIES"
            exit
        fi

        original() {
            ORIGINAL="$1"
            ORIGINAL_METADATA="$(
                $CMD_FFMPEG -i "file:$ORIGINAL" -f ffmetadata - < /dev/null
                date -u +"DATE_DIGITIZED=%Y-%m-%d %H:%M:%S.000" -d @$( stat -c %Y "$ORIGINAL" )
            )"
        }

        no_reduce_noise() {
            NOISE_PROFILE=
            HAVE_NOISE_PROFILE=true
        }

        reduce_noise() {
            NOISE_REDUCTION="$1"
            NOISE_TIME="$2"
            if [ -z "$NOISE_REDUCTION" ]
            then
                echo "Please specify a noise reduction amount"
                exit 1
            fi
            if [ -z "$NOISE_TIME" ]
            then
                if [ -z "$DEFAULT_NOISE_PROFILE" ]
                then
                    cat <<END
Please specify a default noise profile.

Pause a tape (so only background noise is playing) then run:

	$0 --profile

This will save information about the background noise.

END
                    exit 1
                else
                    NOISE_PROFILE="$( echo "$DEFAULT_NOISE_PROFILE" | tr '\t' '\n' )"
                fi
            else
                NOISE_PROFILE="$( $CMD_FFMPEG -ss "${NOISE_TIME/-*/}" -i "file:$ORIGINAL" -t "${NOISE_TIME/*-/}" -vn -c:a pcm_s16le -f wav - | $CMD_SOX -t wav - -n noiseprof 2>/dev/null )"
            fi
            HAVE_NOISE_PROFILE=true
        }

        audio_delay() {
            if [[ "$1" =~ ^[0.]*$ ]]
            then AUDIO_DELAY=
            else AUDIO_DELAY="$1"
            fi
        }

        calculate_segment() {
            # Calculate segment options:
            if [ -z "$1" ]
            then
                SEGMENT_START_OPTS=
                SEGMENT_END_OPTS=
                CHAPTER_METADATA=
                VIDEO_FILTER_SPLIT=
                VIDEO_FILTER_LIST='[0:v]'
                AUDIO_FILTER_SPLIT=
                AUDIO_FILTER_LIST='[0:a]'
                CHAPTER_NUMBER=1
            else
                SEGMENT_START="${1/-*/}"
                CHAPTER_END="${1/*-/}"
                SEGMENT_START_MS="$( parse_time "$SEGMENT_START" )"
                SEGMENT_START_OPTS="-ss $SEGMENT_START"
                shift

                CHAPTER_END_MS="$(( $( parse_time "$CHAPTER_END" ) - SEGMENT_START_MS ))";
                TOTAL_TIME_MS="$CHAPTER_END_MS"
                CHAPTER_END_S="$( ms_to_s "$CHAPTER_END_MS" )";
                VIDEO_FILTER_SPLIT="[0:v] trim=end=$CHAPTER_END_S [v0]"
                AUDIO_FILTER_SPLIT="[0:a] atrim=end=$CHAPTER_END_S [a0]"
                VIDEO_FILTER_LIST="[v0] ";
                AUDIO_FILTER_LIST="[a0] ";
                CHAPTER_NUMBER=1
                if [ -z "$*" ]
                then CHAPTER_TITLE="$SEGMENT_TITLE"
                else CHAPTER_TITLE="Chapter $CHAPTER_NUMBER"
                fi
                CHAPTER_METADATA="[CHAPTER]
TIMEBASE=1/1000000000
START=0
END=${TOTAL_TIME_MS}000000
title=$CHAPTER_TITLE
"
                for CHAPTER in "$@"
                do
                    CHAPTER_START_MS="$(( $( parse_time "${CHAPTER/-*/}" ) - SEGMENT_START_MS ))";
                    CHAPTER_START_S="$( ms_to_s "$CHAPTER_START_MS" )";
                    CHAPTER_END_MS="$((   $( parse_time "${CHAPTER/*-/}" ) - SEGMENT_START_MS ))";
                    CHAPTER_END_S="$(   ms_to_s "$CHAPTER_END_MS" )";
                    VIDEO_FILTER_SPLIT="$VIDEO_FILTER_SPLIT ;
[0:v] trim=start=$CHAPTER_START_S:end=$CHAPTER_END_S,    setpts=PTS-STARTPTS [v$CHAPTER_NUMBER]"
                    AUDIO_FILTER_SPLIT="$AUDIO_FILTER_SPLIT ;
[0:a] atrim=start=$CHAPTER_START_S:end=$CHAPTER_END_S,    asetpts=PTS-STARTPTS [a$CHAPTER_NUMBER]"
                    VIDEO_FILTER_LIST="$VIDEO_FILTER_LIST[v$CHAPTER_NUMBER] ";
                    AUDIO_FILTER_LIST="$AUDIO_FILTER_LIST[a$CHAPTER_NUMBER] ";
                    CHAPTER_NUMBER="$(( CHAPTER_NUMBER + 1 ))"
                    CHAPTER_METADATA="${CHAPTER_METADATA}[CHAPTER]
TIMEBASE=1/1000000000
START=${TOTAL_TIME_MS}000000
END=$(( TOTAL_TIME_MS + CHAPTER_END_MS - CHAPTER_START_MS ))000000
title=Chapter $CHAPTER_NUMBER
"
                    TOTAL_TIME_MS="$(( TOTAL_TIME_MS + CHAPTER_END_MS - CHAPTER_START_MS ))"
                done

                TOTAL_TIME_MS="${TOTAL_TIME_MS}000" # microseconds -> milliseconds
            fi

            if [ -z "$TRANSCODE_VIDEO_FILTERS" ]
            then VIDEO_FILTER="$VIDEO_FILTER_SPLIT ;
${VIDEO_FILTER_LIST}concat=n=$CHAPTER_NUMBER [video]"
            else VIDEO_FILTER="$VIDEO_FILTER_SPLIT ;
${VIDEO_FILTER_LIST}concat=n=$CHAPTER_NUMBER, $TRANSCODE_VIDEO_FILTERS [video]"
            fi
            AUDIO_FILTER="$AUDIO_FILTER_SPLIT ;
${AUDIO_FILTER_LIST}concat=n=$CHAPTER_NUMBER:v=0:a=1 [audio]"
        }

        play_segment() {
            shift
            calculate_segment "$@"
            cat >&2 <<END
Suggested video filters:
END
            echo -n 'measuring...' >&2
            # Segment playback command:
            $CMD_FFPLAY $SEGMENT_START_OPTS -i "$ORIGINAL" -vf "$TRANSCODE_VIDEO_FILTERS, cropdetect=24:2:1500" 2> >(
                sed -une 's/.*t:0*\([0-9]*\)\.\([0-9]*\) \(crop=[0-9:]*\)$/\1\2 \3/p' | {
                    read START_TIME OLD_CROP
                    MESSAGE=
                    while read TIME CROP
                    do
                        DURATION="$(( TIME - START_TIME ))"
                        if [ "$CROP" != "$OLD_CROP" ]
                        then
                            [[ $DURATION -gt 2000000 ]] && echo -n $'\nmeasuring...' >&2
                            OLD_CROP="$CROP"
                            START_TIME="$TIME"
                            MESSAGE=
                            DURATION=0
                        fi
                        if [[ $DURATION -gt 2000000 ]]
                        then
                            echo -n $'\r'"$( echo -n "$MESSAGE" | tr -c '' ' ' )"$'\r' >&2
                            MESSAGE="TRANSCODE_VIDEO_FILTERS=\"$CROP, pad=$SOURCE_WIDTH:$SOURCE_HEIGHT:(ow-iw)/2:(oh-ih)/2\" # (measured good for ${DURATION/%??????/} second(s))"
                            echo -n "$MESSAGE" >&2
                        fi
                    done
                }
            )
            HAVE_CREATED_SEGMENTS=true
        }

        # build the components of a segment, but not the segment itself:
        prepare_segment() {
            PREPARE_SEGMENT=true
            segment "$@"
        }

        # build a segment:
        segment() {
            SEGMENT_FILENAME="$1" ; shift
            SEGMENT_TITLE="$( basename "${SEGMENT_FILENAME/\.*/}" )"
            HAVE_CREATED_SEGMENTS=true

            AUDIO_FILE="temp/$SEGMENT_TITLE.wav"
            METADATA_FILE="temp/$SEGMENT_TITLE.txt"

            if [ -z "$HAVE_NOISE_PROFILE" ]
            then
                echo "Please add a noise profile (for details, search your script for 'noise reduction')"
                exit 1
            fi

            CURRENT_STAGE=1
            STAGE_COUNT=2
            [ -e "$AUDIO_FILE" ] || STAGE_COUNT="$(( STAGE_COUNT + 1 ))"
            case "$SEGMENT_FILENAME" in
                *.mkv)
                    STAGE_COUNT=$(( STAGE_COUNT - 1 ))
                    ;;
                *.*)
                    ;;
                *)
                    echo "$( tput setaf 1 )Error:$( tput sgr0 ) please add a file extension for segment $( tput setaf 4 )$SEGMENT_FILENAME$( tput sgr0 )"
                    exit 1
            esac

            calculate_segment "$@"

            [ "$STAGE_COUNT" -eq 1 ] || date +"%c $( tput setaf 4 )$SEGMENT_FILENAME$( tput sgr0 ) started ($STAGE_COUNT stages)"

            case "$SEGMENT_FILENAME" in
                "$ORIGINAL")
                    echo "Can't create segment '$SEGMENT_FILENAME' - would have the same name as the original file"
                    return 1
                    ;;
                *.mkv)
                    # allow variable frame rate
                    FRAMERATE_OPTS=
                    ;;
                *)
                    START_TIME="$( date +%s )"
                    while IFS== read PARAMETER VALUE
                    do
                        if   [ "$PARAMETER" = frame ]
                        then FRAME=$VALUE
                        else
                            [ "$PARAMETER" = out_time_ms ] && OUT_TIME_MS="$VALUE"
                            echo $PARAMETER=$VALUE
                        fi
                        TOTAL_TIME_MS=$OUT_TIME_MS
                        FRAMERATE_OPTS="-r ${FRAME}000000/$OUT_TIME_MS"
                    done < <( $CMD_FFMPEG $SEGMENT_START_OPTS -i "file:$ORIGINAL" -filter_complex "$VIDEO_FILTER" -map '[video]' -vcodec rawvideo -an -f null /dev/null -progress /dev/stdout < /dev/null ) \
                         > >( ffmpeg_progress "$SEGMENT_FILENAME calculating framerate ($CURRENT_STAGE/$STAGE_COUNT)" )
                    CURRENT_STAGE=$(( CURRENT_STAGE + 1 ))
                    ;;
            esac

            mkdir -p "$( dirname "$SEGMENT_FILENAME" )" temp

            # Build metadata for segment:
            [ -e "$METADATA_FILE" ] || echo -n "$ORIGINAL_METADATA"$'\n'"; To add chapter titles etc., edit the values below then re-run."$'\n'"title=$SEGMENT_TITLE"$'\n'"$CHAPTER_METADATA" > "$METADATA_FILE"

            # Build audio file for segment:
            MESSAGE=
            if [ -e "$AUDIO_FILE" ]
            then
                AUDIO_TIME_MS="$( $CMD_SOX --info -D "$AUDIO_FILE" | sed -e 's/\.//' )"
                if [[ $AUDIO_TIME_MS -gt $TOTAL_TIME_MS ]]
                then
                    AUDIO_OFFSET_MS="$(( AUDIO_TIME_MS - TOTAL_TIME_MS ))"
                    AUDIO_LENGTH_WORD="long"
                    AUDIO_ERROR_SUGGESTION="if you really want your audio to continue past the end of your video, add some periods of black video"
                else
                    AUDIO_OFFSET_MS="$(( TOTAL_TIME_MS - AUDIO_TIME_MS ))"
                    AUDIO_LENGTH_WORD="short"
                    AUDIO_ERROR_SUGGESTION="if you really want silence at the end of your video, add silence to the end of the audio file"
                fi
                if [[ $AUDIO_OFFSET_MS -gt 1000000 ]]
                then
                    echo "$( tput setaf 1 )Error:$( tput sgr0 ) audio file $( tput setaf 4 )${SEGMENT_FILENAME/\.*/}.$( tput setaf 1 )wav$( tput sgr0 ) is ${AUDIO_OFFSET_MS/%??????/} seconds too $AUDIO_LENGTH_WORD"
                    echo "Please fix the problem and try again"
                    echo "  * to rebuild the audio automatically, delete the .wav file and re-run."
                    echo "  * $AUDIO_ERROR_SUGGESTION"
                    echo
                    return
                fi
            else
                START_TIME="$( date +%s )"
                # Step one: calculate audio delay
                case "${AUDIO_DELAY:0:1}X" in # Step two: shift the audio according to the audio delay
                    X)
                        # no audio delay
                        EXTRA_INPUT=
                        DELAY_FILTER=
                        OUT_STREAM="[audio]"
                        ;;
                    -)
                        # negative audio delay - trim start
                        EXTRA_INPUT=
                        DELAY_FILTER="; [audio] atrim=start=${AUDIO_DELAY:1} [trimmed_audio]"
                        OUT_STREAM="[trimmed_audio]"
                        ;;
                    *)
                        # positive audio delay - prepend silence
                        DELAY_FILTER="; aevalsrc=0:s=$AUDIO_BITRATE:c=2:d=$AUDIO_DELAY [padding]; [padding] [audio] concat=v=0:a=1 [padded_audio]"
                        OUT_STREAM="[padded_audio]"
                        ;;
                esac

                SOX_AUDIO_FILTERS="$TRANSCODE_AUDIO_FILTERS" # SOX audio filters transform the input, whereas $AUDIO_FILTER cuts it up
                if [ -n "$TRANSCODE_AUDIO_FREQUENCY_RANGE" ]
                then
                    LOWER_BOUND="${TRANSCODE_AUDIO_FREQUENCY_RANGE/ */}"
                    UPPER_BOUND="${TRANSCODE_AUDIO_FREQUENCY_RANGE/* /}"
                    for (( Hz=0; Hz<$LOWER_BOUND; Hz+=10 ))
                    do SOX_AUDIO_FILTERS="equalizer $Hz 1h -50 $SOX_AUDIO_FILTERS"
                    done
                    if [ "$UPPER_BOUND" -lt $(( AUDIO_BITRATE / 4 )) ]
                    then
                        for (( Hz="$UPPER_BOUND"; Hz<=$UPPER_BOUND+500; Hz+=10 ))
                        do SOX_AUDIO_FILTERS="equalizer $Hz 1h -50 $SOX_AUDIO_FILTERS"
                        done
                        for (( Hz="$UPPER_BOUND"; Hz<=$UPPER_BOUND*2; Hz+=50 ))
                        do SOX_AUDIO_FILTERS="equalizer $Hz 5h -90 $SOX_AUDIO_FILTERS"
                        done
                        for (( Hz="$UPPER_BOUND"*2; Hz<=$(( AUDIO_BITRATE / 2 )); Hz+=100 ))
                        do SOX_AUDIO_FILTERS="equalizer $Hz 10h -120 $SOX_AUDIO_FILTERS"
                        done
                    else
                        for (( Hz="$UPPER_BOUND"; Hz<=$(( AUDIO_BITRATE / 2 )); Hz+=50 ))
                        do SOX_AUDIO_FILTERS="equalizer $Hz 5h -90 $SOX_AUDIO_FILTERS"
                        done
                    fi
                fi

                # Step two: audio extraction command
                START_TIME="$( date +%s )"
                if [ -z "$NOISE_PROFILE" ] # Step three: denoise based on the default noise profile, then normalise audio levels
                then $CMD_SOX --temp "$( dirname "$AUDIO_FILE" )" -S -t wav - "$AUDIO_FILE"                                                                $SOX_AUDIO_FILTERS
                else $CMD_SOX --temp "$( dirname "$AUDIO_FILE" )" -S -t wav - "$AUDIO_FILE" noisered <( echo "$NOISE_PROFILE" ) "${NOISE_REDUCTION:-0.21}" $SOX_AUDIO_FILTERS
                fi \
                    2> >( sox_progress "$( tput setaf 4 )$SEGMENT_FILENAME$( tput sgr0 ) creating audio ($CURRENT_STAGE/$STAGE_COUNT)" ) \
                    < <(
                    $CMD_FFMPEG \
                                $SEGMENT_START_OPTS \
                                -i "file:$ORIGINAL" \
                                -filter_complex "$AUDIO_FILTER$DELAY_FILTER" \
                                -map "$OUT_STREAM" \
                                -vn -f wav -
                    )
                CURRENT_STAGE=$(( CURRENT_STAGE + 1 ))
            fi
            echo -n $'\r'"$( echo -n "$MESSAGE" | tr -c '' ' ' )"$'\r' >&2

            OUT_TIME_S="${OUT_TIME_MS/%??????/}"
            PLAYLIST="$PLAYLIST
#EXTINF:${OUT_TIME_S:-0},$SEGMENT_TITLE
$SEGMENT_FILENAME
"

            if [ -z "$PREPARE_SEGMENT" ]
            then
                if [ -e "$SEGMENT_FILENAME" ]
                then
                    date +"%c: renamed old $( tput setaf 4 )$SEGMENT_FILENAME$( tput sgr0 ) to $( tput setaf 4 )$SEGMENT_FILENAME.old$( tput sgr0 )"
                    mv "$SEGMENT_FILENAME" "$SEGMENT_FILENAME.old"
                fi

                # Build video file for segment:
                START_TIME="$( date +%s )"
                if [ "$STAGE_COUNT" -eq 1 ]
                then STAGE_MESSAGE=
                else STAGE_MESSAGE=" creating video ($CURRENT_STAGE/$STAGE_COUNT)"
                fi

                # Transcoding command:
                $CMD_FFMPEG \
                    \
                    -loglevel 16 \
                    \
                    -progress file://>( ffmpeg_progress "$( tput setaf 4 )$SEGMENT_FILENAME$( tput sgr0 )$STAGE_MESSAGE" ) \
                    \
                    $SEGMENT_START_OPTS -i "file:$ORIGINAL" \
                    -i "file:$AUDIO_FILE" \
                    -i "file:$METADATA_FILE" \
                    \
                    -filter_complex "$VIDEO_FILTER" \
                    \
                    -c:v "$TRANSCODE_VIDEO_FORMAT" $TRANSCODE_VIDEO_OPTIONS \
                    -c:a "$TRANSCODE_AUDIO_FORMAT" $TRANSCODE_AUDIO_OPTIONS \
                    -f   "$TRANSCODE_MUXER_FORMAT" $TRANSCODE_MUXER_OPTIONS \
                    \
                    -map 1:0 -map [video] -map_metadata 2 -map_chapters 2 \
                    -metadata "$( date -u +"DATE_ENCODED=%Y-%m-%d %H:%M:%S.000" )" \
                    $FRAMERATE_OPTS \
                    "file:$SEGMENT_FILENAME" \
                    || exit 1

                sleep 0.1 # quick-and-dirty way to ensure ffmpeg_progress finishes before we print the next line
                date +"%c $( tput setaf 4 )$SEGMENT_FILENAME$( tput sgr0 ) finished"
            fi

            PREPARE_SEGMENT=

        }

        playlist() {
            echo "$PLAYLIST" > "$1"
            PLAYLIST=$'#EXTM3U\n'
            date +"%c $( tput setaf 4 )$1$( tput sgr0 ) created"
        }

        PREPARE_SEGMENT=
        SCRIPT_FILE="$( readlink -f "$2" )"
        cd "$( dirname "$SCRIPT_FILE" )"
        source "$SCRIPT_FILE"

        [ -z "$HAVE_CREATED_SEGMENTS" ] && echo "Please specify at least one segment"

    ;;

    *)
        echo "$HELP_MESSAGE"

esac