本文基于上一篇文章 FFmpeg + OpenSLES 实现音频解码播放 ,利用 FFmpeg 对一个 Mp4 文件的音频流进行解码,然后将解码后的 PCM 音频数据进行重采样,最后利用 OpenSLES 进行播放的同时,将 PCM 音频一个通道的数据实时渲染成柱状图。

提取一个通道的音频数据

在上一篇文章,我们构建 OpenSLES 播放器时,对数据格式的定义如下:

SLDataFormat_PCM pcm = {SL_DATAFORMAT_PCM,//format type(SLuint32)2,//channel count 双通道SL_SAMPLINGRATE_44_1,//44100hzSL_PCMSAMPLEFORMAT_FIXED_16,// bits per sample 2字节=16bitSL_PCMSAMPLEFORMAT_FIXED_16,// container sizeSL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,// channel maskSL_BYTEORDER_LITTLEENDIAN // endianness 小端序
};

从上面代码中可以看出,音频驱动接收的 PCM 数据的采样率是 44.1kHz,双通道,采样大小 2 字节。由于我们要渲染的是一个通道的 PCM 数据,所以需要对双通道的数据做一个提取。

如上图所示,解码后的 PCM 数据是 2 个通道的数据交叉存储,当使用指针偏移提取某一通道的数据时,每次偏移的步长是 2 字节 X 通道数 = 4 个字节。

提取某一通道的 PCM 数据方式如下,通过该方式我们可以将一帧音频数据每个通道的数据进行分离。

