Harmony鸿蒙应用开发-录音保存并播放音频

功能介绍:

录音并保存为m4a格式的音频,然后播放该音频,参考文档使用AVRecorder开发音频录制功能(ArkTS),更详细接口信息请查看接口文档@ohos.multimedia.media (媒体服务)

知识点:

  1. 熟悉使用AVRecorder录音并保存在本地。
  2. 熟悉使用AVPlayer播放本地音频文件。
  3. 熟悉对敏感权限的动态申请方式,本项目的敏感权限为MICROPHONE

使用环境:

  • API 9
  • DevEco Studio 4.0 Release
  • Windows 11
  • Stage模型
  • ArkTS语言

所需权限:

  1. ohos.permission.MICROPHONE

效果图:

核心代码:

src/main/ets/utils/Permission.ets是动态申请权限的工具:

javascript 复制代码
import bundleManager from '@ohos.bundle.bundleManager';
import abilityAccessCtrl, { Permissions } from '@ohos.abilityAccessCtrl';

async function checkAccessToken(permission: Permissions): Promise<abilityAccessCtrl.GrantStatus> {
  let atManager = abilityAccessCtrl.createAtManager();
  let grantStatus: abilityAccessCtrl.GrantStatus;

  // 获取应用程序的accessTokenID
  let tokenId: number;
  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 (err) {
    console.error(`getBundleInfoForSelf failed, code is ${err.code}, message is ${err.message}`);
  }

  // 校验应用是否被授予权限
  try {
    grantStatus = await atManager.checkAccessToken(tokenId, permission);
  } catch (err) {
    console.error(`checkAccessToken failed, code is ${err.code}, message is ${err.message}`);
  }

  return grantStatus;
}

export async function checkPermissions(permission: Permissions): Promise<boolean> {
  let grantStatus: abilityAccessCtrl.GrantStatus = await checkAccessToken(permission);

  if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
    return true
  } else {
    return false
  }
}

src/main/ets/utils/Recorder.ets是录音工具类,进行录音和获取录音数据。

javascript 复制代码
import media from '@ohos.multimedia.media';
import fs from '@ohos.file.fs';
import promptAction from '@ohos.promptAction';
import audio from '@ohos.multimedia.audio';

export default class AudioRecorder {
  private audioFile = null
  private avRecorder: media.AVRecorder | undefined = undefined;
  private avProfile: media.AVRecorderProfile = {
    audioBitrate: 48000, // 音频比特率
    audioChannels: audio.AudioChannel.CHANNEL_1, // 音频声道数
    audioCodec: media.CodecMimeType.AUDIO_AAC, // 音频编码格式,当前只支持aac next5支持mp3
    audioSampleRate: audio.AudioSamplingRate.SAMPLE_RATE_16000, // 音频采样率
    fileFormat: media.ContainerFormatType.CFT_MPEG_4A, // 封装格式,当前只支持m4a
  };
  private avConfig: media.AVRecorderConfig = {
    audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC, // 音频输入源,这里设置为麦克风
    profile: this.avProfile,
    url: '', // 录音文件的url
  };

  // 注册audioRecorder回调函数
  setAudioRecorderCallback() {
    if (this.avRecorder != undefined) {
      // 错误上报回调函数
      this.avRecorder.on('error', (err) => {
        console.error(`录音器发生错误,错误码为:${err.code}, 错误信息为:${err.message}`);
      })
    }
  }

