android 使用 SoundPool 語音播報

邢闖洋發表於2021-08-18

這段時間又開始搗鼓 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 協議》,轉載必須註明作者和本文連結

相關文章