audio如何播放base64格式音频(pcm2wav)

2,042 阅读5分钟

前置知识

1、audio与video标签支持的文件格式

  • audio标签支持的文件格式
    • (1)ogg:一种新的音频压缩格式,是完全免费、开放和没有专利限制的
    • (2)mp3:是一种音频压缩技术。它被设计用来大幅度地降低音频数据量
    • (3)wav:为微软公司开发的一种声音文件格式,声音文件质量和CD相差无几
  • video标签支持的文件格式
    • (1)mp4:mpeg4文件使用H264视频编解码器和AAC音频编解码器(mpeg4=mp4)
    • (2)webm:webm文件使用VP8视频编解码器和Vorbis音频编解码器
    • (3)avi:avi支持256色和RLE压缩,它对视频文件采用了一种有损压缩方式
    • (4)ogv:ogv是html5中的一个名为ogg theora的视频格式

2、

  • 以AA..开头的代表静音数据开头,为纯pcm格式,需要先把pcm转化为wav格式

    浏览器是无法直接播放 pcm 音频的,因为 pcm 是比较原始的音频格式

    PCM(Puls Code Modulation)全称脉码调制录音,PCM录音就是将声音的模拟信号表示成0,1标识的数字信号,未经任何编码和压缩处理,所以可以认为PCM是未经压缩的音频原始格式。PCM格式文件中不包含头部信息,播放器无法知道采样率,声道数,采样位数,音频数据大小等信息,导致无法播放。

  • 只有不是A的字符开头的,才可能有wav的头部,可以利用'data:audio/wav;base64,' + base64的audio数据;拼接形成url

实现

主要代码

import '@/utils/polyfill';

const playAudio= async (file : File) => {
  //file为用户上传的含有base64格式音频数据的文件(业务上:该base64可能已经含有wav头部可能只是纯pcm格式)
  const text = await readerPlainText(file.raw!);
  const json = JSON.parse(text);
  //audio为base64的音频数据,sampleRate为采样率
  const audio =json.audio;
  const sampleRate = json.sampling;
  //file.audioCodec为业务上就包含了这个判断字段,也可以采用audio.startWith('A')来简要判断格式
  if (file.audioCodec === 'pcm') {
    const audioBuffer = base64ToArrayBuffer(audio);
    const wav = pcm2wav.encodeWav(Buffer.from(audioBuffer), {
      sampleRate: sampleRate || 24000,
      sampleBits: 16,
      numChannels: 1,
    });
    audioSrc.value = URL.createObjectURL(new Blob([wav]));
  } else {
    audioSrc.value = 'data:audio/wav;base64,' + audio;
  }
}

补充

以下主要是对上面主要代码的一些补充解释

1、引入polyfill.ts

需要引入polyfill.ts这个文件,否则报错Buffer is not defined 因为Buffer是node的东西,浏览器中JS没有Buffer对象

//polyfill.ts
import fill from 'buffer';

declare global {
  interface Window {
    Buffer: any;
  }
}

window.Buffer = fill.Buffer;

解决:将Buffer对象改用ArrayBuffer对象,ArrayBuffer对象是 ES6 才写入标准的。浏览器原生提供ArrayBuffer()构造函数,用来生成实例。它接受一个整数作为参数,表示这段二进制数据占用多少个字节。

//base64ToArrayBuffer 函数
export const base64ToArrayBuffer = (base64: string) => {
  const binary_string = window.atob(base64);
  const len = binary_string.length;
  const bytes = new Uint8Array(len);
  for (let i = 0; i < len; i++) {
    bytes[i] = binary_string.charCodeAt(i);
  }
  return bytes;
};

转换前:

image.png 转换后: image.png

2、pcm2wav代码实现

image.png

//pcm2wav.ts
/* eslint-disable @typescript-eslint/ban-ts-comment */
/* ------PCM二进制转wav格式。将PCM数据拼上encodeWAV返回的头即可-------- */

/**
 * @param rawPCM buffer || binary
 * @param options.numChannels
 * @param options.sampleRate
 * @param options.byteRate
 * @return Buffer
 * @throws Exception
 */
interface Options {
  numChannels: number;
  sampleRate: number;
  sampleBits: number;
}

