HarmonyOS《柚兔学伴》项目实战16-录音功能与权限管理

HarmonyOS《柚兔学伴》项目实战16-录音功能与权限管理 第16篇录音功能与权限管理引言柚兔学伴的口语对话功能允许用户通过语音与 AI 交流而非手动打字。这需要完整的录音流程请求麦克风权限 → 创建录音器 → 录音 → 停止 → 上传 → 语音识别。本篇将深入剖析 HarmonyOS 的 AVRecorder 录音 API、运行时权限管理机制以及录音 UI 的交互设计。RecordUtils 单例录音工具类采用单例模式封装了 AVRecorder 的完整生命周期// common/src/main/ets/util/RecordUtils.etsexportclassRecordUtils{privatefilesDirgetContext(this).filesDir;privatefdPath:string|undefinedundefinedprivatefilePath:string|undefinedundefinedprivatefile:fs.File|undefinedundefinedprivatestaticmInstance:RecordUtils;privateconstructor(){}staticgetInstance():RecordUtils{if(!RecordUtils.mInstance){RecordUtils.mInstancenewRecordUtils();}returnRecordUtils.mInstance;}privateavRecorder:media.AVRecorder|undefinedundefined;}单例的必要性AVRecorder 是有状态的资源全局只应存在一个录音实例避免多个录音器同时占用麦克风导致冲突。录音配置参数privateavProfile:media.AVRecorderProfile{audioBitrate:100000,// 音频比特率 100kbpsaudioChannels:2,// 双声道audioCodec:media.CodecMimeType.AUDIO_AAC,// AAC 编码audioSampleRate:48000,// 48kHz 采样率fileFormat:media.ContainerFormatType.CFT_MPEG_4A,// M4A 封装格式};privateavConfig:media.AVRecorderConfig{audioSourceType:media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC,// 麦克风输入profile:this.avProfile,url:fd://,// 占位运行时替换为实际 fd};参数选择说明AAC 编码HarmonyOS 当前只支持 AAC 音频编码格式M4A 封装对应 AAC 编码HarmonyOS 当前只支持 M4A 封装格式48kHz 采样率CD 音质标准满足语音识别精度需求双声道保证音频质量上传后语音识别更准确fd:// 协议HarmonyOS 文件访问协议通过文件描述符传递录音数据文件创建与 fd 获取getFiledFd():void{this.filePaththis.filesDir/audio_${Date.now()}.m4athis.filefs.openSync(this.filePath,fs.OpenMode.READ_WRITE|fs.OpenMode.CREATE);console.info(录音文件地址${this.file.fd})this.fdPathfd://this.file.fd;this.avConfig.urlthis.fdPath;}关键步骤以时间戳命名文件避免冲突audio_1700000000000.m4a使用fs.openSync以读写创建模式打开文件将文件描述符fd转换为fd://协议路径将fdPath赋给avConfig.urlAVRecorder 将录音数据写入此文件录音回调注册setAudioRecorderCallback(){if(this.avRecorder!undefined){this.avRecorder.on(stateChange,(state:media.AVRecorderState,reason:media.StateChangeReason){console.info(AudioRecorder current state is${state});})this.avRecorder.on(error,(err:BusinessError){console.error(AudioRecorder failed, code is${err.code}, message is${err.message});})}}stateChange监听录音器状态变化created → prepared → started → stopped → released便于调试和状态追踪error录音错误回调如麦克风被占用、存储空间不足等异常完整录音流程开始录音asyncstartRecordingProcess(){if(this.avRecorder!undefined){awaitthis.avRecorder.release();this.avRecorderundefined;}try{// 1. 创建录制实例this.avRecorderawaitmedia.createAVRecorder();// 2. 注册回调this.setAudioRecorderCallback();// 3. 获取文件 fdthis.getFiledFd()// 4. 配置录制参数完成准备工作awaitthis.avRecorder.prepare(this.avConfig);// 5. 开始录制awaitthis.avRecorder.start();}catch(e){this.avRecorder!!.state e}}状态流转createAVRecorder→prepare→start对应 AVRecorder 的idle → prepared → started状态变迁。开始前若已有旧实例先release释放。停止录音asyncstopRecordingProcess():Promisestring{if(this.avRecorder!undefined){// 1. 停止录制if(this.avRecorder.statestarted||this.avRecorder.statepaused){awaitthis.avRecorder.stop();}// 2. 释放录制实例awaitthis.avRecorder.release();this.avRecorderundefined;// 3. 关闭录制文件 fdif(this.fdPath){fs.closeSync(this.file)this.fdPathundefined;}}returnthis.filePath!!}停止流程严格遵循stop→release→closeSync。释放 AVRecorder 后必须关闭文件描述符否则文件可能写入不完整。方法返回录音文件的完整路径供后续上传使用。暂停与恢复asyncpauseRecordingProcess(){if(this.avRecorder!undefinedthis.avRecorder.statestarted){awaitthis.avRecorder.pause();}}asyncresumeRecordingProcess(){if(this.avRecorder!undefinedthis.avRecorder.statepaused){awaitthis.avRecorder.resume();}}暂停和恢复都检查当前状态确保只在合法状态下调用。pause只能在started状态调用resume只能在paused状态调用。运行时权限管理HarmonyOS 对麦克风等敏感权限采用运行时授权机制用户必须在使用时主动同意。权限声明在module.json5中声明需要的权限ohos.permission.MICROPHONE运行时请求// ChatPage.etsasyncfunctiongrantPermission():Promiseboolean{constPERMISSIONS:ArrayPermissions[ohos.permission.MICROPHONE,];try{// 获取应用程序的 accessTokenIDletbundleInfo:bundleManager.BundleInfoawaitbundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);letappInfo:bundleManager.ApplicationInfobundleInfo.appInfo;lettokenIdappInfo.accessTokenId;letatManagerabilityAccessCtrl.createAtManager();letpems:ArrayPermissions[];for(leti0;iPERMISSIONS.length;i){letstateawaitatManager.checkAccessToken(tokenId,PERMISSIONS[i]);if(state!abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED){pems.push(PERMISSIONS[i]);}}if(pems.length0){letresultawaitatManager.requestPermissionsFromUser(context,pems);letgrantStatus:Arraynumberresult.authResults;for(leti0;igrantStatus.length;i){if(grantStatus[i]0){// 用户授权}else{// 用户拒绝returnfalse;}}}returntrue;}catch(e){returnfalse;}}权限请求的三步流程获取 Token ID通过bundleManager.getBundleInfoForSelf获取应用的accessTokenId这是权限校验的标识检查已授权状态atManager.checkAccessToken检查权限是否已授予避免重复弹窗请求未授权权限atManager.requestPermissionsFromUser弹出系统授权弹窗用户选择后通过authResults判断结果录音 UI 交互ChatPage 中的录音 UI 采用按钮切换模式Stack(){// 未录音状态显示点击说话按钮Row(){Button(点击 说话,{stateEffect:true,type:ButtonType.Normal}).linearGradient({angle:90,colors:[[0xFF33FF,0.0],[0x1C55FF,1]]}).borderRadius(30).height(50).width(100%).onClick(async(){grantPermission().then(async(isGranted:boolean){if(isGranted){this.isShowRecordtrueRecordUtils.getInstance().startRecordingProcess()}else{ToastUtil.showToast(录音权限未获取)}})});}.visibility(this.isShowRecord?Visibility.None:Visibility.Visible)// 录音中状态显示关闭、动画、发送按钮Row(){Image($r(app.media.ic_record_close)).width(30).onClick((){this.isShowRecordfalse})Lottie({controller:this.controller,animationPath:lottie/lottie_record.json,autoPlay:true,loop:true,}).width(90).height(90)Image($r(app.media.ic_record_send)).width(30).onClick((){this.isShowRecordfalseRecordUtils.getInstance().stopRecordingProcess().then((voicePath:string){this.chatModel.uploadFile(voicePath,(downloadUrl:string){letuuidutil.generateRandomUUID()this.chatModel.voiceRecognition(uuid,this.uid,downloadUrl).then((status){if(statusLoadingStatus.SUCCESS){this.speechIdsetInterval((){this.speechQuery(uuid);},1100)}})})})})}.visibility(this.isShowRecord?Visibility.Visible:Visibility.None)}UI 状态流转默认显示渐变色点击说话按钮录音中隐藏按钮显示三联操作区关闭 | Lottie动画 | 发送Lottie动画录音时播放声波动画提供视觉反馈录音到识别的完整链路用户点击发送后的完整流程停止录音 → 获取文件路径 → 上传到云存储 → 获取下载URL → 提交语音识别 → 轮询识别结果 → 转为文本 → 发送给AIRecordUtils.getInstance().stopRecordingProcess().then((voicePath:string){// 1. 上传录音文件到云存储this.chatModel.uploadFile(voicePath,(downloadUrl:string){// 2. 将录音URL提交给语音识别APIletuuidutil.generateRandomUUID()this.chatModel.voiceRecognition(uuid,this.uid,downloadUrl).then((status){if(statusLoadingStatus.SUCCESS){// 3. 删除云侧录音文件节省存储letcloudPathvoice/voicePath.split(/).pop()asstring;this.chatModel.deleteFile(cloudPath)// 4. 轮询识别结果this.speechIdsetInterval((){this.speechQuery(uuid);},1100)}})})})关键细节识别成功后立即删除云侧录音文件不保留用户语音数据util.generateRandomUUID()生成唯一标识用于关联识别请求与结果轮询间隔 1100ms与 AI 对话的 retrieve 轮询一致语音识别结果处理privatespeechQuery(uuid:string){this.chatModel.voiceQuery(uuid).then((data){clearInterval(this.speechId)letspeechDataJSON.parse(data.toString())asSpeechDataletuerSpeechTextspeechData.result?.text!!letchatDatanewChatData(uerSpeechText,,Role.USER,text,success)this.chatList.push(chatData)this.listScroller.scrollEdge(Edge.Bottom);this.sendMsgToAgent(uerSpeechText,this.name);}).catch((err:BusinessError){clearInterval(this.speechId)ToastUtil.showToast(没听清楚请再说一次吧)});}SpeechData的结构exportclassSpeechData{audio_info?:SpeechDataAudio_infonewSpeechDataAudio_info();result?:SpeechDataResultnewSpeechDataResult();code?:number200;}exportclassSpeechDataResult{additions?:SpeechDataResultAdditionsnewSpeechDataResultAdditions();text?:string;utterances?:SpeechDataResultUtterances[][];}result.text是识别出的完整文本utterances包含逐句的详细识别结果含时间戳和置信度。项目只使用text字段作为用户输入发送给 AI。小结本篇详细介绍了柚兔学伴的录音功能实现AVRecorder API完整的录音生命周期管理——创建、配置、开始、暂停、停止、释放fd:// 协议通过文件描述符传递录音数据HarmonyOS 特有的文件访问方式AAC/M4A 配置48kHz 采样率、双声道、100kbps 比特率满足语音识别精度需求运行时权限三步流程——获取 TokenID → 检查已授权 → 请求未授权权限录音 UI按钮切换 Lottie 声波动画提供直观的录音反馈完整链路录音 → 上传 → 识别 → 轮询 → 文本 → AI 对话实现语音到对话的无缝衔接