1# 音频渲染开发指导 2 3## 简介 4 5AudioRenderer提供了渲染音频文件和控制播放的接口,开发者可以通过本指导,了解如何在输出设备中播放音频文件并管理播放任务。同时,AudioRenderer支持音频中断的功能。 6开发者在调用AudioRenderer提供的各个接口时,需要理解以下名词: 7 8- **音频中断**:当优先级较高的音频流需要播放时,AudioRenderer会中断优先级较低的流。例如,当用户在收听音乐时有来电,则优先级较低音乐播放将被暂停。 9- **状态检查**:在进行应用开发的过程中,建议开发者通过on('stateChange')方法订阅AudioRenderer的状态变更。因为针对AudioRenderer的某些操作,仅在音频播放器在固定状态时才能执行。如果应用在音频播放器处于错误状态时执行操作,系统可能会抛出异常或生成其他未定义的行为。 10- **异步操作**:为保证UI线程不被阻塞,大部分AudioRenderer调用都是异步的。对于每个API均提供了callback函数和Promise函数,以下示例均采用Promise函数,更多方式可参考[音频管理API文档AudioRenderer](../reference/apis/js-apis-audio.md#audiorenderer8)。 11- **焦点模式**:OpenHarmony中有两种焦点模式:**共享焦点模式**和**独立焦点模式**。其中,共享焦点模式是指,同一个应用创建的所有AudioRenderer对象共享一个焦点对象,应用内部无焦点转移,因此无法触发回调通知;独立焦点模式与之相反,即同一个应用创建的每个AudioRenderer对象都拥有独立的焦点对象,会发生焦点抢占,当应用内部发生焦点抢占,将会发生焦点转移,原本拥有焦点的AudioRenderer对象会获取到相关的回调通知。需要注意的是,默认情况下,应用创建的都是共享焦点,开发者可以调用setInterruptMode()来设置创建的焦点模式,完整示例请参考开发指导14。 12 13## 运作机制 14 15该模块提供了音频渲染模块的状态变化示意 16 17**图1** 音频渲染状态示意图 18 19 20 21**PREPARED状态:** 通过调用create()方法进入到该状态。<br> 22**RUNNING状态:** 正在进行音频数据播放,可以在prepared状态通过调用start()方法进入此状态,也可以在pause状态和stopped状态通过调用start()方法进入此状态。<br> 23**PAUSED状态:** 在running状态可以通过pause()方法暂停音频数据的播放,暂停播放之后可以通过调用start()方法继续音频数据播放。<br> 24**STOPPED状态:** 在paused状态可以通过调用stop()方法停止音频数据的播放,在running状态可以通过stop()方法停止音频数据的播放。<br> 25**RELEASED状态:** 在prepared、paused、stop等状态,用户均可通过release()方法释放掉所有占用的硬件和软件资源,并且不会再进入到其他的任何一种状态了。<br> 26 27## 开发指导 28 29详细API含义可参考:[音频管理API文档AudioRenderer](../reference/apis/js-apis-audio.md#audiorenderer8) 30 311. 使用createAudioRenderer()创建一个全局的AudioRenderer实例,以便后续步骤使用。 32 在audioRendererOptions中设置相关参数。该实例可用于音频渲染、控制和获取渲染状态,以及注册通知回调。 33 34 ```js 35 import audio from '@ohos.multimedia.audio'; 36 import fs from '@ohos.file.fs'; 37 38 //音频渲染相关接口自测试 39 @Entry 40 @Component 41 struct AudioRenderer1129 { 42 private audioRenderer: audio.AudioRenderer; 43 private bufferSize;//便于步骤3 write函数调用使用 44 private audioRenderer1: audio.AudioRenderer; //便于步骤14 完整示例调用使用 45 private audioRenderer2: audio.AudioRenderer; //便于步骤14 完整示例调用使用 46 47 async initAudioRender(){ 48 let audioStreamInfo = { 49 samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100, 50 channels: audio.AudioChannel.CHANNEL_1, 51 sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, 52 encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW 53 } 54 let audioRendererInfo = { 55 content: audio.ContentType.CONTENT_TYPE_SPEECH, 56 usage: audio.StreamUsage.STREAM_USAGE_VOICE_COMMUNICATION, 57 rendererFlags: 0 // 0是音频渲染器的扩展标志位,默认为0 58 } 59 let audioRendererOptions = { 60 streamInfo: audioStreamInfo, 61 rendererInfo: audioRendererInfo 62 } 63 this.audioRenderer = await audio.createAudioRenderer(audioRendererOptions); 64 console.log("Create audio renderer success."); 65 } 66 } 67 ``` 68 692. 调用start()方法来启动/恢复播放任务。 70 71 ```js 72 async startRenderer() { 73 let state = this.audioRenderer.state; 74 // Renderer start时的状态应该是STATE_PREPARED、STATE_PAUSED和STATE_STOPPED之一. 75 if (state != audio.AudioState.STATE_PREPARED && state != audio.AudioState.STATE_PAUSED && 76 state != audio.AudioState.STATE_STOPPED) { 77 console.info('Renderer is not in a correct state to start'); 78 return; 79 } 80 81 await this.audioRenderer.start(); 82 83 state = this.audioRenderer.state; 84 if (state == audio.AudioState.STATE_RUNNING) { 85 console.info('Renderer started'); 86 } else { 87 console.error('Renderer start failed'); 88 } 89 } 90 ``` 91 启动完成后,渲染器状态将变更为STATE_RUNNING,然后应用可以开始读取缓冲区。 92 93 943. 调用write()方法向缓冲区写入数据。 95 96 将需要播放的音频数据读入缓冲区,重复调用write()方法写入。请注意引入“import fs from '@ohos.file.fs';”,具体请参考步骤1。 97 98 ```js 99 async writeData(){ 100 // 此处是渲染器的合理的最小缓冲区大小(也可以选择其它大小的缓冲区) 101 this.bufferSize = await this.audioRenderer.getBufferSize(); 102 let dir = globalThis.fileDir; //不可直接访问,没权限,切记!!!一定要使用沙箱路径 103 const filePath = dir + '/file_example_WAV_2MG.wav'; // 需要渲染的音乐文件 实际路径为:/data/storage/el2/base/haps/entry/files/file_example_WAV_2MG.wav 104 console.info(`file filePath: ${ filePath}`); 105 106 let file = fs.openSync(filePath, fs.OpenMode.READ_ONLY); 107 let stat = await fs.stat(filePath); //音乐文件信息 108 let buf = new ArrayBuffer(this.bufferSize); 109 let len = stat.size % this.bufferSize == 0 ? Math.floor(stat.size / this.bufferSize) : Math.floor(stat.size / this.bufferSize + 1); 110 for (let i = 0;i < len; i++) { 111 let options = { 112 offset: i * this.bufferSize, 113 length: this.bufferSize 114 } 115 let readsize = await fs.read(file.fd, buf, options) 116 let writeSize = await new Promise((resolve,reject)=>{ 117 this.audioRenderer.write(buf,(err,writeSize)=>{ 118 if(err){ 119 reject(err) 120 }else{ 121 resolve(writeSize) 122 } 123 }) 124 }) 125 } 126 127 fs.close(file) 128 await this.audioRenderer.stop(); //停止渲染 129 await this.audioRenderer.release(); //释放资源 130 } 131 ``` 132 1334. (可选)调用pause()方法或stop()方法暂停/停止渲染音频数据。 134 135 ```js 136 async pauseRenderer() { 137 let state = this.audioRenderer.state; 138 // 只有渲染器状态为STATE_RUNNING的时候才能暂停 139 if (state != audio.AudioState.STATE_RUNNING) { 140 console.info('Renderer is not running'); 141 return; 142 } 143 144 await this.audioRenderer.pause(); 145 146 state = this.audioRenderer.state; 147 if (state == audio.AudioState.STATE_PAUSED) { 148 console.info('Renderer paused'); 149 } else { 150 console.error('Renderer pause failed'); 151 } 152 } 153 154 async stopRenderer() { 155 let state = this.audioRenderer.state; 156 // 只有渲染器状态为STATE_RUNNING或STATE_PAUSED的时候才可以停止 157 if (state != audio.AudioState.STATE_RUNNING && state != audio.AudioState.STATE_PAUSED) { 158 console.info('Renderer is not running or paused'); 159 return; 160 } 161 162 await this.audioRenderer.stop(); 163 164 state = this.audioRenderer.state; 165 if (state == audio.AudioState.STATE_STOPPED) { 166 console.info('Renderer stopped'); 167 } else { 168 console.error('Renderer stop failed'); 169 } 170 } 171 ``` 172 1735. (可选)调用drain()方法清空缓冲区。 174 175 ```js 176 async drainRenderer() { 177 let state = this.audioRenderer.state; 178 // 只有渲染器状态为STATE_RUNNING的时候才能使用drain() 179 if (state != audio.AudioState.STATE_RUNNING) { 180 console.info('Renderer is not running'); 181 return; 182 } 183 184 await this.audioRenderer.drain(); 185 state = this.audioRenderer.state; 186 } 187 ``` 188 1896. 任务完成,调用release()方法释放相关资源。 190 191 AudioRenderer会使用大量的系统资源,所以请确保完成相关任务后,进行资源释放。 192 193 ```js 194 async releaseRenderer() { 195 let state = this.audioRenderer.state; 196 // 渲染器状态不是STATE_RELEASED或STATE_NEW状态,才能release 197 if (state == audio.AudioState.STATE_RELEASED || state == audio.AudioState.STATE_NEW) { 198 console.info('Renderer already released'); 199 return; 200 } 201 await this.audioRenderer.release(); 202 203 state = this.audioRenderer.state; 204 if (state == audio.AudioState.STATE_RELEASED) { 205 console.info('Renderer released'); 206 } else { 207 console.info('Renderer release failed'); 208 } 209 } 210 ``` 211 2127. (可选)获取渲染器相关信息 213 214 通过以下代码,可以获取渲染器的相关信息。 215 216 ```js 217 async getRenderInfo(){ 218 // 获取当前渲染器状态 219 let state = this.audioRenderer.state; 220 // 获取渲染器信息 221 let audioRendererInfo : audio.AudioRendererInfo = await this.audioRenderer.getRendererInfo(); 222 // 获取音频流信息 223 let audioStreamInfo : audio.AudioStreamInfo = await this.audioRenderer.getStreamInfo(); 224 // 获取音频流ID 225 let audioStreamId : number = await this.audioRenderer.getAudioStreamId(); 226 // 获取纳秒形式的Unix时间戳 227 let audioTime : number = await this.audioRenderer.getAudioTime(); 228 // 获取合理的最小缓冲区大小 229 let bufferSize : number = await this.audioRenderer.getBufferSize(); 230 // 获取渲染速率 231 let renderRate : audio.AudioRendererRate = await this.audioRenderer.getRenderRate(); 232 } 233 ``` 234 2358. (可选)设置渲染器相关信息 236 237 通过以下代码,可以设置渲染器的相关信息。 238 239 ```js 240 async setAudioRenderInfo(){ 241 // 设置渲染速率为正常速度 242 let renderRate : audio.AudioRendererRate = audio.AudioRendererRate.RENDER_RATE_NORMAL; 243 await this.audioRenderer.setRenderRate(renderRate); 244 // 设置渲染器音频中断模式为SHARE_MODE 245 let interruptMode : audio.InterruptMode = audio.InterruptMode.SHARE_MODE; 246 await this.audioRenderer.setInterruptMode(interruptMode); 247 // 设置一个流的音量为0.5 248 let volume : number = 0.5; 249 await this.audioRenderer.setVolume(volume); 250 } 251 ``` 252 2539. (可选)使用on('audioInterrupt')方法订阅渲染器音频中断事件,使用off('audioInterrupt')取消订阅事件。 254 255 当优先级更高或相等的Stream-B请求激活并使用输出设备时,Stream-A被中断。 256 257 在某些情况下,框架会采取暂停播放、降低音量等强制操作,并通过InterruptEvent通知应用。在其他情况下,应用可以自行对InterruptEvent做出响应。 258 259 在音频中断的情况下,应用可能会碰到音频数据写入失败的问题。所以建议不感知、不处理中断的应用在写入音频数据前,使用audioRenderer.state检查播放器状态。而订阅音频中断事件,可以获取到更多详细信息,具体可参考[InterruptEvent](../reference/apis/js-apis-audio.md#interruptevent9)。 260 261 需要说明的是,本模块的订阅音频中断事件与[AudioManager](../reference/apis/js-apis-audio.md#audiomanager)模块中的on('interrupt')稍有不同。自api9以来,on('interrupt')和off('interrupt')均被废弃。在AudioRenderer模块,当开发者需要监听焦点变化事件时,只需要调用on('audioInterrupt')函数,当应用内部的AudioRenderer对象在start\stop\pause等动作发生时,会主动请求焦点,从而发生焦点转移,相关的AudioRenderer对象即可获取到对应的回调信息。但对除AudioRenderer的其他对象,例如FM、语音唤醒等,应用不会创建对象,此时可调用AudioManager中的on('interrupt')获取焦点变化通知。 262 263 ```js 264 async subscribeAudioRender(){ 265 this.audioRenderer.on('audioInterrupt', (interruptEvent) => { 266 console.info('InterruptEvent Received'); 267 console.info(`InterruptType: ${interruptEvent.eventType}`); 268 console.info(`InterruptForceType: ${interruptEvent.forceType}`); 269 console.info(`AInterruptHint: ${interruptEvent.hintType}`); 270 271 if (interruptEvent.forceType == audio.InterruptForceType.INTERRUPT_FORCE) { 272 switch (interruptEvent.hintType) { 273 // 音频框架发起的强制暂停操作,为防止数据丢失,此时应该停止数据的写操作 274 case audio.InterruptHint.INTERRUPT_HINT_PAUSE: 275 console.info('isPlay is false'); 276 break; 277 // 音频框架发起的强制停止操作,为防止数据丢失,此时应该停止数据的写操作 278 case audio.InterruptHint.INTERRUPT_HINT_STOP: 279 console.info('isPlay is false'); 280 break; 281 // 音频框架发起的强制降低音量操作 282 case audio.InterruptHint.INTERRUPT_HINT_DUCK: 283 break; 284 // 音频框架发起的恢复音量操作 285 case audio.InterruptHint.INTERRUPT_HINT_UNDUCK: 286 break; 287 } 288 } else if (interruptEvent.forceType == audio.InterruptForceType.INTERRUPT_SHARE) { 289 switch (interruptEvent.hintType) { 290 // 提醒App开始渲染 291 case audio.InterruptHint.INTERRUPT_HINT_RESUME: 292 this.startRenderer(); 293 break; 294 // 提醒App音频流被中断,由App自主决定是否继续(此处选择暂停) 295 case audio.InterruptHint.INTERRUPT_HINT_PAUSE: 296 console.info('isPlay is false'); 297 this.pauseRenderer(); 298 break; 299 } 300 } 301 }); 302 } 303 ``` 304 30510. (可选)使用on('markReach')方法订阅渲染器标记到达事件,使用off('markReach')取消订阅事件。 306 307 注册markReach监听后,当渲染器渲染的帧数到达设定值时,会触发回调并返回设定的值。 308 309 ```js 310 async markReach(){ 311 this.audioRenderer.on('markReach', 50, (position) => { 312 if (position == 50) { 313 console.info('ON Triggered successfully'); 314 } 315 }); 316 this.audioRenderer.off('markReach'); // 取消markReach事件的订阅,后续将无法监听到“标记到达”事件 317 } 318 ``` 319 32011. (可选)使用on('periodReach')方法订阅渲染器区间标记到达事件,使用off('periodReach')取消订阅事件。 321 322 注册periodReach监听后,**每当**渲染器渲染的帧数到达设定值时,会触发回调并返回设定的值。 323 324 ```js 325 async periodReach(){ 326 this.audioRenderer.on('periodReach',10, (reachNumber) => { 327 console.info(`In this period, the renderer reached frame: ${reachNumber} `); 328 }); 329 330 this.audioRenderer.off('periodReach'); // 取消periodReach事件的订阅,后续将无法监听到“区间标记到达”事件 331 } 332 ``` 333 33412. (可选)使用on('stateChange')方法订阅渲染器音频状态变化事件。 335 336 注册stateChange监听后,当渲染器的状态发生改变时,会触发回调并返回当前渲染器的状态。 337 338 ```js 339 async stateChange(){ 340 this.audioRenderer.on('stateChange', (audioState) => { 341 console.info('State change event Received'); 342 console.info(`Current renderer state is: ${audioState}`); 343 }); 344 } 345 ``` 346 34713. (可选)对on()方法的异常处理。 348 349 在使用on()方法时,如果传入的字符串错误或传入的参数类型错误,程序会抛出异常,需要用try catch来捕获。 350 351 ```js 352 async errorCall(){ 353 try { 354 this.audioRenderer.on('invalidInput', () => { // 字符串不匹配 355 }) 356 } catch (err) { 357 console.info(`Call on function error, ${err}`); // 程序抛出401异常 358 } 359 try { 360 this.audioRenderer.on(1, () => { // 入参类型错误 361 }) 362 } catch (err) { 363 console.info(`Call on function error, ${err}`); // 程序抛出6800101异常 364 } 365 } 366 ``` 367 36814. (可选)on('audioInterrupt')方法完整示例。 369 请注意:在调用前声明audioRenderer1与audioRenderer2对象,具体请参考步骤1。 370 同一个应用中的AudioRender1和AudioRender2在创建时均设置了焦点模式为独立,并且调用on('audioInterrupt')监听焦点变化。刚开始AudioRender1拥有焦点,当AudioRender2获取到焦点时,audioRenderer1将收到焦点转移的通知,打印相关日志。如果AudioRender1和AudioRender2不将焦点模式设置为独立,则监听处理中的日志在应用运行过程中永远不会被打印。 371 ```js 372 async runningAudioRender1(){ 373 let audioStreamInfo = { 374 samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000, 375 channels: audio.AudioChannel.CHANNEL_1, 376 sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S32LE, 377 encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW 378 } 379 let audioRendererInfo = { 380 content: audio.ContentType.CONTENT_TYPE_MUSIC, 381 usage: audio.StreamUsage.STREAM_USAGE_MEDIA, 382 rendererFlags: 0 // 0是音频渲染器的扩展标志位,默认为0 383 } 384 let audioRendererOptions = { 385 streamInfo: audioStreamInfo, 386 rendererInfo: audioRendererInfo 387 } 388 389 //1.1 创建对象 390 this.audioRenderer1 = await audio.createAudioRenderer(audioRendererOptions); 391 console.info("Create audio renderer 1 success."); 392 393 //1.2 设置焦点模式为独立模式 :1 394 this.audioRenderer1.setInterruptMode(1).then( data => { 395 console.info('audioRenderer1 setInterruptMode Success!'); 396 }).catch((err) => { 397 console.error(`audioRenderer1 setInterruptMode Fail: ${err}`); 398 }); 399 400 //1.3 设置监听 401 this.audioRenderer1.on('audioInterrupt', async(interruptEvent) => { 402 console.info(`audioRenderer1 on audioInterrupt : ${JSON.stringify(interruptEvent)}`) 403 }); 404 405 //1.4 启动渲染 406 await this.audioRenderer1.start(); 407 console.info('startAudioRender1 success'); 408 409 //1.5 获取缓存区大小,此处是渲染器的合理的最小缓冲区大小(也可以选择其它大小的缓冲区) 410 const bufferSize = await this.audioRenderer1.getBufferSize(); 411 console.info(`audio bufferSize: ${bufferSize}`); 412 413 //1.6 获取原始音频数据文件 414 let dir = globalThis.fileDir; //不可直接访问,没权限,切记!!!一定要使用沙箱路径 415 const path1 = dir + '/music001_48000_32_1.wav'; // 需要渲染的音乐文件 实际路径为:/data/storage/el2/base/haps/entry/files/music001_48000_32_1.wav 416 console.info(`audioRender1 file path: ${ path1}`); 417 let file1 = fs.openSync(path1, fs.OpenMode.READ_ONLY); 418 let stat = await fs.stat(path1); //音乐文件信息 419 let buf = new ArrayBuffer(bufferSize); 420 let len = stat.size % this.bufferSize == 0 ? Math.floor(stat.size / this.bufferSize) : Math.floor(stat.size / this.bufferSize + 1); 421 422 //1.7 通过audioRender对缓存区的原始音频数据进行渲染 423 for (let i = 0;i < len; i++) { 424 let options = { 425 offset: i * this.bufferSize, 426 length: this.bufferSize 427 } 428 let readsize = await fs.read(file1.fd, buf, options) 429 let writeSize = await new Promise((resolve,reject)=>{ 430 this.audioRenderer1.write(buf,(err,writeSize)=>{ 431 if(err){ 432 reject(err) 433 }else{ 434 resolve(writeSize) 435 } 436 }) 437 }) 438 } 439 fs.close(file1) 440 await this.audioRenderer1.stop(); //停止渲染 441 await this.audioRenderer1.release(); //释放资源 442 } 443 444 async runningAudioRender2(){ 445 let audioStreamInfo = { 446 samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000, 447 channels: audio.AudioChannel.CHANNEL_1, 448 sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S32LE, 449 encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW 450 } 451 let audioRendererInfo = { 452 content: audio.ContentType.CONTENT_TYPE_MUSIC, 453 usage: audio.StreamUsage.STREAM_USAGE_MEDIA, 454 rendererFlags: 0 // 0是音频渲染器的扩展标志位,默认为0 455 } 456 let audioRendererOptions = { 457 streamInfo: audioStreamInfo, 458 rendererInfo: audioRendererInfo 459 } 460 461 //2.1 创建对象 462 this.audioRenderer2 = await audio.createAudioRenderer(audioRendererOptions); 463 console.info("Create audio renderer 2 success."); 464 465 //2.2 设置焦点模式为独立模式 :1 466 this.audioRenderer2.setInterruptMode(1).then( data => { 467 console.info('audioRenderer2 setInterruptMode Success!'); 468 }).catch((err) => { 469 console.error(`audioRenderer2 setInterruptMode Fail: ${err}`); 470 }); 471 472 //2.3 设置监听 473 this.audioRenderer2.on('audioInterrupt', async(interruptEvent) => { 474 console.info(`audioRenderer2 on audioInterrupt : ${JSON.stringify(interruptEvent)}`) 475 }); 476 477 //2.4 启动渲染 478 await this.audioRenderer2.start(); 479 console.info('startAudioRender2 success'); 480 481 //2.5 获取缓存区大小 482 const bufferSize = await this.audioRenderer2.getBufferSize(); 483 console.info(`audio bufferSize: ${bufferSize}`); 484 485 //2.6 获取原始音频数据文件 486 let dir = globalThis.fileDir; //不可直接访问,没权限,切记!!!一定要使用沙箱路径 487 const path2 = dir + '/music002_48000_32_1.wav'; // 需要渲染的音乐文件 实际路径为:/data/storage/el2/base/haps/entry/files/music002_48000_32_1.wav 488 console.info(`audioRender2 file path: ${ path2}`); 489 let file2 = fs.openSync(path2, fs.OpenMode.READ_ONLY); 490 let stat = await fs.stat(path2); //音乐文件信息 491 let buf = new ArrayBuffer(bufferSize); 492 let len = stat.size % this.bufferSize == 0 ? Math.floor(stat.size / this.bufferSize) : Math.floor(stat.size / this.bufferSize + 1); 493 494 //2.7 通过audioRender对缓存区的原始音频数据进行渲染 495 for (let i = 0;i < len; i++) { 496 let options = { 497 offset: i * this.bufferSize, 498 length: this.bufferSize 499 } 500 let readsize = await fs.read(file2.fd, buf, options) 501 let writeSize = await new Promise((resolve,reject)=>{ 502 this.audioRenderer2.write(buf,(err,writeSize)=>{ 503 if(err){ 504 reject(err) 505 }else{ 506 resolve(writeSize) 507 } 508 }) 509 }) 510 } 511 fs.close(file2) 512 await this.audioRenderer2.stop(); //停止渲染 513 await this.audioRenderer2.release(); //释放资源 514 } 515 516 //综合调用入口 517 async test(){ 518 await this.runningAudioRender1(); 519 await this.runningAudioRender2(); 520 } 521 522 ```