HarmonyOS音频流管理开发实战

举报
Jack20 发表于 2026/06/20 20:52:32 2026/06/20
【摘要】 HarmonyOS开发中的音频流管理:AudioRenderer、AudioCapturer、流类型与低延迟流实战核心要点:音频流管理是鸿蒙音频开发的中枢系统。本文从 AudioRenderer(播放流)和 AudioCapturer(录制流)两大核心类入手,深入讲解流类型与用途的映射关系、低延迟流的实现原理、流状态机的完整生命周期管理,以及生产环境下的最佳实践。 一、背景与动机想象一下这...

HarmonyOS开发中的音频流管理:AudioRenderer、AudioCapturer、流类型与低延迟流实战

核心要点:音频流管理是鸿蒙音频开发的中枢系统。本文从 AudioRenderer(播放流)和 AudioCapturer(录制流)两大核心类入手,深入讲解流类型与用途的映射关系、低延迟流的实现原理、流状态机的完整生命周期管理,以及生产环境下的最佳实践。


一、背景与动机

想象一下这个场景:你正在用手机听音乐,突然来了个电话,音乐自动暂停,通话结束后音乐又自动恢复。这个看似简单的功能,背后涉及的就是音频流管理

音频流管理要解决的核心问题是:当多个应用同时想要使用音频硬件时,谁优先?谁让步?谁被静音?

在鸿蒙系统中,每个音频流都有自己的"身份"——流类型(StreamUsage)和内容类型(ContentType)。系统根据这些信息来决定音频焦点(Audio Focus)的分配策略。如果你不理解流类型,你的 App 可能会在不该播放的时候播放,在该暂停的时候不暂停,用户体验一塌糊涂。

更关键的是,对于游戏、乐器、实时通信等场景,低延迟是刚需。普通音频流的延迟可能在 100ms 以上,而低延迟流可以做到 20ms 以内。这 80ms 的差距,对于打击乐手来说就是"能用"和"不能用"的天壤之别。


二、核心原理

2.1 音频流的两面:Renderer 与 Capturer

鸿蒙的音频流分为两个方向:

  • AudioRenderer:输出流,把 PCM 数据"渲染"到音频硬件(扬声器/耳机)
  • AudioCapturer:输入流,从音频硬件(麦克风)“采集” PCM 数据
    图片.png

2.2 流类型与用途

鸿蒙定义了丰富的流类型,每种类型对应不同的音频焦点策略:

StreamUsage ContentType 典型场景 焦点策略
STREAM_USAGE_MUSIC CONTENT_TYPE_MUSIC 音乐播放 可被通话打断,打断后恢复
STREAM_USAGE_VOICE_COMMUNICATION CONTENT_TYPE_SPEECH 语音通话 最高优先级,打断其他所有流
STREAM_USAGE_GAME CONTENT_TYPE_MUSIC 游戏音效 可被通话打断
STREAM_USAGE_NOTIFICATION CONTENT_TYPE_SONIFICATION 通知铃声 短暂播放,不长期占用焦点
STREAM_USAGE_ALARM CONTENT_TYPE_SONIFICATION 闹钟 高优先级,仅被通话打断
STREAM_USAGE_RINGTONE CONTENT_TYPE_RINGTONE 来电铃声 高优先级
STREAM_USAGE_VOICE_ASSISTANT CONTENT_TYPE_SPEECH 语音助手 中等优先级

2.3 流状态机

AudioRenderer 和 AudioCapturer 都遵循严格的状态机模型:
图片.png

重要规则

  • 状态转换必须按顺序进行,跳步会导致异常
  • release() 是终止状态,调用后流不可复用
  • stop() 后可以重新 prepare()start()

2.4 低延迟流原理

低延迟流的核心是减少缓冲区层级。普通音频流的数据路径:

App → 系统混音器 → 音频策略服务 → HALDSP → 扬声器

低延迟流的数据路径:

App → HALDSP → 扬声器

跳过了系统混音器和策略服务两个环节,延迟可以降低 60-80ms。但代价是:低延迟流不参与系统混音,同一时间只能有一个低延迟流活跃。


三、代码实战

3.1 基础:AudioRenderer 播放 PCM 音频

// 文件名:BasicAudioRenderer.ets
// 功能:使用 AudioRenderer 播放 PCM 音频数据

import { audio } from '@kit.AudioKit';
import { fileIo } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';

@Entry
@Component
struct BasicAudioRendererPage {
  @State isPlaying: boolean = false;
  @State playProgress: number = 0;
  @State audioInfo: string = '未加载音频';
  @State volumeLevel: number = 50;

