【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(); // 请求用户权限 } } } }
标签:audio,const,鸿蒙,音频,分贝,案例,设置,权限 From: https://www.cnblogs.com/zhongcx/p/18522879