Android 百度语音合成 (含离线、在线、API合成方式,详细步骤+源码) 您所在的位置:网站首页 百度ai在线语音合成 Android 百度语音合成 (含离线、在线、API合成方式,详细步骤+源码)

Android 百度语音合成 (含离线、在线、API合成方式,详细步骤+源码)

2024-01-08 23:17| 来源: 网络整理| 查看: 265

百度语音合成 声明前言正文一、创建项目二、离线语音合成1. 配置AndroidManifest.xml2. 配置SDK3. 离线SDK初始化4. 导包5. 运行 三、在线语音合成 - SDK方式1. 创建页面2. 编辑代码3. 配置4. 运行 四、在线语音合成 - API方式1. 鉴权返回实体2. 添加框架依赖3. 搭建网络请求框架4. 编辑布局和页面5. 获取鉴权Token6. 动态权限请求7. Api语音合成8. 音频文件下载9. 播放 五、源码

声明

  本文代码请使用真机运行,别用模拟器虚拟机,谢谢!

前言

  我之前写过百度的语音识别,也写过讯飞的语音识别与合成,而有读者看完后说没有百度的语音合成,想在用百度语音识别的同时使用百度的语音合成。所以就有了这篇文章,我的文章也是区别于其他人的文章,所以我有自己的风格。

感兴趣可以先扫码下载体验一下,再决定往不往下面看。

在这里插入图片描述

正文

  首先我们登录这个百度智能云,然后找到语音技术。 在这里插入图片描述 点击创建应用 在这里插入图片描述 在这里插入图片描述 这里选择包名,如果你选择不需要,则只能通过网络API来实现你的语音合成,而选择Android的话就不光可以使用API还能使用SDK,不过这样的话对APK的大小会有增加。 在这里插入图片描述 这里我选择的是Android,因此需要建一个Android项目。

一、创建项目

在这里插入图片描述 先把这个com.llw.speechsynthesis包名填进去。 在这里插入图片描述 立即创建 在这里插入图片描述 查看应用详情。 在这里插入图片描述 这几个值在后面会用到的,记下来。然后回到列表中,领取免费的使用额度。

在这里插入图片描述 注意看这个提示,说明这个额度是有期限的。 在这里插入图片描述 领取之后。

二、离线语音合成

点击左侧的离线合成SDK 在这里插入图片描述 选择应用后,点击确定。 在这里插入图片描述 可以看到激活的30天内,我是5月6号激活,可能你后面看文章的时候就已经是不能用了,所以不要拿到源码之后问我为什么用不了,那只能说明你没有看文章。 在这里插入图片描述 这里看这个是单台设备授权,所以你想要增多的话就要花钱了,点击下载SDK。 在这里插入图片描述

注意这个还要激活SDK才行的。激活是需要序列号的,那么这个序列号那里来呢?点击查看详情 在这里插入图片描述 下载序列号列表,下载后打开如下 在这里插入图片描述 现在这序列号就有了,下面回到 在这里插入图片描述

下载这个SDK

在这里插入图片描述

下载后解压,下面正式来配置这个离线的语音合成了。

1. 配置AndroidManifest.xml

打开项目的AndroidManifest.xml,添加权限。

然后适配api 28以上版本。

添加位置如下图。 在这里插入图片描述

2. 配置SDK

打开刚才的SDK,进入到libs文件夹下 在这里插入图片描述 将这个jar包复制到你的项目的libs下。 在这里插入图片描述 注意到它这个现在是没有展开的,说明还没有加载进去,点击工具栏右上方的小象图标进行项目资源同步。 在这里插入图片描述 同步后的你的jar就加载到项目中了,就是可以展开的。 在这里插入图片描述

进入到main文件夹下 在这里插入图片描述 复制assets和jniLibs这两个文件夹到你的项目的main下面。 在这里插入图片描述 然后展开你的assets文件夹,打开auth.properties文件。修改里面的一些内容。 在这里插入图片描述 这里面的五个值都需要进行修改,前三个值是我们在创建平台应用时生成的,我当时说了你要记下来,就是为了这里使用。那么你只要一一的对应填写替换就可以了,而applicationId:就是我们之前填写的包名,最后的sn:就是下载的序列号,有两个,任意一个都可以。那么将上面的数据改了之后如下所示: 在这里插入图片描述

3. 离线SDK初始化

离线SDK第一次初始化的时候需要联网,进行网络鉴权,鉴权成功之后就可以断网使用了,先完成这个初始化操作。修activity_main.xml

然后在MainActivity中写入一个方法:

