ffmpeg开发之旅(4):MP3编码格式分析与lame库编译封装

原文链接:http://blog.csdn.net/andrexpert/article/77683776

一、Mp3编码格式分析

MP3,全称MPEG Audio Layer3,是一种高效的计算机音频编码方案,它以较大的压缩比(1:10至1:12)将音频文件转换成较小的扩展名为.mp3的文件,且能基本保持原文件的音质。假如有一个4分钟的CD音质的WAV音频,其音频参数为44.1kHz抽样、立体声、采样精度为16位(2字节),那么该音频所占空间为441000*2(声道)*2(字节)*60(秒)*4(分钟)=40.4MB,而对于MP3格式来说,MP3音频只占4MB左右,有利于存储和网络传输。
1. MP3文件结构
MP3文件有由帧(frame)构成的,帧是MP3文件最小的组成单位。MP3音频文件本身没有头部,当希望读取有关MP3音频文件的信息时,可以读取第一帧的头部信息,因此可以切割MP3音频文件的任何部分进行正确播放。整个MP3文件结构大体包括三部分,即TAG_V2(ID3V2)、Frame、TAG_V1(ID3V1),具体描述如下:

2. MP3帧格式
     每个帧都是独立的,它由帧头、附加信息和声音数据组成,其长度随位率的不同而不等,通常每个帧的播放时间为0.026秒。MP3帧结构如下:

     每帧的帧头占4字节(32位),帧头后面可能有两个字节的CRC校验,这两个字节的是否存在取决于帧头部的第16bit,如果为0,则帧头后面无校验,为1则有校验。帧头结构如下:

[cpp] view plaincopy
  1. typedefstruct-tagHeader{
  2. unsigned int sync:        占11位   //同步信息
  3. unsigned int version:    2;    //版本
  4. unsigned int layer:          2;  //层
  5. unsigned int error2protection:     1;   //CRC校正
  6. unsigned int bit2rate2index:        4;   //位率索引
  7. unsigned int sample2rate2index: 2;   //采样率索引
  8. unsigned int padding:                  1;   //空白字
  9. unsigned int extension:               1;    //私有标志
  10. unsigned int channel2mode:       2;   //立体声模式
  11. unsigned int modeextension:      2   ;//保留
  12. unsigned int copyright:                1;  //版权标志
  13. unsigned int original:                   1;  //原始媒体
  14. unsigned int emphasis:               2   ;//强调方式
  15. } HEADER;

其中,sync为同步信息,占11位,全部被设置为1;channel2mode为立体声通道模式,占2为,11表示Single立体声(Mono);其他参数请看 这篇文章 。
二、lame编译与封装
1. Lame库简介

Lame是Mike Cheng于1998年发起的一个开源项目,是目前最好的MP3编码引擎。Lame编码出来的MP3音色纯厚、空间宽广、低音清晰、细节表现良好,它独创的心理音响模型技术保证了CD音频还原的真实性,配合VBR和ABR参数,音质几乎可以媲美CD音频,但文件体积却非常小。

最新版下载: https://sourceforge.net/projects/lame/files/lame/3.99/
2. Lame库编译与封装

(1) 移植Lame库到Android工程
      a. 解压lame-3.99.5,将源码中的libmp3lame目录拷贝到Android工程的cpp目录下;
      b. 将libmp3lame重命名为lame,并删除i386目录、vector目录、depcomp、lame.rc、logoe.ico、Makefile.am、Makefile.in文件;
      c. 拷贝源码中inlude目录下lame.h文件到Android工程cpp目录下lame目录中,lame.h头文件包含了所有调用函数的声明;
      d. 配置CMakeLists.txt文件
          set(SRC_DIR src/main/cpp/lame)
          include_directories(src/main/cpp/lame)
         aux_source_directory(src/main/cpp/lame SRC_LIST)
         add_library(...... ${SRC_LIST})
(2) LameMp3.java,创建调用lame库函数的native方法

