HarmonyOS 鸿蒙Next系统如何进行pcm流式播放

HarmonyOS 鸿蒙Next系统如何进行pcm流式播放 我现在需要实现这样一个音频播放功能:

应用内会从服务端下载音频数据,数据是pcm格式的,流式持续下载,现在需要把这些数据在鸿蒙设备上 直接播放出来,搜到了指导说 AudioRenderer接口跟OHAudio接口都有一个回调模式可以实现,有没有用过的,这两接口使用上区别大吗、回调模式有啥需要注意的?

10 回复

AudioRenderer是ArkTS API, 集成简单,支持后台播放,可播控中心播放。

OHAudio是C/C++ API, 开发复杂些,但性能高,延迟低,会C/C++可选。

两者编码实现区别大,但逻辑一样。

推荐用AudioRenderer,简单。实现步骤和注意的地方:

  1. 从服务端下载的PCM数据,存储可以选择存入文件或缓存,注意若存缓存,注意控制溢出。 使用NetworkKit的http模块的requestInStream请求后,on(‘dataReceive’)接收数据存入文件流或缓存,on(‘dataEnd’)处理下载结束。参考:《ohos.net.http》

  2. AudioRenderer从下载的缓存里或文件里加载PCM数据,注意加载PCM数据延迟处理。 使用AudioRenderer的on(‘writeData’)加载PCM数据,系统会根据各种播放状态和情况调用on(‘writeData’)的回调。注意正确处理数据写入的偏移量和文件结束EOF的情况。当所有音频数据写入完毕后,应通过返回INVALID等方式告知系统,或参考《判断播放结束》。参考:《AudioRenderer》

参考文档的示例代码基本上就可以解决你的需求了。如果有问题可以再问。

希望帮到你。如需OHAudio,可以再留言。有用给个采纳哈。😊

更多关于HarmonyOS 鸿蒙Next系统如何进行pcm流式播放的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


可以做,而且**你这个“服务端持续下载 PCM,边下边播”**的场景,AudioRendererOHAudio 都能实现,核心模式都是:

  • 播放端注册“写数据回调”
  • 系统需要音频帧时回调你
  • 你从本地缓冲队列里取 PCM 数据填进去
  • 数据够就返回有效,不够就先别让它播这次,等下次回调

官方 FAQ 也明确把 PCM 直接播放列成两条路:

先说选型结论

如果你的播放逻辑主要写在 ArkTS / 应用层

优先用 AudioRenderer

原因:

  • 接 ArkTS 业务更直接
  • 状态机、回调、生命周期都更顺手
  • 对“普通流式音频播放”已经够用
  • 你这个“网络下载 PCM -> 播放”场景,用它完全合理

如果你本身就有 Native/C++ 音频链路,或者很在意低时延

优先用 OHAudio

原因:

  • 华为现在对 C/C++ 播放明确写的是“推荐使用 OHAudio
  • 它同时覆盖普通通路和低时延通路
  • 更适合 Native 音频引擎、实时音频、游戏/K歌这类场景
  • 能直接配置低时延模式、frame size、native 回调

两者使用上区别大吗

概念上不大,工程形态上挺大。

相同点

两者都适合 PCM 播放,都有“回调喂数”的方式:

  • AudioRendereron('writeData', ...)
  • OHAudioOH_AudioStreamBuilder_SetRendererWriteDataCallback(...)

两者都要求你:

  • 事先配置 PCM 参数
    • 采样率
    • 声道数
    • 采样格式
    • 编码类型 RAW
  • 在回调里快速填满系统给的 buffer
  • 避免回调线程里做耗时操作
  • 正确处理“数据不足”“最后一帧不满”“中断/停止/释放”

不同点

AudioRenderer

更偏 ArkTS 应用层 API:

  • 上手更简单
  • 和页面、Ability 生命周期衔接自然
  • Promise/Callback 风格统一
  • 更适合“应用业务主导”的播放器

OHAudio

更偏 Native C API:

  • 要自己处理 builder、renderer、回调函数、CMake 链接
  • 更适合已有 C/C++ 音频模块
  • 可直接走普通模式或低时延模式
  • 对回调线程约束更硬,控制更细

你的场景推荐怎么选

