【FFmpeg】使用AudioToolbox播放PCM

跟YUV数据一样,PCM数据也是裸数据流,里面不包含存储格式、通道数、采样率等信息,我们只有知道这些信息,才能正常播放PCM数据,在AudioToolbox中使用AudioStreamBasicDescription结构体来描述这些参数。AudioStreamBasicDescription结构体定义如下:

struct AudioStreamBasicDescription
{
    Float64             mSampleRate;//采样率
    AudioFormatID       mFormatID;//数据格式
    AudioFormatFlags    mFormatFlags;//数据的存储方式
    UInt32              mBytesPerPacket;//每个packet中有几个字节
    UInt32              mFramesPerPacket;//每个packer中有几个frame
    UInt32              mBytesPerFrame;//每个frame中有几个字节
    UInt32              mChannelsPerFrame;//每个frame中有几个通道
    UInt32              mBitsPerChannel;//每个通道有多少位
    UInt32              mReserved;
};

我用到的PCM数据格式是s16le,通道数为2,采样率是44.1K。因此我构建的AudioStreamBasicDescription结构体如下:

int channels = 2;
AudioStreamBasicDescription audioStreamBasicDescription;
audioStreamBasicDescription.mSampleRate = 44100;
audioStreamBasicDescription.mFormatID = kAudioFormatLinearPCM;
audioStreamBasicDescription.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
audioStreamBasicDescription.mFramesPerPacket = 1;
audioStreamBasicDescription.mChannelsPerFrame = channels;
audioStreamBasicDescription.mBitsPerChannel = 16;
audioStreamBasicDescription.mBytesPerPacket = channels*2;
audioStreamBasicDescription.mBytesPerFrame = channels*2;

构建好这些音频参数后,我们就来使用AudioUnit一步一步来播放PCM数据。

1、初始化AudioUnit

- (void)setupAudioUnit {
    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);
}

2、设置AudioUnit属性

- (void)setupAudioUnitProperty:(AudioStreamBasicDescription)audioStreamBasicDescription {
    AudioUnitSetProperty(self.audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &audioStreamBasicDescription, sizeof(audioStreamBasicDescription));
    AURenderCallbackStruct callbackStruct;
    callbackStruct.inputProc = (AURenderCallback)AudioUnitPlayer_AURenderCallback;
    callbackStruct.inputProcRefCon = (__bridge void * _Nullable)(self);
    AudioUnitSetProperty(self.audioUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Global, 0, &callbackStruct, sizeof(callbackStruct));
}

这一步,我们把之前构建好的AudioStreamBasicDescription音频参数告诉AudioUnit,这样它就知道怎么去解析播放PCM数据了。我们还设置了一个回调方法AudioUnitPlayer_AURenderCallback,播放音频时,音频设备需要数据了,设备就是通过这个回调方法告诉我们,它没有PCM数据了,需要我们传输数据给它了。是一个被动的传输过程,而不是我们主动把数据喂给设备。

3、定义AudioUnitPlayer_AURenderCallback方法

static OSStatus AudioUnitPlayer_AURenderCallback(void *inRefCon,
                            AudioUnitRenderActionFlags *ioActionFlags,
                            const AudioTimeStamp *inTimeStamp,
                            UInt32 inBusNumber,
                            UInt32 inNumberFrames,
                            AudioBufferList *ioData)
{
    memset(ioData->mBuffers[0].mData, 0, ioData->mBuffers[0].mDataByteSize);
    AudioUnitPlayer *player = (__bridge AudioUnitPlayer *)inRefCon;
    NSData *data = [player.fileHandle readDataOfLength:ioData->mBuffers[0].mDataByteSize];
    if (data.length<ioData->mBuffers[0].mDataByteSize) {
        if (@available(iOS 13.0, *)) {
            [player.fileHandle seekToOffset:0 error:nil];
        } else {
            [player.fileHandle seekToFileOffset:0];
        }
        [player stopPlayer];
        return noErr;
    }
    memcpy(ioData->mBuffers[0].mData, data.bytes, data.length);
    return noErr;
}

可以看到,我们就是通过这个方法把PCM数据传输给音频设备的。

4、开始播放或暂停

- (void)startPlayer {
    AudioOutputUnitStart(self.audioUnit);
}

- (void)stopPlayer {
    AudioOutputUnitStop(self.audioUnit);
}

完整代码如下:
.h文件

#import <Foundation/Foundation.h>
#import <AudioToolbox/AudioToolbox.h>

NS_ASSUME_NONNULL_BEGIN

@interface AudioUnitPlayer : NSObject

+ (instancetype)player:(AudioStreamBasicDescription)audioDesc pcmPath:(NSString *)pcmPath;
- (void)startPlayer;
- (void)stopPlayer;

@end

NS_ASSUME_NONNULL_END

.m文件

#import "AudioUnitPlayer.h"

@interface AudioUnitPlayer ()

@property (nonatomic, assign) AudioUnit audioUnit;
@property (nonatomic, strong) NSFileHandle *fileHandle;

@end

@implementation AudioUnitPlayer

+ (instancetype)player:(AudioStreamBasicDescription)audioStreamBasicDescription pcmPath:(NSString *)pcmPath
{
    AudioUnitPlayer *obj = [[AudioUnitPlayer alloc] init:audioStreamBasicDescription pcmPath:pcmPath];
    return obj;
}

- (instancetype)init:(AudioStreamBasicDescription)audioStreamBasicDescription pcmPath:(NSString *)pcmPath
{
    self = [super init];
    if (self) {
        [self setupAudioUnit];
        [self setupAudioUnitProperty:audioStreamBasicDescription];
        self.fileHandle = [NSFileHandle fileHandleForReadingAtPath:pcmPath];

    }
    return self;
}

- (void)setupAudioUnit {
    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);
}

- (void)setupAudioUnitProperty:(AudioStreamBasicDescription)audioStreamBasicDescription {
    
    AudioUnitSetProperty(self.audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &audioStreamBasicDescription, sizeof(audioStreamBasicDescription));
    
    AURenderCallbackStruct callbackStruct;
    callbackStruct.inputProc = (AURenderCallback)AudioUnitPlayer_AURenderCallback;
    callbackStruct.inputProcRefCon = (__bridge void * _Nullable)(self);
    AudioUnitSetProperty(self.audioUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Global, 0, &callbackStruct, sizeof(callbackStruct));

}

static OSStatus AudioUnitPlayer_AURenderCallback(void *inRefCon,
                            AudioUnitRenderActionFlags *ioActionFlags,
                            const AudioTimeStamp *inTimeStamp,
                            UInt32 inBusNumber,
                            UInt32 inNumberFrames,
                            AudioBufferList *ioData)
{
    memset(ioData->mBuffers[0].mData, 0, ioData->mBuffers[0].mDataByteSize);
    AudioUnitPlayer *player = (__bridge AudioUnitPlayer *)inRefCon;
    NSData *data = [player.fileHandle readDataOfLength:ioData->mBuffers[0].mDataByteSize];
    if (data.length<ioData->mBuffers[0].mDataByteSize) {
        if (@available(iOS 13.0, *)) {
            [player.fileHandle seekToOffset:0 error:nil];
        } else {
            [player.fileHandle seekToFileOffset:0];
        }
        [player stopPlayer];
        return noErr;
    }
    memcpy(ioData->mBuffers[0].mData, data.bytes, data.length);
    return noErr;
}

- (void)startPlayer {
    AudioOutputUnitStart(self.audioUnit);
}

- (void)stopPlayer {
    AudioOutputUnitStop(self.audioUnit);
}

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

推荐阅读更多精彩内容