function encodeWav(rawPCM: string | Buffer, options: Options): Buffer {
  if (typeof rawPCM === 'string') {
    rawPCM = Buffer.from(rawPCM, 'binary');
  }

  if (!Buffer.isBuffer(rawPCM)) {
    throw new TypeError('pcm data must be Buffer or string');
  }
  const opt = options || {};
  const sampleRate = opt.sampleRate || 16000;
  const numChannels = opt.numChannels || 1;
  const sampleBits = opt.sampleBits || 16;

  const buf = rawPCM;
  //@ts-ignore
  const header = new Buffer.alloc(44);

  header.write('RIFF', 0); // ChunkID。固定
  header.writeUInt32LE(buf.length + 44 - 8, 4); // ChunkSize。下个地址开始到文件尾总字节数,即文件大小-8
  header.write('WAVE', 8); // Format。固定
  header.write('fmt ', 12); // Subchunk1 ID。固定
  header.writeUInt8(16, 16); // Subchunk1 Size。一般为16
  header.writeUInt8(1, 20); // AudioFormat。1表示pcm
  header.writeUInt8(numChannels, 22); // Num Channels。声道数
  header.writeUInt32LE(sampleRate, 24); // SampleRate。采样率
  header.writeUInt32LE((sampleRate * numChannels * sampleBits) / 8, 28); // ByteRate。波特率,即声道数 × 采样频率 × 采样位数 / 8。
  header.writeUInt8((numChannels * sampleBits) / 8, 32); // BlockAlign。声道数 × 采样位数 / 8
  header.writeUInt8(sampleBits, 34); // Bits Per Sample。采样位数
  header.write('data', 36); // Subchunk2 Id。固定
  header.writeUInt32LE(buf.length, 40); // Subchunk2 Size。

  return Buffer.concat([header, buf]);
}

function decodeWav(rawWav: string | Buffer): Buffer {
  if (typeof rawWav === 'string') {
    rawWav = Buffer.from(rawWav, 'binary');
  }

  if (!Buffer.isBuffer(rawWav)) {
    throw new TypeError('pcm data must be Buffer or string');
  }

  // remove the header of pcm format
  rawWav = rawWav.slice(44);

  return rawWav;
}

export default {
  encodeWav,
  decodeWav,
};

3、ArrayBuffer与Uint8Array :

1.常见的js数组

var arr = new Array(5)

2.类型化数组TypedArray

在html5版本时中,TypedArray在WEBGL规范中被引入用于解决Javascript处理二进制数据的问题 (类型化数组也是数组,只不过其元素被设置为特定类型的值)

2.1 类型化数组ArrayBuffer

类型化数组的核心是一个名为ArrayBuffer的类型

每个ArrayBuffer对象表示的只是内存中指定的字节数; 但不会指定这些字节用于保存什么类型的数据; 通过ArrayBuffer能做的,就是为了将来使用而分配一定数量的字节.

// 创建一个8-byte的ArrayBuffer
var b = new ArrayBuffer(8);
 
// 创建一个b的引用,类型是Int32,起始位置在0,结束位置为缓冲区尾部
var v1 = new Int32Array(b);
 
// 创建一个b的引用,类型是Uint8,起始位置在2,结束位置为缓冲区尾部
var v2 = new Uint8Array(b, 2);
 
// 创建一个b的引用,类型是Int16,起始位置在2,总长度为2
var v3 = new Int16Array(b, 2, 2);

4、Buffer.from

5、atob

atob()函数对使用Base64编码编码的数据字符串进行解码。btoa()方法对数据进行编码和传输,否则可能会导致通信问题,然后传输并 atob()再次使用该方法对数据进行解码。例如,您可以编码、传输和解码控制字符,例如 ASCII 值 0 到 31。

const encodedData = btoa("Hello, world"); // encode a string
const decodedData = atob(encodedData); // decode the string

其他解决方法

1.在vue.config.js中配置ProvidePlugin

// vue.config.js
...
configureWebpack: {
  plugins: [
    ...
    new webpack.ProvidePlugin({
      process: 'process/browser', 
      Buffer: ['buffer', 'Buffer']
    })
  ]
}

stackoverflow.com/questions/7…

参考链接