一招教你打通鸿蒙语音识别和语音播报 原创 精华

Piwriw.
发布于 2022-2-12 21:34
浏览
8收藏

春节不停更,此文正在参加「星光计划-春节更帖活动」

前言

  • 大家好久不见了,我是Piwriw.,自从上次分享完关于算法与数据结构的系列之后,我也一直想回到我的开篇的鸿蒙技术上,但是我一直都没有想到很好的方向😞,又是因为年前年后的问题,就一直在社区潜水🐟🐟🐟,也是在社区里面发现了很多有趣的技术,最近很火的eST实现冰墩墩,做到人手一个冰墩墩哈哈哈。
  • 但是就在前几天,我"突然"想到了一个我最近一个项目也在用的技术(这个项目是我参加鸿蒙开发者创新大赛创,所以暂时没办法公布给大家,之后有机会的话,我会再分享出来)—>语言播报(Text to Speech)语音识别(Automatic Speech Recognition, ASR),这些都基于了鸿蒙官网的开发项目下的自带的AI能力—>语音识别语音播报,其实这二个能力可以是基础能力,但是又是十分重要的能力,毕竟谁能拒绝一个掷地有声的交互呢?😁😁
  • 目前由于我本身是使用Java开发,所以在下面的代码中,我只提供Java版本,不过我是在官方文档也没有找到Js和eTS也可以支持这个能力的参考说明,在下面的代码中,我尽量实现了高可用,我把语音识别和语音播报都做成了工具类,所以可能很多详细的配置选项,大家可以上官方文档查看
  • 由于本人能力有限,在过程中难免会出现错误,以及可能也有存在更好的代码,希望大家在评论区发现错误也能留言,大家一起来讨论这个问题💕

语音识别

权限申请

由于我们使用了语音,使用我们要申请录音权限

  • 在config.json中配置上ohos.permission.MICROPHONE的能力。

五大语音识别API类

  • AsrIntent:提供ASR引擎执行时所需要传入的参数类

  • AsrError: 错误码的定义类

  • AsrListener:加载语音识别Listener

  • AsrClient:提供调用ASR引擎服务接口的类

  • AsrResultKey:ASR回调结果中的关键字封装类

至于五大类各执详细的接口内部方法对应的功能,我在这里不过多强调,在官方文档的说明已经十分详细了------>语音识别概述

六大约束和限制

  • 支持的输入文件格式有wav或pcm
  • 当前仅支持对普通话的识别
  • 输入时长不能超过20s
  • 采样要求:采样率16000Hz,单声道
  • 引擎的使用必须初始化和释放处理,且调用必须在UI的主线程中进行
  • 多线程调用:HUAWEI HiAI Engine不支持同一应用使用多线程调用同一接口,这样会使某一线程调用release方法后,卸载模型,导致正在运行的另一些线程出错。故多线程执行同一功能达不到并行的效果。但是引擎支持使用多线程调用不同接口,如开启两个线程同时使用文档矫正和ASR接口

高可用语音识别类

  • 可能看到上面乱七八糟的描述,你已经“晕”了,这是什么,我看不懂啊,没关系,往下看

使用方法

  • 前置: AsrUtils中audioCaptureUtils.init(“你的项目包名”);

    1. 传入context实现,ASR的初始化
    AsrUtils.InitAsrUtils(this);
  • 1.
    1. 使用录音开始前,start()
    AsrUtils.start();
  • 1.
    1. 结束语音录入,stop()
    AsrUtils.stop();
  • 1.
    1. 通过getResultAndClear()方法获取识别结果,返回识别结果,并且除掉缓存,为下一次准备
    String result =AsrUtils.getResultAndClear();
  • 1.

AsrUtils工具类参考代码

package com.piwriw.puzzlepictures.utils;

import ohos.ai.asr.AsrClient;
import ohos.ai.asr.AsrIntent;
import ohos.ai.asr.AsrListener;
import ohos.ai.asr.util.AsrError;
import ohos.ai.asr.util.AsrResultKey;
import ohos.app.Context;
import ohos.media.audio.AudioStreamInfo;
import ohos.utils.PacMap;
import ohos.utils.zson.ZSONObject;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author Piwriw
 * @date 2022/2/12 
 * @motto 你不能做我的诗,正如我不能做你的梦.
 */
public class AsrUtils {
    //采样率限
    private static final int VIDEO_SAMPLE_RATE = 16000;
    private static final int VAD_END_WAIT_MS = 2000;
    private static final int VAD_FRONT_WAIT_MS = 4800;
    private static final int TIMEOUT_DURATION = 20000;
    private static final int BYTES_LENGTH = 1280;
    //线程池相关参数
    private static final int CAPACITY = 6;
    private static final int ALIVE_TIME = 3;
    private static final int POOL_SIZE = 3;

