首页 > 代码库 > iOS音频播放 (四):AudioFile 转

iOS音频播放 (四):AudioFile 转

原文出处 : http://msching.github.io/blog/2014/07/19/audio-in-ios-4/

前言

接着第三篇的AudioStreamFile这一篇要来聊一下AudioFile。和AudioStreamFile一样AudioFileAudioToolBox framework中的一员,它也可以完毕第一篇所述的第2步,读取音频格式信息和进行帧分离。但其实它的功能远不止如此。


AudioFile介绍

依照官方文档的描写叙述:

a C programming interface that enables you to read or write a wide variety of audio data to or from disk or a memory buffer.With Audio File Services you can:

  • Create, initialize, open, and close audio files
  • Read and write audio files
  • Optimize audio files
  • Work with user data and global information

这个类能够用来创建、初始化音频文件;读写音频数据;对音频文件进行优化;读取和写入音频格式信息等等。功能十分强大。可见它不但能够用来支持音频播放。甚至能够用来生成音频文件。当然,在本篇文章中仅仅会涉及一些和音频播放相关的内容(打开音频文件、读取格式信息、读取音频数据,事实上我也仅仅对这些方法有一点了解。其余的功能没用过。

。>_<).


AudioFile的打开“姿势”

AudioFile提供了两个打开文件的方法:

1、 AudioFileOpenURL

1
2
3
4
5
6
7
8
9
10
enum {
  kAudioFileReadPermission      = 0x01,
  kAudioFileWritePermission     = 0x02,
  kAudioFileReadWritePermission = 0x03
};

extern OSStatus AudioFileOpenURL (CFURLRef inFileRef,
                                  SInt8 inPermissions,
                                  AudioFileTypeID inFileTypeHint,
                                  AudioFileID * outAudioFile);

从方法的定义上来看是用来读取本地文件的:

第一个參数,文件路径;

第二个參数,文件的同意使用方式,是读、写还是读写。假设打开文件后进行了同意使用方式以外的操作。就得到kAudioFilePermissionsError错误码(比方Open时声明是kAudioFileReadPermission但却调用了AudioFileWriteBytes);

第三个參数,和AudioFileStream的open方法中一样是一个帮助AudioFile解析文件的类型提示。假设文件类型确定的话应当传入;

第四个參数,返回AudioFile实例相应的AudioFileID。这个ID须要保存起来作为兴许一些方法的參数使用;

返回值用来推断是否成功打开文件(OSSStatus == noErr)。


2、 AudioFileOpenWithCallbacks

1
2
3
4
5
6
7
extern OSStatus AudioFileOpenWithCallbacks (void * inClientData,
                                            AudioFile_ReadProc inReadFunc,
                                            AudioFile_WriteProc inWriteFunc,
                                            AudioFile_GetSizeProc inGetSizeFunc,
                                            AudioFile_SetSizeProc inSetSizeFunc,
                                            AudioFileTypeID inFileTypeHint,
                                            AudioFileID * outAudioFile);

看过第一个Open方法后,这种方法乍看上去让人有点迷茫,没有URL的參数怎样告诉AudioFile该打开哪个文件?还是先来看一下參数的说明吧:

第一个參数。上下文信息,不再多做解释;

第二个參数。当AudioFile须要读音频数据时进行的回调(调用Open和Read方式后同步回调);

第三个參数。当AudioFile须要写音频数据时进行的回调(写音频文件功能时使用,暂不讨论)。

第四个參数,当AudioFile须要用到文件的总大小时回调(调用Open和Read方式后同步回调)。

第五个參数。当AudioFile须要设置文件的大小时回调(写音频文件功能时使用。暂不讨论)。

第六、七个參数和返回值同AudioFileOpenURL方法。

这种方法的重点在于AudioFile_ReadProc这个回调。换一个角度理解。这种方法相比于第一个方法自由度更高,AudioFile须要的仅仅是一个数据源,不管是磁盘上的文件、内存里的数据甚至是网络流仅仅要能在AudioFile须要数据时(Open和Read时)通过AudioFile_ReadProc回调为AudioFile提供合适的数据就能够了,也就是说用法不仅仅能够读取本地文件也能够如AudioFileStream一样以流的形式读取数据。


