HarmonyOS开发音频录制:AVRecorder、权限申请与录音文件管理全攻略

HarmonyOS开发音频录制:AVRecorder、权限申请与录音文件管理全攻略

HarmonyOS开发中的音频录制:AVRecorder、权限申请与录音文件管理全攻略

📌 核心要点:掌握 HarmonyOS 音频录制的完整流程——从权限申请到 AVRecorder 状态机管理,从录音参数配置到文件存储策略,实现生产级录音功能

一、背景与动机

你有没有用过手机上的录音 App?按下录音键,对着手机说话,松手停止,一段清晰的语音备忘录就保存好了。或者在语音聊天里,按住说话、松开发送,对面就能听到你的声音。这些看似简单的操作,背后涉及权限申请、音频采集、编码压缩、文件存储等一系列技术环节。

为什么音频录制比播放更复杂?

权限门槛:麦克风是敏感权限,需要用户主动授权,还得处理拒绝场景

配置参数多:采样率、位深、声道数、编码格式、容器格式……组合起来眼花缭乱

状态管理严格:和 AVPlayer 一样,AVRecorder 也有严格的状态机,乱来就崩溃

文件管理:录音文件存哪里?怎么命名?怎么清理?都是实际问题

异常处理:来电中断、权限被收回、存储空间不足……各种边界情况

今天这篇,咱们就把音频录制的每个环节都拆解清楚。

二、核心原理

2.1 AVRecorder 状态机

AVRecorder 是 HarmonyOS 提供的音频/视频录制核心类。和 AVPlayer 类似,它也是状态驱动的,但状态流转方向不同——从"准备"到"录制"。

打个比方:AVRecorder 就像一台录像机。你得先装好录像带(配置输出文件)、设置好参数(分辨率、帧率),然后才能按录制键。录制过程中可以暂停、恢复,最后停止保存。

stateDiagram-v2

classDef primary fill:#4CAF50,stroke:#388E3C,color:#fff

classDef warning fill:#FF9800,stroke:#F57C00,color:#fff

classDef error fill:#F44336,stroke:#D32F2F,color:#fff

classDef info fill:#2196F3,stroke:#1976D2,color:#fff

classDef purple fill:#9C27B0,stroke:#7B1FA2,color:#fff

[*] --> idle : 创建AVRecorder

idle --> prepared : prepare(config)

prepared --> recording : start()

recording --> paused : pause()

paused --> recording : resume()

recording --> stopped : stop()

paused --> stopped : stop()

stopped --> prepared : prepare(config)

stopped --> idle : reset()

prepared --> idle : reset()

any --> released : release()

any --> error : 异常

class idle primary

class prepared info

class recording primary

class paused warning

class stopped error

class released error

class error error

状态机关键规则:

当前状态

允许的操作

目标状态

idle

prepare(config)

prepared

prepared

start

recording

recording

pause / stop

paused / stopped

paused

resume / stop

recording / stopped

stopped

prepare / reset

prepared / idle

任何状态

release

released

2.2 录音配置参数详解

录音配置是 AVRecorder 的核心,参数选错了要么录不了,要么文件巨大。来看关键参数:

参数

说明

推荐值

audioSourceType

音频源类型

VOICE_COMMUNICATION(通话)/ MIC(麦克风)

profile.audioBitrate

音频比特率

48000 ~ 128000 bps

profile.audioSampleRate

采样率

44100 / 48000 Hz

profile.audioChannels

声道数

1(单声道)/ 2(立体声)

profile.audioCodec

编码格式

AAC_LC / AMR_NB / AMR_WB

profile.fileFormat

容器格式

FORMAT_MPEG_4 / FORMAT_AMR / FORMAT_THREE_GPP

url

输出文件路径

fd:// 或 file://

常见配置组合:

场景

编码

容器

采样率

比特率

声道

高质量录音

AAC_LC

MPEG_4

48000

128000

2

语音备忘

AAC_LC

MPEG_4

44100

64000

1

电话录音

AMR_WB

THREE_GPP

16000

