AVRecorder音频录制实战 原创

一路向北545
发布于 2024-12-10 17:30
浏览
2收藏

AVRecorder可以实现音频录制功能,本文通过音频的录制,暂停,恢复录制,停止录制来展示AVRecorder的功能。

AVRecorder音频录制实战-鸿蒙开发者社区

一、页面设计

使用Image组件加载一张麦克风的图标,当未录制时,显示灰色的图标;点击麦克风图标,开始录制,图标变为蓝色且下方显示录制时间;再次点击图标,音频录制暂停,计时暂停,图标恢复灰色;再此点击,恢复录制,图标变为蓝色,计时继续。点击完成按钮,停止音频的录制,显示播放按钮。

二、开发前准备

应用可以调用麦克风录制音频,但该行为属于隐私敏感行为,在调用麦克风前,需要先向用户申请权限“ohos.permission.MICROPHONE”

在录制开始时,先检查麦克风权限是否已获得,如果已获得, 则正常进行接下来的流程,如果没有获得,那么需要申请该权限

检查某个权限是否已经被授予

async function checkAccessToken(permission: Permissions): Promise<abilityAccessCtrl.GrantStatus> {
  let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
  let grantStatus: abilityAccessCtrl.GrantStatus = abilityAccessCtrl.GrantStatus.PERMISSION_DENIED;
  // 获取应用程序的accessTokenID
  let tokenId: number = 0;
  try {
    let bundleInfo: bundleManager.BundleInfo =
      await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
    let appInfo: bundleManager.ApplicationInfo = bundleInfo.appInfo;
    tokenId = appInfo.accessTokenId;
  } catch (error) {
    const err: BusinessError = error as BusinessError;
  }
  // 校验应用是否被授予权限
  try {
    grantStatus = await atManager.checkAccessToken(tokenId, permission);
  } catch (error) {
    const err: BusinessError = error as BusinessError;
  }
  return grantStatus;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.

动态请求权限

async function requestPermission(context: common.UIAbilityContext) {
  const atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
  atManager.requestPermissionsFromUser(context, permissions).then((data) => {
    const granStatus: Array<number> = data.authResults;
    const length: number = granStatus.length;
    for (let i = 0; i < length; i++) {
      if (granStatus[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
      } else {
        return;
      }
    }
  })
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

三、录音状态监听

录音状态分类:

idle:闲置状态。任何状态下调用reset()后都可以进入idle状态

prepared:参数设置完成。此时可以调用start()开始录制

started:正在录制

paused:录制暂停

stopped:录制停止

released:录制资源释放。

error:错误状态

@State state: media.AVRecorderState = "idle"
// 注册audioRecorder回调函数
setRecorderCallback(): void {
  if (this.avRecorder !== undefined) {
    // 状态机变化回调函数
    this.avRecorder.on('stateChange', (state: media.AVRecorderState, _: media.StateChangeReason) => {
      this.state = state
    })
    // 错误上报回调函数
    this.avRecorder.on('error', (err: BusinessError) => {
    })
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

四、主要代码逻辑

(1)开始录制

async startRecord() {
if (this.avRecorder !== undefined) {
  await this.avRecorder.release();
  this.avRecorder = undefined;
}
// 1.创建录制实例
this.avRecorder = await media.createAVRecorder();
this.setRecorderCallback() //设置录音状态监听
// 2.获取录制文件fd赋予avConfig里的url;参考FilePicker文档
const context = getContext(this);
const path = context.filesDir;
this.filePath = path + '/test.mp3';
const file = fs.openSync(this.filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
const fdNumber = file.fd;
this.avConfig.url = 'fd://' + fdNumber;
// 3.配置录制参数完成准备工作
await this.avRecorder.prepare(this.avConfig);
// 4.开始录制
await this.avRecorder.start();
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

(2)暂停录制

// 暂停录制对应的流程
async pauseRecord(): Promise<void> {
  if (this.avRecorder !== undefined && this.avRecorder.state === 'started') { // 仅在started状态下调用pause为合理状态切换
    await this.avRecorder.pause();
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

(3)恢复录制

// 恢复录制对应的流程
async resumeRecord(): Promise<void> {
  if (this.avRecorder !== undefined && this.avRecorder.state === 'paused') { // 仅在paused状态下调用resume为合理状态切换
    await this.avRecorder.resume();
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

(4)停止录制

// 停止录制对应的流程
async stopRecording(): Promise<void> {
  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();
    // 3.释放录制实例
    await this.avRecorder.release();
    this.avRecorder = undefined;
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

(5)使用AvPlayer播放已录制的音频文件

async play() {
  if (this.avRecorder === undefined) {
    // 创建avPlayer实例对象
    this.avPlayer = await media.createAVPlayer();
    // 创建状态机变化回调函数
    this.setAVPlayerCallback(this.avPlayer);
  }

  let fdPath = 'fd://';
  // 通过UIAbilityContext获取沙箱地址filesDir,以Stage模型为例
  const context = getContext(this) as common.UIAbilityContext;
  const pathDir = context.filesDir;
  const path = pathDir + '/01.mp3';
  // 打开相应的资源文件地址获取fd,并为url赋值触发initialized状态机上报
  const file = await fs.open(this.filePath);
  fdPath = fdPath + '' + file.fd;
  if (this.avPlayer) {
    this.avPlayer.url = fdPath;
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

五、完整案例代码

import { media } from '@kit.MediaKit';
import fs from '@ohos.file.fs';
import { abilityAccessCtrl, bundleManager, common, Permissions } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { AVPlayer } from '../AVPlayer';

// 获取麦克风权限
const permissions: Array<Permissions> = ['ohos.permission.MICROPHONE'];
const avPlayer = new AVPlayer();

async function checkAccessToken(permission: Permissions): Promise<abilityAccessCtrl.GrantStatus> {
  let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
  let grantStatus: abilityAccessCtrl.GrantStatus = abilityAccessCtrl.GrantStatus.PERMISSION_DENIED;
  // 获取应用程序的accessTokenID
  let tokenId: number = 0;
  try {
    let bundleInfo: bundleManager.BundleInfo =
      await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
    let appInfo: bundleManager.ApplicationInfo = bundleInfo.appInfo;
    tokenId = appInfo.accessTokenId;
  } catch (error) {
    const err: BusinessError = error as BusinessError;
  }
  // 校验应用是否被授予权限
  try {
    grantStatus = await atManager.checkAccessToken(tokenId, permission);
  } catch (error) {
    const err: BusinessError = error as BusinessError;
  }
  return grantStatus;
}

async function checkPermissions(context: common.UIAbilityContext): Promise<void> {
  let grantStatus: abilityAccessCtrl.GrantStatus = await checkAccessToken(permissions[0]);
  if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
    // 已经授权,可以继续访问目标操作
  } else {
    // 申请麦克风权限
    requestPermission(context)
  }
}

async function requestPermission(context: common.UIAbilityContext) {
  const atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
  atManager.requestPermissionsFromUser(context, permissions).then((data) => {
    const granStatus: Array<number> = data.authResults;
    const length: number = granStatus.length;
    for (let i = 0; i < length; i++) {
      if (granStatus[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
      } else {
        return;
      }
    }
  })
}

@Entry
@Component
struct Index {
  @State filePath: string = ""
  @State state: media.AVRecorderState = "idle"
  @State avRecorder: media.AVRecorder | undefined = undefined;
  context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
  private avProfile: media.AVRecorderProfile = {
    audioBitrate: 100000, // 音频比特率
    audioChannels: 2, // 音频声道数
    audioCodec: media.CodecMimeType.AUDIO_AAC, // 音频编码格式,当前只支持aac
    audioSampleRate: 48000, // 音频采样率
    fileFormat: media.ContainerFormatType.CFT_MPEG_4A, // 封装格式,当前只支持m4a
  };
  private avConfig: media.AVRecorderConfig = {
    audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC, // 音频输入源,这里设置为麦克风
    profile: this.avProfile,
    url: 'fd://35', // 参考应用文件访问与管理开发示例新建并读写一个文件
  };
  intervalId: number = 0
  @State seconds: number = 0

  // 注册audioRecorder回调函数
  setRecorderCallback(): void {
    if (this.avRecorder !== undefined) {
      // 状态机变化回调函数
      this.avRecorder.on('stateChange', (state: media.AVRecorderState, _: media.StateChangeReason) => {
        this.state = state
      })
      // 错误上报回调函数
      this.avRecorder.on('error', (err: BusinessError) => {
      })
    }
  }

  public secondToTime(d: number): string {
    if (d < 0) {
      d = 0
    }
    let duration = Math.round(d)
    let min = Math.floor(d / 60)
    let second = Math.round(duration - min * 60)
    return (min < 10 ? `0${min}` : `${min}:`) + `:` + (second < 10 ? `0${second}` : `${second}`)
  }

  aboutToAppear(): void {
  }

  build() {
    Column() {
      if (this.state === "released") {
        //录音完成时
        Image(this.playState === "playing" ? $r("app.media.pause") : $r("app.media.play"))
          .width(80)
          .height(80)
          .onClick(() => {
            if (this.playState === "idle" || this.playState === "released") {
              this.play()
            } else if (this.playState === "playing") {
              this.avPlayer?.pause()
            } else if (this.playState === "paused") {
              this.avPlayer?.play()
            }
          })
      } else {
        Image(this.state === "started" ? $r("app.media.record") : $r("app.media.record_normal"))
          .width(80)
          .height(80)
          .onClick(() => {
            if (this.state === "idle" || this.avRecorder === undefined) {
              this.startRecord()
            } else if (this.state === "started") {
              this.pauseRecord()
            } else if (this.state === "paused") {
              this.resumeRecord()
            }
          })
      }
      if (this.state !== "released") {
        Text(this.secondToTime(this.seconds))
          .margin({ top: 10 })
      }


      if (this.state === "started" || this.state === "paused") {
        Button("完成")
          .margin({ top: 10 }).onClick(() => {
          this.stopRecording()
        })
      }
    }
    .alignItems(HorizontalAlign.Center)
    .justifyContent(FlexAlign.Center)
    .height('100%')
    .width('100%')
  }

  // 停止录制对应的流程
  async stopRecording(): Promise<void> {
    if (this.avRecorder !== undefined) {
      // 1. 停止录制
      if (this.avRecorder.state === 'started'
        || this.avRecorder.state === 'paused') { // 仅在started或者paused状态下调用stop为合理状态切换
        await this.avRecorder.stop();
        clearInterval(this.intervalId);
      }
      // 2.重置
      await this.avRecorder.reset();
      // 3.释放录制实例
      await this.avRecorder.release();
      this.avRecorder = undefined;
    }
  }

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

  // 暂停录制对应的流程
  async pauseRecord(): Promise<void> {
    if (this.avRecorder !== undefined && this.avRecorder.state === 'started') { // 仅在started状态下调用pause为合理状态切换
      await this.avRecorder.pause();
    }
  }

  async startRecord() {
    checkPermissions(this.context).then(async () => {
      if (this.avRecorder !== undefined) {
        await this.avRecorder.release();
        this.avRecorder = undefined;
      }
      // 1.创建录制实例
      this.avRecorder = await media.createAVRecorder();
      this.setRecorderCallback() //设置录音状态监听
      // 2.获取录制文件fd赋予avConfig里的url;参考FilePicker文档
      const context = getContext(this);
      const path = context.filesDir;
      this.filePath = path + '/test.mp3';
      const file = fs.openSync(this.filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
      const fdNumber = file.fd;
      this.avConfig.url = 'fd://' + fdNumber;
      // 3.配置录制参数完成准备工作
      await this.avRecorder.prepare(this.avConfig);
      // 4.开始录制
      await this.avRecorder.start();

      //计时
      this.intervalId = setInterval(() => {
        if (this.state === "started") {
          this.seconds++
        }
      }, 1000);
    })

  }

  private avPlayer: media.AVPlayer | undefined = undefined
  @State playState: string = "idle"

  async play() {
    if (this.avRecorder === undefined) {
      // 创建avPlayer实例对象
      this.avPlayer = await media.createAVPlayer();
      // 创建状态机变化回调函数
      this.setAVPlayerCallback(this.avPlayer);
    }

    let fdPath = 'fd://';
    // 通过UIAbilityContext获取沙箱地址filesDir,以Stage模型为例
    const context = getContext(this) as common.UIAbilityContext;
    const pathDir = context.filesDir;
    const path = pathDir + '/01.mp3';
    // 打开相应的资源文件地址获取fd,并为url赋值触发initialized状态机上报
    const file = await fs.open(this.filePath);
    fdPath = fdPath + '' + file.fd;
    if (this.avPlayer) {
      this.avPlayer.url = fdPath;
    }
  }

  // 注册avplayer回调函数
  setAVPlayerCallback(avPlayer: media.AVPlayer): void {
    // 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, _: media.StateChangeReason) => {
      this.playState = state
      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.prepare();
          break;
        case 'prepared': // prepare调用成功后上报该状态机
          console.info('AVPlayer state prepared called.');
          avPlayer.play(); // 调用播放接口开始播放
          break;
        case 'playing': // play成功调用后触发该状态机上报
          console.info('AVPlayer state playing called.');
          break;
        case 'paused': // pause成功调用后触发该状态机上报
          console.info('AVPlayer state paused called.');
          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状态
          break;
        case 'released':
          console.info('AVPlayer state released called.');
          break;
        default:
          console.info('AVPlayer state unknown called.');
          break;
      }
    })
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.
  • 166.
  • 167.
  • 168.
  • 169.
  • 170.
  • 171.
  • 172.
  • 173.
  • 174.
  • 175.
  • 176.
  • 177.
  • 178.
  • 179.
  • 180.
  • 181.
  • 182.
  • 183.
  • 184.
  • 185.
  • 186.
  • 187.
  • 188.
  • 189.
  • 190.
  • 191.
  • 192.
  • 193.
  • 194.
  • 195.
  • 196.
  • 197.
  • 198.
  • 199.
  • 200.
  • 201.
  • 202.
  • 203.
  • 204.
  • 205.
  • 206.
  • 207.
  • 208.
  • 209.
  • 210.
  • 211.
  • 212.
  • 213.
  • 214.
  • 215.
  • 216.
  • 217.
  • 218.
  • 219.
  • 220.
  • 221.
  • 222.
  • 223.
  • 224.
  • 225.
  • 226.
  • 227.
  • 228.
  • 229.
  • 230.
  • 231.
  • 232.
  • 233.
  • 234.
  • 235.
  • 236.
  • 237.
  • 238.
  • 239.
  • 240.
  • 241.
  • 242.
  • 243.
  • 244.
  • 245.
  • 246.
  • 247.
  • 248.
  • 249.
  • 250.
  • 251.
  • 252.
  • 253.
  • 254.
  • 255.
  • 256.
  • 257.
  • 258.
  • 259.
  • 260.
  • 261.
  • 262.
  • 263.
  • 264.
  • 265.
  • 266.
  • 267.
  • 268.
  • 269.
  • 270.
  • 271.
  • 272.
  • 273.
  • 274.
  • 275.
  • 276.
  • 277.
  • 278.
  • 279.
  • 280.
  • 281.
  • 282.
  • 283.
  • 284.
  • 285.
  • 286.
  • 287.
  • 288.
  • 289.
  • 290.

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
收藏 2
回复
举报
2


回复
    相关推荐