  private audioRenderer: audio.AudioRenderer | null = null;
  private renderBuffer: ArrayBuffer | null = null;

  aboutToAppear() {
    this.initRenderer();
  }

  // 初始化音频渲染器
  async initRenderer() {
    try {
      // 1. 创建渲染器参数——定义流的"身份"
      const rendererInfo: audio.AudioRendererInfo = {
        usage: audio.StreamUsage.STREAM_USAGE_MUSIC,       // 音乐流
        rendererFlags: 0,                                   // 默认标志
      };

      // 2. 创建渲染器选项——定义音频参数
      const rendererOptions: audio.AudioRendererOptions = {
        streamInfo: {
          samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100,  // 44.1kHz
          channels: audio.AudioChannel.CHANNEL_2,                   // 立体声
          sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, // 16bit
          encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW,   // PCM 原始数据
        },
        rendererInfo: rendererInfo,
      };

      // 3. 创建渲染器实例
      this.audioRenderer = await audio.createAudioRenderer(rendererOptions);
      console.info('[Renderer] 渲染器创建成功');

      // 4. 注册状态变化回调
      this.audioRenderer.on('stateChange', (state: audio.AudioState) => {
        console.info(`[Renderer] 状态变化: ${state}`);
        switch (state) {
          case audio.AudioState.STATE_RUNNING:
            this.isPlaying = true;
            break;
          case audio.AudioState.STATE_PAUSED:
          case audio.AudioState.STATE_STOPPED:
            this.isPlaying = false;
            break;
        }
      });

      // 5. 注册音频中断回调——处理焦点抢占
      this.audioRenderer.on('audioInterrupt', (interruptEvent: audio.InterruptEvent) => {
        console.info(`[Renderer] 音频中断事件: ${JSON.stringify(interruptEvent)}`);
        this.handleAudioInterrupt(interruptEvent);
      });

      // 更新音频信息
      this.audioInfo = '44100Hz | 立体声 | 16bit PCM';

    } catch (error) {
      console.error(`[Renderer] 初始化失败: ${JSON.stringify(error)}`);
    }
  }

  // 处理音频中断
  private handleAudioInterrupt(interruptEvent: audio.InterruptEvent) {
    if (!this.audioRenderer) return;

    // 判断中断类型
    if (interruptEvent.eventType === audio.InterruptType.INTERRUPT_TYPE_BEGIN) {
      // 中断开始——有更高优先级的流抢占了焦点
      switch (interruptEvent.hintType) {
        case audio.InterruptHint.INTERRUPT_HINT_PAUSE:
          // 系统建议暂停
          this.audioRenderer.pause();
          console.info('[Renderer] 被中断,已暂停');
          break;
        case audio.InterruptHint.INTERRUPT_HINT_STOP:
          // 系统建议停止
          this.audioRenderer.stop();
          console.info('[Renderer] 被中断,已停止');
          break;
        case audio.InterruptHint.INTERRUPT_HINT_DUCK:
          // 系统建议降低音量("压低"效果)
          this.audioRenderer.setVolume(0.2);
          console.info('[Renderer] 被压低,音量降低');
          break;
      }
    } else if (interruptEvent.eventType === audio.InterruptType.INTERRUPT_TYPE_END) {
      // 中断结束——焦点归还
      switch (interruptEvent.hintType) {
        case audio.InterruptHint.INTERRUPT_HINT_RESUME:
          // 系统建议恢复播放
          this.audioRenderer.start();
          console.info('[Renderer] 焦点归还,恢复播放');
          break;
        case audio.InterruptHint.INTERRUPT_HINT_UNDUCK:
          // 系统建议恢复音量
          this.audioRenderer.setVolume(this.volumeLevel / 100);
          console.info('[Renderer] 压低结束,恢复音量');
          break;
      }
    }
  }

  // 开始播放
  async startPlay() {
    if (!this.audioRenderer) return;

    try {
      // 启动渲染器
      await this.audioRenderer.start();
      console.info('[Renderer] 开始播放');

      // 模拟写入 PCM 数据
      this.writePcmData();
    } catch (error) {
      console.error(`[Renderer] 播放失败: ${JSON.stringify(error)}`);
    }
  }

