音视频开发【2】--使用LAME库转换pcm文件到mp3

先说下题外话哈,最近做了个领取电商平台优惠券的小程序,扫码支持下哈~


image

android 使用 AudioRecord 对麦克风进行录音得到的是 pcm 格式的原始音频数据,pcm文件是不能用来播放的,需要进行编码压缩。

LAME是目前非常优秀的一种MP3编码引擎,在业界,转码成MP3格式的音频文件时,最常用的编码器就是LAME库。当达到320Kbit/s以上时,LAME编码出来的音频质量几乎可以和CD的音质相媲美,并且还能保证整个音频文件的体积非常小,因此若要在移动端平台上编码MP3文件,使用LAME便成为唯一的选择。

编译LAME库

android studio3.0+ 默认使用CMake编译native源文件,网上好多文章不合适,这里推荐两篇使用CMake编译LAME库的博客,介绍的都很详细。

Android移植lame库(采用CMake)

Android studio3.0+ 编译Lame库(CMake方式)

音频编码

这里借用《音视频开发进阶指南:基于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的相关方法了。

  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开发的基本流程。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容