HarmonyOS 7 音频与媒体控制实战 03:给音频加入实时效果处理链

做了录音功能之后,产品说能不能给录音加个音效?比如录完音之后加个混响、降噪、变声,用户可以选不同的效果。这就涉及到实时音频处理了——录到的声音实时经过效果处理节点,再播放出来或者存成文件。
HarmonyOS 7 的 OHAudioSuite 提供了一套音频效果处理框架,可以把多个效果节点串成一条效果链,实时处理音频数据。这篇就讲讲怎么用它做一个实时音效处理的 Demo。
一、真实开发中遇到的问题
最开始的想法很简单:录完音,离线处理一遍,加个效果再存。但产品要的是实时效果——用户对着麦克风说话,立刻就能从耳机里听到加了混响的声音,有延时感就不行。
问题来了:
第一,实时处理延迟要低。从麦克风采集到处理完播放,整个链路延迟不能超过 50ms,不然用户说话和听到的声音对不上,体验很差。
第二,效果节点怎么组合?降噪、混响、增益,这几个效果是串起来还是并联?顺序有没有影响?
第三,参数怎么调?混响的房间大小、增益的音量值,用户要能实时调。调的时候会不会卡?
第四,性能问题。实时音频处理很吃 CPU,手机发热怎么办?效果开多了会不会掉帧?
二、这个能力怎么接入
OHAudioSuite 的核心概念是效果链(Effect Chain):把多个效果节点(Effect Node)按顺序串起来,音频数据从一端流进去,经过每个节点处理后从另一端流出来。
接入思路:
1. 创建音频图(AudioGraph):这是整个处理链路的容器。
2. 添加输入节点:麦克风采集节点(AudioCapturer)作为输入源。
3. 添加效果节点:降噪、混响、均衡器这些效果节点,按顺序加到链上。
4. 添加输出节点:扬声器播放节点(AudioRenderer)作为输出。
5. 连接节点:把输入→效果1→效果2→...→输出,按顺序连起来。
6. 启动音频图:start() 之后,音频就开始实时流动了。

