#!/bin/sh
#echo START $0 $* 1>&2

# Build lossy version of 'top copy' audio versions under img/a/a folder.
# This returns the name of the output file providing it exists and is non-empty.
# This is intended for supporting VIDEO and AUDIO tags and RSS feeds.
#
# The sourcefile should be the one with least loss, preferably lossless,
# in a format that ffpmeg can read and that contains audio.
# Input audio files will typically end in .flac or .mp3.
#
# The output suffix can be one of opusL or mp3L or mp3
# in order from lowest bit-rate and fidelity to highest.
# Also opus at 96kbps providing output is smaller than input.
#
# (Do not use to re-encode .mp3 from .mp3 source unless very good reason!)
#
# If the desired output format file exists in the repo
# then that should be used in preference.
#
# Usage:
#     $0 sourcefile outputSuffix
#
# This may internally manage FAILED and DONE flags to save effort.
#
# The output file is not regenerated providing it is newer than the input
# and newer than the 'rebuild everything' flag file.
#
# Assumed to be relatively fast (tens of seconds at most).
#
# Being unable to generate the target object is not an ERROR
# not does it return a non-zero return code.
# For example the source may have no extractable audio,
# or may be unusually small.


INPUT="$1"
SUFFIX="$2"

if [ "" = "$INPUT" ] || [ ! -s "$INPUT" ]; then
    echo "ERROR: $0: input file missing." 1>&2
    exit 2
fi

case "$SUFFIX" in
    mp3) ;; # ~192kbps stereo or mono, fairly transparent
    mp3L) ;; # ~48kbps mono
    opus) ;;  # ~96kbps stereo or mono, fairly transparent
    opusL) ;; # ~16kbps mono
    *)
    echo "ERROR: $0: unsupported output type $SUFFIX." 1>&2
    exit 2;;
esac


## Load any soft site build parameters.
#. .work/softparams.txt


# AUTOGEN cache locations.
CACHEDIRAG=img/a
# If the rebuild flags exists and is newer than any cached result flag,
# unconditionally try to rebuild/refresh everything (carefully, under a lock).
# Else return immediately on cached positive or negative results.
# Assumes that a cached positive result is the most likely.
# Should be touched after any substantive change to image selection/generation.
REBUILDFLAG=$CACHEDIRAG/.rebuild.flag
# Cache sub-dir for audio derivatives.
CACHEDIRAUDIO=a
AGHBDIR=$CACHEDIRAG/$CACHEDIRAUDIO

# Make dir and flags area if necessary.
mkdir -p $AGHBDIR/.flags || exit 1

# Compute the target cache filename.
# See $AGHBDIR/README.txt for canonical format.
#
# Auto-generated (lossy) audio files, named as:
#   * the declared source file basename (no suffix) for human id of original
#   * then a '.' then a hash of the source image to avoid clashes,
#   * then a '.' then the approximate/target bitrate , eg ".45k",
#   * then a '.' suffix suitable for the generated image type,
#     typically ".mp3" or ".opus",
#   * then an optional trailing 'L' for a lo-fi Save-Data version of the audio.
# 
# The defined hash types are:
#   * 'l' then the original image length in bytes in decimal (portable, easy)
#   * 'm' then the hex MD5 original image hash (robust)
AGHBSUFFIX="$(echo "$INPUT" | sed -n -e 's/^.*\([.][a-zA-Z][a-zA-Z0-9]*\)$/\1/p')"
AGHBHASH="l$(wc -c < "$INPUT" | awk '{print $1}')"

# The approximate target bitrate (kbps).
TARGETBITRATEKBPS=0
case "$SUFFIX" in
    mp3) TARGETBITRATEKBPS=192;;
    mp3L) TARGETBITRATEKBPS=48;;
    opus) TARGETBITRATEKBPS=96;;
    opusL) TARGETBITRATEKBPS=16;;
    *)
    echo "ERROR: $0: unsupported output type $SUFFIX." 1>&2
    exit 2;;
esac

# If the raw base name is too long, replace with a crypto hash.
# This need only be long and good enough to avoid collisions.
# MD5 is fine, SHA256 at double the length might be a reasonable upgrade.
AGHBRAWBASE="$(basename "$INPUT" "$AGHBSUFFIX")"
AGHBBASE="$AGHBRAWBASE"
AGHBMAXBASENAME=42
if [ "${#AGHBRAWBASE}" -gt "$AGHBMAXBASENAME" ]; then
    # Try to preserve first word from the original name if not too long.
    NPREFIX="$(echo "$AGHBRAWBASE" |awk -F- '{if(length($1)<10){printf("%s-",$1)}}')"
    NHASH="$(echo "$AGHBRAWBASE" | md5sum | awk '{printf("%s",$1)}')"
    AGHBBASE="$NPREFIX$NHASH"
    #echo "INFO: shortened base to $AGHBBASE for $INPUT." 1>&2
fi

