当接收到音视频的原始数据后,如何利用iOS和Android系统API渲染到扬声器(Speaker)
或屏幕(View)
上
声音的渲染在iOS上使用AudioUnit(AUGraph)
,Android使用OpneSL ES
或AudioTrack
视频渲染, 使用跨平台的渲染技术,即OpneGL ES
将一张图片渲染到屏幕上
iOS AudioUnit
在 iOS 平台上,所有的音频框架底层都是基于AudioUnit实现的,较高层次的音频框架包括: Media Player, AV Foundation, OpenAL和Audio Toolbox, 这些框架都封装了AudioUnit, 然后提供更高层次的API

下面是 iOS 中使用AudioUnit来完成一个音频播放的功能
- 认识
Audio Session
go
// 获取音频会话单例, 其用于管理与获取iOS音频设备。
AVAudioSession *audioSession = [AVAudioSession sharedInstance]
// 根据硬件设备提供的能力来设置类别
[audioSession setCategory:AVAudioSessionCategoryPlayAndRecord error:&error];
// 设置I/O的buffer,buffer越小说明延迟越低
[audioSession setPreferredBufferDuration:0.002 error:&error];
// 设置采样频率,让硬件设备按照设置的采样频率来采集和播放音频
[audioSession setPreferredSampleRate:44100 error:&error];
// 激活AudioSession
[audioSession setActive:YES error:&error];
- 构建
AudioUnit
在创建并启用音频会话之后,就可以构建AudioUnit了。构建 AudioUnit的时候需要指定类型(Type)、子类型(subtype)以及厂商 (Manufacture)。
ini
// 构造AudioUnit描述的结构体, 类型为RemoteIO
AudioComponentDescription ioUnitDescription;
ioUnitDescription.componentType = kAudioUnitType_Output;
ioUnitDescription.componentSubType = kAudioUnitType_RemoteIO;
ioUnitDescription.componentManfacturer = kAudioUnitManufacturer_Apple;
ioUnitDescription.componentFlags = 0;
// 构造真正的AudioUnit, 两种方式
1. 裸创建方式
// 根据AudioUnit描述,找到实际的AudioUnit类型
AudioComponent ioUnitRef = AudioComponentFindNext(NULL, &ioUnitDescription);
AudioUnit ioUnitInstance;
// 根据类型创造AudioUnit对象
AudioComponentInstanceNew(ioUnitRef, &ioUnitInstance);
2. AUGraph创建方式
// 定义AUGraph
AUGraph processingGraph;
NewAuGraph (&processingGraph);
// 按照AudioUnit描述在AUGraph中增加AUNode
AUNode ioNode;
AUGraphAddNode (processingGraph, &ioUnitDescription, &ioNode);
// 打开AUGraph
AUGraphOpen processingGraph;
// 在AUGraph中的某个Node里获得AudioUnit的引用
AudioUnit ioUnit;
AUGrophNodeInfo (processingGraph, ioNode, NULL, &ioUnit);
- AudioUnit的通用参数设置
-
RemoteIO这个AudioUnit是与硬件IO有关的一个Unit,它分为输入端和输出端(I代表Input,O代表Output)
-
输入端一般代表麦克风,输出端一般代表扬声器(Speaker)或者耳机
-
RemoteIO Unit分为Element0和Element1,其中Element0控制输出端,Element1控制输入端,同时每个Element分为Input Scope和Output Scope
-
如果需要使用麦克风的声音播放功能,需要把这个Unit的Element0的OuputScope和Speaker进行连接
-
如果开发者想要使用麦克风的录音功能,必须将Unit的Element1的InputScope和麦克风连接起来
下面是使用扬声器的代码
ini
CSStatus status = noErr;
UInt32 oneFlag = 1;
UInt32 busZero = 0;// Element 0
// 把RemoteIOUnit的Element0的OuputScope连接到Speaker
status = AudioUnitSetProperty(remoteIOUnit, kAudioOuputUnitProperty_EnableIO,kAudioUnitScope_Output,busZero, &oneFlag, sizeof(oneFlag));
CheckStatus(status, @"Could not connect to Speaker", YES);
下面是如何启用麦克风的代码
ini
UInt32 busOne = 1;//Element 1
AudioUnitSetProperty(remoteIOUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, busOne, &oneFlag, sizeof(oneFlag));
设置AudioUnit数据格式, 分为输入和输出两个部分
ini
Unit32 bytePerSample = sizeof(Float32);
AudioStreamBasicDescription asbd;
bzer(&asbd, sizeof(asbd));
asbd.mFormatID = kAudioFormatLinearPCM;//指定音频的编码方式为PCM
asbd.mSampleRate = _sampleRate;//声音的采样率
asbd.mChannelsPerFrame = channesl; // 声道数
asbd.mFramesPerPacket = 1; // 每个packat有几个Frame(帧)
asbd.mFormatFlags = kAudioFormatFlagsNativeFloatPacked | kAudioFormatFlagIsNonInterleaved; // 声音表示格式的参数
AudioUnitSetProperty(remoteIOUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, &asbd, sizeof(asbd));
AudioUnit的分类
-
Effect Unit: 类型是kAudioUnitType_Effect,主要提供
声音特效
处理的功能。有下面子类型 -
均衡效果器: (kAudioUnitSubType_NBandEQ), 频带增强或者减弱能量
-
压缩效果器: (kAudioUnitSubType_DynamicsProcessor)
-
混响效果器: (kAudioUnitSubType_Reverb2), 各种声音叠加到一起,让听感有震撼力
-
Mixer Units: 类型是kAudioUnitType_Mixer,主要提供Mix多路声音的功能。
-
I/O Units: 类型是kAudioUnitType_Output,它的用途就像其分类的名字一样,主要提供的就是I/O的功能。
-
Format Converter Units: 类型是kAudioUnitType_FormatConverter,主要用于提供格式转换的功能。
-
Generator Units: 类型是kAudioUnitType_Generator,在开发中我们经常使用它来提供播放器的功能。
构造一个AUGraph
AUGraph的方式将声音采集、声音处理以及声音输出的整个过程管理起来。
- 直接连接方式
scss
AUGraphConnectNodeInput(mPlayerGraph, mPlayerNode, 0, mPlayerIONode,0);
- 回调的方式,在回调函数中获取音频数据
ini
AURenderCallbackStruct renderProc;
renderProc.inputProc = &inputAvailableCallback;
renderProc.inputProcRefCon = (__bridge void*)self;
AUGraphSetNodeInputCallback)mGraph,ioNode,0,&finalRenderProc);
Android音频渲染
有三种方式
-
MediaPlayer: 适合后台长时间播放本地音乐文件或在线的流媒体文件
-
SoundPool: 适合播放短的音频片段,如按键声音,铃声
-
AudioTrack: 适合低延迟的播放,提供了强大控制能力,适合流媒体播放,需要结合解码器来使用
AudioTrack使用
- 配置AudioTrack
java
public AudioTrack(int streamType, int sampeRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes, int mode)
-
streamType: 电话声音, 系统声音, 铃声,音乐声,警告声,通知声
-
sampeRateInHz: 采样率
csharp
// 切换到播放状态
audioTrack.paly();
// 开启播放线程
playerThread = new Thread(new PlayerThread(), "playerThread");
playerThread.start();
class PlayerThread implements Runable {
private short[] samples;
public void run() {
samples = new short[minBufferSize];
while(!isStop) {
int actualSize = decoder.readSamples(samples);
audioTrack.write(samples, actualSize);
}
}
}
OpenSL ES
全称为 Open Sound Libarary for Embedded Systems, 免费,跨平台针对嵌入式设备优化的硬件音频加速API
视频渲染
OpenGL ES 介绍
OpenGL(Open Graphics Library)定义了一个跨编程语言、跨平台的专业图形程序接口。
可用于二位或三位图像的处理与渲染, 它是一个偶功能强大,调用方便的底层图形库
对于嵌入式设备如手机, 提供了 OpenGL ES
在iOS平台上使用EAGL
提供本地平台对OpenGL ES的实现。
OpenGL主要是做图形图像处理的库,尤其是在移动设备上进行图形图像处理,它的性能优势更能体现出来。
GLSL(OpenGL Shading Language)是OpenGL的着色器语言。开发者使用这种语言编写程序运行在GPU上以进行图形的处理和渲染, 分为两个部分
-
Vetex Shader(顶点着色器)
-
Fragment Shader(片元着色器)
OpenGL ES的实践
- OpenGL 渲染管线
相关的术语有:
-
几何图元
: 包括点,直线,三角形,都是通过顶点(vertext)来指定的 -
模型
: 根据几何图元创造的物体 -
渲染
: 根据模型创造图形的过程
而在显卡中,这些像素点可以组织成帧缓冲区
(FrameBuffer)的形式,帧缓冲区保存了图形硬件为了控制屏幕上所有像素的颜色和强度所需要的全部信息。
渲染管线分为以下几个阶段。
阶段一 指定几何对象
几何对象,就是几何图元,根据具体执行的指令绘制几何图形,OpenGL提供给开发者绘制方法glDrawArrays的第一个参数是绘制mode
, 绘制方式
有以下几种
-
GL_POINTS: 以点的形式进行绘制,通常用在绘制粒子效果的场景中
-
GL_Lines: 以线的方式绘制
-
GL_TRAINGLE_STRIP: 以三角形的方式进行绘制, 所有二位图形的绘制都会用到这种方式
阶段二 顶点处理
不论几何对象如何制定的,所有几何数据都会经过这个阶段,根据模型视图和投影矩阵进行变换来改变顶点的位置,根据纹理坐标和纹理矩阵来改变纹理坐标的位置
阶段三 图元组装
顶点处理后,不论是模型的顶点,还是纹理坐标都是已经确定好了,顶点会将纹理组装成图元
阶段四 栅格化操作
图元数据会被分解成更小的单元并对应帧缓冲区的各个像素, 这些单元被称为片元,一个片元可能包含窗口颜色,纹理坐标等属性
阶段五 片元处理
通过纹理坐标取得纹理(texture)中相对应的片元像素值(texel),根据自己的业务处理(如提亮,饱和度调节,高斯模糊等)来变换这个片元的颜色,输出位gl_FragColor
,用于表示修改后的像素最终结果
阶段六 帧缓冲操作
帧缓冲区的写入操作,也是渲染管线的最后一步,负责将最终的像素值
写到帧缓冲区
中
在OpenGL ES 2.0 ,提供了可编程的着色器代替了渲染管线的某一阶段
-
Vertex Shader(顶点着色器) 替换顶点处理阶段
-
Fragment Shader(片元着色器,又称像素着色器)替换片元处理阶段
glFinish和glFlush
提交给OpenGL的绘图指令并不会马上发送给图形硬件,而是放到帧缓冲区,等待缓冲区满了再发送给图形硬件执行。
因此每次写完绘图代码,需要让其立即完成效果时,开发者都需要在代码后面调用glFlush(异步)或 glFinish(同步)函数。
GLSL 语法和内建函数
OpenGL Shading Language: 为了实现着色器功能而面向开发者提供的一种开发语言
- GLSL 修饰符和基本数据类型
语法与C语言非常相似
-
const: 常量
-
attribute: 经常更改的信息,智能在顶点着色器中使用
-
uniform: 不经常更改的信息, 用于顶点着色器和片元着色器中
-
varying: 修复从顶点着色器向片元着色器传递的变量
基本数据类型: int, float,bool
向量类型: 用于传递多个参数
scss
attriibute vec4 position: Vertex Shader中顶点就是向量
uniform lowp mat4 colorMatrix; // 矩阵类型(4x4), mat2就是2x2, mat3就是3x3
glUniformMatrix4fv(mColorMatrixLocation, 1, false, mColorMatrix); // 传递矩阵到实际的shader中
uniform sample2D textSampler; // Fragment Shader中使用的纹理类型
// 客户端收到句柄后, 就可以为它绑定一个纹理
texId = glActiveTexture(GL_TEXTURE0); // 激活第一个纹理
glBindTexture(GL_TEXTURE_2D, texId);
glUniformli(mGLUniformTexture, 0);
attribute vec2 texcoord;
varing vec2 v_texcoord;
vodi main (void)
{
// 计算顶点坐标
v_texcoord = texcoord;
}
创建显卡执行程序
如何让显卡执行这一组Shader,或者用Shader替换掉OpenGL渲染管线那两个阶段
下图描述了如何创建一个显卡的可执行程序,统称为Program

