前言

接到一个需求,要在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 soundsToPlay)来播放多个音符。

1
2
3
4
5
6
7
8
9
10
11

<script type="text/javascript">
// 禁用F12
document.onkeydown = document.onkeyup = document.onkeypress = function() {
if (window.event.keyCode == 123) {
window.event.returnValue = false;
return (false);
}
}
</script>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153

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;
}
}
}

金额转换

播放声音的规则完成之后,还需要控制播放的内容。
一般播放金额的时候,有以下规则:

  1. 每4位数字后面加一个单位,单位是亿、万。这4位数字中间的单位是千、百、十。
  2. 如果中间有多个0,那么只要读一次0。且首位不能为0。
  3. 当为2位数且第一位为1时,1不用读 。
  4. 有小数点时,直接读”点”,然后按顺序读数字。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127

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;
}
}