    //录音线程
    private  static  ThreadPoolExecutor poolExecutor;
    /* 自定义状态信息
     **  错误:-1
     **  初始:0
     **  init:1
     **  开始输入:2
     **  结束输入:3
     **  识别结束:5
     **  中途出识别结果:9
     **  最终识别结果:10
     */
    public static int state = 0;
    //识别结果
    public static String result;
    //是否开启语音识别
    //当开启时才写入PCM流
    private static boolean  isStarted = false;

    //ASR客户端
    private static AsrClient asrClient;
    //ASR监听对象
    private static AsrListener listener;
    private  static AsrIntent asrIntent;
    //音频录制工具类
    private static AudioCaptureUtils audioCaptureUtils;

    public static void InitAsrUtils(Context context) {
        //实例化一个单声道,采集频率16000HZ的音频录制工具类实例
        audioCaptureUtils = new AudioCaptureUtils(AudioStreamInfo.ChannelMask.CHANNEL_IN_MONO, VIDEO_SAMPLE_RATE);
        //初始化降噪音效
        audioCaptureUtils.init("你的项目包名");
        //结果值初始置空
        result = "";

        //给录音控件初始化一个新的线程池
        poolExecutor = new ThreadPoolExecutor(
                POOL_SIZE,
                POOL_SIZE,
                ALIVE_TIME,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(CAPACITY),
                new ThreadPoolExecutor.DiscardOldestPolicy());

        if (asrIntent == null) {
            asrIntent = new AsrIntent();
            //设置音频来源为PCM流
            //此处也可设置为文件
            asrIntent.setAudioSourceType(AsrIntent.AsrAudioSrcType.ASR_SRC_TYPE_PCM);
            asrIntent.setVadEndWaitMs(VAD_END_WAIT_MS);
            asrIntent.setVadFrontWaitMs(VAD_FRONT_WAIT_MS);
            asrIntent.setTimeoutThresholdMs(TIMEOUT_DURATION);
        }

        if (asrClient == null) {
            //实例化AsrClient
            asrClient = AsrClient.createAsrClient(context).orElse(null);
        }
        if (listener == null) {
            //实例化MyAsrListener
            listener = new MyAsrListener();
            //初始化AsrClient
            asrClient.init(asrIntent, listener);
        }

    }

    //实现AsrListener接口监听类
  private static class MyAsrListener implements AsrListener {

        @Override
        public void onInit(PacMap pacMap) {
            Utils.logInfo("------ init");
            state = 1;
        }

        @Override
        public void onBeginningOfSpeech() {
            state = 2;
        }

        @Override
        public void onRmsChanged(float v) {

        }

        @Override
        public void onBufferReceived(byte[] bytes) {

        }

        @Override
        public void onEndOfSpeech() {
            state = 3;
        }

        @Override
        public void onError(int i) {
            state = -1;
            if (i == AsrError.ERROR_SPEECH_TIMEOUT) {
                //当超时时重新监听
                asrClient.startListening(asrIntent);
            } else {
                Utils.logInfo("======error code:" + i);
                asrClient.stopListening();
            }
        }

        @Override
        public void onResults(PacMap pacMap) {
            state = 10;
            //获取最终结果
            String results = pacMap.getString(AsrResultKey.RESULTS_RECOGNITION);
            ZSONObject zsonObject = ZSONObject.stringToZSON(results);
            ZSONObject infoObject;
            if (zsonObject.getZSONArray("result").getZSONObject(0) instanceof ZSONObject) {
                infoObject = zsonObject.getZSONArray("result").getZSONObject(0);
                String resultWord = infoObject.getString("ori_word").replace(" ", "");
                result += resultWord;
            }
        }

        //中途识别结果
        //pacMap.getString(AsrResultKey.RESULTS_INTERMEDIATE)
        @Override
        public void onIntermediateResults(PacMap pacMap) {
            state = 9;
        }


        @Override
        public void onEnd() {
            state = 5;
            //当还在录音时,重新监听
            if (isStarted)
                asrClient.startListening(asrIntent);
        }

        @Override
        public void onEvent(int i, PacMap pacMap) {

        }

        @Override
        public void onAudioStart() {
            state = 2;

        }

        @Override
        public void onAudioEnd() {
            state = 3;
        }

    }

    public static  void start() {
        if (!isStarted) {
            isStarted = true;
            asrClient.startListening(asrIntent);
            poolExecutor.submit(new AudioCaptureRunnable());
        }
    }

