本文主要介绍如何使用OpenAL进行PCM数据的播放,文中会讲解我在项目中遇到的问题以及如何解决的,对于什么是采样率等基本知识,在此不做介绍。
OpenAL有使用手册,具体的API作用,可以自己进行查阅。
刚进公司,就被分配来做一个项目,项目是接收胎心仪蓝牙传输的数据,进行实时绘制胎心率曲线和实时播放胎心音,其中播放胎心音我使用的便是OpenAL。
先上代码:
OGOpenAL.h
#import
#import
#import
#define AUDIO_SIMPLE_RATE 8000//采样率
#define SOUND_SAMPLES AL_FORMAT_MONO8//声道,8位
//最大缓存个数,如果数据缓存多余MAX_BUFFERS时,数据不加载,抛弃掉。解决延时问题
#define MAX_BUFFERS 13
@interface OGOpenAl : NSObject {
//内容,相当于给音频播放器提供一个环境描述
ALCcontext *m_Context;
//硬件,获取电脑或者ios设备上的硬件,提供支持
ALCdevice *m_Device;
//音源,相当于一个ID,用来标识音源
ALuint m_sourceID;
//线程锁
NSCondition *m_DecodeLock;
}
//初始化播放器
-(BOOL)initOpenAl;
//连续传入PCM音频数据的方法
-(void)openAudio:(NSData *)data length:(UInt32)pLength;
//停止播放
-(void)stopSound;
//释放播放器占用
-(void)clearOpenAL;
@end
OGOpenAL.m
#import "OGOpenAl.h"
#import
@implementation OGOpenAl
-(BOOL)initOpenAl {
NSLog(@"初始化播放器");
if (m_Device ==nil) {
//参数为NULL , 让ALC 使用默认设备,默认一个只能指定一个设备,多次指定会一直返回NULL,导致下面m_Device为nil
m_Device = alcOpenDevice(NULL);
}
if (m_Device==nil) {
//注:执行clearOpenAL方法可以清除Device占用。initOpenAL需与clearOpenAL一对一使用
NSLog(@"初始化播放器失败,播放器Device已被占用,请清除Device占用后再进行初始化");
return NO;
}
if (m_Context==nil) {
if (m_Device) {
//与初始化device是同样的道理
m_Context =alcCreateContext(m_Device, NULL);
alcMakeContextCurrent(m_Context);
}
}
//设置播放方式:扬声器。否则扬声器不发音,只有耳机
NSArray* output = [[AVAudioSession sharedInstance] currentRoute].outputs;
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
[[AVAudioSession sharedInstance] setActive:YES error:nil];
NSLog(@"current output:%@",output);
//初始化音源ID
alGenSources(1, &m_sourceID);
// 设置音频播放是否为循环播放,AL_FALSE是不循环
alSourcei(m_sourceID, AL_LOOPING, AL_FALSE);
// 设置声音数据为流试,(openAL 针对PCM格式数据流)
alSourcef(m_sourceID, AL_SOURCE_TYPE, AL_STREAMING);
//设置音量大小,1.0f表示最大音量。openAL动态调节音量大小就用这个方法
alSourcef(m_sourceID, AL_GAIN, 1.0f);
//多普勒效应,这属于高级范畴,不是做游戏开发,对音质没有苛刻要求的话,一般无需设置
alDopplerVelocity(1.0);
//同上
alDopplerFactor(1.0);
//设置声音的播放速度
alSpeedOfSound(1.0);
//初始化线程锁
m_DecodeLock =[[NSCondition alloc] init];
if (m_Context==nil) {
return NO;
}
return YES;
}
//这个函数就是比较重要的函数了, 将收到的pcm数据放到缓存器中,再拿出来播放
-(void)openAudio:(NSData *)data length:(UInt32)pLength {
//1.对缓存操作进行加锁,操作过程中不允许其他操作,避免多线程调用
[m_DecodeLock lock];
//2.读取错误信息
ALenum error;
error = alGetError();
if (error != AL_NO_ERROR) {
[m_DecodeLock unlock];
NSLog(@"插入PCM数据时发生错误, 错误信息: %d", error);
[self clearBuffer];
return;
}
//3.常规安全性判断
if (data == NULL) {
[m_DecodeLock unlock];
NSLog(@"插入PCM数据为空, 返回");
return;
}
//4.建立缓存区
ALuint bufferID = 0;
alGenBuffers(1, &bufferID);
error = alGetError();
if (error != AL_NO_ERROR) {
[m_DecodeLock unlock];
NSLog(@"建立缓存区发生错误, 错误信息: %d", error);
return;
}
//5.将数据添加到缓存区中
SignedByte bytes[pLength];
[data getBytes:bytes length: pLength];
//6.将转好的NSData存放到之前初始化好的一块buffer区域中并设置好相应的播放格式
alBufferData(bufferID, SOUND_SAMPLES, bytes , (ALsizei)pLength, AUDIO_SIMPLE_RATE);
error = alGetError();
if (error != AL_NO_ERROR) {
[m_DecodeLock unlock];
NSLog(@"向缓存区内插入数据发生错误, 错误信息: %d", error);
return;
}
//7.清除缓存
[self clearBuffer];
//8.读取队列信息。获取音源的缓冲队列,以便监听控制播放的延迟
int processed ,queued;
alGetSourcei(m_sourceID, AL_BUFFERS_PROCESSED, &processed);
alGetSourcei(m_sourceID, AL_BUFFERS_QUEUED, &queued);
NSLog(@"缓存队列数 %d", queued);
//如果缓冲队列大于MAX_BUFFERS则将该包数据抛弃,不添加进入缓冲区,不进行播放。
//目的是降低延迟,例如:延迟时间控制在210ms,每30ms发送一包,则MAX_BUFFERS=7
if (queued <= MAX_BUFFERS) {
//将缓存添加到声源上(添加便会进行播放,不添加不播放)
alSourceQueueBuffers(m_sourceID, 1, &bufferID);
error = alGetError();
if (error != AL_NO_ERROR) {
[m_DecodeLock unlock];
NSLog(@"将缓存区插入队列发生错误, 错误信息: %d", error);
return;
}
}
//9.判断是否有错,有错误则清除缓存
if ((error=alGetError())!=AL_NO_ERROR) {
NSLog(@"play failed");
alDeleteBuffers(1, &bufferID);
[m_DecodeLock unlock];
return;
}
[m_DecodeLock unlock];
//10.播放声音
ALint state;
alGetSourcei(m_sourceID, AL_SOURCE_STATE, &state);
if (state !=AL_PLAYING) {
alSourcePlay(m_sourceID);
}
}
/**
* 播放声音
*/
-(void)playSound {
ALint state;
alGetSourcei(m_sourceID, AL_SOURCE_STATE, &state);
if (state != AL_PLAYING) {
alSourcePlay(m_sourceID);
}
}
/**
* 停止播放
*/
-(void)stopSound {
[self playSound];
ALint state;
alGetSourcei(m_sourceID, AL_SOURCE_STATE, &state);
if (state != AL_STOPPED) {
alSourceStop(m_sourceID);
}
}
/**
* 清空播放器
*/
-(void)clearOpenAL {
//删除声源
alDeleteSources(1, &m_sourceID);
if (m_Context != nil) {
//删除环境
alcDestroyContext(m_Context);
m_Context=nil;
NSLog(@"删除环境");
}
if (m_Device !=nil) {
//关闭设备
alcCloseDevice(m_Device);
m_Device=nil;
NSLog(@"关闭设备");
}
}
//清楚缓存
-(void)clearBuffer{
ALint processed;
//获取音源的缓冲队列
alGetSourcei(m_sourceID, AL_BUFFERS_PROCESSED, &processed);
//遍历清空缓冲区
while (processed--) {
ALuint buffer;
alSourceUnqueueBuffers(m_sourceID, 1, &buffer);
alDeleteBuffers(1, &buffer);
}
}
//获取队列信息
- (void)getInfo {
ALint queued;
ALint processed;
alGetSourcei(m_sourceID, AL_BUFFERS_PROCESSED, &processed);
alGetSourcei(m_sourceID, AL_BUFFERS_QUEUED, &queued);
NSLog(@"process = %d, queued = %d", processed, queued);
}
@end
1.initOpenAl
该方法是初始化播放器方法,播放前必须初始化播放器,目的是设置播放器的参数。
2.openAudio:length:
该方法是播放数据的方法,通过连续不断的调用该方法,传入PCM音频数据,将数据添加到播放队列里面,实现不间断PCM数据播放。
3.clearOpenAL
该方法是释放播放器,清空播放器占用。
1.
initOpenAl方法和clearOpenAL方法必须一对一使用,为什么呢?查看方法内部,你会看到,初始化播放器时,会调用alcOpenDevice的方法,该方法是获取到播放器的device,一旦获取过device后,如果没有释放掉,下次再获取时,会返回null,届时无论你怎么传入播放数据,都会没有声音,因为device是空的。而clearOpenAL便是释放掉device。我刚入手时就遇到这个坑,播放数据怎么也听不到声音。建议外面调用initOpenAl和clearOpenAL时,使用一个标志参数来控制,如isPlay,当为NO时才可以初始化,当为YES时才可以停止。避免连续初始化。
2.
我发现网上很多人的播放器封装,并没有设置扬声器,导致播放音频时,只能带上耳机听声音,扬声器没有声音,因此我在播放器初始化方法里将播放通道设置为扬声器(当然不会影响耳机的播放,因为苹果系统设置了优先级,一旦插入耳机,变强制默认为耳机播放)。
3.
播放的数据,我在使用OpenAL前,试用过AudioQueue框架,他们两在播放数据上的区别是OpenAL播放的数据基线(音量为0时)数值在8位播放器下为127~128,即播放的数据范围是0~255,而AudioQueue的基线是0,播放的数据范围是-127~128,因此如果不进行数据转化,在OpenAL上播放是正常的,拿到AudioQueue下进行播放,底噪会非常大,原声会很模糊很低。包括16位播放器,在使用我的播放器封装前,应该先打印出播放的PCM数据,了解清楚数据的基线是什么,如果不合适需要手动将每个数据进行转化。
4.
我看到很多博客上的播放器封装,都会有播放和停止播放的方法,但是很多人使用的时候并不知道如何使用。盲目的直接调用停止方法,而不停止播放数据的传入,会导致播放器已经停止了播放,但是播放缓存还一直在添加数据,播放缓存最大个数是1024个,当数据超过缓存个数时,程序会崩溃。因此建议在我们APP中使用到停止播放器时(比如APP进入后台需要停止播放、或者其他停止播放),停止播放并释放播放器,重新开始时再初始化播放器进行播放。我在开发过程中就经常遇到APP被打断,然后听不到声音的情况,包括接听电话、进入后台、闹铃等等。
5.
很多人使用播放器进行PCM播放,都是实时播放,那么如何控制播放器的延时呢?我这个进行了控制延时的操作,宏定义了一个延时的缓存个数MAX_BUFFERS,当添加的缓存个数大于MAX_BUFFERS时,后面新添加的缓存不进行播放,直到缓存的音频数据被播放后,缓存个数少于MAX_BUFFERS时才继续进行添加缓存。我们都直到,缓存的多少决定了延时的长短。如果不需要控制延时,建议将延时代码注释掉。使用时需注意,如果方法2.openAudio:length:给入数据过快,而播放过慢导致大量缓存数据堆积,延时的方法会过滤掉大部分数据,而导致播放声音不连续。
6.
很多新手不知道如何控制缓存和播放数据的速度。我在这里简单介绍一下,设每包数据长度为125,采样率位4000Hz,那么你应该每125/4000=1/32=0.03125秒传入一包数据(即调用方法2)。如果你传入数据的速度过快,播放器缓存的数据会越来越多,延时会越来越长,如果传入的数据过慢,播放器播放完数据后,会默认重新播放最后一包数据。
平时很少写博客,表达得不是很流畅,只是想到什么写什么,忘见谅。另外文中避免不了有不足的地方,请大神多多指教。我建了个小QQ群143898492,欢迎大家的加入,希望我的分享能够帮助大家。谢谢!