23850

1

语音消息

AMR_NB

AMR

8000

12200

1

2.3 录音权限与安全流程

flowchart TB

classDef primary fill:#4CAF50,stroke:#388E3C,color:#fff

classDef warning fill:#FF9800,stroke:#F57C00,color:#fff

classDef error fill:#F44336,stroke:#D32F2F,color:#fff

classDef info fill:#2196F3,stroke:#1976D2,color:#fff

classDef purple fill:#9C27B0,stroke:#7B1FA2,color:#fff

A[用户点击录音] --> B{检查麦克风权限}

B -->|已授权| D[创建AVRecorder]

B -->|未授权| C[请求权限]

C -->|用户同意| D

C -->|用户拒绝| E[提示用户去设置授权]

D --> F[配置录音参数]

F --> G[prepare]

G --> H[start录音]

H --> I{录音中}

I -->|用户停止| J[stop]

I -->|来电中断| K[暂停录音]

K -->|通话结束| I

J --> L[保存文件]

L --> M[release释放]

class A primary

class B warning

class C info

class D primary

class E error

class F info

class G primary

class H primary

class I primary

class J warning

class K error

class L primary

class M error

三、代码实战

3.1 录音权限申请与检查

录音的第一步,永远是权限。没有权限,一切都是白搭:

import { abilityAccessCtrl, bundleManager, Permissions } from '@kit.AbilityKit';

import { BusinessError } from '@kit.BasicServicesKit';

@Entry

@Component

struct PermissionManager {

@State hasPermission: boolean = false;

@State permissionStatus: string = '未检查';

// 麦克风权限

private readonly MICROPHONE_PERMISSION: Permissions = 'ohos.permission.MICROPHONE';

aboutToAppear(): void {

this.checkPermission();

}

/**

* 检查是否已授权麦克风权限

*/

async checkPermission(): Promise {

try {

const atManager = abilityAccessCtrl.createAtManager();

const bundleInfo = await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT);

const bundleName = bundleInfo.name;

const grantStatus = await atManager.checkAccessToken(

bundleInfo.appInfo.accessTokenId,

this.MICROPHONE_PERMISSION

);

if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {

this.hasPermission = true;

this.permissionStatus = '已授权 ✅';

} else {

this.hasPermission = false;

this.permissionStatus = '未授权 ❌';

}

} catch (err) {

const error = err as BusinessError;

console.error(`[权限] 检查失败: ${error.message}`);

this.permissionStatus = '检查失败';

}

}

/**

* 请求麦克风权限

*/

async requestPermission(): Promise {

try {

const atManager = abilityAccessCtrl.createAtManager();

const result = await atManager.requestPermissionsFromUser(

getContext(this),

[this.MICROPHONE_PERMISSION]

);

// authResult: 0=已授权, -1=未授权

if (result.authResults[0] === 0) {

this.hasPermission = true;

this.permissionStatus = '已授权 ✅';

console.info('[权限] 麦克风权限已获取');

return true;

} else {

this.hasPermission = false;

this.permissionStatus = '用户拒绝 ❌';

console.warn('[权限] 用户拒绝了麦克风权限');

return false;

}

} catch (err) {

const error = err as BusinessError;

console.error(`[权限] 请求失败: ${error.message}`);

return false;

}

}

/**

* 引导用户去系统设置中手动授权

*/

async openSystemSettings(): Promise {

try {

const context = getContext(this) as common.UIAbilityContext;

// 打开应用设置页面

context.openLink('settings://application_settings');

} catch (err) {

console.error('[权限] 无法打开系统设置');

}

}

