目次


sndpeek を使って話し手を識別する

電話会議やポッドキャスト、そしてライブ・メディア・イベントで誰が話しているのかをコンピューターに通知させる方法

Comments

声紋分析によって確実に個人を認証するのは複雑で、容易なことではありません。しかし、sndpeek といくつかのカスタム・アルゴリズムを使うことによって、複雑さを大幅に軽減させると同時に極めて有効な声紋突き合わせ構成を実現することができます。この記事では、特定の話者の個人用声紋ファイルを記録するように sndpeek を修正するためのツールとコードを紹介します。記録されたファイルのそれぞれがリアルタイムで入力される音声ストリームと照合されて、現在の話者として可能性の高い一致結果が表示されます。

要件

ハードウェア

音声入力の処理機能を備えたシステムが必要です。できれば外付けのマイクロフォンからの音声入力を処理できるシステムがあると理想的です。この記事のコードの開発およびテストには、1,800 MHz のプロセッサーと 1 GB の RAM を搭載した IBM® ThinkPad T42p を使いました。これより処理能力の低いシステムであっても、この記事に記載するコードを使用することはできます。主にリソースを消費するのは sndpeek であり、このプログラムは効率が高いためです。

ソフトウェア

音声処理およびマイクロフォンをサポートするオペレーティング・システムが必要です。現行バージョンの Mac OS X、Windows®、Linux® であればこの条件を満たします。音声に関する構成やトラブルシューティングはこの記事の範囲外ですが、このコードを Vector Linux Live CD でテストしてみるとよいかもしれません。この CD の中には、さまざまな種類の音声ハードウェアを機能設定するために必要なドライバーとコンポーネントの大部分が含まれています。また、ディスプレイ上で詳細がわかるように表示するには、ハードウェアの 3D アクセラレーションも必要です。

sndpeek アプリケーション (「参考文献」を参照) は、Windows、Mac OS X、および Linux で動作するように設計されています。これから説明する修正手順に進む前に、機能的な音声環境が整っていることを確認してください。

突き合わせ用音声ファイル・ライブラリーの構築

音声参照ファイルの要件

音声との正確な突き合わせを行うためには、現在発せられている音声と比較するものがなければなりません。該当する個人の話し声を長時間サンプリングすることで、信頼性の高い突き合わせ用テンプレートを作ることができます。推奨される時間の長さは、無言の時間、単語間の休止などを含めた通常の話し方で 5 分間です。

通常人間が話しているときには、咳払い、キーボードをたたく音、そして回線からの過度のノイズや周囲の雑音など、避けなければならない要素がさまざまにあります。発声による表現以外の音は基準となる声紋に悪影響を及ぼす可能性があるため、比較的雑音のない環境が必要です。

音声を記録して材料が用意できたら、任意の音声編集プログラム (Audacity など) を使って 1 つの個人別音声ファイルの形にします。一例として、この記事の開発で使用する個人別音声ファイルの材料として使ったのは、記録した電話会議と IBM developerWorks ポッドキャストの両方です。

注意する点として、必要なソース・データの量は突き合わせの対象の話者特有の違いに応じて増えることも、あるいは大幅に減ることもあります。音声のごく一部を示した図 1 で、この 2 人の話者の平均的な違いを見比べてください。このグラフはもう 1 つの優れた音声処理ツール、baudline を使ってリアルタイムで生成したものです。

図 1. baudline を使用した平均的な音声波形の例
baudline を使用した平均的な音声波形の例
baudline を使用した平均的な音声波形の例

sndpeek の修正

sndpeek のソース・コード (「参考文献」を参照) をダウンロードして解凍してください。声紋のスペクトル要素の平均を作成するには、sndpeek.cpp ファイルを修正する必要があります。まずは 284 行目にライブラリーのインクルードと変数の宣言を追加します。

リスト 1. ライブラリーのインクルード、変数の宣言
// for reading *.vertex* entries in the current directory
#include <dirent.h>

// voice matching function prototypes
void initialize_vertices( );
void build_match_number( int voices_index );

// for voiceprint matching
int g_voice_spectrum[200];          // human voice useful data in 0-199 range
int g_total_sample_size = 0;        // current size, or number of data points
float g_loudness_threshold = -0.8;  // what is a loud enough sample

