#1. はじめに
最近の音楽の演奏者のトレンドとして,一箇所に集合して一緒に演奏出来ないのでそれぞれに動画を撮り,後で編集して1つの動画を作って公開,と言う流れがあります.
私は音楽の知識はあまりなく,自分で演奏する事は基本的にありません.ですが,身近に音楽関係者がおり,動画の編集を頼まれる事が多くなってきました.これに対応するためにRubyを使ってffmpegを動かし,半自動で動画の編集が出来る環境を作りました.
#2. 処理の流れ
今はスマートフォン等を使って知識の無い人でも簡単に演奏動画を撮影する事が出来ます.・・・が,それ故に皆が様々なフォーマット,動画サイズ,フレームレートで動画を送ってくると言う事でもあります.もっと動画の劣化を防ぐ上手い使い方もあるとは思っていますが,以下のように段階を踏む方法で処理しています.
- リサイズ
- 演奏開始/終了のタイミングを調整
- フェードイン,クロッピング(切り抜き)
- 音量を変更
- 上記を動画の数の分繰り返し
- 調整後の複数の動画を,並べて単体の動画に合成
#3. ソース
Windows環境で動かす事を想定しているため,参考にする方は文字\の使い方等に注意して下さい.
control_file = ARGV[0]
cfile = File.open(control_file, "r")
def system_log( exe, workd )
fho = File.open("#{workd}\\log.txt", "a")
fho.write("#{exe}\n")
fho.close
res = system( exe )
unless(res)
puts "# Error! ffmpeg failed. See #{workd}\\log.txt to confirm command."
exit
end
end
out_width = 0
out_height = 0
movie = []
resize = []
volume = []
fadein = []
delay = []
delay_f = []
to_time = []
blackf = []
crop_xy = []
crop_size = []
skip = []
row_div = []
row_flag = true
ri = 0
for s1 in cfile
next if s1 =~ /^#/
a1 = s1.chomp.split(" ")
if(a1[0] == "ffmpeg")
ffmpeg = a1[1]
next
end
if(a1[0] == "work")
workd = a1[1]
next
end
if(a1[0] == "out")
out_file_name = a1[1]
system("title #{out_file_name}")
next
end
if(a1[0] == "movie")
movie << a1[1]
next
end
if(a1[0] == "resize")
resize << a1[1]
next
end
if(a1[0] == "volume")
volume << a1[1]
next
end
if(a1[0] == "fadein")
fadein << a1[1]
next
end
if(a1[0] == "delay")
delay << a1[1]
delay_f << a1[2] if(a1[1].to_f > 0)
next
end
if(a1[0] == "time")
to_time << a1[1]
next
end
if(a1[0] == "crop")
crop_xy << a1[1]
crop_size << a1[2]
if(row_div.size == 0)
out_width = out_width + a1[2].split("x")[0].to_i
end
if row_flag
out_height = out_height + a1[2].split("x")[1].to_i
row_flag = false
end
next
end
if(a1[0] == "skip")
skip << a1[1]
next
end
if(a1[0] == "row_div")
row_div << movie.size
row_flag = true
next
end
end
cfile.close
Dir.mkdir(workd) unless File.exist?(workd)
efile_mp4 = []
efile_mp3 = []
ovl_vx = 0
ovl_vy = 0
ovl_a = []
fho = File.open("#{workd}\\log.txt", "a")
fho.write("\n#{Time.now}\n")
fho.close
out_width_e = out_width
out_height_e = out_height
fi = 0
w_reset = false
row_div << 9999
for i in 0...movie.size
filen = movie[i].split("\\")[-1].split(".")[0]
filee = movie[i].split("\\")[-1].split(".")[1]
file_rsz = "#{filen}_#{resize[i]}"
rx = resize[i].split("x")[0]
ry = resize[i].split("x")[1]
####################
#### resize part ###
####################
if(delay[i].to_f > 0.0)
fade_time = 0
else
fade_time = -1 * delay[i].to_f
end
if(delay[i].to_f > 0)
if(fadein[i].to_i > 0)
fade_in = "fade=in:0:#{fadein[i]},"
else
fade_in = ""
end
system_log( "#{ffmpeg} -y -i \"#{movie[i]}\" -vf \"#{fade_in}scale=#{rx}:#{ry}\" \"#{workd}\\#{file_rsz}.#{filee}\"", workd ) if(skip[i] == "off")
else
system_log( "#{ffmpeg} -y -i \"#{movie[i]}\" -vf \"scale=#{rx}:#{ry}\" \"#{workd}\\#{file_rsz}.#{filee}\"", workd ) if(skip[i] == "off")
end
######################
### black mov part ###
######################
if(delay[i].to_f > 0.0)
dname = delay[i].gsub(".","p")
bfile = "black_#{resize[i]}_#{dname}.#{filee}"
if(delay_f[fi] == nil)
frate = "30000/1001"
else
frate = delay_f[fi]
end
system_log( "#{ffmpeg} -y -f lavfi -i \"color=c=black:s=#{resize[i]}:r=#{frate}:d=#{delay[i]}\" -f lavfi -i \"aevalsrc=0|0:c=stereo:s=44100:d=#{delay[i]}\" \"#{workd}\\#{bfile}\"", workd ) if(skip[i] == "off")
blackf << bfile
fi = fi + 1
else
blackf << "black_0"
end
#######################
### concat/cut part ###
#######################
if(blackf[i] != "black_0")
# add plus delay
con_filen = "#{workd}\\concat_#{i}.txt"
con_file = File.open(con_filen, "w")
con_file.write("file #{workd}/#{blackf[i]}\n")
con_file.write("file #{workd}/#{file_rsz}.#{filee}\n")
con_file.close
file_cn = "#{file_rsz}_con"
system_log( "#{ffmpeg} -y -safe 0 -f concat -i \"#{con_filen}\" -c:v copy -c:a copy -map 0:v -map 0:a \"#{workd}\\#{file_cn}.#{filee}\"", workd ) if(skip[i] == "off")
elsif(delay[i].to_f < 0)
# add minus delay
cut_time = -1 * delay[i].to_f
file_ct = "#{filen}_cut"
system_log( "#{ffmpeg} -y -i \"#{workd}\\#{file_rsz}.#{filee}\" -ss #{cut_time} \"#{workd}\\#{file_ct}.#{filee}\"", workd ) if(skip[i] == "off")
file_cn = file_ct
else
# not add delay
file_cn = file_rsz
end
###########################
### fade, crop, to part ###
###########################
crop_x = crop_xy[i].split("x")[0]
crop_y = crop_xy[i].split("x")[1]
crop_w = crop_size[i].split("x")[0]
crop_h = crop_size[i].split("x")[1]
cropt = "crop=#{crop_w}:#{crop_h}:#{crop_x}:#{crop_y}"
if(w_reset)
out_width_e = out_width
out_height_e = out_height_e - crop_h.to_i
w_reset = false
end
if( out_width_e - crop_w.to_i > 0 || out_height_e - crop_h.to_i > 0)
padt = ",pad=#{out_width_e}:#{out_height_e}:0:0"
else
padt = ""
end
if(to_time[i].to_f > 0)
tot = "-to #{to_time[i].to_f + delay[i].to_f}"
else
tot = ""
end
file_crp = "#{file_rsz}_cropped"
if(delay[i].to_f > 0)
system_log( "#{ffmpeg} -y -i \"#{workd}\\#{file_cn}.#{filee}\" -vf \"#{cropt}#{padt}\" #{tot} \"#{workd}\\#{file_crp}.#{filee}\"", workd ) if(skip[i] == "off")
else
if(fadein[i].to_i > 0)
fade_in = "fade=in:0:#{fadein[i]},"
else
fade_in = ""
end
system_log( "#{ffmpeg} -y -i \"#{workd}\\#{file_cn}.#{filee}\" -vf \"#{fade_in}#{cropt}#{padt}\" #{tot} \"#{workd}\\#{file_crp}.#{filee}\"", workd ) if(skip[i] == "off")
end
out_width_e = out_width_e - crop_w.to_i
if(row_div.size > 0)
if(i == row_div[ri] - 1)
ovl_vx = 0
ovl_vy = ovl_vy + crop_h.to_i
ri = ri + 1
w_reset = true
else
ovl_vx = ovl_vx + crop_w.to_i
end
else
ovl_vx = ovl_vx + crop_w.to_i
end
ovl_a << [ovl_vx, ovl_vy]
############################
### mp3 out, volume part ###
############################
vol_e = volume[i].to_f / 100
system_log( "#{ffmpeg} -y -i \"#{workd}\\#{file_crp}.#{filee}\" -vn -acodec libmp3lame -ar 44100 -ab 256k -af \"volume=#{vol_e}\" \"#{workd}\\#{filen}.mp3\"", workd ) if(skip[i] == "off")
efile_mp3 << "#{workd}\\#{filen}.mp3"
efile_mp4 << "#{workd}\\#{file_crp}.#{filee}"
end
####################
### overlay part ###
####################
in_file = ""
for efile in efile_mp4
in_file = "#{in_file} -i \"#{efile}\""
end
ovlt = ""
for i in 0...ovl_a.size - 1
if(ovlt == "")
ovlt = "overlay=x=#{ovl_a[i][0]}:y=#{ovl_a[i][1]}"
else
ovlt = "#{ovlt},overlay=x=#{ovl_a[i][0]}:y=#{ovl_a[i][1]}"
end
end
system_log( "#{ffmpeg} -y#{in_file} -filter_complex \"#{ovlt}\" -an \"#{workd}\\out_movie.#{filee}\"", workd )
######################
### add audio part ###
######################
in_file = ""
for efile in efile_mp3
in_file = "#{in_file} -i \"#{efile}\""
end
system_log( "#{ffmpeg} -y -i \"#{workd}\\out_movie.#{filee}\" #{in_file} -filter_complex \"amix=inputs=#{efile_mp3.size}:duration=longest:dropout_transition=2\" \"#{out_file_name}\"", workd )
##3.1 Control file
動画ファイルの所在,編集方法等の指定のために,control fileを作って読み込ませるようにしました.以下は一例です.他,"row_div"と言うキーワードを入れた後の動画は,横ではなく縦に連結されます.
ffmpeg C:\ffmpeg\bin\ffmpeg.exe
work work
out out.mp4
movie org_data\a-san.mp4
resize 1280x720
volume 100
fadein 60
delay 1.5
time 208.5
crop 0x0 640x720
skip off
movie org_data\b-san.mp4
resize 1280x720
volume 120
fadein 60
delay -2.5
time 212.5
crop 320x0 640x720
skip off
ruby movie_on_playing.rb control.txt
のようなコマンドで,controlの情報を基に,ffmpegのコマンドが順次生成され,実行されます.
#4. 処理の解説
注釈部の○○ partに従って,各パートの処理を説明します.
##4.1. Resize part
control fileの"resize"の設定に従って,動画のリサイズを実行します.リサイズの値を"scale="の後に入れ,以下のコマンドを生成します.
ffmpeg.exe -y -i "org_data\a-san.mp4" -vf "scale=1280:720" "work\a-san_1280x720.mp4"
"delay"の値が+の場合は,前に何も映らない動画を連結させるため,ここでfade inの設定もしてしまいます.
ffmpeg.exe -y -i "org_data\a-san.mp4" -vf "fade=in:0:30,scale=1280:720" "work\a-san_1280x720.mp4"
##4.2. Black movie part
"delay"の値が+の場合(動画の開始を遅らせる場合),指定した秒数分の無音(注:音無しではない),映像なしの動画を作成します.値を"d="の後に入れ,以下のコマンドを生成します.
ffmpeg.exe -y -f lavfi -i "color=c=black:s=1280x720:r=30000/1001:d=1.5" -f lavfi -i "aevalsrc=0|0:c=stereo:s=44100:d=1.5" "work\black_1280x720_1p5.mp4"
この処理は連結後の動画に不具合が起きやすいため,"delay [秒数] [フレームレート]"と,3カラム目でフレームを変えられるようにしました.しかし,この方法も上手く行かない場合もあるため,ffmpegを使った動画編集の際には,元の動画のマージンを十分に取った状態で撮影し,削る方が処理は簡単です.これから動画を撮影してもらう場合は演奏者に,「録画ボタンを押して,5秒くらい待ってから演奏してね.」と言いましょう.
##4.3. Concat/cut part
"delay"の値が-の場合(動画を早く開始させる場合),"-ss"の値に入れ,以下のコマンドを生成します.
ffmpeg.exe -y -i "work\a-san_1280x720.mp4" -ss 2.5 "work\a-san_1280x720_cut.mp4"
"delay"の値が+の場合,先程の映像なしの動画と連結させます.
ffmpeg.exe -y -safe 0 -f concat -i "work\concat_1.txt" -c:v copy -c:a copy -map 0:v -map 0:a "work\a-san_1280x720_con.mp4"
##4.4. Fade, crop, to part
ここでは動画のクロッピング,終わる時間の指定を行います."delay"の値が-の場合,ここでfade inの設定を行います.ffmpegではクロップ後のサイズ,クロップ位置の順番に指定しますが,個人的に少し分かり難かったため,control fileでは"crop [クロップ位置] [クロップ後のサイズ]"と言う書式にしています."crop (X1)x(Y1) (X2)x(Y2)"→ffmpegの"crop=(X2):(Y2):(X1):(Y1)"のように変換されます.このように好みに応じて自分で書式を決められるのも,Rubyを使って制御する強みです.
"pad="の値はそれぞれの動画のクロップ後のサイズから計算しています."-to"はcontrol fileの"time"と"delay"の値から算出します.
ffmpeg.exe -y -i "work\a-san_1280x720_cut.mp4" -vf "fade=in:0:30,crop=640:720:0:0,pad=1280:720:0:0" -to 210.0 "work\a-san_1280x720_cropped.mp4"
##4.5. Mp3 out, volume part
タイミングを合わせた(-ssと-toの設定が済んだ)動画から,音のみのファイルを抽出します.ffmpegでは音量100%は"1.0"となりますが,control fileでは100%は"100"としています."volume="の後に"設定値/100"を入れ,以下のコマンドを生成します.
ffmpeg.exe -y -i "work\a-san_1280x720_cropped.mp4" -vn -acodec libmp3lame -ar 44100 -ab 256k -af "volume=1.0" "work\a-san.mp3"
##4.6. Overlay part
個別に調整した動画を,overlayで1つの動画にします.cropの値から算出して以下のコマンドを生成します.
ffmpeg.exe -y -i "work\a-san_1280x720_cropped.mp4" -i "work\b-san_1280x720_cropped.mp4" -filter_complex "overlay=x=640:y=0" -an "work\out_movie.mp4"
##4.7. Add audio part
最後に音を全て入力して完成です.mp3のファイル名を配列で保持し,順に-iで繋ぎ,配列のサイズを"amix=inputs="の値として入れて以下のコマンドを生成します.
ffmpeg.exe -y -i "work\out_movie.mp4" -i "work\a-san.mp3" -i "work\a-san.mp3" -filter_complex "amix=inputs=2:duration=longest:dropout_transition=2" "out.mp4"
この"out.mp4"が完成した動画です.
#5. その他
CUIでコマンドを入力して動画編集する際の大きな欠点として,タイミングやボリュームを見たり聞いたりしながら編集出来ないと言うところがあります.このため,上記の方法で作成した動画は,
- 一部の動画のタイミングを調整したい
- 一部の動画の音量を変更したい
等の要求が出る可能性があります.この時,調整しない動画は先程の結果を使用して高速化させるため,control fileに"skip"と言う項目を作成し,これが"on"の場合はその動画の処理をスキップするようにしています.
#6. 終わりに
ffmpegは非常に高機能で使いやすいツールで,自分は以前から重宝していますが,何十,何百とコマンドを打つ,特に動画のサイズ関係の情報を計算して入力するのはかなりの労力が必要です.この記事がその労力を緩和するために役立てば幸いです.