    public static void stop() {
        isStarted = false;
        asrClient.stopListening();
        audioCaptureUtils.stop();
//        asrClient.destroy();
    }

    //音频录制的线程
    private static class AudioCaptureRunnable implements Runnable {
        @Override
        public void run() {
            byte[] buffers = new byte[BYTES_LENGTH];
            //开启录音
            audioCaptureUtils.start();
            while (isStarted) {
                //读取录音的PCM流
                int ret = audioCaptureUtils.read(buffers, 0, BYTES_LENGTH);
                if (ret <= 0) {
                    Utils.logInfo("======Error read data");
                } else {
                    asrClient.writePcm(buffers, BYTES_LENGTH);
                }
            }
        }
    }

    public static String getResult() {
        return result;
    }

    public static String getResultAndClear() {
        if (result == "")
            return "";
        String results = getResult();
        result = "";
        return results;
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.
  • 166.
  • 167.
  • 168.
  • 169.
  • 170.
  • 171.
  • 172.
  • 173.
  • 174.
  • 175.
  • 176.
  • 177.
  • 178.
  • 179.
  • 180.
  • 181.
  • 182.
  • 183.
  • 184.
  • 185.
  • 186.
  • 187.
  • 188.
  • 189.
  • 190.
  • 191.
  • 192.
  • 193.
  • 194.
  • 195.
  • 196.
  • 197.
  • 198.
  • 199.
  • 200.
  • 201.
  • 202.
  • 203.
  • 204.
  • 205.
  • 206.
  • 207.
  • 208.
  • 209.
  • 210.
  • 211.
  • 212.
  • 213.
  • 214.
  • 215.
  • 216.
  • 217.
  • 218.
  • 219.
  • 220.
  • 221.
  • 222.
  • 223.
  • 224.
  • 225.
  • 226.
  • 227.
  • 228.
  • 229.
  • 230.
  • 231.
  • 232.
  • 233.
  • 234.
  • 235.

语音播报

五大语音播报类

  • TtsClient: TTS接口
  • TtsListener: TTS回调
  • TtsParams: TTS参数
  • TtsEvent: TTS事件
  • PacMap: TTS依赖

同样的具体五大类的详细接口内部功能,大家参考
语音播报开发指导

二大约束与限制

  • 支持超长文本播报,最大文本长度为100000个字符
  • 语音播报不支持多线程调用

高可用的语音播报类

使用方法

  • 前置:这次我们要生成的为2个类:TtsUtils和AudioCaptureUtils
    1. 传入contex实现初始化
  TtsUtils.initTtsEngine(this);
  • 1.
    1. 通过readText(str)方法,播报内容
    TtsUtils.readText(播报内容)
  • 1.

AudioCaptureUtils和AsrUtils

package com.piwriw.puzzlepictures.utils;

import ohos.media.audio.AudioCapturer;
import ohos.media.audio.AudioCapturerInfo;
import ohos.media.audio.AudioStreamInfo;
import ohos.media.audio.SoundEffect;

import java.util.UUID;

/**
 * @author Piwriw
 * @date 2022/2/12
 * @motto 你不能做我的诗,正如我不能做你的梦.
 */
public class AudioCaptureUtils {
    private AudioStreamInfo audioStreamInfo;
    private AudioCapturer audioCapturer;
    private AudioCapturerInfo audioCapturerInfo;

    //channelMask 声道
    //SampleRate 频率
    public AudioCaptureUtils(AudioStreamInfo.ChannelMask channelMask, int SampleRate) {
        this.audioStreamInfo = new AudioStreamInfo.Builder()
                .encodingFormat(AudioStreamInfo.EncodingFormat.ENCODING_PCM_16BIT)
                .channelMask(channelMask)
                .sampleRate(SampleRate)
                .build();
        this.audioCapturerInfo = new AudioCapturerInfo.Builder().audioStreamInfo(audioStreamInfo).build();
    }

    //packageName 包名
    public void init(String packageName) {
        this.init(SoundEffect.SOUND_EFFECT_TYPE_NS, packageName);
    }

    //soundEffect 音效uuid
    //packageName 包名
    public void init(UUID soundEffect, String packageName) {
        if (audioCapturer == null || audioCapturer.getState() == AudioCapturer.State.STATE_UNINITIALIZED)
            audioCapturer = new AudioCapturer(this.audioCapturerInfo);
        audioCapturer.addSoundEffect(soundEffect, packageName);
    }

    public void stop() {
        this.audioCapturer.stop();
    }

    public void destory() {
        this.audioCapturer.stop();
        this.audioCapturer.release();
    }

    public Boolean start() {
        if (audioCapturer == null)
            return false;
        return audioCapturer.start();
    }

    //buffers 需要写入的数据流
    //offset 数据流的偏移量
    //byteslength 数据流的长度
    public int read(byte[] buffers, int offset, int bytesLength) {
        return audioCapturer.read(buffers, offset, bytesLength);
    }

    //获取AudioCapturer的实例audioCapturer
    public AudioCapturer get() {
        return this.audioCapturer;
    }

}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
package com.piwriw.puzzlepictures.utils;

import ohos.ai.tts.TtsClient;
import ohos.ai.tts.TtsListener;
import ohos.ai.tts.TtsParams;
import ohos.ai.tts.constants.TtsEvent;
import ohos.app.Context;
import ohos.utils.PacMap;

import java.io.IOException;
import java.util.UUID;

/**
 * @author Piwriw
 * @date 2022/2/12
 * @motto 你不能做我的诗,正如我不能做你的梦.
 */
public class  TtsUtils {
    /**
     * 语音播报
     */
    private static boolean initItsResult;
    public static void readText(String str) {
        if (initItsResult) {
            Utils.logInfo("initItsResult is true, speakText");
            TtsClient.getInstance().speakText(str, null);
        } else {
            Utils.logInfo("initItsResult is false");
        }
    }

    public  static boolean initTtsEngine(Context context ) {

          TtsListener ttsListener = new TtsListener() {
            @Override
            public void onEvent(int eventType, PacMap pacMap) {

                // 定义TTS客户端创建成功的回调函数
                if (eventType == TtsEvent.CREATE_TTS_CLIENT_SUCCESS) {
                    TtsParams ttsParams = new TtsParams();
                    ttsParams.setDeviceId(UUID.randomUUID().toString());
                    initItsResult = TtsClient.getInstance().init(ttsParams);
                }
            }

            @Override
            public void onSpeechStart(String s) {

            }

            @Override
            public void onSpeechProgressChanged(String s, int i) {

            }

            @Override
            public void onSpeechFinish(String s) {

            }

            @Override
            public void onStart(String utteranceId) {

            }

            @Override
            public void onProgress(String utteranceId, byte[] audioData, int progress) {
            }

            @Override
            public void onFinish(String utteranceId) {

            }

            @Override
            public void onError(String s, String s1) {

            }

        };
      try {
          TtsClient.getInstance().create(context, ttsListener);
      }
      catch (Exception e){
          e.printStackTrace();
          return false;
      }
      return true;
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.

尾声

  • 这次呢,就大概给大家带来的东西就这么多了,可能看到这里,还是有朋友想知道我为什么要分享这个语音识别语音播报,众所周知事出反常必有妖,其实就是我自己踩坑了😕😕,我自己在使用的时候,因为种种不知名的问题,导致我使用的时候出现了一些奇奇怪怪的问题(后面我想可能是生命周期的问题),一开始其实我是想把这个做成service服务的,但是很遗憾失败了(其实就是我不太会,嘻嘻),后面我就想到目前这个方法,问题好像就解决了。
  • 可能看到这里,我这里还想再说几句,其实我在解决我上面的问题的时候,尝试了很多很多解决方法,但是很多问题,由于一些失败也好,还是有我本身做手机App开发的经验不足,还是我采用的是Harmonyos的,是非完全开源的(讲真,有点坑),也导致了我寻找BUG的时候直接受阻了,因为看不了😑😑😑
  • 好了,在最后,我祝大家语音识别和语音播报的代码食用愉快!!! 大家觉得有用的话,记得给我点个赞吧!!! 对了,求加精(期待能被抽中本次星光活动的大奖😬😬😬)!!!一招教你打通鸿蒙语音识别和语音播报-鸿蒙开发者社区

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
已于2022-2-12 23:14:32修改
7
收藏 8
回复
举报
7
3
8
3条回复
按时间正序
/
按时间倒序
红叶亦知秋
红叶亦知秋

好文,必须点赞

1
回复
2022-2-14 10:35:37
Piwriw.
Piwriw. 回复了 红叶亦知秋
好文,必须点赞

感谢支持

1
回复
2022-2-14 12:42:48
wx60081ce951b2e
wx60081ce951b2e

官网文档中说“HUAWEI HiAI Engine不支持同一应用使用多线程调用同一接口,这样会使某一线程调用release方法后,卸载模型,导致正在运行的另一些线程出错。”

我看是用线程池实现的,运行过程中会不会有影响? 

回复
2022-5-3 16:57:10


回复
    相关推荐