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に変換するのに使います

awkbash などは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月に書いてる