| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108 |
- #!/usr/bin/env bash
- # Mix a BGM track into an MP4 video.
- #
- # Usage:
- # bash add-music.sh <input.mp4> [--mood=<name>] [--music=<path>] [--out=<path>]
- #
- # Mood library (in ../assets/, matching bgm-<mood>.mp3):
- # tech — Apple Silicon / product keynote vibe, minimal synth+piano (default)
- # ad — upbeat modern, clear build + drop, social-media ad energy
- # educational — warm, patient, inviting learning tone
- # educational-alt — alternate take of educational
- # tutorial — lo-fi background, stays out of voiceover's way
- # tutorial-alt — alternate take of tutorial
- #
- # Flags (all optional):
- # --mood=<name> pick a preset from the library (default: tech)
- # --music=<path> override with your own audio file (wins over --mood)
- # --out=<path> output path (default: <input-basename>-bgm.mp4)
- #
- # Legacy positional form still works: bash add-music.sh in.mp4 music.mp3 out.mp4
- #
- # Behavior:
- # - Music is trimmed to match video duration
- # - 0.3s fade in, 1.0s fade out (avoids hard cuts)
- # - Video stream copied (no re-encode), audio AAC 192k
- #
- # Examples:
- # bash add-music.sh my.mp4 # default: tech mood
- # bash add-music.sh my.mp4 --mood=ad # switch mood
- # bash add-music.sh my.mp4 --mood=educational --out=final.mp4
- # bash add-music.sh my.mp4 --music=~/Downloads/song.mp3 # bring your own
- #
- set -e
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
- ASSETS_DIR="$SCRIPT_DIR/../assets"
- # ── Parse args ───────────────────────────────────────────────────────
- INPUT=""
- MOOD="tech"
- CUSTOM_MUSIC=""
- OUTPUT=""
- POSITIONAL=()
- for arg in "$@"; do
- case "$arg" in
- --mood=*) MOOD="${arg#*=}" ;;
- --music=*) CUSTOM_MUSIC="${arg#*=}" ;;
- --out=*) OUTPUT="${arg#*=}" ;;
- *) POSITIONAL+=("$arg") ;;
- esac
- done
- # Legacy positional: <input> [music] [output]
- INPUT="${POSITIONAL[0]}"
- [ -z "$CUSTOM_MUSIC" ] && [ -n "${POSITIONAL[1]}" ] && CUSTOM_MUSIC="${POSITIONAL[1]}"
- [ -z "$OUTPUT" ] && [ -n "${POSITIONAL[2]}" ] && OUTPUT="${POSITIONAL[2]}"
- if [ -z "$INPUT" ] || [ ! -f "$INPUT" ]; then
- echo "Usage: bash add-music.sh <input.mp4> [--mood=<name>] [--music=<path>] [--out=<path>]" >&2
- echo "Moods available: $(ls "$ASSETS_DIR" | grep -E '^bgm-.*\.mp3$' | sed 's/^bgm-//;s/\.mp3$//' | tr '\n' ' ')" >&2
- exit 1
- fi
- # ── Resolve music source: --music wins, else --mood ─────────────────
- if [ -n "$CUSTOM_MUSIC" ]; then
- MUSIC="$CUSTOM_MUSIC"
- SOURCE_LABEL="custom: $MUSIC"
- else
- MUSIC="$ASSETS_DIR/bgm-${MOOD}.mp3"
- SOURCE_LABEL="mood: $MOOD"
- fi
- if [ ! -f "$MUSIC" ]; then
- echo "✗ Music not found: $MUSIC" >&2
- echo " Available moods: $(ls "$ASSETS_DIR" | grep -E '^bgm-.*\.mp3$' | sed 's/^bgm-//;s/\.mp3$//' | tr '\n' ' ')" >&2
- exit 1
- fi
- # ── Resolve output path ─────────────────────────────────────────────
- INPUT_DIR="$(cd "$(dirname "$INPUT")" && pwd)"
- INPUT_NAME="$(basename "$INPUT" .mp4)"
- [ -z "$OUTPUT" ] && OUTPUT="$INPUT_DIR/$INPUT_NAME-bgm.mp4"
- # ── Measure video duration, compute fade-out start ──────────────────
- DURATION=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$INPUT")
- if [ -z "$DURATION" ]; then
- echo "✗ Could not read video duration" >&2
- exit 1
- fi
- FADE_OUT_START=$(awk "BEGIN { d = $DURATION - 1; if (d < 0) d = 0; print d }")
- echo "▸ Mixing BGM into video"
- echo " input: $INPUT"
- echo " music: $SOURCE_LABEL"
- echo " duration: ${DURATION}s"
- echo " output: $OUTPUT"
- ffmpeg -y -loglevel error \
- -i "$INPUT" \
- -i "$MUSIC" \
- -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]" \
- -map 0:v -map "[a]" \
- -c:v copy -c:a aac -b:a 192k -shortest \
- "$OUTPUT"
- SIZE=$(du -h "$OUTPUT" | cut -f1)
- echo "✓ Done: $OUTPUT ($SIZE)"
|