Java中AudioFileStream_iOS音频播放 (三):AudioFileStream_2

4、kAudioFileStreamProperty_FormatList

作用和kAudioFileStreamProperty_DataFormat是一样的,区别在于用这个PropertyID获取到是一个AudioStreamBasicDescription的数组,这个参数是用来支持AAC

SBR这样的包含多个文件类型的音频格式。由于到底有多少个format我们并不知晓,所以需要先获取一下总数据大小:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

//获取数据大小Boolean outWriteable;UInt32 formatListSize;OSStatus status = AudioFileStreamGetPropertyInfo(inAudioFileStream, kAudioFileStreamProperty_FormatList, &formatListSize, &outWriteable);if (status != noErr){//错误处理}//获取formatlistAudioFormatListItem *formatList = malloc(formatListSize);OSStatus status = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_FormatList, &formatListSize, formatList);if (status != noErr){//错误处理}//选择需要的格式for (int i = 0; i * sizeof(AudioFormatListItem) < formatListSize; i += sizeof(AudioFormatListItem)){AudioStreamBasicDescription pasbd = formatList[i].mASBD;//选择需要的格式。。}free(formatList);

5、kAudioFileStreamProperty_AudioDataByteCount

顾名思义,音频文件中音频数据的总量。这个Property的作用一是用来计算音频的总时长,二是可以在seek时用来计算时间对应的字节offset。

1

2

3

4

5

6

7

