首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

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

  • 25-04-25 09:44
  • 4380
  • 5563
blog.csdn.net

最近我在研究 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. 第一步:创建实例与参数配置
  1. import { audio } from '@kit.AudioKit';
  2. const audioStreamInfo: audio.AudioStreamInfo = {
  3. samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000, // 48kHz
  4. channels: audio.AudioChannel.CHANNEL_2, // 立体声
  5. sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, // 16位小端
  6. encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW // 原始PCM
  7. };
  8. const audioRendererInfo: audio.AudioRendererInfo = {
  9. usage: audio.StreamUsage.STREAM_USAGE_MUSIC, // 音乐场景
  10. rendererFlags: 0
  11. };
  12. const options: audio.AudioRendererOptions = {
  13. streamInfo: audioStreamInfo,
  14. rendererInfo: audioRendererInfo
  15. };
  16. // 创建实例(异步回调)
  17. audio.createAudioRenderer(options, (err, renderer) => {
  18. if (err) {
  19. console.error(`创建失败: ${err.message}`);
  20. return;
  21. }
  22. console.log('AudioRenderer实例创建成功');
  23. this.renderer = renderer;
  24. });

踩坑点:

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

最佳实践:

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

状态检查必要性:

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

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

  1. import { audio } from '@kit.AudioKit';
  2. import { fileIo as fs } from '@kit.CoreFileKit';
  3. class AudioRendererDemo {
  4. private renderer?: audio.AudioRenderer;
  5. private file?: fs.File;
  6. private bufferSize = 0;
  7. private filePath = getContext().cacheDir + '/test.pcm';
  8. init() {
  9. // 1. 配置参数
  10. const config = this.getAudioConfig();
  11. // 2. 创建实例
  12. audio.createAudioRenderer(config, (err, renderer) => {
  13. if (err) return console.error('初始化失败:', err);
  14. this.renderer = renderer;
  15. this.bindCallbacks(); // 绑定回调
  16. this.openAudioFile(); // 打开文件
  17. });
  18. }
  19. private getAudioConfig(): audio.AudioRendererOptions {
  20. return {
  21. streamInfo: {
  22. samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100,
  23. channels: audio.AudioChannel.CHANNEL_1,
  24. sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
  25. encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
  26. },
  27. rendererInfo: {
  28. usage: audio.StreamUsage.STREAM_USAGE_MUSIC,
  29. rendererFlags: 0
  30. }
  31. };
  32. }
  33. private bindCallbacks() {
  34. this.renderer?.on('writeData', this.handleAudioData.bind(this));
  35. this.renderer?.on('stateChange', (state) => {
  36. console.log(`状态变更:${audio.AudioState[state]}`);
  37. });
  38. }
  39. private handleAudioData(buffer: ArrayBuffer): audio.AudioDataCallbackResult {
  40. // 读取文件数据到buffer
  41. const view = new DataView(buffer);
  42. const bytesRead = fs.readSync(this.file!.fd, buffer);
  43. if (bytesRead === 0) {
  44. // 末尾处理:填充静音
  45. view.setUint8(0, 0); // 示例:填充单字节静音
  46. return audio.AudioDataCallbackResult.VALID;
  47. }
  48. return audio.AudioDataCallbackResult.VALID;
  49. }
  50. private openAudioFile() {
  51. this.file = fs.openSync(this.filePath, fs.OpenMode.READ_ONLY);
  52. }
  53. // 控制方法
  54. start() { /* 见前文startPlayback */ }
  55. pause() { /* 状态检查后调用pause() */ }
  56. stop() { /* 停止并释放文件资源 */ }
  57. release() { /* 见前文releaseResources */ }
  58. }

四、常见问题与解决方案

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

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

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

六、总结:我的学习心得

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,有问题也希望各位随时交流留言,欢迎关注我~

    注:本文转载自blog.csdn.net的李游Leo的文章"https://blog.csdn.net/liyou041/article/details/146350262"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
    复制链接
    复制链接
    相关推荐
    发表评论
    登录后才能发表评论和回复 注册

    / 登录

    评论记录:

    未查询到任何数据!
    回复评论:

    分类栏目

    后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

    热门文章

    135
    HarmonyOS
    关于我们 隐私政策 免责声明 联系我们
    Copyright © 2020-2024 蚁人论坛 (iYenn.com) All Rights Reserved.
    Scroll to Top