[java] view plaincopy
  1. /** JNI调用lame库实现mp3文件封装
  2. * Created by Jiangdg on 2017/6/9.
  3. */
  4. public class LameMp3 {
  5. // 静态加载共享库LameMp3
  6. static {
  7. System.loadLibrary("LameMp3");
  8. }
  9. /** 初始化lame库,配置相关信息
  10. *
  11. * @param inSampleRate pcm格式音频采样率
  12. * @param outChannel pcm格式音频通道数量
  13. * @param outSampleRate mp3格式音频采样率
  14. * @param outBitRate mp3格式音频比特率
  15. * @param quality mp3格式音频质量,0~9,最慢最差~最快最好
  16. */
  17. public native static void lameInit(int inSampleRate, int outChannel,int outSampleRate, int outBitRate, int quality);
  18. /** 编码pcm成mp3格式
  19. *
  20. * @param letftBuf  左pcm数据
  21. * @param rightBuf 右pcm数据,如果是单声道,则一致
  22. * @param sampleRate 读入的pcm字节大小
  23. * @param mp3Buf 存放mp3数据缓存
  24. * @return 编码数据字节长度
  25. */
  26. public native static int lameEncode(short[] letftBuf, short[] rightBuf,int sampleRate, byte[] mp3Buf);
  27. /** 保存mp3音频流到文件
  28. *
  29. * @param mp3buf mp3数据流
  30. * @return 数据流长度rty
  31. */
  32. public native static int lameFlush(byte[] mp3buf);
  33. /**
  34. * 释放lame库资源
  35. */
  36. public native static void lameClose();
  37. }

讲解一下 :通过查看Lame库的API文档(lame-3.99.5\API)可知,使用Lame封装Mp3需要经历四个步骤,即初始化lame引擎、编码pcm为mp3数据帧、写入文件、释放lame引擎资源。因此,在LameMp3 .java中,我们定义与之对应的native方法以便java层调用,最终生成所需的mp3格式文件。
(3) LameMp3.c

