音频编码
这里借用《音视频开发进阶指南:基于Android与Ios的实践》一书里对各种音频编码的介绍。
WAV编码
PCM(脉冲编码调制)是Pulse Code Modulation的缩写。前面已经介绍过PCM大致的工作流程,而WAV编码的一种实现(有多种实现方式,但是都不会进行压缩操作)就是在PCM数据格式的前面加上44字节,分别用来描述PCM的采样率、声道数、数据格式等信息。
特点:音质非常好,大量软件都支持。
适用场合:多媒体开发的中间文件、保存音乐和音效素材。
MP3编码
MP3具有不错的压缩比,使用LAME编码(MP3编码格式的一种实现)的中高码率的MP3文件,听感上非常接近源WAV文件,当然在不同的应用场景下,应该调整合适的参数以达到最好的效果。
特点:音质在128Kbit/s以上表现还不错,压缩比比较高,大量软件和硬件都支持,兼容性好。
适用场合:高比特率下对兼容性有要求的音乐欣赏。
AAC编码
AAC是新一代的音频有损压缩技术,它通过一些附加的编码技术(比如PS、SBR等),衍生出了LC-AAC、HE-AAC、HE-AAC v2三种主要的编码格式。LC-AAC是比较传统的AAC,相对而言,其主要应用于中高码率场景的编码(≥80Kbit/s);HE-AAC(相当于AAC+SBR)主要应用于中低码率场景的编码(≤80Kbit/s);而新近推出的HE-AAC v2(相当于AAC+SBR+PS)主要应用于低码率场景的编码(≤48Kbit/s)。事实上大部分编码器都设置为≤48Kbit/s自动启用PS技术,而>48Kbit/s则不加PS,相当于普通的HE-AAC。
特点:在小于128Kbit/s的码率下表现优异,并且多用于视频中的音频编码。
适用场合:128Kbit/s以下的音频编码,多用于视频中音频轨的编码。
Ogg编码
Ogg是一种非常有潜力的编码,在各种码率下都有比较优秀的表现,尤其是在中低码率场景下。Ogg除了音质好之外,还是完全免费的,这为Ogg获得更多的支持打好了基础。Ogg有着非常出色的算法,可以用更小的码率达到更好的音质,128Kbit/s的Ogg比192Kbit/s甚至更高码率的MP3还要出色。但目前因为还没有媒体服务软件的支持,因此基于Ogg的数字广播还无法实现。Ogg目前受支持的情况还不够好,无论是软件上的还是硬件上的支持,都无法和MP3相提并论。
特点:可以用比MP3更小的码率实现比MP3更好的音质,高中低码率下均有良好的表现,兼容性不够好,流媒体特性不支持。
适用场合:语音聊天的音频消息场景。
使用LAME转换pcm文件到mp3
按照前面编译lame库的博客做下来,现在工程里面已经可以通过 jni 的方式,使用lame的相关方法了。
新建 Mp3Encoder.java 文件,添加相关的 native方法。
public class Mp3Encoder { public native int init(String pcmPath, int audioChannels, int bitRate, int sampleRate, String mp3Path); public native void encode(); public native void destroy(); }
生成 Mp3Encoder.java 对应的头文件(.h文件,使用javah命令自动生成的)com_wyt_myapplication_Mp3Encoder.h ,要是忘了,再看看前面的两篇博客。
/* DO NOT EDIT THIS FILE - it is machine generated */#include <jni.h>/* Header for class com_wyt_myapplication_Mp3Encoder */#ifndef _Included_com_wyt_myapplication_Mp3Encoder#define _Included_com_wyt_myapplication_Mp3Encoder#ifdef __cplusplusextern "C" {#endif/* * Class: com_wyt_myapplication_Mp3Encoder * Method: init * Signature: (Ljava/lang/String;IIILjava/lang/String;)I */JNIEXPORT jint JNICALL Java_com_wyt_myapplication_Mp3Encoder_init (JNIEnv *, jobject, jstring, jint, jint, jint, jstring);/* * Class: com_wyt_myapplication_Mp3Encoder * Method: encode * Signature: ()V */JNIEXPORT void JNICALL Java_com_wyt_myapplication_Mp3Encoder_encode (JNIEnv *, jobject);/* * Class: com_wyt_myapplication_Mp3Encoder * Method: destroy * Signature: ()V */JNIEXPORT void JNICALL Java_com_wyt_myapplication_Mp3Encoder_destroy (JNIEnv *, jobject);#ifdef __cplusplus}#endif#endif
在 src/main/cpp 目录下新建 Mp3Encoder.cpp 文件,对刚才生成的 com_wyt_myapplication_Mp3Encoder.h 头文件里的方法进行实现。
但是方法的实现需要lame库方法的支持,如果在这个文件里完成pcm转mp3的逻辑的话,这个文件逻辑就复杂了。我们先把lame转换pcm到mp3的相关逻辑封装到心得 c/c++ 文件中,在 Mp3Encoder.cpp 文件里仅调用就行,将 java对native方法调用的实现和native方法的具体逻辑的实现分开。
也就是说整个逻辑分为了4层:java 代码——java调用native方法的实现——lame方法的封装——lame方法。对应的四个代表文件为:Mp3Encoder.java——Mp3Encoder.cpp——mp3_encode.cpp(稍后会创建并实现里面的pcm到Mp3的转换逻辑)——lame方法
这里先给除出Mp3Encoder.cpp的代码(我写完代码才写的文章),实际工作中,这一步要放到 mp3_encode.cpp 之后实现。
主要就是3各方法,分别是初始化 lame、进行编码、编码结束后资源释放。
#include "com_wyt_myapplication_Mp3Encoder.h"#include "mp3_encoder.h"Mp3Encoder *encoder = NULL;JNIEXPORT jint JNICALL Java_com_wyt_myapplication_Mp3Encoder_init (JNIEnv *env, jobject jobj, jstring pcmPathParam, jint audioChannelsParam, jint bitRateParam, jint sampleRateParam, jstring mp3PahtParam){ const char* pcmPath = env->GetStringUTFChars(pcmPathParam,NULL); const char* mp3Path = env->GetStringUTFChars(mp3PahtParam,NULL); encoder = new Mp3Encoder(); int ret = encoder->lint(pcmPath, mp3Path, sampleRateParam, audioChannelsParam, bitRateParam); env->ReleaseStringUTFChars(mp3PahtParam, mp3Path); env->ReleaseStringUTFChars(pcmPathParam, pcmPath); return ret; }JNIEXPORT void JNICALL Java_com_wyt_myapplication_Mp3Encoder_encode(JNIEnv *, jobject){ encoder->Encode(); }JNIEXPORT void JNICALL Java_com_wyt_myapplication_Mp3Encoder_destroy(JNIEnv *, jobject){ encoder->Destory(); }
mp3_encode.cpp 创建
mp3_encode.cpp 里主要是在 lame 库方法的基础上,进行简单封装,完成 pcm 到 mp3的转换。
首先定义下 mp3_encode.cpp 对应的头文件(.h文件),头文件里定义了一个 Mp3Encoder 的类,注意这是native层的C++类,和刚才定义的 Mp3Encoder.java 类没有关系。
类里面向外暴露三个方法,供 Mp3Encoder.cpp 文件的三个方法调用。
#include <stdio.h>#include "lame.h"#ifndef MYAPPLICATION_MP3_ENCODER_H#define MYAPPLICATION_MP3_ENCODER_H#ifdef __cplusplusextern "C" {#endifclass Mp3Encoder {private: FILE *pcmFIle; FILE *mp3File; lame_t lameClient;public: Mp3Encoder(); ~Mp3Encoder(); int lint(const char *pcmFilePath, const char *mp3FilePath, int sampleRate, int channels, int bitRate); void Encode(); void Destory(); };#ifdef __cplusplus}#endif#endif
mp3_encode.cpp 文件的实现,代码看着有点长,其实很好理解,主要是初始化lame的相关参数;pcm文件读取的buffer经过lame转换,形成mp3buffer;将mp3buffer写到文件。
#include "mp3_encoder.h"extern "C"Mp3Encoder::Mp3Encoder(){ } Mp3Encoder::~Mp3Encoder(){ }int Mp3Encoder::lint(const char *pcmFilePath, const char *mp3FilePath, int sampleRate, int channels, int bitRate) { int ret = 1; pcmFIle = fopen(pcmFilePath, "rb"); if (pcmFIle) { mp3File = fopen(mp3FilePath, "wb"); if (mp3File) { //初始化lame相关参数,输入/输出采样率、音频声道数、码率 lameClient = lame_init(); lame_set_in_samplerate(lameClient, sampleRate); lame_set_out_samplerate(lameClient, sampleRate); lame_set_num_channels(lameClient, channels); lame_set_brate(lameClient, 128); lame_init_params(lameClient); ret = 0; } } return ret; }void Mp3Encoder::Encode() { int bufferSize = 1024 * 256; short *buffer = new short[bufferSize / 2]; short *leftChannelBuffer = new short[bufferSize / 4];//左声道 short *rightChannelBuffer = new short[bufferSize / 4];//右声道 unsigned char *mp3_buffer = new unsigned char[bufferSize]; size_t readBufferSize = 0; while ((readBufferSize = fread(buffer, 2, bufferSize / 2, pcmFIle)) > 0) { for (int i = 0; i < readBufferSize; i++) { if (i % 2 == 0) { leftChannelBuffer[i / 2] = buffer[i]; } else { rightChannelBuffer[i / 2] = buffer[i]; } } size_t writeSize = lame_encode_buffer( lameClient, (short int *) leftChannelBuffer, (short int *) rightChannelBuffer, (int) (readBufferSize / 2), mp3_buffer, bufferSize); fwrite(mp3_buffer, 1, writeSize, mp3File); } delete [] buffer; delete [] leftChannelBuffer; delete [] rightChannelBuffer; delete [] mp3_buffer; }void Mp3Encoder::Destory() { if (pcmFIle){ fclose(pcmFIle); } if (mp3File){ fclose(mp3File); lame_close(lameClient); } }
将 src/main/cpp/mp3_encoder.cpp,src/main/cpp/Mp3Encoder.cpp 添加到 CMakeLists.txt 的 add_libraty 方法中。不会的话,看一开始那两篇博客。
android 文件的读写权限别忘了,设置 manifest.xml,6.0以上的适配动态权限获取机制,这里就不说了。
到这里基本上就完成了,下面就可以在工程里使用了,比如这里我在 MainActivity 的onCreate() 里使用。
public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity"; private String[] permissions = new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE }; private List<String> mPermissionList = new ArrayList<>(); private static final int MY_PERMISSIONS_REQUEST = 1001; //采样率,现在能够保证在所有设备上使用的采样率是44100Hz, 但是其他的采样率(22050, 16000, 11025)在一些设备上也可以使用。 public static final int SAMPLE_RATE_INHZ = 44100; //声道数。CHANNEL_IN_MONO and CHANNEL_IN_STEREO. 其中CHANNEL_IN_MONO是可以保证在所有设备能够使用的。 public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO; //返回的音频数据的格式。 ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, and ENCODING_PCM_FLOAT. public static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT; // Used to load the 'native-lib' library on application startup. static { System.loadLibrary("native-lib"); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Example of a call to a native method TextView tv = (TextView) findViewById(R.id.sample_text); tv.setText(stringFromJNI()); checkPermissions(); String pcmPath, mp3Path; pcmPath = "/storage/emulated/0/0001.pcm";//pcm文件路径,文件要存在! mp3Path = "/storage/emulated/0/0001.mp3";//转换后mp3文件的保存路径 Mp3Encoder encoder = new Mp3Encoder(); if(encoder.init(pcmPath,CHANNEL_CONFIG,128,SAMPLE_RATE_INHZ,mp3Path) == 0){ Log.d(TAG, "onCreate: encoder-init:success"); encoder.encode(); encoder.destroy(); Log.d(TAG, "onCreate:encode finish"); }else { Log.d(TAG, "onCreate: encoder-init:failed"); } } /** * A native method that is implemented by the 'native-lib' native library, * which is packaged with this application. */ public native String stringFromJNI(); private void checkPermissions() { // Marshmallow开始才用申请运行时权限 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { for (int i = 0; i < permissions.length; i++) { if (ContextCompat.checkSelfPermission(this, permissions[i]) != PackageManager.PERMISSION_GRANTED) { mPermissionList.add(permissions[i]); } } if (!mPermissionList.isEmpty()) { String[] permissions = mPermissionList.toArray(new String[mPermissionList.size()]); ActivityCompat.requestPermissions(this, permissions, MY_PERMISSIONS_REQUEST); } } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == MY_PERMISSIONS_REQUEST) { for (int i = 0; i < grantResults.length; i++) { if (grantResults[i] != PackageManager.PERMISSION_GRANTED) { Log.i(TAG, permissions[i] + " 权限被用户禁止!"); } } // 运行时权限的申请不是本demo的重点,所以不再做更多的处理,请同意权限申请。 } } }
没有pcm文件?自己动手丰衣足食,自己用 AudioRecorder 写个app ,录制一个pcm!(录制pcm的代码写好了,文章还没有写,别在这等啊,我也不知道哪天会写文章。。。)
作者:昵称真难选
链接:https://www.jianshu.com/p/edab7934ec34
共同学习,写下你的评论
评论加载中...
作者其他优质文章