AudioUnit之-录制音频保存为m4a/CAF/WAV文件和播放m4a/CAF/WAV文件(三)

前言

直有时候我们采集音频后希望将它通过网络传输给另一端播放,比如直播时,主播录制的声音通过网络传输给观众再播放,那肯定要先将裸音频数据先压缩了在传输,比如我们可以用aac编码方式压缩,可以用MP3编码方式压缩,他们都可以在保证音质效果的前提下极大的压缩音频数据,至于aac和MP3算法的区别和原理这里就不讨论了。其实能进行aac和mp3压缩的方式有很多,比如可以用FFMpeeg库,也可以用苹果自带的AudioToolbox框架,这里将讨论如何用AudioUnit实现aac压缩音频数据并保存到M4A封装格式中,经过试验,ios不止MP3的压缩方式。如果要选用MP3则需要使用ffmpeg库了

AudioUnit音频系列

AudioUnit之-播放裸PCM音频文件(一)
AudioUnit之-录制音频+耳返(二)
AudioUnit之-录制音频保存为m4a/CAF/WAV文件和播放m4a/CAF/WAV文件(三)
AudioUnit之-录制音频并添加背景音乐(四)
AudioUnit之-generic output(离线)混合音频文件(五)

实现思路

1、录制并保存到m4a中:
前面实现了如何录制音频,如何获取录制的原始音频数据,那么只需要在前文的基础上实现将原始音频数据进行编码,比如AAC编码,然后封装到m4a文件中,如下:
原始音频数据回调(1)-->获取音频数据(2)-->编码(3)-->封装到m4a文件中(4)
这里第(3)步和第(4)步使用AudioUnit file Service即可实现。
2、从m4a文件中读取音频数据并播放:
前面实现了从文件中播放PCM裸数据,使用NSInputStream直接读取音频数据,然后输入给RemoteIO Unit的Element0 Input scope即可实现播放PCM文件
播放m4a等文件的流程如下:
RemoteIO Unit要音频数据回调(1)-->从m4a文件中读取压缩音频数据(2)-->解码为PCM音频数据(3)-->输入给RemoteIO Unit的Element0 Input scope实现播放(4)
这里第(2)步和第(3)步使用AudioUnit file Service即可实现。
总而言之:AudioUnit file Service即可实现保存和播放m4a音频文件的需求。再展开来,caf/wav则一样的流程,只需要更改一下配置参数即可。
那么这个AudioUnit file Service是个撒东东呢?先简单了解下它的特性:

它位于
1、ExtAudioFile 是AudioUnit的一个组件,它提供了将原始音频数据编码为WAV,caff等编码格式的音频数据,同时提供写入文件的接口
2、同时它还提供了从文件中读取数据解码为PCM音频数据的功能
3、编码和解码支持硬编解码和软编解码
4、不能操作PCM裸数据
5、对应的数据结构对象为 ExtAudioFileRef
6、该对象具有编码和封装两大功能

录制并保存为m4a文件代码

如何录制,如何获取音频数据的代码这里就不再多将了,具体可以参考前一篇文章AudioUnit之-录制音频+耳返(二)
我这里将ExtAudioFileRef对象的操作封装到了一个类中,包含创建文件,设置参数,写入,完成写入收尾工作,关闭文件等等接口,以下挑关键代码讲解,具体参考工程
1、创建ExtAudioFileRef写对象

AudioStreamBasicDescription fileDataDesc={0};
    if (_fileTypeId == kAudioFileM4AType) {     // 保存为m4a格式音频文件
        
        fileDataDesc.mFormatID = kAudioFormatMPEG4AAC;        // m4a的编码方式为aac编码
        fileDataDesc.mFormatFlags = kMPEG4Object_AAC_Main;    // aac的编码级别为 main
        fileDataDesc.mChannelsPerFrame = _clientabsdForWriter.mChannelsPerFrame;  // 声道数和输入的PCM一致
        fileDataDesc.mSampleRate = _clientabsdForWriter.mSampleRate;  // 采样率和输入的PCM一致
        fileDataDesc.mFramesPerPacket = 1024; // 对于m4a格式aac编码方式,他压缩后每个packet包固定有1024个frame(这个值算法规定不可修改)
        fileDataDesc.mBytesPerFrame = 0;// 这些填0就好,内部编码算法会自己计算
        fileDataDesc.mBytesPerPacket = 0;// 这些填0就好,内部编码算法会自己计算
        fileDataDesc.mBitsPerChannel = 0;// 这些填0就好,内部编码算法会自己计算
        fileDataDesc.mReserved = 0;
    }   // IOS 不支持MP3的编码,尴尬
