このメンテナンス版ガイドでは、1つのディレクトリ内にある各 MP3 を、静止画カバーまたは自動生成した単色フレーム付きの MP4 に変換します。GNU/Linux 上の Bash を対象とし、ls の出力を解析しないため、空白・タブ・改行を含む名前や、大文字小文字が混在する .mp3 拡張子も壊しません。
スクリプトは、まだ存在しない出力ディレクトリを意図的に新規作成します。候補ファイルは同じファイルシステム上の一時ディレクトリへ書き、ffprobe による検証後にだけ最終名へ変更します。入力ファイルは変更しません。それでもバックアップは残してください。変換後のファイルを音源の唯一のコピーにしてはいけません。
Table of Contents
まず動画が本当に必要か確認する
MP4 はコンテナであり、音質を改善する指定ではありません。静止画の追加が役立つのは、受け取り側が動画ストリームを必須とする場合です。音声だけをアップロードできるなら、架空の動画トラックを持たない M4A のほうが単純で小さくなります。
ffmpeg -nostdin -hide_banner -loglevel error -n
-i "input.mp3" -map 0:a:0 -map_metadata 0
-c:a aac -b:a 192k -movflags +faststart "output.m4a"
非可逆圧縮の MP3 を非可逆圧縮の AAC に変換しても、品質は戻りません。元の MP3 は保管し、数値を上げれば元音源が良くなると考えず、入力と配信先に合わせて AAC のビットレートを選んでください。
安全な一括処理:カバー画像または生成色
次の内容を mp3-to-mp4.sh として保存し、chmod +x mp3-to-mp4.sh を実行します。第3引数で cover または color を選びます。カバーが奇数寸法でも構いません。フィルターは縦横比を保ったまま 1280×720 の範囲に収め、一般的な H.264/yuv420p 処理で必要になる偶数寸法までパディングして、正方形ピクセルを設定します。
#!/usr/bin/env bash
set -Eeuo pipefail
input_dir=${1:?"usage: $0 INPUT_DIR NEW_OUTPUT_DIR [cover|color] [COVER_IMAGE]"}
output_dir=${2:?"usage: $0 INPUT_DIR NEW_OUTPUT_DIR [cover|color] [COVER_IMAGE]"}
mode=${3:-cover}
cover=${4:-cover.png}
[[ -d "$input_dir" ]] || { printf 'not a directory: %sn' "$input_dir" >&2; exit 2; }
[[ ! -e "$output_dir" ]] || {
printf 'output must be a fresh path: %sn' "$output_dir" >&2
exit 2
}
case "$mode" in
cover)
[[ -f "$cover" ]] || { printf 'cover not found: %sn' "$cover" >&2; exit 2; }
video_args=(-loop 1 -framerate 1 -i "$cover")
video_filter='scale=1280:720:force_original_aspect_ratio=decrease,pad=ceil(iw/2)*2:ceil(ih/2)*2:(ow-iw)/2:(oh-ih)/2,setsar=1'
video_title='Cover image'
;;
color)
video_args=(-f lavfi -i 'color=c=black:s=1280x720:r=1')
video_filter='setsar=1'
video_title='Generated black frame'
;;
*)
printf 'mode must be cover or color: %sn' "$mode" >&2
exit 2
;;
esac
# Plain mkdir intentionally fails if another process creates the path first.
mkdir -- "$output_dir"
found=false
while IFS= read -r -d '' input; do
found=true
base=${input##*/}
stem=${base%.*}
final="$output_dir/$stem.mp4"
[[ ! -e "$final" ]] || { printf 'refusing to overwrite: %sn' "$final" >&2; exit 3; }
workdir=$(mktemp -d "$output_dir/.ffmpeg-work.XXXXXX")
partial="$workdir/output.partial.mp4"
(
trap 'rm -f -- "$partial"; rmdir -- "$workdir" 2>/dev/null || true' EXIT
ffmpeg -nostdin -hide_banner -loglevel error -n
"${video_args[@]}" -i "$input"
-map 0:v:0 -map 1:a:0 -map_metadata 1
-vf "$video_filter"
-c:v libx264 -preset medium -tune stillimage -crf 23 -pix_fmt yuv420p
-c:a aac -b:a 192k
-metadata:s:v:0 handler_name="$video_title"
-metadata:s:a:0 handler_name="$stem"
-shortest -movflags +faststart "$partial"
video_codec=$(ffprobe -v error -select_streams v:0
-show_entries stream=codec_name -of default=nw=1:nk=1 "$partial")
audio_codec=$(ffprobe -v error -select_streams a:0
-show_entries stream=codec_name -of default=nw=1:nk=1 "$partial")
video_count=$(ffprobe -v error -select_streams v
-show_entries stream=index -of csv=p=0 "$partial" | awk 'END { print NR + 0 }')
audio_count=$(ffprobe -v error -select_streams a
-show_entries stream=index -of csv=p=0 "$partial" | awk 'END { print NR + 0 }')
duration=$(ffprobe -v error -show_entries format=duration
-of default=nw=1:nk=1 "$partial")
[[ "$video_codec" == h264 && "$audio_codec" == aac ]]
[[ "$video_count" == 1 && "$audio_count" == 1 ]]
awk -v d="$duration" 'BEGIN { exit !(d + 0 > 0) }'
# The temporary file and destination share a filesystem, so this rename is atomic.
[[ ! -e "$final" ]] || { printf 'refusing to overwrite: %sn' "$final" >&2; exit 3; }
mv -- "$partial" "$final"
rmdir -- "$workdir"
trap - EXIT
)
printf 'created: %sn' "$final"
done < <(find "$input_dir" -maxdepth 1 -type f -iname '*.mp3' -print0)
[[ "$found" == true ]] || { printf 'no MP3 files found in %sn' "$input_dir" >&2; exit 4; }
カバー画像を使う場合:
./mp3-to-mp4.sh ./input ./output-cover cover ./cover.png
自動生成した黒いフレームを使う場合は、別の新しい出力ディレクトリを指定します。
./mp3-to-mp4.sh ./input ./output-color color
明示的な -map は、動画と音声を1ストリームずつ選びます。-map_metadata 1 は MP3 入力からグローバルメタデータをコピーし、2つのストリーム単位の handler_name オプションが各トラックに名前を付けます。-shortest は有限長の音声が終わると処理を止め、+faststart はプログレッシブダウンロード向けに MP4 の初期化メタデータをファイル前方へ移します。
AAC を既定にする理由
一部の MP4 ワークフローは MP3 音声を収容できますが、受け取り側の対応状況に左右されます。MP4 では AAC のほうが相互運用しやすい既定値です。対象が MP4 内の MP3 音声を明示的に受け付け、再度の非可逆変換を避ける必要がある場合は、-c:a aac -b:a 192k を -c:a copy に置き換えます。そのうえで対象システムで実地確認し、次も実行してください。
audio_codec=$(ffprobe -v error -select_streams a:0
-show_entries stream=codec_name -of default=nw=1:nk=1 "output.mp4")
[[ "$audio_codec" == mp3 ]] || { printf 'unexpected audio codec: %sn' "$audio_codec" >&2; exit 1; }
mux が成功しただけで再生互換性があるとは判断できません。そのファイルを実際に受け取るブラウザー、アプリ、編集環境、アップロードサービスでテストしてください。
再現可能なスモークテスト
このテストは、短い正弦波 MP3 と奇数寸法のカバー画像を生成し、空白と改行の両方を含むファイル名で2つのモードを実行します。保存したスクリプトがあるディレクトリから実行してください。
set -Eeuo pipefail
test_root=$(mktemp -d)
trap 'rm -rf -- "$test_root"' EXIT
mkdir -- "$test_root/input"
fixture="$test_root/input/"$'tone onenline.mp3'
ffmpeg -nostdin -hide_banner -loglevel error
-f lavfi -i 'sine=frequency=440:duration=1.5'
-c:a libmp3lame -q:a 4 "$fixture"
ffmpeg -nostdin -hide_banner -loglevel error
-f lavfi -i 'color=c=navy:s=641x359' -frames:v 1 "$test_root/cover.png"
./mp3-to-mp4.sh "$test_root/input" "$test_root/output-cover" cover "$test_root/cover.png"
./mp3-to-mp4.sh "$test_root/input" "$test_root/output-color" color
ffprobe -v error -show_entries stream=index,codec_type,codec_name,width,height
-show_entries format=duration -of json
"$test_root/output-cover/"$'tone onenline.mp4'
H.264 動画が1ストリーム、AAC 音声が1ストリーム、正の再生時間、偶数の動画寸法になれば成功です。スクリプトは最終名で出力を公開する前に、これらを機械的に検査します。最後の JSON は人が確認するための証跡になります。
運用上の注意
- 出力ディレクトリは未作成でなければなりません。古いディレクトリの誤利用を防ぎ、
song.mp3とsong.MP3のように stem が重なる場合も、結果を黙って置き換えず失敗させます。 find ... -print0とread -d ''の組み合わせは、任意のファイル名をそのまま保持します。lsの解析では、空白や改行を含む名前を安全に扱えません。.partial.mp4は出力ディレクトリの下に作られるため、最後のmvはファイルシステムをまたぎません。失敗した候補はサブシェルの終了 trap が片付けます。- このスクリプトは再帰しません。サブディレクトリや同名ファイルを出力側へどう対応付けるかも設計した場合に限り、
-maxdepth 1を外してください。 ffmpeg -nは一時候補の上書きを拒否し、最終パスも2回確認します。無人ジョブで本当に上書きしたい場合を除き、-yに変えないでください。
公式一次資料
- FFmpeg のコマンドライン構文、ストリーム選択、`-map`
- `color`、`scale`、`pad`、`setsar` を含む FFmpeg 動画フィルター
- `faststart` を含む FFmpeg MOV/MP4 muxer オプション
- FFmpeg AAC エンコーダー文書
- ffprobe 文書
2017/2023年の原文エクスポート(逐語アーカイブ)
上のメンテナンス版では、安全でないファイル名解析を置き換え、互換性と検証の境界を加えました。出典保存のため、リポジトリにある2017年記事の2023年エクスポートから、目に見える本文をすべて以下に残します。改行と行末空白のみ正規化しています。これは歴史的なコマンドであり、メンテナンス版より優先してコピーすべきものではありません。
~~~markdown
Solution 1:
`ls *.mp3 | while read mp3File ; do outputFile=$(basename "${mp3File}" .mp3) ; ffmpeg -i "${mp3File}" -loop 1 -i image.png -c:a copy -c:v libx264 -shortest "${outputFile}".mp4 ; done
`
Solution 2:
`
$ mkdir out
$ for f in *.mp3; do ffmpeg -f lavfi -i color=s=160x120:r=2 -i "$f"
-c:v libx264 -preset ultrafast -c:a copy -shortest
out/"${f%.mp3}.mp4"; done
`
~~~