  // 写入 PCM 数据
  private async writePcmData() {
    if (!this.audioRenderer) return;

    const bufferSize = await this.audioRenderer.getBufferSize();
    console.info(`[Renderer] 缓冲区大小: ${bufferSize}`);

    // 生成模拟 PCM 数据(440Hz 正弦波,即标准 A 音)
    const sampleRate = 44100;
    const duration = 2; // 2 秒
    const totalSamples = sampleRate * duration;
    const pcmData = new Int16Array(totalSamples * 2); // 立体声

    for (let i = 0; i < totalSamples; i++) {
      // 440Hz 正弦波
      const sample = Math.sin(2 * Math.PI * 440 * i / sampleRate) * 0.3 * 32767;
      const intSample = Math.floor(sample);
      pcmData[i * 2] = intSample;       // 左声道
      pcmData[i * 2 + 1] = intSample;   // 右声道
    }

    // 分块写入
    const chunkSize = Math.floor(bufferSize / 2); // Int16 每个 2 字节
    let offset = 0;

    while (offset < pcmData.length && this.isPlaying) {
      const remaining = pcmData.length - offset;
      const writeSize = Math.min(chunkSize, remaining);
      const chunk = pcmData.slice(offset, offset + writeSize);

      await this.audioRenderer.write(chunk.buffer);
      offset += writeSize;

      // 更新进度
      this.playProgress = Math.floor((offset / pcmData.length) * 100);
    }
  }

  // 暂停播放
  async pausePlay() {
    await this.audioRenderer?.pause();
  }

  // 停止播放
  async stopPlay() {
    await this.audioRenderer?.stop();
    this.playProgress = 0;
  }

  // 设置音量
  async setVolume(value: number) {
    this.volumeLevel = value;
    await this.audioRenderer?.setVolume(value / 100);
  }

  build() {
    Column() {
      Text('AudioRenderer 音频播放')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .fontColor('#E0E0E0')
        .margin({ bottom: 20 })

      // 音频信息
      Row() {
        Text('🎵').fontSize(20)
        Text(this.audioInfo)
          .fontSize(14)
          .fontColor('#4FC3F7')
          .margin({ left: 10 })
      }
      .width('100%')
      .padding(15)
      .borderRadius(12)
      .backgroundColor('#16213e')
      .margin({ bottom: 15 })

      // 播放进度
      Progress({ value: this.playProgress, total: 100, type: ProgressType.Linear })
        .width('100%')
        .color('#6C63FF')
        .margin({ bottom: 15 })

      // 音量控制
      Row() {
        Text('🔊').fontSize(18)
        Slider({
          value: this.volumeLevel,
          min: 0,
          max: 100,
          step: 1,
        })
          .onChange((value: number) => this.setVolume(value))
          .layoutWeight(1)
          .margin({ left: 10 })
        Text(`${this.volumeLevel}%`)
          .fontSize(14)
          .fontColor('#AAAAAA')
          .width(50)
      }
      .margin({ bottom: 20 })

      // 播放控制按钮
      Row() {
        Button('播放')
          .onClick(() => this.startPlay())
          .width(100)
          .backgroundColor('#6C63FF')
          .enabled(!this.isPlaying)

        Button('暂停')
          .onClick(() => this.pausePlay())
          .width(100)
          .backgroundColor('#FFB74D')
          .enabled(this.isPlaying)
          .margin({ left: 10 })

        Button('停止')
          .onClick(() => this.stopPlay())
          .width(100)
          .backgroundColor('#EF5350')
          .margin({ left: 10 })
      }
      .width('100%')
      .justifyContent(FlexAlign.Center)
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .backgroundColor('#0d0d1a')
  }

  aboutToDisappear() {
    // 释放渲染器
    this.audioRenderer?.release();
    this.audioRenderer = null;
  }
}

3.2 进阶:AudioCapturer 录制音频

// 文件名:AudioCapturerDemo.ets
// 功能:使用 AudioCapturer 录制麦克风音频

import { audio } from '@kit.AudioKit';
import { fileIo } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';

@Entry
@Component
struct AudioCapturerPage {
  @State isRecording: boolean = false;
  @State recordDuration: number = 0;  // 录制时长(秒)
  @State audioLevel: number = 0;       // 音频电平(0-100)
  @State recordStatus: string = '就绪';

  private audioCapturer: audio.AudioCapturer | null = null;
  private recordTimer: number = -1;
  private recordedChunks: ArrayBuffer[] = [];

  aboutToAppear() {
    this.initCapturer();
  }

