SoXで音声感知録音:その4 VAD(Voice Activity Detection)の利用

前の関連記事:SoXで音声感知録音:その3 自動録音環境構築まとめ


前回までsilenceエフェクトを使って音の大きさとその持続時間を条件として設定して録音開始と停止を行うことで音声感知録音をしました。しかしマイクで拾う音は人間の耳で感じるよりもとても多く、耳では無音のところも感知してしまったり逆に話し声が途切れたりします。そこでvadエフェクトを使って録音後に話し声を抽出することにしました。

 VAD(Voice Activity Detection)エフェクトで録音後に話し声を抽出する方法


SoXのvadエフェクトを使うと話し声の検出ができます。

「話声」といっても純粋に言葉になっている部分だけを抽出できるほどの精度があるわけではないのですが、音の大きさと持続時間を設定して録音するsilenceエフェクトよりは精度よく話声の録音ができました。

vadエフェクトは録音時に処理できるsilenceエフェクトと違って、既に録音された音声ファイルに対しての処理になります。

また音声ファイルの前後からしか話声を検出できず、話し声に挟まれた静音部分の処理はできません。

そこで音声ファイルを疑似エフェクトnewfileを使って短時間の音声ファイルを複数作り、それを前後からvadエフェクトをかけることによってその短時間の音声ファイルの前後の静音部分を削除します。

それでも静音部分が残るので抽出後のファイルを分割してさらにvadエフェクトをかけるようにしました。

この方法でだいぶ綺麗に話し声が抽出できるようになりました。

SoX, SoXI, soxformat マニュアルページ日本語訳ではnormエフェクトとの併用が推奨されていますが、ノイズがあがるので使用しませんでした。

VAD(Voice Activity Detection)の仕組み自体は東芝レビュー2009年12月 - f01.pdfの文献をみつけましたが理解は難しいです。

Speexのspeexencでもvadオプションがあり使うと音声ファイルの圧縮率は上がったのですが、話し声の抽出をするという使い方はわかりませんでした。

さらにSpeexで次世代版として勧められているOpus CodecのopusencではVADの記載すらみつけられませんでした。

SoXで複数ファイルに分割して連続録音するバッチファイル


SoX(Sound eXchange)で音声感知録音:その1でWindowsにSoXのインストールを行なった状態で行います。

まずは録音時だけに使う設定ファイルsettings.bat。

以下のバッチファイルはすべて同じフォルダに置いて使用します。
rem 音声ファイル形式の設定(拡張子で判別)。
set EXT=flac
rem 1ファイル当たりの録音区間の秒数の設定(秒)。
set DUR=60
rem ディレクトリ名の設定。スクリプトファイルのあるディレクトリの下層に作られる。
rem データディレクトリ名。
set DATA_DIR=sox_data
rem 改名後のデータをいれるディレクトリ名。
set RENAMED_DIR=renamed
rem sox.exeへのパスの設定。環境変数PATHに設定しているときは不要。
rem Windows7 64bitの場合。
path %PATH%;C:\Program Files (x86)\sox-14-4-2
rem WindowsXPの場合。
path %PATH%;C:\Program Files\sox-14-4-2
rem sox.exeにパスが通っているかの確認。
set T=0
for /F "delims= " %%t in ('sox --version') do set T=%%t
if %T%==0 (
  cls
  echo sox.exeが見つかりません。
  echo.
  pause
  exit
)
rem SoXの既定の音声デバイスの設定。
set AUDIODRIVER=waveaudio
rem このスクリプトがあるディレクトリ。
set SD=%CD%
SoXではエフェクトをかけると再エンコードされて非可逆圧縮では音質が落ちるためflacで録音しています。

録音区間が60秒未満になるとファイル名に秒までつけないといけないので1ファイルあたりの録音時間は60秒に設定しています。

