add-music.sh
1 #!/usr/bin/env bash 2 # Mix a BGM track into an MP4 video. 3 # 4 # Usage: 5 # bash add-music.sh <input.mp4> [--mood=<name>] [--music=<path>] [--out=<path>] 6 # 7 # Mood library (in ../assets/, matching bgm-<mood>.mp3): 8 # tech — Apple Silicon / product keynote vibe, minimal synth+piano (default) 9 # ad — upbeat modern, clear build + drop, social-media ad energy 10 # educational — warm, patient, inviting learning tone 11 # educational-alt — alternate take of educational 12 # tutorial — lo-fi background, stays out of voiceover's way 13 # tutorial-alt — alternate take of tutorial 14 # 15 # Flags (all optional): 16 # --mood=<name> pick a preset from the library (default: tech) 17 # --music=<path> override with your own audio file (wins over --mood) 18 # --out=<path> output path (default: <input-basename>-bgm.mp4) 19 # 20 # Legacy positional form still works: bash add-music.sh in.mp4 music.mp3 out.mp4 21 # 22 # Behavior: 23 # - Music is trimmed to match video duration 24 # - 0.3s fade in, 1.0s fade out (avoids hard cuts) 25 # - Video stream copied (no re-encode), audio AAC 192k 26 # 27 # Examples: 28 # bash add-music.sh my.mp4 # default: tech mood 29 # bash add-music.sh my.mp4 --mood=ad # switch mood 30 # bash add-music.sh my.mp4 --mood=educational --out=final.mp4 31 # bash add-music.sh my.mp4 --music=~/Downloads/song.mp3 # bring your own 32 # 33 set -e 34 35 SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" 36 ASSETS_DIR="$SCRIPT_DIR/../assets" 37 38 # ── Parse args ─────────────────────────────────────────────────────── 39 INPUT="" 40 MOOD="tech" 41 CUSTOM_MUSIC="" 42 OUTPUT="" 43 POSITIONAL=() 44 45 for arg in "$@"; do 46 case "$arg" in 47 --mood=*) MOOD="${arg#*=}" ;; 48 --music=*) CUSTOM_MUSIC="${arg#*=}" ;; 49 --out=*) OUTPUT="${arg#*=}" ;; 50 *) POSITIONAL+=("$arg") ;; 51 esac 52 done 53 54 # Legacy positional: <input> [music] [output] 55 INPUT="${POSITIONAL[0]}" 56 [ -z "$CUSTOM_MUSIC" ] && [ -n "${POSITIONAL[1]}" ] && CUSTOM_MUSIC="${POSITIONAL[1]}" 57 [ -z "$OUTPUT" ] && [ -n "${POSITIONAL[2]}" ] && OUTPUT="${POSITIONAL[2]}" 58 59 if [ -z "$INPUT" ] || [ ! -f "$INPUT" ]; then 60 echo "Usage: bash add-music.sh <input.mp4> [--mood=<name>] [--music=<path>] [--out=<path>]" >&2 61 echo "Moods available: $(ls "$ASSETS_DIR" | grep -E '^bgm-.*\.mp3$' | sed 's/^bgm-//;s/\.mp3$//' | tr '\n' ' ')" >&2 62 exit 1 63 fi 64 65 # ── Resolve music source: --music wins, else --mood ───────────────── 66 if [ -n "$CUSTOM_MUSIC" ]; then 67 MUSIC="$CUSTOM_MUSIC" 68 SOURCE_LABEL="custom: $MUSIC" 69 else 70 MUSIC="$ASSETS_DIR/bgm-${MOOD}.mp3" 71 SOURCE_LABEL="mood: $MOOD" 72 fi 73 74 if [ ! -f "$MUSIC" ]; then 75 echo "✗ Music not found: $MUSIC" >&2 76 echo " Available moods: $(ls "$ASSETS_DIR" | grep -E '^bgm-.*\.mp3$' | sed 's/^bgm-//;s/\.mp3$//' | tr '\n' ' ')" >&2 77 exit 1 78 fi 79 80 # ── Resolve output path ───────────────────────────────────────────── 81 INPUT_DIR="$(cd "$(dirname "$INPUT")" && pwd)" 82 INPUT_NAME="$(basename "$INPUT" .mp4)" 83 [ -z "$OUTPUT" ] && OUTPUT="$INPUT_DIR/$INPUT_NAME-bgm.mp4" 84 85 # ── Measure video duration, compute fade-out start ────────────────── 86 DURATION=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$INPUT") 87 if [ -z "$DURATION" ]; then 88 echo "✗ Could not read video duration" >&2 89 exit 1 90 fi 91 FADE_OUT_START=$(awk "BEGIN { d = $DURATION - 1; if (d < 0) d = 0; print d }") 92 93 echo "▸ Mixing BGM into video" 94 echo " input: $INPUT" 95 echo " music: $SOURCE_LABEL" 96 echo " duration: ${DURATION}s" 97 echo " output: $OUTPUT" 98 99 ffmpeg -y -loglevel error \ 100 -i "$INPUT" \ 101 -i "$MUSIC" \ 102 -filter_complex "[1:a]atrim=0:${DURATION},asetpts=PTS-STARTPTS,afade=t=in:st=0:d=0.3,afade=t=out:st=${FADE_OUT_START}:d=1[a]" \ 103 -map 0:v -map "[a]" \ 104 -c:v copy -c:a aac -b:a 192k -shortest \ 105 "$OUTPUT" 106 107 SIZE=$(du -h "$OUTPUT" | cut -f1) 108 echo "✓ Done: $OUTPUT ($SIZE)"