鸿蒙Next使用AVRecorder录制和播放音频
音频录制开发方式系统提供了多样化的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.设置业务需要的监听事件
事件类型 | 说明 |
---|---|
stateChange | 必要事件,监听播放器的state属性改变。 |
error | 必要事件,监听播放器的错误信息。 |
durationUpdate | 用于进度条,监听进度条长度,刷新资源时长。 |
timeUpdate | 用于进度条,监听进度条当前位置,刷新当前时间。 |
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)
}
}
- 0回答
- 0粉丝
- 0关注
- 鸿蒙Next使用AudioCapturer实现音频录制和AI语言转文字
- harmony OS NEXT-音频录制与播放模块
- HarmonyOS 音频录制开发实战【2】
- HarmonyOS 音频录制开发实战【1】
- HarmonyOS 媒体播放开发实战(音频播放)
- HarmonyOS NEXT 实战之元服务:静态案例效果---最近播放音乐
- 鸿蒙Next网络请求HTTP和RCP的使用和对比
- Flutter 鸿蒙化 使用 Flutter Channel实现和Flutter和HarmonyOS交互
- 使用 HarmonyOS NEXT和Mass快速开发NutPITalk
- 鸿蒙Next文件下载RCP单线程和多线程使用对比
- 鸿蒙-flutter-使用FlutterEntry的路由管理和参数传递_上
- 如何加载和使用自定义字体
- Flutter 鸿蒙化 flutter和鸿蒙next混和渲染
- 鸿蒙Next MVVM模式使用
- HarmonyOS Next 之状态管理AppStorage和持久化存储详解与使用案例