我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Android 播放音频(PCM)的两种方法--AudioTrack/OpenSL ES使用简介

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Android 播放音频(PCM)的两种方法--AudioTrack/OpenSL ES使用简介

本文主要介绍Android上可以进行音频(PCM)播放的两个组件–AudioTrack/OpenSL ES的简单使用方法。

对一个音频文件(如MP3文件),如何使用FFmpeg进行解码获取到PCM,之前的文章已经有相应的说明:
https://blog.csdn.net/myvest/article/details/89254452。
那么解码后或者mic采集的PCM数据,是如何播放的呢,首先一般会对PCM数据进行重采样,也即是转换为指定的格式。重采样可以参考:https://blog.csdn.net/myvest/article/details/89442000

最后,进入本文主题,介绍AudioTrack/OpenSL ES的简单使用方法。

1 AudioTrack简介

AudioTrack是Android系统中管理和播放单一音频资源的类,Android提供了java层及native层的api,使用也比较简单,一般我推荐使用这个组件播放。
但需要注意的是,它仅能播放已经解码出来的PCM数据。

1.1 使用方法及API简介

我们以java端的api为例(native层基本一致),AudioTrack使用方法如下:
1、创建:

public AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat,
            int bufferSizeInBytes, int mode);

参数说明:
1)int streamType:指定即将播放的声音类型,对于不同类型,Android的audio系统会有不同处理(如音量等级不同,音量控制不同等),一些常见类型如下,对于音乐文件,我们使用STREAM_MUSIC

STREAM_ALARM:警告声 STREAM_MUSIC:音乐声,例如music等 STREAM_RING:铃声 STREAM_SYSTEM:系统声音,例如低电提示音,锁屏音等 STREAM_VOCIE_CALL:通话声

AudioTrack有两种数据加载模式(MODE_STREAM和MODE_STATIC),对应的是数据加载模式和音频流类型, 对应着两种完全不同的使用场景。

2)int sampleRateInHz:采样率
3)int channelConfig:音频声道对应的layout,如立体声是AudioFormat.CHANNEL_OUT_STEREO
4)int audioFormat:音频格式
5)int bufferSizeInBytes:缓冲区大小
缓冲区大小可以通过函数getMinBufferSize获取,传入采样率、声道layout、音频格式即可,如下:

public AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);

6)int mode:数据加载模式(MODE_STREAM和MODE_STATIC),两种模式对应着两种不同的使用场景

MODE_STREAM:在这种模式下,通过write一次次把音频数据写到AudioTrack中。这和平时通过write系统调用往文件中写数据类似,但这种工作方式每次都需要把数据从用户提供的Buffer中拷贝到AudioTrack内部的Buffer中,这在一定程度上会使引入延时。为解决这一问题,AudioTrack就引入了第二种模式。 MODE_STATIC:这种模式下,在play之前只需要把所有数据通过一次write调用传递到AudioTrack中的内部缓冲区,后续就不必再传递数据了。这种模式适用于像铃声这种内存占用量较小,延时要求较高的文件。但它也有一个缺点,就是一次write的数据不能太多,否则系统无法分配足够的内存来存储全部数据。

2、启动:

public AudioTrack.play();

3、数据注入:

public int write(byte[] audioData, int offsetInBytes, int sizeInBytes);

参数比较简单,数据、偏移、size

4、停止:

public AudioTrack.stop();		

5、释放:

public   AudioTrack.release();

6、获取状态:
STATE_INITIALIZED和STATE_UNINITIALIZED就不用说明了。STATE_NO_STATIC_DATA是个中间状态,当使用MODE_STATIC模式时,创建AudioTrack ,首先会进入改状态,需要write数据后,才会变成STATE_INITIALIZED状态。非STATE_INITIALIZED状态下去进行play的话,会抛出异常,所以MODE_STATIC模式如果没有write就去play是不行的。

    
    public int getState() {
        return mState;
    }
1.2 AudioTrack使用示例

java端示例

