CJ.Blog


JavaFx 语言播报 MTTS 实现

Java调用Windows SAPI.spVoice #

Java作为跨平台语音对调用Windows是没有原生支持的,所以需要使用第三方框架来做中转实现目的。

Jacob是一个Java库,可让Java应用程序与Microsoft Windows DLL或COM库进行通信。它通过使用定制的DLL来实现此目的,Jacob Java类通过JNI与之通信。Java库和dll将Java开发人员与基础Windows库隔离开来,因此Java开发人员不必编写自定义JNI代码。Jacob不用于创建ActiveX插件或Microsoft Windows应用程序内部的其他模块。

项目地址:https://github.com/freemansoft/jacob-project

使用 #

Jacob 1.8下载后里面拥有以下文件

img

将对应当前Java版本的DLL文件放到Java/bin目录,将jacob.jar作为项目依赖引入。

SAPI (Microsoft Speech API ) #

文档地址

使用 jacob 来调用SAPI。

关于如何使用这里的博客讲的很详细:

https://blog.csdn.net/asuyunlong/article/details/50083421/

但是我是用的是第三方语音库,在切换语音包时会报错。

 @Test
    public void test2() {
        MSTTSSpeech speech = new MSTTSSpeech();
        String[] vs = speech.getVoices();
        System.out.println(Arrays.toString(vs));
        String text = "TTS TEST";

        speech.setFormatType(6);
        speech.changeVoice(1);
        // speech.setRate(-1);
        // speech.setRate(-1);
        System.out.println(getClass().getResource("mp3").getPath() + "\\" + UUID.randomUUID() + ".wav");
        String url = getClass().getResource("mp3").getPath();
        File wavFile = new File(url);
        if (!wavFile.exists()) {
            wavFile.getParentFile().mkdirs();
        }
        speech.saveToWav(text, url.substring(1) + "\\" + UUID.randomUUID() + ".wav");
    }

运行测试后出错:

A COM exception has been encountered:
At Invoke of: Voice
Description: 80020003 / 找不到成员

com.jacob.com.ComFailException: A COM exception has been encountered:
At Invoke of: Voice
Description: 80020003 / 找不到成员
    at com.jacob.com.Dispatch.invokev(Native Method)
    ...

Description: 80020003 / 找不到成员。 这个问题我找了很久,在我的电脑无法使用,但是可以使用控制面板项修改系统默认语音达到效果:

C:\Windows\SysWOW64\Speech\SpeechUX\sapi.cpl

img

Wav转Mp3重新编码并合并多个Mp3文件 #

https://lame.sourceforge.io/

现在的需求是,我有一段这样的文本: [P]Call.mp3;[T] 呼叫 XXX 前往 SMT 3号线

其中 [P]Call.mp3;[T] 是现有mp3文件,而其他文本是要通过sapi生成的,而且文本中可能还会出现外部的mp3文件。

所以思路是将mp3文件名称和需要转语音的文本按顺序抽取出来,将文字转wav,wav转mp3,最后再合并mp3文件。

代码 : AudioUtil4.java

public class AudioUtil4 {
    public static String path = null;
    public static String lame = null;
    static MSTTSSpeech speech = new MSTTSSpeech();

