#!/usr/bin/env bash
# Slow motion export test - Koldflux Media Lab, October 27, 2026
# Question: what does a slowdown do to the frame stream, and what does it cost?
#
# Inputs (real files, downloaded to $WORK before running):
#   titmouse.webm  Wikimedia Commons, "Slow Motion Hand-Feeding a Tufted Titmouse.webm",
#                  CC0, 1920x1080, recorded on an Android phone
#   salad.webm     Wikimedia Commons, "Slow Motion Salad - Sony FS700.webm",
#                  CC BY 3.0 (Jack Whaley), 1280x720
#
# Every number printed below comes from ffprobe, from the framemd5 frame hashes,
# or from the python frame-math block. Nothing is computed by hand.
set -euo pipefail

WORK=${1:-/tmp/slowmo-test}
mkdir -p "$WORK"
cd "$WORK"

START=5      # seconds into the source file where the measured excerpt starts
LEN=3        # length of the measured excerpt in seconds

if [ ! -s titmouse.webm ]; then
  curl -sL -o titmouse.webm \
    "https://upload.wikimedia.org/wikipedia/commons/9/9a/Slow_Motion_Hand-Feeding_a_Tufted_Titmouse.webm"
fi
if [ ! -s salad.webm ]; then
  curl -sL -o salad.webm \
    "https://upload.wikimedia.org/wikipedia/commons/b/b5/Slow_Motion_Salad_-_Sony_FS700.webm"
fi

say() { printf '\n===== %s =====\n' "$1"; }

# ---------------------------------------------------------------- versions
say "versions"
ffmpeg -version | head -1
ffprobe -version | head -1
python3 --version

# ---------------------------------------------------------------- inputs
for f in titmouse.webm salad.webm; do
  say "input $f (container tags included)"
  ffprobe -v error -select_streams v:0 \
    -show_entries stream=codec_name,width,height,r_frame_rate,avg_frame_rate,pix_fmt \
    -show_entries format=format_name,duration,size -show_entries format_tags \
    -of default=noprint_wrappers=1 "$f"
done

# ------------------------------------------------- duplicate frame counter
# Counts output frames whose decoded pixels are identical to the frame before.
# Outputs are lossless (ffv1) so a repeated source frame really does decode to
# identical pixels.
dup_report() {
  local file=$1
  ffmpeg -hide_banner -nostdin -y -i "$file" -f framemd5 -hash md5 "$file.framemd5" 2>/dev/null
  python3 - "$file.framemd5" "$file" <<'PY'
import sys
hashfile, media = sys.argv[1], sys.argv[2]
hashes = []
with open(hashfile) as fh:
    for line in fh:
        if line.startswith('#') or not line.strip():
            continue
        hashes.append(line.rsplit(',', 1)[1].strip())
total = len(hashes)
dups = sum(1 for i in range(1, total) if hashes[i] == hashes[i - 1])
distinct = len(set(hashes))
pct = (dups / total * 100) if total else 0.0
print(f"{media}: frames={total} identical_to_previous={dups} ({pct:.2f}% of frames) distinct={distinct}")
PY
}

# ------------------------------------------------------------- control 1x
say "CONTROL 1: excerpt at its native rate, no retime (expected: 0 identical frames)"
ffmpeg -hide_banner -nostdin -y -ss "$START" -t "$LEN" -i titmouse.webm -c:v ffv1 -pix_fmt yuv420p ctrl_1x.mkv
ffprobe -v error -show_entries format=duration,size -of default=noprint_wrappers=1 ctrl_1x.mkv
dup_report ctrl_1x.mkv

# --------------------------------------------------------------- slow 2x
say "SLOW 2x: 30 fps file slowed 2x and exported at 30 fps (expected: about 50% identical frames)"
ffmpeg -hide_banner -nostdin -y -ss "$START" -t "$LEN" -i titmouse.webm -vf "setpts=2*PTS" -r 30 -c:v ffv1 -pix_fmt yuv420p slow_2x_30fps.mkv
ffprobe -v error -show_entries format=duration,size -of default=noprint_wrappers=1 slow_2x_30fps.mkv
dup_report slow_2x_30fps.mkv