你说的是:

  • 服务端持续下载
  • PCM 原始音频
  • 边下边播
  • 没强调极低时延,只是希望稳定流式播放

我建议优先用 AudioRenderer

因为这是个典型的“应用层流式播放”需求,不是游戏/K歌那种强实时音频。
除非你已经有 Native 解码/音频引擎,或者后面还要做回声消除、低时延、混音链路,否则没必要先上 OHAudio

回调模式最需要注意什么

这个才是重点。无论你选哪套接口,真正容易踩坑的是这里。

1. 回调里不要直接等网络

千万不要在音频回调里等待网络下载。

正确做法是两线程模型:

  • 下载线程:持续从服务端收 PCM,写入本地环形缓冲区/RingBuffer
  • 播放回调线程:只负责从 RingBuffer 取数据并拷贝到系统 buffer

也就是:

网络生产者 -> 本地队列 -> 音频回调消费者

如果回调里直接等网络包:

  • 很容易超时
  • 产生爆音、卡顿、断续

2. 一定要做“预缓冲”

不要一拿到第一包就立刻 start()

建议先缓存一小段再开播,比如:

  • 100ms
  • 200ms
  • 300ms

这样网络有轻微抖动时,不会立刻破音。

这是流式 PCM 播放最实用的一点。

3. PCM 参数必须完全匹配

服务端下发的 PCM 格式,必须和播放器配置一致:

  • 采样率:比如 16000 / 24000 / 48000
  • 声道数:单声道还是双声道
  • 采样格式:常见 S16LE
  • 编码类型:RAW

只要有一个不匹配,就会出现:

  • 播放速度不对
  • 声音发尖/发闷
  • 噪音
  • 失真

AudioRenderer 回调特别注意

官方文档对 AudioRendererwriteData 说得很明确:

  • 填满本次回调要求的长度,才返回 VALID
  • 没填满时不要返回 VALID
  • 否则容易出现杂音、卡顿
  • 最后一帧不足时,要自己补静音数据把 buffer 填满

也就是说你不能“有多少写多少,然后告诉系统可以播”。
这点非常关键。

另外文档还建议:

  • 不要在主线程注册回调
  • 不要在回调里做耗时业务
  • 不要等 UI、文件、网络操作

参考官方文档:
使用 AudioRenderer 开发音频播放功能

OHAudio 回调特别注意

OHAudio 的要求更硬一些:

  • 回调里禁止做耗时操作
  • 不要在回调里调用流控制接口
    • Start
    • Pause
    • Stop
    • Flush
    • Release

官方在低时延文档里明确写了,回调线程要和流控制逻辑分离,不然很容易出问题。
参考:
低时延音频播放

此外 OHAudio 从 API 12 开始也推荐用新的写数据回调,并且一样是:

  • 数据够才返回 VALID
  • 不够就返回 INVALID

参考:
推荐使用 OHAudio 开发音频播放功能

针对“流式下载 PCM”我建议你这样设计

方案结构

  1. 网络线程持续下载 PCM chunk
  2. 把 chunk 写入一个线程安全 RingBuffer
  3. 播放前先预缓冲一段数据
  4. 启动 AudioRendererOHAudio
  5. 在回调中按系统要求长度读取数据
  6. 读满则返回 VALID
  7. 读不满则:
    • 中间播放阶段:返回 INVALID,等下一轮
    • 流结束阶段:补静音后结束

队列建议

至少维护这几个状态:

  • bufferedBytes
  • isStarted
  • isDownloading
  • isEos,服务端是否结束
  • isUnderrun,是否发生供数不足

启播阈值

建议按时间算,不按包数算:

startThresholdBytes = sampleRate * channels * bytesPerSample * 0.2

比如 16k、单声道、16bit:

  • 16000 * 1 * 2 * 0.2 = 6400 bytes
  • 也就是先攒约 200ms 再播

低时延要不要开

如果你是:

  • 语音播报
  • 普通音乐/音频内容播放
  • TTS/实时返回音频播放

一般不建议为了这个场景强开低时延。

官方也写了:

  • 游戏、K歌、直播适合低时延
  • 普通音乐、视频播放不建议用低时延

因为低时延意味着:

  • buffer 更小
  • 回调更频繁
  • 更容易因供数不及时而卡顿

你的瓶颈反而更可能是网络抖动,不是系统播放延迟。