次に、以下のコードを 1339 行目に追加してモニター・プロセスを開始します。

リスト 2. display_func 変数の宣言とサンプル・サイズ増分機能
    // simple vU meter, sample incrementer
    int loud_total = 0;
    g_total_sample_size++;

1519 行目の glVertex3f 関数コールの直下にリスト 3 のコードを追加して、モニター・プロセスを終了します。リスト 3 は、ウォーターフォール表示の最初の波形が処理されているときに、現行のデータ・ポイントがスペクトル全体のなかで 0 から 200 に収まっている場合にのみ現行のスペクトル・カウントを設定します。人間の音声に関して最も有用なデータ・ポイント (特に、電話回線で帯域が制限されている場合) の範囲は 0 から 200 までだからです。

リスト 3. 音声スペクトル・データの記録
                        // record spectrum of vertices for storing or analysis
                        //  only for the most significant portion of the 
                        //  current waveform
                        if( i== 0 && j < 200 )
                        {
                          if( pt->y > g_loudness_threshold )
                          {
                            g_voice_spectrum[j]++;
                            loud_total++; 
                          }
                        }// if current waveform and significant position

指定された音声ファイルの読み込みが完了した後に必要となるのは、作成された声紋に対応する保存済みスペクトル情報の出力です。まずは 720 行目で、リスト 4 に記載したコードをリスト 5 のコードに変更してください。

リスト 4. 当初のファイル終了セクション
            else
                memset( buffer, 0, 2 * buffer_size * sizeof(SAMPLE) );
リスト 5. WAV ファイル処理後の頂点ファイルの書き込みと終了
            else
            {
                memset( buffer, 0, 2 * buffer_size * sizeof(SAMPLE) );
            }

            fprintf( stdout, "Vertex freq. count in %s.vertex \n", g_filename);
            FILE *out_file;
            static char str[1024];
            sprintf( str, "%s.vertex", g_filename);

            out_file = fopen(str, "w");
            fprintf(out_file, "%2.0d\n", g_total_sample_size);
            fprintf(out_file, "%s\n",str);
            for( int i = 0; i < 200; i++ )
              fprintf( out_file, "%03d  %08d\n", i, g_voice_spectrum[i]);
            fclose(out_file);
            exit( 0 );

make linux-alsa によるビルドが成功すると、それからは sndpeek 'personVoice'.wav コマンドを使って好きなだけ頂点ファイルを作成することができます。それぞれの頂点ファイルは ''personVoice'wav.vertex という名前でカレント・ディレクトリーに作成されます。.vertex ファイルを直接編集して、話者の名前を判読しやすいものに変更しても構いません。

平均近似値を使った突き合わせアルゴリズム

方法

図 1 を見るとわかるように、人間が話しているときの声には、人それぞれに異なる平均周波数特性があります。この特性は強い音の成分を持つ言語では変わってくることもありますが、英語を話す人の音声は実際に発声されている単語によらず、非常によく似ていることが実証的証拠により示されています。以下に示す修正ではこの事実を利用し、*.vertex ファイルに保存されたすべての声紋のデータと、現行の波形との差を単純に計算します。

sndpeek の追加修正 - 突き合わせの実装

突き合わせ構成を仕上げるには、他にも設定しなければならない変数とデータ構造体があります。リスト 6 の内容を sndpeek.cpp の 296 行目に配置してください。

リスト 6. 突き合わせの変数の宣言
struct g_vprint
{ 
  char name[50];        // voice name
  int sample_size;      // number of data samples from vertex file
  int freq_count[200];  // spectrum data from vertex file
  int draw_frame[48];   // render memory
  int match_number;     // last 3 running average
  int average_match[3]; // last 3 data points
} ;

int g_total_voices = 0;        // number of vertex files read
int g_maximum_voices = 5;      // max first 5 vertex files in ./
g_vprint g_voices[5];
int g_average_match_count = 3; // running average of match number
int g_dev_threshold = 20;      // deviation between voiceprint and current

struct g_text_characteristics
{ 
  float x;
  float y;
  float r;
  float g;
  float b;
} ;

static g_text_characteristics g_text_attr[5];

上記の変数が配置されると、initialize_vertices 関数によって頂点データが適切な構造体にロードされます。続いてリスト 7 のコードを 399 行目に追加します。