UInt64 audioDataByteCount;UInt32 byteCountSize = sizeof(audioDataByteCount);OSStatus status = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_AudioDataByteCount, &byteCountSize, &audioDataByteCount);if (status != noErr){//错误处理}

2014.8.2

补充: 发现在流播放的情况下,有时数据流量比较小时会出现ReadyToProducePackets还是没有获取到audioDataByteCount的情况,这时就需要近似计算audioDataByteCount。一般来说音频文件的总大小一定是可以得到的(利用文件系统或者Http请求中的contentLength),那么计算方法如下:

1

2

3

UInt32 dataOffset = ...; //kAudioFileStreamProperty_DataOffsetUInt32 fileLength = ...; //音频文件大小UInt32 audioDataByteCount = fileLength - dataOffset;

5、kAudioFileStreamProperty_ReadyToProducePackets

这个PropertyID可以不必获取对应的值,一旦回调中这个PropertyID出现就代表解析完成,接下来可以对音频数据进行帧分离了。

计算时长Duration

获取时长的最佳方法是从ID3信息中去读取,那样是最准确的。如果ID3信息中没有存,那就依赖于文件头中的信息去计算了。

计算duration的公式如下:

1

double duration = (audioDataByteCount * 8) / bitRate

音频数据的字节总量audioDataByteCount可以通过kAudioFileStreamProperty_AudioDataByteCount获取,码率bitRate可以通过kAudioFileStreamProperty_BitRate获取也可以通过Parse一部分数据后计算平均码率来得到。

对于CBR数据来说用这样的计算方法的duration会比较准确,对于VBR数据就不好说了。所以对于VBR数据来说,最好是能够从ID3信息中获取到duration,获取不到再想办法通过计算平均码率的途径来计算duration。

分离音频帧

读取格式信息完成之后继续调用AudioFileStreamParseBytes方法可以对帧进行分离,并同步的进入AudioFileStream_PacketsProc回调方法。

a4c26d1e5885305701be709a3d33442f.png

回调的定义:

1

2

3

4

5

typedef void (*AudioFileStream_PacketsProc)(void * inClientData,UInt32 inNumberBytes,UInt32 inNumberPackets,const void * inInputData,AudioStreamPacketDescription * inPacketDescriptions);

第一个参数,一如既往的上下文对象;

第二个参数,本次处理的数据大小;

第三个参数,本次总共处理了多少帧(即代码里的Packet);

第四个参数,本次处理的所有数据;

第五个参数,AudioStreamPacketDescription数组,存储了每一帧数据是从第几个字节开始的,这一帧总共多少字节。

1

2

3

4

5

6

7

8

//AudioStreamPacketDescription结构//这里的mVariableFramesInPacket是指实际的数据帧只有VBR的数据才能用到(像MP3这样的压缩数据一个帧里会有好几个数据帧)struct AudioStreamPacketDescription{SInt64 mStartOffset;UInt32 mVariableFramesInPacket;UInt32 mDataByteSize;};

下面是我按照自己的理解实现的回调方法片段:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

static void MyAudioFileStreamPacketsCallBack(void *inClientData,UInt32 inNumberBytes,UInt32 inNumberPackets,const void *inInputData,AudioStreamPacketDescription *inPacketDescriptions){//处理discontinuous..if (numberOfBytes == 0 || numberOfPackets == 0){return;}BOOL deletePackDesc = NO;if (packetDescriptioins == NULL){//如果packetDescriptioins不存在,就按照CBR处理,平均每一帧的数据后生成packetDescriptioinsdeletePackDesc = YES;UInt32 packetSize = numberOfBytes / numberOfPackets;packetDescriptioins = (AudioStreamPacketDescription *)malloc(sizeof(AudioStreamPacketDescription) * numberOfPackets);for (int i = 0; i < numberOfPackets; i++){UInt32 packetOffset = packetSize * i;descriptions[i].mStartOffset = packetOffset;descriptions[i].mVariableFramesInPacket = 0;if (i == numberOfPackets - 1){packetDescriptioins[i].mDataByteSize = numberOfBytes - packetOffset;}else{packetDescriptioins[i].mDataByteSize = packetSize;}}}for (int i = 0; i < numberOfPackets; ++i){SInt64 packetOffset = packetDescriptioins[i].mStartOffset;UInt32 packetSize = packetDescriptioins[i].mDataByteSize;//把解析出来的帧数据放进自己的buffer中...}if (deletePackDesc){free(packetDescriptioins);}}

inPacketDescriptions这个字段为空时需要按CBR的数据处理。但其实在解析CBR数据时inPacketDescriptions一般也会有返回,因为即使是CBR数据帧的大小也不是恒定不变的,例如CBR的MP3会在每一帧的数据后放1

byte的填充位,这个填充位也并非时时刻刻存在,所以帧的大小会有1

byte的浮动。(比如采样率44.1KHZ,码率160kbps的CBR

MP3文件每一帧的大小在522字节和523字节浮动。所以不能因为有inPacketDescriptions没有返回NULL而判定音频数据就是VBR编码的)。

Seek

就音频的角度来seek功能描述为“我要拖到xx分xx秒”,而实际操作时我们需要操作的是文件,所以我们需要知道的是“我要拖到xx分xx秒”这个操作对应到文件上是要从第几个字节开始读取音频数据。

对于原始的PCM数据来说每一个PCM帧都是固定长度的,对应的播放时长也是固定的,但一旦转换成压缩后的音频数据就会因为编码形式的不同而不同了。对于CBR而言每个帧中所包含的PCM数据帧是恒定的,所以每一帧对应的播放时长也是恒定的;而VBR则不同,为了保证数据最优并且文件大小最小,VBR的每一帧中所包含的PCM数据帧是不固定的,这就导致在流播放的情况下VBR的数据想要做seek并不容易。这里我们也只讨论CBR下的seek。

CBR数据的seek一般是这样实现的(参考并修改自matt的blog):

1、近似地计算应该seek到哪个字节

1

2

3

4

5

6

7

double seekToTime = ...; //需要seek到哪个时间,秒为单位UInt64 audioDataByteCount = ...; //通过kAudioFileStreamProperty_AudioDataByteCount获取的值SInt64 dataOffset = ...; //通过kAudioFileStreamProperty_DataOffset获取的值double durtion = ...; //通过公式(AudioDataByteCount * 8) / BitRate计算得到的时长//近似seekOffset = 数据偏移 + seekToTime对应的近似字节数SInt64 approximateSeekOffset = dataOffset + (seekToTime / duration) * audioDataByteCount;

2、计算seekToTime对应的是第几个帧(Packet)

我们可以利用之前Parse得到的音频格式信息来计算PacketDuration。audioItem.fileFormat.mFramesPerPacket

/audioItem.fileFormat.mSampleRate;

1

2

3

4

5

6

//首先需要计算每个packet对应的时长AudioStreamBasicDescription asbd = ...; 通过kAudioFileStreamProperty_DataFormat或者kAudioFileStreamProperty_FormatList获取的值double packetDuration = asbd.mFramesPerPacket / asbd.mSampleRate//然后计算packet位置SInt64 seekToPacket = floor(seekToTime / packetDuration);

3、使用AudioFileStreamSeek计算精确的字节偏移和时间

AudioFileStreamSeek可以用来寻找某一个帧(Packet)对应的字节偏移(byte

offset):

如果找到了就会把ioFlags加上kAudioFileStreamSeekFlag_OffsetIsEstimated,并且给outDataByteOffset赋值,outDataByteOffset就是输入的seekToPacket对应的字节偏移量,我们可以根据outDataByteOffset来计算出精确的seekOffset和seekToTime;

如果没找到那么还是应该用第1步计算出来的approximateSeekOffset来做seek;

1

2

3

4

5

6

7

8

9

10

11

12

13

14

SInt64 seekByteOffset;UInt32 ioFlags = 0;SInt64 outDataByteOffset;OSStatus status = AudioFileStreamSeek(audioFileStreamID, seekToPacket, &outDataByteOffset, &ioFlags);if (status == noErr && !(ioFlags & kAudioFileStreamSeekFlag_OffsetIsEstimated)){//如果AudioFileStreamSeek方法找到了帧的字节偏移,需要修正一下时间seekToTime -= ((seekByteOffset - dataOffset) - outDataByteOffset) * 8.0 / bitRate;seekByteOffset = outDataByteOffset + dataOffset;}else{seekByteOffset = approximateSeekOffset;}

4、按照seekByteOffset读取对应的数据继续使用AudioFileStreamParseByte进行解析

如果是网络流可以通过设置range头来获取字节,本地文件的话直接seek就好了。调用AudioFileStreamParseByte时注意刚seek完第一次Parse数据需要加参数kAudioFileStreamParseFlag_Discontinuity。

关闭AudioFileStream

AudioFileStream使用完毕后需要调用AudioFileStreamClose进行关闭,没啥特别需要注意的。

1

extern OSStatus AudioFileStreamClose(AudioFileStreamID inAudioFileStream);

小结

本篇关于AudioFileStream做了详细介绍,小结一下:

使用AudioFileStream首先需要调用AudioFileStreamOpen,需要注意的是尽量提供inFileTypeHint参数帮助AudioFileStream解析数据,调用完成后记录AudioFileStreamID;

当有数据时调用AudioFileStreamParseBytes进行解析,每一次解析都需要注意返回值,返回值一旦出现noErr以外的值就代表Parse出错,其中kAudioFileStreamError_NotOptimized代表该文件缺少头信息或者其头信息在文件尾部不适合流播放;

使用AudioFileStreamParseBytes需要注意第四个参数在需要合适的时候传入kAudioFileStreamParseFlag_Discontinuity;

调用AudioFileStreamParseBytes后会首先同步进入AudioFileStream_PropertyListenerProc回调来解析文件格式信息,如果回调得到kAudioFileStreamProperty_ReadyToProducePackets表示解析格式信息完成;

解析格式信息完成后继续调用AudioFileStreamParseBytes会进入MyAudioFileStreamPacketsCallBack回调来分离音频帧,在回调中应该将分离出来的帧信息保存到自己的buffer中

seek时需要先近似的计算seekTime对应的seekByteOffset,然后利用AudioFileStreamSeek计算精确的offset,如果能得到精确的offset就修正一下seektime,如果无法得到精确的offset就用之前的近似结果

AudioFileStream使用完毕后需要调用AudioFileStreamClose进行关闭;

示例代码

AudioStreamer和FreeStreamer这两个优秀的开源播放器都用到AudioFileStream大家可以借鉴。我自己也写了一个简单的AudioFileStream封装。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值