技术背景
首先交代一下调研的技术背景:
根据调研,集成语音播报的方式市面上大致有三种:
第一种:基于Android系统自带的TextToSpeech类和讯飞API实现的语音播报
由于TextToSpeech只支持英文,德语,意大利语,法语,西班牙语,不支持中文,所以需要安装科大讯飞引擎,这种方式不支持使用,毕竟播报的固定就那么几个字,在线文字转音频,用TTS比较麻烦
第二种:仿支付宝方式,提前录制好音频,然后根据金额拼接成一段音频
我采用的是这种方式,自己封装实现语音播报功能
第三种:集成第三方SDK实现语音播报(例如讯飞)
这种使用方便但是成本高
需求
公众号扫码付款成功,app进行接收播报,播报文案为“收款成功XX元”,收到多条消息进行顺序播报
功能实现
在这里主要是讲解第二种方式,采用音频拼接合成方式
思路:
1、 拿到要播放的金额,把金额转为大写,大写转音频
2、 播放单个语音
3、 顺序播放
实现部分:
1、新建一个关于金额的工具类,写两个数组,一个是数字的0-9的数组,一个是拾到亿的数组,写一个方法进行判断替换,返回关于金额的中文大写文字,将大写文字转成音频
代码如下:
/**
* 关于金钱的工具类
*/
public class MoneyUtils {
private static final char[] NUM = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
private static final char[] CHINESE_UNIT = {'元', '拾', '佰', '仟', '万', '拾', '佰', '仟', '亿', '拾', '佰', '仟'};
/**
* 返回关于钱的中文式大写数字,支仅持到亿
*/
public static String readInt(int moneyNum) {
String res = "";
int i = 0;
if (moneyNum == 0) {
return "0";
}
if (moneyNum == 10) {
return "拾";
}
if (moneyNum > 10 && moneyNum < 20) {
return "拾" + moneyNum % 10;
}
while (moneyNum > 0) {
res = CHINESE_UNIT[i++] + res;
res = NUM[moneyNum % 10] + res;
moneyNum /= 10;
}
return res.replaceAll("0[拾佰仟]", "0")
.replaceAll("0+亿", "亿")
.replaceAll("0+万", "万")
.replaceAll("0+元", "元")
.replaceAll("0+", "0")
.replace("元", "");
}
}
/**
* 返回数字对应的音频
*
* @param integerPart
* @return
*/
private static List readIntPart(String integerPart) {
List result = new ArrayList<>();
String intString = MoneyUtils.readInt(Integer.parseInt(integerPart));
int len = intString.length();
for (int i = 0; i < len; i++) {
char current = intString.charAt(i);
if (current == '拾') {
result.add(VoiceConstants.TEN);
} else if (current == '佰') {
result.add(VoiceConstants.HUNDRED);
} else if (current == '仟') {
result.add(VoiceConstants.THOUSAND);
} else if (current == '万') {
result.add(VoiceConstants.TEN_THOUSAND);
} else if (current == '亿') {
result.add(VoiceConstants.TEN_MILLION);
} else {
result.add(String.valueOf(current));
}
}
return result;
}
2、创建一个MediaPlayer,数据源从assets中获取,调用prepareAsync()方法,异步加载,并设置监听,加载完毕后进行播放,监听播放完成的状态,在播放完成之后播放下一条语音
/**
* 开始播报
*
* @param voicePlay
*/
private void start(final List voicePlay) {
synchronized (VoicePlay.this) {
final MediaPlayer mMediaPlayer = new MediaPlayer();
final CountDownLatch mCountDownLatch = new CountDownLatch(1);
AssetFileDescriptor assetFileDescription = null;
try {
final int[] counter = {0};
assetFileDescription = FileUtils.getAssetFileDescription(mContext,
String.format(VoiceConstants.FILE_PATH, voicePlay.get(counter[0])));
mMediaPlayer.setDataSource(
assetFileDescription.getFileDescriptor(),
assetFileDescription.getStartOffset(),
assetFileDescription.getLength());
mMediaPlayer.prepareAsync();
mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mMediaPlayer.start();
}
});
mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mediaPlayer) {
// mediaPlayer -> {
mediaPlayer.reset();
counter[0]++;
if (counter[0] < voicePlay.size()) {
try {
AssetFileDescriptor fileDescription2 = FileUtils.getAssetFileDescription(mContext,
String.format(VoiceConstants.FILE_PATH, voicePlay.get(counter[0])));
mediaPlayer.setDataSource(
fileDescription2.getFileDescriptor(),
fileDescription2.getStartOffset(),
fileDescription2.getLength());
mediaPlayer.prepare();
} catch (IOException e) {
e.printStackTrace();
mCountDownLatch.countDown();
}
} else {
mediaPlayer.release();
mCountDownLatch.countDown();
}
}
// }
});
} catch (Exception e) {
e.printStackTrace();
mCountDownLatch.countDown();
} finally {
if (assetFileDescription != null) {
try {
assetFileDescription.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
try {
mCountDownLatch.await();
notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
3、短时间多次播报请求,采用同步方式进行,一条播完播放下一条,这里采用synchronized + notifyAll() 实现,当然也可以用其他的方式
player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
mp.reset();
counter[0]++;
if (counter[0] < list.size()) {
try {
AssetFileDescriptor fileDescriptor = FileUtils.getAssetFileDescription(String.format("sound/tts_%s.mp3", list.get(counter[0])));
mp.setDataSource(fileDescriptor.getFileDescriptor(), fileDescriptor.getStartOffset(), fileDescriptor.getLength());
mp.prepare();
} catch (IOException e) {
e.printStackTrace();
latch.countDown();
}
} else {
mp.release();
latch.countDown();
}
}
});
遇到的问题及解决方案(因为项目推送用到个推,所以遇到这些问题,可以参考一下)
1.第一次初始化个推得到cid有延迟,造成得不到cid无法调用后台服务接口进行个推绑定cid,所以接收不到推送消息进行语音播报
解决办法:第一次登陆完成之后进行个推初始化,在首页进行cid判断,如果没有就用定时器每三秒去取一次cid,直到取到cid就关闭定时器调用后台接口绑定个推,(注意定时器执行了五次也就是15秒才取到cid,每个设备获取cid的时间不确定),如果出现cid一直取不到,但是定时器一直没有关闭在获取cid的情况下,设定定时器只要执行十次也就是30秒就自动关闭定时器
2.语音播报语速缓慢的问题
解决方法:使用MediaPlay组件进行语速加速,无法解决问题,发现是一个音频前后有无声部分,所以进行音频剪切
3.app登陆状态下,手机亮屏,过了20分钟app就自动挂了,所以无法接收到推送消息进行语音播报
解决方法:修改个推服务,设置为前台服务,可保证亮屏状态下,app一直在运行
4.在同一台设备上,登陆多个不同的账户,如登陆三个账户,则会推送三条重复的消息进行三次播报
解决方法:造成这个原因是每次登录都要调用后台接口,进行绑定,需传参cid、账户id,后台绑定同一个cid却有三个不同的账户,所以每次推送会推送三条,所以在退出登录的时候,做登出的操作,调用后台接口进行解绑
PS.以上为自己的经验总结分享,有不足的地方欢迎指正,共同进步。