9

一招教你打通鸿蒙语音识别和语音播报

 2 years ago
source link: https://os.51cto.com/article/701825.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
一招教你打通鸿蒙语音识别和语音播报-51CTO.COM
一招教你打通鸿蒙语音识别和语音播报 原创
作者:Piwriw 2022-02-17 17:19:31
语言播报(Text to Speech)和语音识别(Automatic Speech Recognition, ASR),这些都基于了鸿蒙官网的开发项目下的自带的AI能力—>语音识别、语音播报,其实这二个能力可以是基础能力。

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

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

​https://harmonyos.51cto.com​

大家好久不见了,我是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(“你的项目包名”);

传入context实现,ASR的初始化

 AsrUtils.InitAsrUtils(this);

使用录音开始前,start()

 AsrUtils.start();

结束语音录入,stop()

    AsrUtils.stop();

通过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

传入contex实现初始化

  TtsUtils.initTtsEngine(this);

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

这次呢,就大概给大家带来的东西就这么多了,可能看到这里,还是有朋友想知道我为什么要分享这个语音识别和语音播报,众所周知事出反常必有妖,其实就是我自己踩坑了😕😕,我自己在使用的时候,因为种种不知名的问题,导致我使用的时候出现了一些奇奇怪怪的问题(后面我想可能是生命周期的问题),一开始其实我是想把这个做成service服务的,但是很遗憾失败了(其实就是我不太会,嘻嘻),后面我就想到目前这个方法,问题好像就解决了。

可能看到这里,我这里还想再说几句,其实我在解决我上面的问题的时候,尝试了很多很多解决方法,但是很多问题,由于一些失败也好,还是有我本身做手机App开发的经验不足,还是我采用的是Harmonyos的,是非完全开源的(讲真,有点坑),也导致了我寻找BUG的时候直接受阻了,因为看不了😑😑😑

好了,在最后,我祝大家语音识别和语音播报的代码食用愉快!!!

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

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

​https://harmonyos.51cto.com​

f79fe4915b7031e28ef393cf0369ae93094c77.jpg

责任编辑:jianghua 来源: 鸿蒙社区
zanpc.bd208a1.pngzanpchover.fdd60ba.png
weixin.23cd8b3.png 分享到微信
weibo.16d6b4f.png 分享到微博

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK