使用 sndpeek 识别说话者

来源:互联网 发布:淘宝主图有什么要求 编辑:程序博客网 时间:2024/05/05 20:15

使用 sndpeek 识别说话者

让计算机帮助您识别电话会议、podcast 和新闻直播中的说话者

 

 

级别: 中级

Nathan Harrington (harrington.nathan@gmail.com), 程序员, IBM 

2008 5 29

使用 sndpeek 和自定义算法在预先录制的库中寻找匹配的语音。创建应用程序帮助您识别电话会议、podcast 和新闻直播中的说话者。构建基本的辅助程序以帮助有听力障碍的人士在带宽有限的环境中识别说话者。

通过声波纹实现可靠的身份验证十分复杂和困难。但是,sndpeek 和一些自定义算法可以提供一种声波纹匹配配置,这种配置适当降低了复杂度,同时保留了较高程度的有效性。本文将演示修改 sndpeek 所需的工具和代码,从而针对给定讲话者录制个人声波纹文件。随后将把所有这些文件与传入的实时音频流相比较,从而提供当前说话者的最佳猜测匹配和可视化。

要求

硬件

需要系统能够处理可能来自外部麦克风的声音输入。本文中的代码是在支持 1,800-MHz 处理器和 1 GB RAM IBM® ThinkPad T42p 上开发和测试的。性能稍差一些的系统应当能够使用本文提供的代码,因为 sndpeek 是主要的资源消耗者并且是一个高效的程序。

软件

需要可支持声音处理和麦克风的操作系统,Mac OS XWindows® Linux® 的当前版本都可以。虽然声音配置和故障排除超出了本文的范围,但是在 Vector Linux Live CD 上测试这段代码可能十分有用,因为 Vector Linux Live CD 拥有在各种声音硬件上实现有效设置所需的大部分驱动程序和组件。还需要用于显示的硬件 3-D 加速功能。

sndpeek 应用程序(请参阅 参考资料)被设计为在 WindowsMac OS X Linux 上工作。在继续处理本文所述的修改之前,请确保拥有运行正常的音频环境。

 


回页首

 

构建用于匹配的声音文件库

语音参考文件要求

为了精确匹配语音,要求具有可以与当前声音相比较的内容。需要有持续时间较长的声音示例,从而以此作为匹配对象创建可靠的模板。示例长度最好为 5 分钟左右的普通讲话,包括沉默、单词之间的停顿等。

应当避免混入很多其他交谈特性,例如咳嗽、键盘噼啪响声以及过度的电话线或环境噪声。需要使用噪声相对较小的环境,因为声音表达以外的任何声音都会对参考声波纹产生不利影响。

需要使用您最喜爱的音频编辑程序(例如 Audacity)把可用的已录制语音材料连接成单语音(single-voice)音频文件。例如,我使用了录制的电话会议和 IBM developerWorks podcast 作为撰写这篇文章时使用的单语音音频文件的原始材料。

注意,您可能需要更多或非常少的源数据,这取决于要匹配的说话者的自身差异。考虑图 1 和一小部分语音之间的平均差异。该图形是使用另一个优秀的音频处理工具 baudline 实时生成的。


1. 使用 baudline 得到的平均语音波形示例

修改 sndpeek

下载并解压缩 sndpeek 源代码(请参阅 参考资料)。构建平均声波纹的频谱组件要求修改 sndpeek.cpp 文件。首先在第 284 行开始添加一些库包含(include)语句和变量声明。


清单 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++;

 

把清单 3 中所示的代码直接添加到第 1519 行中的 glVertex3f 函数调用之下,完成监视过程。如果谱阵图(waterfall)中只有第一个波形正在处理中,并且当前数据点处于整个频谱图中从 0 200 的位置,清单 3 将设置当前的频谱计数。人类语音的最有用数据点(尤其是在带宽有限的电话线中)是在 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 文件后写入 vertex 文件并退出

               

            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 命令构建任意数量的 vertex 文件。将在名为 'personVoice'wav.vertex 的当前目录中创建所有 vertex 文件。您可能会发现直接编辑 .vertex 文件并把说话者的姓名更改为更明显的内容会非常有用。

 


回页首

 

使用均值逼近匹配算法

策略

如图 1 所示,人类语音在说话时将生成独特的平均特征频率。尽管针对不同语言,音调会影响这个特性,但是经验表明说英语的人不管实际上说了什么单词,其声音都极为相似。因此下面的修改将利用这个事实,创建当前波形与所有存储在 *.vertex 文件中的声波纹之间的偏差数量的简单计数。

进一步修改 sndpeek —— 匹配实现

要完成匹配,我们需要设置一些附加变量和数据结构。从 sndpeek.cpp 的第 296 行开始,放入清单 6 中的内容。


清单 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 函数将把 vertex 数据装入相应结构。从第 399 行开始,添加请单 7 中的代码。


清单 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 行的 main 子程序中。


清单 8. 在主程序调用中载入 vertices

               

    // load vertices if not building new ones

    if( !g_filename ) initialize_vertices();

 

下面显示了如何把数据初始化为默认值并指定要渲染的匹配文本的位置。从第 955 行开始,在 initialize_analysis 函数内,添加清单 9 中的代码。


清单 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 的内容放到第 1449 行,位于已存在的 compute_log_function 之下。build_match_number 的第一部分将查看录制的声波纹频谱中的当前值是否超出可接受的偏差。如果是,则对变量加 1 以计算当前样例的所有偏差。在确保至少有三个数据点可用后,将根据最新的三条记录的平均值设置当前匹配数。为了更加精确,请考虑增加必要的样例数或者数据点数以进行平均。


清单 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 可视化规则维护相似性是通过在其自己的谱阵图显示中渲染文本实现的。清单 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 测试更改。如果构建过程中没有错误,则应当会看到显示正常谱阵图和 lissjous 可视化的普通 sndpeek 窗口。测试程序的最简单方法之一是从各个单一演讲者的源文件中抽取 10 秒到 30 秒间隔的声音片段,然后拼接在一起。根据声波纹文件的大小、涉及的说话者和各种其他因素,您可能需要调整上述更改中实现的一些选项。每个人开始说话时,您应当会看到作为 sndpeek 可视化的一部分显示的相关名字,以及输出到 stdout 的文本百分比匹配。要获得演示示例,请参阅 参考资料 小节中的演示视频链接。

 


回页首

 

结束语

正如可以在演示视频中看到的那样,结果并不是 100% 精确,但是却可以有效地识别说话者。通过工具和本文中对 sndpeek 的修改,您可以开始根据声波纹从音频记录中识别不同的人。

考虑把实时语音监视器挂接到下一次电话会议中,从而可以清楚地了解谁在何时讲话。在 Web 会议页面中创建一个附加小部件以帮助团队新成员自动识别说话者。跟踪电视节目中的某些语音,这样在播放您最喜爱的电视节目时就会立刻知道。超越来电显示功能,识别给您留言的人是谁,而不仅仅是显示来电号码。

 


回页首

 

下载

描述

名字

大小

下载方法

样例代码

os-sndpeek.voicePrint_0.1.zip

15KB

HTTP

 

关于下载方法的信息

 

 

 

参考资料

学习

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文
  • sndpeek 程序由普林斯顿大学托管。
  • YouTube.com 上查看演示视频或 直接下载
  • developerWorks 文章 工作中用声音控制计算机上命令的执行介绍了使用 sndpeek 的另一个项目。
  • 通过 Audacity 了解音频文件的更多信息。
  • 了解如何使用 baudline 来监视实时音频属性。
  • 收听针对软件开发人员的有趣访谈和讨论,一定要访问 developerWorks podcast
  • 随时关注 developerWorks 技术事件和网络广播
  • 查阅最近将在全球举办的面向 IBM 开放源码开发人员的研讨会、交易展览、网络广播和其他 活动
  • 访问 developerWorks 开放源码专区,获得丰富的 how-to 信息、工具和项目更新,帮助您用开放源码技术进行开发,并与 IBM 产品结合使用。
  • 查看免费的 developerWorks On demand demo 观看并了解 IBM 及开源技术和产品功能。


获得产品和技术

  • 下载 sndpeek,它是由普林斯顿大学托管的实时音频可视化程序。
  • 使用 IBM 试用软件 改进您的下一个开发项目,这些软件可以通过下载或从 DVD 中获得。
  • 下载 IBM 产品评估版,并开始使用 DB2®Lotus®Rational®Tivoli® WebSphere® 的应用程序开发工具和中间件产品。


讨论

  • 参与 developerWorks blog 并加入 developerWorks 社区。

 

关于作者

 

Nathan Harrington IBM 的一位程序员,目前主要从事 Linux 和资源定位技术方面的工作。

 

原创粉丝点击