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個播放器。所以播放器用完還是及時的刪除比較好。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章