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

2025-06-27 22:55:39
105次阅读
0个评论

音频录制开发方式系统提供了多样化的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音频文件。 先看一下实现效果: 录音演示.gif 录制实现步骤: 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)
  }
}
收藏00

登录 后评论。没有帐号? 注册 一个。