以下来看一下AudioFile_GetSizeProcAudioFile_ReadProc这两个读取功能相关的回调

1
2
3
4
5
6
7
typedef SInt64 (*AudioFile_GetSizeProc)(void * inClientData);

typedef OSStatus (*AudioFile_ReadProc)(void * inClientData,
                                       SInt64 inPosition,
                                       UInt32 requestCount,
                                       void * buffer,
                                       UInt32 * actualCount);

首先是AudioFile_GetSizeProc回调。这个回调非常好理解,返回文件总长度就可以,总长度的获取途径自然是文件系统或者httpResponse等等。

接下来是AudioFile_ReadProc回调:

第一个參数,上下文对象,不再赘述;

第二个參数,须要读取第几个字节開始的数据;

第三个參数,须要读取的数据长度;

第四个參数,返回參数,是一个数据指针而且其空间已经被分配。我们须要做的是把数据memcpy到buffer中。

第五个參数。实际提供的数据长度,即memcpy到buffer中的数据长度;

返回值,假设没有不论什么异常产生就返回noErr,假设有异常能够依据异常类型选择须要的error常量返回(一般用不到其它返回值,返回noErr就足够了)。

这里须要解释一下这个回调方法的工作方式。AudioFile须要数据时会调用回调方法,须要数据的时间点有两个:

  1. Open方法调用时,因为AudioFile的Open方法调用过程中就会对音频格式信息进行解析。仅仅有符合要求的音频格式才干被成功打开否则Open方法就会返回错误码(换句话说。Open方法一旦调用成功就相当于AudioStreamFile在Parse后返回ReadyToProducePackets一样,仅仅要Open成功就能够開始读取音频数据。详见第三篇),所以在Open方法调用的过程中就须要提供一部分音频数据来进行解析;

  2. Read相关方法调用时,这个不须要多说非常好理解;

通过回调提供数据时须要注意inPosition和requestCount參数,这两个參数指明了本次回调须要提供的数据范围是从inPosition開始requestCount个字节的数据。这里又能够分为两种情况:

  1. 有充足的数据:那么我们须要把这个范围内的数据复制到buffer中,而且给actualCount赋值requestCount,最后返回noError;

  2. 数据不足:没有充足数据的话就仅仅能把手头有的数据复制到buffer中。须要注意的是这部分被拷贝的数据必须是从inPosition開始的连续数据,拷贝完毕后给actualCount赋值实际拷贝进buffer中的数据长度后返回noErr。这个过程能够用以下的代码来表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static OSStatus MyAudioFileReadCallBack(void *inClientData,
                                        SInt64 inPosition,
                                        UInt32 requestCount,
                                        void *buffer,
                                        UInt32 *actualCount)
{
    __unsafe_unretained MyContext *context = (__bridge MyContext *)inClientData;

    *actualCount = [context availableDataLengthAtOffset:inPosition maxLength:requestCount];
    if (*actualCount > 0)
    {
        NSData *data = [context dataAtOffset:inPosition length:*actualCount];
        memcpy(buffer, [data bytes], [data length]);
    }

    return noErr;
}

讲到这里又须要分两种情况:

2.1. Open方法调用时的回调数据不足:AudioFile的Open方法会依据文件格式类型分几步进行数据读取以解析确定是否是一个合法的文件格式,当中每一步的inPosition和requestCount都不一样。假设某一步不成功就会直接进行下一步,假设几部下来都失败了。那么Open方法就会失败。简单的说就是在调用Open之前首先须要保证音频文件的格式信息完整。这就意味着AudioFile并不能独立用于音频流的读取,在流播放时首先须要使用AudioStreamFile来得到ReadyToProducePackets标志位来保证信息完整。

2.2. Read方法调用时的回调数据不足:这样的情况下inPosition和requestCount的数值与Read方法调用时传入的參数有关,数据不足对于Read方法本身没有影响。仅仅要回调返回noErr,Read就成功,仅仅是实际交给Read方法的调用方的数据会不足,那么就把这个问题的处理交给了Read的调用方;


读取音频格式信息