# --------------------------------------------------------------- slow 4x
say "SLOW 4x: 30 fps file slowed 4x and exported at 30 fps (expected: about 75% identical frames)"
ffmpeg -hide_banner -nostdin -y -ss "$START" -t "$LEN" -i titmouse.webm -vf "setpts=4*PTS" -r 30 -c:v ffv1 -pix_fmt yuv420p slow_4x_30fps.mkv
ffprobe -v error -show_entries format=duration,size -of default=noprint_wrappers=1 slow_4x_30fps.mkv
dup_report slow_4x_30fps.mkv

# ------------------------------- control 2: the second real file, 23.976 fps
say "CONTROL 2: salad.webm (23.976 fps) slowed 2x and exported at 23.976 fps (expected: about 50% identical frames)"
ffmpeg -hide_banner -nostdin -y -ss "$START" -t "$LEN" -i salad.webm -vf "setpts=2*PTS" -r 24000/1001 -c:v ffv1 -pix_fmt yuv420p salad_2x.mkv
ffprobe -v error -show_entries format=duration,size -of default=noprint_wrappers=1 salad_2x.mkv
dup_report salad_2x.mkv

# ---------------------------------------------------------- interpolation
say "INTERPOLATION: the same 4x slowdown with in-between frames synthesized"
interp_start=$(date +%s.%N)
ffmpeg -hide_banner -nostdin -y -ss "$START" -t "$LEN" -i titmouse.webm -vf "setpts=4*PTS,minterpolate=fps=30:mi_mode=mci" -r 30 -c:v ffv1 -pix_fmt yuv420p interp_smooth.mkv
interp_end=$(date +%s.%N)
python3 - "$interp_start" "$interp_end" <<'PY'
import sys
print(f"interpolation encode wall time = {float(sys.argv[2]) - float(sys.argv[1]):.2f} s for the 3 s excerpt")
PY
ffprobe -v error -show_entries format=duration,size -of default=noprint_wrappers=1 interp_smooth.mkv
dup_report interp_smooth.mkv
ls -l slow_4x_30fps.mkv interp_smooth.mkv

# ---------------------------------------------------------- frame math
say "FRAME MATH: captured frames per second of output, for each capture rate and slowdown"
python3 - <<'PY'
def distinct_per_output_second(capture_fps, slowdown):
    return capture_fps / slowdown

rows = []
for capture_fps, capture_name in [(30, '30 fps phone video (measured on titmouse.webm)'),
                                  (24000/1001, '23.976 fps camera file (measured on salad.webm)'),
                                  (120, '120 fps (Apple Slo-mo front camera, published)'),
                                  (240, '240 fps (Apple Slo-mo back camera, published)')]:
    for slowdown in (1, 1.5, 2, 4, 8):
        kept = distinct_per_output_second(capture_fps, slowdown)
        repeats = max(0.0, 1.0 - kept / 30.0)
        rows.append((capture_name, capture_fps, slowdown, kept, repeats))

print(f"{'capture rate':52} {'slowdown':>9} {'captured frames per output second':>34} {'if exported at 30 fps':>21}")
for name, cfps, slow, distinct, dup_ratio in rows:
    print(f"{name:52} {slow:>8}x {distinct:>34.3f} {dup_ratio * 100:>20.2f}%")
PY
say "done"

# --- Editor review addition, 2026-10-27: changing only the export rate is not slow motion ---
# A synthetic 240 fps source (ffmpeg testsrc2, 2 s, 480 frames), exported two ways at 30 fps.
ffmpeg -v error -y -f lavfi -i testsrc2=size=640x360:rate=240:duration=2 -c:v ffv1 src240.mkv
ffmpeg -v error -y -i src240.mkv -r 30 -c:v ffv1 only-r30.mkv                      # export rate only
ffmpeg -v error -y -i src240.mkv -vf "setpts=8*PTS" -r 30 -c:v ffv1 retimed8.mkv   # retime 8x, then 30 fps
for f in src240 only-r30 retimed8; do
  echo "$f $(ffprobe -v error -count_frames -select_streams v:0 -show_entries stream=r_frame_rate,nb_read_frames -show_entries format=duration -of csv=p=0 $f.mkv | tr "\n" " ")"
done
echo "retimed8 frames identical to the one before: $(ffmpeg -v error -i retimed8.mkv -f framemd5 - | grep -v "^#" | awk -F, "{print \$NF}" | uniq -d | wc -l)"
