美文网首页音视频开发直播技术专题
基于iOS平台的最简单的FFmpeg音频播放器(一)

基于iOS平台的最简单的FFmpeg音频播放器(一)

作者: Aiewing | 来源:发表于2018-05-23 19:24 被阅读458次
  • 之前的FFmpeg的视频播放器的文章,肯定已经可以让初学者在视频的解码过程有一定的了解了,现在我们来深度解析一下音频的解码和播放,实质上音频和视频的解码过程也是是极为雷同的(这里说的是针对FFmpeg的代码层次,不是实际的解码过程)。

  • 在学习音频的解码和播放之前,小伙伴们理应对音频的支持有所了解,才可以更好的理解音频的解码和播放,建议可以去看雷神的博客,其实我也不知道他的文章是否有对音频的详细讲解,已经很久没有看过了,反正就是丢你雷总,看了总比不看好。

  • 音频的解码和播放,我们还是分为三个章节来讲解,但是有所不同的是,我们这次反过来讲,我们先讲解音频的播放,然后再讲解音频的解码。

基于iOS平台的最简单的FFmpeg音频播放器(一)
基于iOS平台的最简单的FFmpeg音频播放器(二)
基于iOS平台的最简单的FFmpeg音频播放器(三)

正式开始

音频的基本特性

  • 本来说好不讲音频的基本知识的,但是还是忍不住普及一下。所谓的手机上的声音就是声卡把数字信号转化成模型信号,然后通过线圈和磁铁产生不同的震动,这就是产生了我们平时听见手机扬声器的声音了。
  • 想要播放一个音频,我们需要的是什么数据,我目前遇到的都是AV_SAMPLE_FMT_S16格式的数据,解释一下就是非平面的16位的PCM格式的原数据,这个是iPhone手机使用原生的录制功能出来的音频数据,不理解没关系,我也不理解,不影响写代码

1.初始化音频播放器

  • 上面好像有奇怪的东西出现,说了一些没什么卵用的东西,肯定是我被另一个人格操控了。

1.1 创建音频组件描述

AudioComponentDescription description = {0};
description.componentType = kAudioUnitType_Output;
description.componentSubType = kAudioUnitSubType_RemoteIO;
description.componentManufacturer = kAudioUnitManufacturer_Apple;
  • AudioComponentDescription是一个用来描述音频组建的结构体。
  • componentType是一个音频组件通用的独特的四字节标识码,我们这里是要播放音频所以选择的是kAudioUnitType_Output,里面还有很多类型,反正我是没用到过。
  • componentSubType是根据componentType设置的类型,iOS平台kAudioUnitSubType_RemoteIO,如果是iOS和OSX平台通用的是kAudioUnitSubType_VoiceProcessingIO
  • componentManufacturer是厂商的身份验证,这个不用考虑了,我选kAudioUnitManufacturer_Apple

1.2 创建音频输出单元

AudioComponent component = AudioComponentFindNext(NULL, &description);
status = AudioComponentInstanceNew(component, &_audioUnit);
if (status != noErr) {
    NSLog(@"无法创建音频输出单元");
    return false;
}
  • AudioUnit.framework这个库提供DSP数字信号处理相关的插件,包括编解码,混音,音频均衡等。
  • AudioComponentFindNext()用于寻找最接近匹配的音频组件。
  • AudioComponentInstanceNew() 创建一个音频组件的实例,就是我们需要的音频输出单元。

1.3 获取设备音频的基本信息

// 获取硬件的输出信息
    size = sizeof(AudioStreamBasicDescription);
    status = AudioUnitGetProperty(_audioUnit,
                                           kAudioUnitProperty_StreamFormat,
                                           kAudioUnitScope_Input,
                                           0,
                                           &_outputFormat,
                                           &size);
    if (status != noErr) {
        NSLog(@"无法获取硬件的输出流格式");
        return false;
    }
    
    _numBytesPerSample = _outputFormat.mBitsPerChannel / 8;
    _numOutputChannels = _outputFormat.mChannelsPerFrame;
  • AudioStreamBasicDescription是描述音频流数据基本属性的全部信息的结构体。
  • AudioUnitGetProperty()作用是获取Audio Unit属性的函数,常用的属性有:
    - kAudioUnitProperty_StreamFormatAudio unit输入输出流的数据格式)
    - kAudioOutputUnitProperty_EnableIO(开启或者禁用I/O)
    - kAudioUnitProperty_SetRenderCallback(设置Audio unit的播放回调,下面会用到)
  • _numBytesPerSample = _outputFormat.mBitsPerChannel / 8;是获取每一个采样数据的字节数,固定就是单个通道的采样数据的字节数除以8.
  • _numOutputChannels = _outputFormat.mChannelsPerFrame;如果是1,那就是单声道,如果是2,那就是双声道。

1.4 设置Audio Unit回调

// 设置回调
    AURenderCallbackStruct callbackStruct;
    callbackStruct.inputProc = renderCallback;
    callbackStruct.inputProcRefCon = (__bridge void *)(self);
    
    status = AudioUnitSetProperty(_audioUnit,
                                  kAudioUnitProperty_SetRenderCallback,
                                  kAudioUnitScope_Input,
                                  0,
                                  &callbackStruct,
                                  sizeof(callbackStruct));
    if (status != noErr) {
        NSLog(@"无法设置音频输出单元的回调");
        return false;
    }
  • AURenderCallbackStruct是很重要的一个结构体,控制音频输入输出回调,这里当函数回调时,我们就要把准备播放的数据加入到播放列表中。
  • 这边的回调函数是固定写法,下面会花一点时间单独讲解。