build() {

Column({ space: 20 }) {

Text('🎤 麦克风权限管理')

.fontSize(24)

.fontWeight(FontWeight.Bold)

Text(`权限状态: ${this.permissionStatus}`)

.fontSize(16)

.fontColor(this.hasPermission ? '#4CAF50' : '#F44336')

if (!this.hasPermission) {

Column({ space: 12 }) {

Text('录音功能需要麦克风权限')

.fontSize(14)

.fontColor('#888')

Button('请求权限')

.width(200)

.backgroundColor('#2196F3')

.onClick(() => this.requestPermission())

Button('去设置中授权')

.width(200)

.backgroundColor('#FF9800')

.onClick(() => this.openSystemSettings())

}

} else {

Text('权限已就绪,可以开始录音 🎉')

.fontSize(16)

.fontColor('#4CAF50')

}

}

.width('100%')

.height('100%')

.justifyContent(FlexAlign.Center)

.padding(20)

}

}

⚠️ 重要:别忘了在 module.json5 中声明权限:

"requestPermissions": [

{

"name": "ohos.permission.MICROPHONE",

"reason": "$string:microphone_reason",

"usedScene": {

"abilities": ["EntryAbility"],

"when": "inuse"

}

}

]

3.2 AVRecorder 完整录音器

有了权限,就可以正式录音了。下面是一个功能完整的录音器,支持暂停/恢复、计时、文件管理:

import { media } from '@kit.MediaKit';

import { fileIo as fs } from '@kit.CoreFileKit';

import { common } from '@kit.AbilityKit';

import { BusinessError } from '@kit.BasicServicesKit';

// 录音配置预设

interface RecordPreset {

name: string;

audioCodec: media.CodecMimeType;

fileFormat: media.ContainerFormatType;

sampleRate: number;

bitrate: number;

channels: number;

}

@Entry

@Component

