Android LAME原生音频

Android LAME原生音频

码农世界 2024-05-28 前端 72 次浏览 0个评论

前言

我想大家都做过录音的功能吧,首先想到的是不是MediaRecorder?今天我们不用MediaRecorder,而是使用LAME库自己编译音频编码模块,很明显,这个需要用到NDK。凡是涉及到音视频编解码这块的,都需要用到Android NDK(Native Development Kit),原生开发工具包。即使用C/C++代码实现音频的采样和编码,然后使用Java去调用原生模块实现录音功能。

音视频相关基础知识

我来简单过一下基础的音视频相关知识。

Audio Sample:音频采样,通常指录制音频采样文件PCM的过程,PCM(脉冲编码调制)是一种音频流,称为裸流。人耳听到的是模拟信号,PCM是把声音从模拟信号转化为数字信号的技术。

Audio Track:音轨,封装格式的音频文件通常由多个音轨组成,比如MP3文件就是一种封装格式的音频文件,与之相对的就是音频原始采样数据PCM。比如一首歌,歌声是一个音轨,吉他声、鼓声等一些混合在其中的声音各自也是一个音轨。

Audio Channel:声道,比如左声道、右声道和环绕立体声。

Bitrate:比特率,俗称码率。它直接决定声音的清晰度即声音特征的详细程度,SQ、HQ音质是通过它来判断的,码率越高,音频文件越大,质量也越高。

Sample Rate:采样率,大多数沿用国际通用的标准采样率,即44.100kHZ或者48.000kHZ,肯定是不能录制超声波和次声波的,因为人耳感知不到。

录音和播放声音的详细过程。

录音:音频采样编码->音频封装

播放声音:音频解封装->音频解码播放

录音首先采样并编码得到PCM文件,然后封装PCM文件为MP3、WAV、FLAC等文件,播放声音首先也要解封装成PCM文件,然后对PCM文件解码播放。

编译共享库so

编译共享库so的过程有两种方式,一种是使用ndk-build+Android.mk+Application.mk,一种是CMake+CMakeLists.txt。今天我们采用最原始的方式,mk文件。其实Android.mk和CMakeLists.txt很多东西是一一对应的。