1.5 初始化Audio Unit

// 初始化音频输出单元
    status = AudioUnitInitialize(_audioUnit);
    if (status != noErr) {
        NSLog(@"无法初始化音频输出单元");
        return false;
    }
  • Audio Unit一旦被初始化成功之后,他的输入输出的格式就都是有效的,而且不能改变内存分配。

2.启动Audio Unit

// 启动音频输出单元
    OSStatus status = AudioOutputUnitStart(_audioUnit);
    if (status == noErr) {
        _playing = true;
    } else {
        _playing = false;
    }
  • 启动了Audio Unit,放入音频数据,音频就开始播放了,苹果自带的音频播放器就这么简单。

3.音频播放回调解析

  • 前面说过,音频播放回调会单独拿出来讲,因为不是所有的数据都是可以直接播放的,还需要做一定的处理。
#pragma mark - CallBack
static OSStatus renderCallback (void                        *inRefCon,
                                AudioUnitRenderActionFlags    * ioActionFlags,
                                const AudioTimeStamp         * inTimeStamp,
                                UInt32                        inOutputBusNumber,
                                UInt32                        inNumberFrames,
                                AudioBufferList                * ioData)
{
    AieAudioManager * aam = (__bridge AieAudioManager *)inRefCon;
    return [aam renderFrames:inNumberFrames ioData:ioData];
}
  • 这个函数是Audio Unit渲染通知API,也是渲染输出回调。
  • inRefCon第一个参数就是我们之前设置函数函数回调的时候,传入的inputProcRefCon
  • ioActionFlags是用来描述上下文的标记。
  • inTimeStamp是音频渲染的时间戳。
  • inOutputBusNumber是与音频渲染相关的总线号。
  • inNumberFramesioData中音频数据采样帧的数量。
  • ioData是一个列表,用来存放准备播放的数据。
- (BOOL)renderFrames:(UInt32)numFrames ioData:(AudioBufferList *)ioData
{
    // 第一步
    for (int iBuffer = 0; iBuffer < ioData->mNumberBuffers; iBuffer++) {
        memset(ioData->mBuffers[iBuffer].mData, 0, ioData->mBuffers[iBuffer].mDataByteSize);
    }
    
    if (_playing && _outputBlock) {
        // 第二步
        _outputBlock(_outData, numFrames, _numOutputChannels);
        
        if (_numBytesPerSample == 2) {
            // 第三步
            float scale = (float)INT16_MAX;
            vDSP_vsmul(_outData,
                       1,
                       &scale,
                       _outData,
                       1,
                       numFrames * _numOutputChannels);
            // 第四步
            for (int iBuffer = 0; iBuffer < ioData->mNumberBuffers; ++iBuffer) {
                int thisNumChannels = ioData->mBuffers[iBuffer].mNumberChannels;
                for (int iChannel = 0; iChannel < thisNumChannels; ++iChannel) {
                    vDSP_vfix16(_outData+iChannel,
                                _numOutputChannels,
                                (SInt16 *)ioData->mBuffers[iBuffer].mData+iChannel,
                                thisNumChannels, numFrames);
                }
            }
        }
    }
    
    return noErr;
}

1. 第一步是初始化ioData,这个算是属于一个C语言范畴的函数,就是把ioData列表中所有的数据都用0来填充。
2. 第二步是使用block从外界,把需要的数据(实际上就是解码后的PCM数据)填充到_outData中来。
3. 第三步是利用vDSP_vsmul()对数据做了一个乘法的运算。
4. 第四步是对所有ioData数据,根据不同的声道,使用vDSP_vfix16()将非交错的16位带符号整数转化成单精度浮点型(因为苹果设备都是每个采样点16bit量化),如果是每个采样点32bit量化,那就需要使用vDSP_vflt32()

  • 做完这些操作之后,音频就可以进行播放了。
  • 上面提到的sDSP函数都是属于Accelerate.framework加速处理框架,其中包括向量和矩阵算法傅里叶变换双二次滤波等。

4.拓展方法

  • 如果说一个最简单的音频播放器,那么上面就已经满足了,但是如果作为一个功能全面的应用,那还是远远不足的,下面我就简单的描述下。
  • Audio Session可以帮我们处理各种系统逻辑,如果你是一个以语音为主的应用,设置kAudioSessionProperty_AudioCategory属性可以让应用不会随着静音键和屏幕关闭而静音,可以在后台播放。
  • 如果你的应用需要播放声音又需要录音,那就设置这个属性kAudioSessionCategory_PlayAndRecord
  • 如果你需要监听耳机的插入或者拔出,那你需要监听kAudioSessionProperty_AudioRouteChange这个属性,作用是监听音频路线是否改变。
  • 如果你想让应用主动获取系统音量,那就监听kAudioSessionProperty_CurrentHardwareOutputVolume这个属性。

结尾

  • 代码会在整个播放器讲完之后再给出完整的。
  • 由于放了FFmpeg库,所以Demo会很大,下载的时候比较费时。
  • 谢谢阅读

相关文章

网友评论

    本文标题:基于iOS平台的最简单的FFmpeg音频播放器(一)

    本文链接:https://www.haomeiwen.com/subject/szrkjftx.html