iOS 音频流播(三)

本篇我们介绍AudioFile和AudioFileStream。在第一篇技术栈的分析里,我们提到过AudioFile和AudioFileStream都可以用来解析采样率、码率、时长等信息,分离原始音频数据中的音频帧。这两个都可以使用在流式播放中,当然,不仅限于流播,本地音频也一样可以使用。
  可能有的小伙伴会有疑问,既然它们俩功能相似,选择其中一个不就可以了吗?其实不然,AudioFile的功能远比AudioFileStream强大,除了共同的解析音频数据分离音频帧之外,它还可以读取音频数据,甚至可以写音频(生成音频文件),而AudioFileStream本身没有读取音频数据的功能。看起来选择AudioFile就OK了,不料AudioFile却需要AudioFileStream来保证数据的完整性,否则会大大增加出错的可能性(别急别急,且看下文一一道来)。
  下面我们就先认识一下Mr.AudioFileStream。

初始化AudioFileStream

首先当然是调用AudioFileStreamOpen()生成一个AudioFileStream实例,函数声明如下:

OSStatus    
AudioFileStreamOpen (
                            void * __nullable                       inClientData,
                            AudioFileStream_PropertyListenerProc    inPropertyListenerProc,
                            AudioFileStream_PacketsProc             inPacketsProc,
                            AudioFileTypeID                         inFileTypeHint,
                            AudioFileStreamID __nullable * __nonnull outAudioFileStream)
  • 第一个参数,inClientData是一个上下文对象,会回传给回调函数。必须保证inClientData生命周期足够长,否则在回调函数里使用的时候就是一个野指针了。一般我们会传自身(self),在回调函数取出后再调用自身的实例方法(C和OC虽然可以混编,但是C函数并不是作为OC类的实例方法存在的,所以C函数里的self并不指代OC对象)。
  • 第二个参数,inPropertyListenerProc是歌曲信息解析的回调,每解析出一个就会进行一次回调。
  • 第三个参数,inPacketsProc是分离音频帧的回调,每解析出一部分音频帧就会进行一次回调。
  • 第四个参数,inFileTypeHint是音频文件格式的描述信息,这个参数来帮助AudioFileStream对文件格式进行解析。这个参数在文件信息不完整(例如信息有缺陷)时尤其有用,它可以给与AudioFileStream一定的提示,帮助其绕过文件中的错误或者缺失从而成功解析文件,如果无法确定可以传入0。
// AudioToolBox定义的AudioFileTypeID
CF_ENUM(AudioFileTypeID) {
        kAudioFileAIFFType              = 'AIFF',
        kAudioFileAIFCType              = 'AIFC',
        kAudioFileWAVEType              = 'WAVE',
        kAudioFileSoundDesigner2Type    = 'Sd2f',
        kAudioFileNextType              = 'NeXT',
        kAudioFileMP3Type               = 'MPG3',   // mpeg layer 3
        kAudioFileMP2Type               = 'MPG2',   // mpeg layer 2
        kAudioFileMP1Type               = 'MPG1',   // mpeg layer 1
        kAudioFileAC3Type               = 'ac-3',
        kAudioFileAAC_ADTSType          = 'adts',
        kAudioFileMPEG4Type             = 'mp4f',
        kAudioFileM4AType               = 'm4af',
        kAudioFileM4BType               = 'm4bf',
        kAudioFileCAFType               = 'caff',
        kAudioFile3GPType               = '3gpp',
        kAudioFile3GP2Type              = '3gp2',       
        kAudioFileAMRType               = 'amrf'        
};
  • 第五个参数,outAudioFileStream代表生成的AudioFileStream实例,这个参数必须保存起来作为后续一些方法的参数使用。
  • 返回值表示是否调用成功(status == noErr),关于OSStatus的解释,可以参阅这里

注意:在播放网络音频时,很多链接并没有指明音频格式,此时可以根据MIME type来确定音频格式,而本地音频可以根据文件扩展名确定。MIME type与扩展名有关,用于确定文件的类型。 在HTTP请求中,MIME type通过请求头中的 Content-Type 表示。iOS中可通过 <MobileCoreServices/UTType.h> 中定义的相关方法可以实现 fileExtension <--> UTType <--> mimeType 的互转。具体转换方法可以看我之前的一篇博文