  // 初始化音频采集器
  async initCapturer() {
    try {
      // 1. 创建采集器参数
      const capturerInfo: audio.AudioCapturerInfo = {
        source: audio.SourceType.SOURCE_TYPE_MIC,  // 麦克风源
        capturerFlags: 0,
      };

      // 2. 创建采集器选项
      const capturerOptions: audio.AudioCapturerOptions = {
        streamInfo: {
          samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100,
          channels: audio.AudioChannel.CHANNEL_1,                    // 单声道录音
          sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
          encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW,
        },
        capturerInfo: capturerInfo,
      };

      // 3. 创建采集器实例
      this.audioCapturer = await audio.createAudioCapturer(capturerOptions);
      console.info('[Capturer] 采集器创建成功');

      // 4. 注册状态变化回调
      this.audioCapturer.on('stateChange', (state: audio.AudioState) => {
        console.info(`[Capturer] 状态变化: ${state}`);
        switch (state) {
          case audio.AudioState.STATE_RUNNING:
            this.isRecording = true;
            this.recordStatus = '录制中';
            break;
          case audio.AudioState.STATE_STOPPED:
            this.isRecording = false;
            this.recordStatus = '已停止';
            break;
        }
      });

      // 5. 注册音频中断回调
      this.audioCapturer.on('audioInterrupt', (interruptEvent: audio.InterruptEvent) => {
        console.info(`[Capturer] 音频中断: ${JSON.stringify(interruptEvent)}`);
        // 录制中断处理(如电话呼入时停止录音)
        if (interruptEvent.eventType === audio.InterruptType.INTERRUPT_TYPE_BEGIN) {
          this.stopRecording();
        }
      });

      // 6. 注册读数据回调——持续获取麦克风数据
      this.audioCapturer.on('readData', (buffer: ArrayBuffer) => {
        // 计算音频电平(RMS 值)
        this.calculateAudioLevel(buffer);

        // 保存录音数据
        this.recordedChunks.push(buffer.slice(0));
      });

    } catch (error) {
      console.error(`[Capturer] 初始化失败: ${JSON.stringify(error)}`);
    }
  }

  // 计算音频电平
  private calculateAudioLevel(buffer: ArrayBuffer) {
    const data = new Int16Array(buffer);
    let sum = 0;
    for (let i = 0; i < data.length; i++) {
      sum += data[i] * data[i];
    }
    const rms = Math.sqrt(sum / data.length);
    // 归一化到 0-100
    this.audioLevel = Math.min(100, Math.floor((rms / 32767) * 200));
  }

  // 开始录制
  async startRecording() {
    if (!this.audioCapturer) return;

    try {
      // 清空之前的录音数据
      this.recordedChunks = [];
      this.recordDuration = 0;

      // 启动采集器
      await this.audioCapturer.start();
      console.info('[Capturer] 开始录制');

      // 启动计时器
      this.recordTimer = setInterval(() => {
        this.recordDuration++;
      }, 1000);

    } catch (error) {
      console.error(`[Capturer] 录制失败: ${JSON.stringify(error)}`);
    }
  }

  // 停止录制
  async stopRecording() {
    if (!this.audioCapturer) return;

    try {
      await this.audioCapturer.stop();
      if (this.recordTimer !== -1) {
        clearInterval(this.recordTimer);
        this.recordTimer = -1;
      }

      // 保存录音文件
      await this.saveRecording();
    } catch (error) {
      console.error(`[Capturer] 停止失败: ${JSON.stringify(error)}`);
    }
  }

  // 保存录音到文件
  private async saveRecording() {
    if (this.recordedChunks.length === 0) return;

    try {
      const context = getContext(this) as common.UIAbilityContext;
      const outputPath = context.filesDir + `/recording_${Date.now()}.pcm`;
      const file = fileIo.openSync(outputPath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);

      // 合并所有录音块
      let totalSize = 0;
      for (const chunk of this.recordedChunks) {
        totalSize += chunk.byteLength;
      }

      const merged = new ArrayBuffer(totalSize);
      const mergedView = new Uint8Array(merged);
      let offset = 0;
      for (const chunk of this.recordedChunks) {
        mergedView.set(new Uint8Array(chunk), offset);
        offset += chunk.byteLength;
      }

      fileIo.writeSync(file.fd, merged);
      fileIo.closeSync(file);

      console.info(`[Capturer] 录音已保存: ${outputPath}, 大小: ${totalSize} 字节`);
      this.recordStatus = `已保存 (${(totalSize / 1024).toFixed(1)}KB)`;

    } catch (error) {
      console.error(`[Capturer] 保存失败: ${JSON.stringify(error)}`);
    }
  }

