几种播放音频文件的方式(十三) —— OpenAL框架之分步解析(二)

版本记录

版本号 时间
V1.0 2017.12.29

前言

ios系统中有很多方式可以播放音频文件,这里我们就详细的说明下播放音乐文件的原理和实例。感兴趣的可以看我写的上面几篇。
1. 几种播放音频文件的方式(一) —— 播放本地音乐
2. 几种播放音频文件的方式(二) —— 音效播放
3. 几种播放音频文件的方式(三) —— 网络音乐播放
4. 几种播放音频文件的方式(四) —— 音频队列服务(Audio Queue Services)(一)
5. 几种播放音频文件的方式(五) —— 音频队列服务(Audio Queue Services)简介(二)
6. 几种播放音频文件的方式(六) —— 音频队列服务(Audio Queue Services)之关于音频队列(三)
7. 几种播放音频文件的方式(七) —— 音频队列服务(Audio Queue Services)之录制音频(四)
8. 几种播放音频文件的方式(八) —— 音频队列服务(Audio Queue Services)之播放音频(五)
9. 几种播放音频文件的方式(九) —— Media Player框架之基本概览(一)
10. 几种播放音频文件的方式(十) —— Media Player框架之简单播放音频示例(二)
11. 几种播放音频文件的方式(十一) —— AudioUnit框架之基本概览(一)
12. 几种播放音频文件的方式(十二) —— OpenAL框架之基本概览(一)

分步解析

这一篇文章主要就是对OpenAL播放音频的过程进行分步解析,文章内容来自别人,下面给出链接,致敬原作者—— IOS使用OpenAL播放音频文件。

其实,不仅可以参考这个作者所写的内容,我们还可以参考苹果官网给出的demo,OpenALExample 和 GLAirplay。

这里我们只谈最基本的实现,加载声音文件,播放声音。至于3D音效,多普勒效应环境音效设置,声音位置,收听位置等都不进行配置。


导入平台头文件

#include   
#include   
#include   
#include   

文件需要使用.m文件,因为需要使用Foundation.h的功能来加载Bundle的声音文件。m后缀文件是c和objc混编的文件类型。AudioToolbox可以对音频文件信息的解析和设置,以配合OpenAL的使用。


初始化OpenAL

下面还是直接看代码。

static ALCdevice*                device                 = NULL;  
static ALCcontext*               context                = NULL;  
static alBufferDataStaticProcPtr alBufferDataStaticProc = NULL;  
  
  
struct AudioPlayer  
{  
    ALuint sourceId;  
    ALuint bufferId;  
};  
  
static void Init()  
{  
    // get static buffer data API  
    alBufferDataStaticProc = (alBufferDataStaticProcPtr) alcGetProcAddress(NULL, (const ALCchar*) "alBufferDataStatic");  
      
    // create a new OpenAL Device  
    // pass NULL to specify the system’s default output device  
    device = alcOpenDevice(NULL);  
      
    if (device != NULL)  
    {  
        // create a new OpenAL Context  
        // the new context will render to the OpenAL Device just created  
        context = alcCreateContext(device, 0);  
          
        if (context != NULL)  
        {  
            // make the new context the Current OpenAL Context  
            alcMakeContextCurrent(context);  
        }  
    }  
    else  
    {  
        ALogE("Audio Init failed, OpenAL can not open device");  
    }  
      
    // clear any errors  
    alGetError();  
}  
  • OpenAL全局只需要一个ALCdeviceALCcontext
  • 我们抽象了一个AudioPlayer,用来对应一个播放器,bufferId就是加载到内存的音频数据,sourceId是对应OpenAL播放器。
  • alBufferDataStatic是OpenAL的一个扩展,相对于alBufferData来说的。功能是加载音频数据到内存并关联到bufferId。只不过,alBufferData会拷贝音频数据所以调用后,我们可以free掉音频数据。而alBufferDataStatic并不会拷贝,所以音频数据data我们要一直保留并自己管理。

获取适合OpenAL使用的音频数据