リスト 7. initialize_vertices 関数
//-----------------------------------------------------------------------------
// Name: initialize_vertices
// Desc: load "voiceprint" data from *.vertex
//-----------------------------------------------------------------------------
void initialize_vertices()
{
  DIR           *current_directory;
  struct dirent *dir;
  current_directory = opendir(".");
  if (current_directory)
  {
    while ((dir = readdir(current_directory)) != NULL)
    { 
      if( strstr( dir->d_name, ".vertex" ) && g_total_voices < g_maximum_voices )
      { 
        FILE * in_file;
        char * line = NULL;
        size_t len = 0;
        ssize_t read;
        int line_pos = 0;

        in_file = fopen( dir->d_name, "r");
        if (in_file == NULL)
             exit(EXIT_FAILURE);

        // file format is sample size, file name, then data all on separate lines
        while ((read = getline(&line, &len, in_file)) != -1)
        { 
          if( line_pos == 0 )
          { 
            g_voices[g_total_voices].sample_size = atoi(line);
            // intialize structure variables
            g_voices[g_total_voices].match_number = -1;
            for( int j=0; j< g_average_match_count; j++ )
              g_voices[g_total_voices].average_match[j] = -1;
          }else if( line_pos == 1 )
          { 
            sprintf( g_voices[g_total_voices].name, "%s", line );
          }else
          { 
            // read numbers 0-200  frequency count
            static char temp_str[1024] ;
            g_voices[g_total_voices].freq_count[
              atoi( (strncpy(temp_str, line, 4))) ] =
                atoi( (strncpy(temp_str, line+5, 8)) );
          }
          line_pos++;
        }

        fclose(in_file);

        g_total_voices++;
      }// if vertex file
    }// while files left

    closedir(current_directory);
  }// if directory exists

}

initialize_vertices 関数はメイン・プログラム内で呼び出されなければならないので、メイン・サブルーチン内の597 行目に以下の関数コールを追加します。

リスト 8. メイン・プログラム内での頂点データのロードの呼び出し
    // load vertices if not building new ones
    if( !g_filename ) initialize_vertices();

リスト 9 は、データをデフォルト値に初期化し、一致結果のテキストをレンダリングする位置を指定するコードです。このコードを initialize_analysis 関数内の 955 行目に追加してください。

リスト 9. 分析データ構造体とテキスト表示属性の初期化
    // initialize the spectrum buckets for voice, text color and position attr.
    for( int i=0; i < 200; i++ )
      g_voice_spectrum[i]= 0;
      
    g_text_attr[0].x = -0.2f;
    g_text_attr[0].y = -0.35f;
    
    g_text_attr[1].x = 0.8f;
    g_text_attr[1].y = 0.35f;
    
    g_text_attr[2].x = 0.8f;
    g_text_attr[2].y = -0.35f;
    
    g_text_attr[3].x = 0.2f;
    g_text_attr[3].y = -0.35f;
    

    g_text_attr[0].r = 1.0f;
    g_text_attr[0].g = 0.0f;
    g_text_attr[0].b = 0.0f;
    g_text_attr[1].r = 0.0f;
    g_text_attr[1].g = 0.0f;
    g_text_attr[1].b = 1.0f;
    g_text_attr[2].r = 0.0f;
    g_text_attr[2].g = 1.0f;
    g_text_attr[2].b = 0.0f;
    g_text_attr[3].r = 0.01f;
    g_text_attr[3].g = 1.0f;
    g_text_attr[3].b = 0.0f;

最後に追加する関数は build_match_number です。リスト 10 の内容を既存の compute_log_function の下の 1449 行目に挿入してください。build_match_number の最初の部分は、記録された声紋スペクトルの現行の値と、突き合わせ対象のスペクトルの値との差が、許容の範囲にあるかどうかを調べ、範囲外であれば変数がインクリメントされます。このようにして現行のサンプルすべての差が計算されます。使用可能なデータ・ポイントが 3 つ以上あることが確認された後、最新の 3 回の記録の平均に基づいて現行の一致数が設定されます。精度を高めるには、必要なサンプル数または平均を取るデータ・ポイントの数を増やすことを検討してください。

