• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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![audio-renderer-state](figures/audio-renderer-state.png)
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     ```