鸿蒙 Next 使用 AVRecorder 录制和播放音频
- 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
@ComponentV2
struct 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>
版权声明: 本文为 InfoQ 作者【auhgnixgnahz】的原创文章。
原文链接:【http://xie.infoq.cn/article/8e748494ca04e9e273a4e4899】。文章转载请联系作者。

auhgnixgnahz
还未添加个人签名 2018-07-10 加入
欢迎关注:HarmonyOS开发笔记
评论