解析音频数据

上文AudioFileStream并没有提供读取音频数据的接口,所以音频数据的读取需要自行实现。本地播放可以通过NSFileHandle提供的接口,流播时通过HTTP请求获得。在得到音频数据之后,调用AudioFileStreamParseBytes()就可以进行解析了。

OSStatus
AudioFileStreamParseBytes(  
                                AudioFileStreamID               inAudioFileStream,
                                UInt32                          inDataByteSize,
                                const void *                    inData,
                                AudioFileStreamParseFlags       inFlags)
  • 第一个参数,inAudioFileStream是初始化时得到的AudioFileStreamID。
  • 第二个参数,inDataByteSize是本次解析的数据长度。
  • 第三个参数,inData是本次解析的音频数据。
  • 第四个参数,inFlags表示本次解析与上一次是否是连续关系。在第一篇中我们提到过形如MP3的数据都以帧的形式存在的,解析时也需要以帧为单位解析。但在解码之前我们不可能知道每个帧的边界在第几个字节,所以就会出现这样的情况:我们传给AudioFileStreamParseBytes的数据在解析完成之后会有一部分数据余下来,这部分数据是接下去那一帧的前半部分,如果再次有数据输入需要继续解析时就必须要用到前一次解析余下来的数据才能保证帧数据完整,所以在正常播放的情况下传入0即可。需要传入kAudioFileStreamParseFlag_Discontinuity的情况有两个,一个是在seek完毕之后,显然seek后的数据和之前的数据完全无关;另一个和AudioFileStream的bug有关,在回调得到kAudioFileStreamProperty_ReadyToProducePackets之后,在正常解析第一包之前最好都传入kAudioFileStreamParseFlag_Discontinuity。
  • 返回值表示本次解析是否成功(同样status == noErr)。

注意:AudioFileStreamParseBytes()函数每次调用都必须检查返回值,一旦出错就没有必要继续解析了。注意一下若是返回这个kAudioFileStreamError_NotOptimized,说明这个音频文件无法流播,只能下载完所有数据才能播放。

解析歌曲信息

调用AudioFileStreamParseBytes()之后首先会解析歌曲信息,每解析出一个,同步回调AudioFileStream_PropertyListenerProc。

typedef void (*AudioFileStream_PropertyListenerProc)(
                                            void *                          inClientData,
                                            AudioFileStreamID               inAudioFileStream,
                                            AudioFileStreamPropertyID       inPropertyID,
                                            AudioFileStreamPropertyFlags *  ioFlags)
  • 第一个参数,inClientData是初始化时指定的上下文信息。
  • 第二个参数,inAudioFileStream指代AudioFileStream对象。
  • 第三个参数,inPropertyID表示音频的信息,可以通过AudioFileStreamGetProperty()函数取值。
  • 第四个参数,ioFlags表示这个property是否需要被缓存。

来看一下AudioFileStreamGetProperty()函数。

OSStatus
AudioFileStreamGetProperty( 
                            AudioFileStreamID                   inAudioFileStream,
                            AudioFileStreamPropertyID           inPropertyID,
                            UInt32 *                            ioPropertyDataSize,
                            void *                              outPropertyData)
  • 第一个参数,inAudioFileStream指代AudioFileStream对象。
  • 第二个参数,inPropertyID表示想获取哪个property。
  • 第三个参数,想要获取的property所表示的数据结构大小,对于大小不定的propertyID,需要先调用AudioFileStreamGetPropertyInfo()函数先获取一下大小,比如kAudioFileStreamProperty_FormatList。
  • 第四个参数,outPropertyData是一个返回参数,会返回获取的property的值。