// 根据指定的封装格式,指定的编码方式创建ExtAudioFileRef对象
    OSStatus status = ExtAudioFileCreateWithURL((__bridge CFURLRef)recordFileUrl, _fileTypeId, &fileDataDesc, NULL, kAudioFileFlags_EraseFile, &_audioFile);
    if (status != noErr) {
        NSLog(@"ExtAudioFileCreateWithURL faile %d",status);
        return -1;
    }

解释下ExtAudioFileCreateWithURL()函数;

extern OSStatus
ExtAudioFileCreateWithURL(  CFURLRef                            inURL,
                            AudioFileTypeID                     inFileType,
                            const AudioStreamBasicDescription * inStreamDesc,
                            const AudioChannelLayout * __nullable inChannelLayout,
                            UInt32                              inFlags,
                            ExtAudioFileRef __nullable * __nonnull outExtAudioFile)

参数一:要创建的音频文件路径
参数二:封装的格式,比如m4a类型为kAudioFileM4AType
参数三:编码对应的描述
参数四:传NULL
参数五:操作文件的方式,这里选择每次创建文件时擦除已有内容
参数六:生成的ExtAudioFileRef对象
如果要保存为caf/wav等格式,只需要将kAudioFileM4AType改成kAudioFileCAFType、kAudioFileWAVEType,且要创建的音频文件名后缀改为.caf、.wav
同时参数三修改成编码对应描述即可,caf/wav编码对应描述请参考工程代码。
2、设置硬编码还是软编码,设置app输入音频数据格式
特别是设置app输入音频数据格式,非常重要

// 指定是硬件编码还是软件编码
    UInt32 codec = kAppleSoftwareAudioCodecManufacturer;
    status = ExtAudioFileSetProperty(_audioFile, kExtAudioFileProperty_CodecManufacturer, sizeof(codec), &codec);
    if (status != noErr) {
        NSLog(@"ExtAudioFileSetProperty kExtAudioFileProperty_CodecManufacturer fail %d",status);
        return -1;
    }
    
    /** 遇到问题:返回1718449215错误;
     *  解决方案:_clientabsdForWriter格式不正确,比如ASDB中mFormatFlags与所对应的mBytesPerPacket等等不符合,那么会造成这种错误
     */
    // 指定输入给ExtAudioUnitRef的音频PCM数据格式(必须要有)
    status = ExtAudioFileSetProperty(_audioFile, kExtAudioFileProperty_ClientDataFormat, sizeof(_clientabsdForWriter), &_clientabsdForWriter);
    if (status != noErr) {
        NSLog(@"ExtAudioFileSetProperty kExtAudioFileProperty_ClientDataFormat fail %d",status);
        return -1;
    }

3、写入原始音频数据接口

- (OSStatus)writeFrames:(UInt32)framesNum toBufferData:(AudioBufferList*)bufferlist async:(BOOL)async
{
    if (_audioFile == nil) {
        NSLog(@"文件创建未成功 无法写入");
        return -1;
    }
    
    OSStatus status = noErr;
    if (async) {
         status = ExtAudioFileWriteAsync(_audioFile, framesNum, bufferlist);
    } else {
        status = ExtAudioFileWrite(_audioFile, framesNum, bufferlist);
    }
    
    return status;
}

这里写入数据分两种情况,同步写入和异步写入,很好理解,同步写入就是写入成功后函数才返回,异步写入就是函数立马写入。
写入函数将完成编码工作和部分属性写入工作
4、完成音频文件全部属性写入工作

- (void)closeFile
{
    if (_audioFile) {
        ExtAudioFileDispose(_audioFile);
        _audioFile = nil;
    }
}

此步骤很重要,调用后将完成音频文件全部属性的写入工作并且关闭文件句柄及释放相关资源。
如果不调用此函数,音频文件无法正常播放。
所谓音频文件属性,比如m4a文件中会包含一部分数据,这部分数据描述了音频的采样率,采样格式,采样位数,编码方式等等信息,具体请查阅相关文档
5、在音频录制回到中调用第三步封装的写入数据接口

if(保存PCM文件){
....
这里是前文讲解的录制音频并保存到PCM文件中代码
....
} else if(player.dataWriteForNonPCM){    // 先压缩再保存
        // 内部将实现压缩并且封装格式
        [player.dataWriteForNonPCM writeFrames:inNumberFrames toBufferData:bufferList];
}

当然使用音频写入之前还得初始化。

播放m4a音频文件

和写入一样,将从ExtAudioFileRef中读取数据封装到一个类中,下面讲解读取音频数据的关键代码
1、创建ExtAudioFileRef读对象