private AudioTrack audioTrack = null;
	private static int sampleRateInHz = 44100;
	private static int channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
	private static int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
	private int bufferSize = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
	class processThread implements Runnable {
......省略
		public void run() {		    			
		    byte[] outBuf = new byte[DECODE_BUFFER_SIZE];
		    if(audioTrack == null){
				audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRateInHz, channelConfig, audioFormat, bufferSize,
					AudioTrack.MODE_STREAM);
				if(audioTrack == null){
					mFFdec.decodeDeInit();			
					return ;
				}
		    }
		    try {
			   if ( audioTrack.getState() != AudioTrack.STATE_UNINITIALIZED) {
					audioTrack.play();
			   }
			   while(true){
				   int size = mFFdec.decodeFrame(outBuf);
				   if(size > 0){
						if(mFFdec.getMediaType() == mFFdec.MEDIA_TYPE_AUDIO){//audio
							audioTrack.write(outBuf, 0, size);
						}
				   }else{
				   		break;
				   }
			   }			   
			   if (audioTrack.getState() != AudioTrack.STATE_UNINITIALIZED) {
					audioTrack.stop();
					audioTrack.release();
			   }			   
		    }catch (Exception ex) {
				   ex.printStackTrace();
		    } catch (Throwable t) {
				   t.printStackTrace();
		    }		    
		    audioTrack = null;
		    mFFdec.decodeDeInit();
		}
	}
2 OpenSL ES简介

OpenSL ES(Open Sound Library for Embedded Systems,开源的嵌入式声音库)是一个免授权费、跨平台、C语言编写的适用于嵌入式系统的硬件加速音频库。简单来说,它提供了一些标准化的api,让开发者可以在不同硬件平台上,操作音频设备。包括播放,录制等等。

虽然是C语音,但其采用了面向对象的方式,在开发中,我们使用对象(object)和接口(interface)来开发。

2.1 对象(object)和接口(interface) 对象:提供一组资源极其状态的抽象,例如录音器对象,播放器对象。 接口:对象提供一特定功能方法的抽象,例如播放器对象的播放接口。

也就是说每种对象都提供了一些最基础的操作:Realize,Resume,GetState,Destroy 等等,但是对象不能直接使用,必须通过其 GetInterface 函数用ID号拿到指定接口(如播放器的播放接口),然后通过该接口来访问功能函数。

所有对象在创建后都要调用Realize 进行初始化,释放时需要调用Destroy 进行销毁。

2.2 OpenSL ES 音频播放方法

音频播放场景:
在这里插入图片描述
从该场景图来讲解播放方法:
1、创建OpenSL engine,
2、通过engine创建 AudioPlayer和outputMix
3、指定AudioPlayer的输入为DataSource,输出为outputMix,outputMix是会关联到设备的默认输出。
4、启动播放。

下面详细讲解各个部分

2.2.1 Engine

OpenSL ES 里面最核心的对象,它主要提供如下两个功能:
(1) 管理 Audio Engine 的生命周期。
(2) 提供管理接口: SLEngineItf,该接口可以用来创建所有其他的对象。

使用步骤:
1、创建Engine

SLresult SLAPIENTRY slCreateEngine(
SLObjectItf *pEngine,
SLuint32 numOptions
constSLEngineOption *pEngineOptions,
SLuint32 numInterfaces,
constSLInterfaceID *pInterfaceIds,
constSLboolean *pInterfaceRequired
)

参数说明:

pEngine:指向输出的engine对象的指针。 numOptions:可选配置数组的大小。 pEngineOptions:可选配置数组。 numInterfaces:对象要求支持的接口数目,不包含隐含的接口。 pInterfaceId:对象需要支持的接口id的数组。 pInterfaceRequired:指定每个要求接口的接口是可选或者必须的标志位数组。如果要求的接口没有实现,创建对象会失败并返回错误码SL_RESULT_FEATURE_UNSUPPORTED。

2、创建完engine后就可以通过

SL_IID_ENGINE
获取管理接口:

