写点什么

鸿蒙 Next 使用 AVRecorder 录制和播放音频

作者:auhgnixgnahz
  • 2025-06-25
    北京
  • 本文字数:7029 字

    阅读完需:约 23 分钟

音频录制开发方式系统提供了多样化的 API:


1.AudioCapturer: 用于音频输入的 ArkTS/JS API,仅支持 PCM 格式。应用可以在音频输出后添加数据处理,要求开发者具备音频处理的基础知识,适用于更专业、更多样化的媒体录制应用开发。


2.OpenSL ES: 一套跨平台标准化的音频 Native API,同样提供音频输入原子能力,仅支持 PCM 格式,适用于从其他嵌入式平台移植,或依赖在 Native 层实现音频输入功能的录音应用使用。


3.OHAudio: 用于音频输入的 Native API,此 API 在设计上实现归一,同时支持普通音频通路和低时延通路。仅支持 PCM 格式,适用于依赖 Native 层实现音频输入功能的场景。


4.本文介绍一下 Media Kit 中的 AVRecorder 实现音频录制和音频播放,用于音频录制的 ArkTS/JS API,集成了音频输入录制、音频编码和媒体封装的功能。开发者可以直接调用设备硬件如麦克风录音,并生成 m4a 音频文件。


先看一下实现效果:



录制实现步骤:


1.在 module.json5 中添加麦克风权限


{        "name" : "ohos.permission.MICROPHONE",        "reason": "$string:microphone_reason",        "usedScene": {          "abilities": [            "EntryAbility"          ],          "when":"inuse"        }      }
复制代码


2.需要录音时向用户动态申请权限


3.创建 AVRecorder 实例,添加监听事件,监听状态变化及错误上报


4.配置音频录制参数,设置文件路径、文件名,文件打开录制完成之后要关闭


5.配置完成之后可以调用 prepare,准备完成之后可以开始录制。


6.录制完成之后,资源回收。


播放实现步骤:


1.录制文件保存在目录/data/storage/el2/base/haps/entry/files/


2.获取当前目录下的 mp3 文件


3.创建实例 createAVPlayer()


4.设置业务需要的监听事件



5.设置资源:设置属性 url,AVPlayer 进入 initialized 状态


6.准备播放:在回调中 initialized 回调中调用 prepare()


7.播放控制


8.结束回收资源


全部代码:


import { fileIo as fs, ListFileOptions } from '@kit.CoreFileKit';import { microphone_permissions, requestMicrophone, requestPermissionOnSetting } from '../utils/RequestPermission';import { common } from '@kit.AbilityKit';import { showToast } from '../utils/ToastUtil';import { media } from '@kit.MediaKit';import { BusinessError } from '@kit.BasicServicesKit';import { audio } from '@kit.AudioKit';import { TimeFormatUtil } from '../utils/TimeFormatUtil';import { OpenCustomDialogUtil, Params } from '../utils/DialogUtil';
// 获取应用文件路径let context = getContext(this) as common.UIAbilityContext;
let avProfile: media.AVRecorderProfile = { audioBitrate: 48000, // 音频比特率。 audioChannels: 2, // 音频声道数。 audioCodec: media.CodecMimeType.AUDIO_MP3, // 音频编码格式,当前支持ACC,MP3,G711MU。 audioSampleRate: 48000, // 音频采样率。 fileFormat: media.ContainerFormatType.CFT_MP3, // 封装格式,当前支持MP4,M4A,MP3,WAV。};

// 查看文件列表function getListFile(): string[] { let listFileOption: ListFileOptions = { recursion: false, //是否递归子目录下文件名 listNum: 0, //当设置0时,列出所有文件 filter: { suffix: [".mp3"], //文件后缀名完全匹配 displayName: ["*"], //文件名模糊匹配 fileSizeOver: 0, //文件大小匹配,大于指定大小的文件 lastModifiedAfter: new Date(0).getTime() //文件最近修改时间匹配 } }; //以同步方式列出当前目录下所有文件名和目录名 let files= fs.listFileSync(context.filesDir, listFileOption);
return files}

