百度语音合成

  • 声明
  • 前言
  • 正文
    • 一、创建项目
    • 二、离线语音合成
      • 1. 配置AndroidManifest.xml
      • 2. 配置SDK
      • 3. 离线SDK初始化
      • 4. 导包
      • 5. 运行
    • 三、在线语音合成 - SDK方式
      • 1. 创建页面
      • 2. 编辑代码
      • 3. 配置
      • 4. 运行
    • 四、在线语音合成 - API方式
      • 1. 鉴权返回实体
      • 2. 添加框架依赖
      • 3. 搭建网络请求框架
      • 4. 编辑布局和页面
      • 5. 获取鉴权Token
      • 6. 动态权限请求
      • 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,添加权限。

 <uses-permission android:name="android.permission.INTERNET" /><uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /><uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /><uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />

然后适配api 28以上版本。

     <!--支持api level 28 以上编译--><uses-libraryandroid:name="org.apache.http.legacy"android:required="false" />

添加位置如下图。

2. 配置SDK

打开刚才的SDK,进入到libs文件夹下

将这个jar包复制到你的项目的libs下。

注意到它这个现在是没有展开的,说明还没有加载进去,点击工具栏右上方的小象图标进行项目资源同步。

同步后的你的jar就加载到项目中了,就是可以展开的。

进入到main文件夹下

复制assets和jniLibs这两个文件夹到你的项目的main下面。

然后展开你的assets文件夹,打开auth.properties文件。修改里面的一些内容。

这里面的五个值都需要进行修改,前三个值是我们在创建平台应用时生成的,我当时说了你要记下来,就是为了这里使用。那么你只要一一的对应填写替换就可以了,而applicationId:就是我们之前填写的包名,最后的sn:就是下载的序列号,有两个,任意一个都可以。那么将上面的数据改了之后如下所示:

3. 离线SDK初始化

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

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"tools:context=".MainActivity"><Buttonandroid:text="离线SDK合成"android:onClick="offlineSDK"android:layout_width="match_parent"android:layout_height="wrap_content"/></LinearLayout>

然后在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,布局代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="fill_parent"android:layout_height="fill_parent"android:orientation="vertical"><LinearLayoutandroid:layout_width="fill_parent"android:layout_height="50dp"android:orientation="horizontal"android:weightSum="3"><Buttonandroid:id="@+id/speak"android:layout_width="fill_parent"android:layout_height="fill_parent"android:layout_weight="1"android:lines="2"android:text="合成并播放"android:textSize="12dp" /><Buttonandroid:id="@+id/stop"android:layout_width="fill_parent"android:layout_height="fill_parent"android:layout_weight="2"android:lines="2"android:text="停止合成引擎"android:textSize="12dp" /></LinearLayout><ScrollViewandroid:layout_width="match_parent"android:layout_height="match_parent"android:layout_above="@+id/btn"><TextViewandroid:id="@+id/showText"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_margin="10dp"android:background="@android:color/darker_gray"android:minLines="3"android:scrollbars="vertical" /></ScrollView></LinearLayout>

下面再来看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,该类没有任何依赖,可以直接复制进你的项目* <p>* 默认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";@Overrideprotected 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. 设置listenermSpeechSynthesizer.setSpeechSynthesizerListener(listener);// 3. 设置appId,appKey.secretKeyint 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 ,默认 5mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_VOLUME, "9");// 设置合成的语速,0-15 ,默认 5mSpeechSynthesizer.setParam(SpeechSynthesizer.PARAM_SPEED, "5");// 设置合成的语调,0-15 ,默认 5mSpeechSynthesizer.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<String, String> 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);}// 检测参数,通过一次后可以去除,出问题再打开debugInitConfig 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() {@Overridepublic 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*/@Overridepublic 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");}@Overrideprotected 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<String> 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);}}@Overridepublic void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {// 此处为android 6.0以上动态授权的回调,用户自行实现。}
}

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

3. 配置

然后修改AndroidManifest.xml

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

 <Buttonandroid:text="在线SDK合成"android:onClick="onlineSDK"android:layout_width="match_parent"android:layout_height="wrap_content"/>

在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{}闭包下添加如下依赖:

 //retrofit2implementation '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.8targetCompatibility = 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 <T>*/
public abstract class NetCallBack<T> implements Callback<T> {//这里实现了retrofit2.Callback//访问成功回调@Overridepublic void onResponse(Call<T> call, Response<T> response) {//数据返回if (response != null && response.body() != null && response.isSuccessful()) {onSuccess(call, response);} else {onFailed(response.raw().toString());}}//访问失败回调@Overridepublic void onFailure(Call<T> call, Throwable t) {Log.d("data str", t.toString());onFailed(t.toString());}//数据返回public abstract void onSuccess(Call<T> call, Response<T> 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 <T>          泛型规范* @return api接口服务*/public static <T> T createService(Class<T> 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<GetTokenResponse> 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<ResponseBody> 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,里面的代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"tools:context=".OnlineAPIActivity"><EditTextandroid:layout_margin="12dp"android:background="#FFF"android:padding="12dp"android:gravity="top"android:textColor="#000"android:id="@+id/et_text"android:hint="请输入要合成的文本"android:layout_width="match_parent"android:layout_height="100dp"/><Buttonandroid:id="@+id/btn_synth_api"android:text="在线API合成"android:layout_width="match_parent"android:layout_height="wrap_content"/><Buttonandroid:id="@+id/btn_play"android:text="播放合成的音频"android:visibility="gone"android:layout_width="match_parent"android:layout_height="wrap_content"/>
</LinearLayout>

下面先到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 {@Overrideprotected 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方法

 @Overridepublic 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<GetTokenResponse>() {@Overridepublic void onSuccess(Call<GetTokenResponse> call, Response<GetTokenResponse> response) {if (response.body() != null) {//鉴权TokenaccessToken = response.body().getAccess_token();Log.d(TAG, accessToken);}}@Overridepublic void onFailed(String errorStr) {Log.e(TAG, "获取Token失败,失败原因:" + errorStr);accessToken = null;}});}

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

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

 <Buttonandroid:text="在线API合成"android:onClick="onlineAPI"android:layout_width="match_parent"android:layout_height="wrap_content"/>

然后在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<ResponseBody>() {@Overridepublic void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {if (response.isSuccessful()) {Log.d(TAG,"请求成功");} else {Log.d(TAG, "请求失败");}}@Overridepublic void onFailure(Call<ResponseBody> 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() {@Overridepublic void onStart() {Log.d(TAG, "开始");}@Overridepublic void onProgress(int progress) {Log.d(TAG, "进度:" + progress);}@Overridepublic void onFinish(String path) {Log.d(TAG, "完成:" + path);mPath = path;//显示播放控件btnPlay.setVisibility(View.VISIBLE);}@Overridepublic void onFail(String errorInfo) {Log.d(TAG, "异常:" + errorInfo);}};

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

 /*** 写入磁盘* @param response 响应体* @param downloadListener 下载监听*/private void writeToDisk(Response<ResponseBody> 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,山高水长,后会有期~

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

  1. [附源码]计算机毕业设计Python+uniapp基于android手机设计并实现在线点单系统APPo682z(程序+源码+LW+远程部署)

    [附源码]计算机毕业设计Python+uniapp基于android手机设计并实现在线点单系统APPo682z(程序+源码+LW+远程部署) 该项目含有源码.文档.程序.数据库.配套开发软件.软件安装 ...

  2. Android 百度文字识别(详细步骤+源码)

    运行效果图 识别到的内容: {"words_result":[{"words":"突然间有想看书的冲动"},{"words&quo ...

  3. Android 高德地图API(详细步骤+源码)

    高德地图API使用详解 前言 正文 一.创建应用 ① 获取PackageName ② 获取调试版安全码SHA1 ③ 获取发布版安全码SHA1 二.配置Android Studio工程 ① 导入SDK ...

  4. Android 百度语音识别(详细步骤+源码)

    前言 因为项目中用到了语音识别的技术,但是项目源码我不能公开,所以,重新写一个简单的集成教程,不喜可不看,不做键盘侠,文明你我他. 效果图 识别结果 最终效果 源码在文章最后,不需要下载积分什么的,哪 ...

  5. Android 腾讯位置服务使用(详细步骤+源码)

    腾讯位置服务使用 前言 正文 一.注册腾讯位置服务账号 二.创建平台应用Appkey 三.创建并配置AS工程 四.定位 ① 连续定位 ② 单次定位 ③ 后台定位 ④ 地理围栏 五.地图 ① 基础地图 ...

  6. 集成Android免费语音合成功能(在线、离线、离在线融合),有这一篇文章就够了(离线)

    原址 集成Android免费语音合成功能(在线.离线.离在线融合),有这一篇文章就够了(在线) 集成Android免费语音合成功能(在线.离线.离在线融合),有这一篇文章就够了(离在线融合)     ...

  7. 集成Android免费语音合成功能(在线、离线、离在线融合)

    集成Android免费语音合成功能(在线.离线.离在线融合),有这一篇文章就够了(离线) 集成Android免费语音合成功能(在线.离线.离在线融合),有这一篇文章就够了(离在线融合) 转眼间,大半年 ...

  8. 集成Android免费语音合成功能(在线、离线、离在线融合),有这一篇文章就够了(在线)

    集成Android免费语音合成功能(在线.离线.离在线融合),有这一篇文章就够了(离线) 集成Android免费语音合成功能(在线.离线.离在线融合),有这一篇文章就够了(离在线融合)     转眼间 ...

  9. STM32毕业设计——基于STM32+JAVA+Android的六足机器人控制系统设计与实现(毕业论文+程序源码)——六足机器人控制系统

    基于STM32+JAVA+Android的六足机器人控制系统设计与实现(毕业论文+程序源码) 大家好,今天给大家介绍基于STM32+JAVA+Android的六足机器人控制系统设计与实现,文章末尾附有 ...

最新文章

  1. 文件得编码和文件名的编码是不一样的
  2. C语言用循环结构算平均值,C语言循环结构选择题().doc
  3. 使用宝塔部署node项目_使用宝塔面板进行项目的自动部署WebHook
  4. 啥?这就是一个高级报表/BI数据分析工程师的一天?
  5. 机器学习之数据归一化
  6. WMI远程访问问题解决方法
  7. Python+django网页设计入门(8):网站项目文件夹布局
  8. AcWing 116. 飞行员兄弟(二维指数型枚举)
  9. and/or(||)的理解
  10. 减小pdf大小 打印 低分辨率
  11. Qt 未找到文件:NMAKE
  12. 记录:起个撒名了, 就叫 《方向》 吧....
  13. Android 设备Id 唯一不重复,Redmi
  14. 点云ply格式文件详解
  15. Human-like learning在对话机器人中的魔性运用
  16. 关于使用selenium工具调用Firefox浏览器登录淘宝、京东web端的试验
  17. 事还得慢慢做,环境还得靠自己准备
  18. 低代码可以做什么?以织信informat这个平台为例说说
  19. 深度学习在静息态功能磁共振成像中的应用
  20. raspberry树莓派 -- CAN收发 - waveshare微雪

热门文章

  1. python工程师是什么专业-python工程师的工作一般都在做什么?
  2. python分布式计算框架_基于Python的分布式计算平台-DPark
  3. Java 形参和实参
  4. Docker 容器化技术(介绍)
  5. Mendix敏捷开发零基础学习《三》-高级 (数据删除保护机制、数据关联删除、Security安全、调用外部接口、调用JAVA代码)
  6. java jvm垃圾回收算法_深入理解JVM虚拟机2:JVM垃圾回收基本原理和算法
  7. 易安卓读取HTML,易安卓(E4A)怎么保存设置?
  8. CAD中如何移动一点至一个绝对坐标
  9. 制作html版圣诞礼物,10个圣诞礼物制作灵感 创意圣诞卡片手工制作
  10. python中文注释与单行注释_Python单行注释方法