DEV Community

Cover image for HarmonyOS Audio-Video: Lame MP3 Encoding Implementation
kouwei qing
kouwei qing

Posted on • Edited on

HarmonyOS Audio-Video: Lame MP3 Encoding Implementation

HarmonyOS Audio-Video: Lame MP3 Encoding Implementation

Background

MP3 is a widely used audio compression format, renowned for its efficient compression algorithm and broad compatibility. It is one of the most popular audio formats, supported by almost all audio playback devices, mobile devices, computers, and audio software. This makes MP3 a de facto standard format—compatibility, rather than compression performance, is the key to MP3's market dominance.

However, MP3 is a copyrighted encoding. Most mobile phone manufacturers do not include MP3 hardware encoders, only hardware decoders. The most commonly used open-source MP3 software encoder is Lame. This article takes Lame as an example to implement an MP3 software encoder based on Lame, covering the full process from cross-platform compilation to application integration.

Compiling Lame

There are three common compilation methods for third-party open-source C/C++ libraries:

  • cmake
  • make
  • configure

Different build scripts require configuring different variables. Lame uses the Configure build script, and configuration parameters can be viewed via ./Configure -h. OpenHarmony provides a cross-compilation framework called lycium. After configuring third-party library information using a template, execute the build script. The lame module is already included in the tpc_c_cplusplus project's thirdparty directory, allowing direct compilation.

To compile, enter the lycium directory and run: ./build.sh lame. Upon completion, the user/lame directory in lycium will contain the compiled dynamic libraries.

When compiling on a macOS ARM-based computer, the following error occurred (viewed in 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
Enter fullscreen mode Exit fullscreen mode

The error indicated a failure to generate the config.h header file, caused by version mismatches in tools依赖 (dependencies) for the Configure-based Lame. Compilation succeeded after switching to an Intel-based computer:

Integrating into a HarmonyOS Project

Next, integrate the compiled Lame dynamic library into the project via precompilation.

First, create a native C++ project:

Create a third_party/lame folder under the cpp directory, and copy the compiled SO files and exported header files to this path:

Modify CMakeLists.txt to import the precompiled Lame dynamic library and add linking:

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)
Enter fullscreen mode Exit fullscreen mode

Configure header file search paths to use Lame's headers:

include_directories(${NATIVERENDER_ROOT_PATH}  
                    ${NATIVERENDER_ROOT_PATH}/include  
                   ${NATIVERENDER_ROOT_PATH}/third_party/lame/include  
                    )
Enter fullscreen mode Exit fullscreen mode

The library is now ready for use after integration.

Recording MP3 Audio Files

After integrating the encoding library, encapsulate C++ interfaces for TS-side calling. Here, we provide three basic interfaces:

  1. Create an encoder
  2. Encode data
  3. Close the encoder

Creating the Encoder

After creating the Lame encoder, set encoding parameters:

  • Input audio sampling rate
  • Input audio channel count
  • Output sampling rate
  • Output bitrate
  • Output quality

Define the initLame method with five parameters:

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);
Enter fullscreen mode Exit fullscreen mode

Next, configure the encoder:

lame = lame_init();  
lame_set_in_samplerate(lame, inSamplerate);  
lame_set_num_channels(lame, inChannel);// Input stream channels  
lame_set_out_samplerate(lame, outSamplerate);  
lame_set_brate(lame, outBitrate);  
lame_set_quality(lame, quality);  
lame_init_params(lame);
Enter fullscreen mode Exit fullscreen mode

Encoding Audio Data

The Lame encoding function prototype is as follows:

/*  
 * 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: Psychoacoustic problems  
 *  
 * The required mp3buf_size can be computed from num_samples, samplerate, and encoding rate.  
 * Here's a worst-case estimate:  
 *   mp3buf_size (bytes) = 1.25 * num_samples + 7200  
 *  
 * A tighter bound (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  
 * Test first if using this!  
 *  
 * 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), L & R channels will be averaged into L  
 * before encoding only the L channel. This overwrites 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,      /* Samples per channel           */  
        unsigned char*      mp3buf,        /* Encoded MP3 stream            */  
        const int           mp3buf_size ); /* Valid octets in mp3buf        */
Enter fullscreen mode Exit fullscreen mode

This requires left and right channel data, samples per channel, output buffer, and buffer size.

The NAPI interface accepts three buffers:

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; // Data type  
    napi_value left_input_buffer;  
    size_t byte_offset; // Data offset  
    size_t length; // Byte size  
    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;  
}
Enter fullscreen mode Exit fullscreen mode

Key methods used: napi_get_typedarray_info and napi_get_arraybuffer_info:

  • The Native C++ side receives the ArkTS Array, uses napi_get_typedarray_info to obtain data for the typedarray, and then uses napi_get_arraybuffer_info to fetch array data.
  • The ArkTS side receives the Array from the Native C++ side, creates an arraybuffer via napi_create_arraybuffer, generates a typedarray with napi_create_typedarray, stores the arraybuffer in output_array, assigns values to the arraybuffer, and returns output_array.

Closing the Encoder

Call lame_close to close the encoder. Before closing, use lame_encode_flush to retrieve cached data and ensure data integrity:

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; // Data type  
    napi_value output_buffer;  
    size_t byte_offset; // Data offset  
    size_t length; // Byte size  
    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;  
}
Enter fullscreen mode Exit fullscreen mode

Issues Encountered

  1. After linking the mp3lame library, calling a native method threw an error: Cannot read property encodeLame of undefined. The issue occurred because the compiled mp3lame.so included a version number, which was removed when copying to the project. The problem was resolved by retaining the highest bit of the version number.
  2. Threading issues: Encoding is a time-consuming operation that requires processing in a separate thread. Create threads and caches on the C++ side for interaction.

Summary

Due to copyright restrictions, Android and iOS devices do not directly provide MP3 hardware encoders, relying on the Lame third-party library for MP3 recording. This article introduces the full process of implementing MP3 software encoding on HarmonyOS—from third-party library compilation to project integration and interface encapsulation. Although HarmonyOS offers hardware MP3 encoding, this article provides best practices for integrating third-party C++ libraries using Lame as an example.

Top comments (0)