1# 使用AudioRenderer开发音频播放功能 2 3AudioRenderer是音频渲染器,用于播放PCM(Pulse Code Modulation)音频数据,相比[AVPlayer](../media/using-avplayer-for-playback.md)而言,可以在输入前添加数据预处理,更适合有音频开发经验的开发者,以实现更灵活的播放功能。 4 5## 开发指导 6 7使用AudioRenderer播放音频涉及到AudioRenderer实例的创建、音频渲染参数的配置、渲染的开始与停止、资源的释放等。本开发指导将以一次渲染音频数据的过程为例,向开发者讲解如何使用AudioRenderer进行音频渲染,建议搭配[AudioRenderer的API说明](../../reference/apis-audio-kit/js-apis-audio.md#audiorenderer8)阅读。 8 9下图展示了AudioRenderer的状态变化,在创建实例后,调用对应的方法可以进入指定的状态实现对应的行为。需要注意的是在确定的状态执行不合适的方法可能导致AudioRenderer发生错误,建议开发者在调用状态转换的方法前进行状态检查,避免程序运行产生预期以外的结果。 10 11为保证UI线程不被阻塞,大部分AudioRenderer调用都是异步的。对于每个API均提供了callback函数和Promise函数,以下示例均采用callback函数。 12 13**图1** AudioRenderer状态变化示意图 14 15 16 17在进行应用开发的过程中,建议开发者通过[on('stateChange')](../../reference/apis-audio-kit/js-apis-audio.md#onstatechange-8)方法订阅AudioRenderer的状态变更。因为针对AudioRenderer的某些操作,仅在音频播放器在固定状态时才能执行。如果应用在音频播放器处于错误状态时执行操作,系统可能会抛出异常或生成其他未定义的行为。 18 19- prepared状态: 通过调用[createAudioRenderer()](../../reference/apis-audio-kit/js-apis-audio.md#audiocreateaudiorenderer8)方法进入到该状态。 20 21- running状态: 正在进行音频数据播放,可以在prepared状态通过调用[start()](../../reference/apis-audio-kit/js-apis-audio.md#start8)方法进入此状态,也可以在paused状态和stopped状态通过调用[start()](../../reference/apis-audio-kit/js-apis-audio.md#start8)方法进入此状态。 22 23- paused状态: 在running状态可以通过调用[pause()](../../reference/apis-audio-kit/js-apis-audio.md#pause8)方法暂停音频数据的播放并进入paused状态,暂停播放之后可以通过调用[start()](../../reference/apis-audio-kit/js-apis-audio.md#start8)方法继续音频数据播放。 24 25- stopped状态: 在paused/running状态可以通过[stop()](../../reference/apis-audio-kit/js-apis-audio.md#stop8)方法停止音频数据的播放。 26 27- released状态: 在prepared、paused、stopped等状态,用户均可通过[release()](../../reference/apis-audio-kit/js-apis-audio.md#release8)方法释放掉所有占用的硬件和软件资源,并且不会再进入到其他的任何一种状态了。 28 29### 开发步骤及注意事项 30 311. 配置音频渲染参数并创建AudioRenderer实例,音频渲染参数的详细信息可以查看[AudioRendererOptions](../../reference/apis-audio-kit/js-apis-audio.md#audiorendereroptions8)。 32 33 ```ts 34 import { audio } from '@kit.AudioKit'; 35 36 let audioStreamInfo: audio.AudioStreamInfo = { 37 samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000, // 采样率。 38 channels: audio.AudioChannel.CHANNEL_2, // 通道。 39 sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, // 采样格式。 40 encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW // 编码格式。 41 }; 42 43 let audioRendererInfo: audio.AudioRendererInfo = { 44 usage: audio.StreamUsage.STREAM_USAGE_MUSIC, // 音频流使用类型:音乐。根据业务场景配置,参考StreamUsage。 45 rendererFlags: 0 // 音频渲染器标志。 46 }; 47 48 let audioRendererOptions: audio.AudioRendererOptions = { 49 streamInfo: audioStreamInfo, 50 rendererInfo: audioRendererInfo 51 }; 52 53 audio.createAudioRenderer(audioRendererOptions, (err, data) => { 54 if (err) { 55 console.error(`Invoke createAudioRenderer failed, code is ${err.code}, message is ${err.message}`); 56 return; 57 } else { 58 console.info('Invoke createAudioRenderer succeeded.'); 59 let audioRenderer = data; 60 } 61 }); 62 ``` 63 642. 调用on('writeData')方法,订阅监听音频数据写入回调,推荐使用API version 12支持返回回调结果的方式。 65 66 - API version 12开始该方法支持返回回调结果,系统可以根据开发者返回的值来决定此次回调中的数据是否播放。 67 68 > **注意:** 69 > 70 > - 能填满回调所需长度数据的情况下,返回audio.AudioDataCallbackResult.VALID,系统会取用完整长度的数据缓冲进行播放。请不要在未填满数据的情况下返回audio.AudioDataCallbackResult.VALID,否则会导致杂音、卡顿等现象。 71 > 72 > - 在无法填满回调所需长度数据的情况下,建议开发者返回audio.AudioDataCallbackResult.INVALID,系统不会处理该段音频数据,然后会再次向应用请求数据,确认数据填满后返回audio.AudioDataCallbackResult.VALID。 73 > 74 > - 回调函数结束后,音频服务会把缓冲中数据放入队列里等待播放,因此请勿在回调外再次更改缓冲中的数据。对于最后一帧,如果数据不够填满缓冲长度,开发者需要使用剩余数据拼接空数据的方式,将缓冲填满,避免缓冲内的历史脏数据对播放效果产生不良的影响。 75 76 ```ts 77 import { audio } from '@kit.AudioKit'; 78 import { BusinessError } from '@kit.BasicServicesKit'; 79 import { fileIo as fs } from '@kit.CoreFileKit'; 80 import { common } from '@kit.AbilityKit'; 81 82 class Options { 83 offset?: number; 84 length?: number; 85 } 86 87 let bufferSize: number = 0; 88 // 请在组件内获取context,确保this.getUIContext().getHostContext()返回结果为UIAbilityContext。 89 let context = this.getUIContext().getHostContext() as common.UIAbilityContext; 90 let path = context.cacheDir; 91 // 确保该沙箱路径下存在该资源。 92 let filePath = path + '/StarWars10s-2C-48000-4SW.wav'; 93 let file: fs.File = fs.openSync(filePath, fs.OpenMode.READ_ONLY); 94 95 let writeDataCallback = (buffer: ArrayBuffer) => { 96 let options: Options = { 97 offset: bufferSize, 98 length: buffer.byteLength 99 }; 100 101 try { 102 fs.readSync(file.fd, buffer, options); 103 bufferSize += buffer.byteLength; 104 // 系统会判定buffer有效,正常播放。 105 return audio.AudioDataCallbackResult.VALID; 106 } catch (error) { 107 console.error('Error reading file:', error); 108 // 系统会判定buffer无效,不播放。 109 return audio.AudioDataCallbackResult.INVALID; 110 } 111 }; 112 113 audioRenderer.on('writeData', writeDataCallback); 114 ``` 115 116 - API version 11该方法不支持返回回调结果,系统默认回调中的数据均为有效数据。 117 118 > **注意:** 119 > 120 > - 请确保填满回调所需长度数据,否则会导致杂音、卡顿等现象。 121 > 122 > - 在无法填满回调所需长度数据的情况下,建议开发者选择暂时停止写入数据(不暂停音频流),阻塞回调函数,等待数据充足时,再继续写入数据,确保数据填满。在阻塞回调函数后,如需调用AudioRenderer相关接口,需先解阻塞。 123 > 124 > - 开发者如果不希望播放本次回调中的音频数据,可以主动将回调中的数据块置空(置空后,也会被系统统计到已写入的数据,播放静音帧)。 125 > 126 > - 回调函数结束后,音频服务会把缓冲中数据放入队列里等待播放,因此请勿在回调外再次更改缓冲中的数据。对于最后一帧,如果数据不够填满缓冲长度,开发者需要使用剩余数据拼接空数据的方式,将缓冲填满,避免缓冲内的历史脏数据对播放效果产生不良的影响。 127 128 ```ts 129 import { BusinessError } from '@kit.BasicServicesKit'; 130 import { fileIo as fs } from '@kit.CoreFileKit'; 131 import { common } from '@kit.AbilityKit'; 132 133 class Options { 134 offset?: number; 135 length?: number; 136 } 137 138 let bufferSize: number = 0; 139 // 请在组件内获取context,确保this.getUIContext().getHostContext()返回结果为UIAbilityContext。 140 let context = this.getUIContext().getHostContext() as common.UIAbilityContext; 141 let path = context.cacheDir; 142 // 确保该沙箱路径下存在该资源。 143 let filePath = path + '/StarWars10s-2C-48000-4SW.wav'; 144 let file: fs.File = fs.openSync(filePath, fs.OpenMode.READ_ONLY); 145 let writeDataCallback = (buffer: ArrayBuffer) => { 146 // 如果开发者不希望播放某段buffer,可在此处添加判断并对buffer进行置空处理。 147 let options: Options = { 148 offset: bufferSize, 149 length: buffer.byteLength 150 }; 151 fs.readSync(file.fd, buffer, options); 152 bufferSize += buffer.byteLength; 153 }; 154 155 audioRenderer.on('writeData', writeDataCallback); 156 ``` 157 1583. 调用start()方法进入running状态,开始渲染音频。 159 160 ```ts 161 import { BusinessError } from '@kit.BasicServicesKit'; 162 163 audioRenderer.start((err: BusinessError) => { 164 if (err) { 165 console.error(`Renderer start failed, code is ${err.code}, message is ${err.message}`); 166 } else { 167 console.info('Renderer start success.'); 168 } 169 }); 170 ``` 171 1724. 调用stop()方法停止渲染。 173 174 ```ts 175 import { BusinessError } from '@kit.BasicServicesKit'; 176 177 audioRenderer.stop((err: BusinessError) => { 178 if (err) { 179 console.error(`Renderer stop failed, code is ${err.code}, message is ${err.message}`); 180 } else { 181 console.info('Renderer stopped.'); 182 } 183 }); 184 ``` 185 1865. 调用release()方法销毁实例,释放资源。 187 188 ```ts 189 import { BusinessError } from '@kit.BasicServicesKit'; 190 191 audioRenderer.release((err: BusinessError) => { 192 if (err) { 193 console.error(`Renderer release failed, code is ${err.code}, message is ${err.message}`); 194 } else { 195 console.info('Renderer released.'); 196 } 197 }); 198 ``` 199 200### 选择正确的StreamUsage 201 202创建播放器时候,开发者需要根据应用场景指定播放器的`StreamUsage`,选择正确的`StreamUsage`可以避免用户遇到不符合预期的行为。 203 204在音频API文档[StreamUsage](../../reference/apis-audio-kit/js-apis-audio.md#streamusage)介绍中,列举了每一种类型推荐的应用场景。例如音乐场景推荐使用`STREAM_USAGE_MUSIC`,电影或者视频场景推荐使用`STREAM_USAGE_MOVIE`,游戏场景推荐使用`STREAM_USAGE_GAME`,等等。 205 206如果开发者配置了不正确的`StreamUsage`,可能带来一些不符合预期的行为。例如以下场景。 207 208- 游戏场景错误使用`STREAM_USAGE_MUSIC`类型,游戏应用将无法和其他音乐应用并发播放,而游戏场景通常可以与其他音乐应用并发播放。 209- 导航场景错误使用`STREAM_USAGE_MUSIC`类型,导航应用播报时候会导致正在播放的音乐停止播放,而导航场景我们通常期望正在播放的音乐仅仅降低音量播放。 210 211### 配置合适的音频采样率 212 213采样率:指音频每秒单个声道样点数,单位为Hz。 214重采样:根据输入输出音频采样率的差异,进行上采样(通过插值增加样点数)或下采样(通过抽取减少样点数)。 215 216AudioRenderer支持枚举类型AudioSamplingRate中定义的所有采样率。 217若通过AudioRenderer设置的输入音频采样率与设备输出采样率不一致,系统会将输入音频重采样为设备输出采样率。 218 219若为减少重采样功耗,可使用采样率与输出设备采样率一致的输入音频。推荐使用48k采样率。 220 221### 完整示例 222 223下面展示了使用AudioRenderer渲染音频文件的示例代码。 224 225```ts 226import { audio } from '@kit.AudioKit'; 227import { BusinessError } from '@kit.BasicServicesKit'; 228import { fileIo as fs } from '@kit.CoreFileKit'; 229import { common } from '@kit.AbilityKit'; 230 231const TAG = 'AudioRendererDemo'; 232 233class Options { 234 offset?: number; 235 length?: number; 236} 237 238let bufferSize: number = 0; 239let renderModel: audio.AudioRenderer | undefined = undefined; 240let audioStreamInfo: audio.AudioStreamInfo = { 241 samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000, // 采样率。 242 channels: audio.AudioChannel.CHANNEL_2, // 通道。 243 sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, // 采样格式。 244 encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW // 编码格式。 245}; 246let audioRendererInfo: audio.AudioRendererInfo = { 247 usage: audio.StreamUsage.STREAM_USAGE_MUSIC, // 音频流使用类型:音乐。根据业务场景配置,参考StreamUsage。 248 rendererFlags: 0 // 音频渲染器标志。 249}; 250let audioRendererOptions: audio.AudioRendererOptions = { 251 streamInfo: audioStreamInfo, 252 rendererInfo: audioRendererInfo 253}; 254// 请在组件内获取context,确保this.getUIContext().getHostContext()返回结果为UIAbilityContext。 255let context = this.getUIContext().getHostContext() as common.UIAbilityContext; 256let path = context.cacheDir; 257// 确保该沙箱路径下存在该资源。 258let filePath = path + '/StarWars10s-2C-48000-4SW.wav'; 259let file: fs.File = fs.openSync(filePath, fs.OpenMode.READ_ONLY); 260let writeDataCallback = (buffer: ArrayBuffer) => { 261 let options: Options = { 262 offset: bufferSize, 263 length: buffer.byteLength 264 }; 265 266 try { 267 fs.readSync(file.fd, buffer, options); 268 bufferSize += buffer.byteLength; 269 // API version 11 不支持返回回调结果,从 API version 12 开始支持返回回调结果。 270 return audio.AudioDataCallbackResult.VALID; 271 } catch (error) { 272 console.error('Error reading file:', error); 273 // API version 11 不支持返回回调结果,从 API version 12 开始支持返回回调结果。 274 return audio.AudioDataCallbackResult.INVALID; 275 } 276}; 277 278// 初始化,创建实例,设置监听事件。 279function init() { 280 audio.createAudioRenderer(audioRendererOptions, (err, renderer) => { // 创建AudioRenderer实例。 281 if (!err) { 282 console.info(`${TAG}: creating AudioRenderer success`); 283 renderModel = renderer; 284 if (renderModel !== undefined) { 285 (renderModel as audio.AudioRenderer).on('writeData', writeDataCallback); 286 } 287 } else { 288 console.info(`${TAG}: creating AudioRenderer failed, error: ${err.message}`); 289 } 290 }); 291} 292 293// 开始一次音频渲染。 294function start() { 295 if (renderModel !== undefined) { 296 let stateGroup = [audio.AudioState.STATE_PREPARED, audio.AudioState.STATE_PAUSED, audio.AudioState.STATE_STOPPED]; 297 if (stateGroup.indexOf((renderModel as audio.AudioRenderer).state.valueOf()) === -1) { // 当且仅当状态为prepared、paused和stopped之一时才能启动渲染。 298 console.error(TAG + 'start failed'); 299 return; 300 } 301 // 启动渲染。 302 (renderModel as audio.AudioRenderer).start((err: BusinessError) => { 303 if (err) { 304 console.error('Renderer start failed.'); 305 } else { 306 console.info('Renderer start success.'); 307 } 308 }); 309 } 310} 311 312// 暂停渲染。 313function pause() { 314 if (renderModel !== undefined) { 315 // 只有渲染器状态为running的时候才能暂停。 316 if ((renderModel as audio.AudioRenderer).state.valueOf() !== audio.AudioState.STATE_RUNNING) { 317 console.info('Renderer is not running'); 318 return; 319 } 320 // 暂停渲染。 321 (renderModel as audio.AudioRenderer).pause((err: BusinessError) => { 322 if (err) { 323 console.error('Renderer pause failed.'); 324 } else { 325 console.info('Renderer pause success.'); 326 } 327 }); 328 } 329} 330 331// 停止渲染。 332async function stop() { 333 if (renderModel !== undefined) { 334 // 只有渲染器状态为running或paused的时候才可以停止。 335 if ((renderModel as audio.AudioRenderer).state.valueOf() !== audio.AudioState.STATE_RUNNING && (renderModel as audio.AudioRenderer).state.valueOf() !== audio.AudioState.STATE_PAUSED) { 336 console.info('Renderer is not running or paused.'); 337 return; 338 } 339 // 停止渲染。 340 (renderModel as audio.AudioRenderer).stop((err: BusinessError) => { 341 if (err) { 342 console.error('Renderer stop failed.'); 343 } else { 344 fs.close(file); 345 console.info('Renderer stop success.'); 346 } 347 }); 348 } 349} 350 351// 销毁实例,释放资源。 352async function release() { 353 if (renderModel !== undefined) { 354 // 渲染器状态不是released状态,才能release。 355 if (renderModel.state.valueOf() === audio.AudioState.STATE_RELEASED) { 356 console.info('Renderer already released'); 357 return; 358 } 359 // 释放资源。 360 (renderModel as audio.AudioRenderer).release((err: BusinessError) => { 361 if (err) { 362 console.error('Renderer release failed.'); 363 } else { 364 console.info('Renderer release success.'); 365 } 366 }); 367 } 368} 369``` 370 371当同优先级或高优先级音频流要使用输出设备时,当前音频流会被中断,应用可以自行响应中断事件并做出处理。具体的音频并发处理方式可参考[处理音频焦点事件](audio-playback-concurrency.md)。 372