  build() {
    Column() {
      Text('AudioCapturer 音频录制')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .fontColor('#E0E0E0')
        .margin({ bottom: 20 })

      // 录制状态
      Row() {
        Circle({ width: 16, height: 16 })
          .fill(this.isRecording ? '#EF5350' : '#555555')
          .animation({ duration: 300 })
        Text(this.recordStatus)
          .fontSize(16)
          .fontColor(this.isRecording ? '#EF5350' : '#888888')
          .margin({ left: 12 })
      }
      .margin({ bottom: 20 })

      // 录制时长
      Text(this.formatDuration(this.recordDuration))
        .fontSize(48)
        .fontWeight(FontWeight.Bold)
        .fontColor('#E0E0E0')
        .margin({ bottom: 20 })

      // 音频电平指示器
      Column() {
        Text('音频电平').fontSize(12).fontColor('#888888').margin({ bottom: 8 })
        Row() {
          ForEach(Array.from({ length: 20 }, (_, i) => i), (index: number) => {
            Column()
              .width(8)
              .height(`${Math.max(5, this.audioLevel * (index < this.audioLevel / 5 ? 1 : 0.05))}%`)
              .backgroundColor(
                index < this.audioLevel / 5 * 0.6 ? '#81C784' :
                index < this.audioLevel / 5 * 0.8 ? '#FFB74D' : '#EF5350'
              )
              .borderRadius(2)
              .margin({ right: 2 })
          })
        }
        .width('100%')
        .height(60)
        .alignItems(VerticalAlign.Bottom)
      }
      .width('100%')
      .padding(15)
      .borderRadius(12)
      .backgroundColor('#16213e')
      .margin({ bottom: 20 })

      // 录制配置信息
      Row() {
        Text('44100Hz | 单声道 | 16bit PCM')
          .fontSize(12)
          .fontColor('#888888')
      }
      .margin({ bottom: 20 })

      // 控制按钮
      Button(this.isRecording ? '停止录制' : '开始录制')
        .onClick(() => {
          if (this.isRecording) {
            this.stopRecording();
          } else {
            this.startRecording();
          }
        })
        .width(200)
        .height(60)
        .backgroundColor(this.isRecording ? '#EF5350' : '#6C63FF')
        .borderRadius(30)
        .fontSize(18)
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .backgroundColor('#0d0d1a')
    .alignItems(HorizontalAlign.Center)
  }

  // 格式化时长
  private 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')}`;
  }

  aboutToDisappear() {
    if (this.recordTimer !== -1) {
      clearInterval(this.recordTimer);
    }
    this.audioCapturer?.release();
    this.audioCapturer = null;
  }
}

3.3 高级:低延迟流实现实时音频回路

低延迟流最常见的应用场景是实时音频回路(Audio Loopback)——从麦克风采集音频,经过处理后立即从扬声器输出。这是乐器 App、K 歌 App 的核心能力。

// 文件名:LowLatencyAudioStream.ets
// 功能:低延迟音频流——实时麦克风到扬声器的回路

import { audio } from '@kit.AudioKit';

// 低延迟流配置
interface LowLatencyConfig {
  sampleRate: number;
  channelCount: number;
  bufferSize: number;       // 每帧缓冲区大小(采样数)
  totalBufferFrames: number; // 总缓冲帧数
}

@Entry
@Component
struct LowLatencyAudioStreamPage {
  @State isLoopbackActive: boolean = false;
  @State latencyMs: number = 0;
  @State statusText: string = '就绪';
  @State cpuUsage: number = 0;

  private capturer: audio.AudioCapturer | null = null;
  private renderer: audio.AudioRenderer | null = null;
  private isRunning: boolean = false;

  // 低延迟配置——使用更小的缓冲区
  private lowLatencyConfig: LowLatencyConfig = {
    sampleRate: 48000,      // 48kHz 更适合低延迟
    channelCount: 1,        // 单声道减少数据量
    bufferSize: 480,        // 10ms 一帧(48000 / 100 = 480)
    totalBufferFrames: 3,   // 3 帧总缓冲 = 30ms
  };

  aboutToAppear() {
    this.initLowLatencyStreams();
  }