[java] view plaincopy
  1. // 本地实现
  2. // Created by jianddongguo on 2017/6/14.
  3. #include <jni.h>
  4. #include "LameMp3.h"
  5. #include "lame/lame.h"
  6. // 声明一个lame_global_struct指针变量
  7. // 可认为是一个全局上下文
  8. static lame_global_flags *gfp = NULL;
  9. JNIEXPORT void JNICALL
  10. Java_com_teligen_lametomp3_LameMp3_lameInit(JNIEnv *env, jclass type, jint inSampleRate,
  11. jint outChannelNum, jint outSampleRate, jint outBitRate,
  12. jint quality) {
  13. if(gfp != NULL){
  14. lame_close(gfp);
  15. gfp = NULL;
  16. }
  17. //  初始化编码器引擎,返回一个lame_global_flags结构体类型指针
  18. //  说明编码所需内存分配完成,否则,返回NULL
  19. gfp = lame_init();
  20. LOGI("初始化lame库完成");
  21. // 设置输入数据流的采样率,默认为44100Hz
  22. lame_set_in_samplerate(gfp,inSampleRate);
  23. // 设置输入数据流的通道数量,默认为2
  24. lame_set_num_channels(gfp,outChannelNum);
  25. // 设置输出数据流的采样率,默认为0,单位KHz
  26. lame_set_out_samplerate(gfp,outSampleRate);
  27. lame_set_mode(gfp,MPEG_mode);
  28. // 设置比特压缩率,默认为11
  29. lame_set_brate(gfp,outBitRate);
  30. // 编码质量,推荐2、5、7
  31. lame_set_quality(gfp,quality);
  32. // 配置参数
  33. lame_init_params(gfp);
  34. LOGI("配置lame参数完成");
  35. }
  36. JNIEXPORT jint JNICALL
  37. Java_com_teligen_lametomp3_LameMp3_lameFlush(JNIEnv *env, jclass type, jbyteArray mp3buf_) {
  38. jbyte *mp3buf = (*env)->GetByteArrayElements(env, mp3buf_, NULL);
  39. jsize len = (*env)->GetArrayLength(env,mp3buf_);
  40. // 刷新pcm缓存,以"0"填充保证最后几帧的完整
  41. // 刷新mp3缓存,返回最后的几帧
  42. int resut = lame_encode_flush(gfp,        // 全局上下文
  43. mp3buf, // 指向mp3缓存的指针
  44. len);  // 有效mp3数据长度
  45. (*env)->ReleaseByteArrayElements(env, mp3buf_, mp3buf, 0);
  46. LOG_I("写入mp3数据到文件,返回帧数=%d",resut);
  47. return  resut;
  48. }
  49. JNIEXPORT void JNICALL
  50. Java_com_teligen_lametomp3_LameMp3_lameClose(JNIEnv *env, jclass type) {
  51. // 释放所占内存资源
  52. lame_close(gfp);
  53. gfp = NULL;
  54. LOGI("释放lame资源");
  55. }
  56. JNIEXPORT jint JNICALL
  57. Java_com_teligen_lametomp3_LameMp3_lameEncode(JNIEnv *env, jclass type, jshortArray letftBuf_,
  58. jshortArray rightBuf_, jint sampleRate,
  59. jbyteArray mp3Buf_) {
  60. if(letftBuf_ == NULL || mp3Buf_ == NULL){
  61. LOGI("letftBuf和rightBuf 或mp3Buf_不能为空");
  62. return -1;
  63. }
  64. jshort *letftBuf = NULL;
  65. jshort *rightBuf = NULL;
  66. if(letftBuf_ != NULL){
  67. letftBuf = (*env)->GetShortArrayElements(env, letftBuf_, NULL);
  68. }
  69. if(rightBuf_ != NULL){
  70. rightBuf = (*env)->GetShortArrayElements(env, rightBuf_, NULL);
  71. }
  72. jbyte *mp3Buf = (*env)->GetByteArrayElements(env, mp3Buf_, NULL);
  73. jsize readSizes = (*env)->GetArrayLength(env,mp3Buf_);
  74. // 将PCM数据编码为mp3
  75. int result = lame_encode_buffer(gfp, // 全局上下文
  76. letftBuf,    // 左通道pcm数据
  77. rightBuf,   // 右通道pcm数据
  78. sampleRate, // 通道数据流采样率
  79. mp3Buf, // mp3数据缓存起始地址
  80. readSizes);      // 缓存地址中有效mp3数据长度
  81. // 释放资源
  82. if(letftBuf_ != NULL){
  83. (*env)->ReleaseShortArrayElements(env, letftBuf_, letftBuf, 0);
  84. }
  85. if(rightBuf_ != NULL){
  86. (*env)->ReleaseShortArrayElements(env, rightBuf_, rightBuf, 0);
  87. }
  88. (*env)->ReleaseByteArrayElements(env, mp3Buf_, mp3Buf, 0);
  89. LOG_I("编码pcm为mp3,数据长度=%d",result);
  90. return  result;
  91. }

讲解一下 :通过查看lame.h源码,gfp 为结构体lame_global_struct的一个指针变量,该变量用于指向该结构体。lame_global_struct结构体声明了编码所需的各种参数,具体代码如下:
lame_global_flags *gfp = NULL;
typedef struct lame_global_struct lame_global_flags;
struct lame_global_struct {
    unsigned int class_id;
    unsigned long num_samples; 
    int     num_channels;    
    int     samplerate_in;  
    int     samplerate_out;    brate;          
    float   compression_ratio; 
    .....
}
另外,在配置lame编码引擎时,有一个lame_set_quality函数用来设定编码的质量。也许你会问,音频编码质量一般不是由比特率决定的,为什么还需要这个设置?嗯,比特率决定编码质量是没错的,这里的参数主要是用来选择编码处理的算法,不同的算法处理的效果和速度是不一样的。比如,当quality为0时,选择的算法是最好的,但处理的速度是最慢的;当quality为9时,选择的算法是最差的,但是速度是最快的。通常,官方推荐以下三种设置,即:
         quality= 2     质量接近最好,速度不是很慢;
         quality=5     质量很好,速度还行;
         quality=7     质量良好, 速度很快;