一个更落地的建议

你现在如果是 ArkTS 工程

直接上:

  • audio.createAudioRenderer(...)
  • audioRenderer.on('writeData', ...)
  • RingBuffer 缓冲网络数据
  • 先预缓冲再 start()

你现在如果:

  • PCM 来自 Native 解码器
  • 已经有 C++ 网络/解码模块
  • 想后续切低时延/音频引擎

直接上 OHAudio

最后给你一个简化判断

  • 想快落地,业务在 ArkTSAudioRenderer
  • 想做 Native 音频引擎或低时延OHAudio
  • 你的当前需求:我更推荐 AudioRenderer + RingBuffer + 预缓冲

如果你愿意,我下一条可以直接给你一份:

  • ArkTS 版 AudioRenderer 流式 PCM 播放骨架代码
  • C++ 版 OHAudio 流式 PCM 播放骨架代码

这个场景如果主链路在 ArkTS,优先建议用 AudioRenderer 的 on(‘writeData’) 回调模式;OHAudio 更适合已有 C/C++ 音频链路、对低延迟/底层控制要求高的场景。AVPlayer 更适合播放封装后的媒体资源,不适合直接喂裸 PCM 流。

实现上建议把“下载线程”和“音频回调线程”解耦:下载到的数据先进入环形缓冲区,writeData 回调里只做快速取数并填充 buffer,不要在回调里等待网络、做 IO 或复杂计算。创建 AudioRenderer 时的采样率、声道数、采样格式必须和服务端 PCM 完全一致,否则会变速、杂音或无声。开始播放前可以先缓存一小段水位;数据不足时填静音,避免爆音和 underrun。官方也有基于 AudioRenderer 播放 PCM 的实践文档,可参考:https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-playing-pcm-audio-based-audiorenderer

三、OHAudio 回调模式的额外注意点(如果选Native方案)

  1. 要自己管理线程和内存
    OHAudio没有ArkTS层的封装,所有的音频会话、缓冲区、回调都要自己在C/C++里处理,线程同步、内存泄漏都得自己盯,不然容易出各种奇怪的崩溃。

  2. 低延迟模式的坑
    它支持低延迟播放,但低延迟模式下缓冲区很小,对数据喂送的及时性要求更高,稍微慢一点就会断流,适合游戏、实时语音这种场景,普通音频播放没必要折腾。

  3. 跨线程回调的问题
    OHAudio的回调是跑在Native的系统线程里的,和你ArkTS层的线程是隔离的,如果你要把ArkTS层下载的数据传给OHAudio,得自己写NAPI桥接,处理好跨线程数据传递的问题,比纯ArkTS麻烦很多。

四、补充:AVPlayer能不能直接用?
评论里说的AVPlayer,其实更适合播放完整的音频文件(比如mp3、aac),不适合你这种边下载边播的PCM流,因为它没法直接喂裸PCM数据进去,还是得转格式或者封装,不如直接用AudioRenderer方便。

五、给你的建议
如果你的项目不是那种极致低延迟的实时语音,直接用ArkTS的AudioRenderer回调模式就行,开发快、坑少,完全能满足流式PCM播放的需求。

一、先搞懂两个接口的本质区别

  1. AudioRenderer(ArkTS侧的API)
    这个是鸿蒙给应用层(ArkTS/ArkUI)封装好的音频渲染接口,你在Stage模型里直接调用就行,不用碰C/C++代码。它的回调模式,就是系统音频线程会定期“喊你”,让你喂PCM数据进去,刚好适合你这种边下载边喂数据的流式场景。

  2. OHAudio(Native层的API)
    这个是给Native(C/C++)代码用的接口,属于NAPI开发范畴,比AudioRenderer更底层,性能上限更高,但门槛也高,得自己处理线程、内存这些。

简单说:想省事、快速实现,选AudioRenderer;追求极致低延迟、高性能,才考虑OHAudio。