  // 初始化低延迟流
  async initLowLatencyStreams() {
    try {
      // ===== 创建低延迟采集器 =====
      const capturerOptions: audio.AudioCapturerOptions = {
        streamInfo: {
          samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000,
          channels: audio.AudioChannel.CHANNEL_1,
          sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
          encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW,
        },
        capturerInfo: {
          source: audio.SourceType.SOURCE_TYPE_MIC,
          capturerFlags: 0,  // 低延迟标志(HarmonyOS 6 可用)
        },
      };

      this.capturer = await audio.createAudioCapturer(capturerOptions);

      // ===== 创建低延迟渲染器 =====
      const rendererOptions: audio.AudioRendererOptions = {
        streamInfo: {
          samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000,
          channels: audio.AudioChannel.CHANNEL_1,
          sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
          encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW,
        },
        rendererInfo: {
          usage: audio.StreamUsage.STREAM_USAGE_MUSIC,
          rendererFlags: 0,
        },
      };

      this.renderer = await audio.createAudioRenderer(rendererOptions);

      // 注册采集器的读数据回调——核心回路逻辑
      this.capturer.on('readData', (buffer: ArrayBuffer) => {
        if (!this.isRunning || !this.renderer) return;

        // ★ 核心:将采集到的数据直接写入渲染器
        // 这就是"回路"——麦克风 → 扬声器
        const writeLen = this.renderer.write(buffer);
        if (writeLen < 0) {
          console.warn('[Loopback] 写入渲染器失败');
        }
      });

      // 注册渲染器的音频中断回调
      this.renderer.on('audioInterrupt', (interruptEvent: audio.InterruptEvent) => {
        if (interruptEvent.eventType === audio.InterruptType.INTERRUPT_TYPE_BEGIN) {
          this.stopLoopback();
          console.info('[Loopback] 被中断,停止回路');
        }
      });

      this.statusText = '初始化完成';
      console.info('[Loopback] 低延迟流初始化完成');

    } catch (error) {
      console.error(`[Loopback] 初始化失败: ${JSON.stringify(error)}`);
      this.statusText = `初始化失败: ${error.message}`;
    }
  }

  // 启动音频回路
  async startLoopback() {
    if (!this.capturer || !this.renderer) return;

    try {
      // 先启动渲染器(消费者),再启动采集器(生产者)
      // 顺序很重要!先启动消费者避免缓冲区溢出
      await this.renderer.start();
      await this.capturer.start();

      this.isRunning = true;
      this.isLoopbackActive = true;
      this.statusText = '回路运行中';

      // 计算理论延迟
      const frameDuration = this.lowLatencyConfig.bufferSize / this.lowLatencyConfig.sampleRate * 1000;
      this.latencyMs = Math.round(frameDuration * this.lowLatencyConfig.totalBufferFrames);

      console.info(`[Loopback] 回路启动,理论延迟: ${this.latencyMs}ms`);

      // 启动性能监控
      this.startPerformanceMonitor();

    } catch (error) {
      console.error(`[Loopback] 启动失败: ${JSON.stringify(error)}`);
      this.statusText = `启动失败: ${error.message}`;
    }
  }

  // 停止音频回路
  async stopLoopback() {
    this.isRunning = false;

    try {
      // 先停采集器,再停渲染器
      await this.capturer?.stop();
      await this.renderer?.stop();

      this.isLoopbackActive = false;
      this.statusText = '已停止';
      console.info('[Loopback] 回路已停止');

    } catch (error) {
      console.error(`[Loopback] 停止失败: ${JSON.stringify(error)}`);
    }
  }

  // 性能监控
  private startPerformanceMonitor() {
    const monitorInterval = setInterval(() => {
      if (!this.isRunning) {
        clearInterval(monitorInterval);
        return;
      }
      // 模拟 CPU 占用(实际应通过系统 API 获取)
      this.cpuUsage = 5 + Math.random() * 15;
    }, 1000);
  }