リスト 10. build_match_number 関数
//-----------------------------------------------------------------------------
// Name: build_match_number
// Desc: compute the current deviation and average of last 3 deviations
//-----------------------------------------------------------------------------
void build_match_number(int voices_index)
{ 

  int total_dev = 0;
  int temp_match = 0;

  for( int i=0; i < 200; i++ )
  {  
    int orig =  g_voices[voices_index].freq_count[i] /
                  (g_voices[voices_index].sample_size/100);
    if( abs( orig - g_voice_spectrum[i]) >= g_dev_threshold)
      total_dev ++;

  }// for each spectrum frequency count

  // walk the average back in time
  for( int i=2; i > 0; i-- )
  { 
    g_voices[voices_index].average_match[i] =
      g_voices[voices_index].average_match[i-1];
    if( g_voices[voices_index].average_match[i] == -1 )
      temp_match = -1;
  }
  g_voices[voices_index].average_match[0] = total_dev;

  // if all 3 historical values have been recorded
  if( temp_match != -1 )
  { 
    g_voices[voices_index].match_number =
      (g_voices[voices_index].average_match[0] +
        g_voices[voices_index].average_match[1] +
        g_voices[voices_index].average_match[2]) / 3;
  }
  
}

音声エントリーの突き合わせは、いよいよ完成に近づいてきました。最後のステップとして、1712 行目のすぐ下にリスト 11 の内容を追加し、突き合わせを実行します。最新の一致結果は、レンダリングの状態とは関係なく決定されることに注意してください。これは、十分な一致データが得られない場合、テキストのウォーターフォール表示時に遡って正確なレンダリングを行うためです。データのサンプル全体がすでに読み取られている場合には、現行の一致状態が build_match_number サブルーチンによって作成され、最も一致する声紋が stdout に出力されるとともにレンダリング対象として設定されます。その後、次の実行に備えてデータが再初期化されます。

データのサンプルがまだ完全に読み取られていない場合、最新のテキストのみが画面に出力されます。回線での音量が十分であるいうことは、通常は誰かが話しているということです。この場合にはレンダリング変数が設定されるため、別のサンプルの収集中でも確実に最新の一致結果がレンダリングされ続けることになります。

リスト 11. 突き合わせの主要処理
        // run the voice match if a filename is not specified
        if( !g_filename )
        {
          
          // compute most recent match
          int lowestIndex = 0;
          for( int vi=0; vi < g_total_voices; vi++ )
          { 
            if( g_voices[vi].match_number < g_voices[lowestIndex].match_number )
              lowestIndex = vi;
          }// for voice index vi
          
          if( g_total_sample_size == 100 )
          { 
            g_total_sample_size = 0;
            for( int j =0; j < g_total_voices; j++ )
              build_match_number( j );
            
            // decide if first frame is renderable
            if( g_voices[lowestIndex].match_number != -1 &&
                  g_voices[lowestIndex].match_number < 20 )
            { 
              fprintf(stdout, "%d %s", 
                        g_voices[lowestIndex].match_number,
                        g_voices[lowestIndex].name );
              fflush(stdout);
              g_voices[lowestIndex].draw_frame[0] = 1;
            }
            
            // reset the current spectrum
            for( int i=0; i < 200; i++ )
              g_voice_spectrum[i]= 0;
          
          }else
          { 
            // fill in render frame if virtual vU meter active
            if( loud_total > 50 && g_voices[lowestIndex].match_number < 20 )
            { 
              if(  g_voices[lowestIndex].match_number != -1 )
                g_voices[lowestIndex].draw_frame[0] =1;
            
            }//if enough signal
          
          }// if sample size reached
          
          // move frames back in time
          for( int vi = 0; vi < g_total_voices; vi++ )
          { 
            for( i= (g_depth-1); i > 0; i-- )
              g_voices[vi].draw_frame[i] = g_voices[vi].draw_frame[i-1];
            g_voices[vi].draw_frame[i] = 0;
          
          }//shift back in time
        
        }// if not a g_filename

一致結果の表示

突き合わせが完了してレンダリング状態が更新されたら、後は実際に一致結果のテキストを画面上に描画するだけです。sndpeek の通常の表示に合わせるため、テキストを sndpeek 独自のウォーターフォール表示でレンダリングします。リスト 12 に示すレンダリング・プロセスでは、一致した声紋に応じたカスタム・カラーを使用しています。このリスト 12 のコードは、sndpeek.cpp の 1893 行目に挿入してください。