(4) CMakeList.txt

[html] view plaincopy
  1. #指定所需的Cmake最低版本
  2. cmake_minimum_required(VERSION 3.4.1)
  3. #指定源码路径,即将src/main/cpp/lame路径赋值给SRC_DIR
  4. set(SRC_DIR src/main/cpp/lame)
  5. # 指定头文件路径
  6. include_directories(src/main/cpp/lame)
  7. # 将src/main/cpp/lame目录下的所有文件名赋值给SRC_LIST
  8. aux_source_directory(src/main/cpp/lame SRC_LIST)
  9. # add_library:指定生成库文件,包括三个参数:
  10. # LameMp3为库文件的名称;SHARED表示动态链接库;
  11. # src/main/cpp/LameMp3.c和${SRC_LIST}指定生成库文件所需的源文件
  12. #其中,${}的作用是引入src/main/cpp/lame目录下的所有源文件
  13. add_library(
  14. LameMp3
  15. SHARED
  16. src/main/cpp/LameMp3.c ${SRC_LIST})
  17. #在指定的目录中搜索库log,并将其路径保存到变量log-lib中
  18. find_library( # Sets the name of the path variable.
  19. log-lib
  20. # Specifies the name of the NDK library that
  21. # you want CMake to locate.
  22. log )
  23. # 将库${log-lib} 链接到LameMp3动态库中,包括两个参数
  24. #LameMp3为目标库
  25. # ${log-lib}为要链接的库
  26. target_link_libraries( # Specifies the target library.
  27. LameMp3
  28. # Links the target library to the log library
  29. # included in the NDK.
  30. ${log-lib} )

讲解一下 :Cmake是一个跨平台的编译工具,它允许使用简单的语句来描述所有平台的编译过程,并输出各种类型的Makefile或Project文件。Cmake所有的语句命令都写在CMakeLists.txt文件中,主要规则如下:
    a. 在Cmake中,注释由#字符开始到此行的结束;
    b. 命令不区分大小写,参数需区分大小写;
    c. 命令由命令名、参数列表组成,参数间使用空格进行分隔;
(5) build.gradle(Module app),选择编译平台

[html] view plaincopy
  1. android {
  2. defaultConfig {
  3. // ...代码省略
  4. externalNativeBuild {
  5. cmake {
  6. cppFlags ""
  7. }
  8. }
  9. // 选择编译平台
  10. ndk{
  11. abiFilters 'x86', 'x86_64', 'armeabi', 'armeabi-v7a','arm64-v8a'
  12. }
  13. }
  14. // ...代码省略
  15. externalNativeBuild {
  16. cmake {
  17. path "CMakeLists.txt"
  18. }
  19. }
  20. }

三、开源项目:Lame4Mp3
       Lame4Mp3是基于Lame库实现的开源项目,本项目结合Android官方提供的MediaCodec API,可以满足将PCM数据流编码为AAC或MP3格式数据,并且支持AAC和Mp3同时编码,适用于本地录制mp3/aac文件和在Android直播中进行边播边录(mp3)等场合。使用方法和源码分析如下:
1. 添加依赖
(1) 在工程build.gradle中添加

[html] view plaincopy
  1. allprojects {
  2. repositories {
  3. ...
  4. maven { url 'https://jitpack.io' }
  5. }
  6. }

(2) 在module的gradle中添加

[html] view plaincopy
  1. dependencies {
  2. compile 'com.github.jiangdongguo:Lame4Mp3:v1.0.0'
  3. }

2. Lame4Mp3使用方法
(1) 配置参数