二、AudioRenderer 回调模式的核心注意点(直接用的话重点看这个)

  1. 数据喂得够不够快,别断流
    回调是系统定时触发的,比如每10ms喊你一次要数据,你必须在回调里把足够的PCM数据塞进去,不然就会出现卡顿、爆音。
    流式场景下,你下载的线程要和回调线程做好缓冲,比如搞个环形队列,下载的数据先扔队列里,回调里直接从队列取,别让回调里直接等网络下载,不然必超时。

  2. 格式参数必须和PCM数据严格对齐
    创建AudioRenderer的时候,配置的采样率、位深(比如16bit)、声道数、数据格式,必须和你服务端下的PCM数据完全一致,不然声音会变调、杂音甚至完全没声。
    举个例子:你服务端是44100Hz、16bit、单声道的PCM,创建的时候就必须配一样的参数,不能瞎写。

  3. 回调里别做耗时操作
    回调函数是跑在系统的音频线程里的,这里面只能做“取数据、塞数据”这种轻量操作,别在里面做IO、网络、复杂计算,不然会阻塞音频线程,直接爆音。

  4. 线程安全问题
    你下载数据的线程和AudioRenderer的回调线程,都要访问同一个数据缓冲区,必须做好加锁,不然会出现数据错乱、崩溃。

主要一个napi开发(也就是Native层),一个直接arkts开发;

如果仅仅是为了播放pcm格式的音频, 用AVPlayer也可以

使用AudioRenderer渲染音频文件的示例代码

import { audio } from '@kit.AudioKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
// ...
const TAG = 'AudioRendererDemo';
class Options {
  public offset?: number;
  public length?: number;
}
// ...

let audioRenderer: audio.AudioRenderer | undefined = undefined;
let audioStreamInfo: audio.AudioStreamInfo = {
  samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000, // 采样率。
  channels: audio.AudioChannel.CHANNEL_2, // 通道。
  sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, // 采样格式。
  encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW // 编码格式。
};
let audioRendererInfo: audio.AudioRendererInfo = {
  usage: audio.StreamUsage.STREAM_USAGE_MUSIC, // 音频流使用类型:音乐。根据业务场景配置,参考StreamUsage。
  rendererFlags: 0 // 音频渲染器标志。
};
let audioRendererOptions: audio.AudioRendererOptions = {
  streamInfo: audioStreamInfo,
  rendererInfo: audioRendererInfo
};
let writeDataCallback: audio.AudioRendererWriteDataCallback;

async function initArguments(context: common.UIAbilityContext) {
  let bufferSize: number = 0;
  let file = await context.resourceManager.getRawFd('32_xiyouji.pcm');
  writeDataCallback = (buffer: ArrayBuffer) => {
    let options: Options = {
      offset: bufferSize,
      length: buffer.byteLength
    };

    try {
      let bufferLength = fs.readSync(file.fd, buffer, options);
      bufferSize += buffer.byteLength;
      // 如果当前回调传入的数据不足一帧,空白区域需要使用静音数据填充,否则会导致播放出现杂音。
      if (bufferLength < buffer.byteLength) {
        let view = new DataView(buffer);
        for (let i = bufferLength; i < buffer.byteLength; i++) {
          // 空白区域填充静音数据。当使用音频采样格式为SAMPLE_FORMAT_U8时0x7F为静音数据,使用其他采样格式时0为静音数据。
          view.setUint8(i, 0);
        }
      }
      // API version 11不支持返回回调结果,从API version 12开始支持返回回调结果。
      // 如果开发者不希望播放某段buffer,返回audio.AudioDataCallbackResult.INVALID即可。
      return audio.AudioDataCallbackResult.VALID;
    } catch (error) {
      console.error('Error reading file:', error);
      // ...
      // API version 11不支持返回回调结果,从API version 12开始支持返回回调结果。
      return audio.AudioDataCallbackResult.INVALID;
    }
  };
}

// 初始化,创建实例,设置监听事件。
async function init() {
  audio.createAudioRenderer(audioRendererOptions, (err, renderer) => { // 创建AudioRenderer实例。
    if (!err) {
      console.info(`${TAG}: creating AudioRenderer success`);
      // ...
      audioRenderer = renderer;
      if (audioRenderer !== undefined) {
        audioRenderer.on('writeData', writeDataCallback);
        // ...
      }
    } else {
      console.info(`${TAG}: creating AudioRenderer failed, error: ${err.message}`);
      // ...
    }
  });
}

