HarmonyOS 音视频之Lame MP3编码实现

MP3是一种广泛使用的音频压缩格式,以其高效的压缩算法和广泛的兼容性而闻名,是最为普及的音频格式之一,几乎所有的音频播放设备、移动设备、计算机和音频软件都支持 MP3 播放。这使得 MP3 成为事实上的标准格式,比起压缩性能兼容性才更是MP3 市场占有率的保障。

但是MP3是版权编码,一般手机厂商不包含MP3硬件编码器,只有MP3硬件解码器,市面上最常用的开源MP3软编码器是Lame,本文我们以Lame 为例,从跨平台编译到接入应用全流程基于Lame 事件MP3 软编码器。

编译Lame

一般三方开源的C/C++库有三种编译方式:

  • cmake
  • make
  • configure

不同构建脚本要配置不同变量,lame 是基于Configure 构建脚本的,通过./Configure -h查看配置参数。OpenHarmony 提供了一套交叉编译框架lycium,根据模版配置三方库信息后执行构建脚本即可。tpc_c_cplusplus 项目thirdparty中已经包含了 lame模块,我们直接编译即可。

进入lycium目录下执行:./build.sh lame即可开始编译,编译完成后可以在lycium目录下看到user/lame目录,里面已经编译好对应的动态库。

在macos arm版本电脑中编译报错,在tpc_c_cplusplus/thirdparty/lame/lame-3.100/armeabi-v7a-build/build.log查看日志后:

1 error generated.
1 error generated.
1 error generated.
../../mpglib/dct64_i386.c:34:10: fatal error: 'config.h' file not found
#include <config.h>
         ^~~~~~~~~~
make[2]: *** [tabinit.lo] Error 1
make[2]: *** Waiting for unfinished jobs....
make[2]: *** [common.lo] Error 1
make[2]: *** [interface.lo] Error 1
make[2]: *** [decode_i386.lo] Error 1
make[2]: *** [layer1.lo] Error 1
1 error generated.
1 error generated.
make[2]: *** [layer2.lo] Error 1
make[2]: *** [dct64_i386.lo] Error 1
1 error generated.
make[2]: *** [layer3.lo] Error 1
make[1]: *** [all-recursive] Error 1
** [tabinit.lo] Error 1
make[2]: *** Waiting for unfinished jobs....
make[2]: *** [common.lo] Error 1
make[2]: *** [interface.lo] Error 1
make[2]: *** [decode_i386.lo] Error 1
make[2]: *** [layer1.lo] Error 1
1 error generated.
1 error generated.
make[2]: *** [layer2.lo] Error 1
make[2]: *** [dct64_i386.lo] Error 1
1 error generated.
make[2]: *** [layer3.lo] Error 1
make[1]: *** [all-recursive] Error 1
make: *** [all] Error 2
"build.log" 310L, 21047Bmake: *** [all] Error 2

生成config.h头文件失败,lame是基于Configure的,依赖的一些工具版本不匹配导致,更换Intel电脑后编译成功:

HarmonyOS 音视频之Lame MP3编码实现

接入HarmonyOS 工程

接下来将编译好的lame动态库以预编译的方式接入到我们的工程中。

首先创建native c++工程:

HarmonyOS 音视频之Lame MP3编码实现

在cpp目录下新建third_party/lame文件夹,将编译好的so和导出的头文件拷贝到该路径下:

HarmonyOS 音视频之Lame MP3编码实现

接下来修改CMakeLists.txt,导入预编译好的lame动态库,添加链接:

add_library(lame SHARED IMPORTED)  
set_target_properties(lame  
    PROPERTIES  
    IMPORTED_LOCATION ${CMAKE_CURRENT_SOURCE_DIR}/third_party/lame/libs/${OHOS_ARCH}/libmp3lame.so)  
  
add_library(audio_engine SHARED napi_init.cpp)  
target_link_libraries(audio_engine PUBLIC libace_napi.z.so lame)