[java] view plaincopy
  1. Mp3Recorder mMp3Recorder = Mp3Recorder.getInstance();
  2. // 配置AudioRecord参数
  3. mMp3Recorder.setAudioSource(Mp3Recorder.AUDIO_SOURCE_MIC);
  4. mMp3Recorder.setAudioSampleRare(Mp3Recorder.SMAPLE_RATE_8000HZ);
  5. mMp3Recorder.setAudioChannelConfig(Mp3Recorder.AUDIO_CHANNEL_MONO);
  6. mMp3Recorder.setAduioFormat(Mp3Recorder.AUDIO_FORMAT_16Bit);
  7. // 配置Lame参数
  8. mMp3Recorder.setLameBitRate(Mp3Recorder.LAME_BITRATE_32);
  9. mMp3Recorder.setLameOutChannel(Mp3Recorder.LAME_OUTCHANNEL_1);
  10. // 配置MediaCodec参数
  11. mMp3Recorder.setMediaCodecBitRate(Mp3Recorder.ENCODEC_BITRATE_1600HZ);
  12. mMp3Recorder.setMediaCodecSampleRate(Mp3Recorder.SMAPLE_RATE_8000HZ);
  13. // 设置模式
  14. //  Mp3Recorder.MODE_AAC 仅编码得到AAC数据流
  15. //  Mp3Recorder.MODE_MP3 仅编码得到Mp3文件
  16. //  Mp3Recorder.MODE_BOTH 同时编码
  17. mMp3Recorder.setMode(Mp3Recorder.MODE_BOTH);

(2) 开始编码

[java] view plaincopy
  1. mMp3Recorder.start(filePath, fileName, new Mp3Recorder.OnAACStreamResultListener() {
  2. @Override
  3. public void onEncodeResult(byte[] data, int offset, int length, long timestamp) {
  4. Log.i("MainActivity","acc数据流长度:"+data.length);
  5. }
  6. });

(3) 停止编码

[java] view plaincopy
  1. mMp3Recorder.stop();

3. Lame4Mp3源码解析
     Mp3Recorder.java中主要包括三个功能块:PCM数据采集、AAC编码、Mp3编码,其中,PCM数据采集和AAC编码在以前的博文中有详细剖析,所以这里只着重解析Mp3编码,核心代码如下:

[java] view plaincopy
  1. public void start(final String filePath, final String fileName,final OnAACStreamResultListener listener){
  2. this.listener = listener;
  3. new Thread(new Runnable() {
  4. @Override
  5. public void run() {
  6. try {
  7. if(!isRecording){
  8. // 第一步:初始化lame引擎
  9. initLameMp3();
  10. initAudioRecord();
  11. initMediaCodec();
  12. }
  13. int readBytes = 0;
  14. byte[] audioBuffer = new byte[2048];
  15. byte[] mp3Buffer = new byte[1024];
  16. // 如果文件路径不存在,则创建
  17. if(TextUtils.isEmpty(filePath) || TextUtils.isEmpty(fileName)){
  18. Log.i(TAG,"文件路径或文件名为空");
  19. return;
  20. }
  21. File file = new File(filePath);
  22. if(! file.exists()){
  23. file.mkdirs();
  24. }
  25. String mp3Path = file.getAbsoluteFile().toString()+File.separator+fileName+".mp3";
  26. FileOutputStream fops = null;
  27. try {
  28. while(isRecording){
  29. readBytes = mAudioRecord.read(audioBuffer,0,bufferSizeInBytes);
  30. Log.i(TAG,"读取pcm数据流,大小为:"+readBytes);
  31. if(readBytes >0 ){
  32. if(mode == MODE_AAC || mode == MODE_BOTH){
  33. // 将PCM编码为AAC
  34. encodeBytes(audioBuffer,readBytes);
  35. }
  36. if(mode == MODE_MP3 || mode == MODE_BOTH){
  37. // 打开mp3文件输出流
  38. if(fops == null){
  39. try {
  40. fops = new FileOutputStream(mp3Path);
  41. } catch (FileNotFoundException e) {
  42. e.printStackTrace();
  43. }
  44. }
  45. // 将byte[] 转换为 short[]
  46. // 将PCM编码为Mp3,并写入文件
  47. short[] data = transferByte2Short(audioBuffer,readBytes);
  48. int encResult = LameMp3.lameEncode(data,null,data.length,mp3Buffer);
  49. Log.i(TAG,"lame编码,大小为:"+encResult);
  50. if(encResult != 0){
  51. try {
  52. fops.write(mp3Buffer,0,encResult);
  53. } catch (IOException e) {
  54. e.printStackTrace();
  55. }
  56. }
  57. }
  58. }
  59. }
  60. // 录音完毕
  61. if(fops != null){
  62. int flushResult =  LameMp3.lameFlush(mp3Buffer);
  63. Log.i(TAG,"录制完毕,大小为:"+flushResult);
  64. if(flushResult > 0){
  65. try {
  66. fops.write(mp3Buffer,0,flushResult);
  67. } catch (IOException e) {
  68. e.printStackTrace();
  69. }
  70. }
  71. try {
  72. fops.close();
  73. } catch (IOException e) {
  74. e.printStackTrace();
  75. }
  76. }
  77. }finally {
  78. Log.i(TAG,"释放AudioRecorder资源");
  79. stopAudioRecorder();
  80. stopMediaCodec();
  81. }
  82. }finally {
  83. Log.i(TAG,"释放Lame库资源");
  84. stopLameMp3();
  85. }
  86. }
  87. }).start();
  88. }

