功能介绍:
录音并保存为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;
// 获取应用程序的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
是录音工具类,进行录音和获取录音数据。
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
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
添加:
"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 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}`);
})
}
}