@Entry@ComponentV2struct AudioTest{ private avRecorder: media.AVRecorder | undefined = undefined; @Local fileName :string='' @Local path: string ='' // 文件沙箱路径,文件后缀名应与封装格式对应。 @Local audioFile: fs.File |null = null
textTimerController: TextTimerController = new TextTimerController(); private count: number = 0; private isSeek: boolean = true; // 用于区分模式是否支持seek操作。 @Local format: string = 'mm:ss.SS'; @Local files :string[]=[]
@Local duration:string='00:00:00'; @Local currentt:string='00:00:00'; @Local playState:boolean=false; private avPlayer?: media.AVPlayer @Local isShowSheet: boolean = false // 注册avplayer回调函数。 setAVPlayerCallback(avPlayer: media.AVPlayer) { //监听资源播放资源的时长,单位为毫秒(ms) avPlayer.on('durationUpdate', (duration: number) => { console.info('durationUpdate called,new duration is :' + duration); this.duration =TimeFormatUtil.formatToHMS(duration) }); //监听资源播放当前时间,单位为毫秒(ms) avPlayer.on('timeUpdate', (time:number) => { console.info('timeUpdate called,and new time is :' + time); this.currentt =TimeFormatUtil.formatToHMS(time) }); // seek操作结果回调函数。 avPlayer.on('seekDone', (seekDoneTime: number) => { console.info(`AVPlayer seek succeeded, seek time is ${seekDoneTime}`); }); // error回调监听函数,当avPlayer在操作过程中出现错误时调用 reset接口触发重置流程。 avPlayer.on('error', (err: BusinessError) => { console.error(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`); avPlayer.reset(); // 调用reset重置资源,触发idle状态。 }); // 状态机变化回调函数。 avPlayer.on('stateChange', async (state: string, reason: media.StateChangeReason) => { switch (state) { case 'idle': // 成功调用reset接口后触发该状态机上报。 console.info('AVPlayer state idle called.'); avPlayer.release(); // 调用release接口销毁实例对象。 break; case 'initialized': // avplayer 设置播放源后触发该状态上报。 console.info('AVPlayer state initialized called.'); avPlayer.audioRendererInfo = { usage: audio.StreamUsage.STREAM_USAGE_MUSIC, // 音频流使用类型:音乐。根据业务场景配置,参考StreamUsage。 rendererFlags: 0 // 音频渲染器标志。 }; avPlayer.prepare(); break; case 'prepared': // prepare调用成功后上报该状态机。 console.info('AVPlayer state prepared called.'); break; case 'playing': // play成功调用后触发该状态机上报。 console.info('AVPlayer state playing called.'); this.playState =true break; case 'paused': // pause成功调用后触发该状态机上报。 console.info('AVPlayer state paused called.'); this.playState =false break; case 'completed': // 播放结束后触发该状态机上报。 console.info('AVPlayer state completed called.'); avPlayer.stop(); //调用播放结束接口。 break; case 'stopped': // stop接口成功调用后触发该状态机上报。 console.info('AVPlayer state stopped called.'); avPlayer.reset(); // 调用reset接口初始化avplayer状态。 this.playState =false break; case 'released': console.info('AVPlayer state released called.'); break; default: console.info('AVPlayer state unknown called.'); break; } }); } aboutToAppear(): void {
this.files =[...getListFile()] } // 创建文件以及设置avConfig.url。 async initRecordingProcess(): Promise<void> { // 1.创建录制实例。 this.avRecorder = await media.createAVRecorder(); this.setAudioRecorderCallback();
} async prepareRecordingProcess(): Promise<void> { if (this.avRecorder != undefined && (this.avRecorder.state == 'idle'|| this.avRecorder.state != 'stopped')) this.fileName = new Date().getFullYear()+'_'+(new Date().getMonth()+1)+'_'+new Date().getDate()+'_'+new Date().getTime()+'.mp3'; this.path = context.filesDir +'/'+ this.fileName; this.audioFile = fs.openSync(this.path, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE); let avConfig: media.AVRecorderConfig = { audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC, // 音频输入源,这里设置为麦克风。 profile: avProfile, url: 'fd://' + this.audioFile.fd, // 参考应用文件访问与管理中的开发示例获取创建的音频文件fd填入此处。 }; // 2.获取录制文件fd赋予avConfig里的url // 3.配置录制参数完成准备工作。 // await this.avRecorder.prepare(avConfig); this.avRecorder!.prepare(avConfig).then(() => { console.log('Invoke prepare succeeded.'); this.textTimerController.reset() }, (err: BusinessError) => { console.error(`Invoke prepare failed, code is ${err.code}, message is ${err.message}`); }) } // 开始录制对应的流程。 async startRecordingProcess() {
if (this.avRecorder != undefined && this.avRecorder.state === 'prepared') { await this.avRecorder.start(); } } // 注册audioRecorder回调函数。 setAudioRecorderCallback() { if (this.avRecorder != undefined) { // 状态机变化回调函数。 this.avRecorder.on('stateChange', (state: media.AVRecorderState, reason: media.StateChangeReason) => { console.log(`---------AudioRecorder current state is ${state}`); if (state.toString()=='started') { this.textTimerController.start() }else if (state.toString()=='paused'){ this.textTimerController.pause() }else if (state.toString()=='stopped'){ this.textTimerController.pause() fs.closeSync(this.audioFile) this.files =[...getListFile()] } }) // 错误上报回调函数。 this.avRecorder.on('error', (err: BusinessError) => { console.error(`AudioRecorder failed, code is ${err.code}, message is ${err.message}`); }) } } // 暂停录制对应的流程。 async pauseRecordingProcess() { if (this.avRecorder != undefined && this.avRecorder.state === 'started') { // 仅在started状态下调用pause为合理状态切换。 await this.avRecorder.pause(); } }

// 恢复录制对应的流程。 async resumeRecordingProcess() { if (this.avRecorder != undefined && this.avRecorder.state === 'paused') { // 仅在paused状态下调用resume为合理状态切换。 await this.avRecorder.resume(); } }

// 停止录制对应的流程。 async stopRecordingProcess() { if (this.avRecorder != undefined) { // 1. 停止录制。 if (this.avRecorder.state === 'started' || this.avRecorder.state === 'paused') { // 仅在started或者paused状态下调用stop为合理状态切换。 await this.avRecorder.stop(); } // 2.重置。 await this.avRecorder.reset();
} }
async aboutToDisappear(): Promise<void> { if (this.avRecorder != undefined) { // 3.释放录制实例。 await this.avRecorder!.release(); this.avRecorder = undefined; // 4.关闭录制文件fd。 fs.closeSync(this.audioFile) }
} build() { Column({space:10}){ TextTimer({ count: 60*60*1000, controller: this.textTimerController }) .format(this.format) .fontColor(Color.Black) .fontSize(50) .onTimer((utc: number, elapsedTime: number) => { }) Button('1.请求权限').onClick(()=>{ requestMicrophone(getContext(this) as common.UIAbilityContext).then((result)=>{ if (result) { showToast('获取成功') }else { showToast('获取失败') requestPermissionOnSetting(microphone_permissions,getContext(this) as common.UIAbilityContext).then((result)=>{ if (result) { showToast('获取成功') }else { showToast('获取失败') } }) } }) }) Button('2.创建实例').onClick(()=>{ this.initRecordingProcess() }) Button('3.准备录制').onClick(()=>{ this.prepareRecordingProcess() }) Button('开始录制').onClick(()=>{ this.startRecordingProcess() }) Button('暂停录制').onClick(()=>{ this.pauseRecordingProcess() }) Button('恢复录制').onClick(()=>{ this.resumeRecordingProcess() }) Button('停止录制').onClick(()=>{ this.stopRecordingProcess() }) List(){ ForEach(this.files,(item: string)=>{ ListItem(){ Text(item + this.getFileSize(item)).onClick(async ()=>{ // 创建avPlayer实例对象。 this.avPlayer = await media.createAVPlayer(); // 创建状态机变化回调函数。 this.setAVPlayerCallback(this.avPlayer); let fdPath = 'fd://'; // 通过UIAbilityContext获取沙箱地址filesDir,以Stage模型为例。 if (context != undefined) { let path = context.filesDir +'/'+ item; // 打开相应的资源文件地址获取fd,并为url赋值触发initialized状态机上报。 let file = await fs.open(path); fdPath = fdPath + '' + file.fd; this.isSeek = true; // 支持seek操作。 this.avPlayer.url = fdPath; } this.isShowSheet = !this.isShowSheet })
}.height(40) }) } .bindSheet(this.isShowSheet, this.SheetBuilder(), { height:150, showClose: false,//是否显示关闭图标 dragBar: true,//是否显示控制条 scrollSizeMode: ScrollSizeMode.FOLLOW_DETENT, onWillDismiss: ((DismissSheetAction: DismissSheetAction) => { //关闭二次确认弹框,使用我们之前定义的全局openCustomDialog let parms =new Params('提示','确定关闭音频播放?',()=>{ OpenCustomDialogUtil.closeDialog() },()=>{ this.avPlayer!.reset(); DismissSheetAction.dismiss() OpenCustomDialogUtil.closeDialog() }) OpenCustomDialogUtil.init(this.getUIContext(),parms); OpenCustomDialogUtil.openDialog() }) }) } }
getFileSize(fileName:string):string { let stat = fs.statSync(context.filesDir +'/'+ fileName); return ' '+(stat.size/1024).toFixed(2)+'KB ' }
@Builder SheetBuilder() { Column() {
Row({space:20}){ Text(this.currentt) Button(this.playState?'暂停':'开始').onClick(()=>{ if (!this.playState) { this.avPlayer!.play(); // 调用播放接口开始播放。 }else { this.avPlayer!.pause(); } })
Text(this.duration) }
}.width('100%').height('100%').backgroundColor(Color.White).justifyContent(FlexAlign.Center) }}</void></void></void>
复制代码


发布于: 刚刚阅读数: 4
用户头像

auhgnixgnahz

关注

还未添加个人签名 2018-07-10 加入

欢迎关注:HarmonyOS开发笔记

评论

发布
暂无评论
鸿蒙Next使用AVRecorder录制和播放音频_鸿蒙Next_auhgnixgnahz_InfoQ写作社区