Android.mkCMakeLists.txt
LOCAL_MODULE、LOCAL_SRC_FILESadd_library
LOCAL_CFLAGSadd_definitions
LOCAL_C_INCLUDESinclude_directories
LOCAL_STATIC_LIBRARIES、LOCAL_SHARED_LIBRARIESadd_library + set_target_properties
LOCAL_LDLIBSfind_library
C文件
Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LAME_LIBMP3_DIR := lame-3.100_libmp3lame
LOCAL_MODULE    := mp3lame
LOCAL_SRC_FILES :=\
$(LAME_LIBMP3_DIR)/bitstream.c \
$(LAME_LIBMP3_DIR)/fft.c \
$(LAME_LIBMP3_DIR)/id3tag.c \
$(LAME_LIBMP3_DIR)/mpglib_interface.c \
$(LAME_LIBMP3_DIR)/presets.c \
$(LAME_LIBMP3_DIR)/quantize.c \
$(LAME_LIBMP3_DIR)/reservoir.c \
$(LAME_LIBMP3_DIR)/tables.c  \
$(LAME_LIBMP3_DIR)/util.c \
$(LAME_LIBMP3_DIR)/VbrTag.c \
$(LAME_LIBMP3_DIR)/encoder.c \
$(LAME_LIBMP3_DIR)/gain_analysis.c \
$(LAME_LIBMP3_DIR)/lame.c \
$(LAME_LIBMP3_DIR)/newmdct.c \
$(LAME_LIBMP3_DIR)/psymodel.c \
$(LAME_LIBMP3_DIR)/quantize_pvt.c \
$(LAME_LIBMP3_DIR)/set_get.c \
$(LAME_LIBMP3_DIR)/takehiro.c \
$(LAME_LIBMP3_DIR)/vbrquantize.c \
$(LAME_LIBMP3_DIR)/version.c \
MP3Encoder.c
include $(BUILD_SHARED_LIBRARY)
  • LOCAL_PATH :=$(call my-dir)

    当前文件在系统中的路径,必须为Android.mk文件的第一行。

  • include $(CLEAR_VARS)

    清除上一次构建中的全局变量,开始一次新的编译。

  • LOCAL_MODULE

    生成的模块的名称,这里是动态库so文件的名称,so文件的名称拼接为lib「模块名」.so

    该模块的编译的目标名,用于区分各个模块,名字必须是唯一并不包含空格的,如果编译目标是 so 库,那么该 so 库的名称就是 lib 项目名 .so。

  • LOCAL_SRC_FILES

    要编译的.c或.cpp文件,.h和.hpp文件可以不用出现在这里,系统会自动包含。

  • include $(BUILD_SHARED_LIBRARY)

    include开头的是构建系统的内置变量,此行代码的意思是构建动态库,也称共享库,还有以下几种取值。

    BUILD_STATIC_LIBRARY: 构建静态库。

    PREBUILT_STATIC_LIBRARY: 将静态库包装成一个模块。

    PREBUILT_SHARED_LIBRARY: 将静态库包装成一个模块。

    BUILD_EXECUTABLE: 构建可执行文件。

    Application.mk
    APP_ABI := armeabi  armeabi-v7a  arm64-v8a  x86  x86_64  mips  mips64
    APP_MODULES := mp3lame
    APP_CFLAGS += -DSTDC_HEADERS
    APP_PLATFORM := android-21
    
    • APP_ABI ABI(Application Binary Interface)应用二级制接口,这是一种计算机科学中的概念,用于描述软件库或操作系统与应用程序之间的二进制通信方式。它跟CPU指令集对应。
    • APP_MODULES 指定模块
    • APP_FLAGS 指定编译过程的flag,“DSTDC_HEADERS” 是一个编程中常见的宏定义,通常用于检查标准库头文件是否已经包含。这个宏定义通常在C/C++代码中使用,用于确保标准库的头文件已经正确包含,以便程序可以正常编译和运行。如果没有正确包含标准库头文件,编译器可能会报错或者出现未定义的行为。
    • APP_PLATFORM 指定创建的动态库的平台。

      与JNI相关的文件

      /* DO NOT EDIT THIS FILE - it is machine generated */
      #include 
      #ifndef _Included_Mp3Encoder
      #define _Included_Mp3Encoder
      #ifdef __cplusplus
      extern "C" {
      #endif
      JNIEXPORT void JNICALL Java_com_dorachat_dorachat_recorder_mp3_Mp3Encoder_init
        (JNIEnv *, jclass, jint, jint, jint, jint, jint);
      JNIEXPORT jint JNICALL Java_com_dorachat_dorachat_recorder_mp3_Mp3Encoder_encode
        (JNIEnv *, jclass, jshortArray, jshortArray, jint, jbyteArray);
      JNIEXPORT jint JNICALL Java_com_dorachat_dorachat_recorder_mp3_Mp3Encoder_flush
        (JNIEnv *, jclass, jbyteArray);
      JNIEXPORT void JNICALL Java_com_dorachat_dorachat_recorder_mp3_Mp3Encoder_close
        (JNIEnv *, jclass);
      #ifdef __cplusplus
      }
      #endif
      #endif
      

      lame的jni层主要定义4个方法init、encode、flush和close。

      #include "lame-3.100_libmp3lame/lame.h"
      #include "Mp3Encoder.h"
      static lame_global_flags *glf = NULL;
      JNIEXPORT void JNICALL Java_com_dorachat_dorachat_recorder_mp3_Mp3Encoder_init(
              JNIEnv *env, jclass cls, 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);
      }
      JNIEXPORT jint JNICALL
      Java_com_dorachat_dorachat_recorder_mp3_Mp3Encoder_encode(
              JNIEnv *env, jclass cls, 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;
      }
      JNIEXPORT jint JNICALL
      Java_com_dorachat_dorachat_recorder_mp3_Mp3Encoder_flush(
              JNIEnv *env, jclass cls, 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;
      }
      JNIEXPORT void JNICALL Java_com_dorachat_dorachat_recorder_mp3_Mp3Encoder_close(
              JNIEnv *env, jclass cls) {
          lame_close(glf);
          glf = NULL;
      }
      

      这个需要你会一点C语言的基础,然后就可以轻松调用lame库的函数了。

      package com.dorachat.dorachat.recorder.mp3;
      public class Mp3Encoder {
          static {
              System.loadLibrary("mp3lame");
          }
          public native static void close();
          public native static int encode(short[] buffer_l, short[] buffer_r, int samples, byte[] mp3buf);
          public native static int flush(byte[] mp3buf);
          public native static void init(int inSampleRate, int outChannel, int outSampleRate, int outBitrate, int quality);
          public static void init(int inSampleRate, int outChannel, int outSampleRate, int outBitrate) {
              init(inSampleRate, outChannel, outSampleRate, outBitrate, 7);
          }
      }
      

      我们再看一下java层。

      Java层大致实现

      我们录音肯定是要在后台Service中进行的,可以使用onStartCommand进行调用,这里只是一个简单的功能,就不使用aidl跨进程了。开始录音时我们需要开一个线程去采样音频数据。

      public void start(String filePath, RecordConfig config) {
          this.currentConfig = config;
          if (state != RecordState.IDLE && state != RecordState.STOP) {
              Logger.e(TAG, "状态异常当前状态: %s", state.name());
              return;
          }
          resultFile = new File(filePath);
          String tempFilePath = getTempFilePath();
          LogUtils.dformat(TAG, "----------------开始录制 %s------------------------", currentConfig.getFormat().name());
          LogUtils.dformat(TAG, "参数: %s", currentConfig.toString());
          LogUtils.iformat(TAG, "pcm缓存 tmpFile: %s", tempFilePath);
          LogUtils.iformat(TAG, "录音文件 resultFile: %s", filePath);
          tmpFile = new File(tempFilePath);
          audioRecordThread = new AudioRecordThread();
          audioRecordThread.start();
      }
      
      private class AudioRecordThread extends Thread {
          private AudioRecord audioRecord;
          private int bufferSize;
          AudioRecordThread() {
              bufferSize = AudioRecord.getMinBufferSize(currentConfig.getSampleRate(),
                      currentConfig.getChannelConfig(), currentConfig.getEncodingConfig()) * RECORD_AUDIO_BUFFER_TIMES;
              Logger.d(TAG, "record buffer size = %s", bufferSize);
              audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, currentConfig.getSampleRate(),
                      currentConfig.getChannelConfig(), currentConfig.getEncodingConfig(), bufferSize);
              if (currentConfig.getFormat() == RecordConfig.RecordFormat.MP3) {
                  if (mp3EncodeThread == null) {
                      initMp3EncoderThread(bufferSize);
                  } else {
                      LogUtils.e("mp3EncodeThread != null, 请检查代码");
                  }
              }
          }
          @Override
          public void run() {
              super.run();
              switch (currentConfig.getFormat()) {
                  case MP3:
                      startMp3Recorder();
                      break;
                  default:
                      startPcmRecorder();
                      break;
              }
          }
          private void startPcmRecorder() {
              state = RecordState.RECORDING;
              notifyState();
              LogUtils.d("开始录制PCM");
              FileOutputStream fos = null;
              try {
                  fos = new FileOutputStream(tmpFile);
                  audioRecord.startRecording();
                  byte[] byteBuffer = new byte[bufferSize];
                  while (state == RecordState.RECORDING) {
                      int end = audioRecord.read(byteBuffer, 0, byteBuffer.length);
                      notifyData(byteBuffer);
                      fos.write(byteBuffer, 0, end);
                      fos.flush();
                  }
                  audioRecord.stop();
                  files.add(tmpFile);
                  if (state == RecordState.STOP) {
                      makeFile();
                  } else {
                      LogUtils.i("暂停");
                  }
              } catch (Exception e) {
                  LogUtils.e(e.getMessage());
                  notifyError("录音失败");
              } finally {
                  try {
                      if (fos != null) {
                          fos.close();
                      }
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
              }
              if (state != RecordState.PAUSE) {
                  state = RecordState.IDLE;
                  notifyState();
                  LogUtils.d("录音结束");
              }
          }
          private void startMp3Recorder() {
              state = RecordState.RECORDING;
              notifyState();
              try {
                  audioRecord.startRecording();
                  short[] byteBuffer = new short[bufferSize];
                  while (state == RecordState.RECORDING) {
                      int end = audioRecord.read(byteBuffer, 0, byteBuffer.length);
                      if (mp3EncodeThread != null) {
                          mp3EncodeThread.addChangeBuffer(new Mp3EncodeThread.ChangeBuffer(byteBuffer, end));
                      }
                      notifyData(ByteUtils.toBytes(byteBuffer));
                  }
                  audioRecord.stop();
              } catch (Exception e) {
                  LogUtils.e(e.getMessage());
                  notifyError("录音失败");
              }
              if (state != RecordState.PAUSE) {
                  state = RecordState.IDLE;
                  notifyState();
                  stopMp3Encoded();
              } else {
                  LogUtils.d("暂停");
              }
          }
      }
      private void stopMp3Encoded() {
          if (mp3EncodeThread != null) {
              mp3EncodeThread.stopSafe(new Mp3EncodeThread.EncordFinishListener() {
                  @Override
                  public void onFinish() {
                      notifyFinish();
                      mp3EncodeThread = null;
                  }
              });
          } else {
              LogUtils.e("mp3EncodeThread is null, 代码业务流程有误,请检查!");
          }
      }
      private void makeFile() {
          switch (currentConfig.getFormat()) {
              case MP3:
                  return;
              case WAV:
                  mergePcmFile();
                  makeWav();
                  break;
              case PCM:
                  mergePcmFile();
                  break;
              default:
                  break;
          }
          notifyFinish();
          LogUtils.i("录音完成! path: %s ; 大小:%s", resultFile.getAbsoluteFile(), resultFile.length());
      }
      /**
       * 添加Wav头文件。
       */
      private void makeWav() {
          if (!FileUtils.isFile(resultFile) || resultFile.length() == 0) {
              return;
          }
          byte[] header = WavUtils.generateWavFileHeader((int) resultFile.length(), currentConfig.getSampleRate(), currentConfig.getChannelCount(), currentConfig.getEncoding());
          WavUtils.writeHeader(resultFile, header);
      }
      /**
       * 合并文件。
       */
      private void mergePcmFile() {
          boolean mergeSuccess = mergePcmFiles(resultFile, files);
          if (!mergeSuccess) {
              notifyError("合并失败");
          }
      }
      /**
       * 合并PCM文件。
       *
       * @param recordFile 输出文件
       * @param files      多个文件源
       * @return 是否成功
       */
      private boolean mergePcmFiles(File recordFile, List files) {
          if (recordFile == null || files == null || files.size() <= 0) {
              return false;
          }
          FileOutputStream fos = null;
          BufferedOutputStream outputStream = null;
          byte[] buffer = new byte[1024];
          try {
              fos = new FileOutputStream(recordFile);
              outputStream = new BufferedOutputStream(fos);
              for (int i = 0; i < files.size(); i++) {
                  BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(files.get(i)));
                  int readCount;
                  while ((readCount = inputStream.read(buffer)) > 0) {
                      outputStream.write(buffer, 0, readCount);
                  }
                  inputStream.close();
              }
          } catch (Exception e) {
              LogUtils.e(e.getMessage());
              return false;
          } finally {
              try {
                  if (outputStream != null) {
                      outputStream.close();
                  }
                  if (fos != null) {
                      fos.close();
                  }
              } catch (IOException e) {
                  e.printStackTrace();
              }
          }
          for (int i = 0; i < files.size(); i++) {
              files.get(i).delete();
          }
          files.clear();
          return true;
      }
      /**
       * 根据当前的时间生成相应的文件名。
       */
      private String getTempFilePath() {
          String fileDir = String.format(Locale.getDefault(), "%s/Record/", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).getAbsolutePath());
          if (!FileUtils.createOrExistsDir(fileDir)) {
              LogUtils.e("文件夹创建失败:" + fileDir);
          }
          String fileName = String.format(Locale.getDefault(), "record_tmp_%s", FileUtils.getNowString(new SimpleDateFormat("yyyyMMdd_HH_mm_ss", Locale.SIMPLIFIED_CHINESE)));
          return String.format(Locale.getDefault(), "%s%s.pcm", fileDir, fileName);
      }
      /**
       * 表示当前录制状态。
       */
      public enum RecordState {
          /**
           * 空闲状态。
           */
          IDLE,
          /**
           * 录音中。
           */
          RECORDING,
          /**
           * 暂停中。
           */
          PAUSE,
          /**
           * 正在停止。
           */
          STOP,
          /**
           * 录音流程结束(转换结束)。
           */
          FINISH
      }
      

      在Mp3EncodeThread中init初始化录制参数,然后调用encode进行编码录制,录制完成调用flush把缓冲区冲一下水,清洗一下。最后调用一下close,完美。

      最后附上录制流程细节的代码。

      package com.dorachat.dorachat.recorder.mp3;
      import com.dorachat.dorachat.recorder.RecordConfig;
      import com.dorachat.dorachat.recorder.RecordService;
      import java.io.File;
      import java.io.FileNotFoundException;
      import java.io.FileOutputStream;
      import java.io.IOException;
      import java.util.Collections;
      import java.util.LinkedList;
      import java.util.List;
      public class Mp3EncodeThread extends Thread {
          private static final String TAG = Mp3EncodeThread.class.getSimpleName();
          private List cacheBufferList = Collections.synchronizedList(new LinkedList());
          private File file;
          private FileOutputStream os;
          private byte[] mp3Buffer;
          private EncodeFinishListener encodeFinishListener;
          /**
           * 是否已停止录音。
           */
          private volatile boolean isOver = false;
          /**
           * 是否继续轮询数据队列。
           */
          private volatile boolean start = true;
          public Mp3EncodeThread(File file, int bufferSize) {
              this.file = file;
              mp3Buffer = new byte[(int) (7200 + (bufferSize * 2 * 1.25))];
              RecordConfig currentConfig = RecordService.getCurrentConfig();
              int sampleRate = currentConfig.getSampleRate();
              Mp3Encoder.init(sampleRate, currentConfig.getChannelCount(), sampleRate, currentConfig.getRealEncoding());
          }
          @Override
          public void run() {
              try {
                  this.os = new FileOutputStream(file);
              } catch (FileNotFoundException e) {
                  return;
              }
              while (start) {
                  ChangeBuffer next = next();
                  lameData(next);
              }
          }
          public void addChangeBuffer(ChangeBuffer changeBuffer) {
              if (changeBuffer != null) {
                  cacheBufferList.add(changeBuffer);
                  synchronized (this) {
                      notify();
                  }
              }
          }
          public void stopSafe(EncodeFinishListener encodeFinishListener) {
              this.encodeFinishListener = encodeFinishListener;
              isOver = true;
              synchronized (this) {
                  notify();
              }
          }
          private ChangeBuffer next() {
              for (;;) {
                  if (cacheBufferList == null || cacheBufferList.size() == 0) {
                      try {
                          if (isOver) {
                              finish();
                          }
                          synchronized (this) {
                              wait();
                          }
                      } catch (Exception e) {
                      }
                  } else {
                      return cacheBufferList.remove(0);
                  }
              }
          }
          private void lameData(ChangeBuffer changeBuffer) {
              if (changeBuffer == null) {
                  return;
              }
              short[] buffer = changeBuffer.getData();
              int readSize = changeBuffer.getReadSize();
              if (readSize > 0) {
                  int encodedSize = Mp3Encoder.encode(buffer, buffer, readSize, mp3Buffer);
                  try {
                      os.write(mp3Buffer, 0, encodedSize);
                  } catch (IOException e) {
                  }
              }
          }
          private void finish() {
              start = false;
              final int flushResult = Mp3Encoder.flush(mp3Buffer);
              if (flushResult > 0) {
                  try {
                      os.write(mp3Buffer, 0, flushResult);
                      os.close();
                  } catch (final IOException e) {
                  }
              }
              if (encodeFinishListener != null) {
                  encodeFinishListener.onFinish();
              }
          }
          public static class ChangeBuffer {
              private short[] rawData;
              private int readSize;
              public ChangeBuffer(short[] rawData, int readSize) {
                  this.rawData = rawData.clone();
                  this.readSize = readSize;
              }
              short[] getData() {
                  return rawData;
              }
              int getReadSize() {
                  return readSize;
              }
          }
          public interface EncodeFinishListener {
          
              /**
               * 格式转换完毕。
               */
              void onFinish();
          }
      }
      

      总结

      这只是Android音视频开发的冰山一角,主要是为了演示大概的开发流程,如果需要深入研究Android NDK,一定需要先把C/C++语言学好,这样才能走得更远。

转载请注明来自码农世界,本文标题:《Android LAME原生音频》

百度分享代码,如果开启HTTPS请参考李洋个人博客
每一天,每一秒,你所做的决定都会改变你的人生!

发表评论

快捷回复:

评论列表 (暂无评论,72人围观)参与讨论

还没有评论,来说两句吧...

Top