  build() {
    Column() {
      Text('低延迟音频回路')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .fontColor('#E0E0E0')
        .margin({ bottom: 8 })

      Text('Low Latency Audio Loopback')
        .fontSize(12)
        .fontColor('#6C63FF')
        .margin({ bottom: 20 })

      // 延迟指标
      Row() {
        Column() {
          Text(`${this.latencyMs}`)
            .fontSize(36)
            .fontWeight(FontWeight.Bold)
            .fontColor('#4FC3F7')
          Text('延迟 (ms)')
            .fontSize(12)
            .fontColor('#888888')
            .margin({ top: 4 })
        }
        .layoutWeight(1)

        Column() {
          Text(`${this.cpuUsage.toFixed(1)}`)
            .fontSize(36)
            .fontWeight(FontWeight.Bold)
            .fontColor(this.cpuUsage > 30 ? '#EF5350' : '#81C784')
          Text('CPU (%)')
            .fontSize(12)
            .fontColor('#888888')
            .margin({ top: 4 })
        }
        .layoutWeight(1)
      }
      .width('100%')
      .padding(20)
      .borderRadius(12)
      .backgroundColor('#16213e')
      .margin({ bottom: 15 })

      // 配置信息
      Column() {
        Text('低延迟配置').fontSize(14).fontWeight(FontWeight.Bold).fontColor('#CE93D8')
        Row() {
          Text(`采样率: ${this.lowLatencyConfig.sampleRate}Hz`)
            .fontSize(12).fontColor('#AAAAAA')
          Text(' | ').fontColor('#555')
          Text(`缓冲: ${this.lowLatencyConfig.bufferSize} 采样/帧`)
            .fontSize(12).fontColor('#AAAAAA')
          Text(' | ').fontColor('#555')
          Text(`总帧: ${this.lowLatencyConfig.totalBufferFrames}`)
            .fontSize(12).fontColor('#AAAAAA')
        }
        .margin({ top: 8 })
      }
      .width('100%')
      .padding(15)
      .borderRadius(12)
      .backgroundColor('#16213e')
      .alignItems(HorizontalAlign.Start)
      .margin({ bottom: 15 })

      // 状态文本
      Text(this.statusText)
        .fontSize(14)
        .fontColor(this.isLoopbackActive ? '#81C784' : '#888888')
        .margin({ bottom: 20 })

      // 控制按钮
      Button(this.isLoopbackActive ? '停止回路' : '启动回路')
        .onClick(() => {
          if (this.isLoopbackActive) {
            this.stopLoopback();
          } else {
            this.startLoopback();
          }
        })
        .width(200)
        .height(60)
        .backgroundColor(this.isLoopbackActive ? '#EF5350' : '#6C63FF')
        .borderRadius(30)
        .fontSize(18)

      // 警告提示
      if (this.isLoopbackActive) {
        Row() {
          Text('⚠️')
            .fontSize(16)
          Text('回路运行中,请使用耳机避免啸叫!')
            .fontSize(13)
            .fontColor('#FFB74D')
            .margin({ left: 8 })
        }
        .width('100%')
        .padding(12)
        .borderRadius(10)
        .backgroundColor('#3E2723')
        .margin({ top: 15 })
      }
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .backgroundColor('#0d0d1a')
    .alignItems(HorizontalAlign.Center)
  }

  aboutToDisappear() {
    this.isRunning = false;
    this.capturer?.release();
    this.renderer?.release();
    this.capturer = null;
    this.renderer = null;
  }
}

四、踩坑与注意事项

4.1 常见陷阱

陷阱 现象 解决方案
流类型选错 音乐被闹钟打断后不恢复 使用正确的 StreamUsage,音乐用 STREAM_USAGE_MUSIC
忘记处理音频中断 来电后 App 继续播放但无声音 必须注册 audioInterrupt 回调并正确处理
缓冲区大小不当 播放卡顿或爆音 使用 getBufferSize() 获取系统推荐大小
状态转换跳步 调用 start() 直接报错 严格按 PREPARED → RUNNING → PAUSED/STOPPED 顺序
低延迟流啸叫 扬声器播放麦克风采集的声音形成正反馈 必须使用耳机,或加入回声消除算法
release 后复用 调用 release() 后再调用 start() 崩溃 release 是终止操作,需要重新 create

4.2 音频中断处理最佳实践

// 完整的音频中断处理模板
function handleAudioInterrupt(
  renderer: audio.AudioRenderer,
  event: audio.InterruptEvent,
  savedVolume: number
): number {
  let currentVolume = savedVolume;

  if (event.eventType === audio.InterruptType.INTERRUPT_TYPE_BEGIN) {
    switch (event.hintType) {
      case audio.InterruptHint.INTERRUPT_HINT_PAUSE:
        // 电话呼入等场景——暂停播放
        renderer.pause();
        // ★ 重要:不要在这里调用 release()
        // 等中断结束后可以恢复
        break;

      case audio.InterruptHint.INTERRUPT_HINT_STOP:
        // 闹钟等场景——停止播放
        renderer.stop();
        break;

      case audio.InterruptHint.INTERRUPT_HINT_DUCK:
        // 导航提示音等场景——降低音量但不暂停
        currentVolume = 0.2;  // 降到 20%
        renderer.setVolume(currentVolume);
        break;

      case audio.InterruptHint.INTERRUPT_HINT_NONE:
        // 被静音但不暂停(如另一个音乐 App 播放)
        renderer.setVolume(0);
        break;
    }
  } else {
    // INTERRUPT_TYPE_END——中断结束
    switch (event.hintType) {
      case audio.InterruptHint.INTERRUPT_HINT_RESUME:
        // 恢复播放
        renderer.start();
        break;

      case audio.InterruptHint.INTERRUPT_HINT_UNDUCK:
        // 恢复音量
        renderer.setVolume(savedVolume);
        currentVolume = savedVolume;
        break;
    }
  }

  return currentVolume;
}

4.3 低延迟流注意事项

