前言

移动应用开发中音频播放的复杂性常被低估。开发者往往认为调用单个播放接口即可满足所有声音需求。项目进入中后期时短音效延迟、长音频状态管理与多流并发的资源竞争问题会集中爆发。

鸿蒙6 提供三套核心音频播放 API,分别是 SoundPool、AVPlayer 与 AudioRenderer。它们各自覆盖特定场景。本文深入探讨这三个 API 在低延迟音效与并发播放场景中的核心难点并提供符合最新规范的实战代码。

一、 音频播放场景的多样性挑战

音频播放需根据使用场景分裂为多个维度。按钮点击需要极致的低延迟。用户按下按钮后声音反馈若超过50毫秒便会产生操作脱节感。背景音乐需要完整的播放控制。暂停、跳转、循环功能必须稳定可靠。游戏音效要求多流并发且互不干扰。实时语音对延迟与资源占用极为敏感。任何不必要的内存拷贝都会影响通话质量。

鸿蒙设计三套 API 正是为了应对这种场景分化。SoundPool 面向短音效。它通过预加载机制将解码后的音频数据常驻内存以实现毫秒级响应。AVPlayer 专注长音频。它提供完整的状态机管理和媒体控制能力。AudioRenderer 处理专业级的实时音频流。它将底层硬件访问权限开放给开发者。明确场景的核心诉求是选择正确 API 的前提。

二、 SoundPool 短音效的低延迟实现

SoundPool 的设计逻辑是用空间换时间。它在播放前将音频文件完全解码并存储在内存中。播放时只需将内存数据推送到音频硬件从而避免实时解码开销。这种设计使 SoundPool 能够实现极低延迟。它完美适配按钮点击与系统提示音场景。

预加载带来了资源管理挑战。音频数据常驻内存意味着更高的内存占用。管理不当极易引发内存泄露。实际开发中需要在应用启动时初始化并预加载常用音效。在应用退出时务必释放所有资源。最新 API 规范中 play 方法已改为返回 Promise 流 ID。开发者需妥善保存该 ID 以备后续单独控制。

import { media } from '@kit.MediaKit';
import { audio } from '@kit.AudioKit';
import { Context } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

class ButtonSoundPlayer {
  private soundPool: media.SoundPool | null = null;
  private soundId: number = 0;
  private isReady: boolean = false;

  async init(context: Context): Promise<void> {
    const audioRendererInfo: audio.AudioRendererInfo = {
      usage: audio.StreamUsage.STREAM_USAGE_MUSIC,
      rendererFlags: 1
    };

    this.soundPool = await media.createSoundPool(14, audioRendererInfo);

    this.soundPool.on('loadComplete', (soundId: number) => {
      this.soundId = soundId;
      this.isReady = true;
    });

    const fd = await context.resourceManager.getRawFd('audio/button_click.mp3');
    this.soundId = await this.soundPool.load(fd.fd, fd.offset, fd.length);
  }

  async play(): Promise<number | undefined> {
    if (!this.soundPool || !this.isReady) {
      return undefined;
    }

    const playParams: media.PlayParameters = {
      loop: 0,
      rate: 1,
      leftVolume: 0.8,
      rightVolume: 0.8,
      priority: 0
    };

    try {
      const streamId = await this.soundPool.play(this.soundId, playParams);
      return streamId;
    } catch (err) {
      const error = err as BusinessError;
      console.error(`短音效播放失败 ${error.message}`);
      return undefined;
    }
  }

  async destroy(): Promise<void> {
    if (this.soundPool) {
      await this.soundPool.release();
      this.soundPool = null;
    }
  }
}

三、 AVPlayer 长音频的状态机控制

AVPlayer 采用按需加载与流式播放设计。长音频文件预加载到内存既不现实也会导致崩溃。AVPlayer 的核心在于状态机管理。播放流程从 idle、initialized、prepared 到 playing、completed 都有严格的流转规则。

最常见的致命错误是给播放器赋值 URL 后立刻调用 prepare 方法。底层解析是异步过程。给 fdSrc 赋值后必须等待状态机回调进入 initialized 状态才能调用 prepare。如果不遵循状态机流转规则系统会直接抛出状态异常并导致播放失败。

import { media } from '@kit.MediaKit';
import { Context } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

class AudioPlayer {
  private player: media.AVPlayer | null = null;
  private isPlaying: boolean = false;