//小端序存储的音频数据
uint8_t* pByte = audioFrame->data;
for(int i=0; i<audioFrame->dataSize; i++) {short *pShort = pByte + i * 4;//左声道值short leftChannelValue = *pShort;pShort = pByte + i * 4 + 2;//右声道值short rightChannelValue = *pShort;}

另外需要注意的是,数据的存储方式分为大端序和小端序,小端序指低地址存放低位、高地址存放高位,大端序与小端序相反,即低地址存放高位,分离通道数据需要注意。

//大端序存储的音频数据
uint8_t* pByte = audioFrame->data;
for(int i=0; i<audioFrame->dataSize; i++) {short *pShort = pByte + i * 4;//左声道值short leftChannelValue = ((*pShort & 0xFF00) >> 8) | ((*pShort & 0x00FF) << 8);pShort = pByte + i * 4 + 2;//右声道值short rightChannelValue = ((*pShort & 0xFF00) >> 8) | ((*pShort & 0x00FF) << 8);}

OpenGL ES 渲染音频数据

OpenGLES 全称 OpenGL for Embedded Systems ,是三维图形应用程序接口 OpenGL 的子集,本质上是一个跨编程语言、跨平台的编程接口规范,主要应用于嵌入式设备,如手机、平板等。

利用 OpenGL 渲染音频数据,本质上就是根据音频数据的值去构建一组如下图所示的网格,最终渲染成条状图。

根据音频数据的值去构建条状图网格

接下来就是代码实现过程,首先在 Java 层创建 GLSurfaceView 的 Render ,FFMediaPlayer 中增加对应 Native 函数:

private GLSurfaceView.Renderer mAudioGLRender = new GLSurfaceView.Renderer() {@Overridepublic void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {FFMediaPlayer.native_OnAudioVisualSurfaceCreated();}@Overridepublic void onSurfaceChanged(GL10 gl10, int w, int h) {FFMediaPlayer.native_OnAudioVisualSurfaceChanged(w, h);}@Overridepublic void onDrawFrame(GL10 gl10) {FFMediaPlayer.native_OnAudioVisualDrawFrame();}
};public class FFMediaPlayer {static {System.loadLibrary("learn-ffmpeg");}//......//for audio visual renderpublic static native void native_OnAudioVisualSurfaceCreated();public static native void native_OnAudioVisualSurfaceChanged(int width, int height);public static native void native_OnAudioVisualDrawFrame();}

对应 Java 层接口的 JNI :

//可视化音频的渲染接口
JNIEXPORT void JNICALL
Java_com_byteflow_learnffmpeg_media_FFMediaPlayer_native_1OnAudioVisualSurfaceCreated(JNIEnv *env,jclass clazz) {AudioVisualRender::GetInstance()->OnAudioVisualSurfaceCreated();
}JNIEXPORT void JNICALL
Java_com_byteflow_learnffmpeg_media_FFMediaPlayer_native_1OnAudioVisualSurfaceChanged(JNIEnv *env,jclass clazz,jint width,jint height) {AudioVisualRender::GetInstance()->OnAudioVisualSurfaceChanged(width, height);
}JNIEXPORT void JNICALL
Java_com_byteflow_learnffmpeg_media_FFMediaPlayer_native_1OnAudioVisualDrawFrame(JNIEnv *env,jclass clazz) {AudioVisualRender::GetInstance()->OnAudioVisualDrawFrame();
}

Native 层实现音频渲染的类:

#include <LogUtil.h>
#include <GLUtils.h>
#include "AudioVisualRender.h"
#include <gtc/matrix_transform.hpp>
#include <detail/type_mat.hpp>
#include <detail/type_mat4x4.hpp>
#include <render/video/OpenGLRender.h>AudioVisualRender* AudioVisualRender::m_pInstance = nullptr;
std::mutex AudioVisualRender::m_Mutex;AudioVisualRender *AudioVisualRender::GetInstance() {if(m_pInstance == nullptr) {std::unique_lock<std::mutex> lock(m_Mutex);if(m_pInstance == nullptr) {m_pInstance = new AudioVisualRender();}}return m_pInstance;
}void AudioVisualRender::ReleaseInstance() {std::unique_lock<std::mutex> lock(m_Mutex);if(m_pInstance != nullptr) {delete m_pInstance;m_pInstance = nullptr;}}void AudioVisualRender::OnAudioVisualSurfaceCreated() {ByteFlowPrintE("AudioVisualRender::OnAudioVisualSurfaceCreated");if (m_ProgramObj)return;char vShaderStr[] ="#version 300 es\n""layout(location = 0) in vec4 a_position;\n""layout(location = 1) in vec2 a_texCoord;\n""uniform mat4 u_MVPMatrix;\n""out vec2 v_texCoord;\n""void main()\n""{\n""    gl_Position = u_MVPMatrix * a_position;\n""    v_texCoord = a_texCoord;\n""    gl_PointSize = 4.0f;\n""}";char fShaderStr[] ="#version 300 es                                     \n""precision mediump float;                            \n""in vec2 v_texCoord;                                 \n""layout(location = 0) out vec4 outColor;             \n""uniform float drawType;                             \n""void main()                                         \n""{                                                   \n""  if(drawType == 1.0)                               \n""  {                                                 \n""      outColor = vec4(1.5 - v_texCoord.y, 0.3, 0.3, 1.0); \n""  }                                                 \n""  else if(drawType == 2.0)                          \n""  {                                                 \n""      outColor = vec4(1.0, 1.0, 1.0, 1.0);          \n""  }                                                 \n""  else if(drawType == 3.0)                          \n""  {                                                 \n""      outColor = vec4(0.3, 0.3, 0.3, 1.0);          \n""  }                                                 \n""}                                                   \n";//生成着色器程序m_ProgramObj = GLUtils::CreateProgram(vShaderStr, fShaderStr);if (m_ProgramObj == GL_NONE) {LOGCATE("VisualizeAudioSample::Init create program fail");}//设置 MVP Matrix 变换矩阵// Projection matrixglm::mat4 Projection = glm::ortho(-1.0f, 1.0f, -1.0f, 1.0f, 0.1f, 100.0f);//glm::mat4 Projection = glm::frustum(-ratio, ratio, -1.0f, 1.0f, 4.0f, 100.0f);//glm::mat4 Projection = glm::perspective(45.0f, ratio, 0.1f, 100.f);// View matrixglm::mat4 View = glm::lookAt(glm::vec3(0, 0, 4), // Camera is at (0,0,1), in World Spaceglm::vec3(0, 0, 0), // and looks at the originglm::vec3(0, 1, 0)  // Head is up (set to 0,-1,0 to look upside-down));// Model matrixglm::mat4 Model = glm::mat4(1.0f);Model = glm::scale(Model, glm::vec3(1.0f, 1.0f, 1.0f));Model = glm::rotate(Model, 0.0f, glm::vec3(1.0f, 0.0f, 0.0f));Model = glm::rotate(Model, 0.0f, glm::vec3(0.0f, 1.0f, 0.0f));Model = glm::translate(Model, glm::vec3(0.0f, 0.0f, 0.0f));m_MVPMatrix = Projection * View * Model;}void AudioVisualRender::OnAudioVisualSurfaceChanged(int w, int h) {ByteFlowPrintE("AudioVisualRender::OnAudioVisualSurfaceChanged [w, h] = [%d, %d]", w, h);glClearColor(1.0f, 1.0f, 1.0f, 1.0);glViewport(0, 0, w, h);}void AudioVisualRender::OnAudioVisualDrawFrame() {ByteFlowPrintD("AudioVisualRender::OnAudioVisualDrawFrame");glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);std::unique_lock<std::mutex> lock(m_Mutex);if (m_ProgramObj == GL_NONE || m_pAudioBuffer == nullptr) return;UpdateMesh();lock.unlock();// Generate VBO Ids and load the VBOs with dataif(m_VboIds[0] == 0){glGenBuffers(2, m_VboIds);glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[0]);glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * m_RenderDataSize * 6 * 3, m_pVerticesCoords, GL_DYNAMIC_DRAW);glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[1]);glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * m_RenderDataSize * 6 * 2, m_pTextureCoords, GL_DYNAMIC_DRAW);}else{glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[0]);glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(GLfloat) * m_RenderDataSize * 6 * 3, m_pVerticesCoords);glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[1]);glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(GLfloat) * m_RenderDataSize * 6 * 2, m_pTextureCoords);}if(m_VaoId == GL_NONE){glGenVertexArrays(1, &m_VaoId);glBindVertexArray(m_VaoId);glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[0]);glEnableVertexAttribArray(0);glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (const void *) 0);glBindBuffer(GL_ARRAY_BUFFER, GL_NONE);glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[1]);glEnableVertexAttribArray(1);glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), (const void *) 0);glBindBuffer(GL_ARRAY_BUFFER, GL_NONE);glBindVertexArray(GL_NONE);}// Use the program objectglUseProgram(m_ProgramObj);glBindVertexArray(m_VaoId);GLUtils::setMat4(m_ProgramObj, "u_MVPMatrix", m_MVPMatrix);GLUtils::setFloat(m_ProgramObj, "drawType", 1.0f);glDrawArrays(GL_TRIANGLES, 0, m_RenderDataSize * 6);GLUtils::setFloat(m_ProgramObj, "drawType", 2.0f);glDrawArrays(GL_LINES, 0, m_RenderDataSize * 6);}void AudioVisualRender::UpdateAudioFrame(AudioFrame *audioFrame) {if(audioFrame != nullptr) {ByteFlowPrintD("AudioVisualRender::UpdateAudioFrame audioFrame->dataSize=%d", audioFrame->dataSize);std::unique_lock<std::mutex> lock(m_Mutex);if(m_pAudioBuffer != nullptr && m_pAudioBuffer->dataSize != audioFrame->dataSize) {delete m_pAudioBuffer;m_pAudioBuffer = nullptr;delete [] m_pTextureCoords;m_pTextureCoords = nullptr;delete [] m_pVerticesCoords;m_pVerticesCoords = nullptr;}if(m_pAudioBuffer == nullptr) {m_pAudioBuffer = new AudioFrame(audioFrame->data, audioFrame->dataSize);m_RenderDataSize = m_pAudioBuffer->dataSize / RESAMPLE_LEVEL;m_pVerticesCoords = new vec3[m_RenderDataSize * 6]; //(x,y,z) * 6 pointsm_pTextureCoords = new vec2[m_RenderDataSize * 6]; //(x,y) * 6 points} else {memcpy(m_pAudioBuffer->data, audioFrame->data, audioFrame->dataSize);}lock.unlock();}
}//创建和更新条状图的网格,这里一帧音频数据太大,进行了采样
void AudioVisualRender::UpdateMesh() {float dy = 0.25f / MAX_AUDIO_LEVEL;float dx = 1.0f / m_RenderDataSize;for (int i = 0; i < m_RenderDataSize; ++i) {int index = i * RESAMPLE_LEVEL; //RESAMPLE_LEVEL 表示采样间隔short *pValue = (short *)(m_pAudioBuffer->data + index);float y = *pValue * dy;y = y < 0 ? y : -y;vec2 p1(i * dx, 0 + 1.0f);vec2 p2(i * dx, y + 1.0f);vec2 p3((i + 1) * dx, y + 1.0f);vec2 p4((i + 1) * dx, 0 + 1.0f);m_pTextureCoords[i * 6 + 0] = p1;m_pTextureCoords[i * 6 + 1] = p2;m_pTextureCoords[i * 6 + 2] = p4;m_pTextureCoords[i * 6 + 3] = p4;m_pTextureCoords[i * 6 + 4] = p2;m_pTextureCoords[i * 6 + 5] = p3;m_pVerticesCoords[i * 6 + 0] = GLUtils::texCoordToVertexCoord(p1);m_pVerticesCoords[i * 6 + 1] = GLUtils::texCoordToVertexCoord(p2);m_pVerticesCoords[i * 6 + 2] = GLUtils::texCoordToVertexCoord(p4);m_pVerticesCoords[i * 6 + 3] = GLUtils::texCoordToVertexCoord(p4);m_pVerticesCoords[i * 6 + 4] = GLUtils::texCoordToVertexCoord(p2);m_pVerticesCoords[i * 6 + 5] = GLUtils::texCoordToVertexCoord(p3);}
}void AudioVisualRender::Init() {m_VaoId = GL_NONE;m_pTextureCoords = nullptr;m_pVerticesCoords = nullptr;memset(m_VboIds, 0, sizeof(GLuint) * 2);m_pAudioBuffer = nullptr;}//释放内存
void AudioVisualRender::UnInit() {if (m_pAudioBuffer != nullptr) {delete m_pAudioBuffer;m_pAudioBuffer = nullptr;}if (m_pTextureCoords != nullptr) {delete [] m_pTextureCoords;m_pTextureCoords = nullptr;}if (m_pVerticesCoords != nullptr) {delete [] m_pVerticesCoords;m_pVerticesCoords = nullptr;}
}

最后只需要在 OpenSLES 播放器的回调函数(见上篇文章)中调用下面函数即可:

AudioFrame *audioFrame = m_AudioFrameQueue.front();
if (nullptr != audioFrame && m_AudioPlayerPlay) {SLresult result = (*m_BufferQueue)->Enqueue(m_BufferQueue, audioFrame->data, (SLuint32) audioFrame->dataSize);if (result == SL_RESULT_SUCCESS) {//最后只需要在 OpenSLES 播放器的回调函数中调用 UpdateAudioFrame 函数即可AudioVisualRender::GetInstance()->UpdateAudioFrame(audioFrame);m_AudioFrameQueue.pop();delete audioFrame;}}

FFmpeg 开发(04):FFmpeg + OpenGLES 实现音频可视化播放相关推荐

  1. 【Android FFMPEG 开发】FFMPEG 音频重采样 ( 初始化音频重采样上下文 SwrContext | 计算音频延迟 | 计算输出样本个数 | 音频重采样 swr_convert )

    文章目录 I . FFMPEG 播放视频流程 II . FFMPEG 音频重采样流程 III . FFMPEG 音频重采样 IV . FFMPEG 初始化音频重采样上下文 SwrContext V . ...

  2. 【Android FFMPEG 开发】FFMPEG 读取音视频流中的数据到 AVPacket ( 初始化 AVPacket 数据 | 读取 AVPacket )

    文章目录 I . FFMPEG 获取 AVPacket 数据前置操作 II . FFMPEG 获取 AVPacket 数据流程 III . FFMPEG AVPacket 结构体 IV . AVPac ...

  3. 【Android FFMPEG 开发】FFMPEG 获取编解码器 ( 获取编解码参数 | 查找编解码器 | 获取编解码器上下文 | 设置上下文参数 | 打开编解码器 )

    文章目录 博客简介 . FFMPEG 编解码器获取流程 I . FFMPEG 获取音视频流的编解码参数 AVCodecParameters *codecpar II . FFMPEG 查找解码器 av ...

  4. 【Android FFMPEG 开发】FFMPEG ANativeWindow 原生绘制 ( 设置 ANativeWindow 缓冲区属性 | 获取绘制缓冲区 | 填充数据到缓冲区 | 启动绘制 )

    文章目录 I . FFMPEG ANativeWindow 原生绘制 前置操作 II . FFMPEG 原生绘制流程 III . 设置 ANativeWindow 绘制窗口属性 ANativeWind ...

  5. 【Android FFMPEG 开发】FFMPEG ANativeWindow 原生绘制 ( Java 层获取 Surface | 传递画布到本地 | 创建 ANativeWindow )

    文章目录 I . FFMPEG ANativeWindow 原生绘制 II . FFMPEG 原生绘制流程 III . Java 层获取 Surface 画布 IV . 传递 Surface 画布到 ...

  6. 【Android FFMPEG 开发】FFMPEG AVFrame 图像格式转换 YUV - RGBA ( 获取 SwsContext | 初始化图像数据存储内存 | 图像格式转换 )

    文章目录 I . FFMPEG AVFrame 图像数据帧处理 前置操作 II . FFMPEG 解码 AVPacket 数据到 AVFrame 流程 III. FFMPEG 解码前后的图像格式 IV ...

  7. 【Android FFMPEG 开发】FFMPEG 解码 AVPacket 数据到 AVFrame ( AVPacket-解码器 | 初始化 AVFrame | 解码为 AVFrame 数据 )

    文章目录 I . FFMPEG 解码 AVPacket 数据到 AVFrame 前置操作 II . FFMPEG 解码 AVPacket 数据到 AVFrame 流程 III . FFMPEG 发送 ...

  8. 【Android FFMPEG 开发】FFMPEG 获取 AVStream 音视频流 ( AVFormatContext 结构体 | 获取音视频流信息 | 获取音视频流个数 | 获取音视频流 )

    文章目录 博客简介 . FFMPEG 音视频流 获取流程 I . FFMPEG 获取音视频流信息 ( AVFormatContext 结构体 ) II . FFMPEG 获取 音视频流 数量 III ...

  9. FFmpeg开发(三):音频播放器的实现

    OpenSL基础 OpenSLES接口可以直接载Native层处理音频数据,减少了Java层到Native层在采集.播放和编解码过程中的数据拷贝. OpenSLES文档:OpenSL_ES_Speci ...

最新文章

  1. android焦距跟实际距离,手机等效焦距一算便知_nubia Z5S Mini_手机Android频道-中关村在线...
  2. jenkins执行xctool命令出现command not found问题解决方法
  3. ov5640帧率配置_赛博朋克2077 优化设置大全!帧数50暴涨100
  4. [蓝桥杯][算法提高VIP]线段和点(排序+贪心)
  5. CentOS6.5 常用命令
  6. Ubuntu添加开机自动启动程序的方法 / 修改 Ubuntu 启动顺序
  7. LSA7 通告默认路由(1)
  8. linux jira mysql_JIRA配置连接MySQL数据库
  9. ×××技术漫谈之IPSec(附MPLS)
  10. 【动态规划笔记】背包方案数量
  11. NHibernate Configuring
  12. 开发工具总结(6)之Android Studio模板配置详解(提高开发效率必备技能)
  13. 7-12 文件传输 (25 point(s))
  14. markdown 书写代码
  15. 微信小程序常见的UI框架/组件库总结
  16. 01. Introdunction to Zero Knowlege -- Alon Rosen[零知识介绍]
  17. truetype字体怎么转换成普通字体_【转】TrueType(二)字体格式解析
  18. Spring @Value读取配置文件
  19. 云计算方向研究热点、 云计算有怎样的发展前景?
  20. Linux学习之——/etc/sysconfig目录

热门文章

  1. 论文阅读笔记(二):Bridging Video-text Retrieval with Multiple Choice Questions
  2. excel如何从身份证号码中提取虚岁?
  3. Windows10恢复任务栏中的“中/英”切换图标
  4. 2022年广西二级建造师建设工程安全生产法律制度多选题及答案
  5. 第87节:Java中的Bootstrap基础与SQL入门
  6. 测试工具研发_(2)测试框架驱动之布局调整
  7. Android-开发技术周报,你掌握了多少
  8. compilation terminated.
  9. 韩国牛奶进口税率及进口牛奶那些流程事项2021-08-19
  10. 使用这个方法,让你的表格一目了然,建议收藏