  // 开始录制
  async startRecord(audioPath: string) {
    // 1.创建录制实例
    this.avRecorder = await media.createAVRecorder();
    this.setAudioRecorderCallback();
    // 创建并打开录音文件
    this.audioFile = fs.openSync(audioPath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
    // 2.获取录制文件fd赋予avConfig里的url
    this.avConfig.url = `fd://${this.audioFile.fd}`
    // 3.配置录制参数完成准备工作
    await this.avRecorder.prepare(this.avConfig);
    // 4.开始录制
    await this.avRecorder.start();
    console.info('正在录音...')
  }

  // 暂停录制
  async pauseRecord() {
    // 仅在started状态下调用pause为合理状态切换
    if (this.avRecorder != undefined && this.avRecorder.state === 'started') {
      await this.avRecorder.pause();
    }
  }

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

  // 停止录制
  async stopRecord() {
    if (this.avRecorder != undefined) {
      // 1. 停止录制
      // 仅在started或者paused状态下调用stop为合理状态切换
      if (this.avRecorder.state === 'started'
        || this.avRecorder.state === 'paused') {
        await this.avRecorder.stop();
      }
      // 2.重置
      await this.avRecorder.reset();
      // 3.释放录制实例
      await this.avRecorder.release();
      // 4.关闭录制文件fd
      fs.closeSync(this.audioFile);

      promptAction.showToast({ message: "录音成功!" })
    }
  }
}

还需要在src/main/module.json5添加所需要的权限,注意是在module中添加,关于字段说明,也需要在各个的string.json添加:

javascript 复制代码
    "requestPermissions": [
      {
        "name": "ohos.permission.MICROPHONE",
        "reason": "$string:record_reason",
        "usedScene": {
          "abilities": [
            "EntryAbility"
          ],
          "when": "always"
        }
      }
    ]

页面代码如下:

javascript 复制代码
import { checkPermissions } from '../utils/Permission';
import abilityAccessCtrl, { Permissions } from '@ohos.abilityAccessCtrl';
import common from '@ohos.app.ability.common';
import AudioCapturer from '../utils/Recorder';
import AudioRecorder from '../utils/Recorder';
import promptAction from '@ohos.promptAction';
import media from '@ohos.multimedia.media';
import fs from '@ohos.file.fs';


// 需要动态申请的权限
const permissions: Array<Permissions> = ['ohos.permission.MICROPHONE'];
// 获取程序的上下文
const context = getContext(this) as common.UIAbilityContext;
// 获取项目的files目录
const filesDir = context.filesDir;
// 如果文件夹不存在就创建
fs.access(filesDir, (err, res: boolean) => {
  if (!res) {
    fs.mkdirSync(filesDir)
  }
});

// 录音文件路径
let audioPath = filesDir + "/audio.m4a";

@Entry
@Component
struct Index {
  @State recordBtnText: string = '按下录音'
  @State playBtnText: string = '播放音频'
  // 录音器
  private audioRecorder?: AudioRecorder;
  // 播放器
  private avPlayer
  private playIng: boolean = false

  // 页面显示时
  async onPageShow() {
    // 判断是否已经授权
    let promise = checkPermissions(permissions[0])
    promise.then((result) => {
      if (result) {
        // 初始化录音器
        if (this.audioRecorder == null) {
          this.audioRecorder = new AudioRecorder()
        }
      } else {
        this.reqPermissionsAndRecord(permissions)
      }
    })
    // 创建avPlayer实例对象
    this.avPlayer = await media.createAVPlayer();
    // 创建状态机变化回调函数
    this.setAVPlayerCallback();
    console.info('播放器准备完成')
  }

  build() {
    Row() {
      RelativeContainer() {
        // 录音按钮
        Button(this.recordBtnText)
          .id('btn1')
          .width('90%')
          .margin({ bottom: 10 })
          .alignRules({
            bottom: { anchor: '__container__', align: VerticalAlign.Bottom },
            middle: { anchor: '__container__', align: HorizontalAlign.Center }
          })
          .onTouch((event) => {
            switch (event.type) {
              case TouchType.Down:
                console.info('按下按钮')
              // 判断是否有权限
                let promise = checkPermissions(permissions[0])
                promise.then((result) => {
                  if (result) {
                    // 开始录音
                    this.audioRecorder.startRecord(audioPath)
                    this.recordBtnText = '录音中...'
                  } else {
                    // 申请权限
                    this.reqPermissionsAndRecord(permissions)
                  }
                })
                break
              case TouchType.Up:
                console.info('松开按钮')
                if (this.audioRecorder != null) {
                  // 停止录音
                  this.audioRecorder.stopRecord()
                }
                this.recordBtnText = '按下录音'
                break
            }
          })

        // 录音按钮
        Button(this.playBtnText)
          .id('btn2')
          .width('90%')
          .margin({ bottom: 10 })
          .alignRules({
            bottom: { anchor: 'btn1', align: VerticalAlign.Top },
            middle: { anchor: '__container__', align: HorizontalAlign.Center }
          })
          .onClick(() => {
            if (!this.playIng) {
              this.playBtnText = '播放中...'
              // 播放音频
              this.playAudio(audioPath)
            }else {
              // 停止播放
              this.stopPlay()
            }
          })
      }
      .width('100%')
      .height('100%')
    }
    .width('100%')
    .height('100%')
  }

