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

系统教程10个月前发布 creeper
120 0 0

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

​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​

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

© 版权声明

相关文章