我们需要加载声音文件,解析音频数据,修改音频数据格式为OpenAL需要的,获取最终的可以传递给OpenAL使用的音频数据。这几步封装了一个函数,先解释在看完整的代码。

  • 首先我们要获取Bundle的文件路径。
  • 然后,利用AudioToolBox的功能来读取并解析这个数据。OpenAL加载数据到Buffer,需要音频的采样频率,通道数,码率,数据大小等信息。
  • 接着,OpenAL只能播放特定格式和属性的音频文件。再次使用AudioToolBox的功能来对音频数据进行设置,以达到需求。
  • 最后,把处理好的数据和信息返回。

下面我们看代码。

static inline void* GetAudioData(char* filePath, ALsizei* outDataSize, ALenum* outDataFormat, ALsizei* outSampleRate)  
{  
    AudioStreamBasicDescription fileFormat;  
    AudioStreamBasicDescription outputFormat;  
    SInt64                      fileLengthInFrames = 0;  
    UInt32                      propertySize       = sizeof(fileFormat);  
    ExtAudioFileRef             audioFileRef       = NULL;  
    void*                       data               = NULL;  
  
    NSString*                   path               = [[NSBundle mainBundle] pathForResource:[NSString stringWithUTF8String:filePath] ofType:nil];  
    CFURLRef                    fileUrl            = CFURLCreateWithString(kCFAllocatorDefault, (CFStringRef) path, NULL);  
    OSStatus                    error              = ExtAudioFileOpenURL(fileUrl, &audioFileRef);  
      
    CFRelease(fileUrl);  
      
    if (error != noErr)  
    {  
        ALogE("Audio GetAudioData ExtAudioFileOpenURL failed, error = %x, filePath = %s", (int) error, filePath);  
        goto label_exit;  
    }  
      
    // get the audio data format  
    error = ExtAudioFileGetProperty(audioFileRef, kExtAudioFileProperty_FileDataFormat, &propertySize, &fileFormat);  
      
    if (error != noErr)  
    {  
        ALogE("Audio GetAudioData ExtAudioFileGetProperty(kExtAudioFileProperty_FileDataFormat) failed, error = %x, filePath = %s", (int) error, filePath);  
        goto label_exit;  
    }  
      
    if (fileFormat.mChannelsPerFrame > 2)  
    {  
        ALogE("Audio GetAudioData unsupported format, channel count = %u is greater than stereo, filePath = %s", fileFormat.mChannelsPerFrame, filePath);  
        goto label_exit;  
    }  
      
    // set the client format to 16 bit signed integer (native-endian) data  
    // maintain the channel count and sample rate of the original source format  
    outputFormat.mSampleRate       = fileFormat.mSampleRate;  
    outputFormat.mChannelsPerFrame = fileFormat.mChannelsPerFrame;  
    outputFormat.mFormatID         = kAudioFormatLinearPCM;  
    outputFormat.mBytesPerPacket   = outputFormat.mChannelsPerFrame * 2;  
    outputFormat.mFramesPerPacket  = 1;  
    outputFormat.mBytesPerFrame    = outputFormat.mChannelsPerFrame * 2;  
    outputFormat.mBitsPerChannel   = 16;  
    outputFormat.mFormatFlags      = kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked | kAudioFormatFlagIsSignedInteger;  
      
    // set the desired client (output) data format  
    error = ExtAudioFileSetProperty(audioFileRef, kExtAudioFileProperty_ClientDataFormat, sizeof(outputFormat), &outputFormat);  
      
    if(error != noErr)  
    {  
        ALogE("Audio GetAudioData ExtAudioFileSetProperty(kExtAudioFileProperty_ClientDataFormat) failed, error = %x, filePath = %s", (int) error, filePath);  
        goto label_exit;  
    }  
      
    // get the total frame count  
    propertySize = sizeof(fileLengthInFrames);  
    error        = ExtAudioFileGetProperty(audioFileRef, kExtAudioFileProperty_FileLengthFrames, &propertySize, &fileLengthInFrames);  
      
    if(error != noErr)  
    {  
        ALogE("Audio GetAudioData ExtAudioFileGetProperty(kExtAudioFileProperty_FileLengthFrames) failed, error = %x, filePath = %s", (int) error, filePath);  
        goto label_exit;  
    }  
      
//--------------------------------------------------------------------------------------------------  
      
    // read all the data into memory  
    UInt32 framesToRead = (UInt32) fileLengthInFrames;  
    UInt32 dataSize     = framesToRead * outputFormat.mBytesPerFrame;  
      
    *outDataSize        = (ALsizei) dataSize;  
    *outDataFormat      =  outputFormat.mChannelsPerFrame > 1 ? AL_FORMAT_STEREO16 : AL_FORMAT_MONO16;  
    *outSampleRate      = (ALsizei) outputFormat.mSampleRate;  
  
    int index           = AArrayStrMap->GetIndex(fileDataMap, filePath);  
      
    if (index < 0)  
    {  
        data = malloc(dataSize);  
          
        if (data != NULL)  
        {  
            AudioBufferList dataBuffer;  
            dataBuffer.mNumberBuffers              = 1;  
            dataBuffer.mBuffers[0].mDataByteSize   = dataSize;  
            dataBuffer.mBuffers[0].mNumberChannels = outputFormat.mChannelsPerFrame;  
            dataBuffer.mBuffers[0].mData           = data;  
              
            // read the data into an AudioBufferList  
            error = ExtAudioFileRead(audioFileRef, &framesToRead, &dataBuffer);  
              
            if(error != noErr)  
            {  
                free(data);  
                data = NULL; // make sure to return NULL  
                ALogE("Audio GetAudioData ExtAudioFileRead failed, error = %x, filePath = %s", (int) error, filePath);  
                goto label_exit;  
            }  
        }  
          
        AArrayStrMapInsertAt(fileDataMap, filePath, -index - 1, data);  
    }  
    else  
    {  
        data = AArrayStrMapGetAt(fileDataMap, index, void*);  
    }  
      
      
    label_exit:  
      
    // dispose the ExtAudioFileRef, it is no longer needed  
    if (audioFileRef != 0)  
    {  
        ExtAudioFileDispose(audioFileRef);  
    }  
      
    return data;  
}  

