Android集成语音播报

技术背景

首先交代一下调研的技术背景:

根据调研,集成语音播报的方式市面上大致有三种:

第一种:基于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<String> readIntPart(String integerPart) {
    List<String> 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<String> 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.以上为自己的经验总结分享,有不足的地方欢迎指正,共同进步。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352