完整的过程如下
csharp
// 1. 创建shader的容器对象,返回一个容器的句柄, shaderType有GL_VERTEX_SHADER和GL_FRAGMENT_SHADER两种类型
GLuint glCreateShader(GLenum shaderType);
// 2. 创建这个shader的源代码,就是图最右边的两个Shader Content, 把开发者编写的着色器程序加载到着色器句柄所关联的内存中
void glShaderSource(GLuint shader, int numOfStrings, const char **strings, int *lenOfStrings)
// 编译该Shader
void glCompileShader(GLuint shader);
// 验证Shader是否编译成功了
void glGetShaderiv(GLuint shader, GLenum pname, GLint*params);
// Vertex Shader 和 Fragment Shader创建后, 下面是如何通过这两个Shader来创建Program(显卡可执行程序)
1. 创建一个对象,作为程序的容器,函数返回容器的句柄
GLunit glCreateProgram(void);
2. 编译的Shader附加到刚刚创建的程序
void glAttachShader(GLuint program, GLuint shader);
3. 链接程序
void glLinkProgram(GLunint program);
OpenGL上下文环境搭建
EGL是双缓冲的工作模式,即有一个Back Frame Buffer和一个Front Frame Buffer。
正常绘制的操作目标都是Back Frame Buffer, 操作完毕之后,调用eglSwapBuffer这个API,将绘制完毕的FrameBuffer交换到Front Frame Buffer并显示出来
Android 平台使用EGL这套机制,EGL承担了为OpenGL提供上下文环境以及窗口管理的职责。
iOS平台不允许直接渲染到屏幕上,因此要使用EAGL的 renderBuffer来代替。
Android 环境搭建
Android平台使用OpenGL ES, 有两种方式
-
直接使用GLSurfaceView, 通过这个方式使用OpenGL ES比较简单,但因为不需要开发者搭建上下文环境,以及创建显示设备,但是不够灵活,很多真正的OpenGL 核心用法(比如共享上下文来达到多线程共同操作一份纹理)都不能直接使用
-
使用EGL的API来搭建OpenGL ES 上下文环境,下面是完整过程
arduino
1. 引入.so库
LOCAL_LDLIBS += -lEGL
LOCAL_LDLIBS += -lGLESv2
// EGL库头文件
#include <EGL/egl.h>
#include <EGL?eglext.h>
// OpenGL ES库头文件
#include <GLES2/g12.h>
#include <GLES2/gl2ext.h>
// 获取EGLDisplay(封装系统物理屏幕的数据类型)作为OpenGL ES渲染的目标
if ((display == eglGetDisplay(EGL_DEFAULT_DISPLAY)) == EGL_NO_DISPLAY) {
return false;
}
// 初始化显示设备
if (!eglInitialize(display, 0, 0)) {
return false;
}
// 配置色彩格式,像素格式,RGBA的表示以及SurfaceType
const EGLint attribs[] = {EGL_BUFFER_SIZE, 32,
EGL_ALPHA_SIZE,8,
EGL_BLUE_SIZE, 8,
EGL_GREEN_SIZE,8,
EGL_RED_SIZE,8,
EGL_RENDERABLE_TYPE, egl_PENGL_ES2_BIT,
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_NONE};
if (!eglChooseConfig(display, attribs, &config, 1, &numConfigs)) {
return false;
}
// 创建OpenGL 上下文环境-EGLContext
EGLint attributes[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
if (!(context = eglCreateContext(display, config, NULL, eglContextAttributes))) {
return false;
}
EGLSurface surface = NULL;
EGLint format;
if (!eglGetConfigAttrib(display, config, EGL_NATIVE_VISUAL_ID, &format)) {
return surface;
}
// 将输出渲染到设备的屏幕上的,将EGL和设备的屏幕连接起来, EGL作为桥的功能, 使用EGLSurface连接起来
ANativeWindow_setBuffersGeometry(_window, 0, 0, format);
if (!(surface = eglCreateWindowSurface(display, config, _window, 0))) {
return;
}
#include <android/native_widnow.h>
#include <android/native_window_jni.h>
// ANATiveWindow API 的代码如下所示
ANativeWindow *window = ANativeWindow_fromSurface(env, surface);
EGLSurface surface;
EGLint PbufferAttributes[] = {EGL_WIDTH, width, EGL_HEIGHT, height, EGL_NONe, EGL_NONE};
egelCreatePbufferSurface(display, config, PbufferAttributes))
// 开新线程,执行OpenGL ES渲染操作,绑定显示设备和上下文环境,然后执行RenderLoop循环了
eglMakeCurrent(display, eglSurface, eglSurface, context);
// 销毁资源, 必须在这个线程销毁
eglDestorySurface(display, eglSurface);
eglDestoryContext(display, context);
iOS 环境搭建
在iOS平台上不允许开发者使用OpenGL ES直接渲染屏幕,必须使 用FrameBuffer与RenderBuffer来进行渲染。
若要使用EAGL,则必须先创建一个RenderBuffer,然后让OpenGL ES渲染到该RenderBuffer上去。 而该RenderBuffer则需要绑定到一个CAEAGLLayer上面去,这样开发者 最后调用EAGLContext的presentRenderBuffer方法,就可以将渲染结果输出到屏幕上去了。
实际上,在调用这个方法时,EAGL也会执行类似于前面的swapBuffer过程,将OpenGL ES渲染的结果绘制到物理屏幕上去(View的Layer)。
过程如下:
- 首先编写一个View类,继承自UIView, 然后重写父类UIView的方法layerClass,并且返回CAEAGLLayer类型
kotlin
+ (Class)layerClass {
return [CAEAGLLayer class];
}
- 然后在改View 的 initWithFrame 方法中,获得layer并且设置参数,包括色彩模式
ini
- (id)initWithFrame:(CGRect)frame {
CAEAGLLayer *eaglLayer = (CAEAGLLayer*)self.layer;
NSDictionary *dict = @{@(kEAGLDrawablePropertyRetainedBacking):@(NO), @(KEAGLDrawablePropertyColorFormat):@(KEAGLColorFormatRGB565)}
[eaglLayer setOpaque:YES];
[eaglLayer setDrawableProperties:dict];
return self;
}
scss
// 3. 构造EAGLContext 与RenderBuffer并绑定到Layer上, 同时必须开辟一个线程中执行如下操作
EAGLContext *_context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
// 绑定操作
[EAGLContext setCurrentContext:_context];
// 创建帧缓冲区
glGenFramebuffers[1, &_FrameBuffer);// 创建绘制缓冲区glGenRenderbuffers(1, &_renderBuffer);// 绑定帧缓冲区到渲染管线glBindFrameBuffer(GL_FRAMEBUFFER, _FrameBuffer);// 绑定绘制缓冲区到渲染管线glBindRenderbuffer(GL_RENDERBUFFER, _renderbuffer);// 为绘制缓冲区分配存储区,将CAEAGLLayer的绘制存储区作为绘制缓冲区[_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer*)self.layer];
// 获取绘制缓冲区的像素宽度
glGetRenderBufferParameteriv(GL_RENDER_BUFFER, GL_RENDER_BUFFER_WIDTH, &_backingWidth);
// 获取绘制缓冲区的像素高度
glGetRenderBufferParameteriv(GL_RENDER_BUFFER, GL_RENDER_BUFFER_HEIGHT, &_backingHeight);
// 将绘制缓冲区绑定到帧缓冲区
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,GL_RENDERBUFFER, _renderbuffer);
// 检查FrameBuffer的status
GLenum status = glCheckFrameBufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE) {
// failed to make complete frame buffer object
}
这样就可以将绘制的结果显示到屏幕上了
OpenGL ES中的纹理
OpenGL中的纹理可以用来表示图像、照片、视频画面等数据。
下面是如何加载一张图片作为OpenGL的纹理, 首先要在显卡建立一个纹理对象
scss
// 第一个参数需要几个纹理对象, 第二个存储创建的纹理对象句柄数组
void glGenTextures(GLSizeei n, GLuint *textures);
// 如果只创建一个纹理对象,可以直接传递纹理id
glGetTextures(1, &texId);
// 绑定纹理对象
// glBindTexture(GLTEXTURE_2D, texId);
但是在OpenGL ES的操作过程中必须告诉OpenGL ES具体操作的是哪一个纹理对象。
一般在视频的渲染与处理的时候使用GL_LINEAR这种过滤方式。
接下来如何将PNG素材的内容放到该纹理对象上, OpenGL的大部分纹理一般都只接受RGBA类型的数据,所以我们需要对PNG这种压缩格式进行解码操作。
arduino
glTextImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0 , GL_RGBA, gL_UNSIGNED_BYTE, pixels);
这样讲该RGBA数组表示的像素内容上传到显卡里面texId所代表的纹理对象中去了, 以后只要使用该纹理对象, 其实表示的就是这个PNG图片
书写Vertext Shader,代码如下
swift
static char *common_vertext_shader = "attribute vec4 postion; \n attribute verc2 texcoord; \n varying vec2 v_texcoord;"
下面是真正的绘制操作
scss
// 规定窗口的大小, screenWidth表示绘制区域的宽度, screenHeight表示高度
glViewport(0, 0, screenWidth, screenHeight);
// 使用显卡绘制程序
glUseProgram(mGLProgId);
// 设置物体坐标
GLFloat vertices[] = {-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f};
glVertexAttribPointer(mGLVertexCorrds, 2, GL_FLOAT, 0,0,vertices};
glEnableVertexAttribArray(mGLVertexCoords);
// 设置纹理坐标
GLFloat texCoords1[] = {0, 0, 1, 0, 0, 1, 1,1};
GLFloat texCoords2[] = {0,1,1,1,0,0,1,0};
glVertexAttribPointer(mGLTextureCorrds, 2, GL_FLOAT, 0,0,texCoords2};
glEnableVertexAttribArray(mGLTextureCoords);
// 指定将要绘制的纹理对象并且传递给对应的FragmentShader;
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texId);
glUniformli(mGLUniformTexture, 0);
// 执行绘制操作
glDrawArrays(GL_TRANGLE_STRIP, 0, 4);
至此就可以在绘制区域(屏幕)绘制出最初的PNG图片了