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

先說下題外話哈喳逛,最近做了個領取電商平臺優(yōu)惠券的小程序主籍,掃碼支持下哈~


image

android 使用 AudioRecord 對麥克風進行錄音得到的是 pcm 格式的原始音頻數(shù)據(jù)碍粥,pcm文件是不能用來播放的只搁,需要進行編碼壓縮谋国。

LAME是目前非常優(yōu)秀的一種MP3編碼引擎,在業(yè)界,轉碼成MP3格式的音頻文件時,最常用的編碼器就是LAME庫袁铐。當達到320Kbit/s以上時,LAME編碼出來的音頻質量幾乎可以和CD的音質相媲美横浑,并且還能保證整個音頻文件的體積非常小剔桨,因此若要在移動端平臺上編碼MP3文件,使用LAME便成為唯一的選擇徙融。

編譯LAME庫

android studio3.0+ 默認使用CMake編譯native源文件洒缀,網(wǎng)上好多文章不合適,這里推薦兩篇使用CMake編譯LAME庫的博客张咳,介紹的都很詳細帝洪。

Android移植lame庫(采用CMake)

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

音頻編碼

這里借用《音視頻開發(fā)進階指南:基于Android與Ios的實踐》一書里對各種音頻編碼的介紹。

WAV編碼

PCM(脈沖編碼調制)是Pulse Code Modulation的縮寫脚猾。前面已經(jīng)介紹過PCM大致的工作流程葱峡,而WAV編碼的一種實現(xiàn)(有多種實現(xiàn)方式,但是都不會進行壓縮操作)就是在PCM數(shù)據(jù)格式的前面加上44字節(jié)龙助,分別用來描述PCM的采樣率砰奕、聲道數(shù)、數(shù)據(jù)格式等信息提鸟。

特點:音質非常好军援,大量軟件都支持。

適用場合:多媒體開發(fā)的中間文件称勋、保存音樂和音效素材胸哥。

MP3編碼

MP3具有不錯的壓縮比,使用LAME編碼(MP3編碼格式的一種實現(xiàn))的中高碼率的MP3文件赡鲜,聽感上非常接近源WAV文件空厌,當然在不同的應用場景下,應該調整合適的參數(shù)以達到最好的效果银酬。

特點:音質在128Kbit/s以上表現(xiàn)還不錯嘲更,壓縮比比較高,大量軟件和硬件都支持揩瞪,兼容性好赋朦。

適用場合:高比特率下對兼容性有要求的音樂欣賞。

AAC編碼

AAC是新一代的音頻有損壓縮技術,它通過一些附加的編碼技術(比如PS宠哄、SBR等)壹将,衍生出了LC-AAC、HE-AAC琳拨、HE-AAC v2三種主要的編碼格式瞭恰。LC-AAC是比較傳統(tǒng)的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的碼率下表現(xiàn)優(yōu)異,并且多用于視頻中的音頻編碼涌萤。

適用場合:128Kbit/s以下的音頻編碼淹遵,多用于視頻中音頻軌的編碼。

Ogg編碼

Ogg是一種非常有潛力的編碼负溪,在各種碼率下都有比較優(yōu)秀的表現(xiàn)透揣,尤其是在中低碼率場景下。Ogg除了音質好之外川抡,還是完全免費的辐真,這為Ogg獲得更多的支持打好了基礎。Ogg有著非常出色的算法崖堤,可以用更小的碼率達到更好的音質侍咱,128Kbit/s的Ogg比192Kbit/s甚至更高碼率的MP3還要出色。但目前因為還沒有媒體服務軟件的支持密幔,因此基于Ogg的數(shù)字廣播還無法實現(xiàn)楔脯。Ogg目前受支持的情況還不夠好,無論是軟件上的還是硬件上的支持胯甩,都無法和MP3相提并論昧廷。

特點:可以用比MP3更小的碼率實現(xiàn)比MP3更好的音質,高中低碼率下均有良好的表現(xiàn)蜡豹,兼容性不夠好麸粮,流媒體特性不支持。

適用場合:語音聊天的音頻消息場景镜廉。

使用LAME轉換pcm文件到mp3

按照前面編譯lame庫的博客做下來弄诲,現(xiàn)在工程里面已經(jīng)可以通過 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 頭文件里的方法進行實現(xiàn)断序。

