前言

前几篇给智能生活助手加了 Agent 协作、大模型对话、视觉识别,但语音交互这块一直是个遗憾——录出来的声音干巴巴的,播放也没什么空间感。HarmonyOS 7 的空间音频引擎就是来解决这个问题的。

空间音频引擎的架构

HarmonyOS 7 的音频引擎不再是简单的"录音→播放",而是引入了**音频节点图(Audio Graph)**的概念。你可以把它理解成一个音频处理流水线,由不同的处理节点(Node)组合而成。

系统预置了几类核心节点:

  • AudioCapturerNode:音频采集,就是麦克风输入
  • DenoiseNode:降噪,支持组合降噪策略
  • VoiceChangeNode:变声,可以调整音色、音调
  • SpatialRenderNode:空间渲染,模拟 3D 空间声场
  • AudioRendererNode:最终输出到扬声器或耳机

这些节点可以自由组合。比如你想做一个"录音→降噪→变声→空间渲染→播放"的链路,就把这五个节点按顺序串起来。

录音:先把声音采进来

智能生活助手的语音交互第一步就是录音。用 AudioCapturer 配合新的节点图 API:

import { audioEngine, audioSession } from '@kit.MediaKit';

class VoiceRecorder {
  private graph: audioEngine.AudioGraph | null = null;
  private capturerNode: audioEngine.AudioCapturerNode | null = null;
  private denoiseNode: audioEngine.DenoiseNode | null = null;
  private bufferChunks: ArrayBuffer[] = [];

  async startRecording(): Promise<void> {
    // 创建音频处理图
    this.graph = audioEngine.createAudioGraph({
      sampleRate: 16000,    // 16kHz 适合语音识别
      channelCount: 1,      // 单声道录音
      sampleFormat: audioEngine.SampleFormat.SAMPLE_FORMAT_F32LE
    });

    // 采集节点
    this.capturerNode = this.graph.createNode(
      audioEngine.NodeType.AUDIO_CAPTURER, {
        audioSource: audioEngine.AudioSource.MIC,
        // 开启系统级 AEC 回声消除,播放 TTS 时不会录到回声
        enableAEC: true
      }
    ) as audioEngine.AudioCapturerNode;

    // 降噪节点
    this.denoiseNode = this.graph.createNode(
      audioEngine.NodeType.DENOISE, {
        // 组合降噪:环境降噪 + 人声增强
        modes: [
          audioEngine.DenoiseMode.ENVIRONMENTAL,
          audioEngine.DenoiseMode.VOICE_ENHANCE
        ],
        // 降噪强度,0-1,太高会把人声也削掉
        intensity: 0.7
      }
    ) as audioEngine.DenoiseNode;

    // 连接节点:麦克风 → 降噪 → 输出回调
    this.graph.connect(this.capturerNode, this.denoiseNode);
    
    // 设置输出回调,拿到处理后的音频数据
    this.denoiseNode.onData((buffer: ArrayBuffer) => {
      this.bufferChunks.push(buffer);
    });

    // 启动
    await this.graph.start();
  }

  async stopRecording(): Promise<ArrayBuffer> {
    await this.graph?.stop();
    
    // 合并所有音频片段
    const totalLength = this.bufferChunks.reduce(
      (sum, chunk) => sum + chunk.byteLength, 0
    );
    const merged = new ArrayBuffer(totalLength);
    const view = new Uint8Array(merged);
    let offset = 0;
    for (const chunk of this.bufferChunks) {
      view.set(new Uint8Array(chunk), offset);
      offset += chunk.byteLength;
    }
    
    // 清理
    this.bufferChunks = [];
    await this.graph?.release();
    this.graph = null;
    
    return merged;
  }
}

A clean Notion-style code snippet visualization sh

enableAEC 这个参数很关键。如果你的助手在播报 TTS 的同时用户说话了(打断场景),不开 AEC 的话麦克风会把 TTS 的声音也录进去,形成回声。开了之后系统会自动消除扬声器输出的部分。

变声:给助手一个有辨识度的声音

