鸿蒙AI能力之语音识别

开发
文章旨在帮助大家开发录音及语音识别时少踩一点坑。AI语音识别不需要任何权限,但此处使用到麦克风录制音频,就需要申请麦克风权限。

[[441758]]

想了解更多内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com

文章旨在帮助大家开发录音及语音识别时少踩一点坑。

效果

鸿蒙AI能力之语音识别-鸿蒙HarmonyOS技术社区

左侧为简易UI布局及识别成果,右侧为网易云播放的测试音频。

开发步骤

IDE安装、项目创建等在此略过。App采用SDK版本为API 6,使用JS UI。

1.权限申请

AI语音识别不需要任何权限,但此处使用到麦克风录制音频,就需要申请麦克风权限。

在config.json配置文件中添加权限:

  1. "reqPermissions": [ 
  2.       { 
  3.         "name""ohos.permission.MICROPHONE" 
  4.       } 
  5.     ] 

 在MainAbility中显示申明麦克风权限:

  1. @Override 
  2.     public void onStart(Intent intent) { 
  3.         super.onStart(intent); 
  4.         requestPermission(); 
  5.     } 
  6.  
  7.     //获取权限 
  8.     private void requestPermission() { 
  9.         String[] permission = { 
  10.                 "ohos.permission.MICROPHONE"
  11.         }; 
  12.         List<String> applyPermissions = new ArrayList<>(); 
  13.         for (String element : permission) { 
  14.             if (verifySelfPermission(element) != 0) { 
  15.                 if (canRequestPermission(element)) { 
  16.                     applyPermissions.add(element); 
  17.                 } 
  18.             } 
  19.         } 
  20.         requestPermissionsFromUser(applyPermissions.toArray(new String[0]), 0); 
  21.     } 

2.创建音频录制的工具类

首先创建音频录制的工具类AudioCaptureUtils。

而音频录制需要用到AudioCapturer类,而在创建AudioCapture类时又会用到AudioStreamInfo类及AudioCapturerInfo类,所以我们分别申明以上3个类的变量。

  1. private AudioStreamInfo audioStreamInfo; 
  2.    private AudioCapturer audioCapturer; 
  3.    private AudioCapturerInfo audioCapturerInfo; 

 在语音识别时对音频的录制是由限制的,限制如下:

鸿蒙AI能力之语音识别-鸿蒙HarmonyOS技术社区

所以我们在录制音频时需要注意:

1.采样率16000HZ

2.声道为单声道

3.仅支持普通话

作为工具类,为了使AudioCaptureUtils能多处使用,我们在创建构造函数时,提供声道与频率的参数重载,并在构造函数中初始化AudioStreamInfo类及AudioCapturerInfo类。

  1. //channelMask 声道 
  2. //SampleRate 频率 
  3.   public AudioCaptureUtils(AudioStreamInfo.ChannelMask channelMask, int SampleRate) { 
  4.         this.audioStreamInfo = new AudioStreamInfo.Builder() 
  5.                 .encodingFormat(AudioStreamInfo.EncodingFormat.ENCODING_PCM_16BIT) 
  6.                 .channelMask(channelMask) 
  7.                 .sampleRate(SampleRate) 
  8.                 .build(); 
  9.         this.audioCapturerInfo = new AudioCapturerInfo.Builder().audioStreamInfo(audioStreamInfo).build(); 
  10.     } 

 在init函数中进行audioCapturer的初始化,在初始化时对音效进行设置,默认为降噪模式。

  1. //packageName 包名 
  2.  public void init(String packageName) { 
  3.         this.init(SoundEffect.SOUND_EFFECT_TYPE_NS,packageName ); 
  4.     } 
  5. //soundEffect 音效uuid 
  6. //packageName 包名 
  7.    public void init(UUID soundEffect, String packageName) { 
  8.         if (audioCapturer == null || audioCapturer.getState() == AudioCapturer.State.STATE_UNINITIALIZED) 
  9.             audioCapturer = new AudioCapturer(this.audioCapturerInfo); 
  10.         audioCapturer.addSoundEffect(soundEffect, packageName); 
  11.     } 

 初始化后提供start、stop和destory方法,分别开启音频录制、停止音频录制和销毁,此处都是调用AudioCapturer类中对应函数。

  1. public void stop(){ 
  2.        this.audioCapturer.stop(); 
  3.    } 
  4.  
  5.    public void destory(){ 
  6.        this.audioCapturer.stop(); 
  7.        this.audioCapturer.release(); 
  8.    } 
  9.  
  10.    public Boolean start() { 
  11.        if (audioCapturer == null
  12.            return false
  13.        return audioCapturer.start(); 
  14.    } 

 提供一个读取音频流的方法及获取AudioCapturer实例的方法。

  1. //buffers 需要写入的数据流 
  2. //offset 数据流的偏移量 
  3. //byteslength 数据流的长度 
  4.  public int read(byte[] buffers, int offset, int bytesLength){ 
  5.         return audioCapturer.read(buffers,offset,bytesLength); 
  6.     } 
  7.  
  8. //获取AudioCapturer的实例audioCapturer 
  9.   public AudioCapturer get(){ 
  10.         return this.audioCapturer; 
  11.     } 

3.创建语音识别的工具类

在上面我们已经创建好一个音频录制的工具类,接下来在创建一个语音识别的工具类 AsrUtils。

我们再回顾一下语音识别的约束与限制:

鸿蒙AI能力之语音识别-鸿蒙HarmonyOS技术社区

在此补充一个隐藏限制,PCM流的长度只允许640与1280两种长度,也就是我们音频读取流时只能使用640与1280两种长度。

接下来我们定义一些基本常量:

  1. //采样率限定16000HZ 
  2.    private static final int VIDEO_SAMPLE_RATE = 16000; 
  3. //VAD结束时间 默认2000ms 
  4.    private static final int VAD_END_WAIT_MS = 2000; 
  5. //VAD起始时间 默认4800ms  
  6. //这两参数与识别准确率有关,相关信息可百度查看,在此使用系统默认 
  7.    private static final int VAD_FRONT_WAIT_MS = 4800; 
  8. //输入时常 20000ms 
  9.    private static final int TIMEOUT_DURATION = 20000; 
  10.  
  11. //PCM流长度仅限640或1280 
  12.    private static final int BYTES_LENGTH = 1280; 
  13. //线程池相关参数 
  14.    private static final int CAPACITY = 6; 
  15.    private static final int ALIVE_TIME = 3; 
  16.    private static final int POOL_SIZE = 3; 

 因为要在后台持续录制音频,所以需要开辟一个新的线程。此处用到java的ThreadPoolExecutor类进行线程操作。

定义一个线程池实例以及其它相关属性如下:

  1. //录音线程 
  2.    private ThreadPoolExecutor poolExecutor;  
  3.  /* 自定义状态信息 
  4.     **  错误:-1 
  5.     **  初始:0 
  6.     **  init:1 
  7.     **  开始输入:2 
  8.     **  结束输入:3 
  9.     **  识别结束:5 
  10.     **  中途出识别结果:9 
  11.     **  最终识别结果:10 
  12.     */ 
  13.    public int state = 0; 
  14.    //识别结果 
  15.    public String result; 
  16. //是否开启语音识别 
  17. //当开启时才写入PCM流 
  18.    boolean isStarted = false
  19.  
  20. //ASR客户端 
  21.    private AsrClient asrClient; 
  22. //ASR监听对象 
  23.    private AsrListener listener; 
  24.    AsrIntent asrIntent; 
  25. //音频录制工具类 
  26.    private AudioCaptureUtils audioCaptureUtils; 

在构造函数中初始化相关属性:

  1. public AsrUtils(Context context) { 
  2.         //实例化一个单声道,采集频率16000HZ的音频录制工具类实例 
  3.         this.audioCaptureUtils = new AudioCaptureUtils(AudioStreamInfo.ChannelMask.CHANNEL_IN_MONO, VIDEO_SAMPLE_RATE); 
  4.         //初始化降噪音效 
  5.         this.audioCaptureUtils.init("com.panda_coder.liedetector"); 
  6.         //结果值设为空 
  7.         this.result = ""
  8.         //给录音控件初始化一个新的线程池 
  9.         poolExecutor = new ThreadPoolExecutor( 
  10.                 POOL_SIZE, 
  11.                 POOL_SIZE, 
  12.                 ALIVE_TIME, 
  13.                 TimeUnit.SECONDS, 
  14.                 new LinkedBlockingQueue<>(CAPACITY), 
  15.                 new ThreadPoolExecutor.DiscardOldestPolicy()); 
  16.  
  17.         if (asrIntent == null) { 
  18.             asrIntent = new AsrIntent(); 
  19.             //设置音频来源为PCM流 
  20.             //此处也可设置为文件             
  21.             asrIntent.setAudioSourceType(AsrIntent.AsrAudioSrcType.ASR_SRC_TYPE_PCM); 
  22.             asrIntent.setVadEndWaitMs(VAD_END_WAIT_MS); 
  23.             asrIntent.setVadFrontWaitMs(VAD_FRONT_WAIT_MS); 
  24.             asrIntent.setTimeoutThresholdMs(TIMEOUT_DURATION); 
  25.         } 
  26.  
  27.         if (asrClient == null) { 
  28.             //实例化AsrClient 
  29.             asrClient = AsrClient.createAsrClient(context).orElse(null); 
  30.         } 
  31.         if (listener == null) { 
  32.             //实例化MyAsrListener 
  33.             listener = new MyAsrListener(); 
  34.             //初始化AsrClient 
  35.             this.asrClient.init(asrIntent, listener); 
  36.         } 
  37.      
  38.     } 
  39.  
  40. //够建一个实现AsrListener接口的类MyAsrListener  
  41.  class MyAsrListener implements AsrListener { 
  42.  
  43.         @Override 
  44.         public void onInit(PacMap pacMap) { 
  45.             HiLog.info(TAG, "====== init"); 
  46.             state = 1; 
  47.         } 
  48.  
  49.         @Override 
  50.         public void onBeginningOfSpeech() { 
  51.             state = 2; 
  52.         } 
  53.  
  54.         @Override 
  55.         public void onRmsChanged(float v) { 
  56.  
  57.         } 
  58.  
  59.         @Override 
  60.         public void onBufferReceived(byte[] bytes) { 
  61.  
  62.         } 
  63.  
  64.         @Override 
  65.         public void onEndOfSpeech() { 
  66.             state = 3; 
  67.         } 
  68.  
  69.         @Override 
  70.         public void onError(int i) { 
  71.             state = -1; 
  72.             if (i == AsrError.ERROR_SPEECH_TIMEOUT) { 
  73.                 //当超时时重新监听 
  74.                 asrClient.startListening(asrIntent); 
  75.             } else { 
  76.                 HiLog.info(TAG, "======error code:" + i); 
  77.                 asrClient.stopListening(); 
  78.             } 
  79.         } 
  80.  
  81.         //注意与onIntermediateResults获取结果值的区别 
  82.         //pacMap.getString(AsrResultKey.RESULTS_RECOGNITION); 
  83.         @Override 
  84.         public void onResults(PacMap pacMap) { 
  85.             state = 10; 
  86.             //获取最终结果  
  87.             //{"result":[{"confidence":0,"ori_word":"你 好 ","pinyin":"NI3 HAO3 ","word":"你好。"}]} 
  88.             String results = pacMap.getString(AsrResultKey.RESULTS_RECOGNITION); 
  89.             ZSONObject zsonObject = ZSONObject.stringToZSON(results); 
  90.             ZSONObject infoObject; 
  91.             if (zsonObject.getZSONArray("result").getZSONObject(0) instanceof ZSONObject) { 
  92.                 infoObject = zsonObject.getZSONArray("result").getZSONObject(0); 
  93.                 String resultWord = infoObject.getString("ori_word").replace(" """); 
  94.                 result += resultWord; 
  95.             } 
  96.         } 
  97.  
  98.         //中途识别结果 
  99.         //pacMap.getString(AsrResultKey.RESULTS_INTERMEDIATE) 
  100.         @Override 
  101.         public void onIntermediateResults(PacMap pacMap) { 
  102.             state = 9; 
  103. //            String result = pacMap.getString(AsrResultKey.RESULTS_INTERMEDIATE); 
  104. //            if (result == null
  105. //                return
  106. //            ZSONObject zsonObject = ZSONObject.stringToZSON(result); 
  107. //            ZSONObject infoObject; 
  108. //            if (zsonObject.getZSONArray("result").getZSONObject(0) instanceof ZSONObject) { 
  109. //                infoObject = zsonObject.getZSONArray("result").getZSONObject(0); 
  110. //                String resultWord = infoObject.getString("ori_word").replace(" """); 
  111. //                HiLog.info(TAG, "=========== 9 " + resultWord); 
  112. //            } 
  113.         } 
  114.  
  115.  
  116.         @Override 
  117.         public void onEnd() { 
  118.             state = 5; 
  119.             //当还在录音时,重新监听 
  120.             if (isStarted) 
  121.                 asrClient.startListening(asrIntent); 
  122.         } 
  123.  
  124.         @Override 
  125.         public void onEvent(int i, PacMap pacMap) { 
  126.  
  127.         } 
  128.  
  129.         @Override 
  130.         public void onAudioStart() { 
  131.             state = 2; 
  132.  
  133.         } 
  134.  
  135.         @Override 
  136.         public void onAudioEnd() { 
  137.             state = 3; 
  138.         } 
  139.     } 

开启识别与停止识别的函数:

  1. public void start() { 
  2.      if (!this.isStarted) { 
  3.          this.isStarted = true
  4.          asrClient.startListening(asrIntent); 
  5.          poolExecutor.submit(new AudioCaptureRunnable()); 
  6.      } 
  7.  } 
  8.  
  9.  public void stop() { 
  10.      this.isStarted = false
  11.      asrClient.stopListening(); 
  12.      audioCaptureUtils.stop(); 
  13.  } 
  14.  
  15. //音频录制的线程 
  16.  private class AudioCaptureRunnable implements Runnable { 
  17.      @Override 
  18.      public void run() { 
  19.          byte[] buffers = new byte[BYTES_LENGTH]; 
  20. //开启录音 
  21.          audioCaptureUtils.start(); 
  22.          while (isStarted) { 
  23.     //读取录音的PCM流 
  24.              int ret = audioCaptureUtils.read(buffers, 0, BYTES_LENGTH); 
  25.              if (ret <= 0) { 
  26.                  HiLog.error(TAG, "======Error read data"); 
  27.              } else { 
  28.         //将录音的PCM流写入到语音识别服务中 
  29.         //若buffer的长度不为1280或640时,则需要手动处理成1280或640 
  30.                  asrClient.writePcm(buffers, BYTES_LENGTH); 
  31.              } 
  32.          } 
  33.      } 
  34.  } 

识别结果是通过listener的回调获取的结果,所以我们在处理时是将结果赋值给result,通过getresult或getResultAndClear函数获取结果。

  1. public String getResult() { 
  2.        return result; 
  3.    } 
  4.  
  5.    public String getResultAndClear() { 
  6.        if (this.result == ""
  7.            return ""
  8.        String results = getResult(); 
  9.        this.result = ""
  10.        return results; 
  11.    } 

4.创建一个简易的JS UI,并通过JS调ServerAbility的能力调用Java

hml代码:

  1. <div class="container"
  2.     <div> 
  3.         <button class="btn" @touchend="start">开启</button> 
  4.         <button class="btn" @touchend="sub">订阅结果</button> 
  5.         <button class="btn" @touchend="stop">关闭</button> 
  6.     </div> 
  7.     <text class="title"
  8.         语音识别内容: {{ text }} 
  9.     </text> 
  10. </div> 

样式代码:

  1. .container { 
  2.     flex-direction: column
  3.     justify-content: flex-start; 
  4.     align-items: center; 
  5.     width: 100%; 
  6.     height: 100%; 
  7.     padding: 10%; 
  8.  
  9. .title { 
  10.     font-size: 20px; 
  11.     color: #000000; 
  12.     opacity: 0.9; 
  13.     text-align: left
  14.     width: 100%; 
  15.     margin: 3% 0; 
  16.  
  17. .btn{ 
  18.     padding: 10px 20px; 
  19.     margin:3px; 
  20.     border-radius: 6px; 

 js逻辑控制代码:

  1. //js调Java ServiceAbility的工具类 
  2. import { jsCallJavaAbility } from '../../common/JsCallJavaAbilityUtils.js'
  3.  
  4. export default { 
  5.     data: { 
  6.         text: "" 
  7.     }, 
  8.     //开启事件 
  9.     start() { 
  10.         jsCallJavaAbility.callAbility("ControllerAbility",100,{}).then(result=>{ 
  11.             console.log(result) 
  12.         }) 
  13.     }, 
  14. //关闭事件 
  15.     stop() { 
  16.         jsCallJavaAbility.callAbility("ControllerAbility",101,{}).then(result=>{ 
  17.             console.log(result) 
  18.         }) 
  19.         jsCallJavaAbility.unSubAbility("ControllerAbility",201).then(result=>{ 
  20.             if (result.code == 200) { 
  21.                 console.log("取消订阅成功"); 
  22.             } 
  23.         }) 
  24.     }, 
  25. //订阅Java端结果事件 
  26.     sub() { 
  27.         jsCallJavaAbility.subAbility("ControllerAbility", 200, (data) => { 
  28.             let text = data.data.text 
  29.             text && (this.text += text) 
  30.         }).then(result => { 
  31.             if (result.code == 200) { 
  32.                 console.log("订阅成功"); 
  33.             } 
  34.         }) 
  35.     } 

ServerAbility:

  1. public class ControllerAbility extends Ability { 
  2.     AnswerRemote remote = new AnswerRemote(); 
  3.     AsrUtils asrUtils; 
  4.     //订阅事件的委托 
  5.     private static HashMap<Integer, IRemoteObject> remoteObjectHandlers = new HashMap<Integer, IRemoteObject>(); 
  6.  
  7.     @Override 
  8.     public void onStart(Intent intent) { 
  9.         HiLog.error(LABEL_LOG, "ControllerAbility::onStart"); 
  10.         super.onStart(intent); 
  11.     //初始化语音识别工具类 
  12.         asrUtils = new AsrUtils(this); 
  13.     } 
  14.  
  15.  
  16.     @Override 
  17.     public void onCommand(Intent intent, boolean restart, int startId) { 
  18.     } 
  19.  
  20.     @Override 
  21.     public IRemoteObject onConnect(Intent intent) { 
  22.         super.onConnect(intent); 
  23.         return remote.asObject(); 
  24.     } 
  25.  
  26.     class AnswerRemote extends RemoteObject implements IRemoteBroker { 
  27.         AnswerRemote() { 
  28.             super(""); 
  29.         } 
  30.  
  31.         @Override 
  32.         public boolean onRemoteRequest(int code, MessageParcel data, MessageParcel reply, MessageOption option) { 
  33.             Map<String, Object> zsonResult = new HashMap<String, Object>(); 
  34.             String zsonStr = data.readString(); 
  35.             ZSONObject zson = ZSONObject.stringToZSON(zsonStr); 
  36.             switch (code) { 
  37.                 case 100: { 
  38.             //当js发送code为100时,开启语音识别 
  39.                     asrUtils.start(); 
  40.                     break; 
  41.                 } 
  42.                 case 101: { 
  43.             //当js发送code为101时,关闭语音识别 
  44.                     asrUtils.stop(); 
  45.                     break; 
  46.                 } 
  47.                 case 200: { 
  48.             //当js发送code为200时,订阅获取识别结果事件 
  49.                     remoteObjectHandlers.put(200 ,data.readRemoteObject()); 
  50.             //定时获取语音识别结果并返回JS UI                     
  51.                 getAsrText(); 
  52.                     break; 
  53.                 } 
  54.                 default: { 
  55.                     reply.writeString("service not defined"); 
  56.                     return false
  57.                 } 
  58.             } 
  59.             reply.writeString(ZSONObject.toZSONString(zsonResult)); 
  60.             return true
  61.         } 
  62.  
  63.         @Override 
  64.         public IRemoteObject asObject() { 
  65.             return this; 
  66.         } 
  67.     } 
  68.  
  69.     public void getAsrText() { 
  70.         new Thread(() -> { 
  71.             while (true) { 
  72.                 try { 
  73.                     Thread.sleep(1 * 500); 
  74.                     Map<String, Object> zsonResult = new HashMap<String, Object>(); 
  75.                     zsonResult.put("text",asrUtils.getResultAndClear()); 
  76.                     ReportEvent(200, zsonResult); 
  77.  
  78.                 } catch (RemoteException | InterruptedException e) { 
  79.                     break; 
  80.                 } 
  81.             } 
  82.         }).start(); 
  83.     } 
  84.  
  85.     private void ReportEvent(int remoteHandler, Object backData) throws RemoteException { 
  86.         MessageParcel data = MessageParcel.obtain(); 
  87.         MessageParcel reply = MessageParcel.obtain(); 
  88.         MessageOption option = new MessageOption(); 
  89.         data.writeString(ZSONObject.toZSONString(backData)); 
  90.         IRemoteObject remoteObject = remoteObjectHandlers.get(remoteHandler); 
  91.         remoteObject.sendRequest(100, data, reply, option); 
  92.         reply.reclaim(); 
  93.         data.reclaim(); 
  94.     } 
  95.  

至此简易的语音识别功能完毕。

相关演示:https://www.bilibili.com/video/BV1E44y177hv/ 

完整代码开源:https://gitee.com/panda-coder/harmonyos-apps/tree/master/AsrDemo

想了解更多内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com

 

责任编辑:jianghua 来源: 鸿蒙社区
相关推荐

2021-05-06 11:18:23

人工智能语音识别

2021-05-06 11:13:06

人工智能语音识别

2022-02-17 17:19:31

鸿蒙语音识别语音播报

2011-05-31 16:38:47

Android 实现语音

2023-07-06 08:41:20

TTS​Mac​系统

2016-02-17 10:39:18

语音识别语音合成语音交互

2009-08-21 15:28:23

C#英文

2021-11-23 09:58:35

鸿蒙HarmonyOS应用

2020-09-21 07:00:00

语音识别AI人工智能

2022-12-01 07:03:22

语音识别人工智能技术

2009-07-21 15:28:06

Windows Emb

2011-01-18 11:52:25

Linux语音识别

2014-05-04 13:39:15

人脸识别算法

2017-03-20 10:14:03

语音识别匹配算法模型

2015-10-23 13:41:20

android源码科大讯飞语音识别

2024-01-08 19:30:15

AI开源语音识别

2017-10-27 16:19:23

语音识别CNN

2021-11-17 10:37:39

语音识别技术人工智能

2019-10-12 17:42:33

点赞
收藏

51CTO技术栈公众号