本文介绍以下几点内容:
- 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;
}
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;
}
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);
}
}
}