次に録音を行うsox_rec.bat。
@echo off
rem 録音開始。
call settings.bat
if not exist "%SD%\%DATA_DIR%" (mkdir "%SD%\%DATA_DIR%")
cd %SD%\%DATA_DIR%
for /F "tokens=1,2 delims=." %%x in ('^(for /F "tokens=1,2,3 delims=:" %%p in ^('echo %DUR%.%date:/=%_%time: =0%'^) do @echo %%p%%q%%r^)') do sox -c 1 -d -p | sox -p "%%y_%%x_%%5n.%EXT%" trim 0 %%x : newfile : restart
これを実行するとbatファイルが置いてあるフォルダにsetting.batで設定したデータディレクトリsox_dataが作成されてそこに1分間ごとに区切られた音声ファイルが録音開始日時と録音間隔、連番をファイル名にしてモノラル音声で生成されていきます。

sox_rec.batを起動するsox_start.bat。

次のsox_end.batでsox_rec.batを終了させるためにウィンドウ名をつけて起動することにします。
@echo off
echo sox_rec.batをタイトルをつけて起動。
start "sox_rec.bat" sox_rec.bat
録音を終了させるsox_end.bat。

録音を終了するだけならCtrl+Cでもできますが、コマンドウィンドウも閉じるようにバッチファイルで終了します。
@if(0)==(0) echo off
cscript.exe //nologo /E:JScript "%~f0"
goto :EOF
@end
//SoXを停止させる
var sh = new ActiveXObject("WScript.Shell");
sh.AppActivate("sox_rec.bat");
sh.Sendkeys("^c");
WScript.Sleep(1000);
sh.Sendkeys("y{ENTER} ");
sh.Sendkeys("exit{ENTER} ");
バッチファイル:JScriptを埋め込んでDOSコマンドと変数をやりとりするの方法でWSH JScriptをバッチファイルに埋め込んでいます。

録音した音声ファイルのファイル名を録音区間に変更するバッチファイル


auto_rec_rename.jsをバッチファイルに埋め込むで作ったのを流用します。

sox_rename.bat。

