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

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

播放

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<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的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
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():

1
2
3
4
5
6
7
8
9
document.getElementById('click').onclick = function () {	
document.getElementById('myaudio').play()
document.getElementById('myaudio').pause()

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