`
zzc0000
  • 浏览: 32138 次
  • 性别: Icon_minigender_1
  • 来自: 苏州
社区版块
存档分类
最新评论

app 轻松实现中文语音智能播报, 不必依赖本地引擎

阅读更多

 

 Android系统从1.6版本开始就支持TTS(Text-To-Speech),也就是我们所说的语音合成,不过遗憾的是系统默认的TTS引擎:Pico TTS,并不支持中文。

由此对于广大的炎黄子孙不得不安装我们自己的TTS引擎跟语言包,但中文数据量庞大, 一般语言包下载下来动辄几百M,甚至G级别. 对于我们android应用开发相当不现实, 不可能要求客户为了你的一个APP而安装如此庞大的中文引擎而不惜破费流量. 那么, 是否有其它方式呢? 

如果您经常使用google翻译 http://translate.google.cn/?hl=en, 你稍稍留意就会发现上面有个语音按钮, 语音效果还相当不错. 接下来, 我们谈谈怎么利用google来帮我们使app更加绘声绘影

我们结合目前实际项目来展开本

我所带领的团队目前正在开发的一个项目是移动餐厅, 涉及到餐饮订餐领域, 项目分为商家端与微信端,顾客通过微信点餐后,商家端语音提醒处理,顾客通过支付宝等方式买单,商家也会语音提醒,顾客呼叫服务员,服务员收到语音提醒, 接下来看看实现方式

首先, google翻译所使用的方式是将app下载到本地缓存为mp3文件, 然后播放mp3即可. 
思路很清晰, 找到google语音接口地址, 把需要播报的中文传进去, 返回来我们需要的mp3, 播放它.
接口地址:http://translate.google.cn/translate_tts?ie=UTF-8&q=xxx&tl=zh-CN&total=1&idx=0&textlen=8
只需要把xxx换成我们需要播报的中文即可,当然别忘了URLEncoder.encode(text) 转码
实际实现时, 我们碰到了几处问题, 比如多个语音文件如何使用单独线程边下载边按顺序播报.

我的思路是使用多线程设计模式中生产者消费者模式, 生产者负责mp3文件下载, 消费者发现队列中有mp3文件即进行播报, 播报完成后从队列中删除.

 

import java.net.URLEncoder;

import android.media.MediaPlayer;
import android.util.Log;

import com.gitom.framwork.util.FileUtil;
import com.gitom.framwork.util.HttpDownloader;

public class PlayerHelper {
	
	private VoiceQueue queue = new VoiceQueue();
	private VoicePlayer player = new VoicePlayer(queue);

	/**
	 * 播放器处理监听状态,等待queue中队列新数据
	 */
	public void start() {
		Thread tc = new Thread(player);
		tc.start();
	}

	/**
	 * 播放声音,可由线程压入
	 * 
	 * @param text
	 */
	public void play(String text) {
		VoiceDownloader downloader = new VoiceDownloader(queue);
		downloader.setSource(text);
		Thread tp = new Thread(downloader);
		tp.start();
	}
	
	public void setPlayEnabled(boolean playEnabled) {
		player.setPlayEnabled(playEnabled);
		
		if(playEnabled) {
			start();
		}
	}
	
}

//声音对象
class VoiceItem {
	private String text;

	public VoiceItem(String text) {
		this.text = text;
	}

	public String getText() {
		return text;
	}

	public String toString() {
		return "Voice :" + text;
	}
}

// 共享栈空间
class VoiceQueue {
	VoiceItem sm[] = new VoiceItem[6];
	int index = 0;

	/**
	 * 
	 * @param m
	 *            元素
	 * @return 没有返回值
	 */

	public synchronized void push(VoiceItem m) {
		try {
			while (index == sm.length) {
				System.out.println("!!!!!!!!!超过最大堆数量,执行等待,再压入!!!!!!!!!");
				this.wait();
			}
			this.notify();
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (IllegalMonitorStateException e) {
			e.printStackTrace();
		}

		sm[index] = m;
		index++;
	}

	/**
	 * 
	 * @param b
	 *            true 表示显示,false 表示隐藏
	 * @return 没有返回值
	 */
	public synchronized VoiceItem pop() {
		try {
			while (index == 0) {
				System.out.println("!!!!!!!!!消费光了!!!!!!!!!");
				this.wait();
			}
			this.notify();
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (IllegalMonitorStateException e) {
			e.printStackTrace();
		}
		index--;
		return sm[index];
	}
}

class VoiceDownloader implements Runnable {
	private String text;

	private VoiceQueue ss = new VoiceQueue();

	public VoiceDownloader(VoiceQueue ss) {
		this.ss = ss;
	}

	public void setSource(String string) {
		this.text = string;
	}
	
	/**
	 * show 生产进程.
	 */
	public void run() {
		while (true) {
			if (text != null) {
				final String fileName = text + ".mp3";
				HttpDownloader lo = new HttpDownloader();
				StringBuilder url = new StringBuilder();
				url.append("http://translate.google.cn/translate_tts?ie=UTF-8&q=");
				url.append(URLEncoder.encode(text));
				url.append("&tl=zh-CN&total=1&idx=0&textlen=8");
				lo.downFile(url.toString(), SoundUtils.dir, fileName);

				VoiceItem voice = new VoiceItem(text);

				System.out.println("准备播放:" + text);
				ss.push(voice);

				text = null;
			}
			
			// 在上面一行进行测试是不妥的,对index的访问应该在原子操作里,因为可能在push之后此输出之前又消费了,会产生输出混乱
			try {
				Thread.sleep((int) (Math.random() * 500));
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

class VoicePlayer implements Runnable {
	private MediaPlayer mp = new MediaPlayer();
	private VoiceQueue ss = new VoiceQueue();
	private boolean playEnabled = true;

	public VoicePlayer(VoiceQueue ss) {
		this.ss = ss;
	}
	
	public void setPlayEnabled(boolean playEnabled) {
		this.playEnabled  = playEnabled;
	}

	/**
	 * show 消费进程.
	 */
	public void run() {
		while (playEnabled) {
			if (mp.isPlaying()) {
				// 有文件正在播放,则等待,至播放器状态空闲 继续播放, 播放器本身是异步播放
				try {
					Thread.sleep((int) (Math.random() * 1000));
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				continue;
			}

			VoiceItem m = ss.pop();

			final String fileName = m.getText() + ".mp3";
			try {
				boolean fileExist = FileUtil.isFileExist(SoundUtils.dir
						+ fileName);
				if (fileExist) {
					mp.stop();
					mp.reset();
					mp.setDataSource(FileUtil.getSDPATH() + SoundUtils.dir
							+ fileName);
					mp.prepare();
					mp.start();
				}
			} catch (Exception e) {
				Log.e("mediaPlayer", "error", e);
			}

			System.out.println("播放了:---------" + m.getText());
			// 同上 在上面一行进行测试也是不妥的,对index的访问应该在原子操作里,因为可能在pop之后此输出之前又生产了,会产生输出混乱
			try {
				Thread.sleep((int) (Math.random() * 1000));
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

 
下载后保存至本地缓存中



 

 

然后通过一个静态类实现调用

import java.util.HashMap;
import java.util.Map;

import android.content.Context;

import com.gitom.framwork.xst.db.helper.AppHelper;
import com.gitom.framwork.xst.db.helper.NetworkHelper;

public class SoundUtils {

	private static PlayerHelper player = new PlayerHelper();

	private static Map<String, String> map = new HashMap<String, String>();
	static {
		map.put("号", "浩");
		map.put("x1", "1份");
		map.put("x2", "2份");
		map.put("x3", "3份");
		map.put("x4", "4份");
		map.put("x5", "5份");
		map.put("x6", "6份");
		map.put("x7", "7份");
		map.put("x8", "8份");
		map.put("x9", "9份");
		map.put("\n", "");

		player.start();
	}

	public static String dir = "catering/";

	public static void play(Context context, String argText) {
		if (!AppHelper.getInstance().isVoiceEnabled(context)) {
			return;
		}

		int status = NetworkHelper.getInstance().NetworkConnectStatus();
		if (status != 2) {
			// 仅 WIFI 下语音提示
		}

		final String text = replace(argText);

		player.play(text);
	}
	
	public static void setPlayEnabled(boolean playEnabled) {
		player.setPlayEnabled(playEnabled);
	}

	public static String replace(String argText) {
		String result = argText.replace(" ", "").toLowerCase().trim();
		for (String key : map.keySet()) {
			result = result.replace(key, map.get(key));
		}
		return result;
	}
}

 

上述代码中map并无特殊用途, 只是google在翻译时有时候可能偶尔单个字符不太准确 我们进行字符替换用, 或者特殊字符替换成语音用途字符

值得指出的是, google在线播报确实是一款强大的语音引擎, 帮我们省去了不少麻烦, 减小app体积, 当然有缺点所在, 比如下载需要网络, 上面的代码可以看出, 我们下载一次以后如果下次继续同样的播报内容不会再下载, 使用缓存即可, 当然mp3一般不会大,几十K对用户影响不大, 也可以设置为wifi下才启用.

 

以上为android代码, ios大体类似, 只是语法稍修改即可, 希望本文能帮助到大家, 让我们的app更加强大具有吸引力.


最后欢迎体验移动餐厅,如果有项目难题或项目需求, 可以和我探讨 QQ:285264911
http://app.gitom.com/mobileapp/list/12

 

 

 

 

 

 

 

 

 

  • 大小: 128.2 KB
0
0
分享到:
评论
1 楼 tomora 2017-08-14  
对你实现的功能很感兴趣,可以联系一下吗?谢谢!我的QQ:69980173.

相关推荐

    java开源包101

    GWT Spring 使得在 Spring 框架下构造 GWT 应用变得很简单,提供一个易于理解的依赖注入和RPC机制。 Java扫雷游戏 JVMine JVMine用Applets开发的扫雷游戏,可在线玩。 public class JVMine extends java.applet....

    java开源包3

    GWT Spring 使得在 Spring 框架下构造 GWT 应用变得很简单,提供一个易于理解的依赖注入和RPC机制。 Java扫雷游戏 JVMine JVMine用Applets开发的扫雷游戏,可在线玩。 public class JVMine extends java.applet....

    java开源包4

    GWT Spring 使得在 Spring 框架下构造 GWT 应用变得很简单,提供一个易于理解的依赖注入和RPC机制。 Java扫雷游戏 JVMine JVMine用Applets开发的扫雷游戏,可在线玩。 public class JVMine extends java.applet....

    java开源包1

    GWT Spring 使得在 Spring 框架下构造 GWT 应用变得很简单,提供一个易于理解的依赖注入和RPC机制。 Java扫雷游戏 JVMine JVMine用Applets开发的扫雷游戏,可在线玩。 public class JVMine extends java.applet....

    java开源包11

    GWT Spring 使得在 Spring 框架下构造 GWT 应用变得很简单,提供一个易于理解的依赖注入和RPC机制。 Java扫雷游戏 JVMine JVMine用Applets开发的扫雷游戏,可在线玩。 public class JVMine extends java.applet....

    java开源包2

    GWT Spring 使得在 Spring 框架下构造 GWT 应用变得很简单,提供一个易于理解的依赖注入和RPC机制。 Java扫雷游戏 JVMine JVMine用Applets开发的扫雷游戏,可在线玩。 public class JVMine extends java.applet....

    java开源包6

    GWT Spring 使得在 Spring 框架下构造 GWT 应用变得很简单,提供一个易于理解的依赖注入和RPC机制。 Java扫雷游戏 JVMine JVMine用Applets开发的扫雷游戏,可在线玩。 public class JVMine extends java.applet....

    java开源包5

    GWT Spring 使得在 Spring 框架下构造 GWT 应用变得很简单,提供一个易于理解的依赖注入和RPC机制。 Java扫雷游戏 JVMine JVMine用Applets开发的扫雷游戏,可在线玩。 public class JVMine extends java.applet....

    java开源包10

    GWT Spring 使得在 Spring 框架下构造 GWT 应用变得很简单,提供一个易于理解的依赖注入和RPC机制。 Java扫雷游戏 JVMine JVMine用Applets开发的扫雷游戏,可在线玩。 public class JVMine extends java.applet....

    java开源包8

    GWT Spring 使得在 Spring 框架下构造 GWT 应用变得很简单,提供一个易于理解的依赖注入和RPC机制。 Java扫雷游戏 JVMine JVMine用Applets开发的扫雷游戏,可在线玩。 public class JVMine extends java.applet....

    java开源包7

    GWT Spring 使得在 Spring 框架下构造 GWT 应用变得很简单,提供一个易于理解的依赖注入和RPC机制。 Java扫雷游戏 JVMine JVMine用Applets开发的扫雷游戏,可在线玩。 public class JVMine extends java.applet....

    java开源包9

    GWT Spring 使得在 Spring 框架下构造 GWT 应用变得很简单,提供一个易于理解的依赖注入和RPC机制。 Java扫雷游戏 JVMine JVMine用Applets开发的扫雷游戏,可在线玩。 public class JVMine extends java.applet....

    Java资源包01

    GWT Spring 使得在 Spring 框架下构造 GWT 应用变得很简单,提供一个易于理解的依赖注入和RPC机制。 Java扫雷游戏 JVMine JVMine用Applets开发的扫雷游戏,可在线玩。 public class JVMine extends java.applet....

    JAVA上百实例源码以及开源项目源代码

    5个目标文件,演示Address EJB的实现,创建一个EJB测试客户端,得到名字上下文,查询jndi名,通过强制转型得到Home接口,getInitialContext()函数返回一个经过初始化的上下文,用client的getHome()函数调用Home接口...

    JAVA上百实例源码以及开源项目

    百度云盘分享 ... Java实现的FTP连接与数据浏览程序,实现实例化可操作的窗口。  部分源代码摘录:  ftpClient = new FtpClient(); //实例化FtpClient对象  String serverAddr=jtfServer.getText();...

Global site tag (gtag.js) - Google Analytics