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
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)
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
)
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:
- Create an encoder
- Encode data
- 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);
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);
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 */
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;
}
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 thetypedarray
, and then usesnapi_get_arraybuffer_info
to fetch array data. - The ArkTS side receives the Array from the Native C++ side, creates an
arraybuffer
vianapi_create_arraybuffer
, generates atypedarray
withnapi_create_typedarray
, stores thearraybuffer
inoutput_array
, assigns values to thearraybuffer
, and returnsoutput_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;
}
Issues Encountered
- After linking the
mp3lame
library, calling a native method threw an error:Cannot read property encodeLame of undefined
. The issue occurred because the compiledmp3lame.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. - 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)