音频实时效果链封面

做了录音功能之后,产品说能不能给录音加个音效?比如录完音之后加个混响、降噪、变声,用户可以选不同的效果。这就涉及到实时音频处理了——录到的声音实时经过效果处理节点,再播放出来或者存成文件。

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,把播放状态暴露给系统,让锁屏和控制中心能控制播放。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