当前位置:   article > 正文

OpenHarmony音频渲染开发_openharmony 异步服务开发

openharmony 异步服务开发

1.OpenHarmony音频渲染开发简介

        AudioRenderer提供了渲染音频文件和控制播放的接口,开发者可以通过本博文,了解如何在输出设备中播放音频文件并管理播放任务。同时,AudioRenderer支持音频中断的功能。

        (1).音频中断:当优先级较高的音频流需要播放时,AudioRenderer会中断优先级较低的流。例如,当用户在收听音乐时有来电,则优先级较低音乐播放将被暂停。

        (2).状态检查:在进行应用开发的过程中,建议开发者通过on('stateChange')方法订阅AudioRenderer的状态变更。因为针对AudioRenderer的某些操作,仅在音频播放器在固定状态时才能执行。如果应用在音频播放器处于错误状态时执行操作,系统可能会抛出异常或生成其他未定义的行为。

        (3).异步操作:为保证UI线程不被阻塞,大部分AudioRenderer调用都是异步的。对于每个API均提供了callback函数和Promise函数,以下示例均采用Promise函数,更多方式可参考音频管理API文档AudioRenderer。

        (4).焦点模式:OpenHarmony中有两种焦点模式:共享焦点模式和独立焦点模式。其中,共享焦点模式是指,同一个应用创建的所有AudioRenderer对象共享一个焦点对象,应用内部无焦点转移,因此无法触发回调通知;独立焦点模式与之相反,即同一个应用创建的每个AudioRenderer对象都拥有独立的焦点对象,会发生焦点抢占,当应用内部发生焦点抢占,将会发生焦点转移,原本拥有焦点的AudioRenderer对象会获取到相关的回调通知。需要注意的是,默认情况下,应用创建的都是共享焦点,开发者可以调用setInterruptMode()来设置创建的焦点模式。

2.OpenHarmony音频渲染运作机制

        该模块提供了音频渲染模块的状态变化示意

图1 音频渲染状态示意图

1).PREPARED状态: 通过调用create()方法进入到该状态。

2).RUNNING状态: 正在进行音频数据播放,可以在prepared状态通过调用start()方法进入此状态,也可以在pause状态和stopped状态通过调用start()方法进入此状态。

3).PAUSED状态: 在running状态可以通过pause()方法暂停音频数据的播放,暂停播放之后可以通过调用start()方法继续音频数据播放。

4).STOPPED状态: 在paused状态可以通过调用stop()方法停止音频数据的播放,在running状态可以通过stop()方法停止音频数据的播放。

5).RELEASED状态: 在prepared、paused、stop等状态,用户均可通过release()方法释放掉所有占用的硬件和软件资源,并且不会再进入到其他的任何一种状态了。

3.OpenHarmony音频渲染开发指导

详细API含义可参考:

​​​​​​音频管理API文档AudioRenderer

(1).使用createAudioRenderer()创建一个AudioRenderer实例。 在audioRendererOptions中设置相关参数。该实例可用于音频渲染、控制和获取渲染状态,以及注册通知回调。

  1. import audio from '@ohos.multimedia.audio';
  2. let audioStreamInfo = {
  3. samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100,
  4. channels: audio.AudioChannel.CHANNEL_1,
  5. sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
  6. encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
  7. }
  8. let audioRendererInfo = {
  9. content: audio.ContentType.CONTENT_TYPE_SPEECH,
  10. usage: audio.StreamUsage.STREAM_USAGE_VOICE_COMMUNICATION,
  11. rendererFlags: 0 // 0是音频渲染器的扩展标志位,默认为0
  12. }
  13. let audioRendererOptions = {
  14. streamInfo: audioStreamInfo,
  15. rendererInfo: audioRendererInfo
  16. }
  17. let audioRenderer = await audio.createAudioRenderer(audioRendererOptions);
  18. console.log("Create audio renderer success.");

