讯飞离线语音合成新版(Aikit)-android sdk合成 demo(Java版本)
前言:科大讯飞的新版离线语音合成,由于官网demo是kt语言开发的,咱也看不懂kt,搜遍了全网也没看到一个java版的新版离线语音demo,现记录下,留给有缘人参考!!!!!毕竟咱在这上面遇到了不少的坑。如果能留言指正,那就更好了。
第一步:
官网注册账号---》实名认证---》点击语音合成---》离线语音合成(新版)---》android sdk下载
sdk:下载的sdk是和当前账号绑定的,文档上方有appkey,secret等等
第二步:
安卓项目中设置以下权限,在AndroidManifest.xml中
<!--连接网络权限,用于执行云端语音能力 -->
<uses-permission android:name="android.permission.INTERNET"/>
<!--获取手机录音机使用权限,听写、识别、语义理解需要用到此权限 -->
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<!--读取网络信息状态 -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<!--获取当前wifi状态 -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<!--允许程序改变网络连接状态 -->
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<!--读取手机信息权限 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<!--读取联系人权限,上传联系人需要用到此权限 -->
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<!--外存储写权限,构建语法需要用到此权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!--外存储读权限,构建语法需要用到此权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!--手机定位信息,用来为语义等功能提供定位,提供更精准的服务-->
<!--定位信息是敏感信息,可通过Setting.setLocationEnable(false)关闭定位请求 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!--如需使用人脸识别,还要添加:摄相头权限,拍照需要用到 -->
<uses-permission android:name="android.permission.CAMERA" />
<!--配置权限,用来记录应用配置信息 -->
<uses-permission android:name="android.permission.WRITE_SETTINGS"
tools:ignore="ProtectedPermissions" />
android:requestLegacyExternalStorage="true"
第三步:
获取设备外部存储权限,后续需要把发音人的音频文件拷贝到设备中
/**
* 查看当前设备是否有存储权限:
* 没有:请求获取权限
* 有:复制当前项目assets下的xtts文件夹到设备根目录下(语音合成所必须的文件)
* @param context
*/
private void requestStoragePermission(Context context) {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
REQUEST_STORAGE_PERMISSION);
}
}
/**
* 请求获取存储权限
* @param requestCode
* @param permissions
* @param grantResults
*/
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_STORAGE_PERMISSION) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.i(TAG, "onRequestPermissionsResult: permission granted");
//再次判断存储权限是否已授予
boolean permission = FileUtils.hasStoragePermission(getApplicationContext());
if (!permission) {
Toast.makeText(getApplicationContext(), "没有存储权限,请重新获取!", Toast.LENGTH_SHORT).show();
return;
}
// 应用具有存储权限
Log.i(TAG,"成功获取存储权限!");
//判断xtts文件是否存在,不存在则复制,存在则忽略
FileUtils.createXttsDirAndCopyFile(getApplicationContext());
} else {
Log.i(TAG, "onRequestPermissionsResult: permission denied");
Toast.makeText(this, "You Denied Permission", Toast.LENGTH_SHORT).show();
}
}
}
第四步:
拷贝五个发音人的资源文件到当前设备的xtts文件目录下。这个文件在官方的demo文件中:
package com.epean.store.utils;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.os.Build;
import android.os.Environment;
import android.util.Log;
import com.epean.store.R;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* 讯飞语音合成文件复制公共功能
* 以下五个文件:
* e3fe94474_1.0.0_xTTS_CnCn_xiaoyan_2018_arm.irf
* e4b08c6f3_1.0.0_xTTS_CnCn_xiaofeng_2018_fix_arm.dat
* e4caee636_1.0.2_xTTS_CnCn_front_Emb_arm_2017.irf
* e05d571cc_1.0.0_xTTS_CnCn_xiaoyan_2018_fix_arm.dat
* ebdbd61ae_1.0.0_xTTS_CnCn_xiaofeng_2018_arm.irf
*/
public class FileUtils {
private static final String TAG = "FileUtils";
// 获取外部存储路径
public static String getExternalStoragePath() {
return Environment.getExternalStorageDirectory().getAbsolutePath();
}
// 创建xtts目录
public static void createDirectory(String directoryPath) {
File directory = new File(directoryPath);
if (!directory.exists()) {
if (directory.mkdirs()) {
Log.d(TAG, "Directory created: " + directoryPath);
} else {
Log.e(TAG, "Failed to create directory: " + directoryPath);
}
} else {
Log.d(TAG, "Directory already exists: " + directoryPath);
}
}
// 判断目录是否为空
public static boolean isDirectoryEmpty(String directoryPath) {
File directory = new File(directoryPath);
if (directory.exists() && directory.isDirectory()) {
File[] files = directory.listFiles();
return files == null || files.length == 0;
}
return true;
}
// 递归复制文件
public static void copyFiles(Context context, String sourceDir, String destinationDir) throws IOException {
AssetManager assetManager = context.getAssets();
String[] files = assetManager.list(sourceDir);
if (files != null && files.length > 0) {
createDirectory(destinationDir);
for (String fileName : files) {
String sourcePath = sourceDir + File.separator + fileName;
String destinationPath = destinationDir + File.separator + fileName;
if (assetManager.list(sourcePath).length > 0) {
// 如果是目录,递归复制目录
copyFiles(context, sourcePath, destinationPath);
} else {
// 如果是文件,复制文件
copyFile(context, sourcePath, destinationPath);
}
}
}
}
// 复制文件
public static void copyFile(Context context, String sourcePath, String destinationPath) throws IOException {
InputStream inputStream = null;
OutputStream outputStream = null;
try {
inputStream = context.getAssets().open(sourcePath);
outputStream = new FileOutputStream(destinationPath);
byte[] buffer = new byte[4096];
int length;
while ((length = inputStream.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);
}
Log.d(TAG, "File copied: " + destinationPath);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
Log.e(TAG, "Failed to close input stream", e);
}
}
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
Log.e(TAG, "Failed to close output stream", e);
}
}
}
}
/**
* 创建讯飞语音合成所必须的目录:xtts并复制音频文件
* @param context
*/
public static void createXttsDirAndCopyFile(Context context){
// 获取外部存储路径
String externalStoragePath = FileUtils.getExternalStoragePath();
String xttsFolderPath = externalStoragePath + File.separator + context.getString(R.string.dir);
// 创建xtts文件夹
FileUtils.createDirectory(xttsFolderPath);
// 判断xtts文件夹是否为空
if (FileUtils.isDirectoryEmpty(xttsFolderPath)) {
// 复制assets目录下的xtts文件夹中的所有文件到外部存储的xtts文件夹中
try {
FileUtils.copyFiles(context, context.getString(R.string.dir), xttsFolderPath);
} catch (IOException e) {
Log.e(TAG, "文件复制失败"+e.getMessage());
}
} else {
// xtts文件夹不为空
Log.d(TAG, "xtts folder is not empty. Skipping the operation.");
}
}
public static boolean hasStoragePermission(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
int permissionResult = context.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE);
return permissionResult == PackageManager.PERMISSION_GRANTED;
}
return true;
}
}
第五步:
通用工具播放类,实现代码如下:
initSDK() :sdk整个项目只需要初始化一次
playAudio(String content, Context context):播放通用方法
package com.epean.store.utils;
import android.content.Context;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.os.Environment;
import android.util.Log;
import androidx.annotation.NonNull;
import com.epean.store.R;
import com.iflytek.aikit.core.AiEvent;
import com.iflytek.aikit.core.AiHandle;
import com.iflytek.aikit.core.AiHelper;
import com.iflytek.aikit.core.AiListener;
import com.iflytek.aikit.core.AiRequest;
import com.iflytek.aikit.core.AiResponse;
import com.iflytek.aikit.core.AiText;
import com.iflytek.aikit.core.AuthListener;
import com.iflytek.aikit.core.ErrType;
import org.apache.commons.lang3.StringUtils;
import java.io.File;
import java.util.List;
public class AudioPlayByKeyUtils {
private static final String TAG = "AudioPlayByKeyUtils";
private static int sampleRateInHz = 16000;
private static int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
private static int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
private static int bufferSizeInBytes = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
//语音合成文件缓存数组
private static byte[] cacheArray;
private static AiHandle handle;
//播放组件
private static AudioTrack audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes, AudioTrack.MODE_STREAM);
//SDK初始化
public static void initSDK(Context context){
try {
//外部存储绝对路径
File externalStorageDirectory = Environment.getExternalStorageDirectory();
// 初始化参数构建
AiHelper.Params params = AiHelper.Params.builder()
.appId(context.getString(R.string.appId))
.apiKey(context.getString(R.string.apiKey))
.apiSecret(context.getString(R.string.apiSecret))
.workDir(externalStorageDirectory.getAbsolutePath() +File.separator+ context.getString(R.string.dir))//SDK工作路径,这里为绝对路径
.authInterval(60*60*24) //授权更新间隔
.build();
// 初始化
AiHelper.getInst().init(context, params);
// 注册SDK 初始化状态监听
AiHelper.getInst().registerListener(coreListener);
// 注册能力结果监听
AiHelper.getInst().registerListener(context.getString(R.string.enginID), aiRespListener);
}catch (Exception e){
Log.e(TAG,"语音合成初始化出现异常"+e.getMessage());
}
}
public static void playAudio(String content, Context context){
if (StringUtils.isEmpty(content)){
Log.e(TAG,"播报内容不能为空!");
return;
}
//已初始化则略过
initSDK(context);
//避免脏数据
cacheArray = null;
//音量及播报人等参数设置
AiRequest.Builder paramBuilder = audioParam();
handle = AiHelper.getInst().start(context.getString(R.string.enginID),paramBuilder.build(),null);
if (!handle.isSuccess()) {
Log.e(TAG, "ERROR::START | handle code:" + handle.getCode());
return;
}
//自定义文本参数
AiRequest.Builder dataBuilder = contentParame(content);
//开始合成,合成结果可通过回调接口获取
int ret = AiHelper.getInst().write(dataBuilder.build(), handle);
//ret 值为0 写入成功;非0失败
if (ret != 0) {
String error = "start write failed" + ret;
Log.e(TAG, error);
}
}
/**
* 封装自定义文本参数
* @param content
* @return
*/
@NonNull
private static AiRequest.Builder contentParame(String content) {
AiRequest.Builder dataBuilder = AiRequest.builder();
//输入文本数据
AiText textData = AiText
.get("text")
.data(content) //输入文本
.valid();
dataBuilder.payload(textData);
return dataBuilder;
}
/**
* 音量及播报人等参数设置
*/
@NonNull
private static AiRequest.Builder audioParam() {
AiRequest.Builder paramBuilder = AiRequest.builder();
paramBuilder.param("vcn", "xiaoyan");
paramBuilder.param("language", 1);
paramBuilder.param("pitch", 50);
paramBuilder.param("volume", 50);
paramBuilder.param("speed", 50);
paramBuilder.param("reg", 0);
paramBuilder.param("rdn", 0);
paramBuilder.param("textEncoding", "UTF-8");
return paramBuilder;
}
/**
* SDK监听回调
*/
private static AuthListener coreListener = new AuthListener() {
@Override
public void onAuthStateChange(final ErrType type, final int code) {
Log.i(TAG,"core listener code:" + code);
switch (type) {
case AUTH:
Log.i(TAG,"SDK状态:授权结果码" + code);
break;
case HTTP:
Log.i(TAG,"SDK状态:HTTP认证结果" + code);
break;
default:
Log.i(TAG,"SDK状态:其他错误");
}
}
};
/**
*能力监听回调
*/
private static AiListener aiRespListener = new AiListener() {
//获取合成结果,封装到缓存数组中
@Override
public void onResult(int handleID, List<AiResponse> outputData, Object usrContext) {
if (outputData == null || outputData.isEmpty()) {
return;
}
if (null != outputData && outputData.size() > 0) {
for (int i = 0; i < outputData.size(); i++) {
byte[] bytes = outputData.get(i).getValue();
if (bytes == null) {
continue;
}else {
if (cacheArray == null) {
cacheArray = bytes;
} else {
byte[] resBytes = new byte[(cacheArray != null ? cacheArray.length : 0) + bytes.length];
if (cacheArray != null) {
System.arraycopy(cacheArray, 0, resBytes, 0, cacheArray.length);
}
System.arraycopy(bytes, 0, resBytes, cacheArray != null ? cacheArray.length : 0, bytes.length);
cacheArray = resBytes;
}
}
}
}
}
@Override
public void onEvent(int handleID, int event, List<AiResponse> eventData, Object usrContext){
if (event == AiEvent.EVENT_UNKNOWN.getValue()){
}
if (event == AiEvent.EVENT_START.getValue()){
}
if (event == AiEvent.EVENT_END.getValue()){
if (handle != null){
int rets = AiHelper.getInst().end(handle);
if (rets != 0) {
String error = "end failed" + rets;
Log.e(TAG, error);
}
}
cacheArray = null;
}
if (event == AiEvent.EVENT_PROGRESS.getValue()){
if (cacheArray != null) {
audioTrack.write(cacheArray, 0, cacheArray.length);
audioTrack.play();
}
}
}
@Override
public void onError(int handleID, int err, String msg, Object usrContext){
if (handle != null){
int rets = AiHelper.getInst().end(handle);
if (rets != 0) {
String error = "end failed" + rets;
Log.e(TAG, error);
}
}
}
};
/**
* 释放资源
*/
public static void destory(){
// AiHelper.getInst().unInit();
cacheArray = null;
}
}
讯飞离线语音合成新版(Aikit)-android sdk合成 demo(Java版本)的更多相关文章
- 重装Eclipse、离线安装ADT、Android SDK
由于最新的ADT.Android SDK需要最新版本的Eclipse才能使用,我无奈的只好升级Eclipse.看看自己的Eclipse已经两年没有升级了,也是时候升级了.升级前,有很多的顾虑.因为像这 ...
- 基于讯飞语音API应用开发之——离线词典构建
最近实习在做一个跟语音相关的项目,就在度娘上搜索了很多关于语音的API,顺藤摸瓜找到了科大讯飞,虽然度娘自家也有语音识别.语义理解这块,但感觉应该不是很好用,毕竟之前用过百度地图的API,有问题也找不 ...
- 使用讯飞tts+ffmpeg自动生成视频
参考 FFmpeg 讯飞离线语音合成 起因 某日,看到一个营销号的视频说做视频日进斗金,大意是用软件识别文章小说,搭配一些图片转换成自己的视频.看完当时脑海里冒出一个念头,我也可以,于是有了这番尝试. ...
- Android 离线语音用法(讯飞语音)
这次给大家带来的是项目的离线语音功能. 讯飞开放平台中的离线语音 首先创建开放平台的账号.这个不必多说 然后创建新应用 选择我的应用,例如以下图,注意下我打马赛克的地方,这个appId非常重要 点击进 ...
- android讯飞语音开发常遇到的问题
场景:android项目中共使用了3个语音组件:在线语音听写.离线语音合成.离线语音识别 11208:遇到这个错误,授权应用失败,先检查装机量(3台测试权限),以及appid的申请时间(35天期限), ...
- 讯飞语音JavaWeb语音合成解决方案
在线语音合成 将文字信息转化为声音信息,给应用配上"嘴巴".我们提供了众多极具特色的发音人(音库)供您选择.其合成音在音色.自然度等方面的表现均接近甚至超过了人声.这种语音合成体验 ...
- Android SDK Manager详解
Android基础知识——Android SDK Manager详解 做Android开发时,免不了使用Android SDK Manager,安装需要的sdk版本.buildTools版本等等. ...
- Android sdk目录介绍
android sdk目录介绍 build-tools 各版本SDK编译工具 docs 离线开发者文档Android SDK API参考文档 extras 扩展开发包,如兼容架包. platforms ...
- Ubuntu 15.10 x64 安装 Android SDK
操作系统:Ubuntu 15.10 x64 目标:安装 Android SDK 本文最后更新时间:2015-11-3 安装32位库文件 2013年9月的iPhone 5s是第一款64位手机,而Andr ...
- Android SDK 4.0.3 开发环境配置及运行
最近又装了一次最新版本的ADK环境 目前最新版是Android SDK 4.0.3 本文的插图和文本虽然是Android2.2的 步骤都是一样的,如果安装的过程中遇到什么问题,可以留言,我会尽快回复! ...
随机推荐
- 5.根据uid获取用户所有收货地址信息和设置默认地址
1.总结:昨天主要是实现了根据uid获取该用户的所有收货地址以及设置默认地址:再对默认地址的实现里面让我认识到它的具体操作,首先我们根据aid查询收货地址 再根据收货地址查询到地址归属人的信息,判断u ...
- Sqlmap注入dvwa平台low级别
工具介绍:sqlmap是一款开源的软件 SQL注入攻击是黑客对数据库进行攻击的常用手段之一.随着B/S模式应用开发的发展,使用这种模式编写应用程序的程序员也越来越多.但是由于程序员的水平及经验也参差不 ...
- C# 根据前台传入实体名称,动态查询数据
前言: 项目中时不时遇到查字典表等数据,只需要返回数据,不需要写其他业务,每个字典表可能都需要写一个接口给前端调用,比较麻烦,所以采用下面这种方式,前端只需传入实体名称即可,例如:SysUser 1. ...
- 一文吃透Elasticsearch
本文已经收录到Github仓库,该仓库包含计算机基础.Java基础.多线程.JVM.数据库.Redis.Spring.Mybatis.SpringMVC.SpringBoot.分布式.微服务.设计模式 ...
- RDIFramework.NET代码生成器全新V5.1版本发布
RDIFramework.NET代码生成器介绍 RDIFramework.NET代码生成器,代码.文档一键生成. RDIFramework.NET代码生成器集代码生成.各数据库对象文档生成.数据库常用 ...
- django restframework的简单使用
django restframework的简单使用 1.快速上手 配置: (pip install djangorestframework==3.12.4) # settings.py INSTALL ...
- json解析异常显示{“$ref“:“$[0]“}
在编写测试代码实现一个组织架构树的功能时,部门的parent部门没有显示,出现了json解析异常错误[{"r e f " : " ref":"ref& ...
- 2023-03-06:给定一个二维网格 grid ,其中: ‘.‘ 代表一个空房间 ‘#‘ 代表一堵 ‘@‘ 是起点 小写字母代表钥匙 大写字母代表锁 我们从起点开始出发,一次移动是指向四个基本方向之
2023-03-06:给定一个二维网格 grid ,其中: '.' 代表一个空房间 '#' 代表一堵 '@' 是起点 小写字母代表钥匙 大写字母代表锁 我们从起点开始出发,一次移动是指向四个基本方向之 ...
- 2020-12-07:go中,slice的底层数据结构是什么?
福哥答案2020-12-07: 源码位于runtime/slice.go文件中的slice结构体. type slice struct { array unsafe.Pointer len int c ...
- 2021-07-17:一个不含有负数的数组可以代表一圈环形山,每个位置的值代表山的高度。比如, {3,1,2,4,5}、{4,5,3,1,2}或{1,2,4,5,3}都代表同样结构的环形山。山峰A和山
2021-07-17:一个不含有负数的数组可以代表一圈环形山,每个位置的值代表山的高度.比如, {3,1,2,4,5}.{4,5,3,1,2}或{1,2,4,5,3}都代表同样结构的环形山.山峰A和山 ...