NSURL *fileUrl = [NSURL fileURLWithPath:_filePath];
        // 打开指定的音频文件,并且创建一个ExtAudioFileRef对象,用于读取音频数据
        OSStatus status = ExtAudioFileOpenURL((__bridge CFURLRef)fileUrl, &_audioFile);
        if (status != noErr) {
            NSLog(@"ExtAudioFileOpenURL faile %d",status);
            return nil;
        }

这个函数没什么好讲的,非常简单
2、设置从音频文件中读取输出的音频的属性
比如采样率,声道数,采样位数,存储方式(planner还是packet)等等,重要,app从ExtAudioFileRef中读取出来的数据就是原始的音频数据,系统在读函数中自动为我们解码,所以我们得事先指定这些属性。
获取音频文件中音频的属性

/** 通过ExtAudioFileGetProperty()函数获取文件有关属性,比如编码格式,总共的音频frames数目等等;
         *  这些步骤对于读取数据不是必须的,主要用于打印和分析
         */
        UInt32 size = sizeof(_fileDataabsdForReader);
        status = ExtAudioFileGetProperty(_audioFile, kExtAudioFileProperty_FileDataFormat, &size, &_fileDataabsdForReader);
        if (status != noErr) {
            NSLog(@"ExtAudioFileGetProperty kExtAudioFileProperty_FileDataFormat fail %d",status);
            return nil;
        }

获取音频文件总的frames数目

// 备注:_totalFrames一定要是SInt64类型的,否则会出错。
        size = sizeof(_totalFrames);
        ExtAudioFileGetProperty(_audioFile, kExtAudioFileProperty_FileLengthFrames, &size, &_totalFrames);
        NSLog(@"文件中包含的frame数目: %lld",_totalFrames);

设置从文件中读取数据后经过解码等步骤后最终输出的数据格式

// 对于从文件中读数据,app属于客户端。对于向文件中写入数据,app也属于客户端
        // 设置从文件中读取数据后经过解码等步骤后最终输出的数据格式
        _clientabsdForReader = [ADUnitTool streamDesWithLinearPCMformat:outabsd.mFormatFlags sampleRate:_fileDataabsdForReader.mSampleRate channels:_fileDataabsdForReader.mChannelsPerFrame bytesPerChannel:outabsd.mBitsPerChannel/8];
        size = sizeof(_clientabsdForReader);
        status = ExtAudioFileSetProperty(_audioFile, kExtAudioFileProperty_ClientDataFormat, size, &_clientabsdForReader);

3、读取数据

// 从文件中读取音频数据
- (OSStatus)readFrames:(UInt32*)framesNum toBufferData:(AudioBufferList*)bufferlist
{
    if (_canrepeat) {
        SInt64 curFramesOffset = 0;
        // 目前读取指针的postion
        if (ExtAudioFileTell(_audioFile, &curFramesOffset) == noErr) {
            
            if (curFramesOffset >= _totalFrames) {   // 已经读取完毕
                ExtAudioFileSeek(_audioFile, 0);
                curFramesOffset = 0;
            }
        }
    }
    
    OSStatus status = ExtAudioFileRead(_audioFile, framesNum, bufferlist);
    
    return status;
}

_canrepeat代表是否可以重复读取,设置为NO
framesNum代表实际读取的frames数目,如果文件中没有音频数据了,该值将返回0 status为负数
文件读取完毕后,不用了要记得关闭代码和写是一样的
4、在播放音频的回调中调用读接口

if(读取PCM文件)
....
这里是用NSInputStream读取裸PCM音频文件的代码
.....
} else if(player->_readFile) {
        /** 遇到问题:返回 -50 错误1111: EXCEPTION (-50): "wrong number of buffers"
         *  分析原因:因为前面// todo:zsz 位置1的存储格式之前给的packet,而// todo:zsz 位置2输入的音频格式给的是planner,两边不一致
         *  解决方案:两边保持一直即可
         */
        OSStatus result= (OSStatus)[player->_readFile readFrames:&inNumberFrames toBufferData:ioData];
        if (result <0 || inNumberFrames == 0) {
            [player stop];
            return kCGErrorNoneAvailable;
        }
    }

至此,代码全部讲解完毕,详情请参考工程

项目地址

Demo
写入音频到m4a和从m4a读取音频文件播放封装在了ADExtAudioFile.h/.m中,可以具体查看

你可能感兴趣的:(AudioUnit之-录制音频保存为m4a/CAF/WAV文件和播放m4a/CAF/WAV文件(三))