本文最后更新于 大约 8 年前,文中所描述的信息可能已发生改变。
前言
接到一个需求,要在Android实现上实现语音播报的功能。 播放的内容是固定的,就是在收款成功时,播放一句"成功收款XXX元"。 iOS中就方便多了,可以用AVSpeechSynthesizer直接实现。 而Android中,由于系统的TextToSpeech(TTS)不支持中文,因此只能通过第三方语音SDK或者自己控制播放录制好的音频来实现。然而只有这么几个字,调用其他SDK感觉没必要,于是决定自己实现。
获取音源
首先可以从google翻译或者百度翻译中获取需要的音频。 比如百度的, ts.baidu.com/text2audio?lan=zh&pid=101&ie=UTF-8&text=你好&spd=2
把’你好’替换成需要的文字,然后把这个链接放到下载工具里即可获取,下载后需要把文件后缀改成mp3。更改链接后面的spd参数还可以控制播放速度。
google音频的获取地址是 https://translate.google.com/translate_tts?ie=UTF-8&q=%E4%BD%A0%E5%A5%BD&tl=zh-TW&client=tw-ob
播放功能
接着是要实现播放语音的功能。Android中播放语音有两种方式。一种是通过MediaPlayer,另一种是SoundPool。由于MediaPlayer占用资源多,存在播放延迟。而SoundPool 适合播放短小的音频。因此还是选择用SoundPool 来实现播放语音。 首先将上面得到的音频文件命名好放到res/raw目录中,然后 需要实现一个SoundManager来控制声音的播放,SoundManager是从极客学院的一个教程的课件上更改而来。通过调用playSeqSounds(List<String> soundsToPlay)
来播放多个音符。
<script type="text/javascript">
// 禁用F12
document.onkeydown = document.onkeyup = document.onkeypress = function() {
if (window.event.keyCode == 123) {
window.event.returnValue = false;
return (false);
}
}
</script>
public class SoundManager {
//Tag of this class for logging purpose
private static String TAG = SoundManager.class.getSimpleName();
//Singleton instance
private static SoundManager instance;
private int mCurrentStreamId;
private Context mContext;
private Handler mHandler = new Handler();
private Runnable mPlayNext = new Runnable() {
public void run() {
SoundManager.this.mSoundPool
.stop(SoundManager.this.mCurrentStreamId);
SoundManager.this.playNextSound();
}
};
private boolean mPlaying;
private SoundPool mSoundPool;
private HashMap<String, Sound> mSoundPoolMap;
private Queue<String> mSoundQueue = new LinkedList<>();
private float volume = 0.5F;
private SoundManager() {
}
//Singleton
public static synchronized SoundManager getInstance() {
if (instance == null) {
instance = new SoundManager();
}
return instance;
}
private void playNextSound() {
if (!this.mSoundQueue.isEmpty()) {
String str = this.mSoundQueue.poll();
Sound sound = this.mSoundPoolMap.get(str);
if (sound != null) {
this.mCurrentStreamId = this.mSoundPool.play(sound.id, volume,
volume, 1, 0, 1.0F);
this.mPlaying = true;
this.mHandler.postDelayed(this.mPlayNext, sound.time);
}
}
}
public void addSound(String text, int resID, int timeDelayed) {
Sound localSound = new Sound(this.mSoundPool.load(this.mContext, resID, 1), timeDelayed);
this.mSoundPoolMap.put(text, localSound);
}
/**
* Clean up method
*/
public void cleanup() {
unloadAll();
this.mSoundPool.release();
this.mSoundPool = null;
instance = null;
}
public void initSounds(Context context) {
this.mContext = context;
this.mSoundPool = new SoundPool(1, 3, 0);
this.mSoundPoolMap = new HashMap<>();
this.mPlaying = false;
addSound("0", R.raw.zero, 378);
addSound("1", R.raw.one, 360);
addSound("2", R.raw.two, 342);
addSound("3", R.raw.three, 386);
addSound("4", R.raw.four, 414);
addSound("5", R.raw.five, 373);
addSound("6", R.raw.six, 378);
addSound("7", R.raw.seven, 386);
addSound("8", R.raw.eight, 342);
addSound("9", R.raw.nine, 396);
addSound("shi", R.raw.shi, 396);
addSound("bai", R.raw.bai, 342);
addSound("qian", R.raw.qian, 378);
addSound("wan", R.raw.wan, 342);
addSound("yi", R.raw.yi, 324);
addSound("yuan", R.raw.yuan, 378);
addSound("dot", R.raw.dot, 360);
addSound("cgsk", R.raw.cgsk, 1440);
}
public void playSeqSounds(List<String> soundsToPlay) {
int length = soundsToPlay.size();
for (int j = 0; ; j++) {
if (j >= length) {
if (!this.mPlaying)
playNextSound();
return;
}
String str = soundsToPlay.get(j);
this.mSoundQueue.add(str);
}
}
/**
* Play the sound
*
* @param text text for the sound
*/
public void playSound(String text) {
stopSound();
this.mSoundQueue.add(text);
playNextSound();
}
/**
* Stop playing the sound .
*/
public void stopSound() {
this.mHandler.removeCallbacks(this.mPlayNext);
this.mSoundQueue.clear();
this.mSoundPool.stop(this.mCurrentStreamId);
this.mPlaying = false;
}
/**
* Remove all the sound
*/
public void unloadAll() {
stopSound();
if (this.mSoundPoolMap.size() > 0) {
Iterator<String> localIterator = this.mSoundPoolMap.keySet().iterator();
while (true) {
if (!localIterator.hasNext()) {
this.mSoundPoolMap.clear();
return;
}
String str = localIterator.next();
this.mSoundPool.unload(this.mSoundPoolMap.get(str).id);
}
}
}
private final class Sound {
public int id;
public int time;
public Sound(int id, int time) {
this.id = id;
this.time = time;
}
}
}
金额转换
播放声音的规则完成之后,还需要控制播放的内容。 一般播放金额的时候,有以下规则:
- 每4位数字后面加一个单位,单位是亿、万。这4位数字中间的单位是千、百、十。
- 如果中间有多个0,那么只要读一次0。且首位不能为0。
- 当为2位数且第一位为1时,1不用读 。
- 有小数点时,直接读"点",然后按顺序读数字。
public class VoiceHelper {
private static final String TAG = "VoiceHelper: ";
private static String[] units = {"qian", "bai", "shi"};
public static final String WAN = "wan";
public static final String YI = "yi";
public static final String DOT = "dot";
public static final String CGSK = "cgsk";
public static final String YUAN = "yuan";
// 4321 2345 6789 . 12345
// 四 千 三 百 二 十 一 亿 二 千 三 百 四 十 五 万 六 千 七 百 八 十 九 点 xxxx 元
public static void playNumber(SoundManager soundManager, String price) {
if (!isLegalPrice(price)) {
Log.w(TAG, "error format price");
return;
}
soundManager.stopSound();
List<String> speech = new LinkedList<>();
convertPrice(speech, price);
Log.d(TAG, speech.toString());
soundManager.playSeqSounds(speech);
}
private static void convertPrice(List<String> speech, String price) {
speech.add(CGSK);
String[] strings = price.split("\\.");
String integerString = strings[0];
int startPosition = 0;
//处理 亿位 及以上
if (integerString.length() > 8) {
String oneHundredMillionString = integerString.substring(0, integerString.length() - 8);
startPosition = integerString.length() - 8;
speech.addAll(unitPrice(oneHundredMillionString));
speech.add(YI);
}
//处理 亿位 以下 至 万位
if (integerString.length() > 4) {
String tenHundredString = integerString.substring(startPosition, integerString.length() - 4);
speech.addAll(unitPrice(tenHundredString));
startPosition = integerString.length() - 4;
speech.add(WAN);
}
//处理 万位 以下
String finalString = integerString.substring(startPosition, integerString.length());
if (finalString.equals("0")) {
speech.add("0");
}
else {
speech.addAll(unitPrice(finalString));
}
//处理小数点后的读音
if (strings.length == 2) {
speech.add(DOT);
int decimalLength = strings[1].length();
for (int i = 0; i < decimalLength; i++) {
speech.add(String.valueOf(strings[1].charAt(i)));
}
}
speech.add(YUAN);
}
private static List<String> unitPrice(String simplePrice) {
List<String> result = new LinkedList<>();
int length = simplePrice.length();
//当为2位数且第一位为1时,1不用读
if (length == 2 && simplePrice.charAt(0) == '1') {
result.add(units[2]);
char lastChar = simplePrice.charAt(1);
if (lastChar != '0') {
result.add(String.valueOf(lastChar));
}
return result;
}
boolean readZero = simplePrice.charAt(0) == '0';
int unitPos = 4 - length;
if (simplePrice.length() <= 4) {
for (int i = 0; i < length; i++) {
if (simplePrice.charAt(i) != '0') {
if (readZero) {
result.add("0");
}
readZero = false;
result.add(String.valueOf(simplePrice.charAt(i)));
//最后一个数字不用读单位
if (i != length - 1)
result.add(units[unitPos]);
} else {
readZero = true;
}
unitPos++;
}
}
return result;
}
private static boolean isLegalPrice(String price) {
boolean res = true;
int len = price.length();
for (int i = 0; i < len; i++) {
if (!Character.isDigit(price.charAt(i)) && price.charAt(i) != '.') {
res = false;
break;
}
}
if (price.split("\\.")[0].length() > 12) {
res = false;
}
if (price.length() >= 2 && price.charAt(0) == '0' && price.charAt(1) != '.') {
res = false;
}
return res;
}
}