struct AudioRecorder {

// AVRecorder 实例

private avRecorder: media.AVRecorder | null = null;

// 录音文件路径

private currentFilePath: string = '';

// 录音计时器

private timer: number = -1;

// 状态

@State isRecording: boolean = false;

@State isPaused: boolean = false;

@State recordDuration: number = 0; // 录音时长(秒)

@State recorderState: string = 'idle';

@State recordList: string[] = []; // 录音文件列表

// 录音预设

private presets: Record = {

high: {

name: '高质量',

audioCodec: media.CodecMimeType.AUDIO_AAC,

fileFormat: media.ContainerFormatType.CFT_MPEG_4,

sampleRate: 48000,

bitrate: 128000,

channels: 2

},

normal: {

name: '标准',

audioCodec: media.CodecMimeType.AUDIO_AAC,

fileFormat: media.ContainerFormatType.CFT_MPEG_4,

sampleRate: 44100,

bitrate: 64000,

channels: 1

},

voice: {

name: '语音',

audioCodec: media.CodecMimeType.AUDIO_AMR_WB,

fileFormat: media.ContainerFormatType.CFT_THREE_GPP,

sampleRate: 16000,

bitrate: 23850,

channels: 1

}

};

// 当前使用的预设

private currentPreset: string = 'normal';

aboutToDisappear(): void {

this.releaseRecorder();

}

/**

* 生成录音文件路径

*/

generateFilePath(): string {

const context = getContext(this) as common.Context;

const timestamp = new Date().toISOString().replace(/[:.]/g, '-');

const ext = this.currentPreset === 'voice' ? '.3gp' : '.m4a';

return `${context.filesDir}/recording_${timestamp}${ext}`;

}

/**

* 开始录音

*/

async startRecording(): Promise {

try {

// 创建 AVRecorder

this.avRecorder = await media.createAVRecorder();

this.recorderState = 'idle';

// 注册状态监听

this.avRecorder.on('stateChange', (state: string) => {

this.recorderState = state;

console.info(`[录音] 状态变化: ${state}`);

});

// 错误监听

this.avRecorder.on('error', (err: BusinessError) => {

console.error(`[录音] 错误: ${err.message}`);

this.isRecording = false;

this.isPaused = false;

this.stopTimer();

});

// 生成文件路径

this.currentFilePath = this.generateFilePath();

// 创建文件并获取 fd

const file = fs.openSync(this.currentFilePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);

// 获取当前预设

const preset = this.presets[this.currentPreset];

// 配置录音参数

const config: media.AVRecorderConfig = {

audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC,

profile: {

audioBitrate: preset.bitrate,

audioChannels: preset.channels,

audioCodec: preset.audioCodec,

audioSampleRate: preset.sampleRate,

fileFormat: preset.fileFormat

},

url: `fd://${file.fd}`,

};

// prepare → prepared

await this.avRecorder.prepare(config);

// start → recording

await this.avRecorder.start();

// 更新状态

this.isRecording = true;

this.isPaused = false;

this.recordDuration = 0;

// 启动计时器

this.startTimer();

console.info(`[录音] 开始录制: ${this.currentFilePath}`);

} catch (err) {

const error = err as BusinessError;

console.error(`[录音] 启动失败: ${error.message}`);

this.isRecording = false;

}

}

/**

* 暂停录音

*/

async pauseRecording(): Promise {

if (!this.avRecorder || this.recorderState !== 'recording') return;

try {

await this.avRecorder.pause();

this.isPaused = true;

this.stopTimer();

console.info('[录音] 已暂停');

} catch (err) {

const error = err as BusinessError;

console.error(`[录音] 暂停失败: ${error.message}`);

}

}

/**

* 恢复录音

*/

async resumeRecording(): Promise {

if (!this.avRecorder || this.recorderState !== 'paused') return;

try {

await this.avRecorder.resume();

this.isPaused = false;

this.startTimer();

console.info('[录音] 已恢复');

} catch (err) {

const error = err as BusinessError;

console.error(`[录音] 恢复失败: ${error.message}`);

}

}

/**

* 停止录音

*/

async stopRecording(): Promise {

if (!this.avRecorder) return;

try {

// 停止录音

await this.avRecorder.stop();

// 释放资源

await this.avRecorder.release();

this.avRecorder = null;

// 更新状态

this.isRecording = false;

this.isPaused = false;

this.stopTimer();

// 将录音文件加入列表

this.recordList.unshift(this.currentFilePath);

console.info(`[录音] 已停止,文件保存至: ${this.currentFilePath}`);

} catch (err) {

const error = err as BusinessError;

console.error(`[录音] 停止失败: ${error.message}`);

}

}

/**

* 释放录音器资源

*/

async releaseRecorder(): Promise {

if (this.avRecorder) {

try {

if (this.recorderState === 'recording' || this.recorderState === 'paused') {

await this.avRecorder.stop();

}

await this.avRecorder.release();

this.avRecorder = null;

} catch (err) {

console.error('[录音] 释放失败');

}

}

this.stopTimer();

this.isRecording = false;

this.isPaused = false;

}

/**

* 启动计时器

*/

private startTimer(): void {

this.stopTimer();

this.timer = setInterval(() => {

this.recordDuration++;

}, 1000) as unknown as number;

}

/**

* 停止计时器

*/

private stopTimer(): void {

if (this.timer !== -1) {

clearInterval(this.timer);

this.timer = -1;

}

}

/**

* 格式化时长

*/

formatDuration(seconds: number): string {

const mins = Math.floor(seconds / 60);

const secs = seconds % 60;

return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;

}

/**

* 从路径中提取文件名

*/

getFileName(path: string): string {

return path.split('/').pop() || path;

}

build() {

Column({ space: 20 }) {

// 标题

Text('🎙️ 录音器')

.fontSize(24)

.fontWeight(FontWeight.Bold)

// 录音时长显示

Text(this.formatDuration(this.recordDuration))

.fontSize(64)

.fontWeight(FontWeight.Bold)

.fontColor(this.isRecording ? '#F44336' : '#fff')

.animation({ duration: 500 })

// 状态提示

Text(

this.isRecording

? (this.isPaused ? '已暂停' : '录音中...')

: '点击开始录音'

)

.fontSize(16)

.fontColor('#888')

// 录音预设选择

Row({ space: 12 }) {

ForEach(Object.entries(this.presets), ([key, preset]: [string, RecordPreset]) => {

Text(preset.name)

.fontSize(14)

.padding({ left: 16, right: 16, top: 8, bottom: 8 })

.borderRadius(20)

.backgroundColor(this.currentPreset === key ? '#4CAF50' : '#333')

.fontColor(this.currentPreset === key ? '#fff' : '#aaa')

.onClick(() => {

if (!this.isRecording) {

this.currentPreset = key;

}

})

})

}

// 控制按钮

Row({ space: 20 }) {

if (!this.isRecording) {

// 开始录音按钮

Button('🎙 开始')

.width(80)

.height(80)

.borderRadius(40)

.backgroundColor('#F44336')

.onClick(() => this.startRecording())

} else {

// 暂停/恢复按钮

Button(this.isPaused ? '▶ 恢复' : '⏸ 暂停')

.width(80)

.height(80)

.borderRadius(40)

.backgroundColor(this.isPaused ? '#4CAF50' : '#FF9800')

.onClick(() => {

if (this.isPaused) {

this.resumeRecording();

} else {

this.pauseRecording();

}

})

// 停止按钮

Button('⏹ 停止')

.width(80)

.height(80)

.borderRadius(40)

.backgroundColor('#666')

.onClick(() => this.stopRecording())

}

}

// 录音文件列表

if (this.recordList.length > 0) {

Text('录音文件')

.fontSize(16)

.fontWeight(FontWeight.Medium)

.width('100%')

.padding({ left: 16 })

List({ space: 4 }) {

ForEach(this.recordList, (filePath: string, index: number) => {

ListItem() {

Row({ space: 12 }) {

Text('🎵')

.fontSize(20)

Text(this.getFileName(filePath))

.fontSize(14)

.layoutWeight(1)

.maxLines(1)

.textOverflow({ overflow: TextOverflow.Ellipsis })

}

.width('100%')

.padding(12)

.borderRadius(8)

.backgroundColor('#252540')

}

}, (filePath: string, index: number) => `${index}`)

}

.width('90%')

.height(200)

}

}

.width('100%')

.height('100%')

.justifyContent(FlexAlign.Center)

.padding(20)

.backgroundColor('#1a1a2e')

}

}