sox_dataフォルダにある音声ファイルを録音区間がわかるファイル名に改名してsox_dataフォルダ内のrenamedフォルダに移動させます。
@if(0)==(0) echo off
call settings.bat
rem データディレクトリDATA_DIRのフルパス。
set DATA_DIR_P=%SD%\%DATA_DIR%
rem 改名後のデータをいれるディレクトリRENAMED_DIRのフルパス。
set RENAMED_DIR_P=%SD%\%DATA_DIR%\%RENAMED_DIR%
if not exist "%RENAMED_DIR_P%" (mkdir "%RENAMED_DIR_P%")
echo %DATA_DIR%フォルダにある%EXT%ファイルを改名して%RENAMED_DIR%フォルダに移動させています。
cscript.exe //nologo //E:JScript "%~f0"
GOTO :EOF
@end
var sh = new ActiveXObject("WScript.Shell");
var fso = new ActiveXObject("Scripting.FileSystemObject");
var ext = sh.ExpandEnvironmentStrings("%EXT%");//改名対象拡張子。
var data_dir = sh.ExpandEnvironmentStrings("%DATA_DIR_P%");//データディレクトリdata_dirフルパス。
var renamed_dir = sh.ExpandEnvironmentStrings("%RENAMED_DIR_P%");//改名後のデータをいれるディレクトリrenamed_dirのフルパス。
var fldr = fso.GetFolder(data_dir); //データフォルダオブジェクトの取得。
var fc = new Enumerator(fldr.Files); //ファイルコレクションをEnumeratorオブジェクトにして取得。
var f, new_name, j;
var f_arr = [];//処理対象ファイルオブジェクトを格納する配列。
//RegExpオブジェクトを作成。
var re = new RegExp("^(\\d{4})(\\d{2})(\\d{2})_(\\d{2})(\\d{2})(\\d{2})_(\\d+)_(\\d+)\\." + ext + "$");
//ファイルコレクションの各ファイルに対して。
for (; !fc.atEnd() ; fc.moveNext()) {
    f = fc.item(); //ファイルオブジェクトの取得。
    //ファイル名の判別
    if (re.test(f.Name)) {
        f_arr.push(f);//処理対象となるファイルオブジェクトを配列に格納。ファイル名昇順に取得できているはず。
    } else {
        f.Delete();//処理対象とならないファイルは削除。
    }
}
var fn = f_arr.length//処理するファイル数。
for (var i = 0; i < fn ; i++) {
    f = f_arr[i];//配列からファイルオブジェクトを取得。
    new_name = new_fname(re.exec(f.Name));//正規表現結果配列からファイル名を取得。
    try {
        j = 0;//「年月日_時分-年月日_時分」を示す要素番号。
        f.Move(renamed_dir + "\\" + new_name[j] + "." + ext);//改名して移動。
    } catch(e) {
        j = 1;//「年月日_時分秒-年月日_時分秒」を示す要素番号。
        f.Move(renamed_dir + "\\" + new_name[j] + "." + ext);//改名して移動。
    }
}
//正規表現結果配列から新しいファイル名を返す関数。戻り値は配列[年月日_時分-年月日_時分, 年月日_時分秒-年月日_時分秒]。
function new_fname(r) {
    //ファイル名から記録開始日時のDateオブジェクトを得る。1月を0として開始。
    var start_dt = new Date(r[1], r[2] - 1, r[3], r[4], r[5], r[6]);
    var int_sec = r[7] * 1000; //記録区間長(ミリ秒)
    var int_cnt = r[8]; //記録区間番号
    var start_ms = start_dt.getTime() + int_sec * (int_cnt - 1); //区間開始日時経過ミリ秒
    //経過ミリ秒を数字羅列に変換
    var end_n = date_num(start_ms + int_sec);//区間開始日時。
    var start_n = date_num(start_ms);//区間終了日時。
    var hhmm = start_n[0] + "-" + end_n[0];//新しいファイル名年月日_時分-年月日_時分。
    var hhmmss = start_n[0] + start_n[1] + "-" + end_n[0] + end_n[1];//新しいファイル名年月日_時分秒-年月日_時分秒。
    return [hhmm, hhmmss];
    //経過ミリ秒を年月日の数字羅列に変換する関数。戻り値は配列[年月日_時分, 秒]。
    function date_num(ms) {
        var dt = new Date();//Dateオブジェクトの生成。
        dt.setTime(ms);//経過ミリ秒からDateオブジェクトを取得。
        return [dt.getFullYear() + fm(dt.getMonth() + 1) + fm(dt.getDate()) + "_" + fm(dt.getHours()) + fm(dt.getMinutes()), fm(dt.getSeconds())];
        function fm(m) {
            return ("0" + m).slice(-2);//固定長2桁を返す。
        }
    }
}
60秒以内にsoxを再起動したときに同名のファイルがrenamedフォルダにあるときは秒までファイル名に付加されます。

録音区間を60秒未満に設定したときは37行目のtryの中のtryとcatchの中身を入れ換えるとよいかもしれません。

音声ファイルにvadエフェクトをかけたあとmp3で保存するバッチファイル


extract_from_renamed.bat

renamedフォルダにある音声ファイルにvadエフェクトをかけたあとextracted-vad-reverseフォルダにコピーします。

話し声を抽出した後3秒以下の音声ファイルは削除しています。

逆に20秒より長い音声ファイルは2分割して個別にvadエフェクトをかけて再結合し、再結合後の音声の長さが1秒以下のものは削除しています。

エンコードによる劣化と処理速度の短縮を目的に処理途中のファイルはwav形式で保存し最後にmp3に変換しています。