但是方法的實現(xiàn)需要lame庫方法的支持,如果在這個文件里完成pcm轉mp3的邏輯的話糜烹,這個文件邏輯就復雜了违诗。我們先把lame轉換pcm到mp3的相關邏輯封裝到心得 c/c++ 文件中,在 Mp3Encoder.cpp 文件里僅調用就行疮蹦,將 java對native方法調用的實現(xiàn)和native方法的具體邏輯的實現(xiàn)分開诸迟。

也就是說整個邏輯分為了4層:java 代碼——java調用native方法的實現(xiàn)——lame方法的封裝——lame方法。對應的四個代表文件為:Mp3Encoder.java——Mp3Encoder.cpp——mp3_encode.cpp(稍后會創(chuàng)建并實現(xiàn)里面的pcm到Mp3的轉換邏輯)——lame方法

這里先給除出Mp3Encoder.cpp的代碼(我寫完代碼才寫的文章)愕乎,實際工作中阵苇,這一步要放到 mp3_encode.cpp 之后實現(xiàn)。

主要就是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 創(chuàng)建

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 文件的實現(xiàn)鳍侣,代碼看著有點長,其實很好理解吼拥,主要是初始化lame的相關參數(shù)倚聚;pcm文件讀取的buffer經(jīng)過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相關參數(shù)授账,輸入/輸出采樣率、音頻聲道數(shù)惨驶、碼率
            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以上的適配動態(tài)權限獲取機制攻臀,這里就不說了。

  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;

    //采樣率砌些,現(xiàn)在能夠保證在所有設備上使用的采樣率是44100Hz, 但是其他的采樣率(22050, 16000, 11025)在一些設備上也可以使用。
    public static final int SAMPLE_RATE_INHZ = 44100;
    //聲道數(shù)加匈。CHANNEL_IN_MONO and CHANNEL_IN_STEREO. 其中CHANNEL_IN_MONO是可以保證在所有設備能夠使用的存璃。
    public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO;
    //返回的音頻數(shù)據(jù)的格式。 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 ,錄制一個pcmA状住(錄制pcm的代碼寫好了猫牡,文章還沒有寫,別在這等啊邓线,我也不知道哪天會寫文章淌友。。骇陈。)

總結

本為推薦了兩篇在android studio 3.0以上震庭,使用 CMake 方式編譯lame庫的博客,完成lame庫的集成你雌;然后器联,通過jni開發(fā),使用java代碼,調用封裝好音頻編碼邏輯的native層代碼主籍,完成pcm文件到mp3文件的轉換习贫,完整的描述了jni開發(fā)的基本流程。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末千元,一起剝皮案震驚了整個濱河市苫昌,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌幸海,老刑警劉巖祟身,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異物独,居然都是意外死亡袜硫,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門挡篓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來婉陷,“玉大人,你說我怎么就攤上這事官研』喟模” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵戏羽,是天一觀的道長担神。 經(jīng)常有香客問我,道長始花,這世上最難降的妖魔是什么妄讯? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮酷宵,結果婚禮上亥贸,老公的妹妹穿的比我還像新娘。我一直安慰自己忧吟,他們只是感情好砌函,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著溜族,像睡著了一般讹俊。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上煌抒,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天仍劈,我揣著相機與錄音,去河邊找鬼寡壮。 笑死贩疙,一個胖子當著我的面吹牛讹弯,可吹牛的內容都是我干的。 我是一名探鬼主播这溅,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼组民,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了悲靴?” 一聲冷哼從身側響起臭胜,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎癞尚,沒想到半個月后耸三,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡浇揩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年仪壮,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片胳徽。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡积锅,死狀恐怖,靈堂內的尸體忽然破棺而出养盗,到底是詐尸還是另有隱情乏沸,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布爪瓜,位于F島的核電站,受9級特大地震影響匙瘪,放射性物質發(fā)生泄漏铆铆。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一丹喻、第九天 我趴在偏房一處隱蔽的房頂上張望薄货。 院中可真熱鬧,春花似錦碍论、人聲如沸谅猾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽税娜。三九已至,卻和暖如春藏研,著一層夾襖步出監(jiān)牢的瞬間敬矩,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工蠢挡, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留弧岳,地道東北人凳忙。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像禽炬,于是被迫代替她去往敵國和親涧卵。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內容