前语
都说干事作业要有计划,每天准时完结作业计划剩余的时刻都在摸鱼,那可不行,所以指定了本月的构成方针,咱们能够一同监督
10.19-24音视频中高档52部+面试
10.25-26高档Android组件化强化实战(一二)
10.27-11.3高档Android组件化强化实战(大厂架构演化20章)
中间一切的周六周日都歇息
重视公众号:Android苦做舟
解锁 《Android十二大板块文档》
音视频大合集,从初中高到面试包罗万象;让学习更靠近未来实战。已构成PDF版
十二个模块内容如下:
1.2022最新Android11位大厂面试专题,128道附答案
2.音视频大合集,从初中高到面试包罗万象
3.Android车载运用大合集,从零开端一同学
4.性能优化大合集,离别优化烦恼
5.Framework大合集,从里到外分析的明明白白
6.Flutter大合集,进阶Flutter高档工程师
7.compose大合集,拥抱新技术
8.Jetpack大合集,全家桶一次吃个够
9.架构大合集,轻松应对作业需求
10.Android根底篇大合集,根基安定楼房平地起
11.Flutter番外篇:Flutter面试+项目实战+电子书
12.大厂高档Android组件化强化实战
收拾不易,重视一下吧。开端进入正题,ღ( ・ᴗ・` )
一丶openGL ES介绍
简介OpenGL ES
谈到OpenGL ES,首要咱们应该先去了解一下Android的根本架构,根本架构下图:
在这儿咱们能够找到Libraries里边有咱们现在要接触的库,即OpenGL ES。
根据上图能够知道Android 现在是支撑运用开放的图形库的,特别是经过OpenGL ES API来支撑高性能的2D和3D图形。OpenGL是一个跨渠道的图形API。为3D图形处理硬件指定了一个标准的软件接口。OpenGL ES 是适用于嵌入式设备的OpenGL规范。
根本介绍
Android 能够经过framework结构供给的API或许NDK来支撑OpenGL。本文要点介绍结构供给的接口来运用OpenGL的办法,有关于NDK方面的信息,能够自行去官方文档进行了解。
在Android结构里边两个根本的类答应你运用OpenGL ES API创立和操作图形: GLSurfaceView 和 GLSurfaceView.Renderer。假如您的方针是在Android程序中运用OpenGL,那么首要需求做的作业便是了解这两个类。
GLSurfaceView
这是一个视图类,你能够运用OpenGL API来制作和操作图形目标,这一点在功用上很相似于SurfaceView。你能够经过创立一个SurfaceView的实例并添加你的烘托器来运用这个类。可是假如想要捕捉接触屏的作业,则应该扩展GLSurfaceView以完结接触监听器。关于完结接触监听器的办法,咱们会在后边的文章中进行解说。
GLSurfaceView.Renderer
此接口界说了在GLSurfaceView中制作图形所需的办法。您有必要将此接口的完结作为独自的类供给,并运用GLSurfaceView.setRenderer()
将其附加到您的GLSurfaceView实例。 GLSurfaceView.Renderer要求完结以下办法:
-
onSurfaceCreated()
:创立GLSurfaceView时,体系调用一次该办法。运用此办法履行只需求履行一次的操作,例如设置OpenGL环境参数或初始化OpenGL图形目标。 -
onDrawFrame()
:体系在每次重画GLSurfaceView时调用这个办法。运用此办法作为制作(和重新制作)图形目标的首要履行办法。 -
onSurfaceChanged()
:当GLSurfaceView的发生改动时,体系调用此办法,这些改动包含GLSurfaceView的巨细或设备屏幕方向的改动。例如:设备从纵向变为横向时,体系调用此办法。咱们应该运用此办法来呼应GLSurfaceView容器的改动。
二丶OpenGL ES 环境搭建
环境搭建目的
为了在Android运用程序中运用OpenGL ES制作图形,有必要要为他们创立一个视图容器。其间最直接或许最常用的办法便是完结一个GLSurfaceView和一个GLSurfaceView.Renderer。GLSurfaceView是用OpenGL制作图形的视图容器,GLSurfaceView.Renderer操控在该视图内制作的内容。
下面将解说怎样运用GLSurfaceView 和 GLSurfaceView.Renderer 在一个简略的运用程序的Activity上面做一个最小的完结。
在Manifest中声明OpenGL ES运用
添加以下声明到manifest:
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
假如你的运用程序需求运用纹路紧缩,你还需求声明你的运用程序需求支撑哪种紧缩格局,以便他们安装在兼容的设备上。
<supports-gl-texture android:name="GL_OES_compressed_ETC1_RGB8_texture" />
<supports-gl-texture android:name="GL_OES_compressed_paletted_texture" />
创立一个Activity 用于展现OpenGL ES 图形
运用OpenGL ES的运用程序的Activity和其他运用程的Activity相同,不同的地方在于你设置的Activity的布局。在许多运用OpenGL ES的app中,你能够添加TextView,Button和ListView,还能够添加GLSurfaceView。
下面的代码展现了运用GLSurfaceView做为主视图的根本完结:
public class OpenGLES20Activity extends Activity {
private GLSurfaceView mGLView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Create a GLSurfaceView instance and set it
// as the ContentView for this Activity.
mGLView = new MyGLSurfaceView(this);
setContentView(mGLView);
}
}
留意:请保证你的Android项目针对的版别是否契合。
创立GLSurfaceView目标
GLSurfaceView是一个特殊的View,经过这个View你能够制作OpenGL图画。可是View本身没有做太多的作业,首要的制作是经过设置在View里边的GLSurfaceView.Renderer 来操控的。实践上,创立这个目标的代码是很少的,你能会想尝试越过extends的操作,只去创立一个没有被修正的GLSurfaceView实例,可是不主张这样去做。因为在某些情况下,你需求扩展这个类来捕获接触的作业,捕获接触的作业的办法会在后边的文章里边做介绍。 GLSurfaceView的根本代码很少,为了快速的完结,一般会在运用它的Activity中创立一个内部类来做完结:
class MyGLSurfaceView extends GLSurfaceView {
private final MyGLRenderer mRenderer;
public MyGLSurfaceView(Context context){
super(context);
// Create an OpenGL ES 2.0 context
setEGLContextClientVersion(2);
mRenderer = new MyGLRenderer();
// Set the Renderer for drawing on the GLSurfaceView
setRenderer(mRenderer);
}
}
你能够经过设置GLSurfaceView.RENDERMODE_WHEN_DIRTY
来让你的GLSurfaceView监听到数据改动的时分再去改写,即修正GLSurfaceView的烘托方式。这个设置能够避免重绘GLSurfaceView,直到你调用了requestRender()
,这个设置在默写层面上来说,对你的APP是更有好处的。
创立一个Renderer类
完结了GLSurfaceView.Renderer 类才是真正算是开端能够在运用中运用OpenGL ES。这个类操控着与它关联的GLSurfaceView 制作的内容。在renderer 里边有三个办法能够被Android体系调用,以便知道在GLSurfaceView制作什么以及怎样制作
-
onSurfaceCreated()
– 在View的OpenGL环境被创立的时分调用。 -
onDrawFrame()
– 每一次View的重绘都会调用 -
onSurfaceChanged()
– 假如视图的几许形状发生改动(例如,当设备的屏幕方向改动时),则调用此办法。
下面是运用OpenGL ES 烘托器的根本完结,仅仅做的作业便是在GLSurfaceView制作一个黑色背景。
public class MyGLRenderer implements GLSurfaceView.Renderer {
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
// Set the background frame color
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
}
public void onDrawFrame(GL10 unused) {
// Redraw background color
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
}
public void onSurfaceChanged(GL10 unused, int width, int height) {
GLES20.glViewport(0, 0, width, height);
}
}
总结
上述的内容便是根本的OpenGL ES根本的环境装备,本文的代码仅仅是创立一个简略的Android运用然后运用OpenGL展现一个黑板。尽管没有做其他更加有趣的作业,可是,经过创立这些类,你应该现已具有了运用OpenGL制作图形元素的根底了。
假如你了解OpenGL的API,现在你应该能够在你的APP里边创立一个OpenGL ES的环境,并开端进行画图了。可是假如需求更多的协助来运用OpenGL,就请期待下面的文章吧。
三丶OpenGL ES界说形状
在上篇文章,咱们能够装备好根本的Android OpenGL 运用的环境。可是假如咱们不了解OpenGL ES怎样界说图画的一些根本常识就运用OpenGL ES进行绘图仍是有点扎手的。所以能够在OpenGL ES的View里边界说要制作的形状是进行高端绘图操作的第一步。 本文首要做的作业便是为了解说Android设备屏幕相关的OpenGL ES坐标体系,界说形状,形状面的根底常识,以及界说三角形和正方形。
界说三角形
OpenGL ES答应你运用三维空间坐标系界说制作的图画,所以你在制作一个三角形之前有必要要先界说它的坐标。在OpenGL中,这样做的典型办法是为坐标界说浮点数的极点数组。 为了获得最大的功率,能够将这些坐标写入ByteBuffer,并传递到OpenGL ES图形管道进行处理。
public class Triangle {
private FloatBuffer vertexBuffer;
// number of coordinates per vertex in this array
static final int COORDS_PER_VERTEX = 3;
static float triangleCoords[] = { // in counterclockwise order:
0.0f, 0.622008459f, 0.0f, // top
-0.5f, -0.311004243f, 0.0f, // bottom left
0.5f, -0.311004243f, 0.0f // bottom right
};
// Set color with red, green, blue and alpha (opacity) values
float color[] = { 0.63671875f, 0.76953125f, 0.22265625f, 1.0f };
public Triangle() {
// initialize vertex byte buffer for shape coordinates
ByteBuffer bb = ByteBuffer.allocateDirect(
// (number of coordinate values * 4 bytes per float)
triangleCoords.length * 4);
// use the device hardware's native byte order
bb.order(ByteOrder.nativeOrder());
// create a floating point buffer from the ByteBuffer
vertexBuffer = bb.asFloatBuffer();
// add the coordinates to the FloatBuffer
vertexBuffer.put(triangleCoords);
// set the buffer to read the first coordinate
vertexBuffer.position(0);
}
}
默许情况下,OpenGL ES选用坐标系,[0,0,0](X,Y,Z)指定GLSurfaceView结构的中心,[1,1,0]是结构的右上角,[ – 1,-1,0]是结构的左下角。 有关此坐标系的阐明,请参阅OpenGL ES开发人员攻略。
请留意,此图形的坐标以逆时针次序界说。 绘图次序十分重要,因为它界说了哪一面是您一般想要制作的图形的正面,以及背面。关于这块相关的更多的内容,能够去检查一下相关的OpenGL ES 文档。
界说正方形
能够看到,在OpenGL里边界说一个三角形很简略。可是假如你想要得到一个更杂乱一点的东西呢?比方一个正方形?能够找到许多办法来作到这一点,可是在OpenGL里边制作这个图形的办法是将两个三角形画在一同。
相同,你应该以逆时针的次序为这两个代表这个形状的三角形界说极点,并将这些值放在一个ByteBuffer中。 为避免界说每个三角形共享的两个坐标两次,请运用图纸列表告知OpenGL ES图形管道怎样制作这些极点。 这是这个形状的代码:
public class Square {
private FloatBuffer vertexBuffer;
private ShortBuffer drawListBuffer;
// number of coordinates per vertex in this array
static final int COORDS_PER_VERTEX = 3;
static float squareCoords[] = {
-0.5f, 0.5f, 0.0f, // top left
-0.5f, -0.5f, 0.0f, // bottom left
0.5f, -0.5f, 0.0f, // bottom right
0.5f, 0.5f, 0.0f }; // top right
private short drawOrder[] = { 0, 1, 2, 0, 2, 3 }; // order to draw vertices
public Square() {
// initialize vertex byte buffer for shape coordinates
ByteBuffer bb = ByteBuffer.allocateDirect(
// (### of coordinate values * 4 bytes per float)
squareCoords.length * 4);
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(squareCoords);
vertexBuffer.position(0);
// initialize byte buffer for the draw list
ByteBuffer dlb = ByteBuffer.allocateDirect(
// (### of coordinate values * 2 bytes per short)
drawOrder.length * 2);
dlb.order(ByteOrder.nativeOrder());
drawListBuffer = dlb.asShortBuffer();
drawListBuffer.put(drawOrder);
drawListBuffer.position(0);
}
}
这个比方让你了解用OpenGL创立更杂乱的形状的进程。 一般来说,您运用三角形的集合来制作目标。下面的文章里边,将叙述怎样在屏幕上制作这些形状。
四丶OpenGL ES制作形状
在上文中,咱们运用OpenGL界说了能够被制作出来的形状了,现在咱们想制作出来它们。
初始化形状
在你做任何制作操作之前,你有必要要初始化并加载你准备制作的形状。除非形状的结构(指原始的坐标)在履行进程中发生改动,你都应该在你的Renderer的办法onSurfaceCreated()
中进行内存和功率方面的初始化作业。
public class MyGLRenderer implements GLSurfaceView.Renderer {
...
private Triangle mTriangle;
private Square mSquare;
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
...
// initialize a triangle
mTriangle = new Triangle();
// initialize a square
mSquare = new Square();
}
...
}
制作形状
画一个界说好的形状需求比较多的代码,因为你有必要为图形烘托管线供给一大堆信息。特别的,你有必要界说以下几个东西:
- Vertex Shader – 用于烘托形状的极点的OpenGLES 图形代码。
- Fragment Shader – 用于烘托形状的外观(色彩或纹路)的OpenGLES 代码。
- Program – 一个OpenGLES目标,包含了你想要用来制作一个或多个形状的shader。
你至少需求一个vertexshader来制作一个形状和一个fragmentshader来为形状上色。这些形状有必要被编译然后被添加到一个OpenGLES program中,program之后被用来制作形状。下面是一个展现怎样界说一个能够用来制作形状的根本shader的比方:
public class Triangle {
private final String vertexShaderCode =
"attribute vec4 vPosition;" +
"void main() {" +
" gl_Position = vPosition;" +
"}";
private final String fragmentShaderCode =
"precision mediump float;" +
"uniform vec4 vColor;" +
"void main() {" +
" gl_FragColor = vColor;" +
"}";
...
}
Shader们包含了OpenGLShading Language (GLSL)代码,有必要在运用前编译。要编译这些代码,在你的Renderer类中创立一个工具类办法:
public static int loadShader(int type, String shaderCode){
// create a vertex shader type (GLES20.GL_VERTEX_SHADER)
// or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
int shader = GLES20.glCreateShader(type);
// add the source code to the shader and compile it
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}
为了制作你的形状,你有必要编译shader代码,添加它们到一个OpenGLES program 目标然后链接这个program。在renderer目标的结构器中做这些作业,然后只需做一次即可。
注:编译OpenGLES shader们和链接linkingprogram们是很耗CPU的,所以你应该避免屡次做这些事。假如在运转时你不知道shader的内容,你应该只创立一次code然后缓存它们以避免屡次创立。
public class Triangle() {
...
private final int mProgram;
public Triangle() {
...
int vertexShader = MyGLRenderer.loadShader(GLES20.GL_VERTEX_SHADER,
vertexShaderCode);
int fragmentShader = MyGLRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER,
fragmentShaderCode);
// create empty OpenGL ES Program
mProgram = GLES20.glCreateProgram();
// add the vertex shader to program
GLES20.glAttachShader(mProgram, vertexShader);
// add the fragment shader to program
GLES20.glAttachShader(mProgram, fragmentShader);
// creates OpenGL ES program executables
GLES20.glLinkProgram(mProgram);
}
}
此刻,你现已准备好添加真正的制作调用了。需求为烘托管线指定许多参数来告知它你想画什么以及怎样画。因为制作操作因形状而异,让你的形状类包含自己的制作逻辑是个很好主意。
创立一个draw()
办法负责制作形状。下面的代码设置方位和色彩值到形状的vertexshader和fragmentshader,然后履行制作功用:
private int mPositionHandle;
private int mColorHandle;
private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;
private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex
public void draw() {
// Add program to OpenGL ES environment
GLES20.glUseProgram(mProgram);
// get handle to vertex shader's vPosition member
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
// Enable a handle to the triangle vertices
GLES20.glEnableVertexAttribArray(mPositionHandle);
// Prepare the triangle coordinate data
GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false,
vertexStride, vertexBuffer);
// get handle to fragment shader's vColor member
mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
// Set color for drawing the triangle
GLES20.glUniform4fv(mColorHandle, 1, color, 0);
// Draw the triangle
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
// Disable vertex array
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
一旦完结了一切这些代码,制作该目标只需求在烘托器的onDrawFrame()
办法中调用draw()
办法:
public void onDrawFrame(GL10 unused) {
...
mTriangle.draw();
}
当你运转程序的时分,你就应该看到以下的内容: When you run the application, it should look something like this:
此比方中的代码还有许多问题。首要,它不会打动你和你的朋友。其次,三角形会在你从竖屏变为横屏时被压扁。三角形变形的原因是其极点们没有跟据屏幕的宽高比进行批改。并且这儿展现出来的三角形是静止的,这样的图形是有点无聊的,在“添加动画”的文章中,咱们会运用OpenGL ES 的视图管线来旋转此形状。
五丶OpenGL ES运用投影和相机视图
OpenGL ES环境答应你以更接近于你眼睛看到的物理目标的办法来显现你制作的目标。物理检查的模仿是经过对你所制作的目标的坐标进行数学改换完结的:
-
Projection — 这个改换是根据他们所显现的GLSurfaceView的宽和高来调整制作目标的坐标的。没有这个核算改换,经过OpenGL制作的形状会在不同显现窗口变形。这个投影改动一般只会在OpenGL view的份额被确认或许在你烘托器的
onSurfaceChanged()
办法中被核算。想要了解更多的关于投影和坐标映射的相关信息,请看制作目标的坐标映射。 -
Camera View — 这个换是根据虚拟的相机的方位来调整制作目标坐标的。需求侧重留意的是,OpenGL ES并没有界说一个实在的相机目标,而是供给一个有用办法,经过改换制作目标的显现来模仿一个相机。相机视图改换或许只会在你的GLSurfaceView被确守时被核算,或许根据用户操作或你运用程序的功用来动态改动。
本课程描述怎样创立投影和相机视图并将其运用的到你的GLSurfaceView的制作目标上。
界说投影
投影改动的数据是在你GLSurfaceView.Renderer类的onSurfaceChanged()
办法中被核算的。下面的示例代码是获取GLSurfaceView的高和宽,并经过Matrix.frustumM()
办法用它们填充到投影改换矩阵中。
// mMVPMatrix is an abbreviation for "Model View Projection Matrix"
private final float[] mMVPMatrix = new float[16];
private final float[] mProjectionMatrix = new float[16];
private final float[] mViewMatrix = new float[16];
@Override
public void onSurfaceChanged(GL10 unused, int width, int height) {
GLES20.glViewport(0, 0, width, height);
float ratio = (float) width / height;
// this projection matrix is applied to object coordinates
// in the onDrawFrame() method
Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
}
上面的代码填充有一个投影矩阵mProjectionMatrix,mProjectionMatrix能够在onFrameDraw()办法中与下一部分的相机视图结合在一同。
留意:假如仅仅只把投影矩阵运用的到你制作的目标中,一般你只会得到一个十分空的显现。一般情况下,你还有必要为你要在屏幕上显现的任何内容运用相机视图。
界说相机视图
经过在你的烘托器中添加相机视图改换作为你制作进程的一部分来完结你的制作图画的改换进程。鄙人面的代码中,经过Matrix.setLookAtM()
办法核算相机视图改换,然后将其与之前核算出的投影矩阵结合到一同。兼并后的矩阵接下来会传递给制作的图形。
@Override
public void onDrawFrame(GL10 unused) {
...
// Set the camera position (View matrix)
Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
// Calculate the projection and view transformation
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
// Draw shape
mTriangle.draw(mMVPMatrix);
}
运用投影和相机改换
为了运用在上一部分内容中展现的投影和相机视图改换的兼并矩阵,首要要在之前Triangle类中界说的定点上色器代码中添加一个矩阵变量:
public class Triangle {
private final String vertexShaderCode =
// This matrix member variable provides a hook to manipulate
// the coordinates of the objects that use this vertex shader
"uniform mat4 uMVPMatrix;" +
"attribute vec4 vPosition;" +
"void main() {" +
// the matrix must be included as a modifier of gl_Position
// Note that the uMVPMatrix factor *must be first* in order
// for the matrix multiplication product to be correct.
" gl_Position = uMVPMatrix * vPosition;" +
"}";
// Use to access and set the view transformation
private int mMVPMatrixHandle;
...
}
下一步,修正你的图形目标的draw()
办法来接纳联合改换矩阵,并将它们运用到图形中:
public void draw(float[] mvpMatrix) { // pass in the calculated transformation matrix
...
// get handle to shape's transformation matrix
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
// Pass the projection and view transformation to the shader
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
// Draw the triangle
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
// Disable vertex array
GLES20.glDisableVertexAttribArray(mPositionHandle);
}
一旦你正确的核算并运用投影和相机视图改换,你的绘图目标将会以正确的份额制作,它看起来应该像下面这样:
现在你现已有一个能够以正确份额显现图形的运用了。后边的章节,咱们能够了解怎样为你的图形添加运动了。
六丶OpenGL ES添加运动作用
在屏幕上制作图形仅仅OpenGL的适当根底的特点,你也能够用其他的Android图形结构类来完结这些,包含Canvas和Drawable目标。OpenGL ES为在三维空间中移动和改换供给了额定的功用,并供给了创立有目共睹的用户体会的独特办法。 在本文中,你将进一步运用OpenGL ES学习怎样为你的图形添加一个旋转动作。
旋转一个图形
用来旋转一个制作目标是相对简略的。在你的烘托器中,添加一个新的改换矩阵(旋转矩阵),然后把它与你的投影与相机视图改换矩阵兼并到一同:
private float[] mRotationMatrix = new float[16];
public void onDrawFrame(GL10 gl) {
float[] scratch = new float[16];
...
// Create a rotation transformation for the triangle
long time = SystemClock.uptimeMillis() % 4000L;
float angle = 0.090f * ((int) time);
Matrix.setRotateM(mRotationMatrix, 0, angle, 0, 0, -1.0f);
// Combine the rotation matrix with the projection and camera view
// Note that the mMVPMatrix factor *must be first* in order
// for the matrix multiplication product to be correct.
Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);
// Draw triangle
mTriangle.draw(scratch);
}
假如做了这些改动后你的三角形还没有旋转,请保证你是否注释掉了GLSurfaceView.RENDERMODE_WHEN_DIRTY
设置项,这将鄙人一部分讲到。
答应接连烘托
假如你勤恳地遵从本系列课程的示例代码到这个点,请保证你注释了设置只要当dirty的时分才烘托的烘托方式这一行,不然OpenGL旋转图形,只会递加视点然后等候来自GLSurfaceView容器的对requestRender()
办法的调用:
public MyGLSurfaceView(Context context) {
...
// Render the view only when there is a change in the drawing data.
// To allow the triangle to rotate automatically, this line is commented out:
//setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}
除非你的目标改动没有用户交互,不然一般翻开这个标志是个好主意。准备好取消注释这行代码,因为下一节内容将使这个调用再次适用。
七丶OpenGL ES 呼应接触作业
像旋转三角形相同,经过预设程序来让目标移动关于吸引留意是很有用的,可是假如你想让你的OpenGL图形有用户交互呢?让你的OpenGL ES运用有接触交互的关键是,扩展你的GLSurfaceView的完结重载onTouchEvent()
办法来监听接触作业。 本节内容将向你展现怎样监听接触作业来让用户旋转一个图形。
设置接触作业
为了你的OpenGL ES运用能够呼应接触作业,你有必要在你的GLSurfaceView中完结onTouchEvent()
办法,下面的完结比方展现了怎样监听MotionEvent.ACTION_MOVE
作业,并将该作业转化成图形的旋转视点。
private final float TOUCH_SCALE_FACTOR = 180.0f / 320;
private float mPreviousX;
private float mPreviousY;
@Override
public boolean onTouchEvent(MotionEvent e) {
// MotionEvent reports input details from the touch screen
// and other input controls. In this case, you are only
// interested in events where the touch position changed.
float x = e.getX();
float y = e.getY();
switch (e.getAction()) {
case MotionEvent.ACTION_MOVE:
float dx = x - mPreviousX;
float dy = y - mPreviousY;
// reverse direction of rotation above the mid-line
if (y > getHeight() / 2) {
dx = dx * -1 ;
}
// reverse direction of rotation to left of the mid-line
if (x < getWidth() / 2) {
dy = dy * -1 ;
}
mRenderer.setAngle(
mRenderer.getAngle() +
((dx + dy) * TOUCH_SCALE_FACTOR));
requestRender();
}
mPreviousX = x;
mPreviousY = y;
return true;
}
需求留意的是,核算完旋转视点后,需求调用requestRender()
办法来告知烘托器是时分烘托帧画面了。在本比方中这种办法是最高效的,因为除非旋转有改动,不然帧画面不需求重绘。然而除非你还用setRenderMode()
办法要求烘托器只要在数据改动时才进行重绘,不然这对性能没有任何影响。因而,保证烘托器中的下面这行是取消注释的:
public MyGLSurfaceView(Context context) {
...
// Render the view only when there is a change in the drawing data
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}
露出旋转视点
上面的例程代码中需求你经过在烘托器中添加共有的成员来露出旋转视点。当烘托代码是在独立于你运用程序的主用户界面线程的独自线程履行的时分,你有必要声明这个共有变量是volatile类型的。下面的代码声明了这个变量并且露出了它的getter和setter办法对:
public class MyGLRenderer implements GLSurfaceView.Renderer {
...
public volatile float mAngle;
public float getAngle() {
return mAngle;
}
public void setAngle(float angle) {
mAngle = angle;
}
}
运用旋转
为了运用接触输入发生的旋转,先注释掉发生视点的代码,并添加一个右接触作业发生的视点mAngle:
public void onDrawFrame(GL10 gl) {
...
float[] scratch = new float[16];
// Create a rotation for the triangle
// long time = SystemClock.uptimeMillis() % 4000L;
// float angle = 0.090f * ((int) time);
Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, -1.0f);
// Combine the rotation matrix with the projection and camera view
// Note that the mMVPMatrix factor *must be first* in order
// for the matrix multiplication product to be correct.
Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);
// Draw triangle
mTriangle.draw(scratch);
}
当你完结上面介绍的进程,运转你的程序,然后在屏幕上拖拽你的手指来旋转这个三角形。
八丶OpenGL ES 上色器言语GLSL
前面的文章首要是收拾的Android 官方文档对OpenGL ES支撑的介绍。经过之前的文章,咱们根本上能够完结的根本的形状的制作。
现在到这儿第一阶段的学习,也便是根本的图形制作,根本的交互的完结。
- 平面制作:三角形、正方形、在相机视角下的三角形、五颜六色三角形
- 立体制作:正方体、圆柱体、圆锥体、球体
- 根本交互:手绘点、旋转三角形
知道了根本的图形制作,也知道了根本的交互的完结,现在或许大多数人仍是对整个完结的流程有点懵,最首要的地方或许便是对极点上色器和片元上色器了。前面的运用进程中,咱们大约也对上色器言语有必定的了解了,可是在前面咱们运用的上色器代码仍是很简略的,做的作业也是很有限的,后边的开发进程中,咱们用到的上色器会越来越杂乱,So,这儿咱们想一下上色器言语GLSL。
咱们知道,在OpenGL ES中上色器分为极点上色器和片元上色器。极点上色器是针对每个极点履行一次,用于确认极点的方位。片元上色器是针对每个片元,片元咱们能够了解为每个像素,用于确认每个片元(像素)的色彩。
GLSL 简介
GLSL又叫OpenGL上色言语(OpenGL Shading Language),是用来在OpenGL中上色编程的言语,是一种面向进程的言语,根本的语法和C/C++根本相同,他们是在图形卡的GPU (Graphic Processor Unit图形处理单元)上履行的,代替了固定的烘托管线的一部分,使烘托管线中不同层次具有可编程性。比方:视图转化、投影转化等。GLSL(GL Shading Language)的上色器代码分红2个部分:Vertex Shader(极点上色器)和Fragment(片断上色器)。
在前面的学习中,咱们根本上运用的都是十分简略的上色器,根本上没有运用过GLSL的内置函数,可是在后边咱们完结其他的功用的时分应该就会用到这些内置函数了。
GLSL 根底
GLSL 尽管很相似于C/C++,可是它和C/C++仍是有很大的不同的,比方,没有double,long等类型,没有union、enum、unsigned以及位运算等特性。
根本数据类型
GLSL中的数据类型首要分为标量、向量、矩阵、采样器、结构体、数组、空类型七种类型:
标量:
标量表明的是只要巨细没有方向的量,在GLSL中标量只要bool、int和float三种。关于int,和C相同,能够写为十进制(16)、八进制(020)或许十六进制(0x10)。关于标量的运算,咱们最需求留意的是精度,避免溢出问题。
向量:
向量咱们能够看做是数组,在GLSL一般用于贮存色彩、坐标等数据,针对维数,可分为二维、三维和四位向量。针对存储的标量类型,能够分为bool、int和float。共有vec2、vec3、vec4,ivec2、ivec3、ivec4、bvec2、bvec3和bvec4九种类型,数组代表维数、i表明int类型、b表明bool类型。需求留意的是,GLSL中的向量表明竖向量,所以与矩阵相乘进行改换时,矩阵在前,向量在后(与DirectX正好相反)。向量在GPU中由硬件支撑运算,比CPU快的多。
- 作为色彩向量时,用rgba表明重量,就好像取数组的中详细数据的索引值。三维色彩向量就用rgb表明重量。比方关于色彩向量vec4 color,color[0]和color.r都表明color向量的第一个值,也便是红色的重量。其他相同。
- 作为方位向量时,用xyzw表明重量,xyz分别表明xyz坐标,w表明向量的模。三维坐标向量为xyz表明重量,二维向量为xy表明重量。
- 作为纹路向量时,用stpq表明重量,三维用stp表明重量,二维用st表明重量。
矩阵:
在GLSL中矩阵具有22、33、4*4三种类型的矩阵,分别用mat2、mat3、mat4表明。咱们能够把矩阵看做是一个二维数组,也能够用二维数组下表的办法取里边详细方位的值。
采样器:
采样器是专门用来对纹路进行采样作业的,在GLSL中一般来说,一个采样器变量表明一副或许一套纹路贴图。所谓的纹路贴图能够了解为咱们看到的物体上的皮肤。
结构体:
和C言语中的结构体相同,用struct来界说结构体,关于结构体参考C言语中的结构体。
数组:
数组常识也和C中相同,不同的是数组声明时能够不指定巨细,可是主张在不必要的情况下,仍是指定巨细的好。
空类型:
空类型用void表明,仅用来声明不回来任何值得函数。
数据声明示例:
float a=1.0;
int b=1;
bool c=true;
vec2 d=vec2(1.0,2.0);
vec3 e=vec3(1.0,2.0,3.0)
vec4 f=vec4(vec3,1.2);
vec4 g=vec4(0.2); //适当于vec(0.2,0.2,0.2,0.2)
vec4 h=vec4(a,a,1.3,a);
mat2 i=mat2(0.1,0.5,1.2,2.4);
mat2 j=mat2(0.8); //适当于mat2(0.8,0.8,0.8,0.8)
mat3 k=mat3(e,e,1.2,1.6,1.8);
运算符
GLSL中的运算符有(越靠前,运算优先级越高):
- 索引:[]
- 前缀自加和自减:++,–
- 一元非和逻辑非:~,!
- 加法和减法:+,-
- 等于和不等于:==,!=
- 逻辑异或:^^
- 三元运算符号,挑选:?:
- 成员挑选与混合:.
- 后缀自加和自减:++,–
- 乘法和除法:*,/
- 关系运算符:>,<,=,>=,<=,<>
- 逻辑与:&&
- 逻辑或:||
- 赋值预算:=,+=,-=,*=,/=
类型转化
GLSL的类型转化与C不同。在GLSL中类型不能够自动提高,比方float a=1;便是一种过错的写法,有必要严格的写成float a=1.0,也不能够强制转化,即float a=(float)1;也是过错的写法,可是能够用内置函数来进行转化,如float a=float(1);还有float a=float(true);(true为1.0,false为0.0)等,值得留意的是,低精度的int不能转化为低精度的float。
限制符
在之前的博客中也提到了,GLSL中的限制符号首要有:
- attritude:一般用于各个极点各不相同的量。如极点色彩、坐标等。
- uniform:一般用于关于3D物体中一切极点都相同的量。比方光源方位,共同改换矩阵等。
- varying:表明易变量,一般用于极点上色器传递到片元上色器的量。
- const:常量。 限制符与java限制符相似,放在变量类型之前,并且只能用于全局变量。在GLSL中,没有默许限制符一说。
流程操控
GLSL中的流程操控与C中根本相同,首要有:
- if(){}、if(){}else{}、if(){}else if(){}else{}
- while(){}和do{}while()
- for(;{}
- break和continue
函数
GLSL中也能够界说函数,界说函数的办法也与C言语根本相同。函数的回来值能够是GLSL中的除了采样器的任意类型。关于GLSL中函数的参数,能够用参数用处修饰符来进行修饰,常用修饰符如下:
- in:输入参数,无修饰符时默许为此修饰符。
- out:输出参数。
- inout:既能够作为输入参数,又能够作为输出参数。
浮点精度
与极点上色器不同的是,在片元上色器中运用浮点型时,有必要指定浮点类型的精度,不然编译会报错。精度有三种,分别为:
- lowp:低精度。8位。
- mediump:中精度。10位。
- highp:高精度。16位。
不仅仅是float能够拟定精度,其他(除了bool相关)类型也相同能够,可是int、采样器类型并不必定要求指定精度。加精度的界说如下:
uniform lowp float a=1.0;
varying mediump vec4 c;
当然,也能够在片元上色器中设置默许精度,只需求在片元上色器最上面加上precision <精度> <类型>即可拟定某种类型的默许精度。其他情况相同的话,精度越高,画质越好,运用的资源也越多。
程序结构
前面几篇博客都有运用到上色器,咱们对上色器的程序结构也应该有必定的了解。或许一直沉浸在Android运用开发,没有了解C开发的朋友,对这种结构并不了解。GLSL程序的结构和C言语差不多,main()办法表明进口函数,能够在其上界说函数和变量,在main中能够引用这些变量和函数。界说在函数体以外的叫做全局变量,界说在函数体内的叫做局部变量。与高档言语不通的是,变量和函数在运用前有必要声明,不能再运用的后边声明变量或许函数。
GLSL 内建变量
在上色器中咱们一般都会声明变量来在程序中运用,可是上色器中还有一些特殊的变量,不声明也能够运用。这些变量叫做内建变量。內建变量,适当于上色器硬件的输入和输出点,运用者运用这些输入点输入之后,就会看到屏幕上的输出。经过输出点能够知道输出的某些数据内容。当然,实践上肯定不会这样简略,这么说仅仅为了协助了解。在极点上色器中的内建变量和片元上色器的内建变量是不相同的。上色器中的内建变量有许多,在此,咱们只列出最常用的集中内建变量。
极点上色器的内建变量
输入变量:
- gl_Position:极点坐标
- gl_PointSize:点的巨细,没有赋值则为默许值1,一般设置绘图为点制作才有意义。\
片元上色器的内建变量
输入变量:
- gl_FragCoord:当时片元相对窗口方位所处的坐标。
- gl_FragFacing:bool型,表明是否为归于光栅化生成此片元的对应图元的正面。 输出变量:
- gl_FragColor:当时片元色彩
- gl_FragData:vec4类型的数组。向其写入的信息,供烘托管线的后继进程运用。
常用内置函数
常见函数
- radians(x):视点转弧度
- degrees(x):弧度转视点
- sin(x):正弦函数,传入值为弧度。相同的还有cos余弦函数、tan正切函数、asin横竖弦、acos反余弦、atan横竖切
- pow(x,y):xy
- exp(x):ex
- exp2(x):2x
- log(x):logex
- log2(x):log2x
- sqrt(x):x√
- inversesqr(x):1x√
- abs(x):取x的肯定值
- sign(x):x>0回来1.0,x<0回来-1.0,不然回来0.0
- ceil(x):回来大于或许等于x的整数
- floor(x):回来小于或许等于x的整数
- fract(x):回来x-floor(x)的值
- mod(x,y):取模(求余)
- min(x,y):获取xy中小的那个
- max(x,y):获取xy中大的那个
- mix(x,y,a):回来x∗(1−a)+y∗a
- step(x,a):x< a回来0.0,不然回来1.0
- smoothstep(x,y,a):a < x回来0.0,a>y回来1.0,不然回来0.0-1.0之间平滑的Hermite插值。
- dFdx(p):p在x方向上的偏导数
- dFdy(p):p在y方向上的偏导数
- fwidth(p):p在x和y方向上的偏导数的肯定值之和
几许函数
- length(x):核算向量x的长度
- distance(x,y):回来向量xy之间的间隔
- dot(x,y):回来向量xy的点积
- cross(x,y):回来向量xy的差积
- normalize(x):回来与x向量方向相同,长度为1的向量
矩阵函数
- matrixCompMult(x,y):将矩阵相乘
- lessThan(x,y):回来向量xy的各个重量履行x< y的效果,相似的有greaterThan,equal,notEqual
- lessThanEqual(x,y):回来向量xy的各个重量履行x<= y的效果,相似的有相似的有greaterThanEqual
- any(bvec x):x有一个元素为true,则为true
- all(bvec x):x一切元素为true,则回来true,不然回来false
- not(bvec x):x一切重量履行逻辑非运算
纹路采样函数
纹路采样函数有texture2D、texture2DProj、texture2DLod、texture2DProjLod、textureCube、textureCubeLod及texture3D、texture3DProj、texture3DLod、texture3DProjLod等。
- texture表明纹路采样,2D表明对2D纹路采样,3D表明对3D纹路采样
- Lod后缀,只适用于极点上色器采样
- Proj表明纹路坐标st会除以q
纹路采样函数中,3D在OpenGLES2.0并不是肯定支撑。咱们再次暂时不管3D纹路采样函数。要点只对texture2D函数进行阐明。texture2D具有三个参数,第一个参数表明纹路采样器。第二个参数表明纹路坐标,能够是二维、三维、或许四维。第三个参数参加后只能在片元上色器中调用,且只对采样器为mipmap类型纹路时有用。
九丶OpenGL ES纹路贴图
概念
一般说来,纹路是表明物体外表的一幅或几幅二维图形,也称纹路贴图(texture)。当把纹路依照特定的办法映射到物体外表上的时分,能使物体看上去更加实在。当时流行的图形体系中,纹路制作现已成为一种必不可少的烘托办法。在了解纹路映射时,能够将纹路看做运用在物体外表的像素色彩。在实在国际中,纹路表明一个目标的色彩、图画以及触觉特征。纹路只表明目标外表的五颜六色图画,它不能改动目标的几许方式。更进一步的说,它仅仅一种高强度的核算行为。 归纳为一句便是:纹路贴图便是把一个纹路(关于2D贴图,能够简略的了解为图片),依照所希望的办法显现在许多三角形组成的物体的外表。
原理
首要介绍一下纹路映射时的坐标系,纹路映射的坐标系和极点上色器的坐标系是不相同的。 纹路坐标用浮点数来表明,范围一般从0.0到1.0,左上角坐标为(0.0,0.0),右上角坐标为(1.0,0.0),左下角坐标为(0.0,1.0),右下角坐标为(1.0,1.0),详细如下:
极点上色器的坐标系如下:
将纹路映射到右边的两个三角形上(也便是一个矩形),需求将纹路坐标指定到正确的极点上,才干使纹路正确的显现,不然显现出来的纹路会无法显现,或许出现旋转、翻转、错位等情况。 将右图极点依照V2V1V4V3传入,以三角形条带办法制作,则纹路坐标应依照V2V1V4V3传入。假如依照V3V4V1V2传入,会得到一个旋转了180度的纹路。假如依照V4V3V2V1传入,则会得到一个左右翻转的纹路。
显现纹路图片
咱们能够根据以下进程运用OpenGL ES显现一张图片:
修正上色器
首要,咱们需求修正咱们的上色器,将极点上色器修正为:
attribute vec4 vPosition;
attribute vec2 vCoordinate;
uniform mat4 vMatrix;
varying vec2 aCoordinate;
void main(){
gl_Position=vMatrix*vPosition;
aCoordinate=vCoordinate;
}
能够看到,极点上色器中添加了一个vec2变量,并将这个变量传递给了片元上色器,这个变量便是纹路坐标。接着咱们修正片元上色器为:
precision mediump float;
uniform sampler2D vTexture;
varying vec2 aCoordinate;
void main(){
gl_FragColor=texture2D(vTexture,aCoordinate);
}
片元上色器中,添加了一个sampler2D的变量,sampler2D咱们在前一篇博客GLSL言语根底中提到过,是GLSL的变量类型之一的取样器。texture2D也有提到,它是GLSL的内置函数,用于2D纹路取样,根据纹路取样器和纹路坐标,能够得到当时纹路取样得到的像素色彩。
设置极点坐标和纹路坐标
根据纹路映射原理中的介绍,咱们将极点坐标设置为:
private final float[] sPos={
-1.0f,1.0f, //左上角
-1.0f,-1.0f, //左下角
1.0f,1.0f, //右上角
1.0f,-1.0f //右下角
};
相应的,对照极点坐标,咱们能够设置纹路坐标为:
private final float[] sCoord={
0.0f,0.0f,
0.0f,1.0f,
1.0f,0.0f,
1.0f,1.0f,
};
核算改换矩阵
依照上步设置极点坐标和纹路坐标,大多数情况下咱们得到的必定是一张拉升或许紧缩的图片。为了让图片完好的显现,且不被拉伸和紧缩,咱们需求向制作等腰直角三角形相同,核算一个适宜的改换矩阵,传入极点上色器,代码如下:
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES20.glViewport(0,0,width,height);
int w=mBitmap.getWidth();
int h=mBitmap.getHeight();
float sWH=w/(float)h;
float sWidthHeight=width/(float)height;
if(width>height){
if(sWH>sWidthHeight){
Matrix.orthoM(mProjectMatrix, 0, -sWidthHeight*sWH,sWidthHeight*sWH, -1,1, 3, 7);
}else{
Matrix.orthoM(mProjectMatrix, 0, -sWidthHeight/sWH,sWidthHeight/sWH, -1,1, 3, 7);
}
}else{
if(sWH>sWidthHeight){
Matrix.orthoM(mProjectMatrix, 0, -1, 1, -1/sWidthHeight*sWH, 1/sWidthHeight*sWH,3, 7);
}else{
Matrix.orthoM(mProjectMatrix, 0, -1, 1, -sWH/sWidthHeight, sWH/sWidthHeight,3, 7);
}
}
//设置相机方位
Matrix.setLookAtM(mViewMatrix, 0, 0, 0, 7.0f, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
//核算改换矩阵
Matrix.multiplyMM(mMVPMatrix,0,mProjectMatrix,0,mViewMatrix,0);
}
mMVPMatrix即为咱们所需求的改换矩阵。
显现图片
然后咱们需求做的,就和之前制作正方形相同容易了。和之前不同的是,在制作之前,咱们还需求将纹路和纹路坐标传入上色器:
@Override
public void onDrawFrame(GL10 gl) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT|GLES20.GL_DEPTH_BUFFER_BIT);
GLES20.glUseProgram(mProgram);
onDrawSet();
GLES20.glUniformMatrix4fv(glHMatrix,1,false,mMVPMatrix,0);
GLES20.glEnableVertexAttribArray(glHPosition);
GLES20.glEnableVertexAttribArray(glHCoordinate);
GLES20.glUniform1i(glHTexture, 0);
textureId=createTexture();
//传入极点坐标
GLES20.glVertexAttribPointer(glHPosition,2,GLES20.GL_FLOAT,false,0,bPos);
//传入纹路坐标
GLES20.glVertexAttribPointer(glHCoordinate,2,GLES20.GL_FLOAT,false,0,bCoord);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP,0,4);
}
public abstract void onDrawSet();
public abstract void onDrawCreatedSet(int mProgram);
private int createTexture(){
int[] texture=new int[1];
if(mBitmap!=null&&!mBitmap.isRecycled()){
//生成纹路
GLES20.glGenTextures(1,texture,0);
//生成纹路
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,texture[0]);
//设置缩小过滤为运用纹路中坐标最接近的一个像素的色彩作为需求制作的像素色彩
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,GLES20.GL_NEAREST);
//设置扩大过滤为运用纹路中坐标最接近的若干个色彩,经过加权均匀算法得到需求制作的像素色彩
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MAG_FILTER,GLES20.GL_LINEAR);
//设置盘绕方向S,截取纹路坐标到[1/2n,1-1/2n]。将导致永久不会与border交融
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,GLES20.GL_CLAMP_TO_EDGE);
//设置盘绕方向T,截取纹路坐标到[1/2n,1-1/2n]。将导致永久不会与border交融
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,GLES20.GL_CLAMP_TO_EDGE);
//根据以上指定的参数,生成一个2D纹路
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, mBitmap, 0);
return texture[0];
}
return 0;
}
这样咱们就能够显现出咱们需求显现的图片,并且保证它完好的居中显现并且不会变形了。
十丶经过GLES20与上色器交互
获取上色器程序内成员变量的id(句柄、指针)
GLES20.glGetAttribLocation办法:获取上色器程序中,指定为attribute类型变量的id。 GLES20.glGetUniformLocation办法:获取上色器程序中,指定为uniform类型变量的id。
如:
// 获取指向上色器中aPosition的index
maPositionHandle = GLES20.glGetAttribLocation(mProgram, "aPosition");
// 获取指向上色器中uMVPMatrix的index
muMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
向上色器传递数据
运用上一节获取的指向上色器相应数据成员的各个id,就能将咱们自己界说的极点数据、色彩数据等等各种数据传递到上色器当中了。
// 运用shader程序
GLES20.glUseProgram(mProgram);
// 将终究改换矩阵传入shader程序
GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, MatrixState.getFinalMatrix(), 0);
// 设置缓冲区起始方位
mRectBuffer.position(0);
// 极点方位数据传入上色器
GLES20.glVertexAttribPointer(maPositionHandle, 3, GLES20.GL_FLOAT, false, 20, mRectBuffer);
// 极点色彩数据传入上色器中
GLES20.glVertexAttribPointer(maColorHandle, 4, GLES20.GL_FLOAT, false, 4*4, mColorBuffer);
// 极点坐标传递到极点上色器
GLES20.glVertexAttribPointer(maTextureHandle, 2, GLES20.GL_FLOAT, false, 20, mRectBuffer);
// 答应运用极点坐标数组
GLES20.glEnableVertexAttribArray(maPositionHandle);
// 答应运用极点色彩数组
GLES20.glDisableVertexAttribArray(maColorHandle);
// 答应运用定点纹路数组
GLES20.glEnableVertexAttribArray(maTextureHandle);
// 绑定纹路
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture);
// 图形制作
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, 4);
界说极点特点数组
void glVertexAttribPointer (int index, int size, int type, boolean normalized, int stride, Buffer ptr )
参数意义: index 指定要修正的极点上色器中极点变量id; size 指定每个极点特点的组件数量。有必要为1、2、3或许4。如position是由3个(x,y,z)组成,而色彩是4个(r,g,b,a)); type 指定数组中每个组件的数据类型。可用的符号常量有GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT,GL_UNSIGNED_SHORT, GL_FIXED, 和 GL_FLOAT,初始值为GL_FLOAT; normalized 指定当被拜访时,固定点数据值是否应该被归一化(GL_TRUE)或许直接转化为固定点值(GL_FALSE); stride 指定接连极点特点之间的偏移量。假如为0,那么极点特点会被了解为:它们是紧密摆放在一同的。初始值为0。假如normalized被设置为GL_TRUE,意味着整数型的值会被映射至区间[-1,1],或许区间[0,1](无符号整数),反之,这些值会被直接转化为浮点值而不进行归一化处理; ptr 极点的缓冲数据。
启用或许禁用极点特点数组
调用GLES20.glEnableVertexAttribArray和GLES20.glDisableVertexAttribArray传入参数index。
GLES20.glEnableVertexAttribArray(glHPosition);
GLES20.glEnableVertexAttribArray(glHCoordinate);
假如启用,那么当GLES20.glDrawArrays或许GLES20.glDrawElements被调用时,极点特点数组会被运用。
挑选活动纹路单元。
void glActiveTexture (int texture)
texture指定哪一个纹路单元被置为活动状况。texture有必要是GL_TEXTUREi之一,其间0 <= i < GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS,初始值为GL_TEXTURE0。 GLES20.glActiveTexture()确认了后续的纹路状况改动影响哪个纹路,纹路单元的数量是根据该纹路单元所被支撑的详细完结。
十一丶OpenSL ES
1.Android OpenSL 介绍和开发流程阐明
Android OpenSL ES 介绍
OpenSL ES (Open Sound Library for Embedded Systems)是无授权费、跨渠道、针对嵌入式体系精心优化的硬件音频加快API。它为嵌入式移动多媒体设备上的本地运用程序开发者供给标准化, 高性能,低呼应时刻的音频功用完结办法,并完结软/硬件音频性能的直接跨渠道布置,降低履行难度,促进高档音频市场的开展。简略来说OpenSL ES是一个嵌入式跨渠道免费的音频处理库。
Android的OpenSL ES库是在NDK的platforms文件夹对应android渠道先相应cpu类型里边,如:
Android OpenSL ES 开发流程
OpenSL ES 的开发流程首要有如下6个进程:
1、 创立接口目标
2、设置混音器
3、创立播映器(录音器)
4、设置缓冲行列和回调函数
5、设置播映状况
6、发动回调函数
注明:其间第4步和第6步是OpenSL ES 播映PCM等数据格局的音频是需求用到的。
在运用OpenSL ES的API之前,需求引进OpenSL ES的头文件,代码如下:
#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>
由所以在Native层运用该特性,所需需求在Android.mk中添加链接选项,以便在链接阶段运用到体系体系的OpenSL ES的so库:
LOCAL_LDLIBS += -lOepnSLES
咱们知道OpenSL ES供给的是根据C言语的API,可是它是根据目标和接口的办法供给的,会选用面向目标的思想开发API。因而咱们先来了解一下OpenSL ES中目标和接口的概念:
- 目标:目标是对一组资源及其状况的抽象,每个目标都有一个在其创立时指定的类型,类型决定了目标能够履行的任务集,目标有点相似于C++中类的概念。
- 接口:接口是目标供给的一组特征的抽象,这些抽象会为开发者供给一组办法以及每个接口的类型功用,在代码中,接口的类型由接口ID来标识。
需求要点了解的是,一个目标在代码中其实是没有实践的表明方式的,能够经过接口来改动目标的状况以及运用目标供给的功用。目标有能够有一个或许多个接口的实例,可是接口实例肯定只归于一个目标。
假如明白了OpenSL ES 中目标和接口的概念,那么下面咱们就继续看看,在代码中是怎样运用它们的。
上面咱们也提到过,目标是没有实践的代码表明方式的,目标的创立也是经过接口来完结的。经过获取目标的办法来获取出目标,进而能够拜访目标的其他的接口办法或许改动目标的状况,下面是运用目标和接口的相关阐明。
OpenSL ES 开发最重要的接口类 SLObjectItf
经过SLObjectItf接口类咱们能够创立所需求的各种类型的类接口,比方:
- 创立引擎接口目标:SLObjectItf engineObject
- 创立混音器接口目标:SLObjectItf outputMixObject
- 创立播映器接口目标:SLObjectItf playerObject
以上等等都是经过SLObjectItf来创立的。
SLObjectItf 创立的详细的接口目标实例
OpenSL ES中也有详细的接口类,比方(引擎:SLEngineItf,播映器:SLPlayItf,声响操控器:SLVolumeItf等等)。
创立引擎并完结
OpenSL ES中开端的第一步都是声明SLObjectItf接口类型的引擎接口目标engineObject,然后用办法slCreateEngine创立一个引擎接口目标;创立好引擎接口目标后,需求用SLObjectItf的Realize办法来完结engineObject;最终用SLObjectItf的GetInterface办法来初始化SLEngnineItf目标实例。如:
SLObjectItf engineObject = NULL;//用SLObjectItf声明引擎接口目标
SLEngineItf engineEngine = NULL;//声明详细的引擎目标实例
void createEngine()
{
SLresult result;//回来效果
result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);//第一步创立引擎
result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);//完结(Realize)engineObject接口目标
result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);//经过engineObject的GetInterface办法初始化engineEngine
}
运用引擎目标创立其他接口目标
其他接口目标(SLObjectItf outputMixObject,SLObjectItf playerObject)等都是用引擎接口目标创立的(详细的接口目标需求的参数这儿就说了,可参照ndk比方里边的),如:
//混音器
SLObjectItf outputMixObject = NULL;//用SLObjectItf创立混音器接口目标
SLEnvironmentalReverbItf outputMixEnvironmentalReverb = NULL;////创立详细的混音器目标实例
result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 1, mids, mreq);//运用引擎接口目标创立混音器接口目标
result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);//完结(Realize)混音器接口目标
result = (*outputMixObject)->GetInterface(outputMixObject, SL_IID_ENVIRONMENTALREVERB, &outputMixEnvironmentalReverb);//运用混音器接口目标初始化详细混音器实例
//播映器
SLObjectItf playerObject = NULL;//用SLObjectItf创立播映器接口目标
SLPlayItf playerPlay = NULL;//创立详细的播映器目标实例
result = (*engineEngine)->CreateAudioPlayer(engineEngine, &playerObject, &audioSrc, &audioSnk, 3, ids, req);//运用引擎接口目标创立播映器接口目标
result = (*playerObject)->Realize(playerObject, SL_BOOLEAN_FALSE);//完结(Realize)播映器接口目标
result = (*playerObject)->GetInterface(playerObject, SL_IID_PLAY, &playerPlay);//初始化详细的播映器目标实例
最终便是运用创立好的详细目标实例来完结详细的功用。
2.OpenSL ES 运用示例
首要导入OpenSL ES和其他有必要的库:
-lOpenSLES -landroid
播映assets文件
创立引擎——>创立混音器——>创立播映器——>设置播映状况
JNIEXPORT void JNICALL
Java_com_renhui_openslaudio_MainActivity_playAudioByOpenSL_1assets(JNIEnv *env, jobject instance, jobject assetManager, jstring filename) {
release();
const char *utf8 = (*env)->GetStringUTFChars(env, filename, NULL);
// use asset manager to open asset by filename
AAssetManager* mgr = AAssetManager_fromJava(env, assetManager);
AAsset* asset = AAssetManager_open(mgr, utf8, AASSET_MODE_UNKNOWN);
(*env)->ReleaseStringUTFChars(env, filename, utf8);
// open asset as file descriptor
off_t start, length;
int fd = AAsset_openFileDescriptor(asset, &start, &length);
AAsset_close(asset);
SLresult result;
//第一步,创立引擎
createEngine();
//第二步,创立混音器
const SLInterfaceID mids[1] = {SL_IID_ENVIRONMENTALREVERB};
const SLboolean mreq[1] = {SL_BOOLEAN_FALSE};
result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 1, mids, mreq);
(void)result;
result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
(void)result;
result = (*outputMixObject)->GetInterface(outputMixObject, SL_IID_ENVIRONMENTALREVERB, &outputMixEnvironmentalReverb);
if (SL_RESULT_SUCCESS == result) {
result = (*outputMixEnvironmentalReverb)->SetEnvironmentalReverbProperties(outputMixEnvironmentalReverb, &reverbSettings);
(void)result;
}
//第三步,设置播映器参数和创立播映器
// 1、 装备 audio source
SLDataLocator_AndroidFD loc_fd = {SL_DATALOCATOR_ANDROIDFD, fd, start, length};
SLDataFormat_MIME format_mime = {SL_DATAFORMAT_MIME, NULL, SL_CONTAINERTYPE_UNSPECIFIED};
SLDataSource audioSrc = {&loc_fd, &format_mime};
// 2、 装备 audio sink
SLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject};
SLDataSink audioSnk = {&loc_outmix, NULL};
// 创立播映器
const SLInterfaceID ids[3] = {SL_IID_SEEK, SL_IID_MUTESOLO, SL_IID_VOLUME};
const SLboolean req[3] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE};
result = (*engineEngine)->CreateAudioPlayer(engineEngine, &fdPlayerObject, &audioSrc, &audioSnk, 3, ids, req);
(void)result;
// 完结播映器
result = (*fdPlayerObject)->Realize(fdPlayerObject, SL_BOOLEAN_FALSE);
(void)result;
// 得到播映器接口
result = (*fdPlayerObject)->GetInterface(fdPlayerObject, SL_IID_PLAY, &fdPlayerPlay);
(void)result;
// 得到声响操控接口
result = (*fdPlayerObject)->GetInterface(fdPlayerObject, SL_IID_VOLUME, &fdPlayerVolume);
(void)result;
//第四步,设置播映状况
if (NULL != fdPlayerPlay) {
result = (*fdPlayerPlay)->SetPlayState(fdPlayerPlay, SL_PLAYSTATE_PLAYING);
(void)result;
}
//设置播映音量 (100 * -50:静音 )
(*fdPlayerVolume)->SetVolumeLevel(fdPlayerVolume, 20 * -50);
}
播映pcm文件
(集成到ffmpeg时,也是播映ffmpeg转化成的pcm格局的数据),这儿为了模仿是直接读取的pcm格局的音频文件。
创立播映器和混音器
//第一步,创立引擎
createEngine();
//第二步,创立混音器
const SLInterfaceID mids[1] = {SL_IID_ENVIRONMENTALREVERB};
const SLboolean mreq[1] = {SL_BOOLEAN_FALSE};
result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 1, mids, mreq);
(void)result;
result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
(void)result;
result = (*outputMixObject)->GetInterface(outputMixObject, SL_IID_ENVIRONMENTALREVERB, &outputMixEnvironmentalReverb);
if (SL_RESULT_SUCCESS == result) {
result = (*outputMixEnvironmentalReverb)->SetEnvironmentalReverbProperties(
outputMixEnvironmentalReverb, &reverbSettings);
(void)result;
}
SLDataLocator_OutputMix outputMix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject};
SLDataSink audioSnk = {&outputMix, NULL};
设置pcm格局的频率位数等信息并创立播映器
// 第三步,装备PCM格局信息
SLDataLocator_AndroidSimpleBufferQueue android_queue={SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,2};
SLDataFormat_PCM pcm={
SL_DATAFORMAT_PCM,//播映pcm格局的数据
2,//2个声道(立体声)
SL_SAMPLINGRATE_44_1,//44100hz的频率
SL_PCMSAMPLEFORMAT_FIXED_16,//位数 16位
SL_PCMSAMPLEFORMAT_FIXED_16,//和位数共同就行
SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,//立体声(前左前右)
SL_BYTEORDER_LITTLEENDIAN//结束标志
};
SLDataSource slDataSource = {&android_queue, &pcm};
const SLInterfaceID ids[3] = {SL_IID_BUFFERQUEUE, SL_IID_EFFECTSEND, SL_IID_VOLUME};
const SLboolean req[3] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE};
result = (*engineEngine)->CreateAudioPlayer(engineEngine, &pcmPlayerObject, &slDataSource, &audioSnk, 3, ids, req);
//初始化播映器
(*pcmPlayerObject)->Realize(pcmPlayerObject, SL_BOOLEAN_FALSE);
// 得到接口后调用 获取Player接口
(*pcmPlayerObject)->GetInterface(pcmPlayerObject, SL_IID_PLAY, &pcmPlayerPlay);
设置缓冲行列和回调函数
// 注册回调缓冲区 获取缓冲行列接口
(*pcmPlayerObject)->GetInterface(pcmPlayerObject, SL_IID_BUFFERQUEUE, &pcmBufferQueue);
//缓冲接口回调
(*pcmBufferQueue)->RegisterCallback(pcmBufferQueue, pcmBufferCallBack, NULL);
回调函数:
void * pcmBufferCallBack(SLAndroidBufferQueueItf bf, void * context)
{
//assert(NULL == context);
getPcmData(&buffer);
// for streaming playback, replace this test by logic to find and fill the next buffer
if (NULL != buffer) {
SLresult result;
// enqueue another buffer
result = (*pcmBufferQueue)->Enqueue(pcmBufferQueue, buffer, 44100 * 2 * 2);
// the most likely other result is SL_RESULT_BUFFER_INSUFFICIENT,
// which for this code example would indicate a programming error
}
}
读取pcm格局的文件:
void getPcmData(void **pcm)
{
while(!feof(pcmFile))
{
fread(out_buffer, 44100 * 2 * 2, 1, pcmFile);
if(out_buffer == NULL)
{
LOGI("%s", "read end");
break;
} else{
LOGI("%s", "reading");
}
*pcm = out_buffer;
break;
}
}
设置播映状况并手动开端调用回调函数
// 获取播映状况接口
(*pcmPlayerPlay)->SetPlayState(pcmPlayerPlay, SL_PLAYSTATE_PLAYING);
// 自动调用回调函数开端作业
pcmBufferCallBack(pcmBufferQueue, NULL);
留意:
在回调函数中result = (pcmBufferQueue)->Enqueue(pcmBufferQueue, buffer, 44100 * 2 * 2),最终的“4410022”是buffer的巨细,因为我这儿是指定了没读取一次就从pcm文件中读取了“441002*2”个字节,所以能够正常播映,假如是运用ffmpeg来获取pcm数据源,那么实践巨细要根据每个AVframe的详细巨细来定,这样才干正常播映出声响!(44100 * 2 * 2 表明:44100是频率HZ,2是立体声双通道,2是选用的16位采样即2个字节,所以总的字节数便是:44100 * 2 * 2)
3.运用 OpenSL 播映 PCM 数据
OpenSL ES 是根据NDK也便是c言语的底层开发音频的揭露API,经过运用它能够做到标准化, 高性能,低呼应时刻的音频功用完结办法。
这次是运用OpenSL ES来做一个音乐播映器,它能够播映m4a、mp3文件,并能够暂停和调整音量。
播映音乐需求做一些进程:
创立声响引擎
首要创立声响引擎的目标接口
result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
然后完结它
result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
从声响引擎的目标中抓取声响引擎
result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
创立”输出混音器”
result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 1, ids, req);
完结输出混合音
result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
创立声响播映器
创立和完结播映器
// realize the player
result = (*bqPlayerObject)->Realize(bqPlayerObject, SL_BOOLEAN_FALSE);
assert(SL_RESULT_SUCCESS == result);
(void)result;
// get the play interface
result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_PLAY, &bqPlayerPlay);
assert(SL_RESULT_SUCCESS == result);
(void)result;
设置播映缓冲
数据格局装备
SLDataFormat_PCM format_pcm = {SL_DATAFORMAT_PCM, 1, SL_SAMPLINGRATE_8,
SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16,
SL_SPEAKER_FRONT_CENTER, SL_BYTEORDER_LITTLEENDIAN};
数据定位器 便是定位要播映声响数据的存放方位,分为4种:内存方位,输入/输出设备方位,缓冲区行列方位,和midi缓冲区行列方位。 数据定位器装备
SLDataLocator_AndroidSimpleBufferQueue loc_bufq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};
得到了缓存行列接口,并注册
// get the buffer queue interface
result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_BUFFERQUEUE,
&bqPlayerBufferQueue);
assert(SL_RESULT_SUCCESS == result);
(void)result;
// register callback on the buffer queue
result = (*bqPlayerBufferQueue)->RegisterCallback(bqPlayerBufferQueue, bqPlayerCallback, NULL);
assert(SL_RESULT_SUCCESS == result);
(void)result;
获得其他接口用来操控播映
得到声响特效接口
// get the effect send interface
result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_EFFECTSEND,
&bqPlayerEffectSend);
assert(SL_RESULT_SUCCESS == result);
(void)result;
得到音量接口
// get the volume interface
result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_VOLUME, &bqPlayerVolume);
assert(SL_RESULT_SUCCESS == result);
(void)result;
// set the player's state to playing
result = (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_PLAYING);
assert(SL_RESULT_SUCCESS == result);
(void)result;
供给播映数据
翻开音乐文件
// convert Java string to UTF-8
const char *utf8 = (*env)->GetStringUTFChars(env, filename, NULL);
assert(NULL != utf8);
// use asset manager to open asset by filename
AAssetManager* mgr = AAssetManager_fromJava(env, assetManager);
assert(NULL != mgr);
AAsset* asset = AAssetManager_open(mgr, utf8, AASSET_MODE_UNKNOWN);
// release the Java string and UTF-8
(*env)->ReleaseStringUTFChars(env, filename, utf8);
// the asset might not be found
if (NULL == asset) {
return JNI_FALSE;
}
// open asset as file descriptor
off_t start, length;
int fd = AAsset_openFileDescriptor(asset, &start, &length);
assert(0 <= fd);
AAsset_close(asset);
设置播映数据
SLDataLocator_AndroidFD loc_fd = {SL_DATALOCATOR_ANDROIDFD, fd, start, length};
SLDataFormat_MIME format_mime = {SL_DATAFORMAT_MIME, NULL, SL_CONTAINERTYPE_UNSPECIFIED};
SLDataSource audioSrc = {&loc_fd, &format_mime};
播映音乐
播映音乐只需求经过播映接口改动播映状况就能够了,暂停也是,中止也是,可是暂停有必要之前的播映缓存做了才行,不然那暂停就适当于中止了
result = (*fdPlayerPlay)->SetPlayState(fdPlayerPlay, isPlaying ? SL_PLAYSTATE_PLAYING : SL_PLAYSTATE_PAUSED);
调解音量
SLVolumeItf getVolume()
{
if (fdPlayerVolume != NULL)
return fdPlayerVolume;
else
return bqPlayerVolume;
}
void Java_com_renhui_openslaudio_MainActivity_setVolumeAudioPlayer(JNIEnv env, jclass clazz,
jint millibel)
{
SLresult result;
SLVolumeItf volumeItf = getVolume();
if (NULL != volumeItf) {
result = (volumeItf)->SetVolumeLevel(volumeItf, millibel);
assert(SL_RESULT_SUCCESS == result);
(void)result;
}
}
开释资源
封闭app时开释占用资源
void Java_com_renhui_openslaudio_MainActivity_shutdown(JNIEnv* env, jclass clazz)
{
// destroy buffer queue audio player object, and invalidate all associated interfaces
if (bqPlayerObject != NULL) {
(*bqPlayerObject)->Destroy(bqPlayerObject);
bqPlayerObject = NULL;
bqPlayerPlay = NULL;
bqPlayerBufferQueue = NULL;
bqPlayerEffectSend = NULL;
bqPlayerMuteSolo = NULL;
bqPlayerVolume = NULL;
}
// destroy file descriptor audio player object, and invalidate all associated interfaces
if (fdPlayerObject != NULL) {
(*fdPlayerObject)->Destroy(fdPlayerObject);
fdPlayerObject = NULL;
fdPlayerPlay = NULL;
fdPlayerSeek = NULL;
fdPlayerMuteSolo = NULL;
fdPlayerVolume = NULL;
}
// destroy output mix object, and invalidate all associated interfaces
if (outputMixObject != NULL) {
(*outputMixObject)->Destroy(outputMixObject);
outputMixObject = NULL;
outputMixEnvironmentalReverb = NULL;
}
// destroy engine object, and invalidate all associated interfaces
if (engineObject != NULL) {
(*engineObject)->Destroy(engineObject);
engineObject = NULL;
engineEngine = NULL;
}
}
4.OpenSL 录制 PCM 音频数据**
完结阐明
OpenSL ES的录音要比播映简略一些,在创立好引擎后,再创立好录音接口根本就能够录音了。在这儿咱们做的是流式录音,所以需求用至少2个buffer来缓存录制好的PCM数据,这儿咱们能够动态创立一个二维数组,里边有2个buffer,然后每次录音取出一个,录制好后再写入文件就能够了,2个buffer顺次来存储PCM数据,这样就能够接连录制流式音频数据了,二维数组里边自己维护了一个索引,来标识当时处于哪个buffer录制状况,露出给外部的仅仅调用办法而已,细节对外也是隐藏的。
编码完结
编写缓存buffer行列:RecordBuffer.h、RecordBuffer.cpp
#ifndef OPENSLRECORD_RECORDBUFFER_H
#define OPENSLRECORD_RECORDBUFFER_H
class RecordBuffer {
public:
short **buffer;
int index = -1;
public:
RecordBuffer(int buffersize);
~RecordBuffer();
/**
* 得到一个新的录制buffer
* @return
*/
short* getRecordBuffer();
/**
* 得到当时录制buffer
* @return
*/
short* getNowBuffer();
};
#endif //OPENSLRECORD_RECORDBUFFER_H
#include "RecordBuffer.h"
RecordBuffer::RecordBuffer(int buffersize) {
buffer = new short *[2];
for(int i = 0; i < 2; i++)
{
buffer[i] = new short[buffersize];
}
}
RecordBuffer::~RecordBuffer() {
}
short *RecordBuffer::getRecordBuffer() {
index++;
if(index > 1)
{
index = 0;
}
return buffer[index];
}
short *RecordBuffer::getNowBuffer() {
return buffer[index];
}
这个行列其实便是PCM存储的buffer,getRecordBuffer()为即即将录入PCM数据的buffer,getNowBuffer()是当时录制好的PCM数据的buffer,能够写入文件,即咱们得到的PCM数据。
运用OpenSL ES录制PCM数据
进程分为:创立引擎->初始化IO设备(自动检测麦克风等音频输入设备)->设置缓存行列->设置录制PCM数据标准->设置录音器接口->设置行列接口并设置录音状况为录制->开端录音。
const char *path = env->GetStringUTFChars(path_, 0);
/**
* PCM文件
*/
pcmFile = fopen(path, "w");
/**
* PCMbuffer行列
*/
recordBuffer = new RecordBuffer(RECORDER_FRAMES * 2);
SLresult result;
/**
* 创立引擎目标
*/
result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
/**
* 设置IO设备(麦克风)
*/
SLDataLocator_IODevice loc_dev = {SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT,
SL_DEFAULTDEVICEID_AUDIOINPUT, NULL};
SLDataSource audioSrc = {&loc_dev, NULL};
/**
* 设置buffer行列
*/
SLDataLocator_AndroidSimpleBufferQueue loc_bq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};
/**
* 设置录制标准:PCM、2声道、44100HZ、16bit
*/
SLDataFormat_PCM format_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};
SLDataSink audioSnk = {&loc_bq, &format_pcm};
const SLInterfaceID id[1] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE};
const SLboolean req[1] = {SL_BOOLEAN_TRUE};
/**
* 创立录制器
*/
result = (*engineEngine)->CreateAudioRecorder(engineEngine, &recorderObject, &audioSrc,
&audioSnk, 1, id, req);
if (SL_RESULT_SUCCESS != result) {
return;
}
result = (*recorderObject)->Realize(recorderObject, SL_BOOLEAN_FALSE);
if (SL_RESULT_SUCCESS != result) {
return;
}
result = (*recorderObject)->GetInterface(recorderObject, SL_IID_RECORD, &recorderRecord);
result = (*recorderObject)->GetInterface(recorderObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
&recorderBufferQueue);
finished = false;
result = (*recorderBufferQueue)->Enqueue(recorderBufferQueue, recordBuffer->getRecordBuffer(),
recorderSize);
result = (*recorderBufferQueue)->RegisterCallback(recorderBufferQueue, bqRecorderCallback, NULL);
LOGD("开端录音");
/**
* 开端录音
*/
(*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_RECORDING);
env->ReleaseStringUTFChars(path_, path);
录音回调如下:
void bqRecorderCallback(SLAndroidSimpleBufferQueueItf bq, void *context)
{
// for streaming recording, here we would call Enqueue to give recorder the next buffer to fill
// but instead, this is a one-time buffer so we stop recording
LOGD("record size is %d", recorderSize);
fwrite(recordBuffer->getNowBuffer(), 1, recorderSize, pcmFile);
if(finished)
{
(*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_STOPPED);
fclose(pcmFile);
LOGD("中止录音");
} else{
(*recorderBufferQueue)->Enqueue(recorderBufferQueue, recordBuffer->getRecordBuffer(),
recorderSize);
}
}
这样就完结了OPenSL ES的PCM音频数据录制,咱们这儿拿到了录制的PCM数据能够用mediacodec或ffmpeg来编码成aac格局的音频,也能够直接用推流到服务器来完结音频直播。
完好代码如下:
#include <jni.h>
#include <string>
#include "AndroidLog.h"
#include "RecordBuffer.h"
#include "unistd.h"
extern "C"
{
#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>
}
//引擎接口
static SLObjectItf engineObject = NULL;
//引擎目标
static SLEngineItf engineEngine;
//录音器接口
static SLObjectItf recorderObject = NULL;
//录音器目标
static SLRecordItf recorderRecord;
//缓冲行列
static SLAndroidSimpleBufferQueueItf recorderBufferQueue;
//录制巨细设为4096
#define RECORDER_FRAMES (2048)
static unsigned recorderSize = RECORDER_FRAMES * 2;
//PCM文件
FILE *pcmFile;
//录音buffer
RecordBuffer *recordBuffer;
bool finished = false;
void bqRecorderCallback(SLAndroidSimpleBufferQueueItf bq, void *context)
{
// for streaming recording, here we would call Enqueue to give recorder the next buffer to fill
// but instead, this is a one-time buffer so we stop recording
LOGD("record size is %d", recorderSize);
fwrite(recordBuffer->getNowBuffer(), 1, recorderSize, pcmFile);
if(finished)
{
(*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_STOPPED);
fclose(pcmFile);
LOGD("中止录音");
} else{
(*recorderBufferQueue)->Enqueue(recorderBufferQueue, recordBuffer->getRecordBuffer(),
recorderSize);
}
}
extern "C"
JNIEXPORT void JNICALL
Java_com_renhui_openslrecord_MainActivity_rdSound(JNIEnv *env, jobject instance, jstring path_) {
const char *path = env->GetStringUTFChars(path_, 0);
/**
* PCM文件
*/
pcmFile = fopen(path, "w");
/**
* PCMbuffer行列
*/
recordBuffer = new RecordBuffer(RECORDER_FRAMES * 2);
SLresult result;
/**
* 创立引擎目标
*/
result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
/**
* 设置IO设备(麦克风)
*/
SLDataLocator_IODevice loc_dev = {SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT,
SL_DEFAULTDEVICEID_AUDIOINPUT, NULL};
SLDataSource audioSrc = {&loc_dev, NULL};
/**
* 设置buffer行列
*/
SLDataLocator_AndroidSimpleBufferQueue loc_bq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};
/**
* 设置录制标准:PCM、2声道、44100HZ、16bit
*/
SLDataFormat_PCM format_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};
SLDataSink audioSnk = {&loc_bq, &format_pcm};
const SLInterfaceID id[1] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE};
const SLboolean req[1] = {SL_BOOLEAN_TRUE};
/**
* 创立录制器
*/
result = (*engineEngine)->CreateAudioRecorder(engineEngine, &recorderObject, &audioSrc,
&audioSnk, 1, id, req);
if (SL_RESULT_SUCCESS != result) {
return;
}
result = (*recorderObject)->Realize(recorderObject, SL_BOOLEAN_FALSE);
if (SL_RESULT_SUCCESS != result) {
return;
}
result = (*recorderObject)->GetInterface(recorderObject, SL_IID_RECORD, &recorderRecord);
result = (*recorderObject)->GetInterface(recorderObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
&recorderBufferQueue);
finished = false;
result = (*recorderBufferQueue)->Enqueue(recorderBufferQueue, recordBuffer->getRecordBuffer(),
recorderSize);
result = (*recorderBufferQueue)->RegisterCallback(recorderBufferQueue, bqRecorderCallback, NULL);
LOGD("开端录音");
/**
* 开端录音
*/
(*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_RECORDING);
env->ReleaseStringUTFChars(path_, path);
}extern "C"
JNIEXPORT void JNICALL
Java_com_renhui_openslrecord_MainActivity_rdStop(JNIEnv *env, jobject instance) {
// TODO
if(recorderRecord != NULL)
{
finished = true;
}
}
验证录制效果
有两种办法:
- 运用Android OpenSL ES 开发:运用 OpenSL 播映 PCM 数据的demo进行播映。
- 运用 ffplay 命令播映,命令为:ffplay -f s16le -ar 44100 -ac 2 temp.pcm (命令由来:在录制代码里的参数为录制标准:PCM、2声道、44100HZ、16bit)
十二丶OpenSL ES运用SoundTouch完结PCM音频的变速和变调
缘由
OpenSL ES 学习到现在现已知道 OpenSL ES 不仅能播映和录制PCM音频数据,还能改动声响巨细、设置左声道或右声道播映、还能变速播映,可谓是播映音频的王者。可是变速有一点欠好的便是,尽管播映音频的速度变了,可是相应的音调也随之变了,这样的用户体会就不那么好了。所以就想到了用开源的SoundTouch来完结PCM音频变速和变调,OpenSL ES仅仅单纯的播映PCM数据就能够了。
完结
移植SoundTouch(Android)
下载SoundTouch源码
在项目jni文件夹中创立include和SoundTouch文件夹,并把下载好的SoundTouch里边的include和SoundTouch的源码拷贝进去就能够了,目录结构如下:
用SoundTouch转码PCM源文件
因为SoundTouch默许是float(32bit)格局的数据,这儿需求先改成short(16bit)的格局。翻开STTypes.h文件,修正如下代码:
再注释掉下面这句,不然编译不经过(for x86模仿器):
这样SoundTouch里边处理PCM数据便是用的16bit的数据了。
SoundTouch运用流程
添加命名空间,并创立SoundTouch指针变量
using namespace soundtouch;
SoundTouch *soundTouch;
设置SoundTouch参数
soundTouch = new SoundTouch();
soundTouch->setSampleRate(44100);//设置采样率,此处为44100,根据实践情况可变
soundTouch->setChannels(2);//声道,此处为立体声
soundTouch->setPitch(1);//变调不变速,如0.5、1.0、1.5等
soundTouch->setTempo(1);//变速不变调,如0.5、1.0、2.0等
向SoundTouch中传入获取到的PCM数据,运用:putSamples函数
size = fread(pcm_buffer, 1, 4096 * 2, pcmFile);
soundTouch->putSamples((const SAMPLETYPE *) pcm_buffer, size / 4);
这儿,pcm_buffer是u_int16_t *类型的,也便是说和SoundTouch处理的PCM数据位数是共同的(16bit),所以能够直接传入SoundTouch中。putSamples的第一个参数便是PCM数据指针,第二个参数是采样点的个数,由所以2声道16bit(2byte),所以PCM数据的采样点个数为:num = 巨细(size)/ (2 * 2)。
获取SoundTouch输出的PCM数据:运用receiveSamples函数
num = soundTouch->receiveSamples(sd_buffer, size / 4);
这儿,receiveSamples的第一个参数是SoundTouch(变速或变调)处理后的PCM数据存放的内存地址,第二个参数是或许的最大采样个数,能够和putSamples保持共同,其间sd_buffer是SAMPLETYPE * 类型的,记得要提前分配好内存巨细,最终回来值便是SoundTouch处理后的PCM里边所包含的采样个数,因为或许有缓存,所以应循环读取receiveSamples,直到回来值为0为止。
OpenSL ES播映SoundTouch处理后的PCM音频数据
(*pcmBufferQueue)->Enqueue(pcmBufferQueue, sd_buffer, size * 4);
因为size是采样个数,所以sd_buffer的巨细是:size * 2(声道) * 2(16bit==2字节)。
这样,咱们听到的声响便是经过SoundTouch转码往后的了,如:变速不变调,变调不变速,变速又变调都能够自己设置。
思维发散
*FFmpeg解码得到的PCM数据(uint_8 )运用SoundTouch转码
这儿要处理的便是把uint_8 *(8bit)的数据转化成short(16bit)的数据格局。这儿其实便是做bit的位运算,原理如下如:
转化代码如下:
for (int i = 0; i < size / 2 + 1; i++)
{
sd_buffer[i] = (pcm_buffer[i * 2] | (pcm_buffer[i * 2 + 1] << 8));
}
soundTouch->putSamples((const SAMPLETYPE *) pcm_buffer, size / 4);
后续操作和16bit的相同不变。
总结
尽管是简略的移植SoundTouch到Android来播映PCM数据,可是仍是让咱们了解到了数据在内存中怎样摆放的,然后能够怎样操作最小单位的bit来达到咱们的要求。
重视公众号:Android苦做舟
解锁 《Android十二大板块文档》
音视频大合集,从初中高到面试包罗万象;让学习更靠近未来实战。已构成PDF版
十二个模块内容如下:
1.2022最新Android11位大厂面试专题,128道附答案
2.音视频大合集,从初中高到面试包罗万象
3.Android车载运用大合集,从零开端一同学
4.性能优化大合集,离别优化烦恼
5.Framework大合集,从里到外分析的明明白白
6.Flutter大合集,进阶Flutter高档工程师
7.compose大合集,拥抱新技术
8.Jetpack大合集,全家桶一次吃个够
9.架构大合集,轻松应对作业需求
10.Android根底篇大合集,根基安定楼房平地起
11.Flutter番外篇:Flutter面试+项目实战+电子书
12.大厂高档Android组件化强化实战
收拾不易,重视一下吧,ღ( ・ᴗ・` )