本文打算用iOS上的Audio Unit来播放PCM文件。
文章当中用的Remote I/O Unit类型,下面是官方的一张示意图:
从图中可以看到,两个element都包含输入输出两个端。element0的输出端(output scope)对应的是扬声器,输入端用于输入数据,可以从element1的输出端获取,也可以从其他Unit或文件获取。
element1的输入端表示麦克风,element的输出端表示从麦克风采集到的声音数据。
下面是代码流程:
本文输入的pcm数据,声道数2,格式是浮点数存储,小端,采样率48000,交错存储(也就是LRLR....)。
首先打开pcm文件,用于提供数据:
//打开文件
const char *in_filename = [[[NSBundle mainBundle] pathForResource:@"sintel_f32le_2_48000.pcm" ofType:nil] UTF8String];
_inFile = fopen(in_filename, "rb");
if (!_inFile) {
printf("open file %s failed\n", in_filename);
return;
}
设置audioSession,由于只是播放,所以采用AVAudioSessionCategoryPlayback,
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
[audioSession setCategory:AVAudioSessionCategoryPlayback error:&error];
下面创建一个Audio Unit实例,选择RemoteIO类型,
//设置类型RemoteIO,用于播放
AudioComponentDescription audioDesc;
audioDesc.componentType = kAudioUnitType_Output;
audioDesc.componentSubType = kAudioUnitSubType_RemoteIO;
audioDesc.componentManufacturer = kAudioUnitManufacturer_Apple;
audioDesc.componentFlags = 0;
audioDesc.componentFlagsMask = 0;
AudioComponent inputComponent = AudioComponentFindNext(NULL, &audioDesc);
AudioComponentInstanceNew(inputComponent, &_audioUnit);
通过调用AudioUnitSetProperty,开启扬声器,默认是关闭的,
#define INPUT_BUS 1
#define OUTPUT_BUS 0
UInt32 flag = 1;
//打开element0(OUTPUT_BUS=0)的输出scope,也就是开启扬声器
status = AudioUnitSetProperty(_audioUnit,
kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Output,
OUTPUT_BUS,
&flag,
sizeof(flag));
if (status != noErr) {
NSLog(@"AudioUnitSetProperty error : %d", status);
return;
}
设置输入的pcm音频格式:
//设置输入的音频格式
AudioStreamBasicDescription outputFormat;
memset(&outputFormat, 0, sizeof(outputFormat));
outputFormat.mSampleRate = 48000; // 采样率
outputFormat.mFormatID = kAudioFormatLinearPCM; // PCM格式
/*kAudioFormatFlagIsNonInterleaved表示音频源是非交错的,在回调函数中需要对
ioData->mBuffers数组分别填充各声道的数据。
kAudioFormatFlagIsBigEndian可以指定大端存储的数据。
*/
outputFormat.mFormatFlags = kLinearPCMFormatFlagIsFloat; // 浮点数
outputFormat.mFramesPerPacket = 1; // 非压缩数据,固定填1
outputFormat.mChannelsPerFrame = 2; // 声道数
/*对于交错声音格式,mBytesPerFrame = 声道数 * 每个采样点占的字节数(浮点数是4),
非交错格式,mBytesPerFrame = 每个采样点占的字节数(浮点数是4)
*/
outputFormat.mBytesPerFrame = 2 * 4;
outputFormat.mBytesPerPacket = outputFormat.mBytesPerFrame;
outputFormat.mBitsPerChannel = 8 * 4; //每声道占的位数
//设置element0的输入scope
status = AudioUnitSetProperty(_audioUnit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input,
OUTPUT_BUS,
&outputFormat,
sizeof(outputFormat));
if (status != noErr) {
NSLog(@"AudioUnitSetProperty error : %d", status);
return;
}
设置回调函数,作用是:每当Audio Unit需要数据进行播放时,就会调用回调函数,这样开发人员可以从文件或上一级Unit获得数据,进行填充。
//设置回调函数
AURenderCallbackStruct playCallback;
playCallback.inputProc = PlayCallback;
playCallback.inputProcRefCon = (__bridge void *)self;
AudioUnitSetProperty(_audioUnit,
kAudioUnitProperty_SetRenderCallback,
kAudioUnitScope_Input,
OUTPUT_BUS,
&playCallback,
sizeof(playCallback));
初始化、开启Unit:
status = AudioUnitInitialize(_audioUnit);
if (status != noErr) {
NSLog(@"AudioUnitInitialize error : %d", status);
return;
}
status = AudioOutputUnitStart(_audioUnit);
if (status != noErr) {
NSLog(@"AudioOutputUnitStart error : %d", status);
return;
}
下面是回调函数,从文件中读取数据,填充到ioData->mBuffers[0].mData中,进行播放。
static OSStatus PlayCallback(void *inRefCon,
AudioUnitRenderActionFlags *ioActionFlags,
const AudioTimeStamp *inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList *ioData) {
ViewController *vc = (__bridge ViewController *)inRefCon;
ioData->mBuffers[0].mDataByteSize = (UInt32)fread(ioData->mBuffers[0].mData, 1, ioData->mBuffers[0].mDataByteSize, vc->_inFile);
NSLog(@"out size: %d", ioData->mBuffers[0].mDataByteSize);
if (ioData->mBuffers[0].mDataByteSize <= 0) {
dispatch_async(dispatch_get_main_queue(), ^{
[vc stop];
});
}
return noErr;
}
- (void)stop {
AudioOutputUnitStop(_audioUnit);
AudioUnitUninitialize(_audioUnit);
AudioComponentInstanceDispose(_audioUnit);
fclose(_inFile);
}
下载地址:
https://github.com/whoyouare888/Note/tree/master/FFmpegPlayer_1/AudioUnitPlayPCM