`
zzc0000
  • 浏览: 32974 次
  • 性别: 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.

相关推荐

    smarthome_基于STM32的智能家居控制系统_基于stm32_家居stm32_语音识别系统_STM32智能家居_

    在基于STM32的智能家居系统中,可能采用如Google Assistant或Amazon Alexa等流行的语音助手SDK,或者采用本地化的语音识别引擎,以减少对网络的依赖。这些SDK提供了语音唤醒、关键词识别、语义理解等功能,使得智能...

    讯飞离线语音合成(语音+)

    3. **语记APP**:作为实现离线语音合成的载体,语记APP可能包含了科大讯飞的完整语音合成引擎,用户可以在此平台上体验和使用这项技术。 4. **API/SDK**:为了方便开发者集成离线语音合成,科大讯飞提供了简化后的...

    ASR-Demo

    ASR(Automatic Speech Recognition,自动语音识别)技术是IT领域中的一个重要组成部分,它涉及计算机科学、人工智能和信号处理等多个学科。ASR-Demo是一个基于Streamlit Python包的Web应用程序,用于展示ASR技术的...

    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....

    IOS开发工程师面试常见问题

    HomeKit是苹果为智能家居设备提供的一个平台,它允许iOS应用与支持HomeKit的智能家居产品进行交互,从而实现远程控制等功能。 #### 十六、ApplePay是什么? ApplePay是一种快速安全的支付解决方案,用户可以通过...

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

    Java实现的FTP连接与数据浏览程序 1个目标文件 摘要:Java源码,网络相关,FTP Java实现的FTP连接与数据浏览程序,实现实例化可操作的窗口。 部分源代码摘录: ftpClient = new FtpClient(); //实例化FtpClient对象 ...

Global site tag (gtag.js) - Google Analytics