  // 播放音频
  async playAudio(path: string) {
    this.playIng = true
    let fdPath = 'fd://';
    let res = fs.accessSync(path);
    if (!res) {
      console.error(`音频文件不存在:${path}`);
      this.playIng = false
      return
    }
    console.info(`播放音频文件:${path}`)
    // 打开相应的资源文件地址获取fd
    let file = await fs.open(path);
    fdPath = fdPath + '' + file.fd;
    // url赋值触发initialized状态机上报
    this.avPlayer.url = fdPath;
  }

  // 停止播放
  stopPlay() {
    this.avPlayer.reset();
  }

  // 注册avplayer回调函数
  setAVPlayerCallback() {
    this.avPlayer.on('error', (err) => {
      this.playIng = false
      this.playBtnText = '播放音频'
      console.error(`播放器发生错误,错误码:${err.code}, 错误信息:${err.message}`);
      // 调用reset重置资源,触发idle状态
      this.avPlayer.reset();
    })
    // 状态机变化回调函数
    this.avPlayer.on('stateChange', async (state) => {
      switch (state) {
        case 'initialized':
        // 资源初始化完成,开始准备文件
          this.avPlayer.prepare();
          break;
        case 'prepared':
        // 资源准备完成,开始准备文件
          this.avPlayer.play();
          break;
        case 'completed':
        // 调用reset()重置资源,AVPlayer重新进入idle状态,允许更换资源url
          this.avPlayer.reset();
          break;
        case 'idle':
          this.playIng = false
          this.playBtnText = '播放音频'
          break;
      }
    })
  }

  // 申请权限
  reqPermissionsAndRecord(permissions: Array<Permissions>): void {
    let atManager = abilityAccessCtrl.createAtManager();
    // requestPermissionsFromUser会判断权限的授权状态来决定是否唤起弹窗
    atManager.requestPermissionsFromUser(context, permissions).then((data) => {
      let grantStatus: Array<number> = data.authResults;
      let length: number = grantStatus.length;
      for (let i = 0; i < length; i++) {
        if (grantStatus[i] === 0) {
          // 用户授权,可以继续访问目标操作
          console.info('授权成功')
          if (this.audioRecorder == null) {
            this.audioRecorder = new AudioCapturer()
          }
        } else {
          promptAction.showToast({ message: '授权失败,需要授权才能录音' })
          return;
        }
      }
    }).catch((err) => {
      console.error(`requestPermissionsFromUser failed, code is ${err.code}, message is ${err.message}`);
    })
  }
}

原文:鸿蒙应用开发-录音保存并播放音频_requestpermissions": [ { "name": "ohos.permission.-CSDN博客

相关推荐
nashane8 小时前
HarmonyOS 6学习:CapsLock键失效诊断与长截图完整实现指南
学习·华为·harmonyos
richard_yuu10 小时前
鸿蒙心理测评模块实战|PHQ-9/GAD7双量表答题、实时计分与结果本地化存储
华为·harmonyos
不爱吃糖的程序媛13 小时前
2026年Electron 鸿蒙PC环境搭建指南
人工智能·华为·harmonyos
nashane13 小时前
HarmonyOS 6学习:长截图功能开发中的滚动拼接与权限处理实战
人工智能·华为·harmonyos
大师兄666814 小时前
从零开发一个 HarmonyOS 输入法——KikaInputMethod 完整拆解
harmonyos·服务卡片·harmonyos6·formkit
Python私教19 小时前
鸿蒙 NEXT 也能接 MCP?用 ArkTS 跑通 AI Agent 工具链
人工智能·华为·harmonyos
Swift社区1 天前
分布式能力在鸿蒙 PC 上到底怎么用?
分布式·华为·harmonyos
nashane1 天前
HarmonyOS 6学习:外接键盘CapsLock与长截图功能的实战调试与完整解决方案
学习·华为·计算机外设·harmonyos
aqi002 天前
一文理清 HarmonyOS 6.0.2 涵盖的十个升级点
android·华为·harmonyos·鸿蒙·harmony