鸿蒙媒体开发系列04——音频播放
头像 蓝亭书序 2026-08-24 17:01:33    发布
0 浏览 0 点赞 0 收藏

1、如何选择音频播放开发方式

在HarmonyOS系统中,多种API都提供了音频播放开发的支持,不同的API适用于不同音频数据格式、音频资源来源、音频使用场景,甚至是不同开发语言。因此,选择合适的音频播放API,有助于降低开发工作量,实现更佳的音频播放效果。

  • AVPlayer:功能较完善的音频、视频播放ArkTS/JS API,集成了流媒体和本地资源解析、媒体资源解封装、音频解码和音频输出功能。可以用于直接播放mp3、m4a等格式的音频文件,不支持直接播放PCM格式文件。
  • AudioRenderer:用于音频输出的的ArkTS/JS API,仅支持PCM格式,需要应用持续写入音频数据进行工作。应用可以在输入前添加数据预处理,如设定音频文件的采样率、位宽等,要求开发者具备音频处理的基础知识,适用于更专业、更多样化的媒体播放应用开发。
  • OpenSL ES:一套跨平台标准化的音频Native API,目前阶段唯一的音频类Native API,同样提供音频输出能力,仅支持PCM格式,适用于从其他嵌入式平台移植,或依赖在Native层实现音频输出功能的播放应用使用。
  • 在音频播放中,应用时常需要用到一些急促简短的音效,如相机快门音效、按键音效、游戏射击音效等,当前只能使用AVPlayer播放音频文件替代实现,在HarmonyOS后续版本将会推出相关接口来支持该场景。

2、使用AVPlayer开发音频播放功能

使用AVPlayer可以实现端到端播放原始媒体资源,本文将以完整地播放一首音乐作为示例,讲解AVPlayer音频播放相关功能。

播放的全流程包含:创建AVPlayer,设置播放资源,设置播放参数(音量/倍速/焦点模式),播放控制(播放/暂停/跳转/停止),重置,销毁资源。

在进行应用开发的过程中,我们可以通过AVPlayer的state属性主动获取当前状态或使用on('stateChange')方法监听状态变化。如果应用在音频播放器处于错误状态时执行操作,系统可能会抛出异常或生成其他未定义的行为。如下图所示:

图片

当播放处于prepared / playing / paused / completed状态时,播放引擎处于工作状态,这需要占用系统较多的运行内存。当客户端暂时不使用播放器时,调用reset()或release()回收内存资源,做好资源利用。

2.1、开发步骤及注意事项

  1. 创建实例createAVPlayer(),AVPlayer初始化idle状态。
  2. 设置业务需要的监听事件,搭配全流程场景使用。支持的监听事件如下:
  3. 图片

3. 设置资源:设置属性url,AVPlayer进入initialized状态。

4. 准备播放:调用prepare(),AVPlayer进入prepared状态,此时可以获取duration,设置音量。

5. 音频播控:播放play(),暂停pause(),跳转seek(),停止stop() 等操作。

6. (可选)更换资源:调用reset()重置资源,AVPlayer重新进入idle状态,允许更换资源url。

7. 退出播放:调用release()销毁实例,AVPlayer进入released状态,退出播放。

2.2、Demo

参考以下示例,完整地播放一首音乐。


import media from '@ohos.multimedia.media';
import fs from '@ohos.file.fs';
import common from '@ohos.app.ability.common';

export class AVPlayerDemo {
  private avPlayer;
  private isSeek: boolean = true; // 用于区分模式是否支持seek操作
  private count: number = 0;