成功打开音频文件后就能够读取当中的格式信息了,读取用到的方法例如以下:

1
2
3
4
5
6
7
8
9
extern OSStatus AudioFileGetPropertyInfo(AudioFileID inAudioFile,
                                         AudioFilePropertyID inPropertyID,
                                         UInt32 * outDataSize,
                                         UInt32 * isWritable);
                                      
extern OSStatus AudioFileGetProperty(AudioFileID inAudioFile,
                                     AudioFilePropertyID inPropertyID,
                                     UInt32 * ioDataSize,
                                     void * outPropertyData);    

AudioFileGetPropertyInfo方法用来获取某个属性相应的数据的大小(outDataSize)以及该属性能否够被write(isWritable),而AudioFileGetProperty则用来获取属性相应的数据。对于一些大小可变的属性须要先使用AudioFileGetPropertyInfo获取数据大小才干取获取数据(比如formatList),而有些确定类型单个属性则不必先调用AudioFileGetPropertyInfo直接调用AudioFileGetProperty就可以(比方BitRate)。样例例如以下:

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
AudioFileID fileID; //Open方法返回的AudioFileID

//获取格式信息
UInt32 formatListSize = 0;
OSStatus status = AudioFileGetPropertyInfo(_fileID, kAudioFilePropertyFormatList, &formatListSize, NULL);
if (status == noErr)
{
    AudioFormatListItem *formatList = (AudioFormatListItem *)malloc(formatListSize);
    status = AudioFileGetProperty(fileID, kAudioFilePropertyFormatList, &formatListSize, formatList);
    if (status == noErr)
    {
        for (int i = 0; i * sizeof(AudioFormatListItem) < formatListSize; i += sizeof(AudioFormatListItem))
        {
            AudioStreamBasicDescription pasbd = formatList[i].mASBD;
            //选择须要的格式。。                             
        }
    }
    free(formatList);
}

//获取码率
UInt32 bitRate;
UInt32 bitRateSize = sizeof(bitRate);
status = AudioFileGetProperty(fileID, kAudioFilePropertyBitRate, &size, &bitRate);
if (status != noErr)
{
    //错误处理
}

能够获取的属性有以下这些,大家能够參考文档来获取自己须要的信息(注意到这里有EstimatedDuration,能够得到Duration了):

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
enum
{
  kAudioFilePropertyFileFormat             =    ffmt,
  kAudioFilePropertyDataFormat             =    dfmt,
  kAudioFilePropertyIsOptimized            =    optm,
  kAudioFilePropertyMagicCookieData        =    mgic,
  kAudioFilePropertyAudioDataByteCount     =    bcnt,
  kAudioFilePropertyAudioDataPacketCount   =    pcnt,
  kAudioFilePropertyMaximumPacketSize      =    psze,
  kAudioFilePropertyDataOffset             =    doff,
  kAudioFilePropertyChannelLayout          =    cmap,
  kAudioFilePropertyDeferSizeUpdates       =    dszu,
  kAudioFilePropertyMarkerList             =    mkls,
  kAudioFilePropertyRegionList             =    rgls,
  kAudioFilePropertyChunkIDs               =    chid,
  kAudioFilePropertyInfoDictionary         =    info,
  kAudioFilePropertyPacketTableInfo        =    pnfo,
  kAudioFilePropertyFormatList             =    flst,
  kAudioFilePropertyPacketSizeUpperBound   =    pkub,
  kAudioFilePropertyReserveDuration        =    rsrv,
  kAudioFilePropertyEstimatedDuration      =    edur,
  kAudioFilePropertyBitRate                =    brat,
  kAudioFilePropertyID3Tag                 =    id3t,
  kAudioFilePropertySourceBitDepth         =    sbtd,
  kAudioFilePropertyAlbumArtwork           =    aart,
  kAudioFilePropertyAudioTrackCount        =    atct,
  kAudioFilePropertyUseAudioTrack          =    uatk
}; 

读取音频数据

读取音频数据的方法分为两类:

1、直接读取音频数据:

1
2
3
4
5
extern OSStatus AudioFileReadBytes (AudioFileID inAudioFile,
                                    Boolean inUseCache,
                                    SInt64 inStartingByte,
                                    UInt32 * ioNumBytes,
                                    void * outBuffer);

