写点什么

鸿蒙开发案例:分贝仪

作者:zhongcx
  • 2024-11-03
    广东
  • 本文字数:10187 字

    阅读完需:约 33 分钟

【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 的范围内,以适应仪表盘的显示需求。界面上还有两个按钮,分别用于开始和停止分贝测量。

// 构建UIbuild() {  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@Componentstruct 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(); // 请求用户权限 } } }}

复制代码


用户头像

zhongcx

关注

还未添加个人签名 2024-09-27 加入

还未添加个人简介

评论

发布
暂无评论
鸿蒙开发案例:分贝仪_鸿蒙_zhongcx_InfoQ写作社区