搜索文章

输入关键词开始搜索

如何播放和存储音频流文件

程序设计#JavaScript

今天有个需求,想用一下文本转语音的功能。转换接口返回给我们的音频是一个字符串,因此需要做一些转换才能使用。这里记录下。

播放

接口返回的是一个base64加密的字符串,因此我们需要先对其做解密,然后将其转为Blob对象(Binary Large Object,在Web领域,Blob被定义为包含只读数据的类文件对象),赋值给audio标签的src属性。

这是浏览器端执行的代码(我从MDN找了base64转ArrayBuffer的函数来使用):

<audio src="" id="audio" controls="controls">wav</audio>


<script>
/*\
|*|
|*|  Base64 / binary data / UTF-8 strings utilities (#1)
|*|
|*|  https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding
|*|
|*|  Author: madmurphy
|*|
\*/

/* Array of bytes to base64 string decoding */

function b64ToUint6 (nChr) {

  return nChr > 64 && nChr < 91 ?
      nChr - 65
    : nChr > 96 && nChr < 123 ?
      nChr - 71
    : nChr > 47 && nChr < 58 ?
      nChr + 4
    : nChr === 43 ?
      62
    : nChr === 47 ?
      63
    :
      0;

}

function base64DecToArr (sBase64, nBlockSize) {

  var
    sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), nInLen = sB64Enc.length,
    nOutLen = nBlockSize ? Math.ceil((nInLen * 3 + 1 >>> 2) / nBlockSize) * nBlockSize : nInLen * 3 + 1 >>> 2, aBytes = new Uint8Array(nOutLen);

  for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) {
    nMod4 = nInIdx & 3;
    nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4;
    if (nMod4 === 3 || nInLen - nInIdx === 1) {
      for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) {
        aBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255;
      }
      nUint24 = 0;
    }
  }

  return aBytes;
}

let str = '这是base64字符串,太长了,我就省略了';

var myBuffer = base64DecToArr(str).buffer
// 注意要把内容装入一个数组
var binaryData = [];
binaryData.push(myBuffer);
let url = window.URL.createObjectURL(new Blob(binaryData, {type: "audio/x-wav"}))

document.getElementById('audio').src =  url;
</script>

存储

我们在做Demo的时候,直接引入一个.wav的文件会比引入长长的数据字符串要方便得多,因此我找了一下将音频数据写入文件的方法。同样会用到上面的将base64字符串转为ArrayBuffer的方法:

var fs = require('fs'); //文件读写
var request = require('request'); //发送请求

let contents = [
    '并购重组可视化的动画演示',
    'ST珠江通过定向增发的方式,横向收购京粮股份,最终京粮股份借壳上市。'
];

contents.forEach(async (rawContent) => {
    let content = encodeURI(rawContent);
    let appId = '应用的ID';
    let appKey = '应用的秘钥';
    let url = `http://this.is.url/?app_id=${appId}&app_key=${appKey}&voice_type=1&samplint_rate=0&audio_type=1&speed_rate=100&pitch_rate=0&engine_type=0&text=${content}`;

    await translate(url, rawContent + '.mp3')
})

/**
 * 文本转语音
 * @param {*} url 
 * @param {*} fileName 
 */
async function translate(url, fileName) {
    request({
        url: url, 
        method: "GET", 
        headers: { 
            "content-type": "application/json",
        },
    }, async function (error, response, body) {
        if (!error && response.statusCode == 200) {

            let bodyObj = JSON.parse(body);
            let audioObj = JSON.parse(bodyObj.data)

            await saveToWavFile(fileName, audioObj.audio_data)

        } else {
            console.log(error);
        }
    });
}

/**
 * 将数据流保存到文件中
 * @param {*} path 
 * @param {*} base64Str 
 */
async function saveToWavFile(path, base64Str) {
    // 直接将Buffer写入文件即可,不需要转换了
    let data = base64DecToArr(base64Str);

    // 写入文件
    await fs.writeFile(path, data, async (err) => {
        if (err) {
            console.log('Write failed')
            console.log(err)
        } else {
            console.log(`Write to file ${path} successfully`)
        }
    })
}

function b64ToUint6(nChr) {

    return nChr > 64 && nChr < 91 ?
        nChr - 65 :
        nChr > 96 && nChr < 123 ?
        nChr - 71 :
        nChr > 47 && nChr < 58 ?
        nChr + 4 :
        nChr === 43 ?
        62 :
        nChr === 47 ?
        63 :
        0;

}

function base64DecToArr(sBase64, nBlockSize) {
    var
        sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""),
        nInLen = sB64Enc.length,
        nOutLen = nBlockSize ? Math.ceil((nInLen * 3 + 1 >>> 2) / nBlockSize) * nBlockSize : nInLen * 3 + 1 >>> 2,
        aBytes = new Uint8Array(nOutLen);

    for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) {
        nMod4 = nInIdx & 3;
        nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4;
        if (nMod4 === 3 || nInLen - nInIdx === 1) {
            for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) {
                aBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255;
            }
            nUint24 = 0;
        }
    }

    return aBytes;
}

由于只是临时用一下,所以代码就写得比较随意了。

这是服务端通过node运行的代码,注意里面不再需要将数据转为Blob对象(服务端默认也是没有Blob这个类的),直接将二进制数组写入文件即可。

另外,请求中的中文,记得用encodeURI转一下,否则会报错。

自动播放

移动设备,特别是iOS上,是不允许自动播放音频的,这是出于用户体验的考虑而进行的一个浏览器规范设计。

那么如果我们想要在移动设备上自动播放音频该怎么办呢?

这种情况我们可以考虑通过一些交互,比如一个开屏页,引导用户先去点一下页面,触发交互动作,然后再播放音频。

如何延迟播放音频

如果需要延迟播放音频,仅靠上面的处理还不够,因为直接在setTimeout中触发audio的play()方法,是无法实现播放的。因此我们在用户交互的时候,先触发play(),然后立即暂停,等到真正需要播放的时间到了,再重新触发play():

document.getElementById('click').onclick = function () {	
    document.getElementById('myaudio').play()
    document.getElementById('myaudio').pause()

    setTimeout(function () {
        console.log('start playing')
        document.getElementById('myaudio').play()
    }, 2000)  
}