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

 

 

 

 

 

 

 

 

猜你喜欢

转载自zzc0000.iteye.com/blog/1980820
今日推荐