版权声明:本文为原创文章,未经允许不得转载
博客地址:http://blog.csdn.net/kevindgk
GitHub地址:https://github.com/KevinDGK/MyAudioDemo
现在有个需求,在局域网内实现实时语音,传输层协议使用UDP协议,如果直接使用AudioRecord进行录制音频流并发送到另一端进行播放,音质会非常差,而且断断续续,原因如下:
采样频率: fm = 44.1KHz
量化位数:16bit
声道配置:2(双声道)
那么,码率 V = 44.1K * 16 *2 = 1411.2 Kbps = 176.4KBps,即每秒传输速率大概176.4KB,
若音频帧时间为20ms,每个音频数据包大小为 size = 176.4KBps * 0.02s = 3.528KB,
一般情况下,我们每次读取一个音频帧的数据,可以取整为3600Byte,
所以 每秒大概发送 176.4/3.6=49 个数据包,每个数据包大小为3.6KB。
如果再考虑到数据报头,实测每秒发送约45个数据包,每秒传输速率大概180KB。
由于一般都是使用手机连接Wifi,这就要求网络质量和硬件设备必须很好,而且信道干扰较弱,并且链接的设备不能过多。只要稍微信号不好,就会导致丢包率特别高,而且延时十分大,根本无法满足通信的需要。在这种情况下,我们就需要进行语音压缩、降噪等处理。
如果传输的仅仅是语音信息,那么不需要很高的采样频率,可以使用8KHz进行采样,单通道即可。
private int DEFAULT_SAMPLERATEINHZ = 8000; // 采样频率
private int DEFAULT_AUDIOFORMAT = AudioFormat.ENCODING_PCM_16BIT; // 数据格式
private int DEFAULT_STREAMTYPE = AudioManager.STREAM_MUSIC; // 音频类型
private int DEFAULT_CHANNELCONFIG_OUT = AudioFormat.CHANNEL_OUT_MONO; // 声道配置
private int DEFAULT_MODE = AudioTrack.MODE_STREAM; // 输出模式
private int DEFAULT_CHANNELCONFIG_IN = AudioFormat.CHANNEL_IN_MONO; // 声道配置
private int DEFAULT_AUDIOSOURCE = MediaRecorder.AudioSource.MIC; // 音频来源
官方网址
Speex是一套主要针对语音的开源免费,无专利保护的音频压缩格式。Speex工程着力于通过提供一个可以替代高性能语音编解码来降低语音应用输入门槛 。另外,相对于其它编解码器,Speex也很适合网络应用,在网络应用上有着自己独特的优势。同时,Speex还是GNU工程的一部分,在改版的BSD协议中得到了很好的支持。
Speex是基于CELP并且专门为码率在2-44kbps的语音压缩而设计的。它的特点有:
由于语音压缩的底层代码都是用C/C++写的,所以对于我们可怜的Android来说需要使用NDK进行JNI开发了,如果对这方面不了解的,可以参考一下小编整理的 JNI(一) - Android Studio简单开发流程 ,然后需要再了解一下C语言的一些基础知识,就可以进行简单的JNI开发了,在本文的Demo中,小编也写了大量的注解,希望能够帮到大家。
本项目GitHub地址
将代码集成到自己的项目中的步骤:
第一步:将Demo的整个jni目录复制到自己的main目录下;
第二步:修改复制过来的jni文件夹中的speex_jni.cpp中的方法名
方法名为Java _ 包名 _ 类名 _ 方法名(),中间使用单个下划线连接,详见demo;
第三步:添加gradle配置
将红色框圈住的地方复制到自己的项目中即可。
第四步:编译项目生成.so文件
选择Build->Make Project,然后找到.so库复制到自己的libs下即可:
第五步:在程序中使用
将Speex这个编解码工具类复制到自己的项目中,就可以正常使用了,具体使用方式详见Demo。
private Speex speex; // Speex音频编解码器
speex = new Speex(); // 创建Speex编解码实例
speex.open(4);
speex.encode(recordData,0,encodedbytes,readNumber); // 语音压缩-对音频数据编码
int decode = speex.decode(audioData, decodedShorts, audioData.length); // 对音频数据解码
采样频率: fm = 8KHz
量化位数:16bit
声道配置:1
那么,码率 V = 8K * 16 * 1 = 128Kbps = 16KBps,即每秒传输速率大概16KB,
若音频帧时间为20ms,每帧音频数据大小为 size = 16KBps * 0.02s = 320KB,即160 Short,
设置压缩质量为4,每帧音频数据压缩完后只有20Byte,压缩比为 320:20,即 16:1,每秒发送1/0.02=50个数据包,即单单传递音频数据占用的带宽为 1 KBps,如果需要添加一些数据报头,基本上也能维持在5KBps左右!
将176.4KBps变成16KBps,然后再压缩成1KBps,好了,现在可以满足基本的局域网内的语音传输需求了。如果带上耳机的话,基本上能够很完美的语音通话了。如果不带耳机,随着通话,麦克风或者环境的回声会越来越强,将会严重降低通话质量,我们之后需要做的就是做回声处理了。这个在之后的博客中专门介绍。
尽管Speex也十分的优秀,但是还是被新的技术给踢下了宝座,连它的官网上都写明了该技术已经被Opus给替代,并且宣称Opus的各项性能都将比Speex优秀的多。接下来小编将为大家初步介绍一下Opus的特点和API~
Opus是一个完全开放的、免费的、多功能的音频编解码器。 它在交互式的语音和音乐在互联网中的传输方面有着无与伦比的优势,但是同样致力于存储和流媒体应用程序。它是由互联网工程任务组(IETF)制定的标准,标准格式为RFC 6716,由Skype的SILK编解码器和Xiph.Org的CELT编解码器合并发展而来,号称音频编解码器中的瑞士军刀(来自官方视频)。
官方网址:http://opus-codec.org/
Opus可以处理广泛的音频应用程序,包括IP电话、视频会议、游戏内聊天、甚至远程现场音乐表演。从低比特率窄带语音到非常高质量的立体声音乐,它都可以适用。技术特点:
你可以在RFC 6716标准中阅读完整的规范,包括参考实现。也可以在下载页面获得一个最新的Opus 标准。
Libopus是Opus编解码器的参考实现,我们可以参考该代码进行开发。
为了能在Firefox中支持Opus,Mozilla提供了专门的Opus工具。Opus-tools提供命令行程序来进行编码、检查和解码.opus文件。在HTML中语音相关的可以使用,原来不是Android的,需要用的自己去官方主页下载即可。
小编翻译到这里还没有找到和Android相关的,蓝瘦,香菇!
尽管Opus现在是由IETF指定标准,但是Opus的实现也会一直持续改进。当然,所有未来版本仍将完全符合标准IETF规范。可以在开发界面查看最新的开发版本信息。
opus-1.1.3.tar.gz
小编曾说过,没有对比,就没有伤害。
下图说明了不同的编解码器的质量和比特率直接的函数关系。
narrowband - 窄频
wideband - 宽频
super-wideband - 超宽频
fullband - 全频段
fullband stereo - 全频段立体声
从图上可以看出,Opus的优势十分明显。尤其是较之前的Speex,具有更大的比特率范围以及带宽。
从图中可以看出,Opus在任何比特率下都具有较少的延迟。
Opus进行了几次测试,但是下面仅仅列出基于比特流的几个测试结果。尽管在Opus发布的时候应该给出一个比较好的质量标准化的意见,但是我们希望更新的和更高级的编码器将达到更好的质量。
测试结果
音频测试用例
截止到目前位置,最新的版本是1.13稳定发行版,所以在这里翻译的都是该版本的API。
Opus 编码器
- typedef struct OpusEncoder OpusEncoder
Opus编码器,包含了编码器的全部状态。
- int opus_encoder_get_size (int channels)
获取一个OpusEncoder编码器的大小。
- OpusEncoder * opus_encoder_create (opus_int32 Fs, int channels, int application, int *error)
分配和初始化一个编码器状态。
- int opus_encoder_init (OpusEncoder *st, opus_int32 Fs, int channels, int application)
初始化之前分配的编码器指针st指向的内存中的编码器,并且必须通过使用opus_encoder_get_size()方法来返回最小内存大小。
- opus_int32 opus_encode (OpusEncoder *st, const opus_int16 *pcm, int frame_size, unsigned char *data, opus_int32 max_data_bytes)
编码。
- opus_int32 opus_encode_float (OpusEncoder *st, const float *pcm, int frame_size, unsigned char *data, opus_int32 max_data_bytes)
编码音频流。
- void opus_encoder_destroy (OpusEncoder *st)
释放一个通过opus_encoder_create()分配的OpusEncoder。
- int opus_encoder_ctl (OpusEncoder *st, int request,...)
在Opus编码器中执行CTL函数。
通过官方文档解释和C语言基础,我们知道OpusEncoder *enc 表示一个Opus编码器结构体的指针,指向该编码器的内存,该结构体内部包含了该编码器的全部状态。C语言中是没有类和对象的概念的,但是有结构体,可以用来模拟Java中的类,所以结构体的实例也就可以比作对象。从现在开始,我就成称enc为该编码器对象(的指针),这样说比较习惯。
因为Opus是有状态编码,编码过程始于创建一个编码器状态:
int error;
OpusEncoder *enc;
enc = opus_encoder_create(Fs, channels, application, &error); // 创建
从这一点开来,enc可以用来编码一个音频流。一个编码器在同一时刻仅仅只能用于一个音频流编码,并且对于每一种音频格式初始化的编码器状态,不可以再次初始化。
当执行 opus_encoder_create() 为编码器分配了内存之后,就可以初始化这个预分配的内存:
int size;
int error;
OpusEncoder *enc;
size = opus_encoder_get_size(channels); // 获取需要的最小内存
enc = malloc(size); // 分配内存
error = opus_encoder_init(enc, Fs, channels, application); // 初始化内存
opus_encoder_get_size() 返回编码器对象所需要的内存大小,注意,该代码在未来的版本中可能会变成改变内存大小,所以不要根据这个代码有任何假定,即不要根据获取内存大小这个方法来有一些逻辑上的处理,因为之后的版本变动有可能会影响你的代码。
编码器的状态会一直保存在内存中,并且只有浅复制才能有效的复制该状态,例如:memcpy()。
使用 opus_encoder_ctl() 接口可以改变一些编码器的设置。所有的设置已经被默认设置成推荐值,所以只有在必要的时候才去改变它们。最常见的想要改变的设置如下:
opus_encoder_ctl(enc, OPUS_SET_BITRATE(bitrate));
opus_encoder_ctl(enc, OPUS_SET_COMPLEXITY(complexity));
opus_encoder_ctl(enc, OPUS_SET_SIGNAL(signal_type));
查看CTLS相关编码来获取完整的设置参数列表,大部分参数可以在一个音频流的任何时候设置和改变。
为了对一帧数据编码, opus_encode() 或者 opus_encode_float() 在调用的时候必须使用的是恰好的一帧(2.5,5,10,20,40,60毫秒)音频数据。
len = opus_encode(enc, audio_frame, frame_size, packet, max_packet);
opus_encode() 和 opus_encode_float() 返回实际接入到packet中的编码后的音频数据的字节数。返回值也许是无效的,表明编码错误。如果返回值小于2字节或者更小,那么这个packet不需要发送出去。
一旦这个编码器对象不需要,可以销毁掉:
opus_encoder_destroy(enc);
如果编码器对象是使用opus_encoder_init() 而不是 opus_encoder_create() 方法创建的,那么除了可能需要释放我们手动分配的内存之外,不需要其他的动作。
typedef struct OpusEncoder OpusEncoder
编码器结构体,包含了一个编码器所有的状态。它是位置独立的,可以被随意的复制。
- opus_int32 opus_encode( OpusEncoder∗ st, const opus_int16∗ pcm, int frame_size, unsigned char∗ data, opus_int32 max_data_bytes)
参数:
参数 | 参数类型 | 入参或出参 | 解释 |
---|---|---|---|
st | OpusEncoder∗ | in | 编码器对象 |
pcm | const opus_int16* | in | 输入信号(双声道则为交错模式),长度为frame_size × channels × sizeof(opus_int16),即采样个数 × 声道数 × 16。 |
frame_size | int | in | 输入的音频信号的每个声道的采样数量,这一定是一个Opus框架编码器采样率的大小。例如,当采样率为48KHz的时候,采样数量允许的数值为120、240、480、960、1920和2880。传递一个持续时间少于10 ms的音频数据(480个样本48 kHz),编码器将不会使用LPC或混合模式。 |
data | unsigned char∗ | out | 输出编码结果,至少包含max_data_bytes个字节数。 |
max_data_bytes | opus_int32 | in | 为了输出编码结果分配的内存,它可能用于控制一个即时比特率的上线,但是不应该作为唯一的比特率控制。 |
关于采样率和采样数量的关系,上文已经提到过,opus_encode() 或者 opus_encode_float() 在调用的时候必须使用的是恰好的一帧(2.5,5,10,20,40,60毫秒)音频数据,如果采样频率为48KHz,那么:
∵ 采样频率 Fm = 48KHz
∴ 采样间隔 T = 1/Fm = 1/48000 s = 1/48 ms
∴ 当T0 = 2.5 ms 时,N = T0/T = 2.5/(1/48) = 120,
当T0 = 5.0 ms 时,N = T0/T = 2.5/(1/48) = 240,
当T0 = 10 ms 时,N = T0/T = 2.5/(1/48) = 480,
当T0 = 20 ms 时,N = T0/T = 2.5/(1/48) = 960,
当T0 = 40 ms 时,N = T0/T = 2.5/(1/48) = 1920,
当T0 = 60 ms 时,N = T0/T = 2.5/(1/48) = 2880,
即,当Fm = 48KHz时:
采样时间(ms) | 2.5 | 5 | 10 | 20 | 40 | 60 |
---|---|---|---|---|---|---|
采样个数 | 120 | 240 | 480 | 960 | 1920 | 2880 |
- opus_int32 opus_encode_float( OpusEncoder∗ st, const float∗ pcm, int frame_size, unsigned char∗ data, opus_int32 max_data_bytes)
实在是翻译不动了。。。
其实有了上面的介绍,看官们应该能有个大概的认知,下面贴出一个中文版的翻译文档,大家自己浏览,因为目前我也差不多看了一般,所以小编对该文档的后面章节正确性与否并不负责~
Opus-官方文档中文版
如果对比一下Speex和Opus,会发现从集成和使用的角度来看,十分的相似,API的时候方式也差不多,只不过Opus的功能更加的强大,API也更加丰富,但是也变相的增加了我们的开发难度,需要对C语言有相当好的功底,才可以定制实现自己的需求。
编程之路,任重而道远。先是为了生活而编程,后是为了超越而编程!
微信: