NDK(Native Development Kit)作 HarmonyOS SDK提供的 Native API、编译脚本及工具链集合,便于开发者用 C 或 C++ 实现应用关键功能。它涵盖部分基础底层能力,像 libc 等库以及 ArkTS/JS 与 C 跨语言的 Node-API ,但无 ArkTS/JS API 完整能力。运行时,开发者可借 NDK 的 Node-API 接口访问、创建、操作 JS 对象,也允许 JS 对象使用 Native 动态库。其适用于性能敏感(如游戏、物理模拟)、复用已有 C 或 C++ 库,以及针对 CPU 特性定制库(如 Neon 加速)等场景;而纯 C 或 C++ 应用、期望在多数 HarmonyOS 设备保持兼容的应用则不建议使用。开发者需掌握 Node-API(曾用名 NAPI,基于 Node.js 的 Node-API 扩展但不完全兼容)等基本概念,C API 作为 NDK 曾用名已弃用。
17.1 Native基本知识和开发流程
进行HarmonyOS的Native开发,开发者需掌握一些前置知识。LinuxC语言编程知识必不可少,因为HarmonyOS内核、libc基础库基于POSIX等标准扩展,这有助于理解NDK开发。CMake是HarmonyOS默认支持的构建系统,要通过官方文档了解其基础用法。熟悉NodeAddons开发模式也很关键,ArkTS采用Node-API作为跨语言调用接口,掌握此开发模式能更好使用NDK中的Node-API。同时,具备Clang/LLVM编译器基础知识可编译出更优的Native动态库。NDK是HarmonyOSSDK提供的NativeAPI、编译脚本及工具链集合,用于C或C++实现应用关键功能,覆盖部分基础底层能力但无ArkTS/JSAPI完整能力,运行态可通过Node-API接口操作JS对象及让JS对象使用Native动态库。适用场景有性能敏感场景、复用已有C或C++库、针对CPU特性定制库等,而纯C或C++应用、追求多设备兼容的应用不建议使用。开发前要了解NDK目录结构,build目录存放ohos.toolchain.cmake,编译时用CMAKE_TOOLCHAIN_FILE指定路径;build-tools文件夹和llvm文件夹分别存放编译工具和编译器,之后依据需求从标准C库、Node-API等常用模块中选择进行开发。
17.1.1 NDK 组成架构
NDK(Native Development Kit)主要由以下几部分组成
- Native API
- 标准库接口
-
- 标准 C 库:以musl为基础提供标准C库接口,为开发者在C语言编程时提供基础的函数和类型定义,如字符串处理、内存管理等功能。
- 标准C++库:提供C++运行时库libc++_shared,支持C++语言的特性,方便开发者使用C++进行开发,如面向对象编程、模板等功能。
- 系统功能接口
-
- 日志接口:提供打印日志到系统的HiLog接口,开发者可以利用该接口在开发和调试过程中输出关键信息,便于排查问题和监控程序运行状态。
- 图形接口
- Drawing:系统提供的2D图形库,允许开发者在surface上进行2D图形的绘制,可用于开发一些简单的图形界面或游戏场景。
- OpenGL:提供 OpenGL 3D图形接口,支持开发者创建复杂的3D图形应用,如3D游戏、3D建模等。
- 音频接口:OpenSL ES是用于 2D、3D音频加速的接口库,能够帮助开发者实现音频的播放、录制和处理等功能,提升音频方面的性能。
- 资源访问接口:Rawfile是应用资源访问接口,可用于读取应用中打包的各种资源,如图像、音频、文本等文件。
- 组件接口:ArkUI XComponent组件提供surface与触屏事件等接口,方便开发者开发高性能图形应用,处理用户的触摸交互等操作。
- 并发编程接口:FFRT是基于任务的并发编程框架,帮助开发者实现多任务并发执行,提高程序的性能和响应速度。
- 异步 IO 接口:libuv是三方异步IO库,用于处理异步输入输出操作,避免阻塞主线程,提升程序的效率。
- 数据压缩接口:zlib库提供基本的数据压缩、解压接口,可用于处理数据的压缩存储和传输等场景。
- 跨语言调用接口:Node - API(曾用名 NAPI)是 HarmonyOS中提供ArkTS/JS与C/C++跨语言调用的接口,允许开发者在不同语言编写的代码之间进行交互。
- 编译脚本
- ohos.toolchain.cmake:存放在 build 目录下,是预定义的toolchain脚本文件。CMake编译时需要读取该文件中的默认值,如编译器架构、C++库链接方式等。在编译时,会通过CMAKE_TOOLCHAIN_FILE指出该文件的路径,以便CMake在编译时定位到该文件。
- 编译工具链
- build - tools文件夹:该文件夹放置着 NDK 提供的编译工具,例如CMake就是其中重要的编译工具之一,可用于构建项目。
- llvm 文件夹:放置 NDK 提供的编译器,这些编译器用于将C或C++ 代码编译成可在目标设备上运行的机器码。
17.1.2 Node-API 关键交互流程
ArkTS和C++之间的交互流程,主要分为以下两步:
- 初始化阶段
当ArkTS侧在import一个Native模块时,ArkTS引擎会调用ModuleManager加载模块对应的so及其依赖。首次加载时会触发模块的注册,将模块定义的方法属性挂载到exports对象上并返回该对象。
- 调用阶段
当ArkTS侧通过上述import返回的对象调用方法时,ArkTS引擎会找到并调用对应的C/C++方法。
17.1.3 Native 开发流程
下面通过DevEco Studio的NDK工程模板,来演示如何创建一个NDK工程,以及对应的工程目录介绍,并且介绍基本代码的使用。
- 选择Native C++工程模板
- 创建后目录
在工程配置页面,根据向导配置工程的基本信息后,单击Finish,工具会自动生成示例代码和相关资源,等待工程创建完成。
- entry:应用模块,编译构建生成一个HAP。
-
- src > main > cpp > types:用于存放C++的API接口描述文件
- src > main > cpp > types > libentry > index.d.ts:描述C++ API接口行为,如接口名、入参、返回参数等。
- src > main > cpp > types > libentry> oh-package.json5:配置.so三方包声明文件的入口及包名。
- src > main > cpp > CMakeLists.txt:CMake配置文件,提供CMake构建脚本。
- src > main > cpp > napi_init.cpp:定义C++ API接口的文件。
- src > main > ets:用于存放ArkTS源码。
- src > main > resources:用于存放应用所用到的资源文件,如图形、多媒体、字符串、布局文件等。
17.2 Native 代码开发
HarmonyOS NDK提供多个开放能力库,如图形图像、内存管理、设备管理等,供开发者实现代码逻辑;同时提供业界标准库,如libc标准库、标准C++库、Node-API等。
17.2.1 C/C++标准库
- C++ 兼容性
系统库和应用Native库依赖的C++标准库分别随系统镜像和编译SDK版本升级,跨版本会有ABI兼容性问题。系统库用libc++.so(命名空间__h),应用Native库用libc++_shared.so(命名空间__n1),二者不能混用。使用共享库HAR包构建应用时版本不同可能不兼容,可用相同SDK更新HAR包解决。已知问题如API9及之前SDK的 libc++_shared.so无__emutls_get_address符号,需更新SDK版本。
- musl libc动态链接器
- 动态库加载命名空间隔离:设计目的是管控native库资源访问,动态链接器加载共享库需关联具体命名空间(ns)。HarmonyOS有default ns(搜索系统目录 so)、ndk ns(搜索 NDK 接口 so)、app ns(搜索应用安装路径 so)。default ns和ndk ns 可互访,不能访问app ns;app ns能访问ndk ns,不能访问default ns。
- rpath机制:运行时指定共享库搜索路径,可解决应用多native库加载路径管理问题。
- 其他特性:支持dlclose真实卸载动态库;支持symbol - version机制解决符号重定位和重复符号问题;网络接口select支持fd fortify检测;自API12起部分接口支持特定locale设置及相关函数,strtod_l和wcstod_l 不支持十六进制转换。
17.2.2 使用Node-API实现跨语言交互
使用 Node - API实现跨语言交互,需依其机制完成模块注册和加载等操作。ArkTS/JS侧通过 import 对应so库调用C++方法,Native侧在.cpp文件中实现模块注册,要提供lib库名并在注册回调中定义接口映射关系。
以实现add ()(ArkTS/JS 侧)和Add ()(Native 侧)接口为例,介绍流程:
- Native 侧方法实现
设置模块注册信息:ArkTS侧import native模块加载so时,调用napi_module_register方法,涉及napi_module 的.nm_register_func(模块初始化函数)和.nm_modname(模块名,即ArkTS侧引入so库名)两个关键属性。创建工程后相关代码已在napi_init.cpp配置好。
// entry/src/main/cpp/napi_init.cpp
// 准备模块加载相关信息,将上述Init函数与本模块名等信息记录下来。
static napi_module demoModule = {
.nm_version = 1,
.nm_flags = 0,
.nm_filename = nullptr,
.nm_register_func = Init,
.nm_modname = "entry",
.nm_priv = nullptr,
.reserved = {0},
};
// 加载so时,该函数会自动被调用,将上述demoModule模块注册到系统中。
extern "C" __attribute__((constructor)) void RegisterDemoModule() {
napi_module_register(&demoModule);
}
在napi_init.cpp中实现ArkTS与C++ 接口的绑定和映射,在exports对象上挂载Native方法,在index.d.ts提供JS侧接口方法,在oh-package.json5将index.d.ts与cpp文件关联,在CMakeLists.txt配置打包参数,同时实现CallNative和NativeCallArkTS接口。
以下代码是模块初始化函数Init,用于将ArkTS 接口与C++接口进行绑定和映射,通过napi_define_properties函数在exports对象上挂载CallNative和NativeCallArkTS两个Native方法。
// entry/src/main/cpp/napi_init.cpp
EXTERN_C_START
// 模块初始化
static napi_value Init(napi_env env, napi_value exports) {
// ArkTS接口与C++接口的绑定和映射
napi_property_descriptor desc[] = {
// 注:仅需复制以下两行代码,Init在完成创建Native C++工程以后在napi_init.cpp中已配置好。
{"callNative", nullptr, CallNative, nullptr,
nullptr, nullptr, napi_default, nullptr},
{"nativeCallArkTS", nullptr, NativeCallArkTS,
nullptr, nullptr, nullptr, napi_default, nullptr}
};
// 在exports对象上挂载CallNative/NativeCallArkTS两个Native方法
napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
return exports;
}
EXTERN_C_END
以下代码在index.d.ts文件中提供了JS侧的接口方法,定义了callNative和nativeCallArkTS两个函数的类型。
// entry/src/main/cpp/types/libentry/index.d.ts
export const callNative: (a: number, b: number) => number;
export const nativeCallArkTS: (cb: (a: number) => number) => number;
以下代码在oh-package.json5文件中将index.d.ts与cpp文件关联起来,指定了模块的名称、类型文件路径等信息。
// entry/src/main/cpp/types/libentry/oh-package.json5
{
"name": "libentry.so",
"types": "./index.d.ts",
"version": "",
"description": "Please describe the basic information."
}
以下代码是CMakeLists.txt文件,用于配置CMake打包参数,包括设置CMake的最低版本、项目名称、包含目录、添加库以及指定链接的库等。
# entry/src/main/cpp/CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
project(MyApplication2)
set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
include_directories(${NATIVERENDER_ROOT_PATH}
${NATIVERENDER_ROOT_PATH}/include)
# 添加名为entry的库
add_library(entry SHARED napi_init.cpp)
# 构建此可执行文件需要链接的库
target_link_libraries(entry PUBLIC libace_napi.z.so)
以下代码实现了Native侧的CallNative 接口,该接口接收两个参数,将它们相加并返回结果。
// entry/src/main/cpp/napi_init.cpp
static napi_value CallNative(napi_env env, napi_callback_info info)
{
size_t argc = 2;
// 声明参数数组
napi_value args[2] = {nullptr};
// 获取传入的参数并依次放入参数数组中
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
// 依次获取参数
double value0;
napi_get_value_double(env, args[0], &value0);
double value1;
napi_get_value_double(env, args[1], &value1);
// 返回两数相加的结果
napi_value sum;
napi_create_double(env, value0 + value1, &sum);
return sum;
}
以下代码实现了Native侧的NativeCallArkTS接口,该接口接收一个回调函数作为参数,创建一个整数作为ArkTS 的入参,调用传入的回调函数并返回其结果。
static napi_value NativeCallArkTS(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);
// 创建一个int,作为ArkTS的入参
napi_value argv = nullptr;
napi_create_int32(env, 2, &argv );
// 调用传入的callback,并将其结果返回
napi_value result = nullptr;
napi_call_function(env, nullptr, args[0], 1, &argv, &result);
return result;
}
- ArkTS侧调用C/C++方法实现
ArkTS侧通过import引入Native侧包含处理逻辑的so来使用C/C++方法,如在Index.ets文件中引入并调用相关方法。以下代码在ArkTS的Index组件中,通过import引入Native能力,定义了两个文本组件,分别用于调用callNative和nativeCallArkTS方法,并显示调用结果。
// entry/src/main/ets/pages/Index.ets
// 通过import的方式,引入Native能力。
import nativeModule from 'libentry.so'
@Entry
@Component
struct Index {
@State message: string = 'Test Node-API callNative result: ';
@State message2: string = 'Test Node-API nativeCallArkTS result: ';
build() {
Row() {
Column() {
// 第一个按钮,调用add方法,对应到Native侧的CallNative方法,进行两数相加。
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
.onClick(() => {
this.message += nativeModule.callNative(2, 3);
})
// 第二个按钮,调用nativeCallArkTS方法,对应到Native的NativeCallArkTS,在Native调用ArkTS function。
Text(this.message2)
.fontSize(50)
.fontWeight(FontWeight.Bold)
.onClick(() => {
this.message2 += nativeModule.nativeCallArkTS((a: number)=> {
return a * 2;
});
})
}
.width('100%')
}
.height('100%')
}
}
- 约束限制
- SO命名规则:导入与注册模块名大小写一致,如模块名entry,so名为libentry.so,相关字段设置和ArkTS 侧引用需对应。
- 注册建议:nm_register_func对应函数(如 Init)加static防符号冲突,模块注册入口函数名(如 RegisterDemoModule)需确保唯一。
- 多线程限制:每个引擎实例对应一个JS线程,对象不能跨线程操作,Node-API接口和Native接口入参env都只能在 JS 线程使用。
17.3 案例实战
本节运用前面学过的知识,实现音频低延时录制与播放。
17.3.1 实现音频低时延录制与播放
本示例基于OH_Audio的能力,在Native侧实现了音频低时延录制和播放功能,其中录制使用了音频采集器AudioCapturer,播放使用音频渲染器AudioRenderer,参考本示例可学习OH_Audio的使用,帮助应用开发音频录制与播放场景。
- 弹出麦克风权限访问提示框,点击“允许”,如果点击"禁止"则不可进行录制,需要用户去设置页面给应用授权后方可正常录制
- 在主界面点击“录制和播放”,进入音频录制界面,音频录制界面默认是普通录制界面,打开低时延录制开关可进行低时延录制
- 点击录制按钮,开始录制,开始录制后低时延录制开关变为不可点击状态,录音时间开始计时,5s内不允许结束,30s后会自动结束录制
- 点击暂停按钮,暂停录制,录音时间也停止计时
- 点击继续按钮,继续录制,录音时间继续计时
- 停止录制后,会生成录制结果,界面上有一个低时延播放开关和录制成功的音频播放器,点击低时延播放开关可打开低时延播放功能,点击播放可听到录制的音频,播放未结束之前低时延播放开关为不可点击状态
- 点击返回按按钮回到主页
17.3.1.1 案例效果截图
17.3.1.2 案例运用到的知识点
- 核心知识点
- 基于OpenHarmony平台的音频处理模块,主要实现了音频的录制和播放功能。
- 其他知识点
- ArkTS语言基础
- C++语言基础
- Node-API
- V2版状态管理:@ComponentV2/@Local/
- Stage模型
- 自定义组件和组件生命周期
- @Builder装饰器:自定义构建函数
- @Extend装饰器:定义扩展组件样式
- if/else:条件渲染
- 内置组件:Column/Row/Text/Image/Button/Progress
- 日志管理类的编写
- 常量与资源分类的访问
- @ohos.promptAction (弹窗)
- MVVM模式
17.3.1.3 代码结构解读
├──entry/src/main/cpp/
│ ├──types
│ │ └──libentry
│ │ ├──index.d.ts // 接口导出
│ │ └──oh-package.json5 // 配置管理文件
│ ├──AudioRecording.cpp // 调用native接口
│ └──CMakeLists.txt // 编译脚本
├──entry/src/main/ets/
│ ├──constants
│ │ └──CommonConstants.ets // 常量类
│ ├──entryability
│ │ └──EntryAbility.ets // 程序入口类
│ ├──pages
│ │ └──Index.ets // 主页入口
│ ├──utils
│ │ └──Logger.ets // 日志类
│ └──view
│ └──AudioRecording.ets // 音频录制与播放自定义组件
└──entry/src/main/resource // 应用静态资源目录
17.3.1.4 录音模块实现
- 头文件包含
- :标准库头文件,用于处理字符串。
- <sys/stat.h>:用于获取文件状态信息,如文件大小。
- napi/native_api.h:N-API 的头文件,用于创建 Node.js 原生插件。
- ohaudio/ 相关头文件:自定义的音频处理库头文件,提供音频捕获、渲染和流构建等功能。
#include <string>
#include <sys/stat.h>
#include "napi/native_api.h"
#include "ohaudio/native_audiocapturer.h"
#include "ohaudio/native_audiorenderer.h"
#include "ohaudio/native_audiostreambuilder.h"
#include "ohaudio/native_audiostream_base.h"
2. 全局常量和变量
- GLOBAL_RESMGR 和 TAG:全局常量,可能用于资源管理或日志标记。
- AudioTestConstants 命名空间:包含一些音频测试相关的常量,如参数索引、录制时间等。
- g_filePath:音频文件的路径。
- g_file:文件指针,用于操作音频文件。
- g_readEnd:标记文件是否读取结束。
- g_rendererLowLatency:标记音频渲染器是否使用低延迟模式。
- g_samplingRate 和 g_channelCount:音频采样率和声道数。
- audioCapturer、audioRenderer、builder 和 rendererBuilder:音频捕获器、渲染器和流构建器的指针。
const int GLOBAL_RESMGR = 0xFF00;
const char *TAG = "[Sample_audio]";
namespace AudioTestConstants {
// ...
}
static std::string g_filePath =
"/data/storage/el2/base/haps/entry/files/oh_test_audio.pcm";
FILE *g_file = nullptr;
bool g_readEnd = false;
bool g_rendererLowLatency = false;
int32_t g_samplingRate = 48000;
int32_t g_channelCount = 2;
static OH_AudioCapturer *audioCapturer;
static OH_AudioRenderer *audioRenderer;
static OH_AudioStreamBuilder *builder;
static OH_AudioStreamBuilder *rendererBuilder;
- 获取状态信息的函数
这些函数用于获取音频渲染器、捕获器的状态,文件状态,低延迟模式状态,已写入的帧数和文件大小,并将结果封装成 N-API 值返回给 JavaScript 调用者。
static napi_value GetRendererState(napi_env env, napi_callback_info info);
static napi_value GetCapturerState(napi_env env, napi_callback_info info);
static napi_value GetFileState(napi_env env, napi_callback_info info);
static napi_value GetFastState(napi_env env, napi_callback_info info);
static napi_value GetFramesWritten(napi_env env, napi_callback_info info);
static napi_value GetFileSize(napi_env env, napi_callback_info info);
- 音频回调函数
AudioCapturerOnReadData:音频捕获器读取数据时的回调函数,将捕获的数据写入文件。
AudioRendererOnWriteData:音频渲染器写入数据时的回调函数,从文件中读取数据到缓冲区。
static int32_t AudioCapturerOnReadData(
OH_AudioCapturer *capturer, void *userData, void *buffer, int32_t bufferLen);
static int32_t AudioRendererOnWriteData(
OH_AudioRenderer *renderer, void *userData, void *buffer, int32_t bufferLen);
- 音频捕获和渲染的初始化、控制函数
这些函数用于初始化音频捕获器和渲染器,设置参数和回调函数,启动、暂停、停止和释放音频设备。
static napi_value AudioCapturerLowLatencyInit(napi_env env,
napi_callback_info info);
static napi_value AudioCapturerInit(napi_env env, napi_callback_info info);
static napi_value AudioCapturerStart(napi_env env, napi_callback_info info);
static napi_value AudioCapturerPause(napi_env env, napi_callback_info info);
static napi_value AudioCapturerStop(napi_env env, napi_callback_info info);
static napi_value AudioCapturerRelease(napi_env env, napi_callback_info info);
static napi_value AudioRendererLowLatencyInit(napi_env env,
napi_callback_info info);
static napi_value AudioRendererInit(napi_env env, napi_callback_info info);
static napi_value AudioRendererStart(napi_env env, napi_callback_info info);
static napi_value AudioRendererPause(napi_env env, napi_callback_info info);
static napi_value AudioRendererStop(napi_env env, napi_callback_info info);
static napi_value AudioRendererRelease(napi_env env, napi_callback_info info);
- 关闭文件函数
用于关闭打开的音频文件。
static napi_value CloseFile(napi_env env, napi_callback_info info);
- 模块初始化和注册
- Init 函数:用于初始化 Node.js 模块,将所有的 C++ 函数封装成 JavaScript 方法,并添加到 exports 对象中。
- demoModule:定义了一个 N-API 模块结构体,包含模块的版本、名称、注册函数等信息。
- RegisterEntryModule 函数:在模块加载时自动调用,注册该模块。
EXTERN_C_START static napi_value Init(napi_env env, napi_value exports);
EXTERN_C_END
static napi_module demoModule = {
.nm_version = 1,
.nm_flags = 0,
.nm_filename = nullptr,
.nm_register_func = Init,
.nm_modname = "entry",
.nm_priv = ((void *)0),
.reserved = {0},
};
extern "C" __attribute__((constructor)) void RegisterEntryModule(void) { napi_module_register(&demoModule); }
17.3.1.5 声明Arkts可调用的类型文件
这段代码定义了一系列的 TypeScript 函数类型声明,这些声明对应了前面 C++ 代码中通过 N-API 封装成 Node.js 模块的各种音频操作和状态获取函数。通过这些类型声明,开发者在使用这些函数时可以获得更好的类型检查和代码提示
export const audioCapturesInit: () => void;
export const audioCapturesLowLatencyInit: () => void;
export const audioCapturesStart: () => void;
export const audioCapturesPause: () => void;
export const audioCapturesStop: () => void;
export const audioCapturesRelease: () => void;
export const audioRendererInit: () => void;
export const audioRendererLowLatencyInit: () => void;
export const audioRendererStart: () => void;
export const audioRendererPause: () => void;
export const audioRendererStop: () => void;
export const audioRendererRelease: () => void;
export const getRendererState: () => number;
export const getCapturerState: () => number;
export const getFileState: () => number;
export const getFastState: () => number;
export const getFramesWritten: () => number;
export const getFileSize: () => number;
export const closeFile: () => number;
17.3.16 编写Arkts模块实现页面调用
AudioRecording组件主要实现了音频录制、暂停、继续、停止以及播放等操作,同时允许用户选择低延迟模式进行音频捕获和渲染。组件会在不同的操作状态下展示相应的界面,并实时更新录制和播放的时间信息。
- 模块导入
代码开头导入了多个模块,这些模块提供了不同的功能支持:
@kit.AbilityKit中的abilityAccessCtrl和common,用于权限管理和获取 UI 能力上下文。
@kit.AudioKit中的audio,提供音频相关的功能和状态定义。
@kit.BasicServicesKit中的BusinessError和deviceInfo,用于错误处理和获取设备信息。
libentry.so作为 Node.js 原生插件,封装了底层的音频捕获和渲染功能。
CommonConstants包含了项目中使用的各种常量,如状态值、时间间隔等。
logger用于记录日志,方便调试和错误追踪。
- 组件状态管理
组件中定义了多个状态变量,使用@State装饰器使其成为响应式状态,当这些变量的值发生变化时,组件的 UI 会自动更新:
recordState:记录音频录制的状态,如初始、开始、暂停、继续、停止等。
title:音频文件的标题。
date:录制日期。
playSec:播放的秒数。
renderState:音频渲染器的状态。
recordSec:录制的秒数。
showTime:显示的时间信息。
isRecordOver:标记录制是否结束。
此外,还有一些私有变量用于控制音频捕获和渲染的低延迟模式。
- 生命周期方法
aboutToAppear:在组件即将显示时调用initResource方法,进行资源的初始化,如设置录制状态和获取当前日期。
aboutToDisappear:在组件即将消失时调用releaseResource方法,释放资源,包括清除定时器、释放音频捕获和渲染资源以及关闭文件。
- 界面构建
代码中定义了多个@Builder装饰的方法,用于构建不同状态下的界面:
InitRecord:初始状态界面,包含一个开始录制的按钮。点击按钮后,会请求麦克风权限,根据用户选择的模式初始化音频捕获器并启动录制。
StartedRecord:录制进行中界面,显示录制时间,提供停止和暂停按钮,用于控制录制过程。
PausedRecord:录制暂停界面,同样显示录制时间,提供停止和继续按钮,方便用户继续录制。
FinishedRecord:录制完成界面,显示一个灰色的录制图标,表示录制已结束。
- 主构建方法
build方法是组件的主构建逻辑,根据不同的状态渲染相应的界面。同时,提供了音频捕获和渲染的低延迟模式配置选项,使用Toggle开关控制。当录制完成后,会显示录制结果信息,包括标题、日期、录制时长、播放进度条等,并允许用户进行播放和暂停操作。
- 资源管理
initResource:初始化录制状态和日期,确保组件在开始时处于正确的状态。
releaseResource:在组件销毁时,清除定时器,释放音频捕获和渲染资源,关闭文件,避免资源泄漏。
- 音频操作方法
capturesStart:启动音频捕获,重置录制时间,开始计时,并更新录制状态为开始。
renderCreate:创建音频渲染器,并获取其当前状态。
renderStart:启动音频渲染,实时更新播放进度,根据已写入的帧数和文件大小计算播放时间。当文件读取结束时,停止渲染并进行相应处理。
renderPause:暂停音频渲染,清除定时器,更新渲染状态。
capturesContinue:继续音频捕获,继续计时,并更新录制状态为继续。
capturesStop:停止音频捕获,释放资源,关闭文件,更新录制状态为停止,并创建音频渲染器以便后续播放。
capturesPause:暂停音频捕获,清除定时器,更新录制状态为暂停。
- 辅助方法
formatNumber:将数字格式化为两位数的字符串,不足两位时前面补 0。
getDate:根据传入的模式返回不同格式的日期字符串。
getTimesBySecond:将秒数转换为HH:MM:SS格式的时间字符串,方便在界面上显示。
import { abilityAccessCtrl, common } from '@kit.AbilityKit';
import { audio } from '@kit.AudioKit';
import { BusinessError, deviceInfo } from '@kit.BasicServicesKit';
import testNapi from 'libentry.so';
import CommonConstants from '../constants/CommonConstants';
import { logger } from '../utils/Logger';
let context = getContext(this) as common.UIAbilityContext;
let atManager = abilityAccessCtrl.createAtManager();
@Component
export struct AudioRecording {
/**
* [init,started,continued,paused,stopped]
*/
@State recordState: string = CommonConstants.PLAY_INIT;
@State title: string = 'oh_test_audio';
@State date: string = '';
@State playSec: number = 0;
@State renderState: number = 0;
@State recordSec: number = 0;
@State showTime: string = '00:00:00';
@State isRecordOver: boolean = false;
private audioCapturesLowLatency: boolean = false;
private audioCaptures: boolean = false;
private audioRendererLowLatency: boolean = false;
private interval: number = 0;
aboutToAppear() {
this.initResource();
}
aboutToDisappear() {
this.releaseResource();
}
@Builder
InitRecord() {
Column() {
Image($r('app.media.ic_record'))
.width($r('app.float.record_width'))
.height($r('app.float.record_width'))
}
.width(CommonConstants.FULL_PERCENT)
.height($r('app.float.common_title'))
.id('start_record_btn')
.onClick(() => {
atManager.requestPermissionsFromUser(context,
['ohos.permission.MICROPHONE']).then((data) => {
if (data.authResults[0] !== 0) {
return;
}
if (this.audioCapturesLowLatency) {
testNapi.audioCapturesLowLatencyInit();
}
if (this.audioCaptures ||
(!this.audioCapturesLowLatency && !this.audioCaptures)) {
testNapi.audioCapturesInit();
}
this.capturesStart();
}).catch((err: BusinessError) => {
logger.error('error:' + JSON.stringify(err));
});
})
}
@Builder
StartedRecord() {
Column() {
Text(this.showTime)
.fontSize($r('app.float.time_size'))
.fontWeight(CommonConstants.FONT_WIGHT_500)
.margin({ bottom: $r('app.float.time_margin') })
}
.width(CommonConstants.FULL_PERCENT)
.height($r('app.float.start_height'))
.position({ y: $r('app.float.start_position') })
.id('show_time_txt')
Column() {
Image($r('app.media.ic_recording'))
.width($r('app.float.record_width'))
.height($r('app.float.record_width'))
}
.width(CommonConstants.FULL_PERCENT)
.height($r('app.float.common_title'))
.position({ y: $r('app.float.start_record_position') })
.id('stop_record_btn')
.onClick(() => {
this.capturesStop();
})
Column() {
Image($r('app.media.ic_record_pause'))
.width($r('app.float.common_image'))
.height($r('app.float.common_image'))
Text($r('app.string.pause_audio'))
.fontSize($r('app.float.small_size'))
.id('pause_record_btn')
.margin({ top: $r('app.float.pause_margin_top') })
}
.height($r('app.float.common_title'))
.width($r('app.float.common_title'))
.position({
x: CommonConstants.PAUSE_POSITION_X,
y: $r('app.float.start_record_position')
})
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.capturesPause();
})
}
@Builder
PausedRecord() {
Column() {
Text(this.showTime)
.fontSize($r('app.float.time_size'))
.fontWeight(CommonConstants.FONT_WIGHT_500)
.margin({ bottom: $r('app.float.time_margin') })
}
.width(CommonConstants.FULL_PERCENT)
.height($r('app.float.start_height'))
.position({ y: $r('app.float.start_position') })
Column() {
Image($r('app.media.ic_recording'))
.width($r('app.float.record_width'))
.height($r('app.float.record_width'))
}
.width(CommonConstants.FULL_PERCENT)
.height($r('app.float.common_title'))
.position({ y: $r('app.float.start_record_position') })
.onClick(() => {
this.capturesStop();
})
Column() {
Image($r('app.media.ic_record_continue'))
.width($r('app.float.common_image'))
.height($r('app.float.common_image'))
Text($r('app.string.CONTINUE'))
.fontSize($r('app.float.small_size'))
.margin({ top: $r('app.float.pause_margin_top') })
}
.height($r('app.float.common_title'))
.width($r('app.float.common_title'))
.position({
x: CommonConstants.PAUSE_POSITION_X,
y: $r('app.float.start_record_position')
})
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
.id('continue_record_btn')
.onClick(() => {
this.capturesContinue();
})
}
@Builder
FinishedRecord() {
Column() {
Image($r('app.media.ic_record'))
.width($r('app.float.record_width'))
.height($r('app.float.record_width'))
}
.width(CommonConstants.FULL_PERCENT)
.height($r('app.float.common_title'))
.position({ y: $r('app.float.start_record_position') })
.opacity(CommonConstants.AUDIO_OPACITY)
}
build() {
NavDestination() {
Column() {
Column() {
Row() {
Text($r('app.string.low_latency_captures'))
.fontSize($r('app.float.common_font'))
.fontWeight(CommonConstants.FONT_WIGHT_500)
.fontColor($r('sys.color.font_primary'))
.fontFamily($r('sys.string.ohos_id_text_font_family_medium'))
.opacity(this.isRecordOver ? CommonConstants.AUDIO_OPACITY : 1)
Toggle({
type: ToggleType.Switch,
isOn: this.audioCapturesLowLatency
})
.onChange((isOn: boolean) => {
if (isOn) {
this.audioCaptures = false;
this.audioCapturesLowLatency = true;
} else {
this.audioCaptures = true;
this.audioCapturesLowLatency = false;
}
})
.enabled(this.recordState === CommonConstants.PLAY_INIT &&
deviceInfo.deviceType === CommonConstants.DEVICE_PHONE)
}
.width(CommonConstants.FULL_PERCENT)
.justifyContent(FlexAlign.SpaceBetween)
}
.width(CommonConstants.FULL_PERCENT)
.height($r('app.float.common_title'))
.backgroundColor(Color.White)
.borderRadius($r('app.float.border_radius'))
.margin({ top: $r('app.float.small_margin') })
.justifyContent(FlexAlign.Center)
.padding({
left: $r('app.float.small_padding'),
right: $r('app.float.small_padding')
})
if (this.isRecordOver === true) {
Column() {
Row() {
Text($r('app.string.low_latency_renderer'))
.fontSize($r('app.float.common_font'))
.fontWeight(CommonConstants.FONT_WIGHT_500)
.fontColor($r('sys.color.font_primary'))
.id('renderer_low_latency_btn')
.onChange((isOn: boolean) => {
if (isOn) {
testNapi.audioRendererLowLatencyInit();
this.audioRendererLowLatency = true;
} else {
testNapi.audioRendererInit();
this.audioRendererLowLatency = false;
}
})
.enabled(this.renderState ===
audio.AudioState.STATE_PREPARED &&
deviceInfo.deviceType === 'phone')
}
.width(CommonConstants.FULL_PERCENT)
.justifyContent(FlexAlign.SpaceBetween)
}
.width(CommonConstants.FULL_PERCENT)
.height($r('app.float.common_title'))
.backgroundColor(Color.White)
.justifyContent(FlexAlign.Center)
.borderRadius($r('app.float.border_radius'))
.margin({ top: $r('app.float.small_margin') })
.padding({
left: $r('app.float.small_padding'),
right: $r('app.float.small_padding')
})
Column() {
Text($r('app.string.record_result'))
.fontSize($r('app.float.result_size'))
.opacity(CommonConstants.COMMON_OPACITY)
.position({ x: 0 })
}
.height($r('app.float.result_height'))
.width(CommonConstants.FULL_PERCENT)
.margin({ top: $r('app.float.common_padding') })
Column() {
Row() {
Text(this.title)
.fontSize($r('app.float.common_font'))
.fontWeight(CommonConstants.FONT_WIGHT_500)
.fontColor($r('sys.color.font_primary'))
.width($r('app.float.common_image'))
.height($r('app.float.common_image'))
.id('playing_state')
}
.width(CommonConstants.FULL_PERCENT)
.height($r('app.float.common_height'))
.justifyContent(FlexAlign.SpaceBetween)
Row() {
Text(this.date)
.fontSize($r('app.float.common_font'))
.fontColor($r('sys.color.font_primary'))
.opacity(CommonConstants.COMMON_OPACITY)
Text(this.getTimesBySecond(this.recordSec) + '')
.fontSize($r('app.float.common_font'))
.fontColor($r('sys.color.font_primary'))
.opacity(CommonConstants.COMMON_OPACITY)
}
.width(CommonConstants.FULL_PERCENT)
.height($r('app.float.common_height'))
.justifyContent(FlexAlign.SpaceBetween)
.margin({
top: $r('app.float.time_margin_top'),
bottom: $r('app.float.small_margin')
})
Row() {
Text($r('app.string.play_now'))
.fontSize($r('app.float.small_size'))
.fontFamily($r('sys.string.ohos_id_text_font_family_medium'))
.fontColor($r('sys.color.font_primary'))
.opacity(CommonConstants.COMMON_OPACITY)
Text(this.getTimesBySecond(this.playSec) + '')
.fontSize($r('app.float.small_size'))
.fontFamily($r('sys.string.ohos_id_text_font_family_medium'))
.fontColor($r('sys.color.font_primary'))
.opacity(CommonConstants.COMMON_OPACITY)
}
.justifyContent(FlexAlign.SpaceBetween)
.width(CommonConstants.FULL_PERCENT)
Row() {
Progress({ value: this.playSec,
total: this.recordSec, type: ProgressType.Linear })
.value(this.playSec)
.width(CommonConstants.FULL_PERCENT)
}
.margin({
top: $r('app.float.time_margin_top')
})
}
.width(CommonConstants.FULL_PERCENT)
.backgroundColor(Color.White)
.borderRadius($r('app.float.border_radius'))
.margin({ top: $r('app.float.play_margin_top') })
.padding({
left: $r('app.float.small_padding'),
right: $r('app.float.small_padding'),
top: $r('app.float.common_padding'),
bottom: $r('app.float.common_padding')
})
.id('player_btn')
.onClick(() => {
if (this.renderState === audio.AudioState.STATE_PREPARED) {
this.renderStart();
} else if (this.renderState === audio.AudioState.STATE_RUNNING) {
this.renderPause();
} else if (this.renderState === audio.AudioState.STATE_PAUSED) {
this.renderStart();
} else if (this.renderState === audio.AudioState.STATE_STOPPED) {
this.renderStart();
}
})
}
Row() {
if (this.recordState === CommonConstants.PLAY_INIT) {
this.InitRecord();
} else if (this.recordState === CommonConstants.PLAY_STARTED) {
this.StartedRecord();
} else if (this.recordState === CommonConstants.PLAY_PAUSED) {
this.PausedRecord();
} else if (this.recordState === CommonConstants.PLAY_CONTINUED) {
this.StartedRecord();
} else if (this.recordState === CommonConstants.PLAY_STOPPED) {
this.FinishedRecord();
}
}
.width(CommonConstants.FULL_PERCENT)
.alignItems(VerticalAlign.Center)
.height($r('app.float.row_height'))
.position({ y: CommonConstants.RECORD_POSITION_Y })
}
.width(CommonConstants.FULL_PERCENT)
.height(CommonConstants.FULL_PERCENT)
.justifyContent(FlexAlign.Start)
.padding($r('app.float.border_radius'))
}
.width(CommonConstants.FULL_PERCENT)
.height(CommonConstants.FULL_PERCENT)
.title(getContext(this)
.resourceManager.getStringSync($r('app.string.audio_captures')))
.backgroundColor($r('app.color.audio_background'))
}
initResource() {
try {
this.recordState = CommonConstants.PLAY_INIT;
this.date = this.getDate(1);
} catch (err) {
logger
.error(`AudioRecording:createAudioCapturer err =
${JSON.stringify(err)}`);
}
}
releaseResource() {
if (this.interval) {
clearInterval(this.interval);
}
testNapi.audioCapturesRelease();
this.recordState = 'init';
testNapi.audioRendererRelease();
testNapi.closeFile();
}
capturesStart() {
try {
testNapi.audioCapturesStart();
this.recordSec = 0;
this.recordState = CommonConstants.PLAY_STARTED;
clearInterval(this.interval);
this.interval = setInterval(async () => {
if (this.recordSec >= CommonConstants.TOTAL_SECOND) {
clearInterval(this.interval);
this.capturesStop();
return;
}
this.recordSec++;
this.showTime = this.getTimesBySecond(this.recordSec);
}, CommonConstants.INTERVAL_TIME);
} catch (err) {
logger.error(`AudioRecording:audioCapturer start err =
${JSON.stringify(err)}`);
}
}
renderCreate() {
try {
testNapi.audioRendererInit();
this.renderState = testNapi.getRendererState();
} catch (err) {
logger.error(`createAudioRenderer err = ${JSON.stringify(err)}`);
}
}
renderStart() {
try {
testNapi.audioRendererStart();
this.renderState = testNapi.getRendererState();
if (this.playSec === this.recordSec) {
this.playSec = 0;
}
this.interval = setInterval(async () => {
let playNumber = Math.round(testNapi.getFramesWritten() * 4 / testNapi.getFileSize() * this.recordSec);
this.playSec =
(playNumber < 0 || this.playSec === this.recordSec) ?
this.recordSec : playNumber;
if (testNapi.getFileState()) {
testNapi.audioRendererStop();
testNapi.audioRendererRelease();
testNapi.closeFile();
if (testNapi.getFastState()) {
testNapi.audioRendererLowLatencyInit();
} else {
testNapi.audioRendererInit();
}
clearInterval(this.interval);
this.renderState = testNapi.getRendererState();
return;
}
}, 50);
} catch (err) {
logger.error(`write err:${JSON.stringify(err)}`);
}
}
renderPause() {
try {
testNapi.audioRendererPause();
this.renderState = testNapi.getRendererState();
clearInterval(this.interval);
} catch (err) {
logger.error(`pause err: ${JSON.stringify(err)}`);
}
}
capturesContinue() {
try {
testNapi.audioCapturesStart();
this.recordState = CommonConstants.PLAY_CONTINUED;
logger.info('audioCaptures start ok');
this.interval = setInterval(async () => {
if (this.recordSec >= CommonConstants.TOTAL_SECOND) {
clearInterval(this.interval);
this.capturesStop();
return;
}
this.recordSec++;
this.showTime = this.getTimesBySecond(this.recordSec);
}, CommonConstants.INTERVAL_TIME);
} catch (err) {
logger.error(`AudioRecording:audioCapturer start err = ${JSON.stringify(err)}`);
}
}
capturesStop() {
if (this.recordSec < CommonConstants.MIN_RECORD_SECOND) {
return;
}
try {
testNapi.audioCapturesStop();
testNapi.audioCapturesRelease();
testNapi.closeFile();
this.recordState = CommonConstants.PLAY_STOPPED;
clearInterval(this.interval);
} catch (err) {
this.recordState = CommonConstants.PLAY_STOPPED;
logger.error(`AudioRecording:audioCapturer stop err =
${JSON.stringify(err)}`);
}
this.isRecordOver = true;
this.renderCreate();
}
capturesPause() {
try {
testNapi.audioCapturesPause();
this.recordState = CommonConstants.PLAY_PAUSED;
clearInterval(this.interval);
} catch (err) {
logger.error(`AudioRecording:audioCapturer stop err
= ${JSON.stringify(err)}`);
}
}
formatNumber(num: number) {
if (num <= 9) {
return '0' + num;
} else {
return '' + num;
}
}
getDate(mode: number) {
let date = new Date();
if (mode === 1) {
return `${date.getFullYear()} / ${this.formatNumber(date.getMonth() + 1)} / ${this.formatNumber(date.getDate())}`;
} else {
return `${date.getFullYear()}${this.formatNumber(date.getMonth() + 1)}${this.formatNumber(date.getDate())}`;
}
}
getTimesBySecond(t: number) {
let h = Math.floor(t / 60 / 60 % 24);
let m = Math.floor(t / 60 % 60);
let s = Math.floor(t % 60);
let hs = h < 10 ? '0' + h : h;
let ms = m < 10 ? '0' + m : m;
let ss = s < 10 ? '0' + s : s;
return `${hs}:${ms}:${ss}`;
}
}