从代码可以看出,使用lame引擎编码pcm得到mp3数据,将经历四个步骤:初始化引擎、编码、写入文件、释放内存资源,这个过程与之前我们详细分析的流程一致。但是,有一点需要注意的是,当同时编码AAC和Mp3时,向MediaCodec和Lame引擎输入PCM数据流的方式是不一样的,前者只接受byte[]存储的数据,后者接收short[]存储的数据。也就是说,如果将采集的pcm数据以byte[]来存储,我们需要将其转换为short[],并且需要注意大小端的问题。具体代码如下:

[java] view plaincopy
  1. private short[] transferByte2Short(byte[] data,int readBytes){
  2. // byte[] 转 short[],数组长度缩减一半
  3. int shortLen = readBytes / 2;
  4. // 将byte[]数组装如ByteBuffer缓冲区
  5. ByteBuffer byteBuffer = ByteBuffer.wrap(data, 0, readBytes);
  6. // 将ByteBuffer转成小端并获取shortBuffer
  7. // 小端:数据的高字节保存到内存的高地址中,数据的低字节保存到内存的低地址中
  8. ShortBuffer shortBuffer = byteBuffer.order(ByteOrder.LITTLE_ENDIAN).asShortBuffer();
  9. short[] shortData = new short[shortLen];
  10. shortBuffer.get(shortData, 0, shortLen);
  11. return shortData;
  12. }

GitHub地址:https://github.com/jiangdongguo/Lame4Mp3   欢迎大家star~( 附上LameToMp3 NDK工程)

