IOS使用OpenAL播放音频文件

本文介绍以下几点内容:

  • OpenAL API的使用介绍
  • 从IOS的mainBundle读取载入音频文件
  • OpenAL结合平台音频解析类AudioToolbox实现播放声音
  • 遇到和解决的问题
首先,主要参考了,IOS开发官网的两个demo, OpenALExample 和  GLAirplay。这里我们只谈最基本的实现,加载声音文件,播放声音。至于3D音效,多普勒效应环境音效设置,声音位置,收听位置等都不进行配置。

第一,需要导入的平台头文件。
#include <stddef.h>
#include <Foundation/Foundation.h>
#include <AudioToolbox/AudioToolbox.h>
#include <OpenAL/OpenAL.h>

文件需要使用.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全局只需要一个ALCdevice和ALCcontext。
  • 我们抽象了一个AudioPlayer,用来对应一个播放器,bufferId就是加载到内存的音频数据,sourceId是对应OpenAL播放器。
  • alBufferDataStatic是OpenAL的一个扩展,相对于alBufferData来说的。功能是加载音频数据到内存并关联到bufferId。只不过,alBufferData会拷贝音频数据所以调用后,我们可以free掉音频数据。而alBufferDataStatic并不会拷贝,所以音频数据data我们要一直保留并自己管理。

第三,我们需要加载声音文件,解析音频数据,修改音频数据格式为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等等。


第四,利用音频文件数据,生成我们的播放器对象。
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);
    }
}
  • 首先,生成bufferId,sourceId。
  • 然后,把音频数据关联到bufferId,把bufferId关联到sourceId。
  • 最后,sourceId代表就是OpenAL的播放器,可以设置各种属性。
  • 那么,当我们需要销毁播放器的时候,主要也就是销毁sourceId,和bufferId。

第五,设置播放器的各种属性。
static 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并没有播放完成的回调。随意我们需要在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个播放器。所以播放器用完还是及时的删除比较好。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值