【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(); // 請求使用者許可權 } } } }