add-music.sh 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  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. SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
  35. ASSETS_DIR="$SCRIPT_DIR/../assets"
  36. # ── Parse args ───────────────────────────────────────────────────────
  37. INPUT=""
  38. MOOD="tech"
  39. CUSTOM_MUSIC=""
  40. OUTPUT=""
  41. POSITIONAL=()
  42. for arg in "$@"; do
  43. case "$arg" in
  44. --mood=*) MOOD="${arg#*=}" ;;
  45. --music=*) CUSTOM_MUSIC="${arg#*=}" ;;
  46. --out=*) OUTPUT="${arg#*=}" ;;
  47. *) POSITIONAL+=("$arg") ;;
  48. esac
  49. done
  50. # Legacy positional: <input> [music] [output]
  51. INPUT="${POSITIONAL[0]}"
  52. [ -z "$CUSTOM_MUSIC" ] && [ -n "${POSITIONAL[1]}" ] && CUSTOM_MUSIC="${POSITIONAL[1]}"
  53. [ -z "$OUTPUT" ] && [ -n "${POSITIONAL[2]}" ] && OUTPUT="${POSITIONAL[2]}"
  54. if [ -z "$INPUT" ] || [ ! -f "$INPUT" ]; then
  55. echo "Usage: bash add-music.sh <input.mp4> [--mood=<name>] [--music=<path>] [--out=<path>]" >&2
  56. echo "Moods available: $(ls "$ASSETS_DIR" | grep -E '^bgm-.*\.mp3$' | sed 's/^bgm-//;s/\.mp3$//' | tr '\n' ' ')" >&2
  57. exit 1
  58. fi
  59. # ── Resolve music source: --music wins, else --mood ─────────────────
  60. if [ -n "$CUSTOM_MUSIC" ]; then
  61. MUSIC="$CUSTOM_MUSIC"
  62. SOURCE_LABEL="custom: $MUSIC"
  63. else
  64. MUSIC="$ASSETS_DIR/bgm-${MOOD}.mp3"
  65. SOURCE_LABEL="mood: $MOOD"
  66. fi
  67. if [ ! -f "$MUSIC" ]; then
  68. echo "✗ Music not found: $MUSIC" >&2
  69. echo " Available moods: $(ls "$ASSETS_DIR" | grep -E '^bgm-.*\.mp3$' | sed 's/^bgm-//;s/\.mp3$//' | tr '\n' ' ')" >&2
  70. exit 1
  71. fi
  72. # ── Resolve output path ─────────────────────────────────────────────
  73. INPUT_DIR="$(cd "$(dirname "$INPUT")" && pwd)"
  74. INPUT_NAME="$(basename "$INPUT" .mp4)"
  75. [ -z "$OUTPUT" ] && OUTPUT="$INPUT_DIR/$INPUT_NAME-bgm.mp4"
  76. # ── Measure video duration, compute fade-out start ──────────────────
  77. DURATION=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$INPUT")
  78. if [ -z "$DURATION" ]; then
  79. echo "✗ Could not read video duration" >&2
  80. exit 1
  81. fi
  82. FADE_OUT_START=$(awk "BEGIN { d = $DURATION - 1; if (d < 0) d = 0; print d }")
  83. echo "▸ Mixing BGM into video"
  84. echo " input: $INPUT"
  85. echo " music: $SOURCE_LABEL"
  86. echo " duration: ${DURATION}s"
  87. echo " output: $OUTPUT"
  88. ffmpeg -y -loglevel error \
  89. -i "$INPUT" \
  90. -i "$MUSIC" \
  91. -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]" \
  92. -map 0:v -map "[a]" \
  93. -c:v copy -c:a aac -b:a 192k -shortest \
  94. "$OUTPUT"
  95. SIZE=$(du -h "$OUTPUT" | cut -f1)
  96. echo "✓ Done: $OUTPUT ($SIZE)"