录音处理完,接下来是 TTS 输出。智能生活助手不能老用同一个机器人声音吧?空间音频引擎的变声节点可以实现音色变换:

A Notion-style comparison table or split-view grap

class VoiceOutput {
  private graph: audioEngine.AudioGraph | null = null;

  async speakWithPersona(text: string): Promise<void> {
    // 先把文字转语音(TTS),拿到 PCM 数据
    const ttsAudio = await this.textToSpeech(text);

    this.graph = audioEngine.createAudioGraph({
      sampleRate: 24000,
      channelCount: 2, // 立体声,后面空间渲染要用
      sampleFormat: audioEngine.SampleFormat.SAMPLE_FORMAT_F32LE
    });

    // 输入节点:TTS 的 PCM 数据
    const sourceNode = this.graph.createNode(
      audioEngine.NodeType.AUDIO_BUFFER_SOURCE, {
        buffer: ttsAudio.pcmData,
        loop: false
      }
    );

    // 变声节点:调整为温暖亲切的音色
    const voiceChangeNode = this.graph.createNode(
      audioEngine.NodeType.VOICE_CHANGE, {
        preset: audioEngine.VoicePreset.WARM_FEMALE,
        // 微调参数
        pitchShift: -0.1,   // 略低沉一点
        speedRate: 1.05,    // 稍微快一点,更自然
        formantShift: 0.15  // 共振偏移,增加辨识度
      }
    ) as audioEngine.VoiceChangeNode;

    // 空间渲染节点
    const spatialNode = this.graph.createNode(
      audioEngine.NodeType.SPATIAL_RENDER, {
        // 模拟声源位置:正前方 1 米
        sourcePosition: { x: 0, y: 0, z: -1 },
        // 房间类型,影响混响
        roomType: audioEngine.RoomType.LIVING_ROOM,
        // 双耳渲染(需要耳机)
        binauralEnabled: true
      }
    ) as audioEngine.SpatialRenderNode;

    // 输出节点
    const rendererNode = this.graph.createNode(
      audioEngine.NodeType.AUDIO_RENDERER, {
        // 根据是否戴耳机自动选择输出
        devicePreference: audioEngine.DevicePreference.AUTO
      }
    );

    // 连接:TTS → 变声 → 空间渲染 → 播放
    this.graph.connect(sourceNode, voiceChangeNode);
    this.graph.connect(voiceChangeNode, spatialNode);
    this.graph.connect(spatialNode, rendererNode);

    await this.graph.start();
    
    // 等播放完
    await new Promise<void>((resolve) => {
      sourceNode.onEnd(() => {
        resolve();
      });
    });
    
    await this.graph.release();
  }
}

VoicePreset.WARM_FEMALE 是系统预置的音色之一,还有 ENERGETIC_MALECHILDELDERLY_MALE 等。你也可以不用预置,直接用 pitchShiftformantShift 这些参数手动调。不过说实话,手调挺费劲的,预置音色质量已经很高了。

空间渲染:让声音有"位置感"

空间渲染是整个链路里最有意思的部分。SpatialRenderNode 可以模拟声源在 3D 空间中的位置,配合耳机的双耳渲染,用户会感觉助手的声音从特定方向传来。

在智能生活助手里,我给不同 Agent 配了不同的"声音位置":

// 不同 Agent 的声音位置配置
const agentVoiceConfig: Record<string, SpatialConfig> = {
  'weather': {
    sourcePosition: { x: -0.5, y: 0, z: -1 }, // 左前方
    voicePreset: audioEngine.VoicePreset.CALM_MALE
  },
  'calendar': {
    sourcePosition: { x: 0.5, y: 0, z: -1 },  // 右前方
    voicePreset: audioEngine.VoicePreset.PROFESSIONAL_FEMALE
  },
  'recipe': {
    sourcePosition: { x: 0, y: 0, z: -1.5 },  // 正前方远一点
    voicePreset: audioEngine.VoicePreset.WARM_FEMALE
  }
};

interface SpatialConfig {
  sourcePosition: { x: number; y: number; z: number };
  voicePreset: audioEngine.VoicePreset;
}

