CMスキップ
mplayer用のCMスキップのためのEDLを作成するスクリプトです.公開はしますが,エンコードに利用するのは危険です.自分自身も現在は視聴時のみに利用し,エンコードには使っていません.録画した 24シーズン7 では,上手いことCMスキップで視聴はできました.EDLを利用した視聴やエンコードは次のように行います.
$ mplayer.exe -edl foo.edl foo.avi $ mencoder.exe -edl foo.edl foo.avi -ovc copy -oac copy -o gee.avi
地デジTSファイルをダイレクトに視聴できればいいのですが,mplayerでTSを視聴すると,EDLが効きません.また,TS視聴時のmplayerの("o"キーで表示される左上の)時間表示もあまりにいい加減なので,何らかの不具合があるのかも知れません.aviなら問題はないようです.(TSのPCR?では,TSも視聴可能になった経緯を示しました.)
24シーズン7 は現在フジテレビで夜中に放送中で,来週23話,24話が放送されて完結です.現在,自宅のハードディスクには,1話〜22話がエンコ(xvid,mp3)されたまま眠っています.これをそろそろ見ようかなと思ったのですが,CMがウザイ.作業しながらだと,マニュアルでCMをスキップするのも大変,とは言え編集ソフトでトリミングも大変,それにおそらく通して1回見たら,きっと暫くは見ないだろうから,わざわざ手間を掛けたくない.過去に24のBOX DVD買ったけど,通して1回みただけで,その後視聴していませんから.ということで,EDLを作成するスクリプトを作成したという訳です.その為,24が見られるだけで幸せという「てきとー」なスクリプトです.少なくとも24を全部(24話)見てから不具合がなければ,エンコードにも使うかも.
CMスキップの原理は,無音部分でカットするというものです.
利用したツール一覧
シェル(bash) | シェルスクリプトで手順をまとめています |
awk | awkのスクリプトで,edlを作成しています |
mp3splt | 無音部分の抽出に使います |
MediaInfo(CLI) | エンコード,再生時間の取得に使います |
mplayer.exe | オーディオダンプ,PCM抽出に使います |
lame64 | PCMをmp3に変換するのに使います |
awk,bash などはcygwinでインストールできます.また,MediaInfoはCLIを使います.CLIの名前は「media-info.exe」とリネームして使用しています.mplayer,lame64 は MediCoder に付属しているものを利用しています.
スクリプト
スクリプトは,cue2edl4cmskip.sh と cue2edl4cmskip.awk の2種類です..awk は .sh 内部で利用します.次のように動画ファイルのあるフォルダで実行します.
$ cue2edl4cmskip.sh -i foo.avi
実行したフォルダに,foo.edl が出力されます.1時間の動画で1分ほど掛かります.
TS動画ファイルを直接与えることもできます..sh内部でpcmを抽出して,mp3に変換します.が,mplayerでのTS視聴では edl が上手く効きません.残念です.(TSのPCR?では,TSも視聴可能になった経緯を示しました.)
bash シェルスクリプト(cue2edl4cmskip.sh)
#! /usr/bin/sh # # mp3spltのcueを解析して,CMスキップを行います. # ################################## # 関数 # function error_mssg { echo "usage: $(basename $0) [-d n] -i <input file>" 1>&2 echo " -d n : debug (0:none, 1:verbose, 9:turn over)" 1>&2 } ################################## # 引数処理 # while getopts "i:d:" opt; do case $opt in \?) error_mssg; exit 1; break;; i) ipath="$OPTARG";; d) debug="$OPTARG";; esac done if [[ -z "$ipath" ]]; then error_mssg exit 1 fi ############################################## # 処理 # acodec=$(media-info -f "$ipath"|grep "Audio codecs") acodec=$(echo "$acodec"|sed -e "s/^Audio codecs *: MPEG-1 Audio layer 3$/MP3/") fmp3=${ipath%%.*}.mp3 fcue=${ipath%%.*}.cue fedl=${ipath%%.*}.edl if [[ "$acodec" != "MP3" ]]; then fwav=${ipath%%.*}.wav mplayer.exe "$ipath" -vo null -ao pcm:file="$fwav" lame64.exe "$fwav" "$fmp3" else mplayer.exe "$ipath" -dumpaudio -dumpfile "$fmp3" fi duration=\ $(media-info.exe -f "$fmp3" |grep Duration|head -1 |sed -e "s/^Duration *: *\(.*\)$/\1/") mp3splt.exe -P -E "$fcue" -s "$fmp3" folder=$(cd $(dirname "$0") && pwd) fawk="${folder}/cue2edl4cmskip.awk" awk -v DEBUG=$debug -v DURATION=$duration -f "$fawk" "$fcue" >"$fedl" if [[ -z "$debug" || "$debug" == 0 ]]; then rm "$fmp3" "$fcue" fi
awk スクリプト(cue2edl4cmskip.awk)
14〜31秒をCMとしていますが,14〜16秒の方が安全ですね.
# # 開始処理 # BEGIN{ # 割当て変数 -v DURATION=<録画時間(msec)> total_duration =(DURATION=="")?99999:(DURATION/1000.0); mode ="?"; sound_start =-1; stack_count =0; EDLSTR=""; } # # パタン処理 # $1 ~ /INDEX/{ split( $3, var, ":" ); sound_end =var[1]*60+var[2]+var[3]/100; if( sound_start<0 ){ sound_start =sound_end; next; } if( DEBUG ) printf( "\n mode:%s sound start:%.2f end:%.2f\n", mode, sound_start, sound_end ) > "/dev/stderr"; sound_duration =sound_end-sound_start; type =decision( sound_duration ); mode =transition( mode, type, sound_start, sound_end ); sound_start =sound_end; } # # 終了処理 # END{ sound_end =total_duration; sound_duration =sound_end-sound_start; if( DEBUG ){ printf( "\n*** post-process ***\n" ) > "/dev/stderr"; printf( " mode:%s\n", mode ) > "/dev/stderr"; } type =decision( sound_duration ); mode =transition( mode, type, sound_start, sound_end ); if( mode=="C" && DEBUG!=9 ){ split( stack_summary(), var, " " ); EDLSTR =EDLSTR""output_EDL( var[2], var[3] ); } if( mode=="P" && DEBUG==9 ){ EDLSTR =EDLSTR""output_EDL( sound_start, sound_end ); } printf( "\nEDL:\n%s", EDLSTR ) > "/dev/stderr"; } ################################################## # 関数定義 # # 状態遷移 function transition( mode, type, sound_start, sound_end, next_mode, var ) { next_mode =mode; if( mode=="?" ){ if( type=="P" ){ if( DEBUG==9 ){ EDLSTR =EDLSTR""output_EDL( sound_start, sound_end ); }else{ split( stack_summary(), var, " " ); EDLSTR =EDLSTR""output_EDL( var[2], var[3] ); } stack_purge(); next_mode ="P"; } if( type=="C" ){ stack_push( type, sound_start, sound_end ); next_mode ="C"; } if( type=="?" ){ stack_push( type, sound_start, sound_end ); } } if( mode=="C" ){ if( type=="P" ){ split( stack_top(), var, " " ); if( var[1]=="?" ){ stack_pop(); if( DEBUG==9 ) EDLSTR =EDLSTR""output_EDL( var[2], var[3] ); } if( DEBUG==9 ){ EDLSTR =EDLSTR""output_EDL( sound_start, sound_end ); }else{ split( stack_summary(), var, " " ); EDLSTR =EDLSTR""output_EDL( var[2], var[3] ); } stack_purge(); next_mode ="P"; }else{ stack_push( type, sound_start, sound_end ); } } if( mode=="P" ){ if( type=="?" ){ stack_push( type, sound_start, sound_end ); } if( type=="C" ){ split( stack_bottom(), var, " " ); if( var[1]=="?" ){ stack_shift(); if( DEBUG==9 ) EDLSTR =EDLSTR""output_EDL( var[2], var[3] ); } stack_push( type, sound_start, sound_end ); next_mode ="C"; } if( type=="P" ){ if( DEBUG==9 ) EDLSTR =EDLSTR""output_EDL( sound_start, sound_end ); } } if( DEBUG && mode!=next_mode ) printf( " ---> %s\n", next_mode ) > "/dev/stderr"; return next_mode; } # 継続時間からタイプの判定 function decision( duration, type ) { if( duration<=14 ){ # 14秒以下の場合,判定保留 type ="?"; } if( duration>14 && duration<31 ){ # 14秒より大きく31秒未満ならば,CM type ="C"; } if( duration>=31 ){ # 31秒以上ならば,本編 type ="P"; } if( DEBUG ) printf( " dec:%s duration=%.2f\n", type, duration ) > "/dev/stderr"; return type; } # EDL出力 function output_EDL( start_time, end_time ) { if( DEBUG ){ printf( " EDL:[%-15.2f %-15.2f 0]\n", start_time, end_time ) > "/dev/stderr"; printf( " %s %s 0\n", readable(start_time), readable(end_time) ) >"/dev/stderr"; } printf( "%.2f %.2f 0\n", start_time, end_time ); return sprintf( "%s %s 0\n", readable(start_time), readable(end_time) ); } function readable( time, hour, min, sec, msec ){ hour =int(time/(60*60)); min =int((time-hour*60*60)/60); sec =int(time-hour*60*60-min*60); msec =(time-int(time))*1000000 return sprintf( "%02d:%02d:%02d.%06d", hour, min, sec, msec ); } # スタック操作 function stack_push( type, start_time, end_time, var ) { var =sprintf( "%s %f %f", type, start_time, end_time ); stack[stack_count] =var; stack_count +=1; if( DEBUG ) printf( " pushed:(%02d) %s\n", stack_count, var ) > "/dev/stderr"; } function stack_pop( var ) { if( stack_count==0 ) return ""; stack_count -=1; var =stack[stack_count]; delete stack[stack_count]; return var; } function stack_top() { if( stack_count==0 ) return ""; return stack[stack_count-1]; } function stack_bottom() { if( stack_count==0 ) return ""; return stack[0]; } function stack_shift() { if( stack_count==0 ) return; for( ix=0; ix<stack_count-1; ix++ ){ stack[ix] =stack[ix+1]; } delete stack[stack_count-1]; stack_count -=1; } function stack_summary( var ) { if( stack_count==0 ) return "? 0 0"; split( stack[0], var, " " ); start_time =var[2]; split( stack[stack_count-1], var, " " ); end_time =var[3]; return sprintf( "%s %f %f", var[1], start_time, end_time ); } function stack_purge() { stack_count =0; delete stack; }
手法
上記 cue2edl4cmskip.awk では,次のコメントを削除しましたが,自分のawkスクリプトには次のコメントを付してあります.それを手法の簡易的な説明とします.
# # CMスキップ用EDLの作成 # # 地デジ動画のCMスキップを行うために # mplayerのEDLを作成する # # -v DEBUG=n 0: デバッグ情報なし(未指定時) # 1: デバッグ情報あり # 9: スキップの反転.本編をスキップし,CMだけを視聴. # # -v DURATION=nnnn # 動画(or MP3)の録画時間(msec).未指定時は99999sec(28時間). # #*使い方 # $ awk -f cue2edl4cmskip.awk foo.log >foo.edl # $ awk -v DURATION=3820000 -f cue2edl4cmskip.awk foo.log >foo.edl # $ awk -v DEBUG=1 -f cue2edl4cmskip.awk foo.log >foo.edl 2>debug.txt # #*CUEファイルについて # mp3splt.exe のcueを加工して利用します. # # $ mp3splt.exe -P -E foo.cue -s foo.avi # # フォルダ内に foo.cue という名前でcueが出力されます. # このcueファイルは,MP3の無音部分で切り分けられたリストで, # 再生の頭出し(cue)に使うものです.また,先頭1行にファイル名があります. # #---------------------------------------------- # FILE "foo.mp3" MP3 # TRACK 01 AUDIO # INDEX 01 00:00:00 # TRACK 02 AUDIO # INDEX 01 00:13:82 # TRACK 03 AUDIO # INDEX 01 00:16:08 #---------------------------------------------- # #*手法 # (1)頭出しのデータから各再生時間を算出します. # (a) 継続時間が 14〜31秒ならば,CMと見做します(C). # (b) 31秒以上の場合,本編と見做します(P). # (c) 14秒以下の場合,未確定とします(?). # # (2)未確定(?)の部分はスタックに格納し,後続にC,Pが現れるまで判定を保留します. # # (3)"?"の判定は「C?CはC,P?PはP」が基本的考え方です. # (a) ??・・?P 処理開始時点でPが後続:Cと見做す # (b) ??・・?C 処理開始時点でCが後続:Cと見做す # (c) C??・・?P C判定後,Pが後続:Cと見做す(*1) # (d) C??・・?C C判定後,Cが後続:Cと見做す # (e) P??・・?P P判定後,Pが後続:Pと見做す # (f) P??・・?C P判定後,Cが後続:Cと見做す(*2) # # 簡易的にはよいのですが,(b)*1,(f)*2 には若干の問題があります. # (b)*1の場合,C??・・?P に対するPの直前の数個がPの可能性があります. # (f)*2の場合,P??・・?C に対するPの直後の数個がPの可能性があります. # # (a)の場合も??・・?P に対するPの直前の数個がPの可能性もありますが, # これはほぼ表れないようなので,無視します. # # 完全な判定は無理(無茶)ですが,簡易的に判定しています(コード参照). #
雑感
地デジは相当てきとーな仕組みであることは巷間言わずと知れたことですが,地デジの広報用CMも困ったもので,CM中に無音部分がたくさんあります.その為,小さく分割されてしまい,煩わしったらありゃしない.わざとなのか?わざとなのかい?と妄想チックになりました.
comskipはすばらしそうです.TSも扱えるし,無音検出モードもあるし,コンソールから叩けるし,上手く調整パラメータを見つけられれば,すごいと思います,僕はあきらめました.
補遺
この方法だめですね.mplayer.exe がEDLでうまくスキップできないとかもありますが,無音部分の検出だけでは,例外が結構多くてCMが頻繁に残ってしまうようです.最近*1,スキップ無しで録画したドラマや映画を違和感無く見てます.オンタイムのながら見と同じですから,それでいいのだとわかりました.
*1:補遺は12月に書いてる