jfinal-4.2 通过jocab 调用windows的sapi进行语音合成

jfinal-4.2 通过jocab 调用windows的sapi进行语音合成

[产生背景]

1.因为其他厂商语音合成的内网部署都太贵了

2.众所周知,widows系统本身就有语音合成的功能

3.能不能将windows的语音合成对提供成HTTP API服务.让其他程序调用,这样在内网0费用就可以使用语音合成了


搭建工程

添加依赖

    <dependency>
      <groupId>com.jfinal</groupId>
      <artifactId>jfinal-undertow</artifactId>
      <version>1.6</version>
    </dependency>

    <dependency>
      <groupId>com.jfinal</groupId>
      <artifactId>jfinal</artifactId>
      <version>4.2</version>
    </dependency>

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>

    <!--桥接器:告诉slf4j使用Log4j2 -->
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-slf4j-impl</artifactId>
      <version>2.1</version>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-api</artifactId>
      <version>2.1</version>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-core</artifactId>
      <version>2.1</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.7.25</version>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.16.10</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>com.hynnet</groupId>
      <artifactId>jacob</artifactId>
      <version>1.18</version>
    </dependency>

创建在工程目录下添加lib目录将jacob-1.18-x64.dll添加lib目录

LibraryUtil 将当前工程下的lib目录添加到library path

package com.litong.utils.dll;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;

public class LibraryUtil {

  public static void addLibary() {
    File file = new File("lib");
    String absolutePath = file.getAbsolutePath();
    System.out.println("absolutePath:" + absolutePath);
    if (!file.exists()) {
      file.mkdirs();
    }
    try {
      addLibDir(absolutePath);
    } catch (IOException e1) {
      e1.printStackTrace();
    }
  }

  public static void addLibDir(String s) throws IOException {
    try {
      Field field = java.lang.ClassLoader.class.getDeclaredField("usr_paths");
      field.setAccessible(true);
      String[] paths = (String[]) field.get(null);
      for (int i = 0; i < paths.length; i++) {
        if (s.equals(paths[i])) {
          return;
        }
      }
      String[] tmp = new String[paths.length + 1];
      System.arraycopy(paths, 0, tmp, 0, paths.length);
      tmp[paths.length] = s;
      field.set(null, tmp);
    } catch (IllegalAccessException e) {
      throw new IOException("Failed to get permissions to set library path");
    } catch (NoSuchFieldException e) {
      throw new IOException("Failed to get field handle to set library path");
    }
  }
}

启动类Application的main方法中调用LibraryUtil.addLibary(); 加载

package com.litong.jfinal;

import com.jfinal.config.Constants;
import com.jfinal.config.Handlers;
import com.jfinal.config.Interceptors;
import com.jfinal.config.JFinalConfig;
import com.jfinal.config.Plugins;
import com.jfinal.config.Routes;
import com.jfinal.server.undertow.UndertowConfig;
import com.jfinal.server.undertow.UndertowServer;
import com.jfinal.template.Engine;
import com.litong.jfinal.route.AdminRoutes;
import com.litong.jfinal.route.ApiRoutes;
import com.litong.jfinal.route.FrontRoutes;
import com.litong.jfinal.route.SystemRoutes;
import com.litong.jfinal.utils.PropKitUtil;
import com.litong.jfinal.utils.UndertowUtil;
import com.litong.utils.dll.LibraryUtil;
import com.litong.utils.ip.IPUtil;

public class Application extends JFinalConfig {
  private static String configFileName = PropKitUtil.configFileName;

  public static void main(String[] args) {
    long start = System.currentTimeMillis();
    LibraryUtil.addLibary();
    UndertowUtil.server = UndertowServer.create(Application.class, configFileName);
    // 启动Server全局共享
    UndertowUtil.server.addSystemClassPrefix("com.litong.jfinal.utils.UndertowUtil");
    UndertowUtil.server.start();
    UndertowConfig undertowConfig = UndertowUtil.server.getUndertowConfig();
    int port = undertowConfig.getPort();
    String contextPath = undertowConfig.getContextPath();
    long end = System.currentTimeMillis();
    System.out.println(IPUtil.getThisUrl(port, contextPath));
    System.out.println("启动完成,共使用了" + (end - start) + "ms");
  }

  public void configConstant(Constants me) {
    me.setInjectDependency(true);
    me.setInjectSuperClass(true);
  }

  public void configRoute(Routes me) {
    me.setMappingSuperClass(true);
    me.add(new FrontRoutes()); // 前端路由
    me.add(new AdminRoutes()); // 后端路由
    me.add(new SystemRoutes()); // 系统路由
    me.add(new ApiRoutes()); // API路由
  }

  @Override
  public void configEngine(Engine me) {
  }

  @Override
  public void configPlugin(Plugins me) {
  }

  @Override
  public void configInterceptor(Interceptors me) {
  }

  @Override
  public void configHandler(Handlers me) {
  }
}

配置路由

ApiRoutes 

package com.litong.jfinal.route;

import com.jfinal.config.Routes;
import com.litong.modules.map.controller.MapServiceController;
import com.litong.modules.tts.controller.TTSController;

/**
 * @author bill robot
 * @date 2020年8月16日_下午5:08:54 
 * @version 1.0 
 * @desc
 */
public class ApiRoutes extends Routes {

  @Override
  public void config() {
    add("tts", TTSController.class);
  }

}

TTSController

package com.litong.modules.tts.controller;

import java.io.File;

import com.jfinal.core.Controller;
import com.jfinal.core.paragetter.Para;
import com.litong.modules.tts.service.MsTtsUtil;
import com.litong.modules.tts.vo.TTSVo;

import lombok.extern.slf4j.Slf4j;

/**
 * @author bill robot
 * @date 2020年8月16日_下午11:43:15 
 * @version 1.0 
 * @desc
 */
@Slf4j
public class TTSController extends Controller {

  public void test(@Para("") TTSVo ttsVo) {
    renderJson(ttsVo);
  }

  public void index(@Para("") TTSVo ttsVo) {
    // 设置翻发音人需要修改注册表,我放弃修改了;
    String filePath = MsTtsUtil.stringToFile(ttsVo);
    if (filePath == null) {
      renderText("tts fail");
      log.error("tts fail");
      return;
    }
    File file = new File(filePath);
    if (!file.exists()) {
      String msg = "file is not exists:" + file.getAbsolutePath();
      renderText(msg);
      log.error(msg);
    } else {
      renderFile(file);
    }
    return;
  }
}

MsTtsUtil语音合成工具类

package com.litong.modules.tts.service;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;

import com.jacob.activeX.ActiveXComponent;
import com.jacob.com.Dispatch;
import com.jacob.com.Variant;
import com.jfinal.kit.HashKit;
import com.litong.modules.tts.vo.TTSVo;
import com.litong.utils.dll.LibraryUtil;

import lombok.extern.slf4j.Slf4j;

/**
 * MS 语音合成工具类
 */
@Slf4j
public class MsTtsUtil {

  public static void main(String[] args) {
    LibraryUtil.addLibary();
    TTSVo ttsVo = new TTSVo();
    ttsVo.setType(6);
    ttsVo.setVolume(100);
    ttsVo.setRate(0);
    ttsVo.setText("今天猪肉又涨价了,一下子涨了两块钱");
    stringToFile(ttsVo);
    ttsVo.setText("今天羊肉羊肉也涨价了");
    stringToFile(ttsVo);
    // String path = "text\\过秦论.txt";
    // textToFile(path);
  }

  /**
   * 语音转文字音频
   * 
   * @param txt
   */
  public static String stringToFile(TTSVo ttsVo) {
    ActiveXComponent ax = new ActiveXComponent("Sapi.SpVoice");
    String filePath = getFilePath(ax, ttsVo);
    try {
      // 运行时输出语音内容
      Dispatch spVoice = ax.getObject();
      // 下面是构建文件流到成语音文件
      ax = new ActiveXComponent("Sapi.SpFileStream");
      Dispatch spFileStream = ax.getObject();

      ax = new ActiveXComponent("Sapi.SpAudioFormat");
      Dispatch spAudioFormat = ax.getObject();

      // 设置音频流格式
      Dispatch.put(spAudioFormat, "Type", ttsVo.getType());
      // 设置文件输出流格式
      Dispatch.putRef(spFileStream, "Format", spAudioFormat);
      // 调用输出 文件流打开方法,创建一个.wav文件
      Dispatch.call(spFileStream, "Open", new Variant(filePath), new Variant(3), new Variant(true));
      // 设置声音对象的音频输出流为输出文件对象
      Dispatch.putRef(spVoice, "AudioOutputStream", spFileStream);
      // 设置音量 0到100
      Dispatch.put(spVoice, "Volume", new Variant(ttsVo.getVolume()));
      // 设置朗读速度
      Dispatch.put(spVoice, "Rate", new Variant(ttsVo.getRate()));
      // 开始朗读,实际上是合成得到文件
      Dispatch.call(spVoice, "Speak", new Variant(ttsVo.getText()));

      // 关闭输出文件
      Dispatch.call(spFileStream, "Close");
      Dispatch.putRef(spVoice, "AudioOutputStream", null);

      spAudioFormat.safeRelease();
      spFileStream.safeRelease();
      spVoice.safeRelease();
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      ax.safeRelease();
    }
    return filePath;
  }

  public static String getFilePath(ActiveXComponent ax, TTSVo ttsVo) {
    File file = new File("tts");
    if (!file.exists()) {
      file.mkdirs();
      log.info("mkdir:{}", file.getAbsoluteFile());
    }
    // 文件名规则
    // voice_type_volume_rate_md5
    String voice = getCurrentVoice(ax);
    String filename = append("_", ttsVo.getType(), ttsVo.getVolume(), ttsVo.getRate());
    // 添加文件名称
    filename += "_" + HashKit.md5(ttsVo.getText());
    // 添加文件后缀
    if (ttsVo.getType() == 6) {
      filename += ".wav";
    }
    // 发音人和加目录
    filename = "tts/" + voice + "_" + filename;
    return filename;
  }

  private static String append(String string, int... ints) {
    String retval = new String();
    for (int i : ints) {
      retval += "_" + i;
    }
    return retval;
  }

  private static String getCurrentVoice(ActiveXComponent ax) {
    // 获取Sapi.SpVoice对象的voice属性值,voice的属性值是一个voice对象,对象使用Dispatch包装
    Dispatch voiceDispatche = ax.getProperty("voice").toDispatch();
    // 执行voice对象的GetDescription方法,返回值可能使用String,使用Variant包装
    Variant descripton = Dispatch.call(voiceDispatche, "GetDescription");
    return descripton.toString();
  }

  public static String textToFile(String path,TTSVo ttsVo) {
    ActiveXComponent ax = new ActiveXComponent("Sapi.SpVoice");
    String filePath = getFilePath(ax, ttsVo);
    try {
      // 运行时输出语音内容
      Dispatch spVoice = ax.getObject();

      // 下面是构建文件流到成语音文件
      ax = new ActiveXComponent("Sapi.SpFileStream");
      Dispatch spFileStream = ax.getObject();

      ax = new ActiveXComponent("Sapi.SpAudioFormat");
      Dispatch spAudioFormat = ax.getObject();

      // 设置音频流格式
      Dispatch.put(spAudioFormat, "Type", new Variant(ttsVo.getType()));
      // 设置文件输出流格式
      Dispatch.putRef(spFileStream, "Format", spAudioFormat);
      // 调用输出 文件流打开方法,创建一个.wav文件
      Dispatch.call(spFileStream, "Open", new Variant(filePath), new Variant(3), new Variant(true));
      // 设置声音对象的音频输出流为输出文件对象
      Dispatch.putRef(spVoice, "AudioOutputStream", spFileStream);
      // 设置音量 0到100
      Dispatch.put(spVoice, "Volume", new Variant(ttsVo.getVolume()));
      // 设置朗读速度
      Dispatch.put(spVoice, "Rate", new Variant(ttsVo.getRate()));
      // 输入文件
      File srcFile = new File(path);
      // 使用包装字符流读取文件
      BufferedReader br = new BufferedReader(new FileReader(srcFile));
      String content = br.readLine();
      // 开始朗读,实际上是合成得到文件
      while (content != null) {
        Dispatch.call(spVoice, "Speak", new Variant(content));
        content = br.readLine();
      }
      br.close();
      // 关闭输出文件
      Dispatch.call(spFileStream, "Close");
      Dispatch.putRef(spVoice, "AudioOutputStream", null);

      spAudioFormat.safeRelease();
      spFileStream.safeRelease();
      spVoice.safeRelease();
    } catch (Exception e) {
      e.printStackTrace();
    } finally {
      ax.safeRelease();
    }
    return filePath;
  }
}

添加完成后启动访问浏览器测试

http://192.168.0.10:11023/litongjava-windows-tts/tts?type=6&volume=100&rate=0&text=秦孝公据崤函之固,拥雍州之地,君臣固守以窥周室,有席卷天下,包举宇内,囊括四海之意,并吞八荒之心。当是时也,商君佐之,内立法度,务耕织,修守战之具,外连衡而斗诸侯。于是秦人拱手而取西河之外。
孝公既没,惠文、武、昭襄蒙故业,因遗策,南取汉中,西举巴、蜀,东割膏腴之地,北收要害之郡。诸侯恐惧,会盟而谋弱秦,不爱珍器重宝肥饶之地,以致天下之士,合从缔交,相与为一。当此之时,齐有孟尝,赵有平原,楚有春申,魏有信陵。此四君者,皆明智而忠信,宽厚而爱人,尊贤而重士,约从离衡,兼韩、魏、燕、楚、齐、赵、宋、卫、中山之众。于是六国之士,有宁越、徐尚、苏秦、杜赫之属为之谋,齐明、周最、陈轸、召滑、楼缓、翟景、苏厉、乐毅之徒通其意,吴

把jacob-1.18-x64.dll下载
https://files.cnblogs.com/files/w1441639547/jacob-1.18-x64.rar


评论区

杜福忠

2020-08-17 13:53

优秀!

祥子

2020-08-17 15:41

优秀!

SuperEric

2020-08-17 15:47

实战,优秀!