Files
small-scripts/scripts/mp3-to-mp4
Paul O'Reilly 765f32b4fa Add mp3-to-mp4: convert MP3 to MP4 with static title card
Uses ffmpeg to generate an H.264/AAC video from an MP3 file with a
centred title overlay. Supports optional background image (title at
bottom) or solid colour (title centred). Includes spec, dryrun support,
and 10 tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 18:34:50 +12:00

143 lines
4.4 KiB
Bash
Executable File

#!/usr/bin/env bash
set -uo pipefail
SCRIPT_NAME="$(basename "$0")"
# Defaults
TITLE=""
OUTPUT=""
RESOLUTION="1920x1080"
FONT_SIZE="72"
FONT_COLOR="white"
BG_COLOR="black"
IMAGE=""
DRYRUN=false
usage() {
cat <<EOF
Usage: $SCRIPT_NAME [OPTIONS] <input.mp3>
Convert an MP3 audio file into an MP4 video displaying a static title card.
Arguments:
<input.mp3> Path to the MP3 file (required)
Options:
-t, --title TEXT Title text to display (required)
-o, --output PATH Output file path (default: input with .mp4 extension)
-r, --resolution WxH Video resolution (default: 1920x1080)
--font-size N Font size in pixels (default: 72)
--font-color COLOR Font colour (default: white)
--bg-color COLOR Background colour (default: black; ignored with --image)
-i, --image PATH Background image (scaled to fill; title at bottom)
-n, --dryrun Preview what would happen without creating files
-h, --help Show this help message
EOF
}
die() {
echo "Error: $1" >&2
exit 1
}
# Parse arguments
INPUT=""
while [[ $# -gt 0 ]]; do
case "$1" in
-t|--title) TITLE="$2"; shift 2 ;;
-o|--output) OUTPUT="$2"; shift 2 ;;
-r|--resolution) RESOLUTION="$2"; shift 2 ;;
--font-size) FONT_SIZE="$2"; shift 2 ;;
--font-color) FONT_COLOR="$2"; shift 2 ;;
--bg-color) BG_COLOR="$2"; shift 2 ;;
-i|--image) IMAGE="$2"; shift 2 ;;
-n|--dryrun) DRYRUN=true; shift ;;
-h|--help) usage; exit 0 ;;
-*) die "unknown option: $1" ;;
*) INPUT="$1"; shift ;;
esac
done
# Validate
[[ -z "$INPUT" ]] && die "no input file specified. See --help."
[[ -f "$INPUT" ]] || die "file not found: $INPUT"
[[ -z "$TITLE" ]] && die "--title is required"
command -v ffmpeg >/dev/null 2>&1 || die "ffmpeg not found. Install with: sudo apt install ffmpeg"
command -v ffprobe >/dev/null 2>&1 || die "ffprobe not found. Install with: sudo apt install ffmpeg"
[[ -n "$IMAGE" && ! -f "$IMAGE" ]] && die "image not found: $IMAGE"
# Default output path
if [[ -z "$OUTPUT" ]]; then
OUTPUT="${INPUT%.*}.mp4"
fi
# Get audio duration
DURATION_SECS=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$INPUT" 2>/dev/null) \
|| die "could not read audio duration from: $INPUT"
# Format duration for display
format_duration() {
local secs="${1%.*}"
local mins=$((secs / 60))
local rem=$((secs % 60))
if [[ $mins -gt 0 ]]; then
echo "${mins}m ${rem}s"
else
echo "${rem}s"
fi
}
DURATION_DISPLAY=$(format_duration "$DURATION_SECS")
# Get ffmpeg version
FFMPEG_VERSION=$(ffmpeg -version | head -1 | awk '{print $3}')
if $DRYRUN; then
echo "[dryrun] Input: $INPUT"
echo "[dryrun] Output: $OUTPUT"
echo "[dryrun] Title: $TITLE"
echo "[dryrun] Duration: $DURATION_DISPLAY"
echo "[dryrun] Resolution: $RESOLUTION"
echo "[dryrun] Font size: $FONT_SIZE"
echo "[dryrun] Font color: $FONT_COLOR"
if [[ -n "$IMAGE" ]]; then
echo "[dryrun] Background: $IMAGE (image)"
else
echo "[dryrun] Background: $BG_COLOR"
fi
echo "[dryrun] Dependencies OK: ffmpeg $FFMPEG_VERSION"
echo "[dryrun] Would generate .mp4 video file"
exit 0
fi
# Parse resolution
WIDTH="${RESOLUTION%x*}"
HEIGHT="${RESOLUTION#*x}"
# Escape title for ffmpeg drawtext (colons and backslashes need escaping)
ESCAPED_TITLE=$(echo "$TITLE" | sed "s/'/\\\\\\\\'/g" | sed 's/:/\\:/g')
if [[ -n "$IMAGE" ]]; then
# Image background: scale+crop to fill, title centred at bottom
ffmpeg -y \
-loop 1 -i "$IMAGE" \
-i "$INPUT" \
-vf "scale=${WIDTH}:${HEIGHT}:force_original_aspect_ratio=increase,crop=${WIDTH}:${HEIGHT},drawtext=text='${ESCAPED_TITLE}':fontsize=${FONT_SIZE}:fontcolor=${FONT_COLOR}:x=(w-text_w)/2:y=h-text_h-40" \
-c:v libx264 -tune stillimage -pix_fmt yuv420p \
-c:a aac -b:a 192k \
-shortest \
"$OUTPUT"
else
# Solid colour background: title centred
ffmpeg -y \
-f lavfi -i "color=c=${BG_COLOR}:s=${RESOLUTION}:d=${DURATION_SECS}" \
-i "$INPUT" \
-vf "drawtext=text='${ESCAPED_TITLE}':fontsize=${FONT_SIZE}:fontcolor=${FONT_COLOR}:x=(w-text_w)/2:y=(h-text_h)/2" \
-c:v libx264 -tune stillimage -pix_fmt yuv420p \
-c:a aac -b:a 192k \
-shortest \
"$OUTPUT"
fi
echo "$OUTPUT"