您的位置:首页 > 移动开发

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

2013-11-26 11:32 387 查看

 

 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

 

 

 

 

 

 

 

 

 

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: