功能介绍:
录音并保存为m4a
格式的音频,然后播放该音频,参考文档使用AVRecorder开发音频录制功能(ArkTS),更详细接口信息请查看接口文档:@ohos.multimedia.media (媒体服务)。
知识点:
- 熟悉使用AVRecorder录音并保存在本地。
- 熟悉使用AVPlayer播放本地音频文件。
- 熟悉对敏感权限的动态申请方式,本项目的敏感权限为
MICROPHONE
。
使用环境:
- API 9
- DevEco Studio 4.0 Release
- Windows 11
- Stage模型
- ArkTS语言
所需权限:
- ohos.permission.MICROPHONE
效果图:
核心代码:
src/main/ets/utils/Permission.ets
是动态申请权限的工具:
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;// 获取应用程序的accessTokenIDlet 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
是录音工具类,进行录音和获取录音数据。
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 = nullprivate avRecorder: media.AVRecorder | undefined = undefined;private avProfile: media.AVRecorderProfile = {audioBitrate: 48000, // 音频比特率audioChannels: audio.AudioChannel.CHANNEL_1, // 音频声道数audioCodec: media.CodecMimeType.AUDIO_AAC, // 音频编码格式,当前只支持aacaudioSampleRate: 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里的urlthis.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.关闭录制文件fdfs.closeSync(this.audioFile);promptAction.showToast({ message: "录音成功!" })}}
}
还需要在src/main/module.json5
添加所需要的权限,注意是在module
中添加,关于字段说明,也需要在各个的string.json
添加:
"requestPermissions": [{"name": "ohos.permission.MICROPHONE","reason": "$string:record_reason","usedScene": {"abilities": ["EntryAbility"],"when": "always"}}]
页面代码如下:
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 avPlayerprivate 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)}})breakcase 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 = truelet fdPath = 'fd://';let res = fs.accessSync(path);if (!res) {console.error(`音频文件不存在:${path}`);this.playIng = falsereturn}console.info(`播放音频文件:${path}`)// 打开相应的资源文件地址获取fdlet 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 = falsethis.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状态,允许更换资源urlthis.avPlayer.reset();break;case 'idle':this.playIng = falsethis.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}`);})}
}