  // 注册avplayer回调函数
  setAVPlayerCallback() {
    // seek操作结果回调函数
    this.avPlayer.on('seekDone', (seekDoneTime) => {
      console.info(`AVPlayer seek succeeded, seek time is ${seekDoneTime}`);
    })
    // error回调监听函数,当avPlayer在操作过程中出现错误时调用reset接口触发重置流程
    this.avPlayer.on('error', (err) => {
      console.error(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`);
      this.avPlayer.reset(); // 调用reset重置资源,触发idle状态
    })
    // 状态机变化回调函数
    this.avPlayer.on('stateChange', async (state, reason) => {
      switch (state) {
        case 'idle': // 成功调用reset接口后触发该状态机上报
          console.info('AVPlayer state idle called.');
          this.avPlayer.release(); // 调用release接口销毁实例对象
          break;
        case 'initialized': // avplayer 设置播放源后触发该状态上报
          console.info('AVPlayerstate initialized called.');
          this.avPlayer.prepare().then(() => {
            console.info('AVPlayer prepare succeeded.');
          }, (err) => {
            console.error(`Invoke prepare failed, code is ${err.code}, message is ${err.message}`);
          });
          break;
        case 'prepared': // prepare调用成功后上报该状态机
          console.info('AVPlayer state prepared called.');
          this.avPlayer.play(); // 调用播放接口开始播放
          break;
        case 'playing': // play成功调用后触发该状态机上报
          console.info('AVPlayer state playing called.');
          if (this.count !== 0) {
            console.info('AVPlayer start to seek.');
            this.avPlayer.seek(this.avPlayer.duration); //seek到音频末尾
          } else {
            this.avPlayer.pause(); // 调用暂停接口暂停播放
          }
          this.count++;
          break;
        case 'paused': // pause成功调用后触发该状态机上报
          console.info('AVPlayer state paused called.');
          this.avPlayer.play(); // 再次播放接口开始播放
          break;
        case 'completed': // 播放结束后触发该状态机上报
          console.info('AVPlayer state completed called.');
          this.avPlayer.stop(); //调用播放结束接口
          break;
        case 'stopped': // stop接口成功调用后触发该状态机上报
          console.info('AVPlayer state stopped called.');
          this.avPlayer.reset(); // 调用reset接口初始化avplayer状态
          break;
        case 'released':
          console.info('AVPlayer state released called.');
          break;
        default:
          console.info('AVPlayer state unknown called.');
          break;
      }
    })
  }

  // 以下demo为使用fs文件系统打开沙箱地址获取媒体文件地址并通过url属性进行播放示例
  async avPlayerUrlDemo() {
    // 创建avPlayer实例对象
    this.avPlayer = await media.createAVPlayer();
    // 创建状态机变化回调函数
    this.setAVPlayerCallback();
    let fdPath = 'fd://';
    // 通过UIAbilityContext获取沙箱地址filesDir,以下为Stage模型获方式,如需在FA模型上获取请参考《访问应用沙箱》获取地址
    let context = getContext(this) as common.UIAbilityContext;
    let pathDir = context.filesDir;
    let path = pathDir + '/01.mp3';
    // 打开相应的资源文件地址获取fd,并为url赋值触发initialized状态机上报
    let file = await fs.open(path);
    fdPath = fdPath + '' + file.fd;
    this.avPlayer.url = fdPath;
  }

  // 以下demo为使用资源管理接口获取打包在HAP内的媒体资源文件并通过fdSrc属性进行播放示例
  async avPlayerFdSrcDemo() {
    // 创建avPlayer实例对象
    this.avPlayer = await media.createAVPlayer();
    // 创建状态机变化回调函数
    this.setAVPlayerCallback();
    // 通过UIAbilityContext的resourceManager成员的getRawFd接口获取媒体资源播放地址
    // 返回类型为{fd,offset,length},fd为HAP包fd地址,offset为媒体资源偏移量,length为播放长度
    let context = getContext(this) as common.UIAbilityContext;
    let fileDescriptor = await context.resourceManager.getRawFd('01.mp3');
    // 为fdSrc赋值触发initialized状态机上报
    this.avPlayer.fdSrc = fileDescriptor;
    this.isSeek = false; // 不支持seek操作
  }
}


3、使用AudioRenderer开发音频播放功能

AudioRenderer是音频渲染器,用于播放PCM(Pulse Code Modulation)音频数据,相比AVPlayer而言,可以在输入前添加数据预处理,更适合有音频开发经验的开发者,以实现更灵活的播放功能。

3.1、开发指导

使用AudioRenderer播放音频涉及到AudioRenderer实例的创建、音频渲染参数的配置、渲染的开始与停止、资源的释放等。本开发指导将以一次渲染音频数据的过程为例,向开发者讲解如何使用AudioRenderer进行音频渲染。

下图展示了AudioRenderer的状态变化,在创建实例后,调用对应的方法可以进入指定的状态实现对应的行为。需要注意的是在确定的状态执行不合适的方法可能导致AudioRenderer发生错误,建议开发者在调用状态转换的方法前进行状态检查,避免程序运行产生预期以外的结果。

为保证UI线程不被阻塞,大部分AudioRenderer调用都是异步的。对于每个API均提供了callback函数和Promise函数,以下示例均采用callback函数。AudioRenderer状态变化示意图如下:

图片

在进行应用开发的过程中,建议开发者通过on('stateChange')方法订阅AudioRenderer的状态变更。因为针对AudioRenderer的某些操作,仅在音频播放器在固定状态时才能执行。如果应用在音频播放器处于错误状态时执行操作,系统可能会抛出异常或生成其他未定义的行为。

  • prepared状态:通过调用createAudioRenderer()方法进入到该状态。
  • running状态:正在进行音频数据播放,可以在prepared状态通过调用start()方法进入此状态,也可以在paused状态和stopped状态通过调用start()方法进入此状态。
  • paused状态:在running状态可以通过调用pause()方法暂停音频数据的播放并进入paused状态,暂停播放之后可以通过调用start()方法继续音频数据播放。
  • stopped状态:在paused/running状态可以通过stop()方法停止音频数据的播放。
  • released状态:在prepared、paused、stopped等状态,用户均可通过release()方法释放掉所有占用的硬件和软件资源,并且不会再进入到其他的任何一种状态了。

3.2、开发步骤及注意事项

1. 配置音频渲染参数并创建AudioRenderer实例。


import audio from '@ohos.multimedia.audio';

let audioStreamInfo = {
  samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100,
  channels: audio.AudioChannel.CHANNEL_1,
  sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
  encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
};

let audioRendererInfo = {
  content: audio.ContentType.CONTENT_TYPE_SPEECH,
  usage: audio.StreamUsage.STREAM_USAGE_VOICE_COMMUNICATION,
  rendererFlags: 0
};

let audioRendererOptions = {
  streamInfo: audioStreamInfo,
  rendererInfo: audioRendererInfo
};

audio.createAudioRenderer(audioRendererOptions, (err, data) => {
  if (err) {
    console.error(`Invoke createAudioRenderer failed, code is ${err.code}, message is ${err.message}`);
    return;
  } else {
    console.info('Invoke createAudioRenderer succeeded.');
    let audioRenderer = data;
  }
});


2. 调用start()方法进入running状态,开始渲染音频。


audioRenderer.start((err) => {
  if (err) {
    console.error(`Renderer start failed, code is ${err.code}, message is ${err.message}`);
  } else {
    console.info('Renderer start success.');
  }
});


3. 指定待渲染文件地址,打开文件调用write()方法向缓冲区持续写入音频数据进行渲染播放。如果需要对音频数据进行处理以实现个性化的播放,在写入之前操作即可。


const bufferSize = await audioRenderer.getBufferSize();
let file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
let buf = new ArrayBuffer(bufferSize);
let readsize = await fs.read(file.fd, buf);
let writeSize = await new Promise((resolve, reject) => {
  audioRenderer.write(buf, (err, writeSize) => {
    if (err) {
      reject(err);
    } else {
      resolve(writeSize);
    }
  });
});


4. 调用stop()方法停止渲染。


audioRenderer.stop((err) => {
  if (err) {
    console.error(`Renderer stop failed, code is ${err.code}, message is ${err.message}`);
  } else {
    console.info('Renderer stopped.');
  }
});


5. 调用release()方法销毁实例,释放资源。


audioRenderer.release((err) => {
  if (err) {
    console.error(`Renderer release failed, code is ${err.code}, message is ${err.message}`);
  } else {
    console.info('Renderer released.');
  }
});


3.3、Demo

下面展示了使用AudioRenderer渲染音频文件的示例代码。


import audio from '@ohos.multimedia.audio';
import fs from '@ohos.file.fs';

const TAG = 'AudioRendererDemo';

export default class AudioRendererDemo {
  private renderModel = undefined;
  private audioStreamInfo = {
    samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000, // 采样率
    channels: audio.AudioChannel.CHANNEL_2, // 通道
    sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, // 采样格式
    encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW // 编码格式
  }
  private audioRendererInfo = {
    content: audio.ContentType.CONTENT_TYPE_MUSIC, // 媒体类型
    usage: audio.StreamUsage.STREAM_USAGE_MEDIA, // 音频流使用类型
    rendererFlags: 0 // 音频渲染器标志
  }
  private audioRendererOptions = {
    streamInfo: this.audioStreamInfo,
    rendererInfo: this.audioRendererInfo
  }

  // 初始化,创建实例,设置监听事件
  init() {
    audio.createAudioRenderer(this.audioRendererOptions, (err, renderer) => { // 创建AudioRenderer实例
      if (!err) {
        console.info(`${TAG}: creating AudioRenderer success`);
        this.renderModel = renderer;
        this.renderModel.on('stateChange', (state) => { // 设置监听事件,当转换到指定的状态时触发回调
          if (state == 2) {
            console.info('audio renderer state is: STATE_RUNNING');
          }
        });
        this.renderModel.on('markReach', 1000, (position) => { // 订阅markReach事件,当渲染的帧数达到1000帧时触发回调
          if (position == 1000) {
            console.info('ON Triggered successfully');
          }
        });
      } else {
        console.info(`${TAG}: creating AudioRenderer failed, error: ${err.message}`);
      }
    });
  }

  // 开始一次音频渲染
  async start() {
    let stateGroup = [audio.AudioState.STATE_PREPARED, audio.AudioState.STATE_PAUSED, audio.AudioState.STATE_STOPPED];
    if (stateGroup.indexOf(this.renderModel.state) === -1) { // 当且仅当状态为prepared、paused和stopped之一时才能启动渲染
      console.error(TAG + 'start failed');
      return;
    }
    await this.renderModel.start(); // 启动渲染

    const bufferSize = await this.renderModel.getBufferSize();
    let context = getContext(this);
    let path = context.filesDir;
    const filePath = path + '/test.wav'; // 使用沙箱路径获取文件,实际路径为/data/storage/el2/base/haps/entry/files/test.wav

    let file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
    let stat = await fs.stat(filePath);
    let buf = new ArrayBuffer(bufferSize);
    let len = stat.size % bufferSize === 0 ? Math.floor(stat.size / bufferSize) : Math.floor(stat.size / bufferSize + 1);
    for (let i = 0; i < len; i++) {
      let options = {
        offset: i * bufferSize,
        length: bufferSize
      };
      let readsize = await fs.read(file.fd, buf, options);

      // buf是要写入缓冲区的音频数据,在调用AudioRenderer.write()方法前可以进行音频数据的预处理,实现个性化的音频播放功能,AudioRenderer会读出写入缓冲区的音频数据进行渲染

      let writeSize = await new Promise((resolve, reject) => {
        this.renderModel.write(buf, (err, writeSize) => {
          if (err) {
            reject(err);
          } else {
            resolve(writeSize);
          }
        });
      });
      if (this.renderModel.state === audio.AudioState.STATE_RELEASED) { // 如果渲染器状态为released,停止渲染
        fs.close(file);
        await this.renderModel.stop();
      }
      if (this.renderModel.state === audio.AudioState.STATE_RUNNING) {
        if (i === len - 1) { // 如果音频文件已经被读取完,停止渲染
          fs.close(file);
          await this.renderModel.stop();
        }
      }
    }
  }

  // 暂停渲染
  async pause() {
    // 只有渲染器状态为running的时候才能暂停
    if (this.renderModel.state !== audio.AudioState.STATE_RUNNING) {
      console.info('Renderer is not running');
      return;
    }
    await this.renderModel.pause(); // 暂停渲染
    if (this.renderModel.state === audio.AudioState.STATE_PAUSED) {
      console.info('Renderer is paused.');
    } else {
      console.error('Pausing renderer failed.');
    }
  }

  // 停止渲染
  async stop() {
    // 只有渲染器状态为running或paused的时候才可以停止
    if (this.renderModel.state !== audio.AudioState.STATE_RUNNING && this.renderModel.state !== audio.AudioState.STATE_PAUSED) {
      console.info('Renderer is not running or paused.');
      return;
    }
    await this.renderModel.stop(); // 停止渲染
    if (this.renderModel.state === audio.AudioState.STATE_STOPPED) {
      console.info('Renderer stopped.');
    } else {
      console.error('Stopping renderer failed.');
    }
  }

  // 销毁实例,释放资源
  async release() {
    // 渲染器状态不是released状态,才能release
    if (this.renderModel.state === audio.AudioState.STATE_RELEASED) {
      console.info('Renderer already released');
      return;
    }
    await this.renderModel.release(); // 释放资源
    if (this.renderModel.state === audio.AudioState.STATE_RELEASED) {
      console.info('Renderer released');
    } else {
      console.error('Renderer release failed.');
    }
  }
}


当同优先级或高优先级音频流要使用输出设备时,当前音频流会被中断,应用可以自行响应中断事件并做出处理。

4、使用OpenSL ES开发音频播放功能

OpenSL ES全称为Open Sound Library for Embedded Systems,是一个嵌入式、跨平台、免费的音频处理库。为嵌入式移动多媒体设备上的应用开发者提供标准化、高性能、低延迟的API。HarmonyOS的Native API基于Khronos Group开发的OpenSL ES 1.0.1 API 规范实现,开发者可以通过

4.1、HarmonyOS上的OpenSL ES

OpenSL ES中提供了以下的接口,HarmonyOS当前仅实现了部分OpenSL ES接口,可以实现音频播放的基础功能。

调用未实现接口后会返回SL_RESULT_FEATURE_UNSUPPORTED,当前没有相关扩展可以使用。

以下列表列举了HarmonyOS上已实现的OpenSL ES的接口,具体说明请参考OpenSL ES规范:

  • HarmonyOS上支持的Engine接口:
    • SLresult (*CreateAudioPlayer) (SLEngineItf self, SLObjectItf * pPlayer, SLDataSource *pAudioSrc, SLDataSink *pAudioSnk, SLuint32 numInterfaces, const SLInterfaceID * pInterfaceIds, const SLboolean * pInterfaceRequired)
    • SLresult (*CreateAudioRecorder) (SLEngineItf self, SLObjectItf * pRecorder, SLDataSource *pAudioSrc, SLDataSink *pAudioSnk, SLuint32 numInterfaces, const SLInterfaceID * pInterfaceIds, const SLboolean * pInterfaceRequired)
    • SLresult (*CreateOutputMix) (SLEngineItf self, SLObjectItf * pMix, SLuint32 numInterfaces, const SLInterfaceID * pInterfaceIds, const SLboolean * pInterfaceRequired)
  • HarmonyOS上支持的Object接口:
    • SLresult (*Realize) (SLObjectItf self, SLboolean async)
    • SLresult (*GetState) (SLObjectItf self, SLuint32 * pState)
    • SLresult (*GetInterface) (SLObjectItf self, const SLInterfaceID iid, void * pInterface)
    • void (*Destroy) (SLObjectItf self)
  • HarmonyOS上支持的Playback接口:
    • SLresult (*SetPlayState) (SLPlayItf self, SLuint32 state)
    • SLresult (*GetPlayState) (SLPlayItf self, SLuint32 *pState)
  • HarmonyOS上支持的Volume控制接口:
    • SLresult (*SetVolumeLevel) (SLVolumeItf self, SLmillibel level)
    • SLresult (*GetVolumeLevel) (SLVolumeItf self, SLmillibel *pLevel)
    • SLresult (*GetMaxVolumeLevel) (SLVolumeItf self, SLmillibel *pMaxLevel)
  • HarmonyOS上支持的BufferQueue接口:以下接口需引入<OpenSLES_OpenHarmony.h>使用。
  • 图片

4.2、Demo

【来自官方API介绍】

  1. 添加头文件。


#include "SLES/OpenSLES.h"
#include "SLES/OpenSLES_OpenHarmony.h"
#include "SLES/OpenSLES_Platform.h"


2. 使用slCreateEngine接口和获取engine实例。


SLObjectItf engineObject = nullptr;
slCreateEngine(&engineObject, 0, nullptr, 0, nullptr, nullptr);
(*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);


3. 获取接口SL_IID_ENGINE的engineEngine实例。


SLEngineItf engineEngine = nullptr;
(*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);


4. 配置播放器信息,创建AudioPlayer。


SLDataLocator_BufferQueue slBufferQueue = {
    SL_DATALOCATOR_BUFFERQUEUE,
    0
};

// 具体参数需要根据音频文件格式进行适配
SLDataFormat_PCM pcmFormat = {
    SL_DATAFORMAT_PCM,
    2,                           // 通道数
    SL_SAMPLINGRATE_48,          // 采样率
    SL_PCMSAMPLEFORMAT_FIXED_16, // 音频采样格式
    0,
    0,
    0
};
SLDataSource slSource = {&slBufferQueue, &pcmFormat};
SLObjectItf pcmPlayerObject = nullptr;
(*engineEngine)->CreateAudioPlayer(engineEngine, &pcmPlayerObject, &slSource, nullptr, 0, nullptr, nullptr);
(*pcmPlayerObject)->Realize(pcmPlayerObject, SL_BOOLEAN_FALSE);


5. 获取接口SL_IID_OH_BUFFERQUEUE的bufferQueueItf实例。


SLOHBufferQueueItf bufferQueueItf;
(*pcmPlayerObject)->GetInterface(pcmPlayerObject, SL_IID_OH_BUFFERQUEUE, &bufferQueueItf);


6. 打开音频文件,注册BufferQueueCallback回调。


static void BufferQueueCallback (SLOHBufferQueueItf bufferQueueItf, void *pContext, SLuint32 size)
{
    SLuint8 *buffer = nullptr;
    SLuint32 pSize;
    (*bufferQueueItf)->GetBuffer(bufferQueueItf, &buffer, &pSize);
    // 将待播放音频数据写入buffer
    (*bufferQueueItf)->Enqueue(bufferQueueItf, buffer, size);
}
void *pContext; // 可传入自定义的上下文信息,会在Callback内收到
(*bufferQueueItf)->RegisterCallback(bufferQueueItf, BufferQueueCallback, pContext);


7. 获取接口SL_PLAYSTATE_PLAYING的playItf实例,开始播放。


SLPlayItf playItf = nullptr;
(*pcmPlayerObject)->GetInterface(pcmPlayerObject, SL_IID_PLAY, &playItf);
(*playItf)->SetPlayState(playItf, SL_PLAYSTATE_PLAYING);


8. 结束音频播放。


(*playItf)->SetPlayState(playItf, SL_PLAYSTATE_STOPPED);
(*pcmPlayerObject)->Destroy(pcmPlayerObject);
(*engineObject)->Destroy(engineObject);



©本站发布的所有内容,包括但不限于文字、图片、音频、视频、图表、标志、标识、广告、商标、商号、域名、软件、程序等,除特别标明外,均来源于网络或用户投稿,版权归原作者或原出处所有。我们致力于保护原作者版权,若涉及版权问题,请及时联系我们进行处理。
分类
HarmonyOS
地址:北京市朝阳区北三环东路三元桥曙光西里甲1号第三置业A座1508室 商务内容合作QQ:2291221 电话:13391790444或(010)62178877
版权所有:电脑商情信息服务集团 北京赢邦策略咨询有限责任公司
声明:本媒体部分图片、文章来源于网络,版权归原作者所有,我司致力于保护作者版权,如有侵权,请与我司联系删除

京ICP备:2022009079号-2

京公网安备:11010502051901号

ICP证:京B2-20230255