//创建对象
SLObjectItf engineObject;
slCreateEngine( &engineObject, 0, nullptr, 0, nullptr, nullptr );
//初始化
(*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
//获取管理接口
static SLEngineItf iengine = NULL;
(*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &(iengine));

3、然后通过管理接口iengine,就可以继续创建其他需要的对象。

2.2.2 AudioPlayer

音频播放对象,它需要指定输入(DataSource)和输出(DataSink)
Datasource 代表着输入源的信息,即数据从哪儿来、输入的数据参数是怎样的;
DataSink 代表着输出的信息,即数据输出到哪儿、以什么样的参数来输出。

DataSource 的定义如下:
 typedef struct SLDataSource_ {
      void *pLocator;
      void *pFormat; 
} SLDataSource;
DataSink 的定义如下:
 typedef struct SLDataSink_ {
     void *pLocator;
     void *pFormat; 
} SLDataSink;

其中,pLocator 主要有如下几种:

SLDataLocator_Address SLDataLocator_BufferQueue SLDataLocator_IODevice SLDataLocator_MIDIBufferQueue SLDataLocator_URI

也就是说,输入源/输出源,既可以是 URL,也可以 Device,或者来自于缓冲区队列等等。

那么我们在创建AudioPlayer对象前,还需要先创建输入和输出,输入我们使用一个缓冲队列,输出则使用outputMix输出到默认声音设备。

使用步骤:
1、输入源

	 //输入源为缓冲队列
    SLDataLocator_AndroidSimpleBufferQueue dsLocator = { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 1 };
    // 设置音频格式
    SLDataFormat_PCM outputFormat = { 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 audioSource = { &dsLocator , &outputFormat };

2、输出源
outputMix对象需要通过

CreateOutputMix
接口创建,并调用
Realize
初始化

	static SLObjectItf mix = NULL;
    //创建mix
    (*iengine)->CreateOutputMix(iengine, &mix, 0, NULL, NULL);
    //初始化
    re = (*mix)->Realize(mix, SL_BOOLEAN_FALSE);
    if(re != SL_RESULT_SUCCESS )
    {
        ALOGE("mix Realize error!");
        return false;
    }
    //输出源为mix
    SLDataLocator_OutputMix outmix = {SL_DATALOCATOR_OUTPUTMIX, mix};
    SLDataSink audioSink = {&outmix,NULL};

3、创建AudioPlayer并初始化
AudioPlayer对象需要通过

CreateAudioPlayer
接口创建,并调用
Realize
初始化

	static SLObjectItf player = NULL;
	const SLInterfaceID  outputInterfaces[1] = { SL_IID_BUFFERQUEUE };
	const SLboolean req[] = {SL_BOOLEAN_TRUE};
    //audio player
    (*iengine)->CreateAudioPlayer(iengine, &player, &audioSource, &audioSink, 1, outputInterfaces, req);
    re = (*player)->Realize(player, SL_BOOLEAN_FALSE);
    if(re != SL_RESULT_SUCCESS )
    {
        ALOGE("player Realize error!");
        return false;
    }

4、设置输入队列的回调函数,回调函数在数据不足时会内部调用。然后启动播放即可。
需要通过

SL_IID_ANDROIDSIMPLEBUFFERQUEUE
获取队列接口,并
RegisterCallback
设置回调函数。
需要通过
SL_IID_PLAY
获取播放接口,并设置为播放状态,同时调用
Enqueue
给队列压入一帧空数据。
需要注意的是,入队列的数据并非立刻播放,所以不能把这个数据立刻释放,否则会造成丢帧。

    //获取队列接口
	static SLPlayItf iplay = NULL;
	static SLAndroidSimpleBufferQueueItf pcmQue = NULL;
    (*player)->GetInterface(player, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &pcmQue);
    if(re != SL_RESULT_SUCCESS )
    {
        ALOGE("get pcmQue error!");
        return false;
    }
    //设置回调函数,播放队列空调用
    (*pcmQue)->RegisterCallback(pcmQue, PcmCall, this);
    //获取player接口
    re = (*player)->GetInterface(play, SL_IID_PLAY, &iplayer);
    if(re != SL_RESULT_SUCCESS )
    {
        ALOGE("get iplayer error!");
        return false;
    }
    //设置为播放状态
    (*iplay)->SetPlayState(iplay, SL_PLAYSTATE_PLAYING);
    //启动队列回调
    (*pcmQue)->Enqueue(pcmQue, "", 1);

当播放队列数据为空时,会调用队列的回调函数,所以需要在回调函数中给队列注入音频数据

static void PcmCall(SLAndroidSimpleBufferQueueItf bf, void *contex)
{
if(pcmQue && (*pcmQue))
    {
        if(aFrame == NULL)
        {
            (*pcmQue)->Enqueue(pcmQue, "", 1);
        }
        else
        {   
            //进队列后并不是直接播放,所以需要一个buf来存,不能释放掉
            memcpy(buf, aFrame->data, aFrame->size);
            (*pcmQue)->Enqueue(pcmQue, buf, aFrame->size);
        }
    }
}   

5、播放结束后,需要调用

Destroy
将各个对象释放。

opensl es参考自:https://www.jianshu.com/p/cccb59466e99


作者:wusc'blog


免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Android 播放音频(PCM)的两种方法--AudioTrack/OpenSL ES使用简介

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Android 播放音频(PCM)的两种方法--AudioTrack/OpenSL ES使用简介

本文主要介绍Android上可以进行音频(PCM)播放的两个组件–AudioTrack/OpenSL ES的简单使用方法。 对一个音频文件(如MP3文件),如何使用FFmpeg进行解码获取到PCM,之前的文章已经有相应的说明: https:
2022-06-06

编程热搜

  • Android:VolumeShaper
    VolumeShaper(支持版本改一下,minsdkversion:26,android8.0(api26)进一步学习对声音的编辑,可以让音频的声音有变化的播放 VolumeShaper.Configuration的三个参数 durati
    Android:VolumeShaper
  • Android崩溃异常捕获方法
    开发中最让人头疼的是应用突然爆炸,然后跳回到桌面。而且我们常常不知道这种状况会何时出现,在应用调试阶段还好,还可以通过调试工具的日志查看错误出现在哪里。但平时使用的时候给你闹崩溃,那你就欲哭无泪了。 那么今天主要讲一下如何去捕捉系统出现的U
    Android崩溃异常捕获方法
  • android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
    系统的设置–>电池–>使用情况中,统计的能耗的使用情况也是以power_profile.xml的value作为基础参数的1、我的手机中power_profile.xml的内容: HTC t328w代码如下:
    android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
  • Android SQLite数据库基本操作方法
    程序的最主要的功能在于对数据进行操作,通过对数据进行操作来实现某个功能。而数据库就是很重要的一个方面的,Android中内置了小巧轻便,功能却很强的一个数据库–SQLite数据库。那么就来看一下在Android程序中怎么去操作SQLite数
    Android SQLite数据库基本操作方法
  • ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
    工作的时候为了方便直接打开编辑文件,一些常用的软件或者文件我们会放在桌面,但是在ubuntu20.04下直接直接拖拽文件到桌面根本没有效果,在进入桌面后发现软件列表中的软件只能收藏到面板,无法复制到桌面使用,不知道为什么会这样,似乎并不是很
    ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
  • android获取当前手机号示例程序
    代码如下: public String getLocalNumber() { TelephonyManager tManager =
    android获取当前手机号示例程序
  • Android音视频开发(三)TextureView
    简介 TextureView与SurfaceView类似,可用于显示视频或OpenGL场景。 与SurfaceView的区别 SurfaceView不能使用变换和缩放等操作,不能叠加(Overlay)两个SurfaceView。 Textu
    Android音视频开发(三)TextureView
  • android获取屏幕高度和宽度的实现方法
    本文实例讲述了android获取屏幕高度和宽度的实现方法。分享给大家供大家参考。具体分析如下: 我们需要获取Android手机或Pad的屏幕的物理尺寸,以便于界面的设计或是其他功能的实现。下面就介绍讲一讲如何获取屏幕的物理尺寸 下面的代码即
    android获取屏幕高度和宽度的实现方法
  • Android自定义popupwindow实例代码
    先来看看效果图:一、布局
  • Android第一次实验
    一、实验原理 1.1实验目标 编程实现用户名与密码的存储与调用。 1.2实验要求 设计用户登录界面、登录成功界面、用户注册界面,用户注册时,将其用户名、密码保存到SharedPreference中,登录时输入用户名、密码,读取SharedP
    Android第一次实验

目录