Last active
January 22, 2025 04:43
-
-
Save FlyingFathead/a375091fcfcbc7aee8bbddd6999cc084 to your computer and use it in GitHub Desktop.
Encode `.mp3` files to target size using `ffmpeg`
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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