记音频播放的两个错误

335 阅读6分钟

之前读了ijkPlayer的代码,然后跟着写了整个流程,也可以播放了。最近想把音视频的知识总结规整下,所以想着从头开始写一个播放器,凭记忆写,遇到问题再去查,尽量不去看成熟的大段的代码。只有这样才能把那些不懂的地方暴露出来,就跟你看着参考答案是永远不会解题的。

写完后,很幸运,视频一下就可以了,但是音频却是“莎莎莎”的,只能听到非常微小的原声。

  • AV_SAMPLE_FMT_FLTP

一开始我是想先测一下,就没有管格式里的带plane的,然后播放之后就看了下,解码后的音频格式是AV_SAMPLE_FMT_FLTPFFmpeg的音频格式定义在AVSampleFormat里,后缀里带P的都是Planar类型。

音频分多个声道,每个声道的数据可能在一起,也可能是分离的。这个属性跟iOS里AudioStreamBasicDescriptionNonInteractive(非交错的)是一个概念。

AAC解码后默认就是AV_SAMPLE_FMT_FLTP,所以就加入了planar的支持。但还是播放失败。

  • 画图

音频这种东西,耳朵听得出来有问题,但是你不知道哪里有问题。感觉就是混入了杂音,不断的有一些点很响。

没办法了就画图,就那种音波图,把采样点的数值转化成高度显示出来,就像这种:

IMG_4882.PNG

然后发现有些地方数值很小,而且是一段一段的,然后突然想这错误的一段是不是刚好对应一次音频数据填充,然后把画图的颜色改成交替的,就是一段黄一段黑。果然是这样,然后在代码里留下log标记,这种程序调试也就靠log了。

再用系统的解码器+audioUnit播放,是正确的,也把图画出来,然后对比。

还有这时已经换成1个声道的44100采样率的caf文件,caf内存就是纯的pcm没有编码。这样我就把解码和重采样的因素移除了。

发现是memcpy(buffer, dataBuffer, needReadSize);的问题,buffer是要播放的缓冲区,dataBufferAVFrame的数据或者resample后的源数据,needReadSize是读取大小。

看起来好像没错,可实际dataBuffer的类型是uint8_t **,它和AVFrameextended_data一样,代表是多个Plane的内存,对每层的数据是dataBuffer[i],这才是真实有效的数据。而我是分plane处理的,所以这里正确的写法是memcpy(buffer, dataBuffer[i], needReadSize);,i是plane的索引。

总结:

  • 贴近内存的编程,void*是个危险的东西,你传给它指针还是指针的指针,它都不会报错。
  • 移除干扰因素,把AAC的视频播放改成s16pcm+1channel+44.1k的纯音频,移除解码和重采样的可能错误。
  • 找到正确的对比:一个是画成图,眼睛可以很直观的看出来,而且播放完,图还保留,可以慢慢对比。一个是用了正确的播放手段得到正确的结果来对比。**人都说经验重要,但其实成功的经验才是最重要的!**一个成功的,一个失败的,才能找到问题的关键,到不了成功的那一边,永远不知道问题在哪里。

但到头来,这本质是一个愚蠢的代码错误,甚至不会带来什么技术上的深刻理解,就是思维的错误

  • AAC的播放

解决上面的问题,pcm的播放没问题了,但是AAC的还是有问题。有了上一个问题的经验,我猜想问题最可能还是出在了我的代码里。所以我把代码再检查了一边,尼玛没什么问题。

再看看音波图,看不出什么规律,就是好像变粗了,

IMG_4881.PNG

上一张是正确效果,对比比较明显的只有每个音柱变粗了,波长变大了。但耳朵听来好像差别不大,然后我去查*“音频波长对听感有什么影响?”“变音的原理是什么?”*。可惜没查出什么。

然后逐步排查,先看重采样:swr_convert。看输入的nb_samples,分配的buffer大小有没有影响。可惜没什么用,不过倒是把swr_convert的各个细节给摸透的。

最后,想着我是用AudioUnit写的音频播放,把之前学ijkPlayer时的AudioQueue的播放器拿来试一试有没有区别。

然后迎来了转机:我把采样率调成48000,这个跟音频源一样,然后AudioQueueAudioUnit最大的一个区别是,缓冲区的大小是自己设的,这决定了每一次读取缓冲区的大小,然后正好填了一个和音频源的frame在resample之后的一样的大小。

产生的效果就是:每次AudioQueue读取音频数据都刚好是一个frame的。然后杂音都消失了,激动啊!有了成功的经验,找到问题就不远了!

然后我把缓冲区调大一倍,这样的效果是每次音频读取,正好需要两个frame,然后就出现了下面的音波图:

IMG_4885.PNG

很明确了,只有前一半的数据拿到了,另一半都是0。

然后使用打印内存的手段,一步步的定位错误的位置。打印内存就是:

#define TFMPPrintBuffer_S16(buffer, start, length)\
signed short *checkP = ((signed short*)buffer)+start;\
for(int i = 0; i<length;i++){\
    printf("%d ",*checkP);\
    checkP++;\
}\
printf("\n-------------\n");

因为播放是s16格式的音频,所以使用SInt16,就是signed short

只要打印出来都是00000000的那就是出问题的位置了。

最后定位到问题: memcpy(buffer, dataBuffer, linesize);buffer是播放的缓冲区,dataBuffer是单层数据,linesize是单层的大小。

也算比较隐蔽,问题出在buffer,如果之前已经拷贝了一段内存,那么之后拷贝的位置就该延后,否则就会把之前的覆盖了,之前的丢失,现在的也错位了。而buffer我竟然没有做偏移。

所以上面的现象就是,第一个frame把内存拷贝进去,然后后一个frame的内存有覆盖在了第一个内存上,所以后半段是空的了。

正确的是: memcpy(buffer+(oneLineSize - needReadSize), dataBuffer, linesize); oneLineSize是总长度,needReadSize是剩余长度。或者在每次memcpy后,都把buffer前移拷贝的内存大小。

然后各种播放都好了,sampleRate、bufferSize、channel修改都不受影响。

总结:

  • 不幸的是第二个问题还是一个愚蠢的代码错误。
  • 需要做测试,单元测试。两个问题都是在fillAudioBuffer函数里,也就是外层播放器需要音频数据了,使用这个函数来获取数据。如果我做一个测试:输入模拟的AVFrame,然后查看输出的音频buffer是否符合。不要全部一样,只要查某个offset的数值就可以。
  • 之所以能找出问题,是因为代码走了某些特殊的分支,测试需要覆盖每种不同的分支。很多时候是你的某个分支代码块是对的,某些事错的,然后混在一起,导致结果看起来更难分辨。

最重要的,不要太相信眼睛看+脑子里的逻辑想象,需要实际的输入+输出的测试!

项目代码TFMediaPlayer