// AudioFileStream 定义的所有propertyID
CF_ENUM(AudioFileStreamPropertyID)
{
    kAudioFileStreamProperty_ReadyToProducePackets          =   'redy',
    kAudioFileStreamProperty_FileFormat                     =   'ffmt',
    kAudioFileStreamProperty_DataFormat                     =   'dfmt',
    kAudioFileStreamProperty_FormatList                     =   'flst',
    kAudioFileStreamProperty_MagicCookieData                =   'mgic',
    kAudioFileStreamProperty_AudioDataByteCount             =   'bcnt',
    kAudioFileStreamProperty_AudioDataPacketCount           =   'pcnt',
    kAudioFileStreamProperty_MaximumPacketSize              =   'psze',
    kAudioFileStreamProperty_DataOffset                     =   'doff',
    kAudioFileStreamProperty_ChannelLayout                  =   'cmap',
    kAudioFileStreamProperty_PacketToFrame                  =   'pkfr',
    kAudioFileStreamProperty_FrameToPacket                  =   'frpk',
    kAudioFileStreamProperty_PacketToByte                   =   'pkby',
    kAudioFileStreamProperty_ByteToPacket                   =   'bypk',
    kAudioFileStreamProperty_PacketTableInfo                =   'pnfo',
    kAudioFileStreamProperty_PacketSizeUpperBound           =   'pkub',
    kAudioFileStreamProperty_AverageBytesPerPacket          =   'abpp',
    kAudioFileStreamProperty_BitRate                        =   'brat',
    kAudioFileStreamProperty_InfoDictionary                 =   'info'
};

几个比较有用的propertyID

  • kAudioFileStreamProperty_DataOffset:第一篇提到MP3文件有一个头信息,之后才是真正的音频数据,这个属性表示的就是头信息的大小,在seek操作时有比较大的作用。对于用户来讲,seek操作操作的是时间,但对于编码来讲,我们seek的是文件位置,seek时会根据时间计算出音频数据的字节offset然后需要再加上音频数据的offset才能得到在文件中的真正offset。。
// 注意数据类型一定不能错,否则会获取不到想要的结果。
SInt64 dataOffset;
UInt32 offsetSize = sizeof(dataOffset);
OSStatus status = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_DataOffset, &offsetSize, &dataOffset);
if (status != noErr)
{
    //错误处理
}
  • kAudioFileStreamProperty_AudioDataByteCount:表示真正可播放的音频数据大小(除去头信息),很明显也可以这么计算audioDataByteCount = fileSize - dataOffset。
UInt64 audioDataByteCount;
UInt32 byteCountSize = sizeof(audioDataByteCount);
OSStatus status = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_AudioDataByteCount, &byteCountSize, &audioDataByteCount);
if (status != noErr)
{
    //错误处理
}
  • kAudioFileStreamProperty_BitRate:获取码率可以用来计算音频时长(AudioFileStream没有提供直接获取音频时长的接口)。文件大小与码率的关系在第一篇提到过,从而可得 duration = ((fileSize - dataOffset) * 8) / bitRate。
UInt32 bitRate;
UInt32 bitRateSize = sizeof(bitRate);
OSStatus status = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_BitRate, &bitRateSize, &bitRate);
if (status != noErr)
{
    //错误处理
}
  • kAudioFileStreamProperty_DataFormat:表示音频文件结构信息,是一个AudioStreamBasicDescription的结构体。
struct AudioStreamBasicDescription
{
    // 采样率
    Float64             mSampleRate;
    // 音频的类型,MP3 or WAV
    AudioFormatID       mFormatID;
    // 随mFormatID而定
    AudioFormatFlags    mFormatFlags;
   //  每个数据包中的字节数
    UInt32              mBytesPerPacket;
   // 每个数据包的帧数(第一篇提到过,原始数据如PCM一包一帧,压缩格式如MP3一包多帧)
    UInt32              mFramesPerPacket;
   // 每帧的字节数
    UInt32              mBytesPerFrame;
   // 每帧的声道数
    UInt32              mChannelsPerFrame;
   // 每个声道的采样位数
    UInt32              mBitsPerChannel;
   // 与内存对齐有关
    UInt32              mReserved;
};
// 获取format
AudioStreamBasicDescription asbd;
UInt32 asbdSize = sizeof(asbd);
OSStatus status = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_DataFormat, &asbdSize, &asbd);
if (status != noErr)
{
    //错误处理
} 
  • kAudioFileStreamProperty_FormatList:作用和kAudioFileStreamProperty_DataFormat是一样的,区别在于用这个PropertyID获取到是一个AudioStreamBasicDescription的数组,这个参数是用来支持AAC,SBR这样的包含多个文件类型的音频格式。由于到底有多少个format我们并不知晓,所以需要先获取一下总数据大小:
//获取数据大小
Boolean outWriteable;
UInt32 formatListSize;
OSStatus status = AudioFileStreamGetPropertyInfo(inAudioFileStream, kAudioFileStreamProperty_FormatList, &formatListSize, &outWriteable);
if (status != noErr)
{
    //错误处理
}
//获取formatlist
AudioFormatListItem *formatList = malloc(formatListSize);
OSStatus status = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_FormatList, &formatListSize, formatList);
if (status != noErr)
{
    //错误处理
}
//选择需要的格式
for (int i = 0; i * sizeof(AudioFormatListItem) < formatListSize; i++)
{
    AudioStreamBasicDescription pasbd = formatList[i].mASBD;
    //选择需要的格式。。                             
}
free(formatList);
  • kAudioFileStreamProperty_ReadyToProducePackets:这个PropertyID可以不必获取对应的值,一旦回调中这个PropertyID出现就代表解析完成,接下来可以对音频数据进行帧分离了。
分离音频帧

歌曲信息读取完整后,继续调用AudioFileStreamParseBytes()方法可以对帧进行分离,并同步的进入AudioFileStream_PacketsProc回调方法。

typedef void (*AudioFileStream_PacketsProc)(void * inClientData,
                                            UInt32 numberOfBytes,
                                            UInt32 numberOfPackets,
                                            const void * inInputData,
                                            AudioStreamPacketDescription * inPacketDescriptions);
  • 第一个参数,inClientData是初始化时传入的上下文对象。
  • 第二个参数,numberOfBytes表示本次处理的音频数据总量。
  • 第三个参数,numberOfPackets表示本次处理的数据包数。
  • 第四个参数,inInputData表示本次处理的所有数据。
  • 第五个参数,AudioStreamPacketDescription数组,存储了每一帧数据是从第几个字节开始的,这一帧总共多少字节。
//这里的mVariableFramesInPacket是指实际的数据帧
//只有VBR的数据才能用到(像MP3这样的压缩数据一个帧里会有好几个数据帧)
struct  AudioStreamPacketDescription
{
    // 音频数据从哪里开始
    SInt64  mStartOffset;
    UInt32  mVariableFramesInPacket;
    // 这个数据包的大小
    UInt32  mDataByteSize;
};

AudioFileStream的具体用法可以戳这里。在Xcode-->Edit Scheme中添加启动参数为本地音频文件的路径即可。

关闭AudioFileStream

AudioFileStream使用完毕后需要调用AudioFileStreamClose()进行关闭。

extern OSStatus AudioFileStreamClose(AudioFileStreamID inAudioFileStream); 

关于Mr.AudioFileStream的介绍到这里就差不多了,我们接着说它的大兄弟Mr.AudioFile。当然我们讨论的是音频播放相关的内容,对于AudioFile仅会使用到它解析分离音频帧的部分,对于写音频,这里就不讨论啦。

初始化AudioFile

AudioFile提供了两种读取音频文件的方法。第一种是通过文件路径(因此仅能处理本地音频,略过略过)。第二种是在AudioFile解析分离音频帧的时候提供音频数据给它。在流播的时候,音频数据正好是一点一点通过HTTP请求返回的,所以我们把返回的数据一一提供给AudioFile就可以解析了;对于本地文件,可以用NSFIleHandle读取数据提供给它,效果相同。