# Generated filename.
AGHBFILE="$AGHBDIR/$AGHBBASE.$AGHBHASH.${TARGETBITRATEKBPS}k$AGHBSUFFIX.$SUFFIX"

#$PERFTIMESTAMPS && echo "INFO: TS: GHII: ${OP}: $AGHBFILE" `$PERFTIMESTAMPCMD` 1>&2

# Subdir and file prefix for done/failed markers and timestamps.
FLAGSSDIR=.flags
FLAGSPREF="$(basename $AGHBFILE)"

# Success marker and timestamp (in flags subdir).
AGHBDONE=$AGHBDIR/$FLAGSSDIR/$FLAGSPREF.DONE
# If the success file exists and is new enough, then succeed immediately,
# *IFF* the target image actually exists too!
if [ -e "$AGHBDONE" ] && [ "$AGHBDONE" -nt "$REBUILDFLAG" ] && [ -s "$AGHBFILE" ]; then
    echo "$AGHBFILE"
    exit 0
fi

# Failure marker and timestamp (in flags subdir).
AGHBFAIL="$AGHBDIR/$FLAGSSDIR/$FLAGSPREF.FAIL"
# Don't try if the 'fail' negative cache marker is present and new enough.
# We can assume that it won't be created under us.
if [ -e "$AGHBFAIL" ] && [ "$AGHBFAIL" -nt "$REBUILDFLAG" ]; then
    exit 1
fi

# About to build or rebuild the target image, which may take a while.
echo "INFO: $0: (re)building audio file $AGHBFILE from $INPUT" 1>&2

# Info on the image build.
AGHBINFO="$AGHBDIR/$FLAGSSDIR/$FLAGSPREF.info"

# Try to avoid concurrent attempts to (re)build same (common/popular) files.
# Attempt to sleep here long enough to let a racing task complete
# so that this doesn't fail incorrectly returning an empty result.
# CONTINUE ANYWAY IF LOCK CANNOT BE TAKEN BUT THEN DON'T REMOVE LOCK AT THE END!
AGLOCKFILE="$AGHBFILE.lock"
if lockfile -s 10 -r 50 -l 1301 "$AGLOCKFILE" 1>&2; then
    #echo "INFO: lock acquired: $AGLOCKFILE" 1>&2
    # Recheck to see if task was completed while lock was held to avoid rework!
    if [ -e "$AGHBDONE" ] && [ "$AGHBDONE" -nt "$REBUILDFLAG" ] && [ -s "$AGHBFILE" ]; then
        echo "INFO: build done before lock acquired: $AGLOCKFILE" 1>&2
        rm -f "$AGLOCKFILE"
        echo "$AGHBFILE"
        exit 0
    fi
    if [ -e "$AGHBFAIL" ] && [ "$AGHBFAIL" -nt "$REBUILDFLAG" ]; then
        echo "INFO: build failed before lock acquired: $AGLOCKFILE" 1>&2
        rm -f "$AGLOCKFILE"
        exit 0
    fi
    # Clear info file if about to build image.
    rm -f "$AGHBINFO"
else
    # Replace lock with dummy name so that rm at end doesn't break anything.
    # Replace the info file too.
    AGLOCKFILE="$AGHBFILE.dummy.lock"
    AGHBINFO="$AGHBINFO.dup"
    echo "INFO: lock NOT acquired so possibly doing redundant work though safe: $AGLOCKFILE" | tee -a "$AGHBINFO" 1>&2
fi

# If ffmpeg cannot be found then give up immediately.
FFMPEG="$(which ffmpeg)"
if [ "" = "$FFMPEG" ] || [ ! -x "$FFMPEG" ]; then
    echo "ERROR: $0: ffmpeg not found." | tee -a "$AGHBINFO" 1>&2
    rm -f "$AGLOCKFILE"
    exit 1
fi


# Once into the process of trying to create an autogen image,
# and within the lock scope to reduce contention a little bit,
# in the background remove any very old temp files for all images,
# to get some cleaning up done whenever new work is happening.
# Assume all such temporary files end with .tmp.
# Do this only of rare-ish builds, in this case .mp3 -> .mp3L.
if [ "mp3L" = "$SUFFIX" ] && [ ".mp3" = "$AGHBSUFFIX" ]; then
    find $AGHBDIR/ -maxdepth 1 \( -name '*.tmp' \) -mtime +1 -exec rm -f {} \; 1>&2 &
fi

echo "INFO: attempting to auto-generate lossy audio file: ${AGHBFILE}" >> "$AGHBINFO"

# Primary temporary working output file.
TMPOUT="$AGHBFILE.$$.tmp"

# Pre-tidy!
# Remove DONE / FAILED state flags before starting.
# Remove temporary working output file(s).
rm -f $AGHBFILE.$$.tmp* $AGHBDONE $AGHBFAIL
# Hook in handler to tidy up temp file(s) on forced exit.
trap "rm -f $AGHBFILE.$$.tmp*; exit 1" 1 2 15