3.3 录音文件管理器

录音文件会越来越多,需要一套完整的管理方案——自动命名、文件信息读取、清理过期文件:

import { fileIo as fs } from '@kit.CoreFileKit';

import { common } from '@kit.AbilityKit';

import { BusinessError } from '@kit.BasicServicesKit';

// 录音文件信息模型

interface RecordFileInfo {

fileName: string;

filePath: string;

fileSize: number; // 字节

createTime: number; // 时间戳

duration: number; // 时长(毫秒),可能无法获取

}

@Entry

@Component

struct RecordFileManager {

@State recordFiles: RecordFileInfo[] = [];

@State totalSize: string = '0 B';

@State isRefreshing: boolean = false;

aboutToAppear(): void {

this.loadRecordFiles();

}

/**

* 加载录音目录下所有文件

*/

async loadRecordFiles(): Promise {

this.isRefreshing = true;

const context = getContext(this) as common.Context;

const recordDir = context.filesDir;

try {

// 列出目录下所有文件

const files = fs.listFileSync(recordDir);

const recordFileList: RecordFileInfo[] = [];

for (const fileName of files) {

// 只处理录音文件

if (!fileName.startsWith('recording_')) continue;

const filePath = `${recordDir}/${fileName}`;

try {

const stat = fs.statSync(filePath);

recordFileList.push({

fileName: fileName,

filePath: filePath,

fileSize: stat.size,

createTime: stat.mtime?.valueOf() || 0,

duration: 0 // 时长需要通过 AVPlayer 获取,此处省略

});

} catch (err) {

console.warn(`[文件管理] 无法读取: ${fileName}`);

}

}

// 按创建时间降序排列

recordFileList.sort((a, b) => b.createTime - a.createTime);

this.recordFiles = recordFileList;

// 计算总大小

const totalBytes = recordFileList.reduce((sum, f) => sum + f.fileSize, 0);

this.totalSize = this.formatFileSize(totalBytes);

} catch (err) {

const error = err as BusinessError;

console.error(`[文件管理] 加载失败: ${error.message}`);

} finally {

this.isRefreshing = false;

}

}

/**

* 删除指定录音文件

*/

async deleteFile(filePath: string): Promise {

try {

fs.unlinkSync(filePath);

console.info(`[文件管理] 已删除: ${filePath}`);

// 刷新列表

await this.loadRecordFiles();

} catch (err) {

const error = err as BusinessError;

console.error(`[文件管理] 删除失败: ${error.message}`);

}

}

/**

* 清理超过指定天数的录音文件

*/

async cleanOldFiles(daysToKeep: number = 30): Promise {

const now = Date.now();

const threshold = now - daysToKeep * 24 * 60 * 60 * 1000;

let deletedCount = 0;

for (const file of this.recordFiles) {

if (file.createTime < threshold) {

try {

fs.unlinkSync(file.filePath);

deletedCount++;

} catch (err) {

console.warn(`[文件管理] 清理失败: ${file.fileName}`);

}

}

}

console.info(`[文件管理] 清理完成,删除 ${deletedCount} 个文件`);

await this.loadRecordFiles();

return deletedCount;

}

/**

* 清理所有录音文件

*/

async cleanAllFiles(): Promise {

for (const file of this.recordFiles) {

try {

fs.unlinkSync(file.filePath);

} catch (err) {

console.warn(`[文件管理] 清理失败: ${file.fileName}`);

}

}

await this.loadRecordFiles();

}

/**

* 格式化文件大小

*/

formatFileSize(bytes: number): string {

if (bytes < 1024) return `${bytes} B`;

if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;

if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;

return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;

}

/**

* 格式化时间

*/

formatTime(timestamp: number): string {

if (timestamp === 0) return '未知';

const date = new Date(timestamp);

return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}`;

}

build() {

Column({ space: 16 }) {

// 标题栏

Row() {

Text('📁 录音文件管理')

.fontSize(22)

.fontWeight(FontWeight.Bold)

.layoutWeight(1)

Text(`${this.recordFiles.length} 个文件 | ${this.totalSize}`)

.fontSize(12)

.fontColor('#888')

}

.width('100%')

.padding({ left: 16, right: 16 })

// 操作按钮

Row({ space: 12 }) {

Button('刷新')

.fontSize(12)

.height(32)

.onClick(() => this.loadRecordFiles())

Button('清理30天前')

.fontSize(12)

.height(32)

.backgroundColor('#FF9800')

.onClick(() => this.cleanOldFiles(30))

Button('全部清理')

.fontSize(12)

.height(32)

.backgroundColor('#F44336')

.onClick(() => this.cleanAllFiles())

}

.width('100%')

.padding({ left: 16, right: 16 })

// 文件列表

if (this.recordFiles.length === 0) {

Column() {

Text('📭')

.fontSize(48)

Text('暂无录音文件')

.fontSize(14)

.fontColor('#888')

.margin({ top: 8 })

}

.layoutWeight(1)

.justifyContent(FlexAlign.Center)

} else {

List({ space: 8 }) {

ForEach(this.recordFiles, (file: RecordFileInfo, index: number) => {

ListItem() {

Row({ space: 12 }) {

// 文件图标

Text('🎵')

.fontSize(28)

// 文件信息

Column({ space: 4 }) {

Text(file.fileName)

.fontSize(14)

.fontWeight(FontWeight.Medium)

.maxLines(1)

.textOverflow({ overflow: TextOverflow.Ellipsis })

Row({ space: 12 }) {

Text(this.formatFileSize(file.fileSize))

.fontSize(12)

.fontColor('#888')

Text(this.formatTime(file.createTime))

.fontSize(12)

.fontColor('#888')

}

}

.alignItems(HorizontalAlign.Start)

.layoutWeight(1)

// 删除按钮

Text('🗑️')

.fontSize(20)

.onClick(() => this.deleteFile(file.filePath))

}

.width('100%')

.padding(12)

.borderRadius(8)

.backgroundColor('#252540')

}

.swipeAction({ end: this.deleteSwipeAction(file) })

}, (file: RecordFileInfo) => file.fileName)

}

.width('100%')

.layoutWeight(1)

.padding({ left: 12, right: 12 })

}

}

.width('100%')

.height('100%')

.backgroundColor('#1a1a2e')

}

/**

* 滑动删除组件

*/

@Builder

deleteSwipeAction(file: RecordFileInfo) {

Button('删除')

.backgroundColor('#F44336')

.height('100%')

.onClick(() => this.deleteFile(file.filePath))

}

}

四、踩坑与注意事项

4.1 权限被动态收回

问题:用户在录音过程中去系统设置关闭了麦克风权限,导致录音失败但应用没有感知。

解决方案:监听权限变化,在录音前再次检查权限。

// 在 Ability 的 onPageShow 中检查权限

onPageShow(): void {

this.checkPermission();

}

// 或使用定时检查(录音过程中)

startPermissionMonitor(): void {

this.permissionTimer = setInterval(async () => {

if (this.isRecording) {

const granted = await this.checkPermission();

if (!granted) {

// 权限被收回,立即停止录音

await this.stopRecording();

promptAction.showToast({ message: '麦克风权限已被收回,录音已停止' });

}

}

}, 3000);

}

4.2 fd 文件描述符泄漏

问题:使用 fd:// 方式录音时,AVRecorder 内部持有 fd,如果异常退出没有释放,fd 就泄漏了。

解决方案:在 release() 之前确保 fd 被正确关闭。

// ❌ 错误写法:fd 未关闭

const file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);

await this.avRecorder.prepare({ url: `fd://${file.fd}`, ... });

// 如果这里异常了,file.fd 就泄漏了

// ✅ 正确写法:记录 fd,在释放时关闭

private recordFd: number = -1;

async startRecording(): Promise {

const file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);

this.recordFd = file.fd;

// ...

}

async releaseRecorder(): Promise {

if (this.avRecorder) {

await this.avRecorder.release();

}

if (this.recordFd !== -1) {

fs.closeSync(this.recordFd);

this.recordFd = -1;

}

}

4.3 录音参数不兼容

问题:某些编码格式和容器格式不兼容,导致 prepare() 失败。

常见不兼容组合:

AAC 编码 + AMR 容器 → ❌ 不兼容

AMR_NB 编码 + MPEG_4 容器 → ❌ 不兼容

AAC 编码 + MPEG_4 容器 → ✅ 兼容

AMR_WB 编码 + THREE_GPP 容器 → ✅ 兼容

解决方案:使用上面"常见配置组合"表格中的推荐搭配,不要随意混搭。

4.4 存储空间不足

问题:录音过程中磁盘空间满了,导致写入失败。

解决方案:录音前检查可用空间,录音中监控文件大小。

import { statvfs } from '@kit.CoreFileKit';

async checkAvailableSpace(): Promise {

const context = getContext(this) as common.Context;

const stat = await statvfs.statfs(context.filesDir);

const freeSize = stat.bfree * stat.bsize; // 可用字节数

const minRequired = 10 * 1024 * 1024; // 至少需要 10MB

if (freeSize < minRequired) {

promptAction.showToast({ message: '存储空间不足,请清理后重试' });

return false;

}

return true;

}

4.5 来电中断录音

问题:录音过程中来电,系统可能强制停止录音。

解决方案:监听来电事件,主动暂停录音;通话结束后恢复。

import { call } from '@kit.TelephonyKit';

// 监听通话状态

call.on('callStateChange', (data) => {

if (data.state === call.CallState.CALL_STATE_OFFHOOK) {

// 来电接通,暂停录音

if (this.isRecording && !this.isPaused) {

this.pauseRecording();

}

} else if (data.state === call.CallState.CALL_STATE_IDLE) {

// 通话结束,恢复录音

if (this.isRecording && this.isPaused) {

this.resumeRecording();

}

}

});

五、HarmonyOS 6 适配

5.1 API 变更

变更项

HarmonyOS 5

HarmonyOS 6

AVRecorder 创建

media.createAVRecorder()

保持一致,新增配置选项

音频源类型

AUDIO_SOURCE_TYPE_MIC

新增 AUDIO_SOURCE_TYPE_VOICE_RECOGNITION(语音识别优化)

编码格式

AAC / AMR

新增 OPUS 编码支持

文件格式

MPEG_4 / AMR / THREE_GPP

新增 WEBM 容器格式

降噪处理

无内置支持

新增 noiseSuppression 配置项

5.2 迁移指南

// HarmonyOS 5 写法