(2).调用start()方法来启动/恢复播放任务。

  1. async function startRenderer() {
  2. let state = audioRenderer.state;
  3. // Renderer start时的状态应该是STATE_PREPARED、STATE_PAUSED和STATE_STOPPED之一.
  4. if (state != audio.AudioState.STATE_PREPARED && state != audio.AudioState.STATE_PAUSED &&
  5. state != audio.AudioState.STATE_STOPPED) {
  6. console.info('Renderer is not in a correct state to start');
  7. return;
  8. }
  9. await audioRenderer.start();
  10. state = audioRenderer.state;
  11. if (state == audio.AudioState.STATE_RUNNING) {
  12. console.info('Renderer started');
  13. } else {
  14. console.error('Renderer start failed');
  15. }
  16. }

         启动完成后,渲染器状态将变更为STATE_RUNNING,然后应用可以开始读取缓冲区。

(3).调用write()方法向缓冲区写入数据。

        将需要播放的音频数据读入缓冲区,重复调用write()方法写入。

  1. import fs from '@ohos.file.fs';
  2. import audio from '@ohos.multimedia.audio';
  3. async function writeBuffer(buf) {
  4. // 写入数据时,渲染器的状态必须为STATE_RUNNING
  5. if (audioRenderer.state != audio.AudioState.STATE_RUNNING) {
  6. console.error('Renderer is not running, do not write');
  7. return;
  8. }
  9. let writtenbytes = await audioRenderer.write(buf);
  10. console.info(`Actual written bytes: ${writtenbytes} `);
  11. if (writtenbytes < 0) {
  12. console.error('Write buffer failed. check the state of renderer');
  13. }
  14. }
  15. // 此处是渲染器的合理的最小缓冲区大小(也可以选择其它大小的缓冲区)
  16. const bufferSize = await audioRenderer.getBufferSize();
  17. let dir = globalThis.fileDir; //不可直接访问,没权限,切记!!!一定要使用沙箱路径
  18. const filePath = dir + '/file_example_WAV_2MG.wav'; // 需要渲染的音乐文件 实际路径为:/data/storage/el2/base/haps/entry/files/file_example_WAV_2MG.wav
  19. console.info(`file filePath: ${ filePath}`);
  20. let file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
  21. let stat = await fs.stat(filePath); //音乐文件信息
  22. let buf = new ArrayBuffer(bufferSize);
  23. let len = stat.size % this.bufferSize == 0 ? Math.floor(stat.size / this.bufferSize) : Math.floor(stat.size / this.bufferSize + 1);
  24. for (let i = 0;i < len; i++) {
  25. let options = {
  26. offset: i * this.bufferSize,
  27. length: this.bufferSize
  28. }
  29. let readsize = await fs.read(file.fd, buf, options)
  30. let writeSize = await new Promise((resolve,reject)=>{
  31. this.audioRenderer.write(buf,(err,writeSize)=>{
  32. if(err){
  33. reject(err)
  34. }else{
  35. resolve(writeSize)
  36. }
  37. })
  38. })
  39. }
  40. fs.close(file)
  41. await audioRenderer.stop(); //停止渲染
  42. await audioRenderer.release(); //释放资源

(4).调用pause()方法或stop()方法暂停/停止渲染音频数据。

  1. async function pauseRenderer() {
  2. let state = audioRenderer.state;
  3. // 只有渲染器状态为STATE_RUNNING的时候才能暂停
  4. if (state != audio.AudioState.STATE_RUNNING) {
  5. console.info('Renderer is not running');
  6. return;
  7. }
  8. await audioRenderer.pause();
  9. state = audioRenderer.state;
  10. if (state == audio.AudioState.STATE_PAUSED) {
  11. console.info('Renderer paused');
  12. } else {
  13. console.error('Renderer pause failed');
  14. }
  15. }
  16. async function stopRenderer() {
  17. let state = audioRenderer.state;
  18. // 只有渲染器状态为STATE_RUNNING或STATE_PAUSED的时候才可以停止
  19. if (state != audio.AudioState.STATE_RUNNING && state != audio.AudioState.STATE_PAUSED) {
  20. console.info('Renderer is not running or paused');
  21. return;
  22. }
  23. await audioRenderer.stop();
  24. state = audioRenderer.state;
  25. if (state == audio.AudioState.STATE_STOPPED) {
  26. console.info('Renderer stopped');
  27. } else {
  28. console.error('Renderer stop failed');
  29. }
  30. }

(5).调用drain()方法清空缓冲区。

  1. async function drainRenderer() {
  2. let state = audioRenderer.state;
  3. // 只有渲染器状态为STATE_RUNNING的时候才能使用drain()
  4. if (state != audio.AudioState.STATE_RUNNING) {
  5. console.info('Renderer is not running');
  6. return;
  7. }
  8. await audioRenderer.drain();
  9. state = audioRenderer.state;
  10. }

(6).任务完成,调用release()方法释放相关资源。

         AudioRenderer会使用大量的系统资源,所以请确保完成相关任务后,进行资源释放。

  1. async function releaseRenderer() {
  2. let state = audioRenderer.state;
  3. // 渲染器状态不是STATE_RELEASED或STATE_NEW状态,才能release
  4. if (state == audio.AudioState.STATE_RELEASED || state == audio.AudioState.STATE_NEW) {
  5. console.info('Renderer already released');
  6. return;
  7. }
  8. await audioRenderer.release();
  9. state = audioRenderer.state;
  10. if (state == audio.AudioState.STATE_RELEASED) {
  11. console.info('Renderer released');
  12. } else {
  13. console.info('Renderer release failed');
  14. }
  15. }

7.获取渲染器相关信息

        通过以下代码,可以获取渲染器的相关信息。

  1. // 获取当前渲染器状态
  2. let state = audioRenderer.state;
  3. // 获取渲染器信息
  4. let audioRendererInfo : audio.AudioRendererInfo = await audioRenderer.getRendererInfo();
  5. // 获取音频流信息
  6. let audioStreamInfo : audio.AudioStreamInfo = await audioRenderer.getStreamInfo();
  7. // 获取音频流ID
  8. let audioStreamId : number = await audioRenderer.getAudioStreamId();
  9. // 获取纳秒形式的Unix时间戳
  10. let audioTime : number = await audioRenderer.getAudioTime();
  11. // 获取合理的最小缓冲区大小
  12. let bufferSize : number = await audioRenderer.getBufferSize();
  13. // 获取渲染速率
  14. let renderRate : audio.AudioRendererRate = await audioRenderer.getRenderRate();

 (8).设置渲染器相关信息

        通过以下代码,可以设置渲染器的相关信息。

  1. // 设置渲染速率为正常速度
  2. let renderRate : audio.AudioRendererRate = audio.AudioRendererRate.RENDER_RATE_NORMAL;
  3. await audioRenderer.setRenderRate(renderRate);
  4. // 设置渲染器音频中断模式为SHARE_MODE
  5. let interruptMode : audio.InterruptMode = audio.InterruptMode.SHARE_MODE;
  6. await audioRenderer.setInterruptMode(interruptMode);
  7. // 设置一个流的音量为0.5
  8. let volume : number = 0.5;
  9. await audioRenderer.setVolume(volume);

