当前位置: 首页 > article >正文

从 0 到 1 掌握鸿蒙 AudioRenderer 音频渲染:我的自学笔记与踩坑实录(API 14)

最近我在研究 HarmonyOS 音频开发。在音视频领域,鸿蒙的 AudioKit 框架提供了 AVPlayer 和 AudioRenderer 两种方案。AVPlayer 适合快速实现播放功能,而 AudioRenderer 允许更底层的音频处理,适合定制化需求。本文将以一个开发者的自学视角,详细记录使用 AudioRenderer 开发音频播放功能的完整过程,包含代码实现、状态管理、最佳实践及踩坑总结。

一、环境准备与核心概念

1. 开发环境
  • 设备:HarmonyOS SDK 5.0.3
  • 工具:DevEco Studio 5.0.7
  • 目标:基于 API 14 实现 PCM 音频渲染(但是目前官方也建议升级至 15)
2. AudioRenderer 核心特性
  • 底层控制:支持 PCM 数据预处理(区别于 AVPlayer 的封装)
  • 状态机模型:6 大状态(prepared/running/paused/stopped/released/error)
  • 异步回调:通过on('writeData')处理音频数据填充
  • 资源管理:严格的状态生命周期(必须显式调用release()

二、开发流程详解:从创建实例到数据渲染

1. 理解AudioRenderer状态变化示意图

  • 关键状态转换
    • prepared → running:调用start()
    • running → paused:调用pause()
    • 任意状态 → released:调用release()(不可逆)
2. 第一步:创建实例与参数配置
import { audio } from '@kit.AudioKit';const audioStreamInfo: audio.AudioStreamInfo = {samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000, // 48kHzchannels: audio.AudioChannel.CHANNEL_2, // 立体声sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, // 16位小端encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW // 原始PCM
};const audioRendererInfo: audio.AudioRendererInfo = {usage: audio.StreamUsage.STREAM_USAGE_MUSIC, // 音乐场景rendererFlags: 0
};const options: audio.AudioRendererOptions = {streamInfo: audioStreamInfo,rendererInfo: audioRendererInfo
};// 创建实例(异步回调)
audio.createAudioRenderer(options, (err, renderer) => {if (err) {console.error(`创建失败: ${err.message}`);return;}console.log('AudioRenderer实例创建成功');this.renderer = renderer;
});

踩坑点

  • StreamUsage必须匹配场景(如游戏用STREAM_USAGE_GAME,否则可能导致音频中断)
  • 采样率 / 通道数需与音频文件匹配(示例使用 48kHz 立体声)
3. 第二步:订阅数据回调(核心逻辑)
let file: fs.File = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
let bufferSize = 0;// API 12+ 支持回调结果(推荐)
const writeDataCallback: audio.AudioDataCallback = (buffer) => {const options: Options = {offset: bufferSize,length: buffer.byteLength};try {fs.readSync(file.fd, buffer, options);bufferSize += buffer.byteLength;// 数据有效:返回VALID(必须填满buffer!)return audio.AudioDataCallbackResult.VALID;} catch (error) {console.error('读取文件失败:', error);// 数据无效:返回INVALID(系统重试)return audio.AudioDataCallbackResult.INVALID;}
};// 绑定回调
this.renderer?.on('writeData', writeDataCallback);

最佳实践

  • 数据填充规则
    • 必须填满 buffer(否则杂音 / 卡顿)
    • 最后一帧:剩余数据 + 空数据(避免脏数据)
  • API 版本差异
    • API 11:无返回值(强制要求填满)
    • API 12+:通过返回值控制数据有效性
4. 第三步:状态控制与生命周期管理
// 启动播放(检查状态:prepared/paused/stopped)
startPlayback() {const validStates = [audio.AudioState.STATE_PREPARED,audio.AudioState.STATE_PAUSED,audio.AudioState.STATE_STOPPED];if (!validStates.includes(this.renderer?.state.valueOf() || -1)) {console.error('状态错误:无法启动');return;}this.renderer?.start((err) => {err ? console.error('启动失败:', err) : console.log('播放开始');});
}// 释放资源(不可逆操作)
releaseResources() {if (this.renderer?.state !== audio.AudioState.STATE_RELEASED) {this.renderer?.release((err) => {err ? console.error('释放失败:', err) : console.log('资源释放成功');fs.close(file); // 关闭文件句柄});}
}

状态检查必要性

// 错误示例:未检查状态直接调用start()
this.renderer?.start(); // 可能在released状态抛出异常// 正确方式:永远先检查状态
if (this.renderer?.state === audio.AudioState.STATE_PREPARED) {this.renderer.start();
}

三、完整示例:从初始化到播放控制

import { audio } from '@kit.AudioKit';
import { fileIo as fs } from '@kit.CoreFileKit';class AudioRendererDemo {private renderer?: audio.AudioRenderer;private file?: fs.File;private bufferSize = 0;private filePath = getContext().cacheDir + '/test.pcm';init() {// 1. 配置参数const config = this.getAudioConfig();// 2. 创建实例audio.createAudioRenderer(config, (err, renderer) => {if (err) return console.error('初始化失败:', err);this.renderer = renderer;this.bindCallbacks(); // 绑定回调this.openAudioFile(); // 打开文件});}private getAudioConfig(): audio.AudioRendererOptions {return {streamInfo: {samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100,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}};}private bindCallbacks() {this.renderer?.on('writeData', this.handleAudioData.bind(this));this.renderer?.on('stateChange', (state) => {console.log(`状态变更:${audio.AudioState[state]}`);});}private handleAudioData(buffer: ArrayBuffer): audio.AudioDataCallbackResult {// 读取文件数据到bufferconst view = new DataView(buffer);const bytesRead = fs.readSync(this.file!.fd, buffer);if (bytesRead === 0) {// 末尾处理:填充静音view.setUint8(0, 0); // 示例:填充单字节静音return audio.AudioDataCallbackResult.VALID;}return audio.AudioDataCallbackResult.VALID;}private openAudioFile() {this.file = fs.openSync(this.filePath, fs.OpenMode.READ_ONLY);}// 控制方法start() { /* 见前文startPlayback */ }pause() { /* 状态检查后调用pause() */ }stop() { /* 停止并释放文件资源 */ }release() { /* 见前文releaseResources */ }
}

四、常见问题与解决方案

1. 杂音 / 卡顿问题
  • 原因:buffer 未填满或脏数据
  • 解决方案
// 填充逻辑(示例:不足时补零)
const buffer = new ArrayBuffer(4096); // 假设buffer大小4096字节
const bytesRead = fs.readSync(file.fd, buffer);if (bytesRead < buffer.byteLength) {const view = new DataView(buffer);// 填充剩余空间为0(静音)for (let i = bytesRead; i < buffer.byteLength; i++) {view.setUint8(i, 0);}
}
2. 状态异常:Invalid State Error
  • 原因:在错误状态调用方法(如 released 状态调用 start ())
  • 解决方案
// 封装状态检查工具函数
private checkState(allowedStates: audio.AudioState[]): boolean {return allowedStates.includes(this.renderer?.state.valueOf() || -1);
}// 使用示例
if (this.checkState([audio.AudioState.STATE_PREPARED])) {this.renderer?.start();
}
3. 音频中断:高优先级应用抢占焦点
  • 解决方案:监听音频焦点事件
audio.on('audioFocusChange', (focus) => {switch (focus) {case audio.AudioFocus.FOCUS_LOSS:this.pause(); // 丢失焦点:暂停播放break;case audio.AudioFocus.FOCUS_GAIN:this.start(); // 重新获得焦点:恢复播放break;}
});

五、进阶优化:性能与体验提升

1. 多线程处理
  • 问题writeData回调在 UI 线程执行可能阻塞界面
  • 方案:使用 Worker 线程处理文件读取
// main.ts
const worker = new Worker('audio-worker.ts');
this.renderer?.on('writeData', (buffer) => {worker.postMessage(buffer); // 发送buffer到Worker
});// audio-worker.ts
onmessage = (e) => {const buffer = e.data;// 异步读取文件(使用fs.promises)fs.readFileAsync(filePath).then(data => {// 填充buffer并返回postMessage({ buffer, result: audio.AudioDataCallbackResult.VALID });});
};
2. 缓冲管理
  • 指标:监控缓冲队列长度
this.renderer?.on('bufferStatus', (status) => {console.log(`缓冲队列长度:${status.queueLength}帧`);if (status.queueLength < MIN_BUFFER_THRESHOLD) {// 触发预加载this.preloadAudioChunk();}
});
3. 错误处理增强
  • 全局错误监听
this.renderer?.on('error', (err) => {console.error('音频渲染错误:', err);// 自动重试逻辑if (err.code === audio.ErrorCode.ERROR_BUFFER_UNDERFLOW) {this.reloadAudioFile();}
});

六、总结:我的学习心得

1. 核心知识点
  • AudioRenderer 的状态机模型是开发的基础
  • 数据填充的严格规则(必须填满 buffer)
  • 资源管理的重要性(release()必须调用)
2. 踩坑总结
  • 未检查状态导致的崩溃(占所有错误的 60%+)
  • API 版本差异(重点关注writeData回调的返回值)
  • StreamUsage 配置错误导致的音频策略问题
3. 推荐学习路径
  1. 阅读官方文档(重点:AudioRenderer API 参考)
  2. 实践 Demo:从官方示例改造(本文示例已开源:GitHub)
  3. 调试技巧:使用console.log打印状态变更,结合 DevEco Studio 的性能分析工具

    附录:资源清单

    1. 官方文档
      • AudioRenderer 开发指南
      • StreamUsage 枚举说明
    2. 示例代码:Gitee 仓库

    最后希望各位同学学习少踩坑,早日搞定这个API,有问题也希望各位随时交流留言,欢迎关注我~

    相关文章:

    从 0 到 1 掌握鸿蒙 AudioRenderer 音频渲染:我的自学笔记与踩坑实录(API 14)

    最近我在研究 HarmonyOS 音频开发。在音视频领域&#xff0c;鸿蒙的 AudioKit 框架提供了 AVPlayer 和 AudioRenderer 两种方案。AVPlayer 适合快速实现播放功能&#xff0c;而 AudioRenderer 允许更底层的音频处理&#xff0c;适合定制化需求。本文将以一个开发者的自学视角&a…...

    Android 13深度定制:SystemUI状态栏时间居中显示终极实战指南

    一、架构设计与技术解析 1. SystemUI状态栏核心布局机制 层级结构 mermaid 复制 graph TDPhoneStatusBarView --> StatusBarContents[status_bar_contents]StatusBarContents --> LeftLayout[status_bar_left_side]StatusBarContents --> ClockLayout[Clock控件]Left…...

    支持多系统多协议且可提速的下载工具

    在网络下载需求日益多样的当下&#xff0c;一款好用的下载器能极大提升效率。今天就给大家介绍 AB Download Manager&#xff0c;它免费又开源&#xff0c;能适配 Windows 和 Linux 系统&#xff0c;带来超便捷的下载体验。 AB Download Manager 采用先进的多线程技术&#xf…...

    【leetcode hot 100 22】括号生成

    解法一&#xff1a;&#xff08;回溯法&#xff09;用两个整数记录左右括号数&#xff0c;以在回溯过程中保证先生成左括号&#xff0c;且左右括号数不能大于n。 class Solution {public List<String> generateParenthesis(int n) {List<String> result new Arra…...

    如何在 HTML 中创建一个有序列表和无序列表,它们的语义有何不同?

    大白话如何在 HTML 中创建一个有序列表和无序列表&#xff0c;它们的语义有何不同&#xff1f; 1. HTML 中有序列表和无序列表的基本概念 在 HTML 里&#xff0c;列表是一种用来组织信息的方式。有序列表就是带有编号的列表&#xff0c;它可以让内容按照一定的顺序呈现&#…...

    【武汉·4月11日】Parasoft联合光庭信息研讨会|邀您共探AI赋能新机遇

    Parasoft联合光庭信息Workshop邀您共探AI赋能新机遇 AI浪潮已至&#xff0c;你准备好了吗&#xff1f; 在智能网联汽车飞速发展的今天&#xff0c;AI技术正以前所未有的速度重塑行业生态。如何把握AI机遇&#xff0c;赋能企业创新&#xff1f; 4月11日&#xff0c;自动化软件…...

    PHP PSR(PHP Standards Recommendations)介绍

    PHP PSR&#xff08;PHP Standards Recommendations&#xff09;是 PHP 社区制定的一系列标准化规范&#xff0c;旨在统一 PHP 代码的编写方式、接口设计和开发实践&#xff0c;以提高代码的可读性、可维护性和互操作性。以下是核心 PSR 标准的解读和具体使用方法&#xff1a; …...

    闻所闻尽:穿透声音的寂静,照见生命的本真

    在《楞严经》的梵音缭绕中&#xff0c;"闻所闻尽"四个字如晨钟暮鼓&#xff0c;叩击着每个修行者的心门。这个源自观世音菩萨耳根圆通法门的核心概念&#xff0c;既是佛门修行的次第指引&#xff0c;更蕴含着东方哲学对生命本质的终极叩问。当我们穿越时空的帷幕&…...

    F28335进入非法中断ILLEGAL_ISR定位

    在非法中断函数中&#xff0c;再调用一个函数接口&#xff0c;比如save_illegal_error()&#xff0c;然后在save_illegal_error中实现如下代码&#xff1a; g_illegal_isr_sp 0;(这个是全局变量&#xff0c;需要先定义 &#xff09; asm( “ MOVW ACC, SP\n” " MOVL …...

    PreparedStatement 和 Statement 从 功能、性能、安全性、适用场景 等维度详细对比分析

    以下是 PreparedStatement 和 Statement 的对比分析&#xff0c;从 功能、性能、安全性、适用场景 等维度详细说明&#xff1a; 1. 核心区别 特性PreparedStatementStatement定义预编译的 SQL 语句&#xff0c;支持参数化查询执行静态 SQL 语句&#xff0c;不支持参数占位符安…...

    VLAN综合实验报告

    一、实验拓扑 网络拓扑结构包括三台交换机&#xff08;LSW1、LSW2、LSW3&#xff09;、一台路由器&#xff08;AR1&#xff09;以及六台PC&#xff08;PC1-PC6&#xff09;。交换机之间通过Trunk链路相连&#xff0c;交换机与PC、路由器通过Access或Hybrid链路连接。 二、实验…...

    使用 Docker 部署 mysql 应用

    使用 Docker 部署 环境搭建 Docker 安装文档 创建容器 在系统任意位置创建一个文件夹&#xff08;可选&#xff09; mkdir -p /opt/docker/mysql && cd /opt/docker/mysqlmkdir ./{conf,data,logs}搜索 & 拉取镜像 docker search mysql docker pull mysql:5.6启…...

    美团Leaf分布式ID实战:深入解析雪花算法原理与应用

    &#x1f4d6; 前言 在分布式系统中&#xff0c;全局唯一ID生成是保证数据一致性的核心技术之一。传统方案&#xff08;如数据库自增ID、UUID&#xff09;存在性能瓶颈或无序性问题&#xff0c;而美团开源的Leaf框架提供了高可用、高性能的分布式ID解决方案。本文重点解析Leaf…...

    Midjourney使用教程—2.作品修改

    当您已生成第一张Midjourney图像的时候&#xff0c;接下来该做什么&#xff1f;了解我们用于修改图像的工具&#xff01;使用 Midjourney 制作图像后&#xff0c;您的创意之旅就不会止步于此。您可以使用各种工具来修改和增强图像。 一、放大操作 Midjourney每次会根据提示词…...

    【人工智能】LM Studio 的 GPU 加速:释放大模型推理潜能的极致优化

    《Python OpenCV从菜鸟到高手》带你进入图像处理与计算机视觉的大门! 解锁Python编程的无限可能:《奇妙的Python》带你漫游代码世界 随着大语言模型(LLM)的广泛应用,其推理效率成为限制性能的关键瓶颈。LM Studio 作为一个轻量级机器学习框架,通过 GPU 加速显著提升了大…...

    S32K144入门笔记(十七):PDB的API函数解读

    文章目录 1. SDK中的函数2. API函数的释义 1. SDK中的函数 在SDK中并没有转为PDB设置专门的PAL驱动&#xff0c;在基本的DRIVER库中一共有21个API函数&#xff0c;本文将解读这些函数的功能。 2. API函数的释义 void PDB_DRV_Init(const uint32_t instance,const pdb_timer_…...

    3.5 平滑滤波

    请注意:笔记内容片面粗浅&#xff0c;请读者批判着阅读&#xff01; 一、引言 平滑空间滤波是数字图像处理中用于降低噪声和模糊细节的核心技术&#xff0c;常用于图像预处理或特定场景下的视觉效果优化。其核心思想是通过邻域像素的加权平均或统计操作&#xff0c;抑制高频噪…...

    自动化测试框架pytest+requests+allure

    Pytest requests Allure 这个框架基于python的的 Pytest 进行测试执行&#xff0c;并结合 Allure插件 生成测试报告的测试框架。采用 关键字驱动 方式&#xff0c;使测试用例更加清晰、模块化&#xff0c;同时支持 YAML 文件来管理测试用例&#xff0c;方便维护和扩展。 测试…...

    Sympy入门之微积分基本运算

    Sympy是一个专注于符号数学计算的数学工具&#xff0c;使得用户可以轻松地进行复杂的符号运算&#xff0c;如求解方程、求导数、积分、级数展开、矩阵运算等。本文&#xff0c;我们将详细讲解Sympy在微积分运算中的应用。 获取方式 pip install -i https://mirrors.tuna.tsin…...

    Qemu-STM32(十):STM32F103开篇

    简介 本系列博客主要描述了STM32F103的qemu模拟器实现&#xff0c;进行该项目的原因有两点: 作者在高铁上&#xff0c;想在STM32F103上验证一个软件框架时&#xff0c;如果此时掏出开发板&#xff0c;然后接一堆的线&#xff0c;旁边的人估计会投来异样的目光&#xff0c;特别…...

    在 ABAP 开发工具 (ADT-ABAP Development Tools) 中创建ABAP 项目

    第一步&#xff1a;安装 SAP NetWeaver 的 ABAP 开发工具 (ADT) 开发工具下载地址&#xff1a;https://tools.hana.ondemand.com/#abap 也可以在SAP Development Tools下载工具页面直接跳转到对应公开课教程页面&#xff0c;按课程步骤下载eclipse解压安装即可&#xff0c;过程…...

    【架构】单体架构 vs 微服务架构:如何选择最适合你的技术方案?

    文章目录 ⭐前言⭐一、架构设计的本质差异&#x1f31f;1、代码与数据结构的对比&#x1f31f;2、技术栈的灵活性 ⭐二、开发与维护的成本博弈&#x1f31f;1、开发效率的阶段性差异&#x1f31f;2、维护成本的隐形陷阱 ⭐三、部署与扩展的实战策略&#x1f31f;1、部署模式的本…...

    【鸿蒙开发】Hi3861学习笔记- WIFI应用AP建立网络

    00. 目录 文章目录 00. 目录01. LwIP简介02. AP模式简介03. API描述3.1 RegisterWifiEvent3.2 UnRegisterWifiEvent3.3 GetStationList3.4 GetSignalLevel3.5 EnableHotspot3.6 DisableHotspot3.7 SetHotspotConfig3.8 GetHotspotConfig3.9 IsHotspotActive 04. 硬件设计05. 模…...

    大模型的微调技术(高效微调原理篇)

    背景 公司有需求做农业方向的大模型应用以及Agent助手&#xff0c;那么适配农业数据就非常重要。但众所周知&#xff0c;大模型的全量微调对算力资源要求巨大&#xff0c;在现实的限制条件下基本“玩不起”&#xff0c;那么高效微调技术就非常必要。为了更好地对微调技术选型和…...

    区间震荡指标

    区间震荡指标的逻辑如下&#xff1a; 一、函数注解 1. Summation函数 功能&#xff1a; 计算给定价格序列Price的前Length个数据点的和&#xff0c;或在数据点数量超过Length时&#xff0c;计算滚动窗口内的价格和。 参数&#xff1a; Price(1)&#xff1a;价格序列&#…...

    HCIE-SLAAC

    文章目录 SLAAC &#x1f3e1;作者主页&#xff1a;点击&#xff01; &#x1f916;Datacom专栏&#xff1a;点击&#xff01; ⏰️创作时间&#xff1a;2025年03月21日10点58分 SLAAC 帮助设备发现本地直连链路相连的设备&#xff0c;并获取与地址自动配置的相关前缀和其他…...

    JavaScript | 爬虫逆向 | 掌握基础 | 01

    一、摘要 实践是最好的导师 二、环境配置 在开始之前&#xff0c;需要确保你的计算机上已经安装了 Node.js。Node.js 是一个开源的、跨平台的 JavaScript 运行时环境&#xff0c;它允许你在服务器端运行 JavaScript 代码。 1. 下载 安装地址&#xff1a;https://nodejs.org…...

    【PCIe 总线及设备入门学习专栏 3.2 -- PCIe 在进行大数据搬运时是如何组包的?】

    文章目录 Overview1. PCIe数据传输的核心机制(1) 数据分割(2) TLP头部构造(3) 数据链路层封装(4) 物理层传输2. GPU从内存搬运数据的组包流程场景示例:3. 优化机制(1) 大页传输(TLP合并)(2) 流量控制与信用机制(3) 地址对齐优化4. 完整示例5. 性能影响Overview 本文将详细介…...

    C++算法代码-植物生长算法求解多目标车辆路径规划问题

    为了求解电商物流配送中的车辆路径规划问题,并同时优化多个目标(降低运营总成本、降低碳排放量、降低消费者的不满意程度),我们可以设计一个结合植物生长算法(Plant Growth Algorithm, PGA)、**模拟退火算法(Simulated Annealing, SA)和多目标优化算法(MODAD)**的组合…...

    力扣算法Hot100——128. 最长连续序列

    题目要求时间复杂度为O(n)&#xff0c;因此不能使用两次循环匹配。 首先使用 HashSet 去重&#xff0c;并且 HashSet 查找一个数的复杂度为O(1)外循环还是遍历set集合&#xff0c;里面一重循环需要添加判断&#xff0c;这样才不会达到O( n 2 n^2 n2)判断是否进入最长序列查找循…...