// 开始一次音频渲染。
async function start() {
  if (audioRenderer !== undefined) {
    let stateGroup = [audio.AudioState.STATE_PREPARED, audio.AudioState.STATE_PAUSED, audio.AudioState.STATE_STOPPED];
    if (stateGroup.indexOf(audioRenderer.state.valueOf()) === -1) { // 当且仅当状态为prepared、paused和stopped之一时才能启动渲染。
      console.error(TAG + 'start failed');
      // ...
      return;
    }
    // 启动渲染。
    audioRenderer.start((err: BusinessError) => {
      if (err) {
        console.error('Renderer start failed.');
        // ...
      } else {
        console.info('Renderer start success.');
        // ...
      }
    });
  }
}

async function pause() {
  // 暂停渲染。
  if (audioRenderer !== undefined) {
    // 只有渲染器状态为running的时候才能暂停。
    if (audioRenderer.state.valueOf() !== audio.AudioState.STATE_RUNNING) {
      console.info('Renderer is not running');
      // ...
      return;
    }
    // 暂停渲染。
    audioRenderer.pause((err: BusinessError) => {
      if (err) {
        console.error('Renderer pause failed.');
        // ...
      } else {
        console.info('Renderer pause success.');
        // ...
      }
    });
  }
}

// 停止渲染。
async function stop() {
  if (audioRenderer !== undefined) {
    // 只有渲染器状态为running或paused的时候才可以停止。
    if (audioRenderer.state.valueOf() !== audio.AudioState.STATE_RUNNING &&
      audioRenderer.state.valueOf() !== audio.AudioState.STATE_PAUSED) {
      console.info('Renderer is not running or paused.');
      // ...
      return;
    }
    // 停止渲染。
    audioRenderer.stop((err: BusinessError) => {
      if (err) {
        console.error('Renderer stop failed.');
        // ...
      } else {
        console.info('Renderer stop success.');
        // ...
      }
    });
  }
}

// 销毁实例,释放资源。
async function release() {
  if (audioRenderer !== undefined) {
    // 渲染器状态不是released状态,才能release。
    if (audioRenderer.state.valueOf() === audio.AudioState.STATE_RELEASED) {
      console.info('Renderer already released');
      // ...
      return;
    }

    // ...

    // 释放资源。
    audioRenderer.release((err: BusinessError) => {
      if (err) {
        console.error('Renderer release failed.');
        // ...
      } else {
        // 关闭沙箱文件
        console.info('Renderer release success.');
        // ...
      }
    });
  }
}

我给你推荐一个library,云享社作者写的一个pcm解码器:

[@ospark/free-pcm(V1.0.4)](https://ohpm.openharmony.cn/#/cn/detail/@ospark%2Ffree-pcm)

Free PCM 是一个高性能音频解码库,专为 OpenHarmony/HarmonyOS 设计。支持多种主流音频格式解码为 PCM,内置流式解码引擎与 10 段均衡器。流式解码完美解决你的问题。

在HarmonyOS NEXT中,使用@ohos.multimedia.audio (AudioKit) 的 AudioRenderer 实现PCM流式播放。配置音频参数(采样率、声道、位深),调用 createAudioRenderer 创建实例,然后循环调用 write() 方法写入PCM数据块。支持回调监听写入进度。也可通过 AVPlayerDataSrc 接口处理内存流。

两者均支持PCM流式播放,核心区别在开发语言与性能层级:

  • AudioRenderer 是 ArkTS/JS 侧接口,适合纯鸿蒙应用快速实现;回调通过 on('writeData') 填入数据,编程模型更贴近前端习惯。
  • OHAudio 是 Native (C/C++) 接口,适合高性能、低延迟场景或已有 Native 代码复用;回调模式通过注册 OH_AudioRenderer_CallbacksOH_AudioRenderer_OnWriteData 触发,可获得更精确的时序控制。

回调模式需注意三点:

  1. 非阻塞填充:回调执行在系统音频线程,必须立即填入请求的数据长度后返回,不能在其中做网络IO或长时间计算。
  2. 缓冲区同步:流式下载的数据先写入应用层环形缓冲,回调中从该缓冲取数据,并做好水位控制,防止缓冲区下溢(underrun)导致爆音。
  3. 资源与生命周期start 后回调会持续触发,暂停或停止务必调用 stop,释放时要先 stoprelease,避免堆积未消费数据。
回到顶部