要使用lame的头文件,还需要配置头文件搜索路径:

  include_directories(${NATIVERENDER_ROOT_PATH}  
                    ${NATIVERENDER_ROOT_PATH}/include  
                   ${NATIVERENDER_ROOT_PATH}/third_party/lame/include  
                    )

接入到工程后就可以使用了。

录制MP3 音频文件

编码库接入完成后需要在C++侧封装接口供TS侧调用,这里我们提供三个最基础的接口:

  1. 创建编码器
  2. 对数据进行编码
  3. 关闭编码器

创建编码器

lame创建编码器后需要设置编码参数:

  • 输入音频采样率
  • 输入音频声道数
  • 输出采样率
  • 输出码率
  • 输出质量

定义initLame方法,提供五个参数:

size_t argc = 5;  
napi_value args[5] = {nullptr};  
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);  
int inSamplerate;  
napi_get_value_int32(env, args[0], &inSamplerate);  
  
int inChannel;  
napi_get_value_int32(env, args[1], &inChannel);  
  
int outSamplerate;  
napi_get_value_int32(env, args[2], &outSamplerate);  
  
int outBitrate;  
napi_get_value_int32(env, args[3], &outBitrate);  
  
int quality;  
napi_get_value_int32(env, args[4], &quality);

接下来配置编码器:

lame = lame_init();  
lame_set_in_samplerate(lame, inSamplerate);  
lame_set_num_channels(lame, inChannel);//输入流的声道  
lame_set_out_samplerate(lame, outSamplerate);  
lame_set_brate(lame, outBitrate);  
lame_set_quality(lame, quality);  
lame_init_params(lame);

编码音频数据

lame编码函数原型如下:

/*  
 * input pcm data, output (maybe) mp3 frames. * This routine handles all buffering, resampling and filtering for you. * * return code     number of bytes output in mp3buf. Can be 0 *                 -1:  mp3buf was too small *                 -2:  malloc() problem *                 -3:  lame_init_params() not called *                 -4:  psycho acoustic problems * * The required mp3buf_size can be computed from num_samples, * samplerate and encoding rate, but here is a worst case estimate: * * mp3buf_size in bytes = 1.25*num_samples + 7200 * * I think a tighter bound could be:  (mt, March 2000) * MPEG1: *    num_samples*(bitrate/8)/samplerate + 4*1152*(bitrate/8)/samplerate + 512 * MPEG2: *    num_samples*(bitrate/8)/samplerate + 4*576*(bitrate/8)/samplerate + 256 * * but test first if you use that! * * set mp3buf_size = 0 and LAME will not check if mp3buf_size is * large enough. * * NOTE: * if gfp->num_channels=2, but gfp->mode = 3 (mono), the L & R channels * will be averaged into the L channel before encoding only the L channel * This will overwrite the data in buffer_l[] and buffer_r[]. **/  
int CDECL lame_encode_buffer (  
        lame_global_flags*  gfp,           /* global context handle         */  
        const short int     buffer_l [],   /* PCM data for left channel     */  
        const short int     buffer_r [],   /* PCM data for right channel    */  
        const int           nsamples,      /* number of samples per channel */  
        unsigned char*      mp3buf,        /* pointer to encoded MP3 stream */  
        const int           mp3buf_size ); /* number of valid octets in this  
                                              stream                        */

需要传入左声道,右声道数据,以及每个声道采样个数、输出编码后数据buffer、输出编码后数据buffer大小。

NAPI 接口需要传入三个buffer:

static napi_value NAPI_Global_encodeLame(napi_env env, napi_callback_info info)  
{  
    size_t argc = 4;  
    napi_value args[4] = {nullptr};  
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);  
  
    napi_typedarray_type type;// 数据类型  
    napi_value left_input_buffer;  
    size_t byte_offset;//数据偏移  
    size_t length;//数据字节大小  
    napi_get_typedarray_info(env, args[0], &type, &length, NULL, &left_input_buffer, &byte_offset);  
        void* leftBuffer;   
