Open AnnCY1 opened 9 months ago
已经解决了,原来传过来的数据是 base64编码的pcm16音频数据,需要先转为wav格式的blob对象,然后才能转为url地址,代码我是问GPT问出来的,TypeScript实现:
// 已接收到最后一次消息包,发送结束命令并进行朗读
if (message.status == 2) {
let audioUrl = pcmToAudioUrl(chunk) // chunk是接口返回的base64字符串全部拼接到一起的数据
audioEle.value.src = audioUrl
audioEle.value.play() // 使用<audio>标签进行播放
/**
* 接收base64数据转为音频可播放的url地址
* @param {string} base64Data - base64编码的pcm16音频数据 就是tts接口发送回来的所有数据,不加任何处理
*/
// eslint-disable-next-line no-inner-declarations
function pcmToAudioUrl(base64Data: string): string {
// console.log(base64Data)
let pcmData = base64ToUint8Array(base64Data)
// 创建WAV格式的Blob对象 (这是重点!直接创建blob数据是无法播放的!)
const wavBlob = createWavBlob(pcmData)
// 将URL设置为音频源即可
return URL.createObjectURL(wavBlob)
// base64编码的pcm16音频数据 转换为unit8格式数据
function base64ToUint8Array(base64String: string) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/')
const rawData = window.atob(base64)
const outputArray = new Uint8Array(rawData.length)
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i)
}
return outputArray
}
// 创建WAV格式的Blob对象 (调节采样率来微调语速和音调)
function createWavBlob(pcmData: any) {
const format = 1 // 格式代码(1表示PCM)
const numChannels = 1 // 声道数量(单声道为1,立体声为2)
const sampleRate = 26500 // 采样率(例如44100 Hz)
const bitsPerSample = 16 // 每样本的位数(例如16位)
const blockAlign = numChannels * (bitsPerSample / 8) // 对齐单位
const byteRate = sampleRate * blockAlign // 每秒的字节数
const buffer = new ArrayBuffer(44 + pcmData.length) // WAV文件头部长度为44字节
const view = new DataView(buffer)
// 写入WAV文件头部信息
writeString(view, 0, 'RIFF') // ChunkID
view.setUint32(4, 36 + pcmData.length, true) // ChunkSize
writeString(view, 8, 'WAVE') // Format
writeString(view, 12, 'fmt ') // Subchunk1ID
view.setUint32(16, 16, true) // Subchunk1Size
view.setUint16(20, format, true) // AudioFormat
view.setUint16(22, numChannels, true) // NumChannels
view.setUint32(24, sampleRate, true) // SampleRate
view.setUint32(28, byteRate, true) // ByteRate
view.setUint16(32, blockAlign, true) // BlockAlign
view.setUint16(34, bitsPerSample, true) // BitsPerSample
writeString(view, 36, 'data') // Subchunk2ID
view.setUint32(40, pcmData.length, true) // Subchunk2Size
// 将PCM数据写入buffer
const pcmDataView = new Uint8Array(buffer, 44)
pcmDataView.set(pcmData)
return new Blob([view], { type: 'audio/wav' })
}
// 写入字符串到DataView中的指定位置
function writeString(view: any, offset: any, string: any) {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i))
}
}
}
}
这种方法只能实现接收到全部音频数据后使用audio标签播放,如果想实现流式播放音频,应该还需要一个流式播放器的函数,我看官方提供的demo里面是把音频数据放到一个叫流式播放器的函数里面执行的。
已经解决了,原来传过来的数据是 base64编码的pcm16音频数据,需要先转为wav格式的blob对象,然后才能转为url地址,代码我是问GPT问出来的,TypeScript实现:
// 已接收到最后一次消息包,发送结束命令并进行朗读 if (message.status == 2) { let audioUrl = pcmToAudioUrl(chunk) // chunk是接口返回的base64字符串全部拼接到一起的数据 audioEle.value.src = audioUrl audioEle.value.play() // 使用<audio>标签进行播放 /** * 接收base64数据转为音频可播放的url地址 * @param {string} base64Data - base64编码的pcm16音频数据 就是tts接口发送回来的所有数据,不加任何处理 */ // eslint-disable-next-line no-inner-declarations function pcmToAudioUrl(base64Data: string): string { // console.log(base64Data) let pcmData = base64ToUint8Array(base64Data) // 创建WAV格式的Blob对象 (这是重点!直接创建blob数据是无法播放的!) const wavBlob = createWavBlob(pcmData) // 将URL设置为音频源即可 return URL.createObjectURL(wavBlob) // base64编码的pcm16音频数据 转换为unit8格式数据 function base64ToUint8Array(base64String: string) { const padding = '='.repeat((4 - (base64String.length % 4)) % 4) const base64 = (base64String + padding) .replace(/-/g, '+') .replace(/_/g, '/') const rawData = window.atob(base64) const outputArray = new Uint8Array(rawData.length) for (let i = 0; i < rawData.length; ++i) { outputArray[i] = rawData.charCodeAt(i) } return outputArray } // 创建WAV格式的Blob对象 (调节采样率来微调语速和音调) function createWavBlob(pcmData: any) { const format = 1 // 格式代码(1表示PCM) const numChannels = 1 // 声道数量(单声道为1,立体声为2) const sampleRate = 26500 // 采样率(例如44100 Hz) const bitsPerSample = 16 // 每样本的位数(例如16位) const blockAlign = numChannels * (bitsPerSample / 8) // 对齐单位 const byteRate = sampleRate * blockAlign // 每秒的字节数 const buffer = new ArrayBuffer(44 + pcmData.length) // WAV文件头部长度为44字节 const view = new DataView(buffer) // 写入WAV文件头部信息 writeString(view, 0, 'RIFF') // ChunkID view.setUint32(4, 36 + pcmData.length, true) // ChunkSize writeString(view, 8, 'WAVE') // Format writeString(view, 12, 'fmt ') // Subchunk1ID view.setUint32(16, 16, true) // Subchunk1Size view.setUint16(20, format, true) // AudioFormat view.setUint16(22, numChannels, true) // NumChannels view.setUint32(24, sampleRate, true) // SampleRate view.setUint32(28, byteRate, true) // ByteRate view.setUint16(32, blockAlign, true) // BlockAlign view.setUint16(34, bitsPerSample, true) // BitsPerSample writeString(view, 36, 'data') // Subchunk2ID view.setUint32(40, pcmData.length, true) // Subchunk2Size // 将PCM数据写入buffer const pcmDataView = new Uint8Array(buffer, 44) pcmDataView.set(pcmData) return new Blob([view], { type: 'audio/wav' }) } // 写入字符串到DataView中的指定位置 function writeString(view: any, offset: any, string: any) { for (let i = 0; i < string.length; i++) { view.setUint8(offset + i, string.charCodeAt(i)) } } } }
这种方法只能实现接收到全部音频数据后使用audio标签播放,如果想实现流式播放音频,应该还需要一个流式播放器的函数,我看官方提供的demo里面是把音频数据放到一个叫流式播放器的函数里面执行的。 兄弟,有没有解决Web页面流式播放的问题啊。如果解决了可以一分享一下吗?
基于@AnnCY1的代码改了下,在小程序上ok了
// 参考:https://github.com/PaddlePaddle/PaddleSpeech/issues/3530
// base64编码的pcm16音频数据 转换为unit8格式数据
export function pcmtowav(base64String) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/')
// const rawData = window.atob(base64)
// const rawData = atobPolyfill(base64)
const rawData = atobPolyfill(base64String) // 直接这样也可以
const outputArray = new Uint8Array(rawData.length)
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i)
}
return createWav(outputArray)
}
// 创建WAV格式的Blob对象 (调节采样率来微调语速和音调)
export function createWav(pcmData) {
const format = 1 // 格式代码(1表示PCM)
const numChannels = 1 // 声道数量(单声道为1,立体声为2)
const sampleRate = 24000 // 采样率(例如44100 Hz)
const bitsPerSample = 16 // 每样本的位数(例如16位)
const blockAlign = numChannels * (bitsPerSample / 8) // 对齐单位
const byteRate = sampleRate * blockAlign // 每秒的字节数
const buffer = new ArrayBuffer(44 + pcmData.length) // WAV文件头部长度为44字节
const view = new DataView(buffer)
// 写入WAV文件头部信息
writeString(view, 0, 'RIFF') // ChunkID
view.setUint32(4, 36 + pcmData.length, true) // ChunkSize
writeString(view, 8, 'WAVE') // Format
writeString(view, 12, 'fmt ') // Subchunk1ID
view.setUint32(16, 16, true) // Subchunk1Size
view.setUint16(20, format, true) // AudioFormat
view.setUint16(22, numChannels, true) // NumChannels
view.setUint32(24, sampleRate, true) // SampleRate
view.setUint32(28, byteRate, true) // ByteRate
view.setUint16(32, blockAlign, true) // BlockAlign
view.setUint16(34, bitsPerSample, true) // BitsPerSample
writeString(view, 36, 'data') // Subchunk2ID
view.setUint32(40, pcmData.length, true) // Subchunk2Size
// 将PCM数据写入buffer
const pcmDataView = new Uint8Array(buffer, 44)
pcmDataView.set(pcmData)
return uni.arrayBufferToBase64(view.buffer) // 小程序没有Blog, 直接返回buffer或转base64都可以保存文件后播放
// return view.buffer
// return new Blob([view], {
// type: 'audio/wav'
// })
}
function atobPolyfill(encodedString) {
// 创建一个映射表,用于将 base64 字符转换为二进制字符串
const base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
const base64CharsMap = {};
for (let i = 0; i < base64Chars.length; i++) {
base64CharsMap[base64Chars[i]] = i;
}
// 移除字符串中的填充字符(如果有的话)
let padding = encodedString.length % 4;
if (padding) {
encodedString += '='.repeat(4 - padding);
}
// 将 base64 编码的字符串转换为二进制字符串
let binaryString = '';
for (let i = 0; i < encodedString.length; i += 4) {
const char1 = base64CharsMap[encodedString[i]];
const char2 = base64CharsMap[encodedString[i + 1]];
const char3 = base64CharsMap[encodedString[i + 2]];
const char4 = base64CharsMap[encodedString[i + 3]];
const binary1 = char1.toString(2).padStart(6, '0');
const binary2 = char2.toString(2).padStart(6, '0');
const binary3 = char3 !== undefined ? char3.toString(2).padStart(6, '0') : '00';
const binary4 = char4 !== undefined ? char4.toString(2).padStart(6, '0') : '00';
binaryString += binary1 + binary2 + binary3 + binary4;
}
// 将二进制字符串转换为 ASCII 字符串
let asciiString = '';
for (let i = 0; i < binaryString.length; i += 8) {
const byte = binaryString.substring(i, i + 8);
asciiString += String.fromCharCode(parseInt(byte, 2));
}
return asciiString;
}
English base64 data returned by a webSocket connection cannot be played in the
Interface address: ws://{server}:{port}/paddlespeech/tts/streaming
中文: 使用tts接口,通过webSocket连接返回的base64数据无法在浏览器的
接口地址: ws://{server}:{port}/paddlespeech/tts/streaming