# Higher-fi version of the output file, if this is an 'L' file, else input.
HIGHERFI="$INPUT"
case "$AGHBFILE" in
    *L) HIGHERFI="$(echo "$AGHBFILE" | sed -e 's/L$//')";;
esac
if [ ! -s "$HIGHERFI" ]; then HIGHERFI="$INPUT"; fi
#echo "INFO: higher-fi ${HIGHERFI} version audio file: ${AGHBFILE}" 1>&2


# Flags for ffmpeg assuming that the input file is before and output file after.
MIDDLEFLAGS=""
case "$SUFFIX" in
    mp3) MIDDLEFLAGS="-codec:a libmp3lame -qscale:a 2 -cutoff 18500 -compression_level 0 -f mp3";;
    mp3L) MIDDLEFLAGS="-codec:a libmp3lame -qscale:a 9 -ac 1 -cutoff 10000 -compression_level 0 -f mp3";;
    opus) MIDDLEFLAGS="-codec:a libopus -b:a ${TARGETBITRATEKBPS}k -f opus";;
    opusL) MIDDLEFLAGS="-codec:a libopus -ac 1 -b:a ${TARGETBITRATEKBPS}k -f opus";;
    *)
    echo "ERROR: $0: unsupported output type $SUFFIX." 1>&2
    exit 2;;
esac

# Do the conversion.
# Being unable to generate the lo-fi audio output may be legitimate.
"$FFMPEG" </dev/null -nostdin -loglevel quiet -i "$INPUT" $MIDDLEFLAGS "$TMPOUT" | tee -a "$AGHBINFO" 1>&2
# Output must exist, be non-length length, and smaller than the input.
if [ -s "$TMPOUT" ] && \
    [ "$(wc -c < "$TMPOUT")" -lt "$(wc -c < "$INPUT")" ] && \
    [ "$(wc -c < "$TMPOUT")" -lt "$(wc -c < "$HIGHERFI")" ]; then
    # Succeeded: move into place atomically and globally readable.
    rm -f "$AGHBFAIL"
    chmod a+r,go-wx "$TMPOUT"
    if [ -e  "$AGHBFILE" ]; then chmod -f u+w "$AGHBFILE"; fi
    mv -f "$TMPOUT" "$AGHBFILE"
    touch "$AGHBDONE"
    echo "INFO: $0: (re)build done for $AGHBFILE" 1>&2
    # Echo name of generated output file.
    echo "$AGHBFILE"
else
    rm -f "$TMPOUT" "$AGHBDONE"
    touch "$AGHBFAIL"
    # This is not an error per se: the source may have no audio for example.
    echo "INFO: $0: FAILED to generate $TMPOUT for $AGHBFILE". 1>&2
fi

rm -f "$TMPOUT"
rm -f "$AGLOCKFILE"
wait # Wait for any still-running children.
exit 0


#### END OF CODE ####


# As at 2024-03-24:
#
# The current Audacity lame3.100 settings for .mp3 reported by mediainfo are:
#    -m j -V 2 -q 0 -lowpass 18.5 --vbr-new -b 32
# (Joint stereo ~144kbps VBR, assuming stereo in.)
# ffmpeg flags may be
#    -codec:a libmp3lame -qscale:a 2  [ -joint_stereo 1 ] [ -cutoff 18500 ] [ -compression_level 0 ]
# The docs for libmp3lame claim that compression_level 0 is slowest/best.
#
# The current Audacity lame3.100 settings for .mp3L reported by mediainfo are:
#     -m m -V 9 -q 3 -lowpass 10 --vbr-old -b 32
# (Forced mono ~45kbps VBR.)
# ffmpeg flags may be
#     -codec:a libmp3lame -qscale:a 9 -ac 1  [ -cutoff 10000 ] [ -compression_level 0 ]
# The docs for libmp3lame claim that compression_level 0 is slowest/best.
#
# The current ffmpeg arguments for .opusL are:
#     -codec:a libopus -ac 1 -b:a 16k
# (Forced mono 16kbps VBR.)
#
# See https://trac.ffmpeg.org/wiki/Encode/MP3 also.

# Generated files to be under img/a/a/ ...
#
# Auto-generated (lossy) audio files, named as:
#   * the declared source file basename (no suffix) for human id of original
#   * then a '.' then a hash of the source image to avoid clashes,
#   * then a '.' then the approximate/target bitrate , eg ".45k",
#   * then a '.' suffix suitable for the generated image type,
#     typically ".mp3" or ".opus",
#   * then an optional trailing 'L' for a lo-fi Save-Data version of the audio.
# 
# The defined hash types are:
#   * 'l' then the original image length in bytes in decimal (portable, easy)
#   * 'm' then the hex MD5 original image hash (robust)

# Note: force ffmpeg output format: with ... -f opus output.tmp