リスト 12. 一致結果名のレンダリング
        // draw the renderable voice match text
        if( !g_filename ) 
        {       
          for( int vi=0; vi < g_total_voices; vi++ )
          { 

            for( i=0; i < g_depth; i++ )
            {
            
              if( g_voices[vi].draw_frame[i] == 1 )
              {
                fval = (g_depth -  i) / (float)(g_depth);
                fval = fval /10;
                sprintf( str, g_voices[vi].name );
              
                if( vi == 0 )
                {
                  glColor3f( g_text_attr[vi].r * fval,
                              g_text_attr[vi].g, g_text_attr[vi].b );
                }else if( vi == 1 )
                {
                  glColor3f( g_text_attr[vi].r, 
                              g_text_attr[vi].g, g_text_attr[vi].b * fval);
                }else if( vi == 2 )
                {
                  glColor3f( g_text_attr[vi].r, 
                              g_text_attr[vi].g * fval , g_text_attr[vi].b);
                }else if( vi == 3 )
                {
                  glColor3f( g_text_attr[vi].r,
                              g_text_attr[vi].g * fval, g_text_attr[vi].b);
                }
                draw_string( g_text_attr[vi].x, g_text_attr[vi].y,
                              -i, str, 0.5f );
              }// draw frame check

            }//for depth i

          }//for voice index vi
        }

使用方法

make linux-alsa; sndpeek を実行して、これまでに行った変更をテストしてください。ビルド・プロセスで何もエラーが発生しなければ、通常の sndpeek ウィンドウに通常の波形とリサジュー図が表示されます。このプログラムをテストする最も簡単な方法の 1 つは、単独の話者のソース・ファイルを元に 10 秒間から 30 秒間のさまざまな長さの音声を構成することです。声紋ファイルのサイズや有効な話者をはじめ、各種の要因によって、上記の変更に実装したオプションのいくつかは微調整する必要があるかもしれません。いろいろな人々が話し始めると、話者に対応する名前が sndpeek の一部として表示されると同時に、一致率がテキストで stdout に出力されます。この表示例を見るには、「参考文献」セクションに記載したデモ・ビデオのリンクにアクセスしてください。

まとめとその他の例

デモ・ビデオを見るとわかりますが、結果は 100 パーセント正確ではないものの、話し手を識別する上で非常に役に立ちます。この記事で説明したツールと sndpeek の修正によって、音声記録から声紋に基づいて個人を識別できるようになります。

今度の電話会議ではリアルタイム音声モニターを接続して、誰がいつ話しているかをさらにわかりやすくしてみてはいかがですか。あるいは、話者がチームの新メンバーであることを自動的に識別するウィジェットを作成して Web 会議のページに追加するという方法、さまざまなテレビ番組で音声を追跡し、特定のニュース・キャスターがお気に入りの番組に登場したときにわかるようにするという方法もあります。さらに発信番号の枠を超えて、電話をかけてきた番号だけでなく、誰がメッセージを残したかを特定することもできます。


ダウンロード可能なリソース


関連トピック

  • プリンストン大学が sndpeek プログラムをホストしています。
  • YouTube.com のデモ・ビデオを見てください。このビデオは直接ダウンロードすることもできます。
  • sndpeek を使ったプロジェクトとしては、developerWorks の記事「仕事中に口笛を吹いて、コンピューターのコマンドを実行させる」もあります。
  • Audacity による音声ファイルの詳細を学んでください。
  • baudline でリアルタイム音声属性をモニターする方法を調べてください。
  • オープンソース技術を使用して開発し、IBM の製品と併用するときに役立つ広範囲のハウツー情報、ツール、およびプロジェクト・アップデートについては、developerWorks Open source ゾーンを参照してください。
  • sndpeek をダウンロードしてください。このリアルタイム音声視覚化プログラムはプリンストン大学でホストされています。
  • IBM 製品の評価版をダウンロードして、DB2®、Lotus®、Rational®、Tivoli®、および WebSphere® のアプリケーション開発ツールとミドルウェア製品を使ってみてください。

コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Open source
ArticleID=308789
ArticleTitle=sndpeek を使って話し手を識別する
publish-date=04152008