这里,我使用了ArrayStrMap结构其实就是一个dictionary,用文件路径缓存了最终的data文件。因为,我会使用alBufferDataStatic,所以最终的data文件由我自己管理。并且同一个音频文件数据总是相同的,我就不再去频繁的free在malloc了。

outputFormat就是我们需要的音频格式,使用ExtAudioFileSetProperty能够让我们把原音频数据格式进行转换。这样,我们就可以使用各种音频文件格式来播放了,比如mp3,wav等等。


利用音频文件数据,生成我们的播放器对象

  • 首先,生成bufferIdsourceId
  • 然后,把音频数据关联到bufferId,把bufferId关联到sourceId。
  • 最后,sourceId代表就是OpenAL的播放器,可以设置各种属性。
  • 那么,当我们需要销毁播放器的时候,主要也就是销毁sourceIdbufferId
static inline void InitPlayer(char* filePath, AudioPlayer* player)  
{  
    ALenum  error;  
    ALsizei size;  
    ALenum  format;  
    ALsizei freq;  
    void*   data = GetAudioData(filePath, &size, &format, &freq);  
      
    if ((error = alGetError()) != AL_NO_ERROR)  
    {  
        ALogE("Audio InitPlayer failed, error = %x, filePath = %s", error, filePath);  
    }  
      
    alGenBuffers(1, &player->bufferId);  
    if((error = alGetError()) != AL_NO_ERROR)  
    {  
        ALogE("Audio InitPlayer generate buffer failed, error = %x, filePath = %s", error, filePath);  
    }  
      
    // use the static buffer data API  
    // the data will not copy in buffer so can not free data until buffer deleted  
    alBufferDataStaticProc(player->bufferId, format, data, size, freq);  
      
    if((error = alGetError()) != AL_NO_ERROR)  
    {  
        ALogE("Audio InitPlayer attach audio data to buffer failed, error = %x, filePath = %s", error, filePath);  
    }  
      
//--------------------------------------------------------------------------------------------------  
      
    alGenSources(1, &player->sourceId);  
    if((error = alGetError())!= AL_NO_ERROR)  
    {  
        ALogE("Audio InitPlayer generate source failed, error = %x, filePath = %s", error, filePath);  
    }  
      
    // turn Looping off  
    alSourcei(player->sourceId,                        AL_LOOPING, AL_FALSE);  
      
    // set Source Position  
    alSourcefv(player->sourceId, AL_POSITION,          (const ALfloat[]) {0.0f, 0.0f, 0.0f});  
      
    // set source reference distance  
    alSourcef(player->sourceId,  AL_REFERENCE_DISTANCE, 0.0f);  
      
    // attach OpenAL buffer to OpenAL Source  
    alSourcei(player->sourceId,  AL_BUFFER,             player->bufferId);  
      
    if((error = alGetError()) != AL_NO_ERROR)  
    {  
        ALogE("Audio InitPlayer attach buffer to source failed, error = %x, filePath = %s", error, filePath);  
    }  
}  


