音視頻開發【2】--使用LAME庫轉換pcm文件到mp3

android 使用 AudioRecord 對麥克風進行錄音獲得的是 pcm 格式的原始音頻數據,pcm文件是不能用來播放的,須要進行編碼壓縮。java

LAME是目前很是優秀的一種MP3編碼引擎,在業界,轉碼成MP3格式的音頻文件時,最經常使用的編碼器就是LAME庫。當達到320Kbit/s以上時,LAME編碼出來的音頻質量幾乎能夠和CD的音質相媲美,而且還能保證整個音頻文件的體積很是小,所以若要在移動端平臺上編碼MP3文件,使用LAME便成爲惟一的選擇。android

編譯LAME庫

android studio3.0+ 默認使用CMake編譯native源文件,網上好多文章不合適,這裏推薦兩篇使用CMake編譯LAME庫的博客,介紹的都很詳細。c++

Android移植lame庫(採用CMake)算法

Android studio3.0+ 編譯Lame庫(CMake方式)app

音頻編碼

這裏借用《音視頻開發進階指南:基於Android與Ios的實踐》一書裏對各類音頻編碼的介紹。ide

WAV編碼ui

PCM(脈衝編碼調製)是Pulse Code Modulation的縮寫。前面已經介紹過PCM大體的工做流程,而WAV編碼的一種實現(有多種實現方式,可是都不會進行壓縮操做)就是在PCM數據格式的前面加上44字節,分別用來描述PCM的採樣率、聲道數、數據格式等信息。this

特色:音質很是好,大量軟件都支持。編碼

適用場合:多媒體開發的中間文件、保存音樂和音效素材。spa

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的相關方法了。

  1. 新建 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();
}
複製代碼
  1. 生成 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 __cplusplus
extern "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
複製代碼
  1. 在 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();
}
複製代碼
  1. 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 __cplusplus
extern "C" {
#endif

class 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);
    }
}

複製代碼
  1. 將 src/main/cpp/mp3_encoder.cpp,src/main/cpp/Mp3Encoder.cpp 添加到 CMakeLists.txt 的 add_libraty 方法中。不會的話,看一開始那兩篇博客。

  2. android 文件的讀寫權限別忘了,設置 manifest.xml,6.0以上的適配動態權限獲取機制,這裏就不說了。

  3. 到這裏基本上就完成了,下面就能夠在工程裏使用了,好比這裏我在 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的重點,因此再也不作更多的處理,請贊成權限申請。
        }
    }
}
複製代碼
  1. 沒有pcm文件?本身動手豐衣足食,本身用 AudioRecorder 寫個app ,錄製一個pcm!(錄製pcm的代碼寫好了,文章尚未寫,別在這等啊,我也不知道哪天會寫文章。。。)

總結

本爲推薦了兩篇在android studio 3.0以上,使用 CMake 方式編譯lame庫的博客,完成lame庫的集成;而後,經過jni開發,使用java代碼,調用封裝好音頻編碼邏輯的native層代碼,完成pcm文件到mp3文件的轉換,完整的描述了jni開發的基本流程。

相關文章
相關標籤/搜索