Android集成语音播报

作者: p_ding | 来源:发表于2019-08-01 17:28 被阅读220次

    技术背景

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

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

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

    相关文章

      网友评论

        本文标题:Android集成语音播报

        本文链接:https://www.haomeiwen.com/subject/scscdctx.html