iOS变声语音项目总结

最近做了一个变声语音的项目,里面涉及到很多音频相关的知识,怕时间久了记不住,写下来备忘。

1. 语音的编码

     语音录制的时候要选择一个编码格式,因为移动端的原因,这个编码格式需要满足压缩比高、声音质量较好(至少变声后能听得清说什么),同时还要编码难度小。

     我们前期选择了几种格式:amr、speex、aac、wav。 说下几种编码的优缺点。

     首先amr 是最常用于语音的编码,特别是在移动端上,优点是压缩比相当高,60s 的语音采用8K 采样率、16bit的样本大小,可以达到35K-90K的文件大小。缺点就是 iOS原生是不支持的,需要借助第三方库来实现编解码,比如著名的OpenCore-AMR。(实际开发中还发现 网上能搜到的只有8K采样率的编解码器,16K的只有编码,没有解码,32K以上的搜不到)

      然后是speex, 这种编码格式是专门用于speex这个开源库的,好处之一是开源,而且里面实现了语音的降噪、回音消除等功能,特别适合于VOIP的开发。不过缺点就是整个框架是一体的,单独拆开某个模块来使用可能不太容易,而我们录制和播放的过程中有其他的处理,并不想使用他的那一套代码。而且貌似speex已经不维护了,现在更名为 opus。这个就没有仔细去研究了。 

      aac 这种格式是iOS原生支持的,而且据说声音效果很好,但是缺点就是文件有点大,而且安卓原生sdk也不支持。如果是要做一个iOS本地的语音app,我觉得还是挺不错的。

      wav 就不用多说了,用过windows的都知道,微软自家的,苹果对他的支持也不错,缺点嘛,跟aac一样,太大,只适合在本地录制和播放,不适合移动网络下的传输。


    为了兼容安卓,并且考虑到移动网络的带宽,我们最终选定了amr -nb作为传输格式, 也就是amr 8Khz采样率的编码格式。(其实这里还有一个坑,后面再说)本地的播放我们采用 wav,因为wav是无损的,可以在录制的时候变声,而且可以很方便的转换到amr。


2. 录制和播放

     选择好编码后,就是录制语音。网上有很多开源的代码,我们选择的是官方的SpeakHere,技术上用的是AudioQueueService 来录制和播放。SpeakHere的代码很久没更新了,有些不兼容 ios7 的,我们下载之后解决了很多warning,才开始用起来。 关于AudioQueueService的相关知识,可以自行谷歌百度,总得来说就是通过一个异步队列去获取麦克风的数据(录制) 或者是即将输出的数据(播放),属于CoreAudio框架的一部分。  语音录制还有AudioUnit, AudioFileStream等方案, AudioUnit相对来讲复杂一些,需要自己设定inputBus,outBus,设的不好就什么都听不到。AudioFileStream 是语音流的,可以从服务器上直接播放,也可以下载到本地后再播放(录制?这个好像不能用来录制)。

从项目开发的角度上来看,AudioQueue是最适合的,所以我们果断选择了AudioQueue。


3. 转码

    前面讲到wav跟amr的转换,我们采用的就是先解码wav,将wav的头部信息去除掉,然后读出pcm的音频数据,同时打开一个新的文件,写入amr的头部信息,再将pcm一帧一帧的转换成amr的数据,写入到amr文件中。反之亦然。

      原理挺简单,但是真正做起来还是碰到很多问题。 首先就是录音时候的分辨率跟转码时候的分辨率、变声的分辨率都要一致,否则就会出现声音走样,或者根本不能播放的问题。然后在32位和64位机器上,wav的编码有一点点不一样,主要是32位上采样率和声道数以十六进制写进去的时候需要去掉低字节,至于原因,我也不是很清楚,可能是OC与C++混编导致的吧,这种混编能避免就尽量避免,否则一些莫名奇妙的问题真的不好解决。还有一个就是原本安卓是支持直接编解码amr的,但是由于我们需要做变声处理,不能直接录制完就存起来,而安卓本身MideaPlayer和AudioPlayer封装的很死,没有提供转换编码的方法,所以只能再去源码中提取OpenCore这个库,重新编译so文件。安卓的同学有么有感觉特别坑爹啊~~


4. 变声

   变声我们采用的soundTouch的开源库,现在做变声基本都是用这个。这个库可以做到变调、变速的自己组合,使用起来很简单,先初始化一个soundTouch的实例,然后把录音获取的数据 调用putSamples 方法传进去,然后调用receiveSamples 再传出来,然后再写到自己的文件里。需要注意的是,soundTouch里面的sampleType是根据你的样本大小来定的,如果是16bit的大小,就对应short类型,如果是32bit的大小,就对应float或者int。


5. 下载和播放逻辑

     下载这块其实跟音频关系不大,但是项目中也碰到了不少问题,所以还是提一下。

      我们的音频文件都是按消息中带的Id去服务器拉取的,然后下载到本地再按顺序播放,所以需要一个队列去控制这块的逻辑。最开始我们是把下载跟播放完全分开,下载模块就管理下载的队列,播放模块只管理播放的队列。但是后面发现不行,因为下载是异步的,而播放是同步的,所以还需要一个中间的controller去协调。

      队列控制我们采用的是NSOperationQueue,这里有个问题,就是ios7以后, isConcurrent不能用了,取而代之的是一个叫isAsynchorized的方法,按官方文档的说法,isAsynchorized设为NO,应该就是同步的,但是实测发现完全不靠谱,还是并发的下载,而且有多少个operation,就有多少个线程!难道是我理解错了,同步跟非并发不能对等? 没办法,我们只能再增加一个 maxConcurrentOperationCount = 1 的限制,这才变成了同步的下载。

      由于AudioQueue的回调函数本身是异步执行的,所以当把播放的operation加到队列里面之后,又出现一个问题,就是播放了一两秒之后,就会停下来。这是因为operation结束了,AudioQueue的回调函数就终止了。要解决这个问题,还得再一个runloop去阻塞线程,并且监听AudioQueue 的运行状态,当最后一帧播放完成后,AudioQueue会将running属性置为false,这时候再结束播放的线程就OK了。


按正常情况来说,语音到这里其实也就差不多了,但是要做的事情其实很有很多。由于iPhone麦克风的问题,会将很多杂音录制进来,所以还需要降噪、自动增益,然后还有语音的播放模式问题(听筒跟扬声器的切换)等等,后面抽时间再写吧。


郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。