三、关键代码怎么写
下面是封装的实时音效处理链,文件位置在 entry/src/main/ets/utils/AudioEffectChain.ets:
import { audio } from '@kit.AudioKit';
export class AudioEffectChain {
private audioGraph: audio.AudioGraph | null = null;
private inputNode: audio.AudioCapturer | null = null;
private outputNode: audio.AudioRenderer | null = null;
private reverbNode: audio.Reverb | null = null;
private volumeNode: audio.Volume | null = null;
// 初始化效果链
async initEffectChain(): Promise<void> {
// 1. 创建音频图
this.audioGraph = await audio.createAudioGraph({
latencyMode: audio.LatencyMode.LATENCY_MODE_NORMAL,
strategy: audio.AudioGraphStrategy.STRATEGY_MIN_LATENCY
});
// 2. 添加输入节点(麦克风)
this.inputNode = await this.audioGraph.addNode({
type: audio.AudioNodeType.CAPTURER,
source: audio.SourceType.SOURCE_TYPE_MIC,
samplingRate: 48000,
channels: 1,
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_F32LE
}) as audio.AudioCapturer;
// 3. 添加混响效果节点
this.reverbNode = await this.audioGraph.addNode({
type: audio.AudioNodeType.REVERB,
samplingRate: 48000,
channels: 1
}) as audio.Reverb;
// 4. 添加音量调节节点
this.volumeNode = await this.audioGraph.addNode({
type: audio.AudioNodeType.VOLUME,
samplingRate: 48000,
channels: 1
}) as audio.Volume;
// 5. 添加输出节点(扬声器)
this.outputNode = await this.audioGraph.addNode({
type: audio.AudioNodeType.RENDERER,
samplingRate: 48000,
channels: 1,
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_F32LE
}) as audio.AudioRenderer;
// 6. 连接节点:输入 → 混响 → 音量 → 输出
this.inputNode.connect(this.reverbNode);
this.reverbNode.connect(this.volumeNode);
this.volumeNode.connect(this.outputNode);
}
// 启动效果链
async start(): Promise<void> {
if (this.audioGraph) {
await this.audioGraph.start();
}
}
// 停止效果链
async stop(): Promise<void> {
if (this.audioGraph) {
await this.audioGraph.stop();
}
}
// 调节混响参数
async setReverbParam(roomSize: number, damping: number): Promise<void> {
if (this.reverbNode) {
await this.reverbNode.setParameter({
roomSize: roomSize, // 房间大小 0~1
damping: damping, // 阻尼 0~1
wetLevel: 0.5 // 混响强度
});
}
}
// 调节音量
async setVolume(value: number): Promise<void> {
if (this.volumeNode) {
await this.volumeNode.setParameter({
volume: value // 0~1
});
}
}
// 释放资源
async release(): Promise<void> {
if (this.audioGraph) {
await this.audioGraph.release();
this.audioGraph = null;
}
}
}
这段代码的核心:createAudioGraph 创建处理容器,addNode 添加各个节点,connect 按顺序连接。音频流从麦克风输入,经过混响、音量调节,最后从扬声器输出。整个过程是实时的。
页面里怎么用?文件位置 pages/EffectChainPage.ets:
@Entry
@Component
struct EffectChainPage {
@State isRunning: boolean = false;
@State roomSize: number = 0.5;
@State volume: number = 0.8;
private effectChain: AudioEffectChain = new AudioEffectChain();
async onToggle() {
if (this.isRunning) {
await this.effectChain.stop();
this.isRunning = false;
} else {
await this.effectChain.initEffectChain();
await this.effectChain.start();
this.isRunning = true;
}
}
async onRoomSizeChange(value: number) {
this.roomSize = value;
await this.effectChain.setReverbParam(value, 0.5);
}
async onVolumeChange(value: number) {
this.volume = value;
await this.effectChain.setVolume(value);
}
build() {
Column({ space: 24 }) {
Text('实时音效处理')
.fontSize(24)
.fontWeight(FontWeight.Bold)
Button(this.isRunning ? '停止' : '开始')
.onClick(() => this.onToggle())
// 混响调节
Column() {
Text('混响大小: ' + this.roomSize.toFixed(2))
Slider({
value: this.roomSize,
min: 0,
max: 1,
step: 0.01
})
.onChange((value) => this.onRoomSizeChange(value))
}
.width('80%')
// 音量调节
Column() {
Text('音量: ' + this.volume.toFixed(2))
Slider({
value: this.volume,
min: 0,
max: 1,
step: 0.01
})
.onChange((value) => this.onVolumeChange(value))
}
.width('80%')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}

四、运行过程中怎么处理异常
实时效果链的异常场景:
1. 音频图创建失败:设备不支持低延迟模式,或者音频资源被占用。catch 住异常,降级用普通延迟模式。
2. 节点连接失败:采样率或格式不匹配,连不上。所有节点要用相同的采样率和格式,不然连接报错。
3. 运行时 CPU 占用高:开太多效果节点,CPU 打满,音频就会卡。要监控 CPU 占用,超过阈值就提示用户关掉一些效果。
4. 资源泄漏:页面退出了忘了 release,麦克风一直被占用。aboutToDisappear 里一定要释放。
五、实际开发中容易忽略的问题
1. 采样格式要统一:所有节点的采样率、声道数、采样格式必须一致,不然连不上。我们统一用 48kHz 单声道 float32。
2. 低延迟模式:要选 LATENCY_MODE_NORMAL 或者更低的模式,延迟才够低。普通模式延迟高,实时处理就没意义了。
3. 效果顺序有讲究:降噪要放最前面,混响放中间,音量放最后。顺序不对,效果就不对。比如先加混响再降噪,会把混响的尾巴当噪音消掉。
4. 参数调节不要太频繁:用户拖滑块的时候,每调一次就 setParameter,太频繁了会卡顿。做个节流,100ms 调一次就行。
5. 耳机监听:实时效果一定要让用户戴耳机听,不然扬声器的声音又被麦克风收进去,形成回声反馈,效果会炸。
实时音频效果链看起来很酷,但工程上的坑不少——延迟控制、性能优化、资源管理,每一步都要抠。下一篇讲 AVSession,把播放状态暴露给系统,让锁屏和控制中心能控制播放。
更多推荐



所有评论(0)