/** * 离线SDK合成 * @param view */ public void offlineSDK(View view) { }

当点击这个方法时会进入到离线SDK的页面,这个页面现在还没有的,不过我们的下载SDK里面有现成的,那就拿过来就用好了。

首先将layout下的activity_synth.xml文件复制到项目的layout下。 在这里插入图片描述 在这里插入图片描述 然后将res文件夹下的raw文件夹复制到你的项目的res下: 在这里插入图片描述

在这里插入图片描述 然后就是里面的一些配置类了。 将sample包下的选择的文件和文件夹复制到你的项目的包下。 在这里插入图片描述

在这里插入图片描述

4. 导包

然后依次打开里面的粘贴过来的类,首先是control包下的InitConfig类,里面是会有报错的,因为包名不一致。所以需要重新导包。 在这里插入图片描述 点击import左边的加号或者右边的省略号查看里面的导包信息 在这里插入图片描述 看到这里是报红的。删掉我标注的这一行错误的导包信息。然后往下滑动,到下方你点击报红的这个类,会出现一个提示如下图所示:可以通过快捷键Alt + Enter快速导包 在这里插入图片描述 导包之后这个类就不报错了,就能正常使用了 在这里插入图片描述 那么你刚才复制过来的类都需要重新打开一次,看看里面的包是否有异常,有的话就按刚才的方法来解决就好了。当你把所有的类检查一遍之后,确保都没有异常之后,就可以开始进行这个初始化了。

修改MainActivity中的代码

/** * 离线SDK合成 * @param view */ public void offlineSDK(View view) { startActivity(new Intent(this,SynthActivity.class)); }

点击这个按钮跳转到SynthActivity中。别忘了要在AndroidManifest.xml中注册这个Activity。 在这里插入图片描述

5. 运行

下面运行一下:

在这里插入图片描述 是有声音的,不过这是GIF图,所以你只能看到我的演示效果。那么到此为止,这个离线合成就弄完了,具体的细节你要多看这个SDK的代码,我个人觉得代码太多了,有些乱。

三、在线语音合成 - SDK方式 1. 创建页面

在线合成的方式其实和离线差不了多少,在com.llw.speechsynthesis包下新建一个OnlineActivity,布局是activity_online.xml,布局代码如下:

下面再来看OnlineActivity的代码

2. 编辑代码 package com.llw.speechsynthesis; import android.Manifest; import android.content.pm.PackageManager; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import com.baidu.tts.chainofresponsibility.logger.LoggerProxy; import com.baidu.tts.client.SpeechSynthesizer; import com.baidu.tts.client.SpeechSynthesizerListener; import com.baidu.tts.client.TtsMode; import com.llw.speechsynthesis.control.InitConfig; import com.llw.speechsynthesis.listener.UiMessageListener; import com.llw.speechsynthesis.util.Auth; import com.llw.speechsynthesis.util.AutoCheck; import com.llw.speechsynthesis.util.FileUtil; import com.llw.speechsynthesis.util.IOfflineResourceConst; import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; /** * 除了SDK,该类没有任何依赖,可以直接复制进你的项目 *

* 默认TEMP_DIR = "/sdcard/baiduTTS"; // 重要!请手动将assets目录下的3个dat 文件复制到该目录 * 确保 TEXT_FILENAME 和 MODEL_FILENAME 存在 * Created by fujiayi on 2017/9/14. */ public class OnlineActivity extends AppCompatActivity implements IOfflineResourceConst { /** * 要合成的文本,可以自行改动。 */ private static final String TEXT = "欢迎使用百度语音合成,请在代码中修改合成文本"; protected String appId; protected String appKey; protected String secretKey; protected String sn; // 纯离线合成SDK授权码;离在线合成SDK没有此参数 //TtsMode.ONLINE 纯在线 private TtsMode ttsMode = TtsMode.ONLINE; private boolean isOnlineSDK = TtsMode.ONLINE.equals(DEFAULT_SDK_TTS_MODE); // ================ 纯离线sdk或者选择TtsMode.ONLINE 以下参数无用; private static final String TEMP_DIR = "/sdcard/baiduTTS"; // 重要!请手动将assets目录下的3个dat 文件复制到该目录 // 请确保该PATH下有这个文件 private static final String TEXT_FILENAME = TEMP_DIR + "/" + TEXT_MODEL; // 请确保该PATH下有这个文件 ,m15是离线男声 private static final String MODEL_FILENAME = TEMP_DIR + "/" + VOICE_MALE_MODEL; // ===============初始化参数设置完毕,更多合成参数请至getParams()方法中设置 ================= protected SpeechSynthesizer mSpeechSynthesizer; // =========== 以下为UI部分 ================================================== private TextView mShowText; protected Handler mainHandler; private String desc; // 说明文件 private static final String TAG = "MiniActivity"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); appId = Auth.getInstance(this).getAppId(); appKey = Auth.getInstance(this).getAppKey(); secretKey = Auth.getInstance(this).getSecretKey(); sn = Auth.getInstance(this).getSn(); // 纯离线合成必须有此参数;离在线合成SDK没有此参数 desc = FileUtil.getResourceText(this, R.raw.mini_activity_description); setContentView(R.layout.activity_online); initView(); initPermission(); initTTs(); } /** * 注意此处为了说明流程,故意在UI线程中调用。 * 实际集成中,该方法一定在新线程中调用,并且该线程不能结束。具体可以参考NonBlockSyntherizer的写法 */ private void initTTs() { LoggerProxy.printable(true); // 日志打印在logcat中 boolean isSuccess; if (!isOnlineSDK) { // 检查2个离线资源是否可读 isSuccess = checkOfflineResources(); if (!isSuccess) { return; } else { print("离线资源存在并且可读, 目录:" + TEMP_DIR); } } // 日志更新在UI中,可以换成MessageListener,在logcat中查看日志 SpeechSynthesizerListener listener = new UiMessageListener(mainHandler); // 1. 获取实例 mSpeechSynthesizer = SpeechSynthesizer.getInstance(); mSpeechSynthesizer.setContext(this); // 2. 设置listener mSpeechSynthesizer.setSpeechSynthesizerListener(listener); // 3. 设置appId,appKey.secretKey int result = mSpeechSynthesizer.setAppId(appId); checkResult(result, "setAppId"); result = mSpeechSynthesizer.setApiKey(appKey, secretKey); checkResult(result, "setApiKey"); // 4. 如果是纯离线SDK需要离线功能的话 if (!isOnlineSDK) { // 文本模型文件路径 (离线引擎使用), 注意TEXT_FILENAME必须存在并且可读 mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_TTS_TEXT_MODEL_FILE, TEXT_FILENAME); // 声学模型文件路径 (离线引擎使用), 注意TEXT_FILENAME必须存在并且可读 mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_TTS_SPEECH_MODEL_FILE, MODEL_FILENAME); mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_MIX_MODE, SpeechSynthesizer.MIX_MODE_DEFAULT); // 该参数设置为TtsMode.MIX生效。 // MIX_MODE_DEFAULT 默认 ,wifi状态下使用在线,非wifi离线。在线状态下,请求超时6s自动转离线 // MIX_MODE_HIGH_SPEED_SYNTHESIZE_WIFI wifi状态下使用在线,非wifi离线。在线状态下, 请求超时1.2s自动转离线 // MIX_MODE_HIGH_SPEED_NETWORK , 3G 4G wifi状态下使用在线,其它状态离线。在线状态下,请求超时1.2s自动转离线 // MIX_MODE_HIGH_SPEED_SYNTHESIZE, 2G 3G 4G wifi状态下使用在线,其它状态离线。在线状态下,请求超时1.2s自动转离线 } // 5. 以下setParam 参数选填。不填写则默认值生效 // 设置在线发声音人: 0 普通女声(默认) 1 普通男声 3 情感男声 4 情感儿童声 mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_SPEAKER, "0"); // 设置合成的音量,0-15 ,默认 5 mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_VOLUME, "9"); // 设置合成的语速,0-15 ,默认 5 mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_SPEED, "5"); // 设置合成的语调,0-15 ,默认 5 mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_PITCH, "5"); // mSpeechSynthesizer.setAudioStreamType(AudioManager.MODE_IN_CALL); // 调整音频输出 if (sn != null) { // 纯离线sdk这个参数必填;离在线sdk没有此参数 mSpeechSynthesizer.setParam(PARAM_SN_NAME, sn); } // x. 额外 : 自动so文件是否复制正确及上面设置的参数 Map params = new HashMap(); // 复制下上面的 mSpeechSynthesizer.setParam参数 // 上线时请删除AutoCheck的调用 if (!isOnlineSDK) { params.put(SpeechSynthesizer.PARAM_TTS_TEXT_MODEL_FILE, TEXT_FILENAME); params.put(SpeechSynthesizer.PARAM_TTS_SPEECH_MODEL_FILE, MODEL_FILENAME); } // 检测参数,通过一次后可以去除,出问题再打开debug InitConfig initConfig = new InitConfig(appId, appKey, secretKey, ttsMode, params, listener); AutoCheck.getInstance(getApplicationContext()).check(initConfig, new Handler() { @Override /** * 开新线程检查,成功后回调 */ public void handleMessage(Message msg) { if (msg.what == 100) { AutoCheck autoCheck = (AutoCheck) msg.obj; synchronized (autoCheck) { String message = autoCheck.obtainDebugMessage(); print(message); // 可以用下面一行替代,在logcat中查看代码 // Log.w("AutoCheckMessage", message); } } } }); // 6. 初始化 result = mSpeechSynthesizer.initTts(ttsMode); checkResult(result, "initTts"); } /** * 在线SDK不需要调用,纯离线SDK会检查资源文件 * * 检查 TEXT_FILENAME, MODEL_FILENAME 这2个文件是否存在,不存在请自行从assets目录里手动复制 * * @return 检测是否成功 */ private boolean checkOfflineResources() { String[] filenames = {TEXT_FILENAME, MODEL_FILENAME}; for (String path : filenames) { File f = new File(path); if (!f.canRead()) { print("[ERROR] 文件不存在或者不可读取,请从demo的assets目录复制同名文件到:" + f.getAbsolutePath()); print("[ERROR] 初始化失败!!!"); return false; } } return true; } private void speak() { /* 以下参数每次合成时都可以修改 * mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_SPEAKER, "0"); * 设置在线发声音人: 0 普通女声(默认) 1 普通男声 3 情感男声 4 情感儿童声 * mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_VOLUME, "5"); 设置合成的音量,0-15 ,默认 5 * mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_SPEED, "5"); 设置合成的语速,0-15 ,默认 5 * mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_PITCH, "5"); 设置合成的语调,0-15 ,默认 5 * */ if (mSpeechSynthesizer == null) { print("[ERROR], 初始化失败"); return; } int result = mSpeechSynthesizer.speak(TEXT); mShowText.setText(""); print("合成并播放 按钮已经点击"); checkResult(result, "speak"); } private void stop() { print("停止合成引擎 按钮已经点击"); int result = mSpeechSynthesizer.stop(); checkResult(result, "stop"); } // 下面是UI部分 private void initView() { Button mSpeak = this.findViewById(R.id.speak); Button mStop = this.findViewById(R.id.stop); mShowText = this.findViewById(R.id.showText); mShowText.setText(desc); View.OnClickListener listener = new View.OnClickListener() { @Override public void onClick(View v) { int id = v.getId(); switch (id) { case R.id.speak: speak(); break; case R.id.stop: stop(); break; default: break; } } }; mSpeak.setOnClickListener(listener); mStop.setOnClickListener(listener); mainHandler = new Handler() { /* * @param msg */ @Override public void handleMessage(Message msg) { super.handleMessage(msg); if (msg.obj != null) { print(msg.obj.toString()); } } }; } private void print(String message) { Log.i(TAG, message); mShowText.append(message + "\n"); } @Override protected void onDestroy() { if (mSpeechSynthesizer != null) { mSpeechSynthesizer.stop(); mSpeechSynthesizer.release(); mSpeechSynthesizer = null; print("释放资源成功"); } super.onDestroy(); } private void checkResult(int result, String method) { if (result != 0) { print("error code :" + result + " method:" + method); } } // 下面是android 6.0以上的动态授权 /** * android 6.0 以上需要动态申请权限 */ private void initPermission() { 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 }; ArrayList toApplyList = new ArrayList(); for (String perm : permissions) { if (PackageManager.PERMISSION_GRANTED != ContextCompat.checkSelfPermission(this, perm)) { toApplyList.add(perm); // 进入到这里代表没有权限. } } String[] tmpList = new String[toApplyList.size()]; if (!toApplyList.isEmpty()) { ActivityCompat.requestPermissions(this, toApplyList.toArray(tmpList), 123); } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { // 此处为android 6.0以上动态授权的回调,用户自行实现。 } }

这里的代码其实都是这个SDK中的,直接就可以使用了。我只改动了一点点。

3. 配置

然后修改AndroidManifest.xml 在这里插入图片描述

然后在activity_main.xml中增加一个按钮。

在MainActivity中增加方法。

/** * 在线SDK合成 * @param view */ public void onlineSDK(View view) { startActivity(new Intent(this, OnlineActivity.class)); } 4. 运行

下面运行:

在这里插入图片描述 可以看到在线SDK合成,没有网络时是合成不了的,有网络才行,这里的声音是女声。

四、在线语音合成 - API方式

使用API方式就稍稍有一些麻烦,因为这个设计到网络的请求,而且不是一次请求,首先进行鉴权,拿到token,然后通过Token去请求合成,下载MP3文件,首先要构建网络模块,当然我也只是简单的写一下而已。

1. 鉴权返回实体

在com.llw.imagediscerndemo下新建一个model包,包下新建一个GetTokenResponse类,代码如下:

package com.llw.speechsynthesis.model; /** * 获取鉴权认证Token响应实体 * * @author llw * @date 2021/5/7 16:16 */ public class GetTokenResponse { /** * refresh_token : 25.0141c302b0f460cd0500827fa31f22ce.315360000.1935736936.282335-24113250 * expires_in : 2592000 * session_key : 9mzdCS6a/7/wIFWLR8zFoYs2koSri++RGhSecVXM/vY93At4kxYRajL/xMV17MoxcTAJfadRVaSBxokIqFeQoxsZ8e3NPQ== * access_token : 24.2830c05696b214cf07bfbdf764599b39.2592000.1622968936.282335-24113250 * scope : audio_voice_assistant_get brain_enhanced_asr audio_tts_post brain_speech_realtime public brain_all_scope picchain_test_picchain_api_scope brain_asr_async wise_adapt lebo_resource_base lightservice_public hetu_basic lightcms_map_poi kaidian_kaidian ApsMisTest_Test权限 vis-classify_flower lpq_开放 cop_helloScope ApsMis_fangdi_permission smartapp_snsapi_base smartapp_mapp_dev_manage iop_autocar oauth_tp_app smartapp_smart_game_openapi oauth_sessionkey smartapp_swanid_verify smartapp_opensource_openapi smartapp_opensource_recapi fake_face_detect_开放Scope vis-ocr_虚拟人物助理 idl-video_虚拟人物助理 smartapp_component smartapp_search_plugin avatar_video_test * session_secret : 2cdde5fd8f3fd4394c1b090e2ffa2d1c */ private String refresh_token; private int expires_in; private String session_key; private String access_token; private String scope; private String session_secret; public String getRefresh_token() { return refresh_token; } public void setRefresh_token(String refresh_token) { this.refresh_token = refresh_token; } public int getExpires_in() { return expires_in; } public void setExpires_in(int expires_in) { this.expires_in = expires_in; } public String getSession_key() { return session_key; } public void setSession_key(String session_key) { this.session_key = session_key; } public String getAccess_token() { return access_token; } public void setAccess_token(String access_token) { this.access_token = access_token; } public String getScope() { return scope; } public void setScope(String scope) { this.scope = scope; } public String getSession_secret() { return session_secret; } public void setSession_secret(String session_secret) { this.session_secret = session_secret; } }

下面简单的写一个网络请求框架。

2. 添加框架依赖

打开你的app的build.gradle,在dependencise{}闭包下添加如下依赖:

//retrofit2 implementation 'com.squareup.retrofit2:retrofit:2.4.0' implementation 'com.squareup.retrofit2:converter-gson:2.4.0' implementation 'com.squareup.okhttp3:logging-interceptor:3.4.1' //权限请求框架 implementation 'com.tbruyelle.rxpermissions2:rxpermissions:0.9.4@aar' implementation 'io.reactivex.rxjava2:rxandroid:2.0.2' implementation "io.reactivex.rxjava2:rxjava:2.0.0"

然后在android{}闭包下添加JDK1.8的支持

compileOptions { sourceCompatibility = 1.8 targetCompatibility = 1.8 }

在这里插入图片描述 记得要Sync Now,这里的依赖一个是网络,一个是权限请求,后面都会用到的。

3. 搭建网络请求框架

在com.llw.speechsynthesis下新建一个network包,在这个包下新建一个NetCallBack抽象类。里面的代码如下:

package com.llw.speechsynthesis.network; import android.util.Log; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; /** * 网络请求回调 * * @param */ public abstract class NetCallBack implements Callback {//这里实现了retrofit2.Callback //访问成功回调 @Override public void onResponse(Call call, Response response) {//数据返回 if (response != null && response.body() != null && response.isSuccessful()) { onSuccess(call, response); } else { onFailed(response.raw().toString()); } } //访问失败回调 @Override public void onFailure(Call call, Throwable t) { Log.d("data str", t.toString()); onFailed(t.toString()); } //数据返回 public abstract void onSuccess(Call call, Response response); //失败异常 public abstract void onFailed(String errorStr); }

然后在network包下新增一个ServiceGenerator类,里面的代码如下:

package com.llw.speechsynthesis.network; import java.util.concurrent.TimeUnit; import okhttp3.OkHttpClient; import okhttp3.logging.HttpLoggingInterceptor; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; /** * 接口地址管理 * * @author llw */ public class ServiceGenerator { public static String BASE_URL = null; public static String getBaseUrl(int type) { switch (type) { case 0://鉴权地址 BASE_URL = "https://openapi.baidu.com"; break; case 1://合成地址 BASE_URL = "https://tsn.baidu.com"; break; default: break; } return BASE_URL; } /** * 创建服务 参数就是API服务 * * @param serviceClass 服务接口 * @param 泛型规范 * @return api接口服务 */ public static T createService(Class serviceClass, int type) { //创建OkHttpClient构建器对象 OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder(); //设置请求超时的时间,这里是10秒 okHttpClientBuilder.connectTimeout(20000, TimeUnit.MILLISECONDS); //消息拦截器 因为有时候接口不同在排错的时候 需要先从接口的响应中做分析。利用了消息拦截器可以清楚的看到接口返回的所有内容 HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(); //setlevel用来设置日志打印的级别,共包括了四个级别:NONE,BASIC,HEADER,BODY //BASEIC:请求/响应行 //HEADER:请求/响应行 + 头 //BODY:请求/响应航 + 头 + 体 httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); //为OkHttp添加消息拦截器 okHttpClientBuilder.addInterceptor(httpLoggingInterceptor); //在Retrofit中设置httpclient //设置地址 就是上面的固定地址,如果你是本地访问的话,可以拼接上端口号 例如 +":8080" Retrofit retrofit = new Retrofit.Builder().baseUrl(getBaseUrl(type)) //用Gson把服务端返回的json数据解析成实体 .addConverterFactory(GsonConverterFactory.create()) //放入OKHttp,之前说过retrofit是对OkHttp的进一步封装 .client(okHttpClientBuilder.build()) .build(); //返回这个创建好的API服务 return retrofit.create(serviceClass); } }

下面写接口,在network包下新增ApiService接口,代码如下:

package com.llw.speechsynthesis.network; import com.llw.speechsynthesis.model.GetTokenResponse; import okhttp3.ResponseBody; import retrofit2.Call; import retrofit2.http.Field; import retrofit2.http.FormUrlEncoded; import retrofit2.http.POST; import retrofit2.http.Streaming; /** * API服务 * * @author llw * @date 2021/5/8 10:48 */ public interface ApiService { /** * 获取鉴权认证Token * @param grant_type 类型 * @param client_id API Key * @param client_secret Secret Key * @return GetTokenResponse */ @FormUrlEncoded @POST("/oauth/2.0/token") Call getToken(@Field("grant_type") String grant_type, @Field("client_id") String client_id, @Field("client_secret") String client_secret); /** * 在线API音频合成 * @param tok 鉴权token * @param ctp 客户端类型选择,web端填写固定值1 * @param cuid 用户唯一标识,用来计算UV值。建议填写能区分用户的机器 MAC 地址或 IMEI 码,长度为60字符以内 * @param lan 固定值zh。语言选择,目前只有中英文混合模式,填写固定值zh * @param tex 合成的文本,使用UTF-8编码。小于2048个中文字或者英文数字,文本在百度服务器内转换为GBK后,长度必须小于4096字节(5003、5118发音人需小于512个中文字或者英文数字) * @return 正常合成之后返回一个音频文件 */ @Streaming @FormUrlEncoded @POST("/text2audio") Call synthesis(@Field("tok") String tok, @Field("ctp") String ctp, @Field("cuid") String cuid, @Field("lan") String lan, @Field("tex") String tex); }

里面有两个接口,一个是用来获取鉴权Token的,另一个是用来将文字合成音频文件的。这里会比较的麻烦一些。到此为止这个简单的网络框架就写好了。

4. 编辑布局和页面

在com.llw.speechsynthesis下新建一个OnlineAPIActivity,对应的布局是activity_online_api.xml,里面的代码如下:

下面先到AndroidManifest.xml中去配置Title。 在这里插入图片描述 下面回到OnlineAPIActivity看原始的代码是什么样子。

package com.llw.speechsynthesis; import android.os.Bundle; import androidx.appcompat.app.AppCompatActivity; /** * 在线API合成 * @author llw */ public class OnlineAPIActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_online_api); } }

先来完成页面的初始化。现在布局的控件有三个

声明变量

private static final String TAG = "OnlineAPIActivity"; /** * 输入框 */ private EditText etText; /** * 页面按钮 */ private Button btnSynthApi, btnPlay;

写一个初始化页面的方法

/** * 初始化 */ private void initView() { etText = findViewById(R.id.et_text); btnSynthApi = findViewById(R.id.btn_synth_api); btnPlay = findViewById(R.id.btn_play); btnSynthApi.setOnClickListener(this); btnPlay.setOnClickListener(this); }

这里我给两个按钮添加了点击的监听,那么你需要给Activity实现控件的点击监听。 在这里插入图片描述 然后重写onClick方法

@Override public void onClick(View v) { switch (v.getId()) { case R.id.btn_synth_api://在线API合成 break; case R.id.btn_play://播放音频 break; default: break; } }

然后要在onCreate方法中调用initView()方法。 在这里插入图片描述

5. 获取鉴权Token

声明变量

/** * Api服务 */ private ApiService service; /** * 鉴权Toeken */ private String accessToken;

然后新增一个requestApiGetToken方法,代码如下:

/** * 访问API获取接口 */ private void requestApiGetToken() { String grantType = "client_credentials"; String apiKey = "sKWlGNoBrNyaKaAycoiKFzdT"; String apiSecret = "OwEPWPiSnMNxCF5GFZlORKzP01KwgC1Z"; service = ServiceGenerator.createService(ApiService.class, 0); service.getToken(grantType, apiKey, apiSecret) .enqueue(new NetCallBack() { @Override public void onSuccess(Call call, Response response) { if (response.body() != null) { //鉴权Token accessToken = response.body().getAccess_token(); Log.d(TAG, accessToken); } } @Override public void onFailed(String errorStr) { Log.e(TAG, "获取Token失败,失败原因:" + errorStr); accessToken = null; } }); }

这里的apiKey、apiSecret 的值改成自己平台创建应用时产生,你要是用我的,除了问题又问我为什么,我就只能。。。了。当然也要在onCreate中调用,这样我们已经入页面就会请求接口拿到鉴权Token。 在这里插入图片描述

下面我们运行一下,不过要先在MainActivity中写一个入口才行,在activity_main.xml中增加一个按钮。

然后在MainActivity中增加方法

/** * 在线API合成 * @param view */ public void onlineAPI(View view) { startActivity(new Intent(this,OnlineAPIActivity.class)); }

那么现在你就可以运行了。

在这里插入图片描述 看起来好像什么都没有做是吧。你过你看看控制台的打印。 在这里插入图片描述 这里的鉴权Token就拿到了,这种方式用户就是无感知的。其实这个鉴权Token还有优化的空间,至于怎么做,我在其他的文章写过了,你也可以自己实践。

6. 动态权限请求

因为接口请求之后会下载一个文件到手机本地,因此你需要文件读写权限、

声明变量

/** * 权限请求框架 */ private RxPermissions rxPermissions;

然后在initView中实例化。 在这里插入图片描述 然后新怎一个方法

/** * android 6.0 以上需要动态申请权限 */ @SuppressLint("CheckResult") private void requestPermission() { rxPermissions.request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE) .subscribe(grant -> { if (grant) { //获得权限 } else { Toast.makeText(OnlineAPIActivity.this,"未获取到权限",Toast.LENGTH_SHORT).show(); } }); }

这里也是很简单的代码,当点击在线合成API按钮时,先调用requestPermission方法进行权限的检查。 在这里插入图片描述

7. Api语音合成

这里合成是读取页面中的文本,如果输入框的内容为空则使用默认文字进行语音合成,因此需要一个默认的文本。

声明变量

/** * 默认文本,当输入框未输入使用, */ private String defaultText = "你好!百度。";

然后在权限通过的地方加上这样的一段代码

//如果输入框的内容为空则使用默认文字进行语音合成 String text; if (etText.getText().toString().trim().isEmpty()) { text = defaultText; } else { text = etText.getText().toString().trim(); }

在这里插入图片描述 这段代码产生了一个文本变量,需要将它传到下一个方法中,也就是合成的方法。下面来写这个方法,前面都是铺垫,让你了解这个过程,它是一步一步来的。新增方法requestSynth,代码如下:

/** * 合成请求 * @param text 需要合成语音的文本 */ private void requestSynth(String text) { service = ServiceGenerator.createService(ApiService.class, 1); service.synthesis(accessToken, "1", String.valueOf(System.currentTimeMillis()), "zh", text) .enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { if (response.isSuccessful()) { Log.d(TAG,"请求成功"); } else { Log.d(TAG, "请求失败"); } } @Override public void onFailure(Call call, Throwable t) { Log.e(TAG, "error"); } }); }

然后在这里调用。 在这里插入图片描述 下面可以运行了,会打印请求的结果。 在这里插入图片描述 这里点击按钮之后会请求权限,通过后会获取文本,然后进行语音合成的请求,来看看那控制台打印的结果。 在这里插入图片描述 请求成功了,那么可以进行下一步了。

8. 音频文件下载

因为这里返回的是一个音频文件,因此不能使用常规的方式来处理,下载当然是下载的项目的缓存目录里面去,当前我在Android10.0上是可以实践的,Android11.0可能要进行分区存储才行,这里说明一下。

在listener包下新增一个DownloadListener接口,里面的代码如下:

package com.llw.speechsynthesis.listener; /** * 下载监听 * * @author llw * @date 2021/5/8 9:50 */ public interface DownloadListener { /** * 开始下载 */ void onStart(); /** * 下载进度 * @param progress 当前进度 */ void onProgress(int progress); /** * 下载完成 * @param path 文件路径 */ void onFinish(String path); /** * 下载失败 * @param errorInfo 错误信息 */ void onFail(String errorInfo); }

然后回到OnlineAPIActivity中,声明变量

/** * 文件路径 */ private String mPath; /** * 缓冲区大小 */ private static int sBufferSize = 8192; /** * 文件 */ private File file;

接口的回调

/** * 下载文件监听 */ private DownloadListener listener = new DownloadListener() { @Override public void onStart() { Log.d(TAG, "开始"); } @Override public void onProgress(int progress) { Log.d(TAG, "进度:" + progress); } @Override public void onFinish(String path) { Log.d(TAG, "完成:" + path); mPath = path; //显示播放控件 btnPlay.setVisibility(View.VISIBLE); } @Override public void onFail(String errorInfo) { Log.d(TAG, "异常:" + errorInfo); } };

然后新增一个写入磁盘的方法。

/** * 写入磁盘 * @param response 响应体 * @param downloadListener 下载监听 */ private void writeToDisk(Response response, DownloadListener downloadListener) { //开始下载 downloadListener.onStart(); //输入流 将输入流写入文件 InputStream is = response.body().byteStream(); //文件总长 long totalLength = response.body().contentLength(); //设置文件存放路径 file = new File(getExternalCacheDir() + "/Speech/" + "test.mp3"); //创建文件 if (!file.exists()) { if (!file.getParentFile().exists()) { file.getParentFile().mkdir(); } try { file.createNewFile(); } catch (IOException e) { e.printStackTrace(); downloadListener.onFail("createNewFile IOException"); } } //输出流 OutputStream os = null; long currentLength = 0; try { os = new BufferedOutputStream(new FileOutputStream(file)); byte data[] = new byte[sBufferSize]; int len; while ((len = is.read(data, 0, sBufferSize)) != -1) { os.write(data, 0, len); currentLength += len; //计算当前下载进度 downloadListener.onProgress((int) (100 * currentLength / totalLength)); } //下载完成,并返回保存的文件路径 downloadListener.onFinish(file.getAbsolutePath()); } catch (IOException e) { e.printStackTrace(); downloadListener.onFail("IOException"); } finally { try { is.close(); } catch (IOException e) { e.printStackTrace(); } try { if (os != null) { os.close(); } } catch (IOException e) { e.printStackTrace(); } } }

然后在请求成功的分支中调用这个方法,如下图所示: 在这里插入图片描述 下面你可以运行一下: 在这里插入图片描述 合成之后,当文件下载到本地时,这个播放的按钮就会出现。下面来看看日志。

在这里插入图片描述 这样就成功了。

9. 播放

文件下载成功之后,也拿到了文件的路径了,下面就是通过这个路径去播放这个音频了。 新增一个play方法。

/** * 播放 */ private void play() { if(mPath != null){ MediaPlayer mediaPlayer = new MediaPlayer(); try { mediaPlayer.setDataSource(mPath); mediaPlayer.prepare(); mediaPlayer.start(); } catch (IOException e) { e.printStackTrace(); } } }

在这里插入图片描述 这样就可以了。那么代码就写完了。运行一下:

在这里插入图片描述 由于是GIF,所以你听不到声音,来看这个打印的信息,一次是默认的,一次是我们自己的。 在这里插入图片描述 那么到此为止,我的所有代码就写完了。

五、源码

GitHub源码地址:SpeechSynthesisDemo

CSDN源码下载:SpeechSynthesisDemo.rar

如果本文对你有所帮助,不妨点个赞或者评论一下,也可以说说你的想法和问题,我是初学者-Study,山高水长,后会有期~



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有