第一个參数。FileID。

第二个參数,是否须要cache,一般来说传false。

第三个參数,从第几个byte開始读取数据

第四个參数。这个參数在调用时作为输入參数表示须要读取读取多少数据。调用完毕后作为输出參数表示实际读取了多少数据(即Read回调中的requestCount和actualCount);

第五个參数。buffer指针,须要事先分配好足够大的内存(ioNumBytes大,即Read回调中的buffer,所以Read回调中不须要再分配内存);

返回值表示是否读取成功,EOF时会返回kAudioFileEndOfFileError

使用这种方法得到的数据都是没有进行过帧分离的数据,假设想要用来播放或者解码还必须通过AudioFileStream进行帧分离。

2、按帧(Packet)读取音频数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
extern OSStatus AudioFileReadPacketData (AudioFileID inAudioFile,
                                         Boolean inUseCache,
                                         UInt32 * ioNumBytes,
                                         AudioStreamPacketDescription * outPacketDescriptions,
                                         SInt64 inStartingPacket,
                                         UInt32 * ioNumPackets,
                                         void * outBuffer);
                                      

extern OSStatus AudioFileReadPackets (AudioFileID inAudioFile,
                                      Boolean inUseCache,
                                      UInt32 * outNumBytes,
                                      AudioStreamPacketDescription * outPacketDescriptions,
                                      SInt64 inStartingPacket,
                                      UInt32 * ioNumPackets,
                                      void * outBuffer);

按帧读取的方法有两个,这两个方法看上去几乎相同,就连參数也差点儿同样,但使用场景和效率上却有所不同,官方文档中如此描写叙述这两个方法:

  • AudioFileReadPacketData is memory efficient when reading variable bit-rate (VBR) audio data;
  • AudioFileReadPacketData is more efficient than AudioFileReadPackets when reading compressed file formats that do not have packet tables, such as MP3 or ADTS. This function is a good choice for reading either CBR (constant bit-rate) or VBR data if you do not need to read a fixed duration of audio.
  • Use AudioFileReadPackets only when you need to read a fixed duration of audio data, or when you are reading only uncompressed audio.

仅仅有当须要读取固定时长音频或者非压缩音频时才会用到AudioFileReadPackets。其余时候使用AudioFileReadPacketData会有更高的效率而且更省内存。

以下来看看这些參数:

第一、二个參数,同AudioFileReadBytes

第三个參数,对于AudioFileReadPacketData来说ioNumBytes这个參数在输入输出时都要用到。在输入时表示outBuffer的size。输出时表示实际读取了多少size的数据。而对AudioFileReadPackets来说outNumBytes仅仅在输出时使用,表示实际读取了多少size的数据;

第四个參数,帧信息数组指针,在输入前须要分配内存,大小必须足够存在ioNumPackets个帧信息(ioNumPackets * sizeof(AudioStreamPacketDescription));

第五个參数,在输入时表示须要读取多少个帧,在输出时表示实际读取了多少帧。

第六个參数。outBuffer数据指针,在输入前就须要分配好空间,这个參数看上去两个方法一样但事实上并不是如此。对于AudioFileReadPacketData来说仅仅要分配近似帧大小 * 帧数的内存空间就可以,方法本身会针对给定的内存空间大小来决定最后输出多少个帧,假设空间不够会适当降低出的帧数;而对于AudioFileReadPackets来说则须要分配最大帧大小(或帧大小上界) * 帧数的内存空间才行(最大帧大小和帧大小上界的差别等下会说);这也就是为何第三个參数一个是输入输出双向使用的,而还有一个仅仅是输出时使用的原因。

就这点来说两个方法中前者在使用的过程中要比后者更省内存;

返回值。同AudioFileReadBytes

这两个方法读取后的数据为帧分离后的数据,能够直接用来播放或者解码。