renamedフォルダにある元の音声ファイルはそのまま残しています。
@echo off
rem 2分割して再抽出する音声ファイルの長さ(秒)
set F_LEN=20
rem 記録しない音声ファイルの長さ(秒以下)。(再抽出後は1秒以下)
set SEC=3
rem 処理後のファイルの移動先ディレクトリ名。
set EXTRACTED_DIR=extracted-vad-reverse
call settings.bat
rem 処理後のファイルの移動先ディレクトリEXTRACTED_DIRのフルパス。
set EXTRACTED_DIR_P=%SD%\%DATA_DIR%\%EXTRACTED_DIR%
if not exist "%EXTRACTED_DIR_P%" (mkdir "%EXTRACTED_DIR_P%")
rem 再抽出の作業フォルダのフルパス。
set TMP_DIR_P=%SD%\%DATA_DIR%\%EXTRACTED_DIR%\temp
if not exist "%TMP_DIR_P%" (mkdir "%TMP_DIR_P%")
rem 改名後のデータディレクトリRENAMED_DIRのフルパス。
set RENAMED_DIR_P=%SD%\%DATA_DIR%\%RENAMED_DIR%
rem 処理したファイル数
set FCNT=0
rem 削除したファイル数
set DCNT=0
rem 再抽出したファイル数
set REEXT=0
rem 再抽出後に削除したファイル数
set DCNT2=0
cd "%EXTRACTED_DIR_P%"
setlocal enabledelayedexpansion
for %%f in ("%RENAMED_DIR_P%\*.%EXT%") do (
  set /A FCNT=!FCNT!+1
  rem 変換後のファイル名
  set F_NAME=%%~nf.wav
  cls
  echo 話声抽出後の!F_NAME!の長さが%SEC%秒以下なら削除します。
  echo 既削除数!DCNT!/!FCNT!(再抽出後既削除数!DCNT2!/!REEXT!^)
  sox "%%f" !F_NAME! vad reverse vad reverse
  rem sox --iで測定できないときのためにTをリセットする。
  set T=0
  for /F "tokens=1 delims=." %%t in ('sox --i -D !F_NAME!') do set T=%%t
  if !T! LEQ %SEC% (
    del /Q !F_NAME!
    set /A DCNT=!DCNT!+1
    echo !F_NAME!の長さが%SEC%秒以下なので削除しました。
  )
  rem %F_LEN%秒より長いファイルは半割して再抽出
  if !T! GTR %F_LEN% (
  set /A REEXT=!REEXT!+1
    echo !F_NAME!は%F_LEN%秒より長いので再抽出します
    set /A T2=!T!/2
    sox !F_NAME! "%TMP_DIR_P%\001.wav" trim 0 !T2! vad reverse vad reverse
    sox !F_NAME! "%TMP_DIR_P%\002.wav" trim !T2! vad reverse vad reverse
    del /Q !F_NAME!
    sox "%TMP_DIR_P%\001.wav" "%TMP_DIR_P%\002.wav" !F_NAME!
    del /Q "%TMP_DIR_P%\001.wav"
    del /Q "%TMP_DIR_P%\002.wav"
    for /F "tokens=1 delims=." %%t in ('sox --i -D !F_NAME!') do set T=%%t
    if !T! LEQ 1 (
      del /Q !F_NAME!
      set /A DCNT=!DCNT!+1
   set /A DCNT2=!DCNT2!+1
      echo 話声再抽出後の!F_NAME!の長さが1秒以下なので削除しました。
    )
  )
  rem wavファイルを変換。
  if exist !F_NAME! (
    sox !F_NAME! %%~nf.flac
    del /Q !F_NAME!
  )
)
rem 作業フォルダの削除
rd /S /Q "%TMP_DIR_P%"
endlocal
pause

バッチファイルを使って話し声を抽出する手順


sox_start.batをダブルクリックすると録音が始まります。

録音を終了するにはsox_end.batをダブルクリックします。

だいたい1分間で1.9MBぐらいの大きさのflacファイルになりました。


sox_rename.batをダブルクリックすると録音区間名に改名してrenamedフォルダに移動します。


最後にextract_from_renamed.batをダブルクリックします。

話声抽出後の20150807_2233-20150807_2234.wavの長さが3秒以下なら削除します。
既削除数8/16(再抽出後既削除数0/5)
0150807_2233-20150807_2234.wavの長さが3秒以下なので削除しました。
続行するには何かキーを押してください . . .


Windows7のエクスプローラで音声ファイルの長さをみると60秒より短くなっていることがわかりますがこの値は不正確です。(mp3で出力する場合はSoXの設定が必要です。)

参考にしたサイト


Speex: a free codec for free speech
話し声の圧縮ならmp3よりよさそうです。

Opus Codec
今後はSpeexよりもこちらが主流になるようです。

SoX, SoXI, soxformat マニュアルページ日本語訳
SoXを使えば音声ファイルについてさまざまな処理ができます。

次の関連記事:SoXで音声感知録音:その5 話し声を準リアルタイム録音

PR

0 件のコメント:

コメントを投稿