鸿蒙开发案例:分贝仪
- 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 加入
还未添加个人简介







 
    
 
				 
				 
			


评论