鸿蒙NEXT开发案例:分贝仪
【1】引言
分贝仪是一个简单的应用,用于测量周围环境的噪音水平。通过麦克风采集音频数据,计算当前的分贝值,并在界面上实时显示。该应用不仅展示了鸿蒙系统的基础功能,还涉及到了权限管理、音频处理和UI设计等多个方面。
【2】环境准备
电脑系统:windows 10
开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806
工程版本:API 12
真机:mate60 pro
语言:ArkTS、ArkUI
权限:ohos.permission.MICROPHONE(麦克风权限)
系统库:
• @kit.AudioKit:用于音频处理的库。
• @kit.AbilityKit:用于权限管理和应用能力的库。
• @kit.BasicServicesKit:提供基本的服务支持,如错误处理等。
【3】功能模块
3.1 权限管理
在使用麦克风之前,需要请求用户的权限。如果用户拒绝,会显示一个对话框引导用户手动开启权限。
// 请求用户权限
requestPermissionsFromUser() {
const context = getContext(this) as common.UIAbilityContext;
const atManager = abilityAccessCtrl.createAtManager();
atManager.requestPermissionsFromUser(context, this.requiredPermissions, (err, data) => {
const grantStatus: Array<number> = data.authResults;
if (grantStatus.toString() == "-1") {
this.showAlertDialog();
} else if (grantStatus.toString() == "0") {
this.initialize();
}
});
}
3.2 分贝计算
通过读取麦克风采集的音频数据,计算当前环境的分贝值。计算过程中会对音频样本进行归一化处理,并计算其均方根(RMS)值,最终转换成分贝值。
// 分贝计算
calculateDecibel(pcm: ArrayBuffer): number {
let sum = 0;
const pcmView = new DataView(pcm);
const numSamples = pcm.byteLength / 2;
for (let i = 0; i < pcm.byteLength; i += 2) {
const sample = pcmView.getInt16(i, true) / 32767.0;
sum += sample * sample;
}
const meanSquare = sum / numSamples;
const rmsAmplitude = Math.sqrt(meanSquare);
const referencePressure = 20e-6;
const decibels = 20 * Math.log10(rmsAmplitude / referencePressure);
if (isNaN(decibels)) {
return -100;
}
const minDb = 20;
const maxDb = 100;
const mappedValue = ((decibels - minDb) / (maxDb - minDb)) * 100;
return Math.max(0, Math.min(100, mappedValue));
}
3.3 UI设计
界面上包含一个仪表盘显示当前分贝值,以及一段文字描述当前的噪音水平。分贝值被映射到0到100的范围内,以适应仪表盘的显示需求。界面上还有两个按钮,分别用于开始和停止分贝测量。
// 构建UI
build() {
Column() {
Text("分贝仪")
.width('100%')
.height(44)
.backgroundColor("#fe9900")
.textAlign(TextAlign.Center)
.fontColor(Color.White);
Row() {
Gauge({ value: this.currentDecibel, min: 1, max: 100 }) {
Column() {
Text(`${this.displayedDecibel}分贝`)
.fontSize(25)
.fontWeight(FontWeight.Medium)
.fontColor("#323232")
.width('40%')
.height('30%')
.textAlign(TextAlign.Center)
.margin({ top: '22.2%' })
.textOverflow({ overflow: TextOverflow.Ellipsis })
.maxLines(1);
Text(`${this.displayType}`)
.fontSize(16)
.fontColor("#848484")
.fontWeight(FontWeight.Regular)
.width('47.4%')
.height('15%')
.textAlign(TextAlign.Center)
.backgroundColor("#e4e4e4")
.borderRadius(5);
}.width('100%');
}
.startAngle(225)
.endAngle(135)
.colors(this.gaugeColors)
.height(250)
.strokeWidth(18)
.description(null)
.trackShadow({ radius: 7, offsetX: 7, offsetY: 7 })
.padding({ top: 30 });
}.width('100%').justifyContent(FlexAlign.Center);
Column() {
ForEach(this.typeArray, (item: ValueBean, index: number) => {
Row() {
Text(item.description)
.textAlign(TextAlign.Start)
.fontColor("#3d3d3d");
}.width(250)
.padding({ bottom: 10, top: 10 })
.borderWidth({ bottom: 1 })
.borderColor("#737977");
});
}.width('100%');
Row() {
Button('开始检测').clickEffect({ level: ClickEffectLevel.LIGHT }).onClick(() => {
if (this.audioRecorder) {
this.startRecording();
} else {
this.requestPermissionsFromUser();
}
});
Button('停止检测').clickEffect({ level: ClickEffectLevel.LIGHT }).onClick(() => {
if (this.audioRecorder) {
this.stopRecording();
}
});
}.width('100%')
.justifyContent(FlexAlign.SpaceEvenly)
.padding({
left: 20,
right: 20,
top: 40,
bottom: 40
});
}.height('100%').width('100%');
}
【4】关键代码解析
4.1 权限检查与请求
在应用启动时,首先检查是否已经获得了麦克风权限。如果没有获得权限,则请求用户授权。
// 检查权限
checkPermissions() {
const atManager = abilityAccessCtrl.createAtManager();
const bundleInfo = bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
const tokenId = bundleInfo.appInfo.accessTokenId;
const authResults = this.requiredPermissions.map((permission) => atManager.checkAccessTokenSync(tokenId, permission));
return authResults.every(v => v === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED);
}
// 请求用户权限
requestPermissionsFromUser() {
const context = getContext(this) as common.UIAbilityContext;
const atManager = abilityAccessCtrl.createAtManager();
atManager.requestPermissionsFromUser(context, this.requiredPermissions, (err, data) => {
const grantStatus: Array<number> = data.authResults;
if (grantStatus.toString() == "-1") {
this.showAlertDialog();
} else if (grantStatus.toString() == "0") {
this.initialize();
}
});
}
4.2 音频记录器初始化
在获得权限后,初始化音频记录器,设置采样率、通道数、采样格式等参数,并开始监听音频数据。
// 初始化音频记录器
initialize() {
const streamInfo: audio.AudioStreamInfo = {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100,
channels: audio.AudioChannel.CHANNEL_1,
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
};
const recorderInfo: audio.AudioCapturerInfo = {
source: audio.SourceType.SOURCE_TYPE_MIC,
capturerFlags: 0
};
const recorderOptions: audio.AudioCapturerOptions = {
streamInfo: streamInfo,
capturerInfo: recorderInfo
};
audio.createAudioCapturer(recorderOptions, (err, recorder) => {
if (err) {
console.error(`创建音频记录器失败, 错误码: ${err.code}, 错误信息: ${err.message}`);
return;
}
console.info(`${this.TAG}: 音频记录器创建成功`);
this.audioRecorder = recorder;
if (this.audioRecorder !== undefined) {
this.audioRecorder.on('readData', (buffer: ArrayBuffer) => {
this.currentDecibel = this.calculateDecibel(buffer);
this.updateDisplay();
});
}
});
}
4.3 更新显示
每秒钟更新一次显示的分贝值,并根据当前分贝值确定其所属的噪音级别。
// 更新显示
updateDisplay() {
if (Date.now() - this.lastUpdateTimestamp > 1000) {
this.lastUpdateTimestamp = Date.now();
this.displayedDecibel = Math.floor(this.currentDecibel);
for (const item of this.typeArray) {
if (this.currentDecibel >= item.minDb && this.currentDecibel < item.maxDb) {
this.displayType = item.label;
break;
}
}
}
}
【5】完整代码
5.1 配置麦克风权限
路径:src/main/module.json5
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.MICROPHONE",
"reason": "$string:microphone_reason",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when":"inuse"
}
}
],
5.2 配置权限弹窗时的描述文字
路径:src/main/resources/base/element/string.json
{
"string": [
{
"name": "module_desc",
"value": "module description"
},
{
"name": "EntryAbility_desc",
"value": "description"
},
{
"name": "EntryAbility_label",
"value": "label"
},
{
"name": "microphone_reason",
"value": "需要麦克风权限说明"
}
]
}
5.3 完整代码
路径:src/main/ets/pages/Index.ets
import { audio } from '@kit.AudioKit'; // 导入音频相关的库
import { abilityAccessCtrl, bundleManager, common, Permissions } from '@kit.AbilityKit'; // 导入权限管理相关的库
import { BusinessError } from '@kit.BasicServicesKit'; // 导入业务错误处理
// 定义一个类,用于存储分贝范围及其描述
class ValueBean {
label: string; // 标签
description: string; // 描述
minDb: number; // 最小分贝值
maxDb: number; // 最大分贝值
colorStart: string; // 起始颜色
colorEnd: string; // 结束颜色
// 构造函数,初始化属性
constructor(label: string, description: string, minDb: number, maxDb: number, colorStart: string, colorEnd: string) {
this.label = label;
this.description = description;
this.minDb = minDb;
this.maxDb = maxDb;
this.colorStart = colorStart;
this.colorEnd = colorEnd;
}
}
// 定义分贝仪组件
@Entry
@Component
struct DecibelMeter {
TAG: string = 'DecibelMeter'; // 日志标签
audioRecorder: audio.AudioCapturer | undefined = undefined; // 音频记录器
requiredPermissions: Array<Permissions> = ['ohos.permission.MICROPHONE']; // 需要的权限
@State currentDecibel: number = 0; // 当前分贝值
@State displayedDecibel: number = 0; // 显示的分贝值
lastUpdateTimestamp: number = 0; // 上次更新时间戳
@State displayType: string = ''; // 当前显示类型
// 定义分贝范围及其描述
typeArray: ValueBean[] = [
new ValueBean("寂静", "0~20dB : 寂静,几乎感觉不到", 0, 20, "#02b003", "#016502"),
new ValueBean("安静", '20~40dB :安静,轻声交谈', 20, 40, "#7ed709", "#4f8800"),
new ValueBean("正常", '40~60dB :正常,普通室内谈话', 40, 60, "#ffef01", "#ad9e04"),
new ValueBean("吵闹", '60~80dB :吵闹,大声说话', 60, 80, "#f88200", "#965001"),
new ValueBean("很吵", '80~100dB: 很吵,可使听力受损', 80, 100, "#f80000", "#9d0001"),
];
gaugeColors: [LinearGradient, number][] = [] // 存储仪表颜色的数组
// 组件即将出现时调用
aboutToAppear(): void {
// 初始化仪表颜色
for (let i = 0; i < this.typeArray.length; i++) {
this.gaugeColors.push([new LinearGradient([{ color: this.typeArray[i].colorStart, offset: 0 },
{ color: this.typeArray[i].colorEnd, offset: 1 }]), 1])
}
}
// 请求用户权限
requestPermissionsFromUser() {
const context = getContext(this) as common.UIAbilityContext; // 获取上下文
const atManager = abilityAccessCtrl.createAtManager(); // 创建权限管理器
// 请求权限
atManager.requestPermissionsFromUser(context, this.requiredPermissions, (err, data) => {
const grantStatus: Array<number> = data.authResults; // 获取授权结果
if (grantStatus.toString() == "-1") { // 用户拒绝权限
this.showAlertDialog(); // 显示提示对话框
} else if (grantStatus.toString() == "0") { // 用户同意权限
this.initialize(); // 初始化音频记录器
}
});
}
// 显示对话框提示用户开启权限
showAlertDialog() {
this.getUIContext().showAlertDialog({
autoCancel: true, // 自动取消
title: '权限申请', // 对话框标题
message: '如需使用此功能,请前往设置页面开启麦克风权限。', // 对话框消息
cancel: () => {
},
confirm: {
defaultFocus: true, // 默认聚焦确认按钮
value: '好的', // 确认按钮文本
action: () => {
this.openPermissionSettingsPage(); // 打开权限设置页面
}
},
onWillDismiss: () => {
},
alignment: DialogAlignment.Center, // 对话框对齐方式
});
}
// 打开权限设置页面
openPermissionSettingsPage() {
const context = getContext() as common.UIAbilityContext; // 获取上下文
const bundleInfo =
bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION); // 获取包信息
context.startAbility({
bundleName: 'com.huawei.hmos.settings', // 设置页面的包名
abilityName: 'com.huawei.hmos.settings.MainAbility', // 设置页面的能力名
uri: 'application_info_entry', // 打开设置->应用和元服务
parameters: {
pushParams: bundleInfo.name // 按照包名打开对应设置页
}
});
}
// 分贝计算
calculateDecibel(pcm: ArrayBuffer): number {
let sum = 0; // 初始化平方和
const pcmView = new DataView(pcm); // 创建数据视图
const numSamples = pcm.byteLength / 2; // 计算样本数量
// 归一化样本值并计算平方和
for (let i = 0; i < pcm.byteLength; i += 2) {
const sample = pcmView.getInt16(i, true) / 32767.0; // 归一化样本值
sum += sample * sample; // 计算平方和
}
// 计算平均平方值
const meanSquare = sum / numSamples; // 计算均方
// 计算RMS(均方根)振幅
const rmsAmplitude = Math.sqrt(meanSquare); // 计算RMS值
// 使用标准参考压力值
const referencePressure = 20e-6; // 20 μPa
// 计算分贝值
const decibels = 20 * Math.log10(rmsAmplitude / referencePressure); // 计算分贝
// 处理NaN值
if (isNaN(decibels)) {
return -100; // 返回一个极小值表示静音
}
// 调整动态范围
const minDb = 20; // 调整最小分贝值
const maxDb = 100; // 调整最大分贝值
// 将分贝值映射到0到100之间的范围
const mappedValue = ((decibels - minDb) / (maxDb - minDb)) * 100; // 映射分贝值
// 确保值在0到100之间
return Math.max(0, Math.min(100, mappedValue)); // 返回映射后的值
}
// 初始化音频记录器
initialize() {
const streamInfo: audio.AudioStreamInfo = {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100, // 采样率
channels: audio.AudioChannel.CHANNEL_1, // 单声道
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, // 采样格式
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW // 编码类型
};
const recorderInfo: audio.AudioCapturerInfo = {
source: audio.SourceType.SOURCE_TYPE_MIC, // 音频源为麦克风
capturerFlags: 0 // 捕获标志
};
const recorderOptions: audio.AudioCapturerOptions = {
streamInfo: streamInfo, // 音频流信息
capturerInfo: recorderInfo // 记录器信息
};
// 创建音频记录器
audio.createAudioCapturer(recorderOptions, (err, recorder) => {
if (err) {
console.error(`创建音频记录器失败, 错误码: ${err.code}, 错误信息: ${err.message}`); // 错误处理
return;
}
console.info(`${this.TAG}: 音频记录器创建成功`); // 成功日志
this.audioRecorder = recorder; // 保存记录器实例
if (this.audioRecorder !== undefined) {
// 监听音频数据
this.audioRecorder.on('readData', (buffer: ArrayBuffer) => {
this.currentDecibel = this.calculateDecibel(buffer); // 计算当前分贝值
this.updateDisplay(); // 更新显示
});
}
this.startRecording(); // 开始录音
});
}
// 开始录音
startRecording() {
if (this.audioRecorder !== undefined) { // 检查音频记录器是否已定义
this.audioRecorder.start((err: BusinessError) => { // 调用开始录音方法
if (err) {
console.error('开始录音失败'); // 记录错误信息
} else {
console.info('开始录音成功'); // 记录成功信息
}
});
}
}
// 停止录音
stopRecording() {
if (this.audioRecorder !== undefined) { // 检查音频记录器是否已定义
this.audioRecorder.stop((err: BusinessError) => { // 调用停止录音方法
if (err) {
console.error('停止录音失败'); // 记录错误信息
} else {
console.info('停止录音成功'); // 记录成功信息
}
});
}
}
// 更新显示
updateDisplay() {
if (Date.now() - this.lastUpdateTimestamp > 1000) { // 每隔1秒更新一次显示
this.lastUpdateTimestamp = Date.now(); // 更新最后更新时间戳
this.displayedDecibel = Math.floor(this.currentDecibel); // 将当前分贝值取整并赋值给显示的分贝值
// 遍历分贝类型数组,确定当前分贝值对应的类型
for (const item of this.typeArray) {
if (this.currentDecibel >= item.minDb && this.currentDecibel < item.maxDb) { // 检查当前分贝值是否在某个范围内
this.displayType = item.label; // 设置当前显示类型
break; // 找到对应类型后退出循环
}
}
}
}
// 检查权限
checkPermissions() {
const atManager = abilityAccessCtrl.createAtManager(); // 创建权限管理器
const bundleInfo =
bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION); // 获取包信息
const tokenId = bundleInfo.appInfo.accessTokenId; // 获取应用的唯一标识
// 检查每个权限的授权状态
const authResults =
this.requiredPermissions.map((permission) => atManager.checkAccessTokenSync(tokenId, permission));
return authResults.every(v => v === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED); // 返回是否所有权限都被授予
}
// 构建UI
build() {
Column() {
Text("分贝仪")// 显示标题
.width('100%')// 设置宽度为100%
.height(44)// 设置高度为44
.backgroundColor("#fe9900")// 设置背景颜色
.textAlign(TextAlign.Center)// 设置文本对齐方式
.fontColor(Color.White); // 设置字体颜色
Row() {
Gauge({ value: this.currentDecibel, min: 1, max: 100 }) { // 创建仪表,显示当前分贝值
Column() {
Text(`${this.displayedDecibel}分贝`)// 显示当前分贝值
.fontSize(25)// 设置字体大小
.fontWeight(FontWeight.Medium)// 设置字体粗细
.fontColor("#323232")// 设置字体颜色
.width('40%')// 设置宽度为40%
.height('30%')// 设置高度为30%
.textAlign(TextAlign.Center)// 设置文本对齐方式
.margin({ top: '22.2%' })// 设置上边距
.textOverflow({ overflow: TextOverflow.Ellipsis })// 设置文本溢出处理
.maxLines(1); // 设置最大行数为1
Text(`${this.displayType}`)// 显示当前类型
.fontSize(16)// 设置字体大小
.fontColor("#848484")// 设置字体颜色
.fontWeight(FontWeight.Regular)// 设置字体粗细
.width('47.4%')// 设置宽度为47.4%
.height('15%')// 设置高度为15%
.textAlign(TextAlign.Center)// 设置文本对齐方式
.backgroundColor("#e4e4e4")// 设置背景颜色
.borderRadius(5); // 设置圆角
}.width('100%'); // 设置列宽度为100%
}
.startAngle(225) // 设置仪表起始角度
.endAngle(135) // 设置仪表结束角度
.colors(this.gaugeColors) // 设置仪表颜色
.height(250) // 设置仪表高度
.strokeWidth(18) // 设置仪表边框宽度
.description(null) // 设置描述为null
.trackShadow({ radius: 7, offsetX: 7, offsetY: 7 }) // 设置阴影效果
.padding({ top: 30 }); // 设置内边距
}.width('100%').justifyContent(FlexAlign.Center); // 设置行宽度为100%并居中对齐
Column() {
ForEach(this.typeArray, (item: ValueBean, index: number) => { // 遍历分贝类型数组
Row() {
Text(item.description)// 显示每个类型的描述
.textAlign(TextAlign.Start)// 设置文本对齐方式
.fontColor("#3d3d3d"); // 设置字体颜色
}.width(250) // 设置行宽度为250
.padding({ bottom: 10, top: 10 }) // 设置上下内边距
.borderWidth({ bottom: 1 }) // 设置下边框宽度
.borderColor("#737977"); // 设置下边框颜色
});
}.width('100%'); // 设置列宽度为100%
Row() {
Button('开始检测').clickEffect({ level: ClickEffectLevel.LIGHT }).onClick(() => { // 创建开始检测按钮
if (this.audioRecorder) { // 检查音频记录器是否已定义
this.startRecording(); // 开始录音
} else {
this.requestPermissionsFromUser(); // 请求用户权限
}
});
Button('停止检测').clickEffect({ level: ClickEffectLevel.LIGHT }).onClick(() => { // 创建停止检测按钮
if (this.audioRecorder) { // 检查音频记录器是否已定义
this.stopRecording(); // 停止录音
}
});
}.width('100%') // 设置行宽度为100%
.justifyContent(FlexAlign.SpaceEvenly) // 设置内容均匀分布
.padding({
// 设置内边距
left: 20,
right: 20,
top: 40,
bottom: 40
});
}.height('100%').width('100%'); // 设置列高度和宽度为100%
}
// 页面显示时的处理
onPageShow(): void {
const hasPermission = this.checkPermissions(); // 检查权限
console.info(`麦克风权限状态: ${hasPermission ? '已开启' : '未开启'}`); // 打印权限状态
if (hasPermission) { // 如果权限已开启
if (this.audioRecorder) { // 检查音频记录器是否已定义
this.startRecording(); // 开始录音
} else {
this.requestPermissionsFromUser(); // 请求用户权限
}
}
}
}