IOS中用AudioQueue播放缓冲区数据

最近在做一个在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];
    }

}


这样,播放结束时我上层的应用就可以得到通知,从而使得我们可以开始下一个主题:AudioSession的处理。


源代码下载:Player.zip下载页

联系方式:QQ:348930933






  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值