记音频播放的两个错误
之前读了ijkPlayer的代码,然后跟着写了整个流程,也可以播放了。最近想把音视频的知识总结规整下,所以想着从头开始写一个播放器,凭记忆写,遇到问题再去查,尽量不去看成熟的大段的代码。只有这样才能把那些不懂的地方暴露出来,就跟你看着参考答案是永远不会解题的。
写完后,很幸运,视频一下就可以了,但是音频却是“莎莎莎”的,只能听到非常微小的原声。
- AV_SAMPLE_FMT_FLTP
一开始我是想先测一下,就没有管格式里的带plane的,然后播放之后就看了下,解码后的音频格式是AV_SAMPLE_FMT_FLTP
。FFmpeg
的音频格式定义在AVSampleFormat
里,后缀里带P的都是Planar类型。
音频分多个声道,每个声道的数据可能在一起,也可能是分离的。这个属性跟iOS里AudioStreamBasicDescription
的NonInteractive(非交错的)
是一个概念。
AAC解码后默认就是AV_SAMPLE_FMT_FLTP
,所以就加入了planar的支持。但还是播放失败。
- 画图
音频这种东西,耳朵听得出来有问题,但是你不知道哪里有问题。感觉就是混入了杂音,不断的有一些点很响。
没办法了就画图,就那种音波图,把采样点的数值转化成高度显示出来,就像这种:
IMG_4882.PNG
然后发现有些地方数值很小,而且是一段一段的,然后突然想这错误的一段是不是刚好对应一次音频数据填充,然后把画图的颜色改成交替的,就是一段黄一段黑。果然是这样,然后在代码里留下log标记,这种程序调试也就靠log了。
再用系统的解码器+audioUnit播放,是正确的,也把图画出来,然后对比。
还有这时已经换成1个声道的44100采样率的caf文件,caf内存就是纯的pcm没有编码。这样我就把解码和重采样的因素移除了。
发现是memcpy(buffer, dataBuffer, needReadSize);
的问题,buffer
是要播放的缓冲区,dataBuffer
是AVFrame
的数据或者resample后的源数据,needReadSize
是读取大小。
看起来好像没错,可实际dataBuffer
的类型是uint8_t **
,它和AVFrame
的extended_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,这个跟音频源一样,然后AudioQueue
和AudioUnit
最大的一个区别是,缓冲区的大小是自己设的,这决定了每一次读取缓冲区的大小,然后正好填了一个和音频源的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的数值就可以。 - 之所以能找出问题,是因为代码走了某些特殊的分支,测试需要覆盖每种不同的分支。很多时候是你的某个分支代码块是对的,某些事错的,然后混在一起,导致结果看起来更难分辨。
最重要的,不要太相信眼睛看+脑子里的逻辑想象,需要实际的输入+输出的测试!