9.使用on('audioInterrupt')方法订阅渲染器音频中断事件,使用off('audioInterrupt')取消订阅事件。

         当优先级更高或相等的Stream-B请求激活并使用输出设备时,Stream-A被中断。

        在某些情况下,框架会采取暂停播放、降低音量等强制操作,并通过InterruptEvent通知应用。在其他情况下,应用可以自行对InterruptEvent做出响应。

        在音频中断的情况下,应用可能会碰到音频数据写入失败的问题。所以建议不感知、不处理中断的应用在写入音频数据前,使用audioRenderer.state检查播放器状态。而订阅音频中断事件,可以获取到更多详细信息,具体可参考InterruptEvent

        需要说明的是,本模块的订阅音频中断事件与AudioManager模块中的on('interrupt')稍有不同。自api9以来,on('interrupt')和off('interrupt')均被废弃。在AudioRenderer模块,当开发者需要监听焦点变化事件时,只需要调用on('audioInterrupt')函数,当应用内部的AudioRenderer对象在start\stop\pause等动作发生时,会主动请求焦点,从而发生焦点转移,相关的AudioRenderer对象即可获取到对应的回调信息。但对除AudioRenderer的其他对象,例如FM、语音唤醒等,应用不会创建对象,此时可调用AudioManager中的on('interrupt')获取焦点变化通知。

  1. audioRenderer.on('audioInterrupt', (interruptEvent) => {
  2. console.info('InterruptEvent Received');
  3. console.info(`InterruptType: ${interruptEvent.eventType}`);
  4. console.info(`InterruptForceType: ${interruptEvent.forceType}`);
  5. console.info(`AInterruptHint: ${interruptEvent.hintType}`);
  6. if (interruptEvent.forceType == audio.InterruptForceType.INTERRUPT_FORCE) {
  7. switch (interruptEvent.hintType) {
  8. // 音频框架发起的强制暂停操作,为防止数据丢失,此时应该停止数据的写操作
  9. case audio.InterruptHint.INTERRUPT_HINT_PAUSE:
  10. isPlay = false;
  11. break;
  12. // 音频框架发起的强制停止操作,为防止数据丢失,此时应该停止数据的写操作
  13. case audio.InterruptHint.INTERRUPT_HINT_STOP:
  14. isPlay = false;
  15. break;
  16. // 音频框架发起的强制降低音量操作
  17. case audio.InterruptHint.INTERRUPT_HINT_DUCK:
  18. break;
  19. // 音频框架发起的恢复音量操作
  20. case audio.InterruptHint.INTERRUPT_HINT_UNDUCK:
  21. break;
  22. }
  23. } else if (interruptEvent.forceType == audio.InterruptForceType.INTERRUPT_SHARE) {
  24. switch (interruptEvent.hintType) {
  25. // 提醒App开始渲染
  26. case audio.InterruptHint.INTERRUPT_HINT_RESUME:
  27. startRenderer();
  28. break;
  29. // 提醒App音频流被中断,由App自主决定是否继续(此处选择暂停)
  30. case audio.InterruptHint.INTERRUPT_HINT_PAUSE:
  31. isPlay = false;
  32. pauseRenderer();
  33. break;
  34. }
  35. }
  36. });
  37. audioRenderer.off('audioInterrupt'); // 取消音频中断事件的订阅,后续将无法监听到音频中断事件

(10).使用on('markReach')方法订阅渲染器标记到达事件,使用off('markReach')取消订阅事件。

           注册markReach监听后,当渲染器渲染的帧数到达设定值时,会触发回调并返回设定的值。

  1. audioRenderer.on('markReach', (reachNumber) => {
  2. console.info('Mark reach event Received');
  3. console.info(`The renderer reached frame: ${reachNumber}`);
  4. });
  5. audioRenderer.off('markReach'); // 取消markReach事件的订阅,后续将无法监听到“标记到达”事件

(11). 使用on('periodReach')方法订阅渲染器区间标记到达事件,使用off('periodReach')取消订阅事件。

          注册periodReach监听后,每当渲染器渲染的帧数到达设定值时,会触发回调并返回设定的值。

  1. audioRenderer.on('periodReach', (reachNumber) => {
  2. console.info('Period reach event Received');
  3. console.info(`In this period, the renderer reached frame: ${reachNumber} `);
  4. });
  5. audioRenderer.off('periodReach'); // 取消periodReach事件的订阅,后续将无法监听到“区间标记到达”事件

 (12).使用on('stateChange')方法订阅渲染器音频状态变化事件。

         注册stateChange监听后,当渲染器的状态发生改变时,会触发回调并返回当前渲染器的状态。

  1. audioRenderer.on('stateChange', (audioState) => {
  2. console.info('State change event Received');
  3. console.info(`Current renderer state is: ${audioState}`);
  4. });

 (13).对on()方法的异常处理。

         在使用on()方法时,如果传入的字符串错误或传入的参数类型错误,程序会抛出异常,需要用try catch来捕获。

  1. try {
  2. audioRenderer.on('invalidInput', () => { // 字符串不匹配
  3. })
  4. } catch (err) {
  5. console.info(`Call on function error, ${err}`); // 程序抛出401异常
  6. }
  7. try {
  8. audioRenderer.on(1, () => { // 入参类型错误
  9. })
  10. } catch (err) {
  11. console.info(`Call on function error, ${err}`); // 程序抛出6800101异常
  12. }

