RTC-Audio

iOS 音频流播(三)

2017-05-27  本文已影响789人  anyoptional

本篇我们介绍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)
// 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'        
};

注意:在播放网络音频时,很多链接并没有指明音频格式,此时可以根据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)

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

解析歌曲信息

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

typedef void (*AudioFileStream_PropertyListenerProc)(
                                            void *                          inClientData,
                                            AudioFileStreamID               inAudioFileStream,
                                            AudioFileStreamPropertyID       inPropertyID,
                                            AudioFileStreamPropertyFlags *  ioFlags)

来看一下AudioFileStreamGetProperty()函数。

OSStatus
AudioFileStreamGetProperty( 
                            AudioFileStreamID                   inAudioFileStream,
                            AudioFileStreamPropertyID           inPropertyID,
                            UInt32 *                            ioPropertyDataSize,
                            void *                              outPropertyData)
// 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

// 注意数据类型一定不能错,否则会获取不到想要的结果。
SInt64 dataOffset;
UInt32 offsetSize = sizeof(dataOffset);
OSStatus status = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_DataOffset, &offsetSize, &dataOffset);
if (status != noErr)
{
    //错误处理
}
UInt64 audioDataByteCount;
UInt32 byteCountSize = sizeof(audioDataByteCount);
OSStatus status = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_AudioDataByteCount, &byteCountSize, &audioDataByteCount);
if (status != noErr)
{
    //错误处理
}
UInt32 bitRate;
UInt32 bitRateSize = sizeof(bitRate);
OSStatus status = AudioFileStreamGetProperty(inAudioFileStream, kAudioFileStreamProperty_BitRate, &bitRateSize, &bitRate);
if (status != noErr)
{
    //错误处理
}
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)
{
    //错误处理
} 
//获取数据大小
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);
分离音频帧

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

typedef void (*AudioFileStream_PacketsProc)(void * inClientData,
                                            UInt32 numberOfBytes,
                                            UInt32 numberOfPackets,
                                            const void * inInputData,
                                            AudioStreamPacketDescription * inPacketDescriptions);
//这里的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);

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

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

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

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

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

// 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,妈妈再也不用担心我不会分类了):

解析音频信息

读数据时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);

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

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~~);
下面来看看这些参数:

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

关闭AudioFile

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

extern OSStatus AudioFileClose (AudioFileID inAudioFile);  

下一篇会介绍AudioConverter。

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

上一篇下一篇

猜你喜欢

热点阅读