录制MP3格式的音频( lame 库的编译及使用)

众多周知,mp3 是跨平台性最好的音频格式,由于采用了压缩率更高的有损压缩算法,文件大小是大约每分钟1M,使其在网络中传输更快,占用存储空间也更少;与此同时,它的声音质量也不错,尤其是人声(相声、评书、脱口秀),当然追求无损音乐的除外。
Android 中没有提供录制 mp3 的 API,需要使用开源库 lame,lame 是专门用于编码 mp3 的轻量高效的 c 代码库。由于采用 c 语言编写,故需要用到 jni。

下载lame库

源码导入

解压下载的lame库,把libmp3lame文件夹下后缀为.c .h的文件(不包括子文件夹i386和vector下的)复制到cpp/lame文件夹内,同时把include目录下的lame.h也复制到cpp/lame文件夹内,此时 lame文件夹内包含42个文件。

images.png

(可参考https://github.com/xmaihh/MFSocket/tree/master/liblame/src/main/cpp/lame

修改库文件

打开刚刚拷贝的lame库文件,修改:

  1. util.h 文件,把 570 行的两处 ieee754_float32_t 改为 float 因为Android下并不支持该类型
  2. set_get.h 文件,把头部的 #include <lame.h> 改为 #include "lame.h"
  3. fft.c 文件,删除第47行 #include "vector/lame_intrin.h"
  4. id3tag.c和machine.h两个文件里,將HAVE_STRCHR和HAVE_MEMCPY的ifdef结构体删除或者注释
#ifdef STDC_HEADERS
# include <stdlib.h>
# include <string.h>
#else
/*# ifndef HAVE_STRCHR
#  define strchr index
#  define strrchr rindex
# endif*/
char   *strchr(), *strrchr();
/*# ifndef HAVE_MEMCPY
#  define memcpy(d, s, n) bcopy ((s), (d), (n))
#  define memmove(d, s, n) bcopy ((s), (d), (n))
# endif*/
#endif

可参考以下完整修改文件

diff --git a/VbrTag.c b/VbrTag.c
index 5800a44..36ee7b6 100644
--- a/VbrTag.c
+++ b/VbrTag.c
@@ -26,6 +26,8 @@
 # include <config.h>
 #endif
 
+#include <stdlib.h>
+#include <string.h>
 #include "lame.h"
 #include "machine.h"
 #include "encoder.h"
diff --git a/bitstream.c b/bitstream.c
index aa35915..a2fe294 100644
--- a/bitstream.c
+++ b/bitstream.c
@@ -29,6 +29,7 @@
 
 #include <stdlib.h>
 #include <stdio.h>
+#include <string.h>
 
 #include "lame.h"
 #include "machine.h"
diff --git a/encoder.c b/encoder.c
index 48f46c7..437067f 100644
--- a/encoder.c
+++ b/encoder.c
@@ -30,6 +30,7 @@
 #endif
 
 
+#include <string.h>
 #include "lame.h"
 #include "machine.h"
 #include "encoder.h"
diff --git a/fft.c b/fft.c
index 4eea1ad..27febdb 100644
--- a/fft.c
+++ b/fft.c
@@ -44,7 +44,7 @@
 #include "util.h"
--- a/fft.c
+++ b/fft.c
@@ -44,7 +44,7 @@
 #include "util.h"
 #include "fft.h"
 
-#include "vector/lame_intrin.h"
+//#include "vector/lame_intrin.h"
 
 
 
diff --git a/id3tag.c b/id3tag.c
index ac48510..8f148b8 100644
--- a/id3tag.c
+++ b/id3tag.c
@@ -41,17 +41,20 @@
 # include <string.h>
 # include <ctype.h>
 #else
-# ifndef HAVE_STRCHR
-#  define strchr index
-#  define strrchr rindex
-# endif
+//# ifndef HAVE_STRCHR
+//#  define strchr index
+//#  define strrchr rindex
+//# endif
 char   *strchr(), *strrchr();
-# ifndef HAVE_MEMCPY
-#  define memcpy(d, s, n) bcopy ((s), (d), (n))
-# endif
+//# ifndef HAVE_MEMCPY
+//#  define memcpy(d, s, n) bcopy ((s), (d), (n))
+//# endif
 #endif
 
 
+#include <malloc.h>
+#include <string.h>
+#include <stdlib.h>
 #include "lame.h"
 #include "machine.h"
 #include "encoder.h"
diff --git a/lame.c b/lame.c
index cb82225..299fd56 100644
--- a/lame.c
+++ b/lame.c
@@ -31,6 +31,8 @@
 #endif
 
 
+#include <malloc.h>
+#include <string.h>
 #include "lame.h"
 #include "machine.h"
 
diff --git a/machine.h b/machine.h
index bf6fff2..c675c20 100644
--- a/machine.h
+++ b/machine.h
@@ -31,15 +31,15 @@
 # include <stdlib.h>
 # include <string.h>
 #else
-# ifndef HAVE_STRCHR
-#  define strchr index
-#  define strrchr rindex
-# endif
+//# ifndef HAVE_STRCHR
+//#  define strchr index
+//#  define strrchr rindex
+//# endif
 char   *strchr(), *strrchr();
-# ifndef HAVE_MEMCPY
-#  define memcpy(d, s, n) bcopy ((s), (d), (n))
-#  define memmove(d, s, n) bcopy ((s), (d), (n))
-# endif
+//# ifndef HAVE_MEMCPY
+//#  define memcpy(d, s, n) bcopy ((s), (d), (n))
+//#  define memmove(d, s, n) bcopy ((s), (d), (n))
+//# endif
 #endif
 
 #if  defined(__riscos__)  &&  defined(FPA10)
diff --git a/newmdct.c b/newmdct.c
index 596cac9..ac98abd 100644
--- a/newmdct.c
+++ b/newmdct.c
@@ -30,6 +30,7 @@
 # include <config.h>
 #endif
 
+#include <string.h>
 #include "lame.h"
 #include "machine.h"
 #include "encoder.h"
diff --git a/psymodel.c b/psymodel.c
index 60076ee..1393c2a 100644
--- a/psymodel.c
+++ b/psymodel.c
@@ -145,7 +145,8 @@ blocktype_d[2]        block type to use for previous granule
 #endif
 
 #include <float.h>
-
+#include <stdlib.h>
+#include <string.h>
 #include "lame.h"
 #include "machine.h"
 #include "encoder.h"
diff --git a/quantize.c b/quantize.c
index 9ba9c16..2906c00 100644
--- a/quantize.c
+++ b/quantize.c
@@ -28,6 +28,8 @@
 # include <config.h>
 #endif
 
+#include <stdlib.h>
+#include <string.h>
 #include "lame.h"
 #include "machine.h"
 #include "encoder.h"
diff --git a/quantize_pvt.c b/quantize_pvt.c
:
 #endif
 
 #include <float.h>
-
+#include <stdlib.h>
+#include <string.h>
 #include "lame.h"
 #include "machine.h"
 #include "encoder.h"
diff --git a/quantize.c b/quantize.c
index 9ba9c16..2906c00 100644
--- a/quantize.c
+++ b/quantize.c
@@ -28,6 +28,8 @@
 # include <config.h>
 #endif
 
+#include <stdlib.h>
+#include <string.h>
 #include "lame.h"
 #include "machine.h"
 #include "encoder.h"
diff --git a/quantize_pvt.c b/quantize_pvt.c
index d8d6447..3cd9966 100644
--- a/quantize_pvt.c
+++ b/quantize_pvt.c
@@ -36,6 +36,7 @@
 #include "reservoir.h"
 #include "lame-analysis.h"
 #include <float.h>
+#include <string.h>
 
 
 #define NSATHSCALE 100  /* Assuming dynamic range=96dB, this value should be 92 */
diff --git a/set_get.h b/set_get.h
index 37e4bcd..99ab73c 100644
--- a/set_get.h
+++ b/set_get.h
@@ -21,7 +21,7 @@
 #ifndef __SET_GET_H__
 #define __SET_GET_H__
 
-#include <lame.h>
+#include "lame.h"
 
 #if defined(__cplusplus)
 extern  "C" {
diff --git a/takehiro.c b/takehiro.c
index 67aba1b..ca02f98 100644
--- a/takehiro.c
+++ b/takehiro.c
@@ -27,6 +27,7 @@
 #endif
 
 
+#include <string.h>
 #include "lame.h"
 #include "machine.h"
 #include "encoder.h"
diff --git a/util.c b/util.c
index 43b457c..e9255fe 100644
--- a/util.c
+++ b/util.c
@@ -27,6 +27,7 @@
 #endif
 
 #include <float.h>
+#include <malloc.h>
 #include "lame.h"
 #include "machine.h"
 #include "encoder.h"
diff --git a/util.h b/util.h
index 13f0cd4..b6bf306 100644
--- a/util.h
+++ b/util.h
@@ -567,7 +567,7 @@ extern  "C" {
 
 /* log/log10 approximations */
     extern void init_log_table(void);
-    extern ieee754_float32_t fast_log2(ieee754_float32_t x);
+    extern float fast_log2(float x);
 
     int     isResamplingNecessary(SessionConfig_t const* cfg);
 
diff --git a/vbrquantize.c b/vbrquantize.c
index 0f703b7..60834d3 100644
--- a/vbrquantize.c
+++ b/vbrquantize.c
@@ -27,6 +27,8 @@
 #endif
 
 
+#include <stdlib.h>
+#include <string.h>
 #include "lame.h"
 #include "machine.h"
 #include "encoder.h"

编写CmakeList.txt

cmake_minimum_required(VERSION 3.6.0)
set(CURRENT_DIR ${CMAKE_SOURCE_DIR})
message("CURRENT_DIR:" ${CMAKE_SOURCE_DIR})
include_directories(${CMAKE_SOURCE_DIR}src/main/cpp/lame)

set(LAME_DIR src/main/cpp/lame)
message("LAME_DIR:" ${LAME_DIR})

aux_source_directory(src/main/cpp/lame SRC_LIST)

add_library(mp3lame
        SHARED
        src/main/cpp/MP3Recorder.c
        ${SRC_LIST})

#add_library(mp3lame
#        SHARED
#        src/main/cpp/MP3Recorder.c
#        src/main/cpp/lame/bitstream.c
#        src/main/cpp/lame/fft.c
#        src/main/cpp/lame/id3tag.c
#        src/main/cpp/lame/mpglib_interface.c
#        src/main/cpp/lame/presets.c
#        src/main/cpp/lame/quantize.c
#        src/main/cpp/lame/reservoir.c
#        src/main/cpp/lame/tables.c
#        src/main/cpp/lame/util.c
#        src/main/cpp/lame/VbrTag.c
#        src/main/cpp/lame/encoder.c
#        src/main/cpp/lame/gain_analysis.c
#        src/main/cpp/lame/lame.c
#        src/main/cpp/lame/newmdct.c
#        src/main/cpp/lame/psymodel.c
#        src/main/cpp/lame/quantize_pvt.c
#        src/main/cpp/lame/set_get.c
#        src/main/cpp/lame/takehiro.c
#        src/main/cpp/lame/vbrquantize.c
#        src/main/cpp/lame/version.c)


find_library( # Sets the name of the path variable.
        log-lib
        log)

target_link_libraries(mp3lame
        ${log-lib})

(可参考https://github.com/xmaihh/MFSocket/blob/master/liblame/CMakeLists.txt

编写 java 类和 c 文件

public class MP3Recorder {
    static {
     System.loadLibrary("mp3lame");   
  }
    /**
     * 初始化 lame编码器
     *
     * @param inSampleRate
     *              输入采样率
     * @param outChannel
     *              声道数
     * @param outSampleRate
     *              输出采样率
     * @param outBitrate
     *              比特率(kbps)
     * @param quality
     *              0~9,0最好
     */
    public static native void init(int inSampleRate, int outChannel, int outSampleRate, int outBitrate, int quality);
    
    /**
     *  编码,把 AudioRecord 录制的 PCM 数据转换成 mp3 格式
     *
     * @param buffer_l
     *          左声道输入数据
     * @param buffer_r
     *          右声道输入数据
     * @param samples
     *          输入数据的size
     * @param mp3buf
     *          输出数据
     * @return
     *          输出到mp3buf的byte数量
     */
    public static native int encode(short[] buffer_l, short[] buffer_r, int samples, byte[] mp3buf);
    
    /**
     *  刷写
     *
     * @param mp3buf
     *          mp3数据缓存区
     * @return
     *          返回刷写的数量
     */
    public static native int flush(byte[] mp3buf);
    
    /**
     * 关闭 lame 编码器,释放资源
     */
    public static native void close();
}

生成.h文件

AndroidStudio快速生成jni头文件

编写MP3Recorder.c

#include "lame/lame.h"
#include "MP3Recorder.h"

static lame_global_flags *glf = NULL;
/*
 * Class:     com_android_liblame_MP3Recorder
 * Method:    init
 * Signature: (IIIII)V
 */

JNIEXPORT void JNICALL Java_com_android_liblame_MP3Recorder_init
        (JNIEnv *env, jclass instance, jint inSamplerate, jint outChannel, jint outSamplerate,
         jint outBitrate, jint quality) {
    if (glf != NULL) {
        lame_close(glf);
        glf = NULL;
    }
    glf = lame_init();
    lame_set_in_samplerate(glf, inSamplerate);
    lame_set_num_channels(glf, outChannel);
    lame_set_out_samplerate(glf, outSamplerate);
    lame_set_brate(glf, outBitrate);
    lame_set_quality(glf, quality);
    lame_init_params(glf);

}

/*
 * Class:     com_android_liblame_MP3Recorder
 * Method:    encode
 * Signature: ([S[SI[B)I
 */
JNIEXPORT jint JNICALL Java_com_android_liblame_MP3Recorder_encode
        (JNIEnv *env, jclass instance, jshortArray buffer_l, jshortArray buffer_r, jint samples,
         jbyteArray mp3buf) {
    jshort *j_buffer_l = (*env)->GetShortArrayElements(env, buffer_l, NULL);

    jshort *j_buffer_r = (*env)->GetShortArrayElements(env, buffer_r, NULL);

    const jsize mp3buf_size = (*env)->GetArrayLength(env, mp3buf);
    jbyte *j_mp3buf = (*env)->GetByteArrayElements(env, mp3buf, NULL);

    int result = lame_encode_buffer(glf, j_buffer_l, j_buffer_r,
                                    samples, j_mp3buf, mp3buf_size);

    (*env)->ReleaseShortArrayElements(env, buffer_l, j_buffer_l, 0);
    (*env)->ReleaseShortArrayElements(env, buffer_r, j_buffer_r, 0);
    (*env)->ReleaseByteArrayElements(env, mp3buf, j_mp3buf, 0);

    return result;
}

/*
 * Class:     com_android_liblame_MP3Recorder
 * Method:    flush
 * Signature: ([B)I
 */
JNIEXPORT jint JNICALL Java_com_android_liblame_MP3Recorder_flush
        (JNIEnv *env, jclass instance, jbyteArray mp3buf) {
    const jsize mp3buf_size = (*env)->GetArrayLength(env, mp3buf);
    jbyte *j_mp3buf = (*env)->GetByteArrayElements(env, mp3buf, NULL);

    int result = lame_encode_flush(glf, j_mp3buf, mp3buf_size);

    (*env)->ReleaseByteArrayElements(env, mp3buf, j_mp3buf, 0);

    return result;
}

/*
 * Class:     com_android_liblame_MP3Recorder
 * Method:    close
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_android_liblame_MP3Recorder_close
        (JNIEnv *env, jclass instance) {
    lame_close(glf);
    glf = NULL;

}

配置build.gradle

android {
    ....
    ...
    ..
    .
   //*
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }

}

点一下小锤子 :hammer: MakeProject。

编译生成 so库。

录制MP3格式音频

// 录音状态
    private boolean isRecording;
    
    //开始录音
    private void record() {
        new Thread() {
            @Override
            public void run() {
                // 音源
                int audioSource = MediaRecorder.AudioSource.MIC;
                // 采样率
                int sampleRate = 44100;
                // 声道
                int channelConfig = AudioFormat.CHANNEL_IN_MONO;//单声道
                // 采样位数
                int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
                // 录音缓存区大小
                int bufferSizeInBytes;
                // 文件输出流
                FileOutputStream fos;
                // 录音最小缓存大小
                bufferSizeInBytes = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
                AudioRecord audioRecord = new AudioRecord(audioSource, sampleRate, channelConfig, audioFormat, bufferSizeInBytes);
                try {
                    fos = new FileOutputStream(getExternalCacheDir() + "/demo.mp3");
                    MP3Recorder.init(sampleRate, 2, sampleRate, 128, 5);
                    short[] buffer = new short[bufferSizeInBytes];
                    byte[] mp3buffer = new byte[(int) (7200 + buffer.length * 1.25)];
                    audioRecord.startRecording();
                    isRecording = true;
                    while (isRecording && audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
                        int readSize = audioRecord.read(buffer, 0, bufferSizeInBytes);
                        if (readSize > 0) {
                            int encodeSize = MP3Recorder.encode(buffer, buffer, readSize, mp3buffer);
                            if (encodeSize > 0) {
                                try {
                                    fos.write(mp3buffer, 0, encodeSize);
                                } catch (IOException e) {
                                    e.printStackTrace();
                                }
                            }
                        }
                    }
                    
                    int flushSize = MP3Recorder.flush(mp3buffer);
                    if (flushSize > 0) {
                        try {
                            fos.write(mp3buffer, 0, flushSize);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                    try {
                        fos.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    audioRecord.stop();
                    audioRecord.release();
                    MP3Recorder.close();
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                }
            }
        }.start();
    }
    
 // 停止录音
    private void stop() {
        isRecording = false;
    }

在 AndroidManifest 配置文件中添加录音权限:

        <uses-permission android:name="android.permission.RECORD_AUDIO"/>

Android 6.0 以上还要动态获取权限。

Reference

Android 录音详解(二)—— 录制 mp3 格式音频( lame 库的编译及使用)

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

推荐阅读更多精彩内容