Speex编解码器手册 1.2Beta3版
Speex编解码器(http://www.speex.org/)的存在是因为需要一款开源且免软件专利使用费的语音编解码器,这是任何开源软件可用的必要条件。本质上来说,Speex相对于语音正如Vorbis(注:免费音乐格式)相对于音频/音乐。不像许多其他语音编解码器,Speex不是为移动电话而设计,而是为分封网络(packet network)和网络电话(VoIP)而设计的。所以当然支持基于文件的压缩。
Speex设计灵活,支持多种不同的语音质量和比特率。对高质量语音的支持也就意味着Speex不仅能编码窄带语音(电话语音质量,8kHz采样率),也能编码宽带语音(16kHz采样率)。
为VoIP而不是移动电话而设计意味着Speex对丢失的数据包鲁棒,但对损坏的数据包不鲁棒。这基于在VoIP中数据包要么完整到达要么不能到达这一假设的。因为Speex针对于多种设备,所以它复杂性适度(可调节)并且占用较少内存。
考虑这些设计目标,我们选用CELP作为编码技术。其中一个主要原因是CELP很早就被证明在低比特率(如4.8kbps的DoD CELP)和高比特率(如16kbps的G.728)都能稳定可靠工作。
正如许多开源项目一样,有许多方法可以获取Speex的帮助,包括:
1)此文档
2)Speex网站(http://www.speex.org/)上的其他文档
3)邮件列表:在上于Speex相关主题的讨论(不仅仅针对开发者)
4)IRC:在irc.freenode.net上的主频道是#speex。注意由于时间的不同,可能需要一些时间联系某人,所以请耐心等待。
5)私下给作者邮箱发邮件,但仅限于你不想被公开讨论的私人的或精妙的主题。
在寻求帮助之前(邮件列表或IRC),请最好先阅读本手册(好吧,如果你看到这了,那么这是一个好迹象)。在邮件中讨论已经在手册中具体说明的问题一般是很不礼貌的。另一方面,如果是询问在手册中没有说清楚的问题则是很好地(鼓励)。本手册没有包含Speex的方方面面,所以鼓励每个人问问题,发评论,要求增加的新特性,或简单的让我们知道Speex是如何被使用的。
再说些附加的关于邮件列表的指引。在你向列表中报告Speex中的bug之前,强烈建议(如果可能)先测试下这些bug在使用speexenc和speexdec命令行工具(见第四章)下是否可复现。基于第三方代码(3rd party code)发布的bug不仅难发现,而且经常是由于与Speex无关的错误引起的。
此文档分为如下几章。第二章描述不同的Speex特征并定义此文档中使用的许多术语,第四章描述Speex提供的标准命令行工具,第五章包括用libspeex API编程的具体说明,第七章有Speex相关的信息和标准。
最后三章描述Speex中所使用的算法。阅读这部分需要信号处理相关知识,但对于仅仅使用Speex则不需要。这部分是为那些对于想了解Speex如何工作或想基于Speex研究的人而准备。第八章解释了CELP后的一般思想,第九和第十章是Speex的一些特性。
这张具体描述Speex和它的特征。
在介绍Speex特征之前,先介绍在语音编码中的一些概念,帮助读者更好地理解手册的剩余部分。尽管其中一些事语音/音频处理中的一般概念,但有些也是Speex中特有的。
采样率
采样率是每秒钟采集到的信号样本数,单位是Hertz(Hz)。如果采样率是Fs kHz,则所采样信号的最高频率为Fs/2 kHz(Fs/2被称为奈奎斯特频率)。这是信号处理中的一个基本性质,由采样定理描述。Speex为三种不同的采样率而设计:8kHz,16kHz和32kHz,分别对应窄带,宽带和超宽带。
比特率
在对语音信号编码时,比特率定义为单位时间内的比特数,单位是比特每秒(bps)或通常的千比特每秒(kbps)。注意千比特每秒(kbps)和千字节每秒(kBps)的区别。
质量(可变)
Speex是有损的编解码器,意味着压缩率以输入语音信号的保真度为代价。不像一些其他的语音编解码器,Speex可以控制质量和比特率之间的折中。Speex编码处理大多数时间由一个范围在0到10之间的质量参数控制。在不变比特率(CBR)中,质量参数是一个整数,在可变比特率(VBR)中,质量参数是一个浮点数。
复杂度(可变)
Speex允许编码器拥有可变的复杂度。这通过一个范围在1到10之间的整数控制搜索的执行来实现,类似于gzip和bzip2压缩工具的-1到-9选项。正常使用情况下,复杂度为1的噪声等级比复杂度为10的噪声等级高1到2个dB,但复杂度为10的CPU要求比复杂度为1的高5倍。实际应用中,最好的折中是复杂度2到4,但在编码非语音声音如DTMF声调时更高的复杂度经常被用到。
可变比特率(VBR)
可变比特率(VBR)允许编解码器自适应的根据待编码音频的“难度”动态地改变比特率。在Speex例子中,像元音和高能瞬态变化的声音需要高比特率以获得好的质量,但是摩擦音(如s,f)用低比特率就能充分编码。由于这一原因,VBR在相同的质量下能获得更低的比特率,或在不变比特率下获得更好的质量。除了这些优点,VBR有两个主要缺点:第一,在指定质量情况下,无法保证最终的平均比特率;第二,在一些如网络电话(VoIP)这样的实时应用中,依赖于最大比特率,这在通信信道中必须足够低。
平均比特率(ABR)
平均比特率解决了VBR中的第一个问题,它动态地调整VBR质量以获得指定的比特率。因为质量和比特率是实时调整的(开环的),ABR的全局质量比正好达到目标平均比特率的VBR编码质量稍微差些。
声音活动检测(VAD)
当被激活时,VAD检测待编码的音频是语音还是无声/背景噪声。VAD在VBR编码中默认激活,所以这一选项仅对非VBR编码有效。在这种情况下,Speex检测出非语言段并仅使用足够复现背景噪声的比特率进行编码,这叫“柔化噪音生成”(CNG)。
断续传输(DTX)
断续传输室VAD/VBR的附加操作,当背景噪声平稳时会完全停止传输。在基于文件的操作中,既然我们无法停止写文件,则对于平稳背景噪声帧仅用5比特(对应250bps)。
知觉增强
知觉增强是解码器的一部分,当被启用时,能减少编解码过程中产生的噪声或失真的知觉。在大多数情况下,知觉增强会带来声音客观上的偏离(如仅考虑SNR),但最后仍听起来更好(主管增强)。
等待时间和算法延时
每一个语音编解码器在传输中都会引入延时。对于Speex,延时等于帧长加上处理每一帧需要前几帧的数量。在窄带操作中(8kHz),延时为30ms;在宽带操作中(16kHz)延时为34ms,这不包括编解码帧时的CPU时间。
Speex的主要特性有:
1)免费软件/开源,免专利费和版税
2)利用嵌入比特流集成了窄带和宽带
3)大范围可用比特率(从2.15kbps到44kbps)
4)动态比特率转换(AMR)和可变比特率操作(VBR)
5)声音活动检测(VAD,与VBR集成)和断续传输(DTX)
6)可变复杂度
7)嵌入宽带结构(可伸缩采样率)
8)32kHz超宽带采样率
9)强度立体声编码选项
10)定点实现
这是在1.1.x版本中已经介绍的预处理器模块。预处理器在对音频编码前对音频进行预处理,有三个主要功能:
1)噪声抑制
2)自动增益控制·(AGC)
3)声音活动检测(VAD)
解码器能减少输入信号的背景噪声。预处理能得到高质量语音,而不管降噪后的信号是否通过Speex编码。然而,先降噪再进行编解码是有好处的,因为Speex编解码器通常会对噪声输入同样进行编解码,这将会扩大噪声,而降噪能大大减少这一影响。
自动增益控制(AGC)是为了处理录音音量在不同设置里有很大差别这一问题,AGC将会调整信号音量到参考音量大小。这对网络电话时非常有用的,因为这样就不需要手工调节麦克风增益了。第二个好处是若将麦克风增益设置为较低的等级,更容易避免削波。
预处理器提供的声音活动检测(VAD)比编解码器中直接提供的VAD更先进。
当通过UDP(User DatagramProtocal,用户数据报协议)或RTP(Real TimeProtocal,实时传输协议)传输声音(或其他任何内容)时,数据包可能丢失,不同延时到达,甚至乱序。抖动缓冲器的作用是对数据包进行重排序并保存在足够长的buffer(但有一定限度)里,然后将数据包发送去解码。
在任何免提式通信系统中(图2.1),远端的语音在本地扬声器播放时,经过在房间里传播后又会被麦克风录音。如果将麦克风录音直接又发送到远端,则远端的用户将会听到他自己的回声。声学回声消除器就是为了在将录音发送到远端前消除声学回声。注意回声消除器意味着提高了远端接收的语音质量。
图2.1 声学回声模型
在一些情况下,转换音频的采样率是很有用的,原因有很多,如能混合不同采样率流,能支持声卡不支持的采样率,能转码等。这也是为什么现在重采样器也是Speex项目的一部分的原因。重采样器能在任意采样率间进行转换(采样率必须是有理数),能控制质量和复杂度的折中。
在UNIX/Linux或任何其他支持autoconf的平台(如Win32/cygwin)下编译Speex很容易,只需输入
% https://blog.csdn.net/dreamsparkc/article/details/configure [options]
% make
% make install
Speex配置脚本支持的选项有:
-prefix=<path> 指定安装Speex的基本路径(如/usr)
-enable-shared/-disable-shared 是否编译共享库
-enable-static/-disable-static 是否编译静态库
-disable-wideband 禁用Speex的宽带部分(典型地为了节省空间)
-enable-valgrind 为调试启用valgrind(一款用于内存调试内存泄漏检测以及性能分析的软件开发工具)额外的匹配记录(默认不启用)
-enable-sse 启用SSE(StreamingSimd Extensions,指令集)指令(仅支持x86浮点数)
-enable-fixed-point 为没有浮点单元(FPU)的处理器编译Speex
-enable-arm4-asm 启用ARMv4结构配置特性(仅支持gcc)
-enable-arm5e-asm 启用ARMv5E结构配置特性(仅支持gcc)
-enable-fixed-poin-debug 仅调试定点代码(非常慢)
-enable-epic-48k 启用特殊(不兼容)的4.8kbps窄带模型(在1.1.x和1.2beta版本中已损坏)
-enable-ti-c55x 启用对TI C5x系列的支持
-enable-balckfin-asm 启用Blackfin DSP结构配置特性(仅支持gcc)
-enable-vorbis-psycho 使编码器使用Vorbis心理音响学模型,这处于试验阶段,将来可能取消
Speex可在包括浮点单元和定点单元的许多平台下编译和工作。通常,任何能计算两个16位带符号数乘法(结果是32位)和能在充分地时钟频率下运行(依赖结构)的平台都能运行Speex。已知的Speex的工作平台有(可能还有其他):
1)x86 & x86-64 2)Power 3)SPARC 4)ARM 5)Blackfin 6)Coldfire(68k系列) 7)TI C54xx & C55xx 8)TI C6xxx 9)TriMedia(试验)
已知的Speex工作的操作系统有(可能还有其他):
1)Linux 2)uClinux 3)MacOS X 4)BSD 5)其他UNIX/POSIX变体 6)Symbian
源代码目录中README.xxx文件包含在某个平台或操作系统下编译的额外信息。
以下是在新平台或目前已有平台下配置端口或优化Speex要考虑的一些注意事项。
3.2.1 CPU优化
最影响Speex的CPU使用的是浮点编译还是定点编译。如果你的CPU/DSP没有浮点单元FPU,则定点编译将更快。如果有FPU,则需要测试以下浮点编译和定点编译哪个快。在x86平台中,浮点编译一般更快。为了对Speex定点编译,你需要输入-fixed-point来配置脚本或为编译器定义FIXED-POINT宏。对于1.2beta3版本,现在能禁用浮点兼容的API,这意味着你的代码无需浮点仿真库即可链接,为了达到这一目的,可以通过-disable-float-api配置或定义DISABLE_FLOAT_API宏。等到VBR特性移植到定点,你将需要通过-disable-vbr配置或定义DISABLE_VBR宏。
在一些DSP平台中需要核实的一些事有:1)确定高速缓存被设置为回写式模型;2)如果芯片用SRAM代替高速缓存,确认大量代码处于SRAM而不是RAM中。
如果你要编写汇编,以下函数通常是你必须最先优化的:1)filter_mem16() 2)iir_mem16() 3)vq_nbest() 4)pitch_xcorr() 5)interp_pitch()。滤波器函数filter_mem16()和iir_mem16()应用在转置直接II型(DF2T)。然而对基于乘法累计(MAC)的平台,DF2T需要频繁重装蓄电池,这将使代码非常慢。对于这些平台(如Blackfin和Coldfire),更好的方法是应用这些函数为直接I型(DF1),这在MAC时更容易再装。然而如果这么做,需要确认DF1实现仍能得到像原始DF2T实现一样的滤波器值,这很必要,因为滤波器是时变的并且必须在任何编码器或解码器上直接计算相同的值(不算机器舍入)。
3.2.2 内存优化
内存优化主要是在一些小的嵌入式平台中需要考虑。对于PC,Speex已经足够小,无需内存优化。有几种方法的减少Speex的内存使用,包括代码大小和数据大小。对于优化代码大小,关键是移除你不需要的特性。以下是禁用一些你不需要的特性的例子:1)宽带支持(-disable-wideband);2)立体声支持(移除stereo.c);3)VBR支持(-disable-vbr或DISABLE_VBR);4)你所使用的比特率不要静态码本(*_table.c文件)
Speex也有一些配置临时数组的方法。当使用适当支持C99的编译器时(到2007年,微软编译器不支持,gcc支持),最好定义VAR_ARRAYS,以利用C99的可变大小数组这一特性。下一个最好做的是定义USE_ALLOCA以使Speex能用函数alloca()分配临时数组。注意在许多系统中,alloca()可能无效。如果没有定义VAR_ARRAYS和USE_ALLOCA,则Speex将利用它的内部分配分配一大块”可用空间”,这样的主要缺点是浪费。需要为最坏情况(最坏比特率,最高复杂度设置...)分配足够的栈,默认的,在多个编码器/解码器状态之间内存不共享。最后,如果只能“手动”分配,也有一些方法可以提高。通过复写os_support.h中的speex_alloc_scratch(),可以使所有状态总是返回同一块内存(此时必须特别注意线程)。除此之外,通过重新定义NB_ENC_STACK和NB_DEC_STACKA(宽带类似),可以为提前知道的情况分配内存,此时,需要衡量使用的采样率、比特率和复杂度级别所需的内存数量。
Speex最基本的是命令行编码器(speexenc)和解码器(speexdec)。这些工具产生并读取封装在Ogg容器内的Speex文件。尽管能在任意容器内封装Speex,但Ogg是文件命令行容器。这一部分介绍如何在Ogg中为Speex文件使用命令行工具。
speexenc的作用是从未加工的PCM文件或wave文件生成Speex文件。可通过如下调用:
speexenc[options] input_file output_file
input_file或output_file的‘-’值分别对应标准输入和标准输出。有效的选项有:
-narrowband(-n) 告诉Speex默认将输入视为窄带(8kHz)
-wideband(-n) 告诉Speex将输入视为宽带(16kHz)
-ultra-wideband(-u) 告诉Speex将输入视为超宽带(32kHz)
-quality n 设置编码质量(0-10),默认为8
-bitrate n 编码比特率(比特率将小于等于n)
-vbr n 启用VBR(可变比特率),默认禁用
-abr n 启用ABR(平均比特率)为n kbps,默认禁用
-vad 启用VAD(声音活动检测),默认禁用
-dtx 启用DTX(断续传输),默认禁用
-nrames n 每个Ogg数据包包含n帧(这在低比特率时能节省空间)
-comp n 设置编码速度和质量的折中,n值越大,编码速度越慢(默认为3)
-V 详细的操作,输出所使用的比特率
-help(-h) 输出帮助
-version(-v) 输出版本信息
Speex注释
-comment 增加给定的字符串为额外的注释,这可能花费几倍时间
-author 声道作者
-title 声道标题
原始输入选项
-rate n 原始输入的采样率
-stereo 将原始输入视为立体声
-le 原始输入为小头(little-endian,高位字节放在高地址单元)
-be 原始输入为大头(big-endian,高位字节放在低地址单元)
-8bit 原始输入为8位无符号数
-16bit 原始输入为16位有符号数
speexdec用于解码Speex文件,可通过如下调用
speexdec[options] speex_file [output_file]
input_file或output_file的值‘-’分别对应标准输入和标准输出。若没有指定output_file,则文件通过声卡播放。有效的选项有:
-enh 启用后置滤波(默认)
-no-enh 禁用后置滤波
-force-nb 强制窄带解码
-force-wb 强制宽带解码
-force-uwb 强制超宽带解码
-mono 强制单声道解码
-stereo 强制立体声解码
-rate n 强制解码采样率为n Hz
-packet-loss n 模拟n%随机包丢失
-V 详细操作,输出使用的比特率
-help(-h) 输出帮助
-version(-v) 输出版本信息
libspeex库包含所有使用Speex编解码器对语音编码和解码的函数。若在UNIX系统中链接,需要在编译器命令行中加入-lspeex-lm。注意libspeex的调用时可重入的,但不是线程安全的。这意味着可以在多个线程中条用,但多个线程间对同一状态的调用需要互斥锁保护。代码示例参见附录A,完整的API文档可在Speex网站的文档部分找到(http://www.speex.org/)。
在用Speex编码语音之前,首先需要包括头文件:
#include<speex/speex.h>
然后在代码中,必须声明一个Speex位封装结构和一个Speex编码状态:
SpeexBits bits;
void *enc_state;
然后需要两个初始化:
speex_bits_init(&bits);
enc_state = speex_encoder_init(&speex_nb_mode);
对于宽带编码,speex_nb_mode替换为speex_wb_mode。大多数情况下,你需要知道所使用采样率的帧长,可通过变量frame_size获得(单位是样本数,而不是字节):
speex_encoder_ctl(enc_state,SPEEX_GET_FRAME_SIZE,&frame_size);
实际应用中,当用8,16或32kHz采样率是frame_size通常为20ms。Speex编码器可设置许多参数,但最有用的是控制质量和比特率折中的质量参数:
speex_encoder_ctl(enc_state,SPEEX_SET_QUALITY,&quality);
其中quality是一个0到10之间的整数(包含10)。图9.2描述了窄带时质量和比特率之间的关系。
初始化之后1,对于每一输入帧:
speex_bits_reset(&bits);
speex_encode_int(enc_state,input_frame,&bits);
nbBytes= speex_bits_write(&bits,byte_ptr,MAX_NB_BYTES);
其中input_fram是一个short *指针,只想一个语音帧的开始位置;byte_ptr是一个char *指针,指向将被写的已编码帧;MAX_NB_BYTES是在无溢出情况下能写到byte_ptr的最大字节数;nbBytes是实际写到byte_pte的字节数(已经编码的字节数)。在调用speex_bits_wrtie之前,可通过调用speex_bits_nbytes(&bits)获得需要被写的字节数,返回这一字节数。
也可以使用speex_encode()函数,返回一个指向音频的float *指针。然而这将使得没有FPU的平台(如ARM)最后端口更难懂。在内部,speex_encode()和speex_encode_int()是以相同的方式处理的。编码器是否使用定点版本仅由编译时间标志决定,而不是API级别。
在完成编码之后,释放资源:
speex_bits_destroy(&bits);
speex_encode_deatroy(enc_state);
以上是编码器部分。
在用Speex解码语音之前,首先需要头文件:
#include <speex/speex.h>
你也需要声明一个Speex位封装结构和一个Speex解码状态:
SpeexBitsbits;
void * dec_state;
然后进行初始化:
speex_bits_init(&bits);
dec_state = speex_decoder_int(&speex_nb_mode);
对于宽带解码,speex_nb_mode替换为speex_wb_mode。若需要获得解码器将使用的帧大小,可通过变量frame_size(单位是样本数,而不是字节数)获得:
speex_decoder_ctl(dec_state,SPEEX_GET_FRAME_SIZE,&frame_size);
解码器同样有一个参数可以设置:是否启用知觉增强:
speex_decoder_ctl(dec_state,SPEEX_SET_ENH,&enh);
其中enh初始化为0则禁用知觉增强,值为1则启用。对于1.2-beta1版本,默认启用知觉增强。
解码初始化后,对每一输入帧:
speex_bits_read_from(&bits,input_bytes,nbBytes);
speex_decode_int(dec_state,&bits,output_frame);
其中input_bytes是一个包含一帧接收到的比特流数据的char*指针;nbBytes是比特流的大小(字节);output_frame是一个short*型指针,指向解码语音将被写入的区域。若第二个参数值为NULL,则表明当前帧没有比特。当有一帧丢失时,Speex解码器将尽量“猜测”正确的信号。
跟编码器一样,speex_decode()函数也可以使用,返回一个指向输出音频的float*型指针。在完成解码后,要释放资源:
speex_bits_destroy(&bits);
speex_decoder_destrpy(dec_state);
Speex编码器和解码器支持许多选项,可通过speex_encoder_ctl和speex_decoder_ctl函数设置。这些函数类似于输入输出系统函数调用,他们的原型是:
void speex_encoder_ctl(void *encoder,int request,void *ptr);
void speex_decoder_ctl(void *decoder,int request,void *ptr);
尽管有许多设置函数,但是对于许多应用默认设置一般足够,仅当使用者理解了这些设置并需要时才需设置选项。一个常见错误是设置了许多没必要的设置。
以下是一个允许请求设置值得列表,其中一些仅适用于编码器或解码器。由于最后一个参数是void*型的,所以_ctl()函数不是类型安全的,需要小心使用。类型spx_int32_t同等于C99中的类型int32_t。
SPEEX_SET_ENH:设置知觉增强为开(1)或关(2)(spx_int32_t型,默认为开,仅用于解码器)
SPEEX_GET_ENH:获取知觉增强状态(spx_int32_t型,仅用于解码器)
SPEEX_GET_FRAME_SIZE:获取当前模式下每帧的样本数(spx_int32_t型)
SPEEX_SET_QUALITY:设置编码器语音质量(spx_int32_t型,从0到10,默认为8,仅用于编码器)
SPEEX_GET_QUALITY:获取当前编码器语音质量(spz_int32_t型,从0到10,仅用于编码器)
SPEEX_SET_MODE:设置模型数字,同RTP规格里指定(spx_int32_t型,仅用于编码器)
SPEEX_GET_MODE:获取当前模型数字,同RTP规格里指定(spx_int32_t型,仅用于编码器)
SPEEX_SET_VBR:设置变比特率(VBR)为开(1)或关(0)(spx_int32_t型,默认为关,仅用于编码器)
SPEEX_GET_VBR:获取变比特率(VBR)状态(spx_int32_t型,仅用于编码器)
SPEEX_SET_VBR_QUALITY:设置编码器VBR语音质量(浮点型,从0.0到10.0,默认为8.0,仅用于编码器)
SPEEX_GET_VBR_QUALITY:获取当前编码器VBR语音质量(浮点型,从0.0到10.0,仅用于编码器)
SPEEX_SET_COMPLEXITY:设置分配给编码器的CPU资源(spx_int32_t型,从1到10,默认为2,仅用于编码器)
SPEEX_GEY_COMPLEXITY:获取分配给编码器的CPU资源(spx_int32_t型,从1到10,默认为2,仅用于编码器)
SPEEX_SET_BITRATE:设置比特率为不超过参数的最近整数值(SPX_INT32_t型,单位比特每秒,仅用于编码器)
SPEEX_GET_BITRATE:获取当前比特率(spx_int32_t型,单位比特每秒,仅用于编码器)
SPEEX_SET_SAMPLING_RATE:设置实际采样率(spx_int32_t型,单位Hz)
SPEEX_GET_SAMPLING_RATE:获取实际采样率(spx_int32_t型,单位Hz)
SPEEX_RESET_STATE:重置编码器/解码器状态为初始状态,清空所有内存(不推荐)
SPEEX_SET_VAD:设置声音活动检测(VAD)为开(1)或关(0)(spx_int32_t型,默认为关,仅用于编码器)
SPEEX_GET_VAD:获取声音活动检测(VAD)状态(spx_int32_t型,仅用于编码器)
SPEEX_SET_DTX:设置断续传输(DTX)为开(1)或关(0)(spx_int32_t型,默认为关,仅用于编码器)
SPEEX_GET_DTX:获取断续传输(DTX)状态(spx_int32_t型,仅用于编码器)
SPEEX_SET_ABR:设置平均比特率(ABR)为n(spx_int32_t型,单位比特每秒,仅用于编码器)
SPEEX_GET_ABR:获取平均比特率(ABR)设置(spx_int32_t型,单位比特每秒,仅用于编码器)
SPEEX_SET_PLC_TUNING:告诉编码器对一个包丢失百分比进行优化编码(spx_int32_t型,百分数,仅用于编码器)
SPEEX_GET_PLC_TUNING:获取PLC编码的当前调谐(spx_int32_t型,百分数,仅用于编码器)
SPEEX_SET_VBR_MAX_BITRATE:设置VBR操作中允许的最大比特率(spx_int32_t型,单位比特每秒,仅用于编码器)
SPEEX_GET_VBR_MAX_BITRATE:获取当前VBR操作中允许的最大比特率(spx_int32_t型,单位比特每秒,仅用于编码器)
SPEEX_SET_HIGHPASS:设置高通滤波器为开(1)或关(0)(spx_int32_t型,默认为开)
SPEEX_GET_HIGHPASS:获取当前高通滤波器状态(spx_int32_t型)
Speex模式查询类似于speex_encoder_ctl和speex_decoder_ctl函数调用。因为模式是只读的,所以仅能获取一个特别模式的信息。函数调用如下:
voidspeex_mode_query(Speexmode *mode, int request, void *ptr);
其中request可允许的值有(除非特别声明,这些值通过指针ptr返回):
SPEEX_MODE_FRAME_SIZE 获取模式的帧大小(样本数)
SPEEX_SUBMODE_BITRATE 通过ptr获取子模式比特率(整数,单位bps)
5.5 封包和带内信号
有时期望每个数据包(或其他基本存储单元)不止包含一帧,此时可在调用speex_bits_write写数据流之前调用speex_encode N次来实现。若每个包的帧数不是由带外机制决定,则可以包含一个终止码。终止码用5位对代码15(十进制)编码,如表9.2所示。注意对于1.0.2版本,调用speex_bits_write自动插入终止码来填充最后一字节,这样没有任何其他费用并保证包里没有更多帧时Speex能准确检测到。
可以发送带内信息到另一端。这些信息之前加入一个包含4位信息类型代码的模式14的“伪帧”。表5.1列出了可能的代码,包含他们的意义和信息大小。大多数信息是要发送到另一端编码器或解码器的请求,可以自由接受或无视这些请求。默认的,无视所有带内信息。
表1 带内信号代码
最后,应用可以用代码13定义常用带内信息,信息字节大小用5位编码,解码器若不知如何解码则可以跳过它。
对于1.2beta3版本,Speex中的非编解码部分单独分离成libspeexdsp库,包括预处理器,声学回声消除器,抖动缓冲器和重采样器。在UNIX环境下,可通过在编译器命令行中添加-lspeexdsp -lm链接到你的程序中。正如libspeex,libspeexdsp调用时可重入的,但不是线程安全的,这意味着可以在多线程中使用,但多线程中对同一状态的调用必须由互斥锁保护。
调用Speex预处理器之前,首先需要头文件
#include<speex/speex_preprocess.h>
然后,创建预处理器状态:
SpeexPreprocessState *preprocess_state =speex_preprocess_state_init(frame_size,sampling_rate);
推荐frame_size的大小同编码器中的一样,为20ms。
对于每一输入帧,需要调用:
speex_preprocess_run(preprocess_state, audio_frame);
其中audio_frame可为输入或输出。若不需要输出处理后的数据,可调用以下函数替代:
speex_preprocess_eatimate_update(preprocess_state, audio_frame);
这一函数会更新预处理器内部状态变量而无需计算输出音频,节省CPU周期。
预处理器参数可以改变,通过调用:
speex_preprocess_ctl(preprocess_state, request, ptr);
调用方法同编码器和译码器中的一样,选项在6.1.1中列出。
最后,销毁预处理器状态:
speex_preprocess_state_destroy(preprocess_state);
6.1.1 预处理器选项
同编解码器一样,预处理选项可通过函数控制。可用选项有:
SPEEX_PREPROCESS_SET_DENOISE 开启(1)或关闭(2)降噪(spx_int32_t型)
SPEEX_PREPROCESS_GET_DENOISE 获取降噪状态(spx_int32_t型)
SPEEX_PREPROCESS_SET_AGC 开启(1)或关闭(2)自动增益控制(AGC)(spx_int32_t型)
SPEEX_PREPROCESS_GET_AGC 获取AGC状态(spx_int32_t型)
SPEEX_PREPROCESS_SET_VAD 开启(1)或关闭(2)声音活动检测(VAD)(spx_int32_t型)
SPEEX_PREPRECESS_GET_VAD 获取VAD状态(spx_int32_t型)
SPEEX_PREPROCESS_SET_AGC_LEVEL
SPEEX_PREPROCESS_GET_AGC_LEVEL
SPEEX_PREPROCESS_SET_DEREVERB 开启(1)或关闭(2)混响消除(spx_int32_t型)
SPEEX_PREPROCESS_GET_DEREVERB 获取混响消除状态(spx_int32_t型)
SPEEX_PREPROCESS_SET_DEREVERB_LEVEL 暂时不可用
SPEEX_PREPROCESS_GET_DEREVERB_LEVEL 暂时不可用
SPEEX_PREPROCESS_SET_DEREVERB_DECAY 暂时不可用
SPEEX_PREPROCESS_GET_DEREVERB_DECAY 暂时不可用
SPEEX_PREPROCESS_SET_PROB_START
SPEEX_PREPROCESS_GET_PROB_START
SPEEX_PREPROCESS_SET_PROB_CONTINUE
SPEEX_PREPROCESS_GET_PROB_CONTINUE
SPEEX_PREPROCESS_SET_NOISE_SUPPRESS 设置最大噪声衰减,单位dB(负spx_int32_t型)
SPEEX_PREPROCESS_GET_NOISE_SUPPRESS 获取最大噪声衰减,单位dB(负spx_int32_t型)
SPEEX_PREPROCESS_SET_ECHO_SUPPRESS 设置最大残余回声衰减,单位dB(负spx_int32_t型)
SPEEX_PREPROCESS_GET_ECHO_SUPPRESS 获取最带残余回声衰减,单位dB(负spx_int32_t型)
SPEEX_PREPROCESS_SET_ECHO_SUPPRESS_ACTIVE 近端活动时设置最大残余回声衰减,单位dB(负spx_int32_t型)
SPEEX_PREPROCESS_GET_ECHO_SUPPRESS_ACTIVE 近端活动时获取最大残余回声衰减,单位dB(负spx_int32_t型)
SPEEX_PREPROCESS_SET_ECHO_STATE 为残余回声一直设置相关回声消除器(若没有残余回声消除,为指针或NULL)
SPEEX_PREPROCESS_GET_ECHO_STATE 获取相关回声消除器(指针)
Speex库现在包含一个可用的声学回声消除算法。使用回声消除器之前,需要包含头文件:
#include<speex/speex_echo.h>
然后,创建回声消除器状态:
SpeexEchoState *echo_state = speex_echo_state_int(frame_size. filter_length);
其中frame_size是你一次想处理的数据大小(样本数),filter_length是你想使用的回声消除滤波器长度(样本数,也叫尾长)。推荐使用20ms帧长(或等于编解码器帧长),确保容易执行FFT(2的指数大小最佳)。推荐尾长近似为房间混响时间的三分之一,例如,在小房间中,混响时间是300ms,则尾长最好为100ms(采样率8000Hz对应800样本数)。
创建回声消除器状态后,处理音频:
speex_echo_cancellation(echo_state, input_frame, echo_frame,output_frame);
其中input_frame是麦克风采集到的音频数据(近端),echo_frame是扬声器中播放的音频(远端,待消除),output_frame是回声消除后的数据。
需要重点考虑的是input_frame和echo_frame之间的关系,在任何时候,近端中存在的回声必须发送到echo_frame作为参考信号。换言之,回声消除器不能消除没有参考信号的回声。另一方面,输入信号和参考回声信号的延时必须足够小,否则回声消除器效果不好甚至无效(译者注:具体参考NLMS算法,这是声学回声消除的一个关键问题,就是近端和远端的同步问题,因为不同步算法收敛慢甚至不收敛。微软的回声消除效果很好,但也对系统版本和硬件有要求,因为我们设置的采样率跟硬件实际采样率还是有一点差别的,具体参见微软相关论文)。在完全同步时,代码可以像这样:
write_to_soundcard(echo_frame, frame_size);
read_from_soundcard(input_frame, frame_size);
speex_echo_cancellation(echo_state, input_frame, echo_frame, output_frame);
若想更好的回声消除效果,可在预处理器后设置回声消除器(见6.1),即初始化时调用:
speex_preprocess_ctl(preprocess_state, SPEEX_PREPROCESS_SET_ECHO_STATE,echo_state);
对于1.2-beta2版本,可用另一种方法代替speex_echo_cancellation()函数。当录音和播放需要同步处理时(如不同线程或使用poll()或select()系统调用),很难确定input_frame对应哪一echo_frame(译者注:即同步问题)。作为替代,回放线程可简单调用:
speex_echo_playback(echo_state,echo_frame);
每次播放一帧,就调用录音线程处理录音的一帧:
speex_echo_capture(echo_state, input_frame, output_frame);
在内部,speex_echo_playback()函数简单的将播放帧放在一个buffer中,然后在调用speex_echo_cancel()时由speex_can_capture()调用buffer中数据。使用这种API调用方式的一个副作用是回放帧需要延迟两帧,这是声卡引起的正常延迟。若回放和录音已经完全同步,speex_echo_cancellation()函数能更好地控制回声消除效果。
最后,回声消除状态需要销毁:
speex_echo_state_destroy(echo_state);
也可以重置回声消除器,这样就可以重新利用回声消除器而无需再创建一个新的:
speex_echo_state_reset(echo_state);
6.2.1 故障查找
回声消除器正常工作可能存在几个问题。其中之一是代码中存在bug(或效果是次优的),其他需要考虑的问题有:
1)当换一块声卡来录音和播放时,原来的回声消除器无效,除非两块声卡在同样的时钟脉冲源上有相同的采样时钟“锁定”,否则,它们的时钟总会有一些偏移,这将使得回声消除器不收敛(译者注:这就是软件设置的采样率与硬件实际的采样率有些许差别的问题)。
2)录音和回放之间的延时必须尽可能小。任何信号都是先回放(远端),然后被近端采样录音,过多的延时意味着部分滤波器长度的浪费。最坏情况下,延时比滤波器长度还大,此时不能消除任何回声。
3)回声尾长(滤波器长度)并不是越大越好。实际上,尾长越长,滤波器收敛越慢。当然,尾长太短也不能消除回声,但更常见的问题是许多人设置了一个很长的尾长然后奇怪为什么没有消除回声。
4)非线性失真不能被回声消除所使用的线性自适应滤波器建模,因此不能消除非线性失真。可使用好的音频设备并且避免饱和与削波。
具体可参考Alexey Frunze的《Echo Cancellation Demystified》(http://www.embeddedstar.com/articles/2003/7/article20030720-1.html),解释了回声消除的基本原理。具体算法在不同文章中的描述不同,但回声消除的通用方法就是自适应滤波器。
在1.2beta2版本后,源代码中包含了一个新的echo_diagnostic.m工具。第一步是在建立是定义DUMP_ECHO_CANCEL_DATA,这将会使回声消除自动保存近端、远端和处理后的信号到文件中(aec_rec.sw、sec_play.sw和aec_out.sw),这正是AEC的输入和输出。使用这一工具时,需要开启倍频程:
echo_diagnostic('aec_rec.sw', 'aec_play', 'aec_diagnostoc', 1024);
值1024是滤波器长度,可以改变。将会输出一些有用的信息,回声消除音频将保存到aec_diagnostic.sw。如果输出无效(回声没有消除),可能是播放或录音的问题。
启用抖动缓冲器,需要包含头文件:
#include<speex/speex_jitter.h>
然后初始化一个新的抖动缓冲器:
JitterBuffer *state = jitter_buffer_init(step);
其中step参数是默认时间步长(单位为时间戳的单位),用来调整延时和做隐蔽,合适的值为1,有时更大的值更好。例如,如果你能在20ms帧上做隐蔽,则抖动缓冲器中的点无需一个一个做。另一个例子是针对视频,对少于一帧的延时进行调整没有意义,后面可以随时改变这个值。
抖动缓冲器API基于JitterBufferPacket类型,定义如下:
typedefstruct {
char *data; /*数据包中的字节数据*/
spx_uint32_t len; /*数据包长度,单位字节*/
spx_unit32_t timestamp; /*数据包时间戳*/
spx_uint32_t span; /*数据包覆盖的时间(时间戳为单位)*/
} JitterBufferPacket;
例如,对于音频,timestamp就是RTP时间戳域,span是数据包中编码的样本数。对于Speex窄带,若数据包仅包含一帧,则span为160。
当一个包到达时,将被插入到抖动缓冲器中:
JitterBufferPacketpacket;
/*填充数据包结构中每一项*/
jitter_buffer_put(state, &packet);
当解码器准备解码一个包时,被解码的包可这样获取:
intstart_offset;
err =jitter_buffer_get(state, &packet, desired_span, &start_offset);
若jitter_buffer_put()和jitter_buffer_get()在不同的线程中调用,则需要用互斥锁保护抖动缓冲器的状态。
因为抖动缓冲器不用一个明确的计时器,所以需要告诉程序精确的时间,通过如下调用实现:
jitter_buffer_tick(state);
这需要在播放线程中定期调用,这是播放线程休眠之前的最后一个抖动缓冲器调用(直到更多数据被回放)。在一些例子中,如下调用会更好:
jitter_buffer_remaining_span(state,remaining);
第二个参数用指定还没被写入回放设备的仍保持在buffer中的数据,比如,若声卡需要256个样本数(由deaired_span指定),但jitter_buffer_get()返回320个样本数,则remaining=64。
Speex包含一个重采样器模型,要使用它,需要包含头文件:
#include<speex/speex_resampler.h>
对于每一个要重采样的流,需要创建一个重采样器状态:
SpeexResamplerState *resampler;
resampler = speex_resampler_init(nb_channels, input_rate, output_rate, quality,&err);
其中nb_channels是将被使用的信道数(交错的或非交错的);input_rate是输入流的采样率;output_rate是输出流的采样率;quality是要求的质量设置(0到10)。质量参数能控制质量、复杂度和等待时间之间的折中。高的质量参数意味着更少的噪声和混叠,但有更高的复杂度和更长的等待时间。通常质量参数为3对大多数桌面应用都可接受,质量参数为10在专业音响中最推荐。质量参数为0通常有不错的声音(当然比线性插值重采样好),但能听出人为迹象。
实际重采样通过如下调用执行:
err =speex_resampler_process_int(resampler, channelID, in, &in_length, out,&out_length);
其中channelID是被处理的信道ID,对于单声道流,为0。in指针指向被选信道输入buffer的第一个样本;out指针指向输出的第一个样本。输入输出buffer的大小分别由in_length和out_length指定。完成后,这些值被替代为重采样器读取和写入的样本数。除非发生错误,否则所有输入样本将被读取或/和所有输出样本将被写入。对于浮点型样本,函数speex_resampler_process_float()有相似功能。也可以一次处理多个通道。
未完待续...
待补充...(英文版文档中是这样写的)
Speex既能窄带编码语音也能宽带编码语音,并且支持不同比特率。然而,某一个应用或设备无需支持所有特征。为了满足"Speex兼容“,一个应用最少需满足一些基本特征。
最起码,所有的窄带模型操作必须被解码器支持,这包括通过窄带解码器解码宽带比特流(宽带比特流包含一个能单独接解码的嵌入的窄带比特流)。如果存在,宽带解码器必须能解码窄带比特流,也许能解码所有宽带模式或能解码所有模式的嵌入的窄带部分(包含忽略高代比特)。
对于编码器,最少一个窄带或宽带模式必须被支持。所有的编码模式不是必须支持的主要原因是一些平台可能能处理某些模式下的复杂编码。
RTP有效载荷草案见附录C,最新版本可在网站http://www.speex.org/drafts/latest上获得。草案已于2003年2月26日发送到英特网工程任务组(InternetEngineering Task Force,IETF)并将于3月18日在San Francisco举办的会议上讨论。
现在,你可以对Speex-in-Ogg使用MIME类型audio/x-speex。我们将在不久的将来提供类型audio/speex。
Speex比特流能存储在Ogg文件中。在这一情况下,Ogg文件的第一个包包含表7.1描述的Speex头文件,头文件中的所有整数字段以小头存储(高位字节放在高地址单元)。speex_string字段必须包含”Speex "(尾部3个空格),标识比特流。下一个字段,speex_version包含编码文件的Speex版本。现在可以参见speex_header.[ch]获取更多信息。开始流标识(beginningofstream,b_o_s)在头文件中置为1,包头有packetno=0和granulepos=0。
第二个包包含Speex注释头。使用的Vorbis注释格式在网站中描述:http://www.xiph.org/ogg/vorbis/doc/v-comment.html。这个包有packetno=1和granulepos=0。
第三个之后的包每个都包含一个或多个(头文件中写的数量)Speex帧。这些包的packetno由2开始,granulepos为包中编码的最后样本数。这些包的最后结束流(end ofstream,e_o_s)标识置为1。
表7.1 Ogg/Speex头包
Speex基于CELP,即码激励线性预测(Code Excited Linear Prediction)。本章介绍CELP基本原理,所以如果你已经对CELP很熟悉,可以跳到第9章。CELP技术基于以下3点思想:
1)利用一个线性预测(LP)模型模拟声道
2)使用(自适应的和固定的)密码本条目作为LP模型的输入(激励)
3)在“感知加权域”执行闭合搜索
本章介绍CELP的基本思想,其原理也在不断发展中。
产生语音的声源滤波器模型假设声带是声音的起源(激励信号),并且声道作为一个滤波器对各种语音声音调整谱形状。虽然这只是一个近似,却因为其简单性使得这一模型广泛用于语音编码中。这一模型也是为什么大多数语音编解码器(包括Speex)对音乐信号效果很差的原因。在这一模型中,不同的因素能根据激励(源)和谱形(滤波器)得以区分。浊音(如元音)是一个周期性激励信号,并能用一个时域脉冲序列近似或频域固定间隔的谐波近似。另一方面,摩擦声(如“s”,“sh"和”f“)是一种类似于高斯白噪声的激励信号,所谓的浊音摩擦音(如”z“和”v“)是一个由谐波部分和噪声部分组成的激励信号。
声源滤波器模型通常结合线性预测使用。CELP模型基于声源滤波器模型,如图8.1中CELP解码器所示。
图8.1 语音合成(解码)的CELP模型
线性预测是包括CELP在内的许多语音编码技术的基础,其思想是用x[n]之前的N个样本的线性组合来预测x[n]:
其中y[n]就是x[n]的线性预测。预测误差为:
线性预测的目的就是寻找使得误差平方和函数最小的最优预测系数ai:
可通过对ai求导,并使其等于0求得:
对于一个N阶滤波器,滤波器系数ai通过求解一个N*N线性系统Ra=r求得,其中
R(m)为信号x[n]的自相关:
因为R是Hermitian Toeplitz矩阵,所以可以利用Levinson-Durbin算法,使得复杂度由O(N^3)降低为O(N^2)。而且,可以证明A(z)的所有根都在单位圆内,这意味着1/A(z)总是稳定的。实际应用中由于精确度有限,有两个常用技术可以保证滤波器稳定:第一,将R(0)乘以一个比1稍微大一点的数(如1.0001),等价于对信号添加噪声;第二,也可以对自相关加窗,等价于在频域滤波,减少尖锐的共振。
在浊音段,语音信号是周期的,所以可以通过对过去激励乘以一个增益来近似当前激励信号e[n]:
其中T为音高周期,B为音高增益。我们称其为长期预测,因为激励是由e[n-T]预测而来,而T>>N。
最后的激励e[n]是预测音高来自固定码本信号c[n]之和,即码激励线性预测编码。最后的激励为:
c[n]量化在CELP编解码器分配的大多数位中,意味着不能由线性预测或音高预测获得信息。在z域最终信号X(Z)可表示为:
大多数(即便不是全部)现代音频编解码器都会”形成“噪声,它大多出现在耳朵不能检测到的频域。例如,耳朵更能忍受噪声谱中较大声部分,反之亦然。为了获得最佳语音质量,CELP编解码器在感知加权域最小化噪声均方误差,所以可以在编码器中对误差信号应用一个感知噪声加权滤波器W(z)。在大多数CELP编解码器中,W(z)是一个由线性预测系数(LPC)派生而来零极点加权滤波器,一般用带宽阐述。令合成滤波器1/A(z)代表频谱包络,CELP编解码器典型地导出噪声加权滤波器为:
在Speex参考实现中y1=0.9且y2=0.6。如果滤波器A(z)在z平面有(复)极点pi,滤波器A(z/y)将有一个pi‘=ypi的极点。
加权滤波器应用于误差信号,用来通过analysis-by-synthesis(AbS)最优化码本搜索,这导致趋向于1/W(z)的噪声谱形。虽然这一简单模型是CELP很有效的重要原因,但W(z)却是感知最优噪声加权函数的粗糙近似。图8.2阐释了由图8.1得来的噪声谱形。在本手册中,W(z)指的是噪声加权滤波器而1/W(z)是噪声整形滤波器(或曲线)。
图8.2 CELP中标准噪声整形,任意y轴偏移
CELP中的一个主要原理叫Analysis-by-Synthesis(AbS),即编码(分析)是通过闭环感知最优解码(合成)信号实现的。理论上,最佳的CELP流应通过尝试所有可能的位组合并选择其中产生最佳听觉解码信号的那个组合产生。实际中这明显是不可能的,有两个原因:所需的复杂度超出目前所有可用的硬件,并且”最佳听觉“选择标准是人的听力,机器做不到。
为了利用有限的计算资源进行实时编码,CELP最优化利用感知加权函数分解成更小的、更易管理的、顺序的检索。
这章介绍Speex如何在窄带(8kHz采样率)操作。这一模式下帧大小为20ms,对应160个采样点。每一帧又分成4个子帧,每个子帧40个采样点。
许多设计决策基于如下的初衷和假设:
1)使从先前帧提取的信息数量最小化(为了对丢包具有鲁棒性)
2)动态可选择的码本(LSP,音高和创新)
3)子向量固定码本(创新)
窄带模式下,Speex帧长为20ms(160个采样点),每帧又分成4个帧长5ms(40个采样点)的子帧。对于大多数窄带比特率(8kbps),在帧级接口的编码参数仅有线谱对子(Line SpectralPairs,LSP)和一个全局激励增益gframe,如图9.1所示。其他编码参数都在子帧接口中。
图9.1 帧开环分析
每帧都会应用一个不对称的汉明窗,这个窗中心在第4个子帧,此时执行就会执行线性预测分析。因为线性预测系数(LPC)对量化不鲁棒,所以先将LPC转化为线谱对子(LSP)。LSP与第4个子帧有关并且与前3个子帧相关的LPC用当前和先前LSP系数线性内插获得。LSP系数然后转换回LPC滤波器A^(z)。非量化内插滤波器表示为A(z),能用做加权滤波器W(z),因为它对解码器是没必要的。
为了使Speex对丢包更鲁棒,在LSP系数量化之前不进行预测。LSP用向量量化(VQ)编码,其中高质量模式用30位,低质量模式用18位。
Analysis-by-Synthesis(AbS)编码器如图9.2所示。Speex与其他大多数CELP编解码器主要有3个方面的不同。第一,目前大多数CELP编解码器使用一个单一增益的分数基音估计算法,Speex用一个整数来编码基音周期,但使用一个3抽头预测器(3增益)。因而自适应码本ea[n]可表示为:
其中g0,g1和g2是共同量化的音高增益,e[n]是编解码器激励存储器。值得注意的是若基音周期少于子帧大小,我们在周期T重复此激励。例如,当n-T+1>=0,我们用n-2T+1代替。大多数模式下,基音周期用范围为[17,144]的7位编码,Bi系数用向量量化,其中高比特率(15kbps窄带及以上)用7位,低比特率(11kbps窄带及以下)用5位。
图9.2 子帧Analysis-by-synthesis闭环优化
目前许多CELP编解码器用移动平均(MA)预测来编码固定码本增益,这能获得稍微好点的编码,但依赖于先前已编码的帧。第二个不同点是Speex将固定码本增益编码为全局激励增益gframe与子帧增益修正系数gsubf的积。这样通过消除帧间依赖提高了丢包鲁棒性。子帧增益修正系数在搜索固定码本之前被编码(不是闭环优化)且每个子帧用0到3位,这依赖于比特率。
第三个不同是Speex用固定码本信号的子向量优化代替代数码本。每一个子帧分成5到20个采样点的子向量,每一个子向量从依赖于比特率的码本中选择且所有子向量串联形成一个子帧。例如,3.95kbps模式使用码本中32条目(5位)的20采样点子向量,意味着码本以每子帧10位编码或2000bps。另一方面,18.2kbps模式使用码本条目256(8位)的5采样点子向量,所以码本以每帧64位编码,或12800bps。
Speex定义了7种不同的窄带比特率,范围从250bps到24.6kbps,尽管不推荐使用5.9kbps以下的模式。每个模式的位分配如表9.1所示。每帧以模式ID开始,它以4位编码,允许范围从0到15,但是仅仅使用了前7个值(剩余值保留)。表中的参数以它们在比特流中的打包顺序列出,所有帧参数在子帧参数之前打包。某一子帧参数在下一子帧打包之前全部打包。注意参数描述中的“OL”表示参数是整帧的开环(open loop)估计。
表9.1 窄带模式位分配
目前为止,Speex没有进行平均意见得分(MOS,Mean Opinion Score)主观评测。为了获得理想的质量,表9.2列出了作者的主观意见。值得注意的是不同的人对语音质量的感觉是不同的,设计编解码器的人对主观意见可能有些偏见。最后,值得注意的是大多数编解码器(包括Speex)的编码质量因输入而异的。注意复杂度只是个近似值(精度0.5mflops,使用最低复杂度设置)。大多数模式下解码需要约0.5mflops(有感知增强为1mflops)。
表9.2 不同比特率的质量
这部分在版本1.1.12之前有效。1.2-beta1版本(及之后)没包含,因为新的感知增强还没写好文档。
编解码器的这部分仅应用于解码器,甚至可以在不影响内部操作下改变。因为这个原因,这里提供和描述的实现仅作为参考。增强系统分为两部分。首先,合成滤波器S(z)=1/A(z)替换为增强滤波器:
其中a1和a2依赖于所用的模式,a3=1/r(1-(1-ra1)/(1-ra2)),r=0.9。增强的第二部分包括在激励域使用梳形滤波器增强音高。
对于宽带,Speex用一个正交镜像滤波器(QMF)将带宽一分为二。16kHz信号因此分成两个8kHz信号,一个代表低宽带(0-4kHz),另一个代表高宽带(4-8kHz)。低宽带用第9章描述的窄带模式编码,用这种方式“嵌入式窄带比特流”也能用窄带解码器解码。因为低宽带编码已经描述,这章仅描述高宽带编码。
高宽带的线性预测部分类似于窄带的,唯一的区别是我们仅使用12位编码高宽带LSP,并用一个多级向量量化(MSVQ)。先用6位量化10个系数,然后用6位量化误差。
这部分很简单:高宽带没有音高预测。这有两个原因,第一,通常在这一带宽内只有少量谐波结构(4kHz以上);第二,很难实现,因为QMF折叠4-8kHz为4-0kHz(反转频率轴),意味着谐波的位置已经不处于基频的倍数上。
高宽带激励编码方式与窄带相同。
对于宽带模式,整个窄带帧在高宽带编码前打包,比特流的窄带部分定义在表9.1中,高宽带如表10.1所描述。对于宽带,模式ID与Speex质量设置相同。这意味着可能通过窄带解码器正确解码宽带帧,唯一的警告是如果同一个包内大于一帧,解码器需要跳过高宽带部分,以同步比特流。
表10.1 宽带模式下高带宽位分配
表10.2 宽带解码器不同比特率的解码质量
这部分展示运用Speex API编码和解码语音的代码示例,可通过调用如下指令编码和解码一个文件:
% sampleenc in_file.sw | sampledec out_file.sw
其中的文件都是每个样本16比特编码(机器自然字节顺序)的原始文件(无文件头)。
sampleenc读取一个原始的16位采样的文件,进行编码并输出Speex流到stdout。注意所用的数据包与speexenc/speexdec是不兼容的。
/*代码清单A.1:sampleenc源代码*/
#include
#include
/*The framesize in hardcoded for this sample code but it doesn't have to be*/
#defineFRAME_SIZE 160
intmain(int argc, char argv)
{
char *inFile;
FILE *fin;
short in[FRAME_SIZE];
float input[FRAME_SIZE];
char cbits[200];
int nbBytes;
/*Holds the state of the encoder*/
void *state;
/*Holds bits so they can be read andwritten to by the Speex routines*/
SpeexBits bits;
int i, tmp;
/*Create a new encoder state in narrowbandmode*/
state =speex_encoder_init(&speex_nb_mode);
/*Set the quality to 8 (15 kbps)*/
tmp = 8;
speex_encoder_ctl(state, SPEEX_SET_QUALITY,&tmp);
inFile = argv[1];
fin = fopen(inFile, "r");
/*Initialization of the structure thatholds the bits*/
speex_bits_init(&bits);
while(1)
{
/*Read a 16 bits/sample audio frame*/
fread(in, sizeof(short), FRAME_SIZE,fin);
if (feof(fin))
break;
/*Copy the 16 bits values to float soSpeex can work on them*/
for (i = 0; i < FRAME_SIZE, i++)
input[i] = in[i];
/*Flusf all the bits in the struct sowe can encode a new frame*/
speex_bits_reset(&bits);
/*Encode the frame*/
speex_encode(state, input, &bits);
/*Copy the bits to an array of charthat can be written*/
nbBytes = speex_bits_write(&bits,cbits, 200);
/*Write the size of the frame first.This is what sampledec expects but
it's likely to be different in your ownapplication*/
fwrite(&nbBytes, sizeof(int), 1,stdout);
/*Write the compressed data*/
fwrite(cbits, 1, nbBytes, stdout);
}
/*Destroy the encoder state*/
speex_encoder_destroy(state);
/*Destroy the bit-packing struct*/
speex_bits_destroy(&bits);
fclose(fin);
return 0;
}
sampledec从stdin中读取Speex流,解码并输出到一个原始16位采样文件。注意所用的数据包与speexenc/speexdec是不兼容的。
/*清单A.2:sampledec源代码*/
#include
#include
/*The framesize in hardcoded for this sample code but it doesn't have to be*/
#defineFRAME_SIZE 160
intmain(int argc, char argv)
{
char *outFile;
FILE *fout;
/*Holds the audio that will be written tofile (16 bits per sample)*/
short out[FRAME_SIZE];
/*Speex handle samples as float,so we needan array of floats*/
float output[FRAME_SIZE];
char cbits[200];
int nbBytes;
/*Holds the state of the decoder*/
void * state;
/*Holds bits so they can be read andwritten to by Speex routines*/
SpeexBits bits;
int i, tmp;
/*Create a new decoder state in narrowbandmode*/
state =speex_decoder_init(&speex_nb_mode);
/*Set the perceptual enhancement on*/
tmp = 1;
speex_decoder_ctl(state, SPEEX_SET_ENH,&tmp);
outFile = argv[1];
fout = fopen(outFile, "w");
/*Initialization of the structure thatholds the bits*/
speex_bits_init(&bits);
while(1)
{
/*Read the size encoded by sampleenc,this part will likely be
different in your application*/
fread(&nbBytes, sizeof(int), 1,stdin);
fprintf(stderr, "nbBytes:%d ", nbBytes);
if (feof(stdin))
break;
/*Read the "packet" encodedby sampleenc*/
fread(cbits, 1, nbBytes, stdin);
/*Copy the data into the bit-streamstruct*/
speex_bits_read_from(&bits, cbits,nbBytes);
/*Decode the data*/
speex_decode(state, &bits, output);
/*Copy from float to short (16 bits)for output*/
for (i = 0; i < FRAME_SIZE; i++)
out[i] = output[i];
/*Write the decoded audio to file*/
fwrite(out, sizeof(short), FRAME_SIZE,fout);
}
/*Destroy the decoder state*/
speex_decoder_destroy(state);
/*Destroy the bit-stream truct*/
speex_bits_destroy(&bits);
fclose(fout);
return 0;
}
/*清单B.1:对Speex数据包应用抖动缓冲器代码示例*/
#include
#include "speex_jitter_buffer.h"
#ifndef NULL
#define NULL 0
#endif
void speex_jitter_init(SpeexJitter *jitter,void *decoder, int sampling_rate)
{
jitter->dec = decoder;
speex_decoder_ctl(decoder, SPEEX_GET_FRAME_SIZE,&jitter->frame_size);
jitter->packets = jitter_buffer_init(jitter->frame_size);
speex_bits_init(&jitter->current_packet);
jitter->valid_bits = 0;
}
void speex_jitter_destroy(SpeexJitter*jitter)
{
jitter_buffer_destroy(jitter->packets);
speex_bits_destroy(&jitter->current_packet);
}
void speex_jitter_put(SpeexJitter *jitter,char *packet, int len, int timestamp)
{
JitterBufferPacket p;
p.data = packet;
p.len = len;
p.timestamp = timestamp;
p.span = jitter->frame_size;
jitter_buffer_put(jitter->packet, &p);
}
void speex_jitter_get(SpeexJitter *jitter,spx_int16_t *out, int *current_timestamp)
{
int i;
int ret;
spx_int32_t activity;
char data[2048];
JitterBufferPacket packet;
packet.data = data;
if(jitter->valid_bits)
{
/*Try decoding last received packet*/
ret = speex_decode_init(jitter->dec, &jitter->current_packet,out);
if(ret == 0)
{
jitter_buffer_tick(jitter->packets);
return;
}
else
{
jitter->valid_bits = 0;
}
}
ret = jitter_buffer_get(jitter->packet, &packet,jitter->frame_size, NULL);
if (ret != JITTER_BUFFER_OK)
{
/*Packet is late or lost*/
speex_decode_int(jitter->dec, NULL, out);
}
else
{
speex_bits_read_from(&jitter->current_packet, packet.data,packet.len);
/*Decode packet*/
ret = speex_decode_int(jitter->dec, &jitter->current_packet,out);
if(ret == 0)
{
jitter->valid_bits = 1;
}
else
{
/*Error while decoding*/
for (i = 0; i < jitter->frame_size; i++)
out[i] = 0;
}
}
speex_decoder_ctl(jitter->dec, SPEEX_GET_ACTIVITY, &activity);
if (activity < 30)
jitter_buffer_update_delay(jitter->packets, &packet, NULL);
jitter_buffer_tick(jitter->packets);
}
intspeex_jitter_get_pointer_timestamp(SpeexJitter *jitter)
{
return jitter_buffer_get_pointer_timestamp(jitter->packets);
}
版权声明:
本文来源网络,所有图片文章版权属于原作者,如有侵权,联系删除。
本文网址:https://www.mushiming.com/mjsbk/3006.html