14.on('audioInterrupt')方法完整示例。 同一个应用中的AudioRender1和AudioRender2在创建时均设置了焦点模式为独立,并且调用on('audioInterrupt')监听焦点变化。刚开始AudioRender1拥有焦点,当AudioRender2获取到焦点时,audioRenderer1将收到焦点转移的通知,打印相关日志。如果AudioRender1和AudioRender2不将焦点模式设置为独立,则监听处理中的日志在应用运行过程中永远不会被打印。

  1. async runningAudioRender1(){
  2. let audioStreamInfo = {
  3. samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000,
  4. channels: audio.AudioChannel.CHANNEL_1,
  5. sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S32LE,
  6. encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
  7. }
  8. let audioRendererInfo = {
  9. content: audio.ContentType.CONTENT_TYPE_MUSIC,
  10. usage: audio.StreamUsage.STREAM_USAGE_MEDIA,
  11. rendererFlags: 0 // 0是音频渲染器的扩展标志位,默认为0
  12. }
  13. let audioRendererOptions = {
  14. streamInfo: audioStreamInfo,
  15. rendererInfo: audioRendererInfo
  16. }
  17. //1.1 创建对象
  18. audioRenderer1 = await audio.createAudioRenderer(audioRendererOptions);
  19. console.info("Create audio renderer 1 success.");
  20. //1.2 设置焦点模式为独立模式 :1
  21. audioRenderer1.setInterruptMode(1).then( data => {
  22. console.info('audioRenderer1 setInterruptMode Success!');
  23. }).catch((err) => {
  24. console.error(`audioRenderer1 setInterruptMode Fail: ${err}`);
  25. });
  26. //1.3 设置监听
  27. audioRenderer1.on('audioInterrupt', async(interruptEvent) => {
  28. console.info(`audioRenderer1 on audioInterrupt : ${JSON.stringify(interruptEvent)}`)
  29. });
  30. //1.4 启动渲染
  31. await audioRenderer1.start();
  32. console.info('startAudioRender1 success');
  33. //1.5 获取缓存区大小,此处是渲染器的合理的最小缓冲区大小(也可以选择其它大小的缓冲区)
  34. const bufferSize = await audioRenderer1.getBufferSize();
  35. console.info(`audio bufferSize: ${bufferSize}`);
  36. //1.6 获取原始音频数据文件
  37. let dir = globalThis.fileDir; //不可直接访问,没权限,切记!!!一定要使用沙箱路径
  38. const path1 = dir + '/music001_48000_32_1.wav'; // 需要渲染的音乐文件 实际路径为:/data/storage/el2/base/haps/entry/files/music001_48000_32_1.wav
  39. console.info(`audioRender1 file path: ${ path1}`);
  40. let file1 = fs.openSync(path1, fs.OpenMode.READ_ONLY);
  41. let stat = await fs.stat(path1); //音乐文件信息
  42. let buf = new ArrayBuffer(bufferSize);
  43. let len = stat.size % this.bufferSize == 0 ? Math.floor(stat.size / this.bufferSize) : Math.floor(stat.size / this.bufferSize + 1);
  44. //1.7 通过audioRender对缓存区的原始音频数据进行渲染
  45. for (let i = 0;i < len; i++) {
  46. let options = {
  47. offset: i * this.bufferSize,
  48. length: this.bufferSize
  49. }
  50. let readsize = await fs.read(file.fd, buf, options)
  51. let writeSize = await new Promise((resolve,reject)=>{
  52. this.audioRenderer1.write(buf,(err,writeSize)=>{
  53. if(err){
  54. reject(err)
  55. }else{
  56. resolve(writeSize)
  57. }
  58. })
  59. })
  60. }
  61. fs.close(file1)
  62. await audioRenderer1.stop(); //停止渲染
  63. await audioRenderer1.release(); //释放资源
  64. }
  65. async runningAudioRender2(){
  66. let audioStreamInfo = {
  67. samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000,
  68. channels: audio.AudioChannel.CHANNEL_1,
  69. sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S32LE,
  70. encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
  71. }
  72. let audioRendererInfo = {
  73. content: audio.ContentType.CONTENT_TYPE_MUSIC,
  74. usage: audio.StreamUsage.STREAM_USAGE_MEDIA,
  75. rendererFlags: 0 // 0是音频渲染器的扩展标志位,默认为0
  76. }
  77. let audioRendererOptions = {
  78. streamInfo: audioStreamInfo,
  79. rendererInfo: audioRendererInfo
  80. }
  81. //2.1 创建对象
  82. audioRenderer2 = await audio.createAudioRenderer(audioRendererOptions);
  83. console.info("Create audio renderer 2 success.");
  84. //2.2 设置焦点模式为独立模式 :1
  85. audioRenderer2.setInterruptMode(1).then( data => {
  86. console.info('audioRenderer2 setInterruptMode Success!');
  87. }).catch((err) => {
  88. console.error(`audioRenderer2 setInterruptMode Fail: ${err}`);
  89. });
  90. //2.3 设置监听
  91. audioRenderer2.on('audioInterrupt', async(interruptEvent) => {
  92. console.info(`audioRenderer2 on audioInterrupt : ${JSON.stringify(interruptEvent)}`)
  93. });
  94. //2.4 启动渲染
  95. await audioRenderer2.start();
  96. console.info('startAudioRender2 success');
  97. //2.5 获取缓存区大小
  98. const bufferSize = await audioRenderer2.getBufferSize();
  99. console.info(`audio bufferSize: ${bufferSize}`);
  100. //2.6 获取原始音频数据文件
  101. let dir = globalThis.fileDir; //不可直接访问,没权限,切记!!!一定要使用沙箱路径
  102. const path2 = dir + '/music002_48000_32_1.wav'; // 需要渲染的音乐文件 实际路径为:/data/storage/el2/base/haps/entry/files/music002_48000_32_1.wav
  103. console.info(`audioRender2 file path: ${ path2}`);
  104. let file2 = fs.openSync(path2, fs.OpenMode.READ_ONLY);
  105. let stat = await fs.stat(path2); //音乐文件信息
  106. let buf = new ArrayBuffer(bufferSize);
  107. let len = stat.size % this.bufferSize == 0 ? Math.floor(stat.size / this.bufferSize) : Math.floor(stat.size / this.bufferSize + 1);
  108. //2.7 通过audioRender对缓存区的原始音频数据进行渲染
  109. for (let i = 0;i < len; i++) {
  110. let options = {
  111. offset: i * this.bufferSize,
  112. length: this.bufferSize
  113. }
  114. let readsize = await fs.read(file.fd, buf, options)
  115. let writeSize = await new Promise((resolve,reject)=>{
  116. this.audioRenderer2.write(buf,(err,writeSize)=>{
  117. if(err){
  118. reject(err)
  119. }else{
  120. resolve(writeSize)
  121. }
  122. })
  123. })
  124. }
  125. fs.close(file2)
  126. await audioRenderer2.stop(); //停止渲染
  127. await audioRenderer2.release(); //释放资源
  128. }
  129. async writeBuffer(buf, audioRender) {
  130. let writtenbytes;
  131. await audioRender.write(buf).then((value) => {
  132. writtenbytes = value;
  133. console.info(`Actual written bytes: ${writtenbytes} `);
  134. });
  135. if (typeof(writtenbytes) != 'number' || writtenbytes < 0) {
  136. console.error('get Write buffer failed. check the state of renderer');
  137. }
  138. }
  139. //综合调用入口
  140. async test(){
  141. await runningAudioRender1();
  142. await runningAudioRender2();
  143. }

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/很楠不爱3/article/detail/329297
推荐阅读
相关标签
  

闽ICP备14008679号