  async init(): Promise<void> {
    if (this.player) {
      await this.safeRelease();
    }

    this.player = await media.createAVPlayer();

    this.player.on('stateChange', async (state: string) => {
      switch (state) {
        case 'initialized':
          await this.player?.prepare();
          break;
        case 'prepared':
          await this.player?.play();
          break;
        case 'playing':
          this.isPlaying = true;
          break;
        case 'completed':
        case 'error':
        case 'stopped':
          this.isPlaying = false;
          break;
      }
    });

    this.player.on('error', (err: BusinessError) => {
      console.error(`长音频播放错误 ${err.code} ${err.message}`);
      this.isPlaying = false;
    });
  }

  async play(audioPath: string, context: Context): Promise<void> {
    if (!this.player) {
      return;
    }

    if (this.isPlaying) {
      await this.player.stop();
      await this.player.reset();
    }

    try {
      const fd = await context.resourceManager.getRawFd(audioPath);
      this.player.fdSrc = { fd: fd.fd, offset: fd.offset, length: fd.length };
    } catch (err) {
      this.isPlaying = false;
    }
  }

  async stop(): Promise<void> {
    if (!this.player || !this.isPlaying) {
      return;
    }
    await this.player.stop();
    this.isPlaying = false;
  }

  private async safeRelease(): Promise<void> {
    if (!this.player) {
      return;
    }
    if (this.isPlaying) {
      await this.stop();
    }
    await this.player.release();
    this.player = null;
  }
}

四、 AudioRenderer 实时流的底层处理

AudioRenderer 直接操作 PCM 数据流。它不处理音频文件的解码。这种底层设计让其能够实现极低延迟。开发者需要手动管理音频数据的采集与缓冲区处理。

在真实的实时语音或录音回放业务中单次调用 write 无法完成播放。开发者必须结合事件驱动模型或者异步循环不断向底层缓冲区喂入 PCM 数据。写入操作必须紧跟底层消费速度以避免缓冲区下溢导致声音卡顿。

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

class RealtimeAudioPlayer {
  private audioRenderer: audio.AudioRenderer | null = null;
  private isPlaying: boolean = false;

  async init(): Promise<void> {
    const audioRendererInfo: audio.AudioRendererInfo = {
      usage: audio.StreamUsage.STREAM_USAGE_VOICE_COMMUNICATION,
      rendererFlags: 0
    };

    const audioRendererOptions: audio.AudioRendererOptions = {
      streamInfo: {
        samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_16000,
        channels: audio.AudioChannel.CHANNEL_1,
        sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
        encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
      },
      rendererInfo: audioRendererInfo
    };

    this.audioRenderer = await audio.createAudioRenderer(audioRendererOptions);
  }

  async startStream(): Promise<void> {
    if (!this.audioRenderer || this.isPlaying) {
      return;
    }
    await this.audioRenderer.start();
    this.isPlaying = true;
  }

  async writeDataChunk(pcmData: ArrayBuffer): Promise<void> {
    if (!this.audioRenderer || !this.isPlaying) {
      return;
    }
    try {
      const writeResult = await this.audioRenderer.write(pcmData);
      if (writeResult < 0) {
        console.error(`数据写入异常 错误码 ${writeResult}`);
      }
    } catch (err) {
      console.error(`写入执行失败 ${(err as BusinessError).message}`);
    }
  }

  async stop(): Promise<void> {
    if (!this.audioRenderer || !this.isPlaying) {
      return;
    }
    await this.audioRenderer.stop();
    await this.audioRenderer.release();
    this.audioRenderer = null;
    this.isPlaying = false;
  }
}

五、 并发控制与生命周期管理

并发播放是音频开发的核心难点。SoundPool 天生支持多流并发。合理设置并发上限可以保护硬件资源。重要音效需要设置高优先级。AVPlayer 并发支持较弱。创建多个实例会带来极大内存开销。背景音乐通常只维持单个实例。AudioRenderer 并发完全依赖开发者自己实现的混音算法。

资源管理同样不容忽视。在应用 onCreate 或页面 onPageShow 时初始化音频资源。在 onDestroy 或 onPageHide 时执行 release 释放硬件轨道。当系统资源紧张时需设计优雅的降级策略。例如主动减少预加载的音效数量或降低长音频的缓冲大小。

总结

音频开发涉及多个技术维度与底层资源调度。正确使用鸿蒙 API 需要深刻理解其异步状态机设计与内存管理机制。SoundPool 适合低延迟短音效。AVPlayer 专注长音频流控制。AudioRenderer 提供极致的底层灵活性。严格遵循系统 API 的异步流转规则并建立清晰的资源销毁闭环。这样才能在复杂业务中构建出流畅无异常的音频体验。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