音频的播放,这里用的时OpenSLES,这是一套跨平台,针对嵌入式系统做过优化的api,它为嵌入式移动多媒体设备上
的本地应用程序提供标准化, 高性能,低响应时间的音频功能实现方法,并实现软/硬件音频性能的直接跨平台
部署,降低执行难度.
当然Android平台上音频的播放,也可以借助java层AudioTrack接口,但是因为ffmpeg的整个处理流程都是在native层,所以使用NDK提供的OpenSLES 的api,直接在native层处理音频数据,避免了跟java层之间的数据拷贝,效率更高.
OpenSLES的使用:
OpenSLES通过Object和Interface来使用,什么意思呢?就是一个Object可能提供很多函数,但是你不能直接通过Object来调用它提供的函数,而是要先拿到Object的相应接口Interface,然后通过Interface去调用相应的函数,每一种Object都提供了一系列的Interface,相当于Interface对Object中函数做了一个分类.
使用OpenSLES播放音频的流程:
1. 创建引擎对象
2. 设置混音器
3. 创建播放器
4. 开始,停止播放
结合源码看下实现:
跟视频绘制类似,这里也要有解码线程,播放线程:
void AudioChannel::play() {
//因为frame_queue中数据格式,可能不是我们想要的,所以这里创建一个转换器
//第二个参数,输出声道数,
swrContext = swr_alloc_set_opts(0, AV_CH_LAYOUT_STEREO, AV_SAMPLE_FMT_S16, 44100,
avCodecContext->channel_layout,avCodecContext->sample_fmt,avCodecContext->sample_rate,
0, 0);
swr_init(swrContext);
isPlaying = 1;
setEnable(1);
//解码音频流,单独的线程
pthread_create(&audioDecodeTask, 0, audioDecode_t, this);
//播放音频,单独的线程。
pthread_create(&audioPlayTask, 0, audioPlay_t, this);
}
解码调用的接口,跟视频解码是类似的
void AudioChannel::decode() {
AVPacket *packet = 0;
//从待解码队列中取出待解码数据,送去解码,
while (isPlaying) {
int ret = pkt_queue.deQueue(packet);
//如果取出失败,继续循环,如果停止了播放,就退出循环。
if (!ret) {
continue;
}
if (!isPlaying) {
break;
}
//送去解码,先是send,然后receive
ret = avcodec_send_packet(avCodecContext, packet);
releaseAvPacket(packet);//只要把包交给了解码器,就可以释放了,因为解码器会复制一份,
if (ret <0) {
break;//如果提交失败了,继续循环,实际项目中,要做更多的处理,
}
//获取解码后的数据,
AVFrame *frame = av_frame_alloc();
ret = avcodec_receive_frame(avCodecContext, frame);
if (ret == AVERROR(EAGAIN)) {
continue;//如果需要更多待解码数据,继续循环。
} else if (ret < 0) {
break;
}
//放入解码后的数据队列。
frame_queue.enQueue(frame);
}
}
播放的过程,我把注释写在了代码里,
void AudioChannel::_play() {
//创建播放引擎对象,创建成功后,需要初始化。
SLresult result;
result = slCreateEngine(&engineObject, 0, nullptr, 0, nullptr, nullptr);
if (SL_RESULT_SUCCESS != result) {
return;
}
//引擎对象初始化,第二个参数表示同步还是异步,false表示同步
result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
if (SL_RESULT_SUCCESS != result) {
return;
}
//获取引擎对象,可以提供的接口。
result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineInterface);
//通过引擎接口,调用引擎对象的方法,创建混音器,
result = (*engineInterface)->CreateOutputMix(engineInterface, &outputMixObject, 0, 0, 0);
if (SL_RESULT_SUCCESS != result) {
return;
}
//混音器创建成功,初始化混音器
result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
if (SL_RESULT_SUCCESS != result) {
return;
}
//创建播放器所需的数据源,SLDataSource中的属性,第一个是数据的获取器,或者说是定位器,表示数据从哪里定位,
// 那么数据从哪里定位呢?就是从队列中定位,我们就是往这个队列中放数据,
// SLDataLocator_AndroidSimpleBufferQueue是opensl es专门为android平台定义的队列,
// 第二个是数据的格式,音频数据的格式有多种,
// 我们这里为播放器指定一种,不管解码出的数据格式是什么样的,都可以通过swresample重采样模块,转成我们指定的格式。
SLDataLocator_AndroidSimpleBufferQueue android_queue = {
SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};
//数据类型,声道数,采样率,采样位,容器大小,双声道,小端字节序
SLDataFormat_PCM pcm = {
SL_DATAFORMAT_PCM,
2,
SL_SAMPLINGRATE_44_1,
SL_PCMSAMPLEFORMAT_FIXED_16,
SL_PCMSAMPLEFORMAT_FIXED_16,
SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,
SL_BYTEORDER_LITTLEENDIAN
};
SLDataSource slDataSource = {&android_queue, &pcm};
//创建播放器所需的接收端,SLDataSink, 那么sink,实际是播放过程的一个控制者,它会不断的去拿解码号的数据,
// 送到播放设备区播放。所以sink是对混音器SLDataLocator_OutputMix的包装。
//混音器才是真正去播放音频数据的,播放器实际是对混音器的封装,提供了额外的暂停,快进等操作。
SLDataLocator_OutputMix outputMix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject};
SLDataSink audioSink = {&outputMix, nullptr};
//创建播放器希望获取的接口
const SLInterfaceID ids[1] = {SL_IID_BUFFERQUEUE};
const SLboolean req[1] = {SL_BOOLEAN_TRUE};
//创建播放器,
//第三个参数,SLDataSource,数据源,以队列的形式提供,播放器会从这个队列中拿数据,
// 我们只要往这个队列中放数据,就可以连续播放了。
//第四个参数,audioSink,
//第五个参数,希望获取这个播放器的几套接口,因为只有获取到接口,才能通过接口调用播放器提供的相应方法,
// 比如说播放开始,暂停的方法在一套接口中,处理播放队列的方法,在另一套接口中。相当于用接口对 这个对象的方法进行了分类,
//因为播放器对象,默认提供了一套播放状态控制的接口,不需要主动去获取,这里获取的是额外的一套接口,就是数据队列操作接口,
//第六个参数,希望获取的接口的ID,
//第七个参数,希望获取的接口,是不是必须的。
(*engineInterface)->CreateAudioPlayer(engineInterface, &playerObject, &slDataSource, &audioSink,
1, ids, req);
(*playerObject)->Realize(playerObject, SL_BOOLEAN_FALSE);
//播放器对象有了,怎么让他运行起来?
//1,把播放器设置为播放状态,
//2,把要播放的数据放入播放队列中,
//这里的顺序要是先注册队列回调,然后设置为播放状态。
//获取播放队列操作接口
(*playerObject)->GetInterface(playerObject, SL_IID_BUFFERQUEUE, &playerBufferQueue);
//设备播放队列的回调方法,在这个回调方法中,给播放器填数据,
(*playerBufferQueue)->RegisterCallback(playerBufferQueue, playerBufferQueueCallback,this);
//获取播放状态接口,
(*playerObject)->GetInterface(playerObject, SL_IID_PLAY, &statusInterface);
//设置为播放状态
(*statusInterface)->SetPlayState(statusInterface, SL_PLAYSTATE_PLAYING);
//最后一步,要主动调用一次回调方法,才会开始播放
playerBufferQueueCallback(playerBufferQueue, this);
}
真正开启播放是要调用播放队列接口的回调处理才开始的.
在开始播放前,
//使用转换器,把frame_queue中的数据,转成我们需要的。把转换后的数据放入buffer,返回值表示转换数据的大小。
int AudioChannel::_getData() {
int dataSize = 0;
AVFrame *frame = 0;
while (isPlaying) {
int ret = frame_queue.deQueue(frame);
if (!isPlaying) {
break;
}
if (!ret) {
continue;
}
//第二,三个参数,用来接收转换出来的数据,bufferCount表示这个buffer最多可以装多少数据,
//这里需要注意的最后两个参数,frame->data,
// 最后一个参数frame->nb_samples,表示一个声道的有效样本数,(而不是frame->data的字节数,),
// 一个样本大小,是根据采样位,采样率计算的。
int nb = swr_convert(swrContext, &buffer, bufferCount, (const uint8_t **)frame->data, frame->nb_samples);
//f返回值,表示转换出来的每个声道的样本数,也即是往buffer中装了多少样本数,再乘以样本的大小,可以得到转换数据的字节数。
dataSize = nb * out_channels * out_sampleSize;
//获取这段音频的时刻,pts表示这一帧的时间戳,以time_base为单位的时间戳,time_base是AVRational结构体类型,
// 也就是pts的单位是 (AVRational.Numerator / AVRational.Denominator),这样下面得出的时间单位是秒。
clock = frame->pts * av_q2d(time_base);
break;
}
releaseAvFrame(frame);
return dataSize;
}
//播放器会从这个SLAndroidSimpleBufferQueueItf这个队列中拿数据,那么往这个队列中填的数据的格式,
// 必须是我们在在创建播放器时指定的格式,
// 也就是创建播放器对象的第三个参数SLDataSource slDataSource中指定的SLDataFormat_PCM pcm
void playerBufferQueueCallback(SLAndroidSimpleBufferQueueItf queue, void *context) {
AudioChannel *audioChannel = static_cast(context);
int dataSize = audioChannel->_getData();
//swr_convert,转换后的buffer,提交到播放器的队列中,
if (dataSize >0) {
(*queue)->Enqueue(queue, audioChannel->buffer, dataSize);
}
}
执行到这里,音频就播放出来了,