OSStatus AudioFileOpenWithCallbacks (void * inClientData,
                                            AudioFile_ReadProc inReadFunc,
                                            AudioFile_WriteProc inWriteFunc,
                                            AudioFile_GetSizeProc inGetSizeFunc,
                                            AudioFile_SetSizeProc inSetSizeFunc,
                                            AudioFileTypeID inFileTypeHint,
                                            AudioFileID * outAudioFile);
  • 第一个参数,inClientData是上下文对象。
  • 第二个参数,在AudioFile需要数据时由inReadFunc回调提供。
  • 第三个参数,inWriteFunc与写音频有关,略过。
  • 第四个参数,通过inGetSizeFunc回调告诉AudioFile要解析的音频文件大小,流播中通过请求头的Content-Length获得。
  • 第五个参数,inSetSizeFunc与写音频有关,略过。
  • 第六个参数,inFileTypeHint同AudioFileStream,是文件格式的提示信息,同AudioFileStream。
  • 第七个参数,outAudioFile是生成的AudioFile实例,保存起来留给其它函数当参数使用。

注意:初始化AudioFile时就需要提供音频数据给它,除此之外在调用AudioFileReadXXX()相关方法时也需要提供合适的音频数据给它。

上面说到AudioFile在解析分离音频帧时需要通过两个回调函数通知它。先来看一下提供音频数据的回调 —— AudioFile_ReadProc。