设置播放器的各种属性

tatic void SetLoop(AudioPlayer* player, bool isLoop)  
{  
    ALint isLoopEnabled;  
    alGetSourcei(player->sourceId, AL_LOOPING, &isLoopEnabled);  
      
    if (isLoopEnabled == isLoop)  
    {  
        return;  
    }  
      
    alSourcei(player->sourceId, AL_LOOPING, (ALint) isLoop);  
}  
  
  
static void SetVolume(AudioPlayer* player, int volume)  
{  
    ALogA(volume >= 0 && volume <= 100, "Audio SetVolume volume %d not in [0, 100]", volume);  
    alSourcef(player->sourceId, AL_GAIN, volume / 100.0f);  
      
    ALenum error = alGetError();  
    if(error != AL_NO_ERROR)  
    {  
        ALogE("Audio SetVolume error = %x", error);  
    }  
}  
  
  
static void SetPlay(AudioPlayer* player)  
{  
    alSourcePlay(player->sourceId);  
      
    ALenum error = alGetError();  
    if(error != AL_NO_ERROR)  
    {  
        ALogE("Audio SetPlay error = %x", error);  
    }  
}  
  
  
static void SetPause(AudioPlayer* player)  
{  
    alSourcePause(player->sourceId);  
      
    ALenum error = alGetError();  
    if(error != AL_NO_ERROR)  
    {  
        ALogE("Audio SetPause error = %x", error);  
    }  
}  
  
  
static bool IsPlaying(AudioPlayer* player)  
{  
    ALint state;  
    alGetSourcei(player->sourceId, AL_SOURCE_STATE, &state);  
      
    return state == AL_PLAYING;  
}  

OpenAL并没有播放完成的回调

OpenAL并没有播放完成的回调,所以我们需要在Update中不断的检测播放器的状态。如果不是循环播放的声音,我们就可以删除它,也可以回调给应用程序做其它操作。

static void Update(float deltaSeconds)  
{  
    for (int i = destroyList->size - 1; i > -1; i--)  
    {  
        AudioPlayer* player = AArrayListGet(destroyList, i, AudioPlayer*);  
          
        ALint state;  
        alGetSourcei(player->sourceId, AL_SOURCE_STATE, &state);  
          
        if (state == AL_STOPPED)  
        {  
            alDeleteSources(1, &player->sourceId);  
            alDeleteBuffers(1, &player->bufferId);  
              
            AArrayList->Remove(destroyList, i);  
            AArrayListAdd(cacheList, player);  
        }  
    }  
}  

因为,我使用了alBufferDataStatic,并且缓存音频数据,所以这里只是删除播放器的关联id,并没有删除音频数据。再次播放同一个音频的时候继续使用。OpenAL通常一共只可以申请32个播放器。所以播放器用完还是及时的删除比较好。

后记

未完,待续,后续更精彩~~~

几种播放音频文件的方式(十三) —— OpenAL框架之分步解析(二)_第1张图片

你可能感兴趣的:(几种播放音频文件的方式(十三) —— OpenAL框架之分步解析(二))