ffmpeg开发之旅(4):MP3编码格式分析与lame库编译封装相关推荐

  1. Android直播开发之旅(4):MP3编码格式分析与lame库编译封装

    转载请声明出处:http://blog.csdn.net/andrexpert/article/77683776 一.Mp3编码格式分析 MP3,全称MPEG Audio Layer3,是一种高效的计 ...

  2. ffmpeg开发之旅(3):AAC编码格式分析与MP4文件封装(MediaCodec+MediaMuxer)

    ffmpeg开发之旅(3):AAC编码格式分析与MP4文件封装(MediaCodec+MediaMuxer) (原文链接:http://blog.csdn.net/andrexpert/article ...

  3. FFmpeg开发之旅(二)---音频解码

    [写在前面] 前面我介绍了视频解码的流程,发现基础讲得有点少. 因此这里附上一些额外的基础内容:理解PCM音频数据格式 本篇主要内容: 1.FFmpeg音频解码基本流程 2.libswresample ...

  4. FFmpeg开发之旅(三)---理解过滤图并使用字幕过滤器

    [写在前面] 首先,抛开字幕本身的格式不说. 一般的字幕分三种,内封字幕.内嵌字幕和外挂字幕. 而本篇所讲的是外挂字幕,主要内容有: 1.FFmpeg过滤图基础. 2.使用FFmpeg字幕过滤器添加字 ...

  5. Android直播开发之旅(3):AAC编码格式分析与MP4文件封装(MediaCodec+MediaMuxer)

    Android直播开发之旅(3):AAC编码格式分析与MP4文件封装(MediaCodec+MediaMuxer) (码字不易,转载请声明出处:http://blog.csdn.net/andrexp ...

  6. Android直播开发之旅(17):使用FFmpeg提取MP4中的H264和AAC

    最近在开发中遇到了一个问题,即无法提取到MP4中H264流的关键帧进行处理,且保存到本地的AAC音频也无法正常播放.经过调试分析发现,这是由于解封装MP4得到的H264和AAC是ES流,它们缺失解码时 ...

  7. 音视频开发之旅(36) -FFmpeg +OpenSL ES实现音频解码和播放

    目录 OpenSL ES基本介绍 OpenSL ES播放音频流程 代码实现 遇到的问题 资料 收获 上一篇我们通过AudioTrack实现了FFmpeg解码后的PCM音频数据的播放,在Android上 ...

  8. 音视频开发之旅(34) - 基于FFmpeg实现简单的视频解码器

    目录 FFmpeg解码过程流程图和关键的数据结构 mp4通过FFmpeg解码YUV裸视频数据 遇到的问题 资料 收获 一.FFmpeg解码过程流程图和关键的数据结构 FFmpeg解码涉及的知识点比较多 ...

  9. Android直播开发之旅(13):使用FFmpeg+OpenSL ES播放PCM音频

    文章目录 1. OpenSL ES原理 1.1 OpenSL ES核心API讲解 1.1.1 对象(Object)与接口(Interface) 1.1.2 [OpenSL ES的状态机制](https ...

最新文章

  1. fsLayui缓存使用
  2. PHP 每小时抽奖,php分时间段的抽奖程序代码
  3. java培训第一阶段测试总结_java学习的第一阶段总结
  4. python观察日志(part22)--设置工作目录及文件读取
  5. 锻炼编程能力的10个游戏:通关既巅峰!
  6. 下一代软件工程的思考与点滴实践
  7. 小程序直传阿里云OSS 踩坑
  8. 安装拼音加加时出现问题
  9. 什么事IPC(Inter-Process Communication,进程间通信)
  10. pandas数据拼接
  11. 预先下载的keras库中神经网络模型指定存放路径及如何上传的问题
  12. pythonxy是什么东西_无状态以太坊:二进制状态树实验
  13. 【英译中】如何拍好沙滩照2——2014年7月24日
  14. 人工智能----知识与知识表示
  15. 「通过Docs学Python」(一)前言
  16. 怎样清除bios密码
  17. 利用python和tushare,统计股市每天上涨的概率
  18. 评论:26岁成都唐爽发现成果惊动奥巴马--意外发现新材料极可能成下一代电脑芯片...
  19. HDU 2549 壮志难酬(easy)
  20. 工业RFID应用(三):RFID技术与智能仓储子母穿梭车的应用解决方案

热门文章

  1. 计网实验c/c++ 电子邮件客户端程序实现发送接收邮件
  2. PostgreSQL Java 开发者手册
  3. mysql8清理二进制日志参数binlog_expire_logs_seconds
  4. 使用 Python+request 实现登入淘宝
  5. c语言中数学运算符,运算符在数学和C语言中的区别.doc
  6. BERT微调做中文文本分类
  7. (GCC)STM32基础详解之内存分配
  8. 数据库的文件服务器配置,服务器数据库文件配置
  9. vue3-video-play视频组件的使用(一)——基本使用 HTML5中Video标签的属性、方法和事件汇总
  10. 史上最全最详细2014年初mac air 128G硬盘 4G内存 更换512G硬盘及更新最新操作系统macOS Big Sur操作手册