size_t leftLength;   
napi_get_arraybuffer_info(env, left_input_buffer, &leftBuffer, &leftLength);   
      
napi_value right_input_buffer;  
    napi_get_typedarray_info(env, args[1], &type, &length, NULL, &right_input_buffer, &byte_offset);  
    void* rightBuffer;   
size_t rightLength;   
napi_get_arraybuffer_info(env, right_input_buffer, &rightBuffer, &rightLength);   
      
int samples;  
    napi_get_value_int32(env, args[2], &samples);  
    napi_value mp3_output_buffer;  
    napi_get_typedarray_info(env, args[3], &type, &length, NULL, &mp3_output_buffer, &byte_offset);  
    void* mp3Buffer;   
size_t mp3Length;   
napi_get_arraybuffer_info(env, mp3_output_buffer, &mp3Buffer, &mp3Length);   
      
int result = lame_encode_buffer(lame, (short int*)leftBuffer, (short int*)rightBuffer,  
          samples, (unsigned char*)mp3Buffer, mp3Length);    
napi_value result_value;  
    napi_create_int32(env, result,&result_value);  
    return result_value;  
        
}

主要用到napi_get_typedarray_info和napi_get_arraybuffer_info方法:

  • Native C++ 侧接受传入的 ArkTS Array,通过 napi_get_typedarray_info 将获取到的数据传入数组 typedarray 生成 input_buffer ,然后通过 napi_get_arraybuffer_info 获取数组数据。
  • ArkTS 侧 接收 Native C++ 侧返回的 Array,通过 napi_create_arraybuffer 创建一个 arraybuffer 数组,根据创建的 arraybuffer 通过 napi_create_typedarray 创建一个 typedarray 并将 arraybuffer 存入 output_array,然后给 arraybuffer 赋值,最后返回 output_array。

关闭编码器

调用lame_close 关闭编码器,关闭前需要lame_encode_flush 方法将编码器中缓存的数据获取出来保证数据的完整性:

static napi_value NAPI_Global_flushLame(napi_env env, napi_callback_info info)  
{  
    size_t argc = 1;  
    napi_value args[1] = {nullptr};  
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);  
    napi_typedarray_type type;// 数据类型  
    napi_value output_buffer;  
    size_t byte_offset;//数据偏移  
    size_t length;//数据字节大小  
    napi_get_typedarray_info(env, args[0], &type, &length, NULL, &output_buffer, &byte_offset);  
        void* outBuffer;   
size_t outLength;   
napi_get_arraybuffer_info(env, output_buffer, &outBuffer, &outLength);   
int result = lame_encode_flush(lame, (unsigned char *)outBuffer, outLength);  
    napi_value result_value;  
    napi_create_int32(env, result,&result_value);  
    return result_value;  
}

遇到问题

  1. 链接mp3lame库后调用native 方法报错:Cannot read property encodeLame of undefined,原因是编译出的mp3lame.so包含版本,直接去掉版本号拷贝到工程出现该问题,保留版本号的最高位后问题解决;
  2. 线程问题,编码是耗时操作,需要在独立线程中处理,在C++侧创建线程和缓存来交互。

总结

Android、iOS手机由于版权问题没有直接提供MP3 硬件解码器,在录制MP3 时都是采用Lame三方库。本文介绍了HarmonyOS 下实现MP3 软件编码的全流程:从三方库编译到集成进工程封装调用,虽然HarmonyOS 提供了MP3 的硬件编码方式,但是本文以Lame 为例提供了集成三方C++ 库的最佳实践。

来源:HarmonyOS 技术领航者公众号每天分享一点鸿蒙知识,与您一同成长。同时它还是一个 HarmonyOS 开发助手,可以回答鸿蒙开发过程中遇到的各种问题。

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

(0)

相关推荐

发表回复

登录后才能评论