Unity接入百度语音合成安卓(android)SDK
一、注册百度云账号
选择语音技术,并且创建一个安卓应用,选择语音合成,领取免费配额
创建应用的时候,包名要跟Unity发布安卓时的包名保持一致,如果不一致,则会找不到我们写的脚本
二、下载百度语音合成Demo
我们所需的安卓库文件与jar包都在demo中
下载地址:https://console.bce.baidu.com/ai/?_=1599524377675&fromai=1#/ai/speech/app/detail~appId=1886795
三、创建一个新的安卓项目
我们使用AndroidStudio最新版,目前是3.6.3
如何创建自己的AndroidStudio项目,并且简单的与Unity交互,请看我的另一篇博文
地址:https://blog.csdn.net/weixin_43271060/article/details/90318254
四、导入库文件和jar包
我们已经创建好了自己项目,我们目前的情况是项目中已经存在了其他程序员编写的sdk,为了避免去别人的冲突,我们选择不继承成自UnityPlayerActivity,如何创建不继承UnityPlayerActivity的类?
请参考我的另一篇博文
https://blog.csdn.net/weixin_43271060/article/details/90370009
好了,创建完了我们的项目,开始导入安卓项目需要的库文件与jar包
jar目录:Demo目录/app/libs文件夹下的jar包
如何导入jar包已经在简单与安卓交互这篇博文中写了,所以不再赘述,
库文件目录:
Demo目录/app/src/main/下的jniLibs文件夹,我们将此文件夹导入我们项目中的src/main/目录,直接复制粘贴就好,
五、编写代码
编写百度语音技术主类
package com.xxx.xxx;//自己的包名
import android.content.Context;
import com.cientx.tianguo.Recogn.RecognHandler;
import com.cientx.tianguo.Recogn.RecognListener;
import com.cientx.tianguo.Synthesizer.SpeechSynthesizerHandler;
import com.cientx.tianguo.Synthesizer.SynthesizerListener;
import com.cientx.tianguo.Synthesizer.FileSaveListener;
import com.cientx.tianguo.Wakeup.WakeupHandler;
import com.cientx.tianguo.Wakeup.WakeupListener;
//百度语音主类
public class CientBaiDuVoiceMainActivity {
public static CientBaiDuVoiceMainActivity _instance;
public static CientBaiDuVoiceMainActivity getInstance() {
if (_instance == null) {
_instance = new CientBaiDuVoiceMainActivity();
}
return _instance;
}
}
新建一个Package,名字叫Sythesizer,看下图我们已经创建好了
编写一个语音识别监听类SynthesizerListener
package com.xxx.xxx.Synthesizer;//包名
import com.baidu.tts.client.SpeechError;
public class SynthesizerListener implements com.baidu.tts.client.SpeechSynthesizerListener {
/**
* 保存文件的目录
*/
protected String mDestDir;
protected String mTagDialog = "";
/**
* @param savePath 要保存的目录
* @param tag 当前对话的标识,因为需要重复播放,所以需要标识每段对话
*/
public void SetSynthesizePath(String savePath, String tag){
mDestDir = savePath;
mTagDialog = tag;
}
//合成开始
@Override
public void onSynthesizeStart(String utteranceId) {
}
//合成过程中的数据回调接口
@Override
public void onSynthesizeDataArrived(String utteranceId, byte[] bytes, int progress, int engineType) {
}
/**
* 合成正常结束,每句合成正常结束都会回调,如果过程中出错,则回调onError,不再回调此接口
*
* @param utteranceId
*/
@Override
public void onSynthesizeFinish(String utteranceId) {
}
//播放开始
@Override
public void onSpeechStart(String utteranceId) {
}
/**
* 播放进度回调接口,分多次回调
*
* @param utteranceId
* @param progress 如合成“百度语音问题”这6个字, progress肯定是从0开始,到6结束。 但progress无法保证和合成到第几个字对应。
*/
@Override
public void onSpeechProgressChanged(String utteranceId, int progress) {
}
/**
* 播放正常结束,每句播放正常结束都会回调,如果过程中出错,则回调onError,不再回调此接口
*
* @param utteranceId
*/
@Override
public void onSpeechFinish(String utteranceId) {
}
/**
* 当合成或者播放过程中出错时回调此接口
*
* @param utteranceId
* @param speechError 包含错误码和错误信息
*/
@Override
public void onError(String utteranceId, SpeechError speechError) {
}
}
编写一个合成完后将语音文件保存到手机本地的类
FileSaveListener
package com.xxx.xxx.Synthesizer;//包名
import com.baidu.tts.client.SpeechError;
import com.cientx.tianguo.Util.LogPrint;
import com.cientx.tianguo.Util.SendToUnity;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileSaveListener extends SynthesizerListener {
/**
* 保存的文件名 mTaseName +tag+"-"+ utteranceId, 通常是 output-10001-0.pcm
*/
private String mTaseName = "output-";
/**
* 文件
*/
private File ttsFile;
/**
* ttsFile 文件流
*/
private FileOutputStream ttsFileOutputStream;
/**
* ttsFile 文件buffer流
*/
private BufferedOutputStream ttsFileBufferedOutputStream;
public FileSaveListener() {
}
@Override
public void onSynthesizeStart(String utteranceId) {
LogPrint.Log("准备开始合成,***:" + utteranceId);
String filename = mTaseName + mTagDialog + "-" + utteranceId + ".pcm";
// 保存的语音文件是 16K采样率 16bits编码 单声道 pcm文件。
ttsFile = new File(mDestDir, filename);
LogPrint.Log("合成开始,创建文件: " + ttsFile.getAbsolutePath());//这是日志类,自己创建
try {
if (ttsFile.exists()) {//如果文件存在,则删除
ttsFile.delete();
}
ttsFile.createNewFile();
// 创建FileOutputStream对象
FileOutputStream ttsFileOutputStream = new FileOutputStream(ttsFile);
// 创建BufferedOutputStream对象
ttsFileBufferedOutputStream = new BufferedOutputStream(ttsFileOutputStream);
} catch (IOException e) {
// 请自行做错误处理
e.printStackTrace();
LogPrint.Log("创建文件失败:" + mDestDir + "/" + filename);
throw new RuntimeException(e);
}
}
@Override
public void onSynthesizeDataArrived(String utteranceId, byte[] data, int progress, int engineType) {
// super.onSynthesizeDataArrived(utteranceId, data, progress, engineType);
// LogPrint.Log("合成进度回调, progress:" + progress + ";***:" + utteranceId);
try {
ttsFileBufferedOutputStream.write(data);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 播放正常结束,每句播放正常结束都会回调,如果过程中出错,则回调onError,不再回调此接口
*
* @param utteranceId
*/
@Override
public void onSynthesizeFinish(String utteranceId) {
// super.onSynthesizeFinish(utteranceId);
//LogPrint.Log("合成结束***:" + utteranceId);
SendToUnity.SendUnity("SynthesizeResult","0"+"&"+mDestDir+mTaseName + mTagDialog + "-" + utteranceId + ".pcm");
close();
}
//播放开始
@Override
public void onSpeechStart(String utteranceId) {
// LogPrint.Log("onSpeechStart");
}
/**
* 当合成或者播放过程中出错时回调此接口
*
* @param utteranceId
* @param speechError 包含错误码和错误信息
*/
@Override
public void onError(String utteranceId, SpeechError speechError) {
//LogPrint.Log("onError:" + speechError);
SendToUnity.SendUnity("SynthesizeResult",speechError+"&"+mTaseName + mTagDialog + "-" + utteranceId + ".pcm");
close();
}
/**
* 关闭流,注意可能stop导致该方法没有被调用
*/
private void close() {
if (ttsFileBufferedOutputStream != null) {
try {
ttsFileBufferedOutputStream.flush();
ttsFileBufferedOutputStream.close();
ttsFileBufferedOutputStream = null;
} catch (Exception e2) {
e2.printStackTrace();
}
}
if (ttsFileOutputStream != null) {
try {
ttsFileOutputStream.close();
ttsFileOutputStream = null;
} catch (IOException e) {
e.printStackTrace();
}
}
LogPrint.Log("关闭文件成功");
}
}
上方代码中LogPrint.Log()是我编写的日志类,这个可以自己创建一个,不需要跟我的一样
SendToUnity.SendUnity是发送到Unity中的类,参数是Unity类中的函数名,错误消息&语音文件完整路径,这个类在这里也不多做介绍
编写一个语音识别的处理类
package com.xxx.xxx.Synthesizer;//自己的包名
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import com.baidu.tts.client.SpeechSynthesizer;
import com.baidu.tts.client.SpeechSynthesizerListener;
import com.baidu.tts.client.TtsMode;
import com.xxx.xxx.Util.LogPrint;//日志类,需要自己创建
import static com.cientx.tianguo.Util.GetActivity.getActivityByContext;
public class SpeechSynthesizerHandler {
private Context mContext;
private boolean mIsInit = false;
SpeechSynthesizer mSpeechSynthesizer;
public SpeechSynthesizerHandler(Context context, SpeechSynthesizerListener listener) {
if (mIsInit) {
listener = null;
return;
}
mIsInit = true;
mContext = context;
initPermission(context);
mSpeechSynthesizer = SpeechSynthesizer.getInstance();
mSpeechSynthesizer.setContext(context);
mSpeechSynthesizer.setSpeechSynthesizerListener(listener);
mSpeechSynthesizer.setAppId("申请的appid");
mSpeechSynthesizer.setApiKey("申请的apiKey", "申请的Secret Key");
int result= mSpeechSynthesizer.initTts(TtsMode.ONLINE); // 初始化在线模式
if (result != 0) {
LogPrint.Log("合成初始化失败:"+result);
}
}
//开始合成
/**
* 开始说话
*
* @param speakStr 合成的文字
* @param utteranceId 合成的序号
* @param speakRole 合成后播放的发音人
* @param speakVolume 合成后播放的音量
* @param speakSpeed 合成后播放的语速
*/
public void Speak(String speakStr,String utteranceId,String speakRole,String speakVolume,String speakSpeed) {
LogPrint.Log("开始合成:"+speakStr);
mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_SPEAKER, speakRole);
mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_VOLUME, speakVolume);
mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_SPEED, speakSpeed);
int result= mSpeechSynthesizer.speak(speakStr,utteranceId);//合成并播放
if (result != 0) {
LogPrint.Log("语音合成,合成失败:"+result);
}
}
//暂停播放
public void Pause() {
mSpeechSynthesizer.pause();
}
//继续播放
public void Resume() {
mSpeechSynthesizer.resume();
}
//取消当前的合成。并停止播放。
public void Stop() {
mSpeechSynthesizer.stop();
}
//释放合成实例
public void Release() {
mSpeechSynthesizer.release();
mSpeechSynthesizer = null;
}
private void initPermission(Context context) {
String permissions[] = {
Manifest.permission.INTERNET,
Manifest.permission.ACCESS_NETWORK_STATE,
Manifest.permission.MODIFY_AUDIO_SETTINGS,
Manifest.permission.WRITE_SETTINGS,
Manifest.permission.ACCESS_WIFI_STATE,
Manifest.permission.CHANGE_WIFI_STATE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
};
PackageManager pm = getActivityByContext(context).getPackageManager();
boolean permission_accessWiFi = (PackageManager.PERMISSION_GRANTED ==
pm.checkPermission("android.permission.ACCESS_WIFI_STATE", "com.cientx.tianguo"));
boolean permission_network_state = (PackageManager.PERMISSION_GRANTED ==
pm.checkPermission("android.permission.ACCESS_NETWORK_STATE", "com.cientx.tianguo"));
boolean permission_internet = (PackageManager.PERMISSION_GRANTED ==
pm.checkPermission("android.permission.INTERNET", "com.cientx.tianguo"));
boolean permission_writeStorage = (PackageManager.PERMISSION_GRANTED ==
pm.checkPermission("android.permission.MODIFY_AUDIO_SETTINGS", "com.cientx.tianguo"));
boolean permission_changeWifi = (PackageManager.PERMISSION_GRANTED ==
pm.checkPermission("android.permission.CHANGE_WIFI_STATE", "com.cientx.tianguo"));
boolean permission_writeSettings = (PackageManager.PERMISSION_GRANTED ==
pm.checkPermission("android.permission.WRITE_SETTINGS", "com.cientx.tianguo"));
boolean permission_writeExternal = (PackageManager.PERMISSION_GRANTED ==
pm.checkPermission("android.permission.WRITE_EXTERNAL_STORAGE", "com.cientx.tianguo"));
if (!(permission_accessWiFi && permission_writeStorage && permission_network_state && permission_internet&& permission_changeWifi&& permission_writeSettings&& permission_writeExternal)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
getActivityByContext(context).requestPermissions(permissions, 0x01);
}
}
}
}
上面代码中.Util.GetActivity.getActivityByContext类代码如下
public static Activity getActivityByContext(Context context){
while(context instanceof ContextWrapper){
if(context instanceof Activity){
return (Activity) context;
}
context = ((ContextWrapper) context).getBaseContext();
}
return null;
}
在主类中调用接口
package com.xxx.xxx;//自己的包名
import android.content.Context;
import com.xxx.xxx.Synthesizer.SpeechSynthesizerHandler;
import com.xxx.xxx.Synthesizer.SynthesizerListener;
import com.xxx.xxx.Synthesizer.FileSaveListener;
//百度语音主类
public class CientBaiDuVoiceMainActivity {
public static CientBaiDuVoiceMainActivity _instance;
public static CientBaiDuVoiceMainActivity getInstance() {
if (_instance == null) {
_instance = new CientBaiDuVoiceMainActivity();
}
return _instance;
}
//语音合成控制
SpeechSynthesizerHandler mSynthesizerHandler;
/**
* 语音合成监听
*/
SynthesizerListener mListener;
//语音合成初始化
public void InitSynthesizer(Context context) {
mListener = new FileSaveListener();
mSynthesizerHandler = new SpeechSynthesizerHandler(context, mListener);
}
//开始合成并播放
/**@param speakStr 要合成的话,批量合成时,循环调用当前函数,SDK中有队列缓冲,官方推荐使用
* @param utteranceId 当前对话的序号,单句话合成,序号默认为0
* @param speakRole 发音人,在Unity中设置
* @param speakVolume 音量
* @param speakSpeed 语速
* @param savePath 合成后语音文件要保存的目录
* @param tag 当前对话的标识
* */
public void StartSynthesizer(String speakStr, String utteranceId, String speakRole, String speakVolume, String speakSpeed, String savePath, String tag) {
mListener.SetSynthesizePath(savePath, tag);//设置合成后保存的路径
mSynthesizerHandler.Speak(speakStr, utteranceId, speakRole, speakVolume, speakSpeed);
}
//取消当前的合成。并停止播放。
public void StopSynthesizer() {
mSynthesizerHandler.Stop();
}
//暂停播放当前正在播放的声音
public void Pause() {
mSynthesizerHandler.Pause();
}
//继续播放
public void Resume() {
mSynthesizerHandler.Resume();
}
//释放实例
public void ReleaseSynthesizer() {
mSynthesizerHandler.Release();
mSynthesizerHandler = null;
}
}
六、设置AndroidManifest.xml,发布aar包
为什么要发布aar包呢?因为.aar包内包含了这个sdk的AndroidManifest.xml文件与引用的库文件、引用的jar包,这样就不会与其他的sdk冲突,也不需要与其他的sdk中的Androidmanifest.xml合并
首先需要创建我们的签名,这里不再多说,创建签名的方法请在我的另一篇博文中查看
我们需要在创建的Module中的build.gradle中编写发布arr包的代码
task copyPlugin(type: Copy) {
dependsOn assemble
from('build/outputs/aar')
into('../../Assets/Plugins/Android')
include(project.name + '-release.aar')
}
发布按钮位于右上角的Gradle中
七、在Unity中编写交互代码
void Start()
{
AndroidJNI.AttachCurrentThread();
AndroidJavaClass _androidJC = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
if (_androidJC == null)
{
Debug.Log("JNI initialization failure.");
return;
}
m_AndroidPluginObj = _androidJC.GetStatic<AndroidJavaObject>("currentActivity");
}
/// <summary>
/// 初始化语音合成
/// </summary>
public void InitSynthesize()
{
AndroidJavaClass jc = new AndroidJavaClass("com.xxx.xxx.CientBaiDuVoiceMainActivity");//自己的包名
AndroidJavaObject m_Android = jc.CallStatic<AndroidJavaObject>("getInstance");
if (m_Android != null)
{
m_Android.Call("InitSynthesizer", m_AndroidPluginObj);
}
else
Debug.Log("AndroidPlugin is Null");
}
/// <summary>
/// 开始60字以内语音合成
/// </summary>
public void StartSynthesize()
{
AndroidJavaClass jc = new AndroidJavaClass("com.xxx.xxx.CientBaiDuVoiceMainActivity");//自己的包名
AndroidJavaObject m_Android = jc.CallStatic<AndroidJavaObject>("getInstance");
if (m_Android != null)
{
m_Android.Call("StartSynthesizer", "是一部生命的教科书,是献给生命的礼物", "0", "0", "5", "5", Application.persistentDataPath + "/voice/", "10000");
}
else
Debug.Log("AndroidPlugin is Null");
}
List<string> superLongls = new List<string>();
/// <summary>
/// 开始超过60字的语音合成
/// </summary>
public void StartBatchSynthesize()
{
AndroidJavaClass jc = new AndroidJavaClass("com.cientx.tianguo.CientBaiDuVoiceMainActivity");
AndroidJavaObject m_Android = jc.CallStatic<AndroidJavaObject>("getInstance");
if (m_Android != null)
{
string speakStr =
"天寿仪式通俗讲是一种祭祖行为,我们也叫它孝亲敬祖仪式。与普通祭祖行为相比,天寿仪式是遵循了天地、宇宙、自然规律的祭祖行为,对应天、地、人合一的理想状态。庄严的行为仪式、严谨的风水布局和包含美好祝福的丰盛供品,让天寿仪式具有强大的能量,集健康积极的生命状态、强大的正能量和良性向上的信息于一体,唤醒我们由祖辈传承至今的巨大潜力,助力目标实现,营造幸福生活。您还想了解有关天寿仪式的其他内容吗?1.天寿仪式操作步骤;2.组队天寿仪式操作步骤";
List<string> douHaols = new List<string>();
List<string> juHaols = new List<string>();
List<string> tanHaols = new List<string>();
List<string> fenHaols = new List<string>();
List<string> wenHaols = new List<string>();
//遇到,。!;?都拆分成单独的字符串,如果拆分到最后,单个的字符串还是超出了60个字的限制,则强制把超长的字符串拆分开
SubDouHao(",", speakStr, 0, douHaols);
for (int i = 0; i < douHaols.Count; i++)
{
SubDouHao("。", douHaols[i], 0, juHaols);
}
for (int i = 0; i < juHaols.Count; i++)
{
SubDouHao("!", juHaols[i], 0, tanHaols);
}
for (int i = 0; i < tanHaols.Count; i++)
{
SubDouHao(";", tanHaols[i], 0, fenHaols);
}
for (int i = 0; i < fenHaols.Count; i++)
{
SubDouHao("?", fenHaols[i], 0, wenHaols);
}
for (int i = 0; i < wenHaols.Count; i++)
{
SubStr(wenHaols[i], 0, superLongls);
}
for (int i = 0; i < superLongls.Count; i++)
{
m_Android.Call("StartSynthesizer", superLongls[i], i.ToString(), "0", "5", "5", Application.persistentDataPath + "/voice/", "10001");
}
}
else
Debug.Log("AndroidPlugin is Null");
}
void SubDouHao(string fuhao, string src, int startIndex, List<string> ls)
{
int index = src.IndexOf(fuhao);
if (index < 0)
{
ls.Add(src);
return;
}
string str;
str = src.Substring(startIndex, index);
ls.Add(str);
string remainStr = src.Substring(index + 1, src.Length - (index + 1));
SubDouHao(fuhao, remainStr, 0, ls);
}
private int SubIndex = 0;
void SubStr(string src, int startIndex, List<string> ls)
{
if (src.Length - startIndex < 60)
{
ls.Add(src);
return;
}
string str;
str = src.Substring(startIndex, 60);
ls.Add(str);
SubIndex++;
int len = src.Length - 60 * SubIndex;
if (len >= 60)
{
SubStr(src, 60 * SubIndex, ls);
}
else
{
str = src.Substring(60 * SubIndex, len);
ls.Add(str);
return;
}
}
Dictionary<string, Queue<string>> mDic = new Dictionary<string, Queue<string>>();
Dictionary<string, AudioClip> mAudioDic = new Dictionary<string, AudioClip>();
private int SynthesizeIndex = 0;
public Button repeatPlayBtn;
public AudioSource mAudio;
Queue<string> mPathQueue = new Queue<string>();
/// <summary>
/// 百度语音合成结果回调
/// </summary>
/// <param name="res"></param>
void SynthesizeResult(string res)
{
string[] ress = res.Split('&');
//res 错误码+文件带路径 0&xxx/xxx/output-对话标识-序号.pcm
if (ress[0] == "0")//合成成功
{
SynthesizeIndex++;
if (SynthesizeIndex >= superLongls.Count)
{
repeatPlayBtn.interactable = true;
}
string fileName = ress[1];
string[] splitStr = fileName.Split('-');
if (!mDic.ContainsKey(splitStr[1]))
{
Debug.Log("添加音频文件:" + fileName);
mPathQueue.Enqueue(fileName);
mDic.Add(splitStr[1], mPathQueue);
StartCoroutine(LoadLocalAudio(fileName));
}
else
{
Debug.Log("添加音频文件:"+ fileName);
mDic[splitStr[1]].Enqueue(fileName);
}
mRecognRes.text = "合成的文件:" + fileName;
}
}
IEnumerator LoadLocalAudio(string path)
{
UnityWebRequest _unityWebRequest = UnityWebRequestMultimedia.GetAudioClip(path, AudioType.WAV);
yield return _unityWebRequest.SendWebRequest(); if (_unityWebRequest.isHttpError || _unityWebRequest.isNetworkError)
{
Debug.Log(_unityWebRequest.error.ToString());
}
else
{
AudioClip _audioClip = DownloadHandlerAudioClip.GetContent(_unityWebRequest);
mAudioDic.Add(path, _audioClip);
}
}
public void ChongFuBoFang()
{
repeatPlayBtn.interactable = false;
StartCoroutine(AudioIsEnd(PlayEndCall));
}
private int playAudioIndex = 0;
void PlayEndCall()
{
playAudioIndex++;
if (playAudioIndex >= superLongls.Count)//全部播放完了
{
repeatPlayBtn.interactable = true;
}
}
IEnumerator AudioIsEnd(UnityAction call)
{
while (mDic["10001"].Count > 0)
{
string filePath = mDic["10001"].Dequeue();
AudioClip clip = mAudioDic[filePath];
mAudio.clip = clip;
yield return new WaitForSeconds(clip.length);
if (call != null)
{
call();
}
}
}
百度语音合成仅支持60个汉字以内的合成,所以,如果超过了60个字,则可以进行循环调用合成,合成sdk内部有缓冲队列
最后百度语音识别、语音合成、语音唤醒安卓端sdk与Unity交互的源码,请私信我