金额语音播报,做过支付行业的人都接触过这关功能,实现方法有很多种
比较常见的如 TTS(系统自带声音),语音引擎SDK(一些收费,这里不讨论),MP3(资源文件顺序播报)
TTS
最简单的实现方法吧,不过声音的改变比较困难,需要下载声音引擎
参考上一篇博客《金钱转中文字符串》得到中文字符串
然后使用tts进行播报
textToSpeech = TextToSpeech(this,TextToSpeech.OnInitListener {
if (it == TextToSpeech.SUCCESS){
textToSpeech.language = Locale.CHINESE
textToSpeech .speak("收款"+MoneyUtil.readString("30.57"),TextToSpeech.QUEUE_FLUSH,null,"1")
}
})
一个简单的语音播报就这样实现了
基于MP3资源实现
简书 Android 语音播报实现方案(无SDK),写得非常棒,不过这里使用的是Mediaplayer播放器,这里做了一些改进
由于是基于Mediaplayer,基本是监听播放完成和解码完成的监听器,然后播放完成后立刻解码下一个音频文件。
这种每次需要播放时才去解码语音文件进行播放,文字和文字加的语速间隔是不可控的。比如 一些低端机解码慢
资源准备
https://ai.baidu.com/tech/speech/tts_online
百度AI语音合成,也可以自己录~
准备 0~9 的语音 点,千,百,十,亿,元,***收款成功,等语音
SoundPool
android专门用来播放短时长提示音,缺点是短~,无法知道啥时候播放完成。优点的轻量级,一次load,后面即可实时播放
基于资源的语音播报,一般一个音频大概是0.5s-1s,应该没人会把1 拖到一秒种那么长吧,SoundPool刚好符合需求,短
1、SoundPool的初始化
/**
* SoundPool ID ArrayMap
* 数据量小使用ArrayMap比HashMap性能更好
* */
private var soundId = ArrayMap<String,Int>()
/**
* 初始化,第一次使用时自动初始化,无需手动调用
* @param context
* @param moneyString 播报的金额中文串(通过MoneyUtil获得)
* @param autoPlay 初始化后是否自动播放
* */
private fun init(context: Context,moneyString:String ="",autoPlay:Boolean=false){
var audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build();
var assets = context.assets
mSoundPool = SoundPool.Builder().setAudioAttributes(audioAttributes).setMaxStreams(22).build()
mSoundPool.setOnLoadCompleteListener { _, sampleId, status ->
if (status == 0 &&sampleId == soundId.size){ //所有资源初始化成功
isPrepare = true //解码成功
if (autoPlay){ //是否自动播放
playVoicePool(moneyString,context)
}
}
}
soundId.clear()
soundId["sound/tts_1.mp3"] = mSoundPool.load(assets.openFd("sound/tts_1.mp3"),1)
soundId["sound/tts_2.mp3"] = mSoundPool.load(assets.openFd("sound/tts_2.mp3"),1)
soundId["sound/tts_3.mp3"] = mSoundPool.load(assets.openFd("sound/tts_3.mp3"),1)
soundId["sound/tts_4.mp3"] = mSoundPool.load(assets.openFd("sound/tts_4.mp3"),1)
soundId["sound/tts_5.mp3"] = mSoundPool.load(assets.openFd("sound/tts_5.mp3"),1)
soundId["sound/tts_6.mp3"] = mSoundPool.load(assets.openFd("sound/tts_6.mp3"),1)
soundId["sound/tts_7.mp3"] = mSoundPool.load(assets.openFd("sound/tts_7.mp3"),1)
soundId["sound/tts_8.mp3"] = mSoundPool.load(assets.openFd("sound/tts_8.mp3"),1)
soundId["sound/tts_9.mp3"] = mSoundPool.load(assets.openFd("sound/tts_9.mp3"),1)
soundId["sound/tts_0.mp3"] = mSoundPool.load(assets.openFd("sound/tts_0.mp3"),1)
soundId["sound/tts_dot.mp3"] = mSoundPool.load(assets.openFd("sound/tts_dot.mp3"),1)
soundId["sound/tts_hundred.mp3"] = mSoundPool.load(assets.openFd("sound/tts_hundred.mp3"),1)
soundId["sound/tts_receipt.mp3"] = mSoundPool.load(assets.openFd("sound/tts_receipt.mp3"),1)
soundId["sound/tts_success.mp3"] = mSoundPool.load(assets.openFd("sound/tts_success.mp3"),1)
soundId["sound/tts_ten_million.mp3"] = mSoundPool.load(assets.openFd("sound/tts_ten_million.mp3"),1)
soundId["sound/tts_ten_thousand.mp3"] = mSoundPool.load(assets.openFd("sound/tts_ten_thousand.mp3"),1)
soundId["sound/tts_ten.mp3"] = mSoundPool.load(assets.openFd("sound/tts_ten.mp3"),1)
soundId["sound/tts_thousand.mp3"] = mSoundPool.load(assets.openFd("sound/tts_thousand.mp3"),1)
soundId["sound/tts_yuan.mp3"] = mSoundPool.load(assets.openFd("sound/tts_yuan.mp3"),1)
}
这里可以第一次播放时自动初始化,也可以程序开始直接初始化,都可
2、进行语音播报
fun playVoicePool(moneyString: String,context: Context) {
thread { //异步线程
synchronized(this){ //同步锁
if (isPrepare){ //判断SoudPool是否加载完成
var soundList = getListSound(moneyString) //通过monneyString获取播放的音频
for (i in soundList){
if (soundId[i]!=null){
mSoundPool.play(soundId[i]!!,1f,1f,1,0,1f) //最后一个参数是语速
if (i.contains("tts_success")){ //tts_succes为***收款,间隔长一点
Thread.sleep(1100)
}else{
Thread.sleep(400) // 1,2 十,百,千,亿等等
}
}
}
}else{
init(context,moneyString,true) //还未初始化则初始化
}
}
}
}
3、根据MoneyString获取对应的语音播放链表
/**
* 根据@link MoneyUtil生成的中文串来获取链表
* @param moneyString 播报的金额中文串(通过MoneyUtil获得)
* @return
* */
private fun getListSound(moneyString: String):LinkedList<String>{
var soundList = LinkedList<String>()
var moneyChar = moneyString.toCharArray()
soundList.add("sound/tts_success.mp3")
for (i in moneyChar){
if (NUM.contains(i)){
soundList.add(getStringFormNUM(i))
}
if (CHIN_NUM.contains(i)){
soundList.add(getStringFormCHINNUM(i))
}
}
return soundList
}
/**
* 获取中文字符资源
* @param char
* */
private fun getStringFormCHINNUM(char: Char):String{
var chineseNumPath =""
when(char){
'元' -> chineseNumPath = "sound/tts_yuan.mp3"
'拾' -> chineseNumPath = "sound/tts_ten.mp3"
'佰' -> chineseNumPath = "sound/tts_hundred.mp3"
'仟' -> chineseNumPath = "sound/tts_thousand.mp3"
'万' -> chineseNumPath = "sound/tts_ten_thousand.mp3"
'亿' -> chineseNumPath = "sound/tts_ten_million.mp3"
'点' -> chineseNumPath = "sound/tts_dot.mp3"
}
return chineseNumPath
}
/**
* 获取数字资源
* @param char
* */
private fun getStringFormNUM(char:Char):String{
return String.format("sound/tts_%s.mp3",char)
}
/**
* 释放资源
* */
fun destroyPool(){
if (mSoundPool!=null){
mSoundPool.autoPause()
mSoundPool.release()
mSoundPool == null
}
}
使用单例子模式封装代码(完整代码)
/**
* @author HuangJiaHeng
* @date 2020/6/5.
* 金额语音播报类
* playVoicePool SoundPool 可以有效防止卡顿
*/
class VoiceSpeak {
/**
* SoundPool 使用SoundPool方式才初始化
* */
private lateinit var mSoundPool:SoundPool
/**
* SoundPool ID ArrayMap
* 数据量小使用ArrayMap比HashMap性能更好
* */
private var soundId = ArrayMap<String,Int>()
/**
* SoundPool是否加载完成
* */
private var isPrepare = false
companion object{
private val NUM = arrayListOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9')
private val CHIN_NUM = arrayListOf('元', '拾', '佰', '仟', '万', '亿','点')
val util by lazy(LazyThreadSafetyMode.SYNCHRONIZED){
VoiceSpeak()
}
}
/**
* 释放资源
* */
fun destroyPool(){
if (mSoundPool!=null){
mSoundPool.autoPause()
mSoundPool.release()
mSoundPool == null
}
}
/**
* 以池的方式播放,可以有效减少资源解码卡顿情况 (同步锁)
* @param moneyString 播报的金额中文串(通过MoneyUtil获得)
* @param context
* */
fun playVoicePool(moneyString: String,context: Context) {
thread {
synchronized(this){
if (isPrepare){
var soundList = getListSound(moneyString)
for (i in soundList){
if (soundId[i]!=null){
mSoundPool.play(soundId[i]!!,1f,1f,1,0,1f)
if (i.contains("tts_success")){
Thread.sleep(1100)
}else{
Thread.sleep(400)
}
}
}
}else{
init(context,moneyString,true)
}
}
}
}
/**
* 根据@link MoneyUtil生成的中文串来获取链表
* @param moneyString 播报的金额中文串(通过MoneyUtil获得)
* @return
* */
private fun getListSound(moneyString: String):LinkedList<String>{
var soundList = LinkedList<String>()
var moneyChar = moneyString.toCharArray()
soundList.add("sound/tts_success.mp3")
for (i in moneyChar){
if (NUM.contains(i)){
soundList.add(getStringFormNUM(i))
}
if (CHIN_NUM.contains(i)){
soundList.add(getStringFormCHINNUM(i))
}
}
return soundList
}
/**
* 获取中文字符资源
* @param char
* */
private fun getStringFormCHINNUM(char: Char):String{
var chineseNumPath =""
when(char){
'元' -> chineseNumPath = "sound/tts_yuan.mp3"
'拾' -> chineseNumPath = "sound/tts_ten.mp3"
'佰' -> chineseNumPath = "sound/tts_hundred.mp3"
'仟' -> chineseNumPath = "sound/tts_thousand.mp3"
'万' -> chineseNumPath = "sound/tts_ten_thousand.mp3"
'亿' -> chineseNumPath = "sound/tts_ten_million.mp3"
'点' -> chineseNumPath = "sound/tts_dot.mp3"
}
return chineseNumPath
}
/**
* 获取数字资源
* @param char
* */
private fun getStringFormNUM(char:Char):String{
return String.format("sound/tts_%s.mp3",char)
}
/**
* 初始化,第一次使用时自动初始化,无需手动调用
* @param context
* @param moneyString 播报的金额中文串(通过MoneyUtil获得)
* @param autoPlay 初始化后是否自动播放
* */
private fun init(context: Context,moneyString:String ="",autoPlay:Boolean=false){
var audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build();
var assets = context.assets
mSoundPool = SoundPool.Builder().setAudioAttributes(audioAttributes).setMaxStreams(22).build()
mSoundPool.setOnLoadCompleteListener { _, sampleId, status ->
if (status == 0 &&sampleId == soundId.size){
isPrepare = true
if (autoPlay){
playVoicePool(moneyString,context)
}
}
}
soundId.clear()
soundId["sound/tts_1.mp3"] = mSoundPool.load(assets.openFd("sound/tts_1.mp3"),1)
soundId["sound/tts_2.mp3"] = mSoundPool.load(assets.openFd("sound/tts_2.mp3"),1)
soundId["sound/tts_3.mp3"] = mSoundPool.load(assets.openFd("sound/tts_3.mp3"),1)
soundId["sound/tts_4.mp3"] = mSoundPool.load(assets.openFd("sound/tts_4.mp3"),1)
soundId["sound/tts_5.mp3"] = mSoundPool.load(assets.openFd("sound/tts_5.mp3"),1)
soundId["sound/tts_6.mp3"] = mSoundPool.load(assets.openFd("sound/tts_6.mp3"),1)
soundId["sound/tts_7.mp3"] = mSoundPool.load(assets.openFd("sound/tts_7.mp3"),1)
soundId["sound/tts_8.mp3"] = mSoundPool.load(assets.openFd("sound/tts_8.mp3"),1)
soundId["sound/tts_9.mp3"] = mSoundPool.load(assets.openFd("sound/tts_9.mp3"),1)
soundId["sound/tts_0.mp3"] = mSoundPool.load(assets.openFd("sound/tts_0.mp3"),1)
soundId["sound/tts_dot.mp3"] = mSoundPool.load(assets.openFd("sound/tts_dot.mp3"),1)
soundId["sound/tts_hundred.mp3"] = mSoundPool.load(assets.openFd("sound/tts_hundred.mp3"),1)
soundId["sound/tts_success.mp3"] = mSoundPool.load(assets.openFd("sound/tts_success.mp3"),1)
soundId["sound/tts_ten_million.mp3"] = mSoundPool.load(assets.openFd("sound/tts_ten_million.mp3"),1)
soundId["sound/tts_ten_thousand.mp3"] = mSoundPool.load(assets.openFd("sound/tts_ten_thousand.mp3"),1)
soundId["sound/tts_ten.mp3"] = mSoundPool.load(assets.openFd("sound/tts_ten.mp3"),1)
soundId["sound/tts_thousand.mp3"] = mSoundPool.load(assets.openFd("sound/tts_thousand.mp3"),1)
soundId["sound/tts_yuan.mp3"] = mSoundPool.load(assets.openFd("sound/tts_yuan.mp3"),1)
}
}
使用:
VoiceSpeak.util.playVoicePool(MoneyUtil.readString("30.57"),this)
VoiceSpeak.util.playVoicePool(MoneyUtil.readString("108"),this)
VoiceSpeak.util.playVoicePool(MoneyUtil.readString("556.78"),this)
VoiceSpeak.util.playVoicePool(MoneyUtil.readString("104850000.56"),this)
githubDemo :这么简单就不要demo了吧 【滑稽】
资源放在 assets/sound目录,文件名和路径要和代码里面一致