如果你对声音训练感兴趣,可以去鸿蒙应用市场搜一下**「声研坊」**,下载下来体验体验。录制自己的声音,回放对比,分析发声技巧。体验完了再回来看这篇文章,你会更清楚录音功能是怎么实现的。
写在前面
大家好,我是一名写了十多年Web前端的老兵。从jQuery时代一路走到React/Vue,Web端的录音API(MediaRecorder)功能有限。去年开始转战鸿蒙生态,用ArkTS开发App,发现HarmonyOS的音频录制API功能更强大。
比如:
- 录音控制:Web端用
MediaRecorder.start()/stop();鸿蒙里用AudioCapturer更精细的控制。 - 音频格式:Web端格式支持有限;鸿蒙里可以指定采样率、位深、声道数。
- 文件保存:Web端用Blob生成URL;鸿蒙里直接写入文件系统。
别担心,接下来这篇文章,我会用"声研坊"的录音功能,带你看看HarmonyOS怎么实现音频录制。
这篇文章聊什么
声研坊的录音功能,核心要解决:
- 录音控制:开始、暂停、停止录音
- 音频配置:采样率、格式等参数设置
- 文件保存:录音保存到应用目录
- 录音管理:查看和播放录音文件
第一步:设计录音数据结构
// Web前端同学看这里:Web端用Blob存储录音数据
// 鸿蒙里用文件路径引用录音文件
// 录音记录
interface Recording {
id: string;
title: string;
filePath: string;
duration: number; // 时长(秒)
fileSize: number; // 文件大小(字节)
sampleRate: number; // 采样率
channels: number; // 声道数
createdAt: string;
}
// 录音配置
interface AudioConfig {
sampleRate: number; // 采样率: 8000, 16000, 44100, 48000
channels: number; // 声道数: 1(单声道), 2(立体声)
bitDepth: number; // 位深: 8, 16, 24
format: string; // 格式: 'wav', 'aac', 'mp3'
}
// 预设配置
const AUDIO_PRESETS: AudioConfig[] = [
{ sampleRate: 16000, channels: 1, bitDepth: 16, format: 'wav' },
{ sampleRate: 44100, channels: 1, bitDepth: 16, format: 'wav' },
{ sampleRate: 44100, channels: 2, bitDepth: 16, format: 'wav' },
{ sampleRate: 48000, channels: 2, bitDepth: 24, format: 'wav' },
];
第二步:实现录音功能
// Web前端同学看这里:Web端用navigator.mediaDevices.getUserMedia()获取音频流
// 鸿蒙里用audio.AudioCapturer创建录音器
import { audio } from '@kit.AudioKit';
import { fileIo } from '@kit.CoreFileKit';
let audioCapturer: audio.AudioCapturer | null = null;
let isRecording: boolean = false;
let recordedFile: string = '';
// 创建录音器
async function createAudioCapturer(config: AudioConfig): Promise<void> {
const capturerOptions: audio.AudioCapturerOptions = {
streamInfo: {
samplingRate: config.sampleRate as audio.AudioSamplingRate,
channels: config.channels as audio.AudioChannel,
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
},
capturerInfo: {
source: audio.SourceType.SOURCE_TYPE_MIC,
capturerFlags: 0
}
};
audioCapturer = await audio.createAudioCapturer(capturerOptions);
}
// 开始录音
async function startRecording(context: Context, config: AudioConfig): Promise<boolean> {
try {
await createAudioCapturer(config);
// 准备输出文件
const fileName = `recording_${Date.now()}.wav`;
recordedFile = `${context.filesDir}/${fileName}`;
await audioCapturer!.start();
isRecording = true;
// 读取音频数据并写入文件
const bufferSize = 4096;
const file = fileIo.openSync(recordedFile, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY);
// 注意:实际实现需要持续读取音频数据
// 这里简化处理,实际应使用循环读取
audioCapturer!.read(bufferSize, true).then((buffer: ArrayBuffer) => {
fileIo.writeSync(file.fd, buffer);
});
return true;
} catch (err) {
console.error(`开始录音失败: ${err}`);
return false;
}
}
// 停止录音
async function stopRecording(): Promise<string | null> {
try {
if (audioCapturer) {
await audioCapturer.stop();
await audioCapturer.release();
audioCapturer = null;
}
isRecording = false;
return recordedFile;
} catch (err) {
console.error(`停止录音失败: ${err}`);
return null;
}
}
// 释放资源
function releaseCapturer(): void {
if (audioCapturer) {
audioCapturer.release();
audioCapturer = null;
}
}
第三步:实现录音页面
// Web前端同学看这里:React里我们用state管理录音状态
// 鸿蒙里用@State装饰器,结合定时器更新录音时长
@Entry
@Component
struct RecordingPage {
@State isRecording: boolean = false
@State isPaused: boolean = false
@State duration: number = 0
@State selectedPreset: number = 0
@State recordings: Recording[] = []
private timer: number = 0
async aboutToAppear() {
this.recordings = await getRecordings(getContext(this) as Context);
}
async startRecording() {
const config = AUDIO_PRESETS[this.selectedPreset];
const success = await startRecording(getContext(this) as Context, config);
if (success) {
this.isRecording = true;
this.isPaused = false;
this.duration = 0;
// 开始计时
this.timer = setInterval(() => {
if (!this.isPaused) {
this.duration++;
}
}, 1000);
}
}
async stopRecording() {
const filePath = await stopRecording();
if (filePath) {
clearInterval(this.timer);
this.isRecording = false;
// 保存录音记录
const recording: Recording = {
id: `rec_${Date.now()}`,
title: `录音 ${this.recordings.length + 1}`,
filePath,
duration: this.duration,
fileSize: 0, // 实际应读取文件大小
sampleRate: AUDIO_PRESETS[this.selectedPreset].sampleRate,
channels: AUDIO_PRESETS[this.selectedPreset].channels,
createdAt: new Date().toISOString().slice(0, 10)
};
await saveRecording(getContext(this) as Context, recording);
this.recordings = await getRecordings(getContext(this) as Context);
}
}
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')}`;
}
build() {
Column() {
Text('录音')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
// 录音配置选择
Text('录音质量')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.margin({ bottom: 8 })
Row() {
ForEach(AUDIO_PRESETS, (preset: AudioConfig, index: number) => {
Button(`${preset.sampleRate / 1000}kHz`)
.fontSize(12)
.height(32)
.backgroundColor(this.selectedPreset === index ? '#3b82f6' : '#f3f4f6')
.fontColor(this.selectedPreset === index ? '#ffffff' : '#374151')
.borderRadius(16)
.margin({ right: 8 })
.onClick(() => { this.selectedPreset = index })
})
}
.margin({ bottom: 20 })
// 录音时长显示
Text(this.formatDuration(this.duration))
.fontSize(48)
.fontWeight(FontWeight.Light)
.fontColor(this.isRecording ? '#ef4444' : '#374151')
.margin({ bottom: 20 })
// 录音按钮
Button(this.isRecording ? '停止' : '录音')
.width(100)
.height(100)
.backgroundColor(this.isRecording ? '#ef4444' : '#3b82f6')
.borderRadius(50)
.fontSize(18)
.onClick(() => {
if (this.isRecording) {
this.stopRecording();
} else {
this.startRecording();
}
})
// 录音列表
Text('录音记录')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ top: 30, bottom: 12 })
ForEach(this.recordings, (recording: Recording) => {
Row() {
Column() {
Text(recording.title)
.fontSize(14)
.fontWeight(FontWeight.Medium)
Text(`${this.formatDuration(recording.duration)} · ${recording.createdAt}`)
.fontSize(12)
.fontColor('#6b7280')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Button('播放')
.fontSize(12)
.height(32)
.backgroundColor('#22c55e')
.borderRadius(8)
}
.width('100%')
.padding(12)
.backgroundColor('#ffffff')
.borderRadius(12)
.margin({ bottom: 8 })
})
}
.padding(16)
}
}
第四步:常见问题
4.1 录音权限
问题:调用录音API报错。
解决:在module.json5里声明麦克风权限ohos.permission.MICROPHONE。
4.2 录音文件格式
问题:不同设备支持的音频格式不同。
解决:使用WAV格式,兼容性最好,或者查询设备支持的格式。
总结
这篇文章围绕"声研坊"的录音功能,讲解了:
音频录制
- AudioCapturer的创建和配置
- 录音控制(开始、停止)
- 音频参数设置
文件管理
- 录音文件保存
- 录音记录存储
- 录音列表展示
如果你对"声研坊"感兴趣,欢迎去鸿蒙应用市场搜索下载体验。