typedef OSStatus (*AudioFile_ReadProc)(void * inClientData,
                                       SInt64 inPosition,
                                       UInt32 requestCount,
                                       void * buffer,
                                       UInt32 * actualCount);
  • 第一个参数,inClientData是初始化时传递的上下文对象。
  • 第二个参数,inPosition指明了AudioFile需要从什么位置开始读取音频数据,也就是说从第几个字节开始。
  • 第三个参数,requestCount指明AudioFile请求读取的数据量,只是请求,不代表最后读取的数据量。
  • 第四个参数,buffer是一个数据指针并且其空间已经被分配,我们需要做的是把数据memcpy到buffer中。
  • 第五个参数,actualCount是实际提供的数据长度,即memcpy到buffer中的数据长度。
  • 如果没有出错,返回noErr即可。

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

  • AudioFileOpenWithCallbacks()方法调用时,由于AudioFile的open方法调用过程中就会对音频格式信息进行解析,只有符合要求的音频格式才能被成功打开否则open方法就会返回错误码(换句话说,open方法一旦调用成功就相当于AudioFileStream调用AudioFileStreamParseBytes()后返回ReadyToProducePackets
    一样,只要open成功就可以开始读取音频数据,所以在open方法调用的过程中就需要提供一部分音频数据来进行解析。
  • AudioFileReadXXX()相关方法调用时,读取数据时当然需要提供数据了。

通过回调提供数据时需要注意inPosition和requestCount参数,这两个参数指明了本次回调需要提供的数据范围是从inPosition开始的 requestCount个连续字节的数据。这里又可以分为两种情况:

  • 有充足的数据:那么我们需要把这个范围内的数据拷贝到buffer中,并且给actualCount赋值requestCount,最后返回noError;
  • 数据不足:没有充足数据的话就只能把手头有的数据拷贝到buffer中,需要注意的是这部分被拷贝的数据必须是从inPosition开始的连续数据,拷贝完成后给actualCount赋值实际拷贝进buffer中的数据长度后返回noErr,这个过程可以用下面的代码来表示:
// totalData表示当前拥有的所有音频数据,NSData类型
static OSStatus mAudioFile_ReadProc(
                                     void *     inClientData,
                                     SInt64     inPosition,
                                     UInt32     requestCount,
                                     void *     buffer,
                                     UInt32 *   actualCount)
{
    // 如果需要读取的长度超过拥有的数据长度
    if (inPosition + requestCount > [totalData length]) {
        // 如果读取起点的位置已经超过或等于拥有的数据长度了
        if (inPosition >= [totalData length]) {
            // 此时真正读取长度就没有了
            *actualCount = 0;
        }else{
            // 否则总共拥有的数据长度减去起点就是能读到的所有数据了
            *actualCount = (UInt32)([totalData length] - inPosition);
        }
    }else{
        // 若是不比拥有的数据长度大
        // 真正读取的就是请求的长度
        *actualCount = requestCount;
    }
    
    // EOF 整个文件读取结束
    if (*actualCount == 0) return noErr;
    
    // 最后将从inPosition开始,长度为actualCount的数据拷贝到buffer中
    memcpy(buffer, (uint8_t *)[totalData bytes] + inPosition, *actualCount);
    // 返回noErr
    return noErr;
}

说到这里又需要分两种情况(oh-oh,妈妈再也不用担心我不会分类了):

  • AudioFileOpenWithCallbacks()方法调用时的回调数据不足:AudioFile的Open方法会根据音频文件格式分几步进行数据读取,接着解析以确定是否是一个合法的文件格式,其中每一步的inPosition和requestCount都不一样,如果某一步不成功就会直接进行下一步,如果几部下来都失败了,那么open方法就会失败。简单的说就是在调用open之前首先需要保证音频文件的格式信息完整,这就意味着AudioFile并不能独立用于音频流的读取,在流播放时首先需要使用AudioStreamFile来得到ReadyToProducePackets标志位来保证信息完整;
  • AudioFileReadXXX()方法调用时的回调数据不足:这种情况下inPosition和requestCount的数值与AudioFileReadXXX()方法调用时传入的参数有关,数据不足对于Read方法本身没有影响,只要回调返回noErr,AudioFileReadXXX()就成功,只是实际交给AudioFileReadXXX()方法的调用方的数据会不足,那么就把这个问题的处理交给了AudioFileReadXXX()的调用方,对应播放器的状态就是buffering;
解析音频信息

读数据时AudioFile和AudioFileStream差不多,成功打开AudioFile之后就可以获取歌曲信息了,包括比特率,音频时长等。

OSStatus AudioFileGetPropertyInfo(AudioFileID inAudioFile,
                                         AudioFilePropertyID inPropertyID,
                                         UInt32 * outDataSize,
                                         UInt32 * isWritable);
                                      
OSStatus AudioFileGetProperty(AudioFileID inAudioFile,
                                     AudioFilePropertyID inPropertyID,
                                     UInt32 * ioDataSize,
                                     void * outPropertyData); 

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

- (BOOL)_fillFileFormat
{
    UInt32 size;
    OSStatus status;
    
    // 支持AAC SBR类型的文件
    // kAudioFilePropertyFormatList返回的是AudioFormatListItem数组
    status = AudioFileGetPropertyInfo(_fileID, kAudioFilePropertyFormatList, &size, NULL);
    if (status != noErr) {
        return NO;
    }
    
    // 求出有多少个
    UInt32 numFormats = size / sizeof(AudioFormatListItem);
    // 分配好内存
    AudioFormatListItem *formatList = (AudioFormatListItem *)malloc(size);
    
    // 获取值
    status = AudioFileGetProperty(_fileID, kAudioFilePropertyFormatList, &size, formatList);
    if (status != noErr) {
        free(formatList);
        return NO;
    }
    
    // 只有一个的话直接取出来
    if (numFormats == 1) {
        _fileFormat = formatList[0].mASBD;
    }
    else {
        
        status = AudioFormatGetPropertyInfo(kAudioFormatProperty_DecodeFormatIDs, 0, NULL, &size);
        if (status != noErr) {
            free(formatList);
            return NO;
        }
        
        UInt32 numDecoders = size / sizeof(OSType);
        OSType *decoderIDS = (OSType *)malloc(size);
        
        status = AudioFormatGetProperty(kAudioFormatProperty_DecodeFormatIDs, 0, NULL, &size, decoderIDS);
        if (status != noErr) {
            free(formatList);
            free(decoderIDS);
            return NO;
        }
        
        UInt32 i;
        for (i = 0; i < numFormats; ++i) {
            OSType decoderID = formatList[i].mASBD.mFormatID;
            
            BOOL found = NO;
            for (UInt32 j = 0; j < numDecoders; ++j) {
                if (decoderID == decoderIDS[j]) {
                    found = YES;
                    break;
                }
            }
            
            if (found) {
                break;
            }
        }
        
        free(decoderIDS);
        
        if (i >= numFormats) {
            free(formatList);
            return NO;
        }
        
        _fileFormat = formatList[i].mASBD;
    }
    
    free(formatList);
    return YES;
}

- (BOOL)_fillMiscProperties
{
    UInt32 size;
    OSStatus status;
    
    UInt32 bitRate = 0;
    size = sizeof(bitRate);
    status = AudioFileGetProperty(_fileID, kAudioFilePropertyBitRate, &size, &bitRate);
    if (status != noErr) {
        return NO;
    }
    _bitRate = bitRate;
    
    SInt64 dataOffset = 0;
    size = sizeof(dataOffset);
    status = AudioFileGetProperty(_fileID, kAudioFilePropertyDataOffset, &size, &dataOffset);
    if (status != noErr) {
        return NO;
    }
    _dataOffset = (NSUInteger)dataOffset;
    
    Float64 estimatedDuration = 0.0;
    size = sizeof(estimatedDuration);
    status = AudioFileGetProperty(_fileID, kAudioFilePropertyEstimatedDuration, &size, &estimatedDuration);
    if (status != noErr) {
        return NO;
    }
    _estimatedDuration = estimatedDuration;
    
    return YES;
}

读取音频数据

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

  • 直接读取音频数据:
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进行帧分离。

  • 按包(Packet)读取音频数据:
OSStatus AudioFileReadPacketData (AudioFileID inAudioFile,
                                         Boolean inUseCache,
                                         UInt32 * ioNumBytes,
                                         AudioStreamPacketDescription * outPacketDescriptions,
                                         SInt64 inStartingPacket,
                                         UInt32 * ioNumPackets,
                                         void * outBuffer);          
OSStatus AudioFileReadPackets (AudioFileID inAudioFile,
                                      Boolean inUseCache,
                                      UInt32 * outNumBytes,
                                      AudioStreamPacketDescription * outPacketDescriptions,
                                      SInt64 inStartingPacket,
                                      UInt32 * ioNumPackets,
                                      void * outBuffer);

按包读取的方法有两个,这两个方法看上去差不多,就连参数也几乎相同,但使用场景和效率上却有所不同。只有当需要读取固定时长音频或者非压缩音频时才会用到AudioFileReadPackets(),其余时候使用AudioFileReadPacketData()会有更高的效率并且更省内存(所以AudioFileReadPackets()已经被标记为deprecated~~);
下面来看看这些参数:

  • 第一、二个参数,同AudioFileReadBytes。
  • 第三个参数,对于AudioFileReadPacketData()来说ioNumBytes这个参数在输入输出时都要用到,在输入时表示outBuffer的size,输出时表示实际读取了多少size的数据。而对AudioFileReadPackets()来说outNumBytes只在输出时使用,表示实际读取了多少size的数据;
  • 第四个参数,帧信息数组指针,在输入前需要分配内存,大小必须足够存储ioNumPackets个帧信息(ioNumPackets * sizeof(AudioStreamPacketDescription))。
  • 第五个参数,从第几帧开始读取数据。
  • 第六个参数,在输入时表示需要读取多少个帧,在输出时表示实际读取了多少帧。
  • 第七个参数,outBuffer数据指针,在输入前就需要分配好空间,这个参数看上去两个方法一样但其实并非如此。对于AudioFileReadPacketData()来说只要分配近似帧大小 * 帧数的内存空间即可,方法本身会针对给定的内存空间大小来决定最后输出多少个帧,如果空间不够会适当减少出的帧数;而对于AudioFileReadPackets()来说则需要分配最大帧大小(或帧大小上界) * 帧数的内存空间才行;这也就是为何第三个参数一个是输入输出双向使用的,而另一个只是输出时使用的原因。就这点来说两个方法中前者在使用的过程中要比后者更省内存。
  • 返回值,同AudioFileReadBytes。

这两个方法读取后的数据为帧分离后的数据,可以直接用来播放或者解码。具体使用可以参考这里;

关闭AudioFile

AudioFile使用完毕后需要调用AudioFileClose进行关闭。

extern OSStatus AudioFileClose (AudioFileID inAudioFile);  

下一篇会介绍AudioConverter。

说明:很多话我是直接从这里直接拿过来的,作者总结的非常好,可能我表述来表述去也就那么个意思,所以就直接拿来用了。有些地方,包括我自己遇到的坑,会做一些补充说明,大家知道就好。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容