Skip to content

Instantly share code, notes, and snippets.

@FlyingFathead
Last active January 22, 2025 04:43
Show Gist options
  • Select an option

  • Save FlyingFathead/a375091fcfcbc7aee8bbddd6999cc084 to your computer and use it in GitHub Desktop.

Select an option

Save FlyingFathead/a375091fcfcbc7aee8bbddd6999cc084 to your computer and use it in GitHub Desktop.
Encode `.mp3` files to target size using `ffmpeg`
#!/usr/bin/env bash
#
# ffmpeg_encodetotarget_mp3_strict: Encode input to MP3, never exceed the specified target MB.
# Uses a binary search on bitrate to get as close as possible to the limit without going over.
#
# Usage: ffmpeg_encodetotarget_mp3_strict <inputfile> <target_MB> [<max_iterations>]
#
# Example:
# ffmpeg_encodetotarget_mp3_strict myaudio.wav 5
# => tries to produce an MP3 no bigger than 5 MB
#
# Requirements:
# - ffmpeg
# - ffprobe
# - bc (optional; see below)
#
# NOTE: For best results, feed it pure audio or a known audio track; if the input has multiple streams,
# you should specify -map or something similar.
set -euo pipefail
# Horizontal line function
function hz_line() {
printf '%*s\n' "${COLUMNS:-$(tput cols)}" '' | tr ' ' '-'
}
if [ $# -lt 2 ]; then
echo "Usage: $0 <inputfile> <target_MB> [<max_iterations>]"
exit 1
fi
inputfile="$1"
target_mb="$2"
max_iterations="${3:-3}" # 3 tries by default
# Retrieve audio duration in seconds (float) via ffprobe
duration=$(ffprobe -v error -select_streams a:0 -show_entries format=duration \
-of default=noprint_wrappers=1:nokey=1 "$inputfile" || echo "")
if [ -z "$duration" ]; then
echo "ERROR: Could not retrieve audio duration. Is the input file valid?"
exit 1
fi
# Convert target MB to bytes (`bc` method; doesn't work in Git Bash without installing `bc`)
# target_bytes=$(bc <<< "${target_mb} * 1024 * 1024")
# awk method, works on both Linux and Git Bash
target_bytes=$(awk -v x="$target_mb" 'BEGIN { printf "%.0f", x * 1024 * 1024 }')
# We’ll search for the best bitrate from some min to max.
# Reasonable MP3 range:
min_bitrate=8 # 8 kbps (very low, can be lower if you want)
max_bitrate=320 # 320 kbps (typical MP3 max)
# Final output file follows the pattern <original base>_<size>MB_recode.mp3
outputfile="${inputfile%.*}_${target_mb}MB_recode.mp3"
hz_line
echo ":: Input file: $inputfile"
echo ":: Duration: $duration seconds"
echo ":: Required max size: ${target_mb} MB"
echo ":: Max iterations: $max_iterations"
echo ":: Output file: $outputfile"
hz_line
best_bitrate=$min_bitrate
best_size=0
iteration=1
while [ $iteration -le $max_iterations ]; do
current_bitrate=$(( (min_bitrate + max_bitrate) / 2 ))
echo "Iteration: $iteration"
echo " Trying bitrate: ${current_bitrate} kbps"
temp_output="${outputfile}.temp"
ffmpeg -v error -y -i "$inputfile" -vn \
-c:a libmp3lame -b:a "${current_bitrate}k" \
-f mp3 "$temp_output"
# Get the resulting size (bytes)
actual_size_bytes=$(stat -c%s "$temp_output")
echo " Actual size: ${actual_size_bytes} bytes"
echo " Target size: ${target_bytes} bytes"
if [ "${actual_size_bytes}" -le "${target_bytes}" ]; then
# Good! It's <= limit => see if we can get closer to limit by going higher
if [ "${actual_size_bytes}" -gt "${best_size}" ]; then
best_size="${actual_size_bytes}"
best_bitrate="${current_bitrate}"
cp -f "$temp_output" "${outputfile}.best"
fi
min_bitrate=$((current_bitrate + 1))
echo " -> Under/equal to target. Let's push higher."
else
# Over limit => must go lower
max_bitrate=$((current_bitrate - 1))
echo " -> Over target. Lowering bitrate."
fi
rm -f "$temp_output"
echo
if [ $min_bitrate -gt $max_bitrate ]; then
echo "min_bitrate > max_bitrate => search converged."
break
fi
((iteration++))
done
# Restore the best known result
if [ -f "${outputfile}.best" ]; then
mv -f "${outputfile}.best" "$outputfile"
echo "Final chosen bitrate: ${best_bitrate} kbps"
echo "File size: $(stat -c%s "$outputfile") bytes"
hz_line
echo ":: Final output: $outputfile"
hz_line
else
echo "No best file was produced (something unusual happened)."
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment