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