    static Logger LOG = LoggerFactory.getLogger(AudioUtil4.class);
    //合并mp3
    public static File mergeMp3(List<PlayModel> playModels) throws IOException {
        Vector<FileInputStream> fileInputStreams = new Vector<>();
        for (PlayModel playModel : playModels) {
            if (playModel.isMp3()) {
                playModel.setPath(path + "\\" + playModel.getContent());
            } else {
                playModel.setPath(AudioUtil4.textToMp3(playModel.getContent()));
            }
            fileInputStreams.add(new FileInputStream(playModel.getPath()));
           LOG.info("合并文件:" + playModel.getPath());
        }
        SequenceInputStream sequenceInputStream = new SequenceInputStream(fileInputStreams.elements());
        String mergePath = path + "\\" + UUID.randomUUID() + ".mp3";
        File afterMergeMp3 = new File(mergePath);
        boolean isSuccess = afterMergeMp3.createNewFile();
        if (isSuccess) {
            FileOutputStream fileOutputStream = new FileOutputStream(afterMergeMp3);//destinationfile
            int temp;

            while ((temp = sequenceInputStream.read()) != -1) {
                // System.out.print( (char) temp ); // to print at DOS prompt
                fileOutputStream.write(temp);   // to write to file
            }
            for (FileInputStream f : fileInputStreams) {
                f.close();
            }
            fileOutputStream.close();
            sequenceInputStream.close();
            playModels.forEach(playModel -> {
                if (!playModel.isMp3()) {
                    new File(playModel.getPath()).delete();
                }
            });
        } else {
            throw new IOException("mp3创建失败! " + mergePath);
        }
        return new File(mergePath);
    }
    //获取播放列表 抽取
    public static List<PlayModel> getPlayList(String message) {
        int i = 0;
        char[] messageArr = message.toCharArray();
        List<PlayModel> playModels = new ArrayList<>();
        StringBuilder temp = new StringBuilder();
        while (i < messageArr.length) {
            if (messageArr[i] == '[') {
                if (temp.length() > 0) {
                    playModels.add(new PlayModel(false, temp.toString(), null));
                    temp = new StringBuilder();
                }
                Map<String, Object> mp3 = getEndIndex(i, messageArr);
                i = (int) mp3.get("endIndex");
                playModels.add(new PlayModel(true, mp3.get("mp3").toString(), null));
            } else {
                temp.append(messageArr[i]);
            }
            i += 1;
        }
        if (temp.length() > 0) {
            playModels.add(new PlayModel(false, temp.toString(), null));
        }
        return playModels;
    }
    //抽取
    public static Map<String, Object> getEndIndex(int index, char[] chars) {
        StringBuilder mp3 = new StringBuilder();
        int mp3EndIndex = -1;
        int endIndex = -1;
        for (int i = index + 3; i < chars.length; i++) {
            if (chars[i] == ']') {
                endIndex = i;
                break;
            } else if (chars[i] == ';') {
                mp3EndIndex = i;
            } else {
                if (mp3EndIndex == -1) {
                    mp3.append(chars[i]);
                }
            }
        }
        Map<String, Object> result = new HashMap<>();
        result.put("endIndex", endIndex);
        result.put("mp3", mp3);
        return result;
    }
    //wav转mp3
    public static String textToMp3(String text) throws FileNotFoundException {
       LOG.info("转语音:" + text);
        File wavFile = new File(textToWav(text));
        if (!wavFile.exists()) {
            throw new FileNotFoundException("转语音失败!");
        } else {
            //to mp3 use lame
            String wavPathStr = wavFile.getPath();
            String mp3PathStr = wavFile.getPath().
                    replace(".wav", ".mp3");
            String cmds1 = lame +
                    String.format(" -b128 --resample 24 -m j  %s %s", wavPathStr, mp3PathStr);
//                Arguments = string.Format("-b128 --resample 24 -m j \"{0}\" \"{1}\"", str3, str4)
            //                    String.format(" --silent -b 160 -m m -q 9-resample -tt  %s %s", wavPathStr, mp3PathStr);

            String cmds2 = lame +
                    String.format(" --scale 4 --mp3input %s %s", mp3PathStr, mp3PathStr.replace(".mp3", "L.mp3"));

           LOG.info("lame to mp3 : " + cmds1);
           LOG.info("large voice : " + cmds2);
            CommandUtil.exec(cmds1);
            CommandUtil.exec(cmds2);
            new File(wavPathStr).delete();
            new File(mp3PathStr).delete();
            return mp3PathStr.replace(".mp3", "L.mp3");
        }
    }

    //文本转wav
    private static String textToWav(String text) {
        speech.setFormatType(16);
        File wavFile = new File(path);
        if (!wavFile.exists()) {
            wavFile.getParentFile().mkdirs();
        }
       LOG.info(path);
        String filePath = path + "\\" + UUID.randomUUID() + ".wav";
        speech.saveToWav(text, filePath);
       LOG.info("wav文件保存到 : " + filePath);
        return filePath;
    }

测试:

 @Test
    public void getPlayList() throws IOException {
        File directory = new File("");//设定为当前文件夹
        String mp3Path = directory.getAbsolutePath() + "\\mp3";
        String lamePath = directory.getAbsolutePath() + "\\lame.exe";

        AudioUtil4.path = mp3Path;
        AudioUtil4.lame = lamePath;


        List<PlayModel> playModels = AudioUtil4.getPlayList("[P]PromptRing.MP3;[T]请 袁振 速到 SMD 8号线 ");

        System.out.println("合并mp3:" + AudioUtil4.mergeMp3(playModels));
    }

命令行代码:

CommandUtil.java

public class CommandUtil {
    static Logger LOG = LoggerFactory.getLogger(CommandUtil.class);

    public static int exec(String comm) {
        int finished = 0;
        try {
            Runtime rt = Runtime.getRuntime();
            Process proc = rt.exec(comm);
            InputStream stderr = proc.getErrorStream();
            InputStreamReader isr = new InputStreamReader(stderr);
            BufferedReader br = new BufferedReader(isr);
            String line = null;
            LOG.info("<error></error>");
            while ((line = br.readLine()) != null) {
                String encoded = new String(line.getBytes("gbk"), "GB18030");//GB2312/CP936/GB18030
                LOG.info(encoded);
                LOG.info(line);
            }
            int exitVal = proc.waitFor();
            LOG.info("Process exitValue:" + exitVal);
            finished = exitVal;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return finished;
    }
}

Call Service 核心:

package cn.ciemis.service;

import cn.ciemis.controller.MainController;
import cn.ciemis.db.VoiceJDBC;
import cn.ciemis.entity.*;
import cn.ciemis.log.Log;
import cn.ciemis.log.Logger;
import cn.ciemis.sdk.IPCastSDK;
import cn.ciemis.util.AudioUtil4;
import cn.ciemis.util.FxUtil;
import javafx.application.Platform;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import org.controlsfx.control.StatusBar;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

import static cn.ciemis.controller.MainController.log;

public class CallService extends Service<String> {

    Logger logger = new Logger(log, CallService.class.getName());
    org.slf4j.Logger LOG = LoggerFactory.getLogger(CallService.class);

    private StatusBar statusBar;

    private SettingModel settingModel;

    static IPCastSDK IPCastSDKObj = IPCastSDK.INSTANCE;
    public static IPCastSDK.IPCastCallBack ipcb;//回调函数

    //设置回调函数
    public static IPCastSDK.IPCastCallBack getIpcb() {
        return ipcb;
    }

    public static void setIpcb(IPCastSDK.IPCastCallBack ipcb) {
        CallService.ipcb = ipcb;
    }

    public static void SDKInit() {
        IPCastSDKObj.IPCAST_SDKInit();
    }

    public CallService(StatusBar statusBar) {
        this.statusBar = statusBar;
        try {
            this.settingModel = FxUtil.readIniFile();
        } catch (IOException | NoSuchFieldException | IllegalAccessException e) {
            logger.error("加载配置文件错误!");
            this.settingModel = null;
            e.printStackTrace();
        }
    }


    public void stop() {
        Platform.runLater(() -> {
            statusBar.textProperty().unbind();
            statusBar.progressProperty().unbind();
            statusBar.progressProperty().setValue(0);
            statusBar.setText("已停止...");
            logger.warn("呼叫线程已停止");
            this.cancel();
            this.reset();
            statusBar.setText("正在重启...");
            this.restart();
        });
        if (IPCastSDKObj != null) {
            IPCastSDKObj.IPCAST_DisConnect();
        }

    }

    public void startSdk(SettingModel settingModel) {
        //设置回调函数
        try {
            SDKInit();
            boolean isConn = IPCastSDKObj.IPCAST_Connect(settingModel.getParamIp(), settingModel.getParamUsername(), settingModel.getParamPassword());
            if (!isConn) {
                logger.error("ICT服务器连接失败,检查连接参数是否正确");
//                IPCastSDKObj.IPCAST_ExitStillPlay(1);
                LOG.error("服务器连接失败!");
                stop();
            } else {
                setIpcb((EventNo, ParamStr) -> {
                    LOG.debug(EventNo + " " + ParamStr);
                    return 0;
                });
                IPCastSDKObj.IPCAST_SetCallBack(ipcb);
                LOG.info("连接成功!");
            }
            LOG.info(settingModel.getParamIp());
            LOG.info(settingModel.getParamUsername());
            LOG.info(settingModel.getDbPassword());
        } catch (Exception e) {
            logger.error("初始化DLL错误,检查DLL路径是否正确");
            e.printStackTrace();
            stop();
        }
    }


    public void clearMp3File() {
        File[] playedMp3;
        File file = new File(AudioUtil4.path);
        playedMp3 = file.listFiles();
        if (!Objects.isNull(playedMp3)) {
            for (File f : playedMp3) {
                if (f.getName().contains(AudioUtil4.tail)) {
                    if (f.delete()) {
                        LOG.info("删除成功 : " + f.getAbsolutePath());
                    } else {
                        LOG.warn("删除失败 : " + f.getAbsolutePath());
                    }
                }
            }
        }
    }

    public void sendPlay(String path, int[] pTList) throws UnsupportedEncodingException, InterruptedException {
        IPCastSDK.PlayFile.ByReference[] files = new IPCastSDK.PlayFile.ByReference[1];
        IPCastSDK.PlayFile.ByReference byReference1 = new IPCastSDK.PlayFile.ByReference();
        byReference1.fid = 0;
        byReference1.fname = path.getBytes("gbk");
        byReference1.fvol = 10;
        byReference1.write();
        files[0] = byReference1;
        int sid = -1;
        LOG.info(path);
        LOG.info("播放终端列表: " + Arrays.toString(pTList));
        sid = IPCastSDKObj.IPCAST_FilePlayStart(files, files.length, pTList, pTList.length, 500, IPCastSDK.PLAY_CYC_DANORDE, 0, 0);
        LOG.debug("SID:" + String.valueOf(sid));
    }

    public boolean getTermStatusEx(int[] tids) {
        boolean isIdle = false;
        IPCastSDK.TermAttr.ByReference term = new IPCastSDK.TermAttr.ByReference();
        for (int tid : tids) {
            if (IPCastSDKObj.IPCAST_GetTermStatusEx(tid, term)) {
                if (term.status == 0) {
                    isIdle = true;
                } else {
                    isIdle = false;
                    break;
                }
            } else {
                isIdle = false;
                break;
            }
        }
        return isIdle;
    }

    @Override
    protected void executeTask(Task<String> task) {
        super.executeTask(task);
    }


    @Override
    protected void ready() {
        super.ready();
        LOG.info("ready" + Platform.isFxApplicationThread());
    }

    @Override
    protected void scheduled() {
        super.scheduled();
        LOG.info("scheduled " + Platform.isFxApplicationThread());
    }

    @Override
    protected void running() {
        super.running();
        logger.info("启动呼叫线程");
        LOG.info("running " + Platform.isFxApplicationThread());
    }

    @Override
    protected void succeeded() {
        super.succeeded();
        LOG.info("succeeded " + Platform.isFxApplicationThread());
    }

    @Override
    protected void cancelled() {
        super.cancelled();
        LOG.info("cancelled " + Platform.isFxApplicationThread());
    }

    @Override
    protected void failed() {
        super.failed();
        logger.info("Call Thread Start Failed ! Please Check You Config!");
        stop();
        LOG.info("failed " + Platform.isFxApplicationThread());
    }

    @Override
    protected Task<String> createTask() {
        return new Task<String>() {
            @Override
            protected String call() {
                try {
                    updateMessage("初始化...");
                    startSdk(settingModel);
                    updateProgress(-1, 100);
                    VoiceJDBC voiceJDBC = new VoiceJDBC(settingModel);
                    updateMessage("Run...");
                    while (!isCancelled()) {
                        List<MesAndonPlay> mesAndonIssueDets = voiceJDBC.getTop5Voice();
                        for (MesAndonPlay u : mesAndonIssueDets) {
                            String[] terminalIds = u.getTerminalId().split(",");
                            int[] terminalIdsInt = new int[terminalIds.length];
                            for (int i = 0; i < terminalIds.length; i++) {
                                terminalIdsInt[i] = Integer.parseInt(terminalIds[i]);
                            }
                            boolean idle = getTermStatusEx(terminalIdsInt);
                            LOG.info("idle : " + idle);
                            if (idle) {
                                if (!IPCastSDKObj.IPCAST_ServerStatus()) {
                                    IPCastSDKObj.IPCAST_Connect(settingModel.getParamIp(), settingModel.getParamUsername(), settingModel.getParamPassword());
                                }
                                if (!IPCastSDKObj.IPCAST_ServerStatus()) {
                                    logger.info("无法连接广播 ");
                                    stop();
                                    break;
                                }
                                LOG.info(u.getPlayCommand());
                                List<PlayModel> playModels = AudioUtil4.getPlayList(u.getPlayCommand());
                                File path = AudioUtil4.mergeMp3(playModels);

                                //播放
                                sendPlay(path.getAbsolutePath(), terminalIdsInt);
                                logger.warn(u.getPlayCommand());
                                Thread.sleep(1000);
                                voiceJDBC.finishDet(u.getId());
                            } else {
                                updateProgress(-1, 100);
                                updateMessage("正在呼叫...");
                            }
                        }
                        Thread.sleep(Integer.parseInt(settingModel.refreshTime.getValue()));
                        clearMp3File();
                        updateMessage("等待...");
                    }
                } catch (SQLException | ClassNotFoundException throwables) {
                    logger.error(throwables.getMessage());
                    LOG.error(throwables.getMessage());
                    stop();
                    throwables.printStackTrace();
                } catch (IOException ioException) {
                    logger.error("MP3生成失败!");
                    logger.error(ioException.getMessage());
                    LOG.error(ioException.getMessage());
                    stop();
                    ioException.printStackTrace();
                } catch (Exception e) {
                    logger.error(e.getMessage());
                    LOG.error(e.getMessage());
                    stop();
                    e.printStackTrace();
                }
                //                catch (InterruptedException e) {
                //                    logger.error("播放失败!");
                //                    logger.error(e.getMessage());
                //                    stop();
                //                    e.printStackTrace();
                //                }
                return null;
            }
        };
    }
}
JavaFx Log日志输出 #

界面 :

img

这个log日志的实现参考:most-efficient-way-to-log-messages-to-javafx-textarea-via-threads-with-simple-cu

项目中使用了两个LOG,一个是GUI的LOG界面LogViewer,还有Log4j用来生成log文件。

完毕。