  1. 必须使用耳机:扬声器和麦克风在同一设备上会形成声学回路,导致啸叫
  2. 采样率选择 48000Hz:这是大多数移动设备音频硬件的原生采样率,避免重采样开销
  3. 缓冲区越小延迟越低,但稳定性越差:建议从 10ms(480 采样)开始测试
  4. 低延迟流不参与系统混音:同一时间只能有一个低延迟流活跃
  5. 优先启动消费者:先 start Renderer 再 start Capturer,避免缓冲区溢出

五、HarmonyOS 6 适配

5.1 版本差异

特性 HarmonyOS 5 (API 12) HarmonyOS 6 (API 14)
低延迟流 无专用接口 新增:AudioRendererFlags 低延迟标志
缓冲区控制 固定大小 新增:setBufferSize() 动态调整
采集器降噪 新增:内置 AEC(回声消除)和 NS(噪声抑制)
多声道录制 最多 2 声道 新增:支持最多 8 声道录制
音频焦点 基础焦点管理 增强:AudioFocusManager 统一焦点管理

5.2 迁移指南

// HarmonyOS 5 写法——手动处理低延迟
const rendererOptions: audio.AudioRendererOptions = {
  streamInfo: {
    samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000,
    channels: audio.AudioChannel.CHANNEL_1,
    sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
    encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW,
  },
  rendererInfo: {
    usage: audio.StreamUsage.STREAM_USAGE_MUSIC,
    rendererFlags: 0,  // 普通模式
  },
};

// HarmonyOS 6 写法——使用低延迟标志
// const rendererOptions: audio.AudioRendererOptions = {
//   streamInfo: {
//     samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000,
//     channels: audio.AudioChannel.CHANNEL_1,
//     sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
//     encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW,
//   },
//   rendererInfo: {
//     usage: audio.StreamUsage.STREAM_USAGE_MUSIC,
//     rendererFlags: audio.AudioRendererFlags.FLAG_LOW_LATENCY,  // ★ 低延迟标志
//   },
// };
//
// // 采集器启用内置降噪
// const capturerOptions: audio.AudioCapturerOptions = {
//   streamInfo: { ... },
//   capturerInfo: {
//     source: audio.SourceType.SOURCE_TYPE_MIC,
//     capturerFlags: audio.AudioCapturerFlags.FLAG_AEC |  // 回声消除
//                    audio.AudioCapturerFlags.FLAG_NS,     // 噪声抑制
//   },
// };

5.3 注意事项

  • HarmonyOS 6 的低延迟标志需要在创建渲染器时指定,创建后无法修改
  • 内置 AEC/NS 功能会增加约 5ms 延迟,但能显著提升语音通话质量
  • setBufferSize() 动态调整时,新大小必须大于最小缓冲区大小(通过 getBufferSize() 获取)

六、总结

mindmap
  root((音频流管理))
    AudioRenderer
      播放 PCM 数据
      流类型与用途
      音频中断处理
      音量控制
    AudioCapturer
      麦克风采集
      音频电平检测
      录制文件保存
      降噪处理
    流类型映射
      MUSIC 音乐
      VOICE_COMMUNICATION 通话
      GAME 游戏
      ALARM 闹钟
      NOTIFICATION 通知
    低延迟流
      小缓冲区
      跳过混音器
      48kHz 原生采样率
      耳机防啸叫
    状态管理
      PREPAREDRUNNING
      RUNNINGPAUSED/STOPPED
      release 不可复用
      严格状态转换
    HarmonyOS 6
      FLAG_LOW_LATENCY
      内置 AEC/NS
      动态缓冲区
      AudioFocusManager

    classDef primary fill:#4FC3F7,stroke:#0288D1,color:#000
    classDef warning fill:#FFB74D,stroke:#F57C00,color:#000
    classDef error fill:#EF5350,stroke:#C62828,color:#fff
    classDef info fill:#81C784,stroke:#388E3C,color:#000
    classDef purple fill:#CE93D8,stroke:#7B1FA2,color:#000

关键知识点回顾

  1. AudioRenderer 和 AudioCapturer 是音频流的两个方向——一个输出一个输入,参数配置要匹配
  2. 流类型决定音频焦点策略——选错了类型,你的 App 可能"该响不响、不该响乱响"
  3. 音频中断回调必须正确处理——不处理中断 = 用户体验灾难
  4. 低延迟流的核心是减少缓冲区层级——代价是不参与系统混音
  5. 状态转换必须严格按顺序——跳步调用会导致异常甚至崩溃

音频流管理是音频开发的"交通指挥系统",搞懂了它,你的音频 App 才能在多应用共存的环境中优雅地运行。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。