// 根据 Agent 来源配置空间音频
async function speakFromAgent(agentId: string, text: string): Promise<void> {
  const config = agentVoiceConfig[agentId] ?? {
    sourcePosition: { x: 0, y: 0, z: -1 },
    voicePreset: audioEngine.VoicePreset.WARM_FEMALE
  };

  const ttsAudio = await textToSpeech(text);
  const graph = audioEngine.createAudioGraph({
    sampleRate: 24000,
    channelCount: 2,
    sampleFormat: audioEngine.SampleFormat.SAMPLE_FORMAT_F32LE
  });

  const source = graph.createNode(audioEngine.NodeType.AUDIO_BUFFER_SOURCE, {
    buffer: ttsAudio.pcmData
  });

  const voice = graph.createNode(audioEngine.NodeType.VOICE_CHANGE, {
    preset: config.voicePreset
  });

  const spatial = graph.createNode(audioEngine.NodeType.SPATIAL_RENDER, {
    sourcePosition: config.sourcePosition,
    roomType: audioEngine.RoomType.LIVING_ROOM,
    binauralEnabled: true,
    // HRTF 质量,HIGH 计算量更大但定位更准
    hrtfQuality: audioEngine.HRTFQuality.HIGH
  });

  const renderer = graph.createNode(audioEngine.NodeType.AUDIO_RENDERER, {
    devicePreference: audioEngine.DevicePreference.AUTO
  });

  graph.connect(source, voice);
  graph.connect(voice, spatial);
  graph.connect(spatial, renderer);

  await graph.start();
  await new Promise<void>(resolve => source.onEnd(() => resolve()));
  await graph.release();
}

戴上耳机试试,天气助手的声音从左前方传来,日程助手从右前方,食谱助手从正前方。多 Agent 协作的时候,用户甚至能靠声音位置分辨是哪个 Agent 在说话,不用看屏幕。

完整的语音交互链路

把录音、处理、播放串起来,完整的语音交互是这样的:

async function handleVoiceInteraction(): Promise<void> {
  // 1. 录音
  const recorder = new VoiceRecorder();
  await recorder.startRecording();
  
  // 等待用户说完(可以用 VAD 检测语音结束,这里简化用 3 秒)
  await new Promise(resolve => setTimeout(resolve, 3000));
  const audioData = await recorder.stopRecording();
  
  // 2. 语音转文字(端侧 ASR)
  const transcript = await speechRecognizer.recognize(audioData);
  
  // 3. 大模型理解 + Agent 调度
  const response = await processUserInput(transcript);
  const sourceAgent = response.sourceAgentId;
  
  // 4. 文字转语音 + 空间音频播放
  await speakFromAgent(sourceAgent, response.text);
}

实际项目中,第 2 步的语音识别也建议用端侧模型。HarmonyOS 7 的端侧 ASR 模型支持流式识别,用户一边说一边出文字,体验比录完再识别好很多。

降噪的坑

组合降噪(ENVIRONMENTAL + VOICE_ENHANCE)效果不错,但有个坑:两个模式同时开的时候,intensity 不能超过 0.8。超过的话人声会被过度压缩,听起来像在水下说话。我一开始设了 0.95,结果录出来的声音闷得不行,调了半天才发现是这个问题。

建议的做法是先用 0.6 的默认值,然后根据用户反馈慢慢调。如果你的场景是厨房(背景噪音大),可以把环境降噪的权重调高,人声增强的权重调低。

小结

空间音频引擎给语音交互带来了一个新的维度。不只是声音更好听,而是通过空间定位让用户能"听出"是哪个 Agent 在说话,这种体验上的提升比单纯提高 TTS 质量更明显。

不过空间音频目前最依赖耳机。扬声器模式下双耳渲染基本失效,空间感大打折扣。所以建议在 UI 上做个提示,引导用户戴耳机使用语音交互功能。

下一篇是这个系列的收尾篇——ArkUI 沉浸光感组件。给我们的智能生活助手穿上最后一件"外衣",让视觉交互也能跟上空间化的设计理念。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