鸿蒙开发案例:分贝仪
【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); // 返回是否所有权限都被授予}// 构建UIbuild() {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); // 设置最大行数为1Text(`${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(); // 请求用户权限}}}
}