本文介紹以下幾點內容:
- OpenAL API的使用介紹
- 從IOS的mainBundle讀取載入音頻文件
- OpenAL結合平臺音頻解析類AudioToolbox實現播放聲音
- 遇到和解決的問題
#include <stddef.h>
#include <Foundation/Foundation.h>
#include <AudioToolbox/AudioToolbox.h>
#include <OpenAL/OpenAL.h>
文件需要使用.m文件,因爲需要使用Foundation.h的功能來加載Bundle的聲音文件。m後綴文件是c和objc混編的文件類型。AudioToolbox可以對音頻文件信息的解析和設置,以配合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我們要一直保留並自己管理。
- 首先我們要獲取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;
}
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;
}
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);
}
}
}