diff --git a/lib/build.sh b/lib/build.sh index d24fc11..fa4ccab 100644 --- a/lib/build.sh +++ b/lib/build.sh @@ -505,7 +505,7 @@ build_libopus() { ### MESON ### build_libdav1d() { - local enableAsm='true' + local enableAsm=true # arm64 will fail the build at 0 optimization if [[ "${HOSTTYPE}:${OPT}" == "aarch64:0" ]]; then enableAsm="false" @@ -626,8 +626,7 @@ build_ffmpeg() { --enable-nonfree \ --disable-htmlpages \ --disable-podpages \ - --disable-txtpages \ - --disable-autodetect || return 1 + --disable-txtpages || return 1 ccache make -j"${JOBS}" || return 1 ${SUDO_MODIFY} make -j"${JOBS}" install || return 1 } diff --git a/lib/efg.sh b/lib/efg.sh new file mode 100644 index 0000000..60d0972 --- /dev/null +++ b/lib/efg.sh @@ -0,0 +1,259 @@ +#!/usr/bin/env bash + +efg_usage() { + echo "efg -i input [options]" + echo -e "\t[-l NUM] low value (default: ${LOW})" + echo -e "\t[-s NUM] step value (default: ${STEP})" + echo -e "\t[-h NUM] high value (default: ${HIGH})" + echo -e "\t[-p] plot bitrates using gnuplot" + echo -e "\n\t[-I] system install at ${EFG_INSTALL_PATH}" + echo -e "\t[-U] uninstall from ${EFG_INSTALL_PATH}" + return 0 +} + +set_efg_opts() { + local opts='pl:s:h:i:IU' + local numOpts=${#opts} + # default values + unset INPUT + LOW=0 + STEP=1 + HIGH=30 + PLOT=false + EFG_INSTALL_PATH='/usr/local/bin/efg' + # only using -I or -U + local minOpt=1 + # using all + local maxOpt=${numOpts} + test $# -lt ${minOpt} && echo_fail "not enough arguments" && efg_usage && return 1 + test $# -gt ${maxOpt} && echo_fail "too many arguments" && efg_usage && return 1 + OPTIND=1 + while getopts "${opts}" flag; do + case "${flag}" in + I) + echo_warn "attempting install" + sudo ln -sf "${SCRIPT_DIR}/efg.sh" \ + "${EFG_INSTALL_PATH}" || return 1 + echo_pass "succesfull install" + exit 0 + ;; + U) + echo_warn "attempting uninstall" + sudo rm "${EFG_INSTALL_PATH}" || return 1 + echo_pass "succesfull uninstall" + exit 0 + ;; + i) + if [[ $# -lt 2 ]]; then + echo_fail "wrong arguments given" + efg_usage + return 1 + fi + INPUT="${OPTARG}" + ;; + p) + PLOT=true + ;; + l) + if ! is_positive_integer "${OPTARG}"; then + efg_usage + return 1 + fi + LOW="${OPTARG}" + ;; + s) + if ! is_positive_integer "${OPTARG}"; then + efg_usage + return 1 + fi + STEP="${OPTARG}" + ;; + h) + if ! is_positive_integer "${OPTARG}"; then + efg_usage + return 1 + fi + HIGH="${OPTARG}" + ;; + *) + echo "wrong flags given" + efg_usage + return 1 + ;; + esac + done + + if [[ ! -f ${INPUT} ]]; then + echo "${INPUT} does not exist" + efg_usage + return 1 + fi + + # set custom EFG_DIR based off of sanitized inputfile + local sanitizedInput="$(bash_basename "${INPUT}")" + local sanitizeChars=(' ' '@' ':') + for char in "${sanitizeChars[@]}"; do + sanitizedInput="${sanitizedInput//${char}/}" + done + EFG_DIR+="-${sanitizedInput}" + + echo_info "estimating film grain for ${INPUT}" + echo_info "range: $LOW-$HIGH with $STEP step increments" +} + +efg_segment() { + # number of segments to split video + local segments=30 + # duration of each segment + local segmentTime=3 + + # get times to split the input based + # off of number of segments + local duration + duration="$(get_duration "${INPUT}")" || return 1 + # trim decimal points if any + IFS=. read -r duration _ <<<"${duration}" + # number of seconds that equal 1 percent of the video + local percentTime=$((duration / 100)) + # percent that each segment takes + local percentSegment=$((100 / segments)) + # number of seconds to increment between segments + local timeBetweenSegments=$((percentTime * percentSegment)) + if [[ ${timeBetweenSegments} -lt ${segmentTime} ]]; then + timeBetweenSegments=${segmentTime} + fi + local segmentBitrates=() + + # clean workspace + test -d "${EFG_DIR}" && rm -rf "${EFG_DIR}" + mkdir -p "${EFG_DIR}" + + # split up video into segments based on start times + for ((time = 0; time < duration; time += timeBetweenSegments)); do + local outSegment="${EFG_DIR}/segment-${#segmentBitrates[@]}.mkv" + split_video "${INPUT}" "${time}" "${segmentTime}" "${outSegment}" || return 1 + local segmentBitrate + segmentBitrate="$(get_avg_bitrate "${outSegment}")" || return 1 + segmentBitrates+=("${segmentBitrate}:${outSegment}") + done + local numSegments="${#segmentBitrates[@]}" + + local removeSegments + if [[ ${numSegments} -lt ${ENCODE_SEGMENTS} ]]; then + removeSegments=0 + else + removeSegments=$((numSegments - ENCODE_SEGMENTS)) + fi + + # sort the segments + mapfile -t sortedSegments < <(IFS=: bash_sort "${segmentBitrates[@]}") + # make sure bitrate for each file is actually increasing + local prevBitrate=0 + # remove all but the highest bitrate segments + for segment in "${sortedSegments[@]}"; do + test ${removeSegments} -eq 0 && break + local file currBitrate + IFS=: read -r _ file <<<"${segment}" + currBitrate="$(get_avg_bitrate "${file}")" || return 1 + + if [[ ${currBitrate} -lt ${prevBitrate} ]]; then + echo_fail "${file} is not a higher bitrate than previous" + return 1 + fi + prevBitrate=${currBitrate} + + rm "${file}" || return 1 + removeSegments=$((removeSegments - 1)) + done + +} + +efg_encode() { + echo -n >"${GRAIN_LOG}" + for vid in "${EFG_DIR}/"*.mkv; do + echo "file: ${vid}" >>"${GRAIN_LOG}" + for ((grain = LOW; grain <= HIGH; grain += STEP)); do + local file="$(bash_basename "${vid}")" + local out="${EFG_DIR}/grain-${grain}-${file}" + encode -P 10 -g ${grain} -i "${vid}" "${out}" + echo -e "\tgrain: ${grain}, bitrate: $(get_avg_bitrate "${out}")" >>"${GRAIN_LOG}" + done + done + + less "${GRAIN_LOG}" +} + +efg_plot() { + declare -A normalizedBitrateSums=() + local referenceBitrate='' + local setNewReference='' + + while read -r line; do + local noWhite="${line// /}" + # new file, reset logic + if line_starts_with "${noWhite}" 'file:'; then + setNewReference=true + continue + fi + + IFS=',' read -r grainText bitrateText <<<"${noWhite}" + IFS=':' read -r _ grain <<<"${grainText}" + IFS=':' read -r _ bitrate <<<"${bitrateText}" + if [[ ${setNewReference} == 'true' ]]; then + referenceBitrate="${bitrate}" + setNewReference=false + fi + # bash doesn't support floats, so scale up by 10000 + local normBitrate=$((bitrate * 10000 / referenceBitrate)) + local currSumBitrate=${normalizedBitrateSums[${grain}]} + normalizedBitrateSums[${grain}]=$((normBitrate + currSumBitrate)) + setNewReference=false + done <"${GRAIN_LOG}" + + # create grain:average plot file + local plotFile="${EFG_DIR}/plot.dat" + echo -n >"${plotFile}" + for ((grain = LOW; grain <= HIGH; grain += STEP)); do + local sum=${normalizedBitrateSums[${grain}]} + local avg=$((sum / ENCODE_SEGMENTS)) + echo -e "${grain}\t${avg}" >>"${plotFile}" + done + + # plot data + bash -c 'echo $COLUMNS $LINES' >/dev/null 2>&1 + gnuplot -p -e "\ +set terminal dumb size ${COLUMNS}, ${LINES}; \ +set autoscale; \ +set style line 1 \ +linecolor rgb '#0060ad' \ +linetype 1 linewidth 2 \ +pointtype 7 pointsize 1.5; \ +plot \"${plotFile}\" with linespoints linestyle 1" | less + echo_info "grain log: ${GRAIN_LOG}" +} + +FB_FUNC_NAMES+=('efg') +# shellcheck disable=SC2034 +FB_FUNC_DESCS['efg']='estimate the film grain of a given file' +efg() { + EFG_DIR="${TMP_DIR}/efg" + # encode N highest-bitrate segments + ENCODE_SEGMENTS=5 + + set_efg_opts "$@" || return 1 + test -d "${EFG_DIR}" || mkdir "${EFG_DIR}" + + GRAIN_LOG="${EFG_DIR}/${LOW}-${STEP}-${HIGH}-grains.txt" + + if [[ ${PLOT} == 'true' && -f ${GRAIN_LOG} ]]; then + efg_plot + return $? + fi + + efg_segment || return 1 + efg_encode || return 1 + + if [[ ${PLOT} == 'true' && -f ${GRAIN_LOG} ]]; then + efg_plot || return 1 + fi +} diff --git a/lib/encode.sh b/lib/encode.sh index 16161e3..358faa3 100644 --- a/lib/encode.sh +++ b/lib/encode.sh @@ -22,19 +22,19 @@ set_audio_bitrate() { local numChannels codec numChannels="$(get_num_audio_channels "${file}" "${stream}")" || return 1 local channelBitrate=$((numChannels * 64)) - codec="$(get_stream_codec "${file}" "a:${stream}")" || return 1 + codec="$(get_stream_codec "${file}" "${stream}")" || return 1 if [[ ${codec} == 'opus' ]]; then bitrate+=( - "-c:a:${stream}" + "-c:${stream}" "copy" ) else bitrate+=( - "-filter:a:${stream}" + "-filter:${stream}" "aformat=channel_layouts=7.1|5.1|stereo|mono" - "-c:a:${stream}" + "-c:${stream}" "libopus" - "-b:a:${stream}" + "-b:${stream}" "${channelBitrate}k" ) fi @@ -95,7 +95,7 @@ audio_enc_version() { } encode_usage() { - echo "$(bash_basename "$0") -i input [options] output" + echo "encode -i input [options] output" echo -e "\t[-P NUM] set preset (default: ${PRESET})" echo -e "\t[-C NUM] set CRF (default: ${CRF})" echo -e "\t[-g NUM] set film grain for encode" @@ -105,22 +105,23 @@ encode_usage() { echo -e "\t[-v] Print relevant version info" echo -e "\t[-s] use same container as input, default is mkv" echo -e "\n\t[output] if unset, defaults to ${HOME}/" - echo -e "\n\t[-I] Install this as /usr/local/bin/encode" - echo -e "\t[-U] Uninstall this from /usr/local/bin/encode" + echo -e "\n\t[-I] system install at ${ENCODE_INSTALL_PATH}" + echo -e "\t[-U] uninstall from ${ENCODE_INSTALL_PATH}" return 0 } set_encode_opts() { local opts='vi:pcsdg:P:C:IU' - local numOpts="${#opts}" + local numOpts=${#opts} # default values PRESET=3 CRF=25 GRAIN="" - CROP='false' - PRINT_OUT='false' - DISABLE_DV='false' - local SAME_CONTAINER="false" + CROP=false + PRINT_OUT=false + DISABLE_DV=false + ENCODE_INSTALL_PATH='/usr/local/bin/encode' + local sameContainer="false" # only using -I/U local minOpt=1 # using all + output name @@ -128,18 +129,19 @@ set_encode_opts() { test $# -lt ${minOpt} && echo_fail "not enough arguments" && encode_usage && return 1 test $# -gt ${maxOpt} && echo_fail "too many arguments" && encode_usage && return 1 local optsUsed=0 + OPTIND=1 while getopts "${opts}" flag; do case "${flag}" in I) echo_warn "attempting install" sudo ln -sf "${SCRIPT_DIR}/encode.sh" \ - /usr/local/bin/encode || return 1 + "${ENCODE_INSTALL_PATH}" || return 1 echo_pass "succesfull install" exit 0 ;; U) echo_warn "attempting uninstall" - sudo rm /usr/local/bin/encode || return 1 + sudo rm "${ENCODE_INSTALL_PATH}" || return 1 echo_pass "succesfull uninstall" exit 0 ;; @@ -160,11 +162,11 @@ set_encode_opts() { optsUsed=$((optsUsed + 2)) ;; p) - PRINT_OUT='true' + PRINT_OUT=true optsUsed=$((optsUsed + 1)) ;; c) - CROP='true' + CROP=true optsUsed=$((optsUsed + 1)) ;; d) @@ -172,7 +174,7 @@ set_encode_opts() { optsUsed=$((optsUsed + 1)) ;; s) - SAME_CONTAINER='true' + sameContainer=true optsUsed=$((optsUsed + 1)) ;; g) @@ -217,7 +219,7 @@ set_encode_opts() { fi # use same container for output - if [[ $SAME_CONTAINER == "true" ]]; then + if [[ $sameContainer == "true" ]]; then local fileFormat fileFormat="$(get_file_format "${INPUT}")" || return 1 FILE_EXT='' @@ -235,6 +237,12 @@ set_encode_opts() { OUTPUT="${OUTPUT%.*}" OUTPUT+=".${FILE_EXT}" + if [[ ! -f ${INPUT} ]]; then + echo "${INPUT} does not exist" + efg_usage + return 1 + fi + echo echo_info "INPUT: ${INPUT}" echo_info "GRAIN: ${GRAIN}" diff --git a/lib/ffmpeg.sh b/lib/ffmpeg.sh index 74b1e5c..eb72761 100644 --- a/lib/ffmpeg.sh +++ b/lib/ffmpeg.sh @@ -9,6 +9,33 @@ get_duration() { "${file}" } +get_avg_bitrate() { + local file="$1" + ffprobe \ + -v error \ + -select_streams v:0 \ + -show_entries format=bit_rate \ + -of default=noprint_wrappers=1:nokey=1 \ + "${file}" +} + +split_video() { + local file="$1" + local start="$2" + local time="$3" + local out="$4" + ffmpeg \ + -ss "${start}" \ + -i "${file}" \ + -hide_banner \ + -loglevel error \ + -t "${time}" \ + -map 0:v \ + -reset_timestamps 1 \ + -c copy \ + "${out}" +} + get_crop() { local file="$1" local duration diff --git a/lib/utils.sh b/lib/utils.sh index 73aeed3..93b5d16 100644 --- a/lib/utils.sh +++ b/lib/utils.sh @@ -202,3 +202,24 @@ is_positive_integer() { fi return 0 } + +bash_sort() { + local arr=("$@") + local n=${#arr[@]} + local i j val1 val2 + + # Bubble sort, numeric comparison + for ((i = 0; i < n; i++)); do + for ((j = 0; j < n - i - 1; j++)); do + read -r val1 _ <<<"${arr[j]}" + read -r val2 _ <<<"${arr[j + 1]}" + if (("${val1}" > "${val2}")); then + local tmp=${arr[j]} + arr[j]=${arr[j + 1]} + arr[j + 1]=$tmp + fi + done + done + + printf '%s\n' "${arr[@]}" +} diff --git a/scripts/efg.sh b/scripts/efg.sh new file mode 120000 index 0000000..44b7711 --- /dev/null +++ b/scripts/efg.sh @@ -0,0 +1 @@ +entry.sh \ No newline at end of file