android 使用 SoundPool 语音播报

前言

这段时间又开始捣鼓 android 了
现在有个需求是我们需要在天气预报首页有个播报天气语音的功能,接到需求开始了调研,最终选择了后端对接百度语音合成API,前端调用后端接口获取到语音文件保存到本地使用 SoundPool 播报语音的一个流程。

技术方案准备

  • 百度在线语音合成API
  • Android SoundPool
    SoundPool 通常用在游戏音效,他和 MediaPlay 播放器相比更轻量级,耗费资源小,适合播放几十秒的音频,但相比 MediaPlay 也缺少一些功能,比如没有音频播放完毕的回调,需要自己实现
    在相比 MediaPlay 的优点是它可以多个音频同时播放,这对我们该场景天气预报的语音播报中需要增加背景音乐来说十分有效

实现步骤

后端

后端实现使用 Java,实现一个接口 text2audio 前端传参 text,后端拿到 text 去请求百度语音合成 API,返回的是一个 byte[],然后将 byte[] 进行 base64 返回到客户端

    public static String text2audio(String text, String cuid) {

        try {

            RestTemplate restTemplate = new RestTemplate();

            // 设置请求头
            HttpHeaders headers = new HttpHeaders();
            headers.add("Content-Type", "application/x-www-form-urlencoded");
            // 设置请求参数
            MultiValueMap<String, Object> postParameters = new LinkedMultiValueMap<>();
            postParameters.add("tex", text);
            postParameters.add("tok", getAccessToken());
            postParameters.add("cuid", cuid);
            postParameters.add("ctp", "1");
            postParameters.add("lan", "zh");

            HttpEntity<MultiValueMap<String, Object>> httpEntity = new HttpEntity<>(postParameters, headers);

            ResponseEntity<byte[]> entity = restTemplate.exchange(speechTtsUrlUrl,
                    HttpMethod.POST, httpEntity, byte[].class);

            byte[] body = entity.getBody();

            return new String(Objects.requireNonNull(Base64.encodeBase64(body)));

        } catch (RestClientResponseException ex) {

            log.error("百度AI开放平台语音合成接口报错");
            throw new BaseException(SystemErrorType.SYSTEM_ERROR, "百度AI开放平台语音合成接口报错");
        }

验证该 base64 为有效的方式:拿到该 base64 的字符串后,在开头加上 data:audio/mp3;base64,,然后将整段复制到浏览器打开,可以播放表示没问题

android 客户端实现

android 客户端采用的是 Kotlin 语言编写

实现步骤

  1. 调用接口,获取到返回的 base64 字符串

  2. 将接口返回的 base64 转为 MP3 文件存入本地临时文件

    var fos: FileOutputStream? = null
    // 创建天气预报语音存储路径
    val weatherForecastMp3Path = Environment.getExternalStorageDirectory().absolutePath +
             File.separator + "xxx" + File.separator + "audio/weatherforecast.mp3"
    // createOrExistsFile 可自行实现
    if (FileUtil.createOrExistsFile(weatherForecastMp3Path)) {
     fos = FileOutputStream(weatherForecastMp3Path)
    } else {
     LogUtil.e("create $weatherForecastMp3Path failed!")
    }
    // 将接口返回的 base64 的语音存入文件
    val buffer: ByteArray = BASE64Decoder().decodeBuffer(it)
    fos?.write(buffer)
  3. 初始化 SoundPoo,并设置最大同时播放数为2,并加载已经在本地存好的背景音乐和第 2 步存好的 MP3 文件

    // 初始化 soundPool 播放器
    soundPool = SoundPool.Builder()
    .setMaxStreams(2) // 同时播放数
    .build()
    // 初始化天气预报语音和背景音乐变量
    // R.raw.weatherbeijingmusic 为背景音乐,放在 res/raw 文件夹下
    val weatherBeijingMusicSound: Int? =
    soundPool?.load(requireContext(), R.raw.weatherbeijingmusic, 1)
    val weatherForecastSound: Int? = soundPool?.load(weatherForecastMp3Path, 1)
  4. 使用 MediaPlay 获取 MP3 文件的语音时长,供 handler.postDelayed 定时器执行

    // 使用 mediaPlayer 获取要语音播报时长
    val mediaPlayer = MediaPlayer()
    mediaPlayer.setDataSource(weatherForecastMp3Path)
    mediaPlayer.prepare()
    val weatherForecastMp3Duration = mediaPlayer.duration.toLong()
  5. 开始播放背景音乐和语音播报,并开启 postDelayed 定时器

         // 定义 SoundPool 成功加载音频文件数
         var audioSuccessLoadCount = 0
         soundPool?.setOnLoadCompleteListener { soundPool, sampleId, status ->
    
             // status = 0 为成功加载
             if (status == 0) {
    
                 audioSuccessLoadCount++
    
                 if (audioSuccessLoadCount == 2) {
    
                     /**
                      * 参数依次是
                      * soundID:Load()返回的声音ID号
                      * leftVolume:左声道音量设置
                      * rightVolume:右声道音量设置
                      * priority:指定播放声音的优先级,数值越高,优先级越大。
                      * loop:指定是否循环:-1表示无限循环,0表示不循环,其他值表示要重复播放的次数
                      */
                     weatherBeijingMusicSound?.let {
                         soundPool.play(weatherBeijingMusicSound, 0.6f, 0.6f, 1, 0, 1f)
                     }
                     weatherForecastSound?.let {
                         soundPool.play(weatherForecastSound, 1f, 1f, 1, 0, 1f)
    
                         // 定时器,等到播放完毕,执行 stopAudioFun
                         handler.postDelayed(stopAudioFun, weatherForecastMp3Duration)
                     }
                 }
    
             } else {
                 ToastUtil.showShort("播放失败,请重试")
             }
         }
  6. 在定时器结束后释放相关资源(如有播放动画效果等)

     // 停止语音播报方法
     @SuppressLint("Range")
     val stopAudioFun = Runnable {
         // 执行相关操作
     }

    参考文章

    SoundPool
    blog.csdn.net/huangxiaoguo1/articl...
    www.bbsmax.com/A/GBJr1nAWz0/
    github.com/Thor-jelly/SoundPoolUti...
    github1s.com/masjoel/MySound/blob/...
    blog.csdn.net/chunbichi4375/articl...

    Android SoundPool只能播放6秒 mp3 文件
    blog.csdn.net/jiaqiangziji/article...

    Android获取视频音频的时长的方法
    www.cnblogs.com/ldq2016/p/7233350....

    LottieAnimationView怎么重置为第一帧
    blog.csdn.net/AndroidOliver/articl...

    mp3 剪切
    mp3cut.net/

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!