| 级别: 中级 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 X、Windows® 和 Linux® 的当前版本都可以。虽然声音配置和故障排除超出了本文的范围,但是在 Vector Linux Live CD 上测试这段代码可能十分有用,因为 Vector Linux Live CD 拥有在各种声音硬件上实现有效设置所需的大部分驱动程序和组件。还需要用于显示的硬件 3-D 加速功能。 sndpeek 应用程序(请参阅 参考资料)被设计为在 Windows、Mac 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 |
参考资料 学习
获得产品和技术
- 下载 sndpeek,它是由普林斯顿大学托管的实时音频可视化程序。
- 使用 IBM 试用软件 改进您的下一个开发项目,这些软件可以通过下载或从 DVD 中获得。
- 下载 IBM 产品评估版,并开始使用 DB2®、Lotus®、Rational®、Tivoli® 和 WebSphere® 的应用程序开发工具和中间件产品。
讨论
关于作者
| | | Nathan Harrington 是 IBM 的一位程序员,目前主要从事 Linux 和资源定位技术方面的工作。 | |