const config: media.AVRecorderConfig = {

audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC,

profile: {

audioBitrate: 64000,

audioChannels: 1,

audioCodec: media.CodecMimeType.AUDIO_AAC,

audioSampleRate: 44100,

fileFormat: media.ContainerFormatType.CFT_MPEG_4

},

url: `fd://${fd}`

};

// HarmonyOS 6 写法(新增降噪和自动增益)

const config: media.AVRecorderConfig = {

audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_VOICE_RECOGNITION,

profile: {

audioBitrate: 64000,

audioChannels: 1,

audioCodec: media.CodecMimeType.AUDIO_AAC,

audioSampleRate: 44100,

fileFormat: media.ContainerFormatType.CFT_MPEG_4

},

url: `fd://${fd}`,

// HarmonyOS 6 新增

noiseSuppression: true, // 降噪

autoGainControl: true // 自动增益

};

5.3 新特性

OPUS 编码:低延迟、高质量,适合实时通信场景

WEBM 容器:与 Web 平台更好的兼容性

语音识别优化源:专门为语音识别场景优化的音频采集,自动降噪和增益

录音元数据:支持在录音文件中写入元数据(标题、作者等)

六、总结

mindmap

root((音频录制))

权限管理

麦克风权限

checkAccessToken

requestPermissionsFromUser

引导去系统设置

权限监听

动态收回检测

录音前二次检查

AVRecorder

状态机

idle → prepared → recording

recording ↔ paused

recording/paused → stopped

任何状态 → released

核心API

createAVRecorder

prepare(config)

start / pause / resume / stop

release

配置参数

audioSourceType

audioCodec / fileFormat

sampleRate / bitrate / channels

录音配置

高质量录音

AAC + MPEG_4

48kHz / 128kbps / 立体声

语音备忘

AAC + MPEG_4

44.1kHz / 64kbps / 单声道

电话录音

AMR_WB + 3GP

16kHz / 23.85kbps / 单声道

文件管理

自动命名

recording_时间戳.ext

文件信息

大小 / 创建时间

清理策略

按天数清理

空间不足检测

异常处理

来电中断

暂停/恢复

fd泄漏

release时关闭fd

参数不兼容

使用推荐配置组合

核心要点回顾:

权限先行:麦克风是敏感权限,必须在录音前检查和申请,还要处理用户拒绝和动态收回的场景

状态机驱动:AVRecorder 和 AVPlayer 一样是状态驱动的,prepare → start → pause/stop → release 是标准流程

配置要匹配:编码格式和容器格式必须兼容,推荐使用表格中的标准组合

文件管理:自动命名、及时清理、fd 泄漏防护,是生产级录音功能的基本素养

异常兜底:来电中断、权限收回、存储不足,这些边界情况都要有预案

黄金推荐

大火力 (Barrage) - [JNE]Jaden's Nether Expansion - MC百科
beat365手机客户端下载

大火力 (Barrage) - [JNE]Jaden's Nether Expansion - MC百科

✨ 08-24 💎 价值: 4424
怎么看鼠标驱动 Win10鼠标驱动安装路径在哪里
久发365电子游戏网址多少

怎么看鼠标驱动 Win10鼠标驱动安装路径在哪里

✨ 10-06 💎 价值: 8088
低息贷款攻略:贷款哪个银行利息最低好通过的?2025终极指南
beat365手机客户端下载

低息贷款攻略:贷款哪个银行利息最低好通过的?2025终极指南

✨ 10-17 💎 价值: 9229