以下给出两个方法的使用代码(以MP3为例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
AudioFileID fileID; //Open方法返回的AudioFileID
UInt32 ioNumPackets = ...; //要读取多少个packet
SInt64 inStartingPacket = ...; //从第几个Packet開始读取

UInt32 bitRate = ...; //AudioFileGetProperty读取kAudioFilePropertyBitRate
UInt32 sampleRate = ...; //AudioFileGetProperty读取kAudioFilePropertyDataFormat或kAudioFilePropertyFormatList
UInt32 byteCountPerPacket = 144 * bitRate / sampleRate; //MP3数据每一个Packet的近似大小

UInt32 descSize = sizeof(AudioStreamPacketDescription) * ioNumPackets;
AudioStreamPacketDescription * outPacketDescriptions = (AudioStreamPacketDescription *)malloc(descSize);

UInt32 ioNumBytes = byteCountPerPacket * ioNumPackets;
void * outBuffer = (void *)malloc(ioNumBytes);

OSStatus status = AudioFileReadPacketData(fileID,
                                          false,
                                          &ioNumBytes,
                                          outPacketDescriptions,
                                          inStartingPacket,
                                          &ioNumPackets,
                                          outBuffer);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
AudioFileID fileID; //Open方法返回的AudioFileID
UInt32 ioNumPackets = ...; //要读取多少个packet
SInt64 inStartingPacket = ...; //从第几个Packet開始读取

UInt32 maxByteCountPerPacket = ...; //AudioFileGetProperty读取kAudioFilePropertyMaximumPacketSize,最大的packet大小
//也能够用:
//UInt32 byteCountUpperBoundPerPacket = ...; //AudioFileGetProperty读取kAudioFilePropertyPacketSizeUpperBound,当前packet大小上界(未扫描全文件的情况下)

UInt32 descSize = sizeof(AudioStreamPacketDescription) * ioNumPackets;
AudioStreamPacketDescription * outPacketDescriptions = (AudioStreamPacketDescription *)malloc(descSize);

UInt32 outNumBytes = 0
UInt32 ioNumBytes = maxByteCountPerPacket * ioNumPackets;
void * outBuffer = (void *)malloc(ioNumBytes);

OSStatus status = AudioFileReadPackets(fileID,
                                       false,
                                       &outNumBytes,
                                       outPacketDescriptions,
                                       inStartingPacket,
                                       &ioNumPackets,
                                       outBuffer);

Seek

seek的思路和之前讲AudioFileStream时讲到的是一样的,差别在于AudioFile没有方法来帮助修正seek的offset和seek的时间:

  • 使用AudioFileReadBytes时须要计算出approximateSeekOffset
  • 使用AudioFileReadPacketData或者AudioFileReadPackets时须要计算出seekToPacket

approximateSeekOffset和seekToPacket的计算方法參见第三篇。


关闭AudioFile

AudioFile使用完成后须要调用AudioFileClose进行关闭,没啥特别须要注意的。

1
extern OSStatus AudioFileClose (AudioFileID inAudioFile);  

小结

本篇针对AudioFile的音频读取功能做了介绍。小结一下:

  • AudioFile有两个Open方法,须要针对自身的使用场景选择不同的方法;

  • AudioFileOpenURL用来读取本地文件

  • AudioFileOpenWithCallbacks的使用场景比前者要广泛。使用时须要注意AudioFile_ReadProc,这个回调方法在Open方法本身和Read方法被调用时会被同步调用

  • 必须保证音频文件格式信息可读时才干使用AudioFile的Open方法,AudioFile并不能独立用于音频流的读取。须要配合AudioStreamFile使用才干读取流(须要用AudioStreamFile来推断文件格式信息可读之后再调用Open方法)。

  • 使用AudioFileGetProperty读取格式信息时须要推断所读取的信息是否须要先调用AudioFileGetPropertyInfo获得数据大小后再进行读取;

  • 读取音频数据应该依据使用的场景选择不同的音频读取方法,对于不同的读取方法seek时须要计算的变量也不同样;

  • AudioFile使用完成后须要调用AudioFileClose进行关闭;


演示样例代码

对于本地文件用AudioFile读取比較简单就不在这里提供demo了,对于流播放中的AudioFile使用推荐大家阅读豆瓣的开源播放器代码DOUAudioStreamer。


下篇预告

下一篇将讲述怎样使用AudioQueue

iOS音频播放 (四):AudioFile 转