运行图
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓以下是正文↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
(1)语音合成的方式有很多,为了简洁,我们使用第三方语音合成API,这里选择百度语音。详细注册流程官方文档很详细,这里不做过多介绍。传送门,我们需要的是AppID,API Key和Secret Key 如下图
(2)我们要做java后台处理,所以使用java JDK即可,传送门
(1)创建普通maven工程即可,相信优秀的你一定都会了,这里不啰嗦了。
(2)引入依赖
org.projectlombok
lombok
true
com.baidu.aip
java-sdk
4.11.3
(3)创建百度语音合成工具类
ConstansHelper.java
import com.baidu.aip.speech.AipSpeech;
import com.baidu.aip.speech.TtsResponse;
import com.baidu.aip.util.Util;
import lombok.Data;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
/**
* @Desc:
* @Author: wangyafei
* @Date: 2019/9/5 18:04
* @Version 1.0
*/
@Data
public class ConstansHelper {
//设置APPID/AK/SK 注册百度开发者会生成
public static final String APP_ID = "APP_ID ";
public static final String API_KEY = "API_KEY ";
public static final String SECRET_KEY = "SECRET_KEY ";
/**
* @param spd 语速,取值0-15,默认为5中语速
* @param pit 音调,取值0-15,默认为5中语调
* @param vol 音量,取值0-15,默认为5中音量
* @param per 发音人选择, 0为女声,1为男声,
* 3为情感合成-度逍遥,
* 4为情感合成-度丫丫,默认为普通女
* @param txt 要转换的文本
*/
public static String makeSpeech(String spd, String pit, String vol, String per, String txt) {
// 初始化一个AipSpeech
AipSpeech client = new AipSpeech(APP_ID, API_KEY, SECRET_KEY);
// 可选:设置网络连接参数
client.setConnectionTimeoutInMillis(2000);
client.setSocketTimeoutInMillis(60000);
// 设置可选参数
HashMap options = new HashMap();
if (!"请选择".equals(spd)) {
options.put("spd", spd);
}
if (!"请选择".equals(pit)) {
options.put("pit", pit);
}
if (!"请选择".equals(vol)) {
options.put("vol", vol);
}
if (!"请选择".equals(per)) {
switch (per){
case "女声":
options.put("per", 0);
break;
case "男声":
options.put("per", 1);
break;
case "情感合成-度逍遥":
options.put("per", 3);
break;
case "情感合成-度丫丫":
options.put("per", 4);
break;
}
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
String dirName = sdf.format(new Date());
isChartPathExist("D:\\音频合成\\" + dirName);
// 调用接口
//合成文本长度必须小于1024字节,如果本文长度较长,可以采用多次请求的方式。文本长度不可超过限制
List txtList = splitFunction(txt, 1024);
byte[] all =null;
for (String s : txtList) {
TtsResponse res = client.synthesis(s, "zh", 1, options);
byte[] data = res.getData();
if(data != null){
all = byteMerger(all,data);
}
}
//存储位置,自己决定放到哪里,我放到了D盘
String fileName ="D:\\音频合成\\" + dirName + "\\" + (txt.length()>15?txt.substring(0, 15):txt) + "-"+System.currentTimeMillis() + "output.mp3";
if (all != null) {
try {
Util.writeBytesToFileSystem(all, fileName);
} catch (IOException e) {
e.printStackTrace();
}
}
File file = new File(fileName);
return fileName;
}
/**
* 文件夹磁盘路径 文件夹不存在创建文件夹
* @param dirPath
*/
private static void isChartPathExist(String dirPath) {
File file = new File(dirPath);
if (!file.exists()) {
file.mkdirs();
}
}
/**
* 根据字节长度进行切割
* 中文在不同编码中占用的字节数是不同的,GBK编码中,一个汉字占两个字节,UTF-8编码格式中,一个汉字占3个字节
* @param src
* @param bytes
* @return
*/
public static List splitFunction(String src, int bytes){
try {
if(src == null){
return null;
}
List splitList = new ArrayList();
int startIndex = 0; //字符串截取起始位置
int endIndex = bytes > src.length() ? src.length() : bytes; //字符串截取结束位置
while(startIndex < src.length()){
String subString = src.substring(startIndex,endIndex);
//截取的字符串的字节长度大于需要截取的长度时,说明包含中文字符
//在GBK编码中,一个中文字符占2个字节,UTF-8编码格式,一个中文字符占3个字节。
while (subString.getBytes("GBK").length > bytes) {
--endIndex;
subString = src.substring(startIndex,endIndex);
}
splitList.add(src.substring(startIndex,endIndex));
startIndex = endIndex;
//判断结束位置时要与字符串长度比较(src.length()),之前与字符串的bytes长度比较了,导致越界异常。
endIndex = (startIndex + bytes) > src.length() ?
src.length() : startIndex+bytes ;
}
return splitList;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 合并两个byte数组
* @param byte_1
* @param byte_2
* @return
*/
public static byte[] byteMerger(byte[] byte_1, byte[] byte_2){
if(byte_1 == null){
return byte_2;
}
byte[] byte_3 = new byte[byte_1.length+byte_2.length];
System.arraycopy(byte_1, 0, byte_3, 0, byte_1.length);
System.arraycopy(byte_2, 0, byte_3, byte_1.length, byte_2.length);
return byte_3;
}
}
(4)创建GUI文件
网上大多数是,直接创建一个Test.form页面的教程, 感觉也不是很方便,这里采用直接写代码的方式。
Container.java
import org.springframework.util.StringUtils;
import org.wyf.yuyindemo.utils.ConstansHelper;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.io.IOException;
/**
* @Desc:
* @Author: wangyafei
* @Date: 2019/9/6 10:47
* @Version 1.0
*/
public class Container extends Thread implements ActionListener, WindowListener {
private static int totalTime = 40;
private static float totalDiff = 0.0f;
private TextField txt;
private TextArea taLog; //日志输出区域 不可以设置颜色相关
private JButton jbtStart,jbtReset; //开始按钮;重置按钮
private JComboBox spd,pit,vol,per;//语速,语调。音量,角色
private JLabel tips;
@Override
public void run() {
//画页面
//父窗体
JFrame jFrame = new JFrame("语音生成小工具");
// 创建按钮
jbtStart = new JButton("生成");
jbtReset = new JButton("重置");
jbtStart.addActionListener(this);
jbtReset.addActionListener(this);
// 创建相关的文本域
txt = new TextField("我是文本");
spd = new JComboBox();
pit = new JComboBox();
vol = new JComboBox();
per = new JComboBox();
spd.addItem("请选择");
pit.addItem("请选择");
vol.addItem("请选择");
for (int i = 0; i <15 ; i++) {
spd.addItem(i);
pit.addItem(i);
vol.addItem(i);
}
per.addItem("请选择");
per.addItem("女声");
per.addItem("男声");
per.addItem("情感合成-度逍遥");
per.addItem("情感合成-度丫丫");
JLabel jb1 = new JLabel(" 语速");
JLabel jb2 = new JLabel(" 语调");
JLabel jb3 = new JLabel(" 音量");
JLabel jb4 = new JLabel(" 角色");
JPanel sel = new JPanel(new GridLayout(4,2));
sel.add(jb1);
sel.add(spd);
sel.add(jb2);
sel.add(pit);
sel.add(jb3);
sel.add(vol);
sel.add(jb4);
sel.add(per);
taLog = new TextArea();
taLog.setColumns(30);
taLog.setRows(150);
taLog.setBackground(Color.CYAN);
taLog.setFont(new Font("宋体",Font.BOLD,16));
taLog.setEditable(false);
// 创建相关的Label标签
JLabel labelStart = new JLabel("请输入文本:");
JLabel labelTip1 = new JLabel("仅供测试
如有问题请联系
QQ772173722");
//提示输入文本
JPanel inputTxt = new JPanel(new GridLayout(2, 1));
inputTxt.add(labelStart,BorderLayout.CENTER);
inputTxt.add(txt,BorderLayout.CENTER);
//开始按钮及左右信息
JPanel panel4 = new JPanel(new GridLayout(1, 3));
panel4.add(labelTip1);
panel4.add(jbtStart);
panel4.add(jbtReset);
JPanel log = new JPanel(new GridLayout(1,1));
tips = new JLabel();
log.add(tips);
//上级布局
JPanel panel22 = new JPanel(new GridLayout(4,1));
panel22.add(sel);
panel22.add(inputTxt);
panel22.add(panel4);
panel22.add(tips);
jFrame.setLayout(new BorderLayout());
jFrame.add(panel22, BorderLayout.CENTER);
// 初始化JFrame窗口
jFrame.setLocation(800, 300);
jFrame.setSize(600, 500);
jFrame.setBackground(Color.darkGray);
jFrame.setResizable(true);
jFrame.setVisible(true);
}
/**
* 点击监听器
* @param view
*/
@Override
public void actionPerformed(ActionEvent view) {
if (view.getSource() == jbtStart) {
String spdStr =spd.getSelectedItem().toString();
String pitStr = pit.getSelectedItem().toString();
String volStr = vol.getSelectedItem().toString();
String perStr = per.getSelectedItem().toString();
String txtStr =txt.getText() ;
if (!StringUtils.isEmpty(spdStr) && !StringUtils.isEmpty(pitStr) && !StringUtils.isEmpty(volStr) && !StringUtils.isEmpty(perStr) && !StringUtils.isEmpty(txtStr)) {
makeSpeech(spdStr,pitStr,volStr,perStr,txtStr);
}
}else if(view.getSource() == jbtReset){
//重置组件内容
tips.setText(null);
txt.setText("我是文本");
spd.setSelectedIndex(0);
per.setSelectedIndex(0);
pit.setSelectedIndex(0);
vol.setSelectedIndex(0);
}
}
/**
* 合成语音
* @param spdStr
* @param pitStr
* @param volStr
* @param perStr
* @param txtStr
*/
private void makeSpeech(String spdStr, String pitStr, String volStr, String perStr, String txtStr) {
String fileName = ConstansHelper.makeSpeech(spdStr, pitStr, volStr, perStr, txtStr);
if(!StringUtils.isEmpty(fileName)){
//返回值 ,0=是,1=否,2=取消
int dialog = JOptionPane.showConfirmDialog(null, "合成成功!是否打开文件所在位置?");
tips.setText("文件位置: "+fileName);
if(dialog == 0) {
try {
String open = fileName.substring(0, fileName.lastIndexOf("\\"));
//打开文件所在位置
Runtime.getRuntime().exec("explorer.exe " + open);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
@Override
public void windowOpened(WindowEvent e) {
}
@Override
public void windowClosing(WindowEvent e) {
System.exit(0);
}
@Override
public void windowClosed(WindowEvent e) {
System.exit(0);
}
@Override
public void windowIconified(WindowEvent e) {
}
@Override
public void windowDeiconified(WindowEvent e) {
}
@Override
public void windowActivated(WindowEvent e) {
}
@Override
public void windowDeactivated(WindowEvent e) {
}
public static void main(String[] args) {
Container container = new Container();
container.run();
}
}
(1)默认情况下项目设置project structure(快捷键ctrl+alt+shift+S)里是没有artifacts的,我们新增一个
(2)选择main方法所在的类
注:如果出现"点击jar包没有反应,没有清单文件主函数"的问题,修改目录到src下即可
修改前:
修改后:
(3)此时build一下就可以啦