最近在做一个在iPhone上进行声音处理的软件,需要从内存中获取数据来播放。在网上搜了半天,只找到用audioqueue播放文件的,没有直接播放缓冲区的代码。其实播放缓冲区和播放文件的原理类似,所以自己写了一个,现在分享出来以节省大家的精力。
先了解一下Linear PCM(pulse-code modulated) ,翻译成中文是“线性脉冲编码调制”,这是最简单直白的音频格式,不过我们这里够用了。
有个概念:采样率(sample rate)。计算机只能存储数字信号,声音是模拟信号,只能采样成数字信号再存储。采样率就是每秒采样的次数。
(红色线就是采样的地方,可见数字信号是对模拟信号的近似,采样率越高,与模拟信号的相似度越高。)
就我们的线性PCM编码而言,一般采用的采样率是44100即每秒采样44100次。IOS里都是采用short型(2字节)存储采样数据,即每个采样2字节,现在我们用的是立体声,即2个channel,左声道+右声道,加起来是4字节,因此有以下:
m_audioFormat.mFormatID = kAudioFormatLinearPCM;
m_audioFormat.mSampleRate = 44100;
m_audioFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger;
m_audioFormat.mFramesPerPacket = 1;
m_audioFormat.mBytesPerFrame = 4;
m_audioFormat.mBitsPerChannel = 16;
m_audioFormat.mBytesPerPacket = 4;
m_audioFormat.mChannelsPerFrame = 2;
对于PCM而言,mFramesPerPacket总是1,就是每个包(Packet)里面只有一个帧(Frame),对于其他的压缩编码,一个包里可能会含有多个帧。
要播放,首先要初始化Audioqueue:
bool CAudioQueuePlayer::Init(int sampleRate, int bytesPerFrame, int channelCount)
{
bool ret = true;
OSStatus ecode;
m_audioFormat.mFormatID = kAudioFormatLinearPCM;
m_audioFormat.mSampleRate = sampleRate;
m_audioFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
m_audioFormat.mFramesPerPacket = 1;
m_audioFormat.mBytesPerFrame = bytesPerFrame;
m_audioFormat.mBitsPerChannel = (m_audioFormat.mBytesPerFrame/channelCount)*8;
m_audioFormat.mBytesPerPacket = m_audioFormat.mBytesPerFrame;
m_audioFormat.mChannelsPerFrame = channelCount;
m_queueBufferSize = WANNER_BUFFER_SIZE;
ecode = AudioQueueNewOutput(&m_audioFormat, AQOutputCallback, this, NULL, kCFRunLoopCommonModes, 0, &m_queue);
if(noErr != ecode) ret = false;
ecode = AudioQueueAddPropertyListener(m_queue, kAudioQueueProperty_IsRunning, MyAudioQueuePropertyListenerProc, this);
if(noErr != ecode) ret = false;
for (int i = 0; i < BUFFER_COUNT; ++i) {
OSStatus ecode = AudioQueueAllocateBuffer (m_queue, m_queueBufferSize, &m_queueBuffer[i]);
if(noErr != ecode)
{
ret = false;
break;
}
}
return ret;
}
这段代码的思路:
1.首先初始化audioFormat
2.调用AudioQueueNewOutput来初始化audioqueue,将回调函数“AQOutputCallback”传递给audioqueue,audioqueue在需要播放数据时,会调用你的回调函数来取得数据。
3.调用AudioQueueAddPropertyListener添加属性变化监听器。
4.调用AudioQueueAllocateBuffer申请播放的缓冲区,一般BUFFER_COUNT等于3.
之后就可以播放了:
bool CAudioQueuePlayer::Play()
{
if(m_isRunning)
{
return false;
//Stop();
}
printf("player begin.\n");
m_isRunning = true;
// fill play buffer first.
for (int i = 0; i < BUFFER_COUNT; ++i) {
AQOutputCallback (this, m_queue, m_queueBuffer[i]);
}
bool ret = true;
OSStatus ecode = AudioQueueStart (m_queue, NULL);
if(noErr != ecode) ret = false;
return ret;
}
播放时,audioqueue会不断的调用我们的回调函数(AQOutputCallback)来获取播放数据。
苹果官方例子是播放文件,在回调中就从文件中读取数据,我们这里是播放缓冲区,所以直接从缓冲区获取数据。
在本例中,我使用NSMutableArray来缓冲数据,因此回调中的操作就是将NSMutableArray中的内容逐次拷贝到audioqueue的播放缓冲区中,然后调用AudioQueueEnqueueBuffer来告知audioqueque数据读取完毕。
这块代码由于先用C++写的,后来用Objective C封装,所以稍稍有些绕:
void CAudioQueuePlayer::AQOutputCallback (void *pParam, AudioQueueRef queue, AudioQueueBufferRef queueBuffer)
{
CAudioQueuePlayer* pPlayer = (CAudioQueuePlayer*)pParam;
if (!pPlayer->m_isRunning) return;
queueBuffer->mAudioDataByteSize = pPlayer->m_fnReadData(queueBuffer->mAudioData, pPlayer->m_queueBufferSize, pPlayer->m_readDataParam);
if(queueBuffer->mAudioDataByteSize > 0)
{
AudioQueueEnqueueBuffer(queue, queueBuffer, 0, NULL);
}
else
{
AudioQueueStop (queue, false);
}
}
下面是“ pPlayer->m_fnReadData"的代码:(建议大家下载Player的代码观看)
static unsigned int OnFetchPlayData(void* fillTo, unsigned int size, void* param){
Player * player = (__bridge Player *)param;
if (player.playerQueue.count == 0) {
return 0;
}
char *pFill = (char *)fillTo;
int sizeToRead = size;
while (sizeToRead > 0 && player.iPlayerQueue < [player.playerQueue count]) {
NSData *data = [player.playerQueue objectAtIndex:player.iPlayerQueue];
if ([data length] - player.iPlayerPos > sizeToRead) {
memcpy(pFill, ((char*)[data bytes])+player.iPlayerPos, sizeToRead);
player.iPlayerPos += sizeToRead;
sizeToRead = 0;
}
else{
memcpy(pFill, ((char*)[data bytes])+player.iPlayerPos, [data length] - player.iPlayerPos);
sizeToRead -= [data length] - player.iPlayerPos;
pFill += ([data length] - player.iPlayerPos);
player.iPlayerQueue++;
player.iPlayerPos = 0;
}
}
printf("return size:%d\n", size-sizeToRead);
return size - sizeToRead;
}
记得一开始初始化的时候我们用”AudioQueueAddPropertyListener“安装了一个属性监听器,它会在播放器属性变化时(包括播放结束)得到调用:
void CAudioQueuePlayer::MyAudioQueuePropertyListenerProc(void *pParam, AudioQueueRef inAQ, AudioQueuePropertyID inID)
{
CAudioQueuePlayer* pPlayer = (CAudioQueuePlayer*)pParam;
switch (inID) {
case kAudioQueueProperty_IsRunning:
{
UInt32 bRunning;
UInt32 size = sizeof(bRunning);
OSStatus result = AudioQueueGetProperty(inAQ, kAudioQueueProperty_IsRunning, &bRunning, &size);
if ((result == noErr) && (!bRunning))
{
pPlayer->m_isRunning = false;
if (pPlayer->m_fnOnPlayStop) {
pPlayer->m_fnOnPlayStop(pPlayer->m_onPlayStopParam);
}
printf("player stoped\n");
}
break;
}
default:
break;
}
}
同播放一样,这里的代码是先用C++,后来用Objective C做的封装,下面是pPlayer->m_fnOnPlayStop的代码:
static void OnPlayStop(void* param){
Player *player = (__bridge Player *)param;
if (player.delegate != nil && [player.delegate respondsToSelector:@selector(onPlayStop)]) {
[player.delegate onPlayStop];
}
}
源代码下载:Player.zip下载页
联系方式:QQ:348930933