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

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

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

前言

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

语音识别

权限申请

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

  • 在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. 使用录音开始前,start()
    AsrUtils.start();
    1. 结束语音录入,stop()
    AsrUtils.stop();
    1. 通过getResultAndClear()方法获取识别结果,返回识别结果,并且除掉缓存,为下一次准备
    String result =AsrUtils.getResultAndClear();

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;
    }
}

语音播报

五大语音播报类

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

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

二大约束与限制

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

高可用的语音播报类

使用方法

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

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;
    }

}
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;
    }
}

尾声

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

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
已于2022-2-12 23:14:32修改
7
收藏 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
回复
    相关推荐