采集PCM,将base64片段转换为wav音频文件

需求

开始录音------监听录音数据------结束录音

在监听录音数据过程中:客户端每100ms给前端传输一次数据(pcm数据转成base64),前端需要将base64片段解码、合并、添加WAV头、转成File、上传到 OSS之后将 url 给到服务端处理。

{
  numberOfChannels: 1, // 声道数
  // sampleRate: 16000, // 采样率
  sampleRate: 44100, // 更改采样率为 44100 Hz
  bitsPerChannel: 16, // 位深
  format: 'PCM',
}

概念

pcm是原始音频,mac上可以使用audacity软件播放pcm原始音频文件;

👇

base64编码:将二进制编码成文本格式

👇

atob 将二进制转为 unicode 字符序列,charCodeAt 获取每个字符的unicode编码

👇

Uint8Array 是包含8位(一个字节)的无符号整数序列,用于处理二进制数据

👇

ArrayBuffer 在内存中分配一段连续的空间,存储二进制数据,如数字、图像、音频文件等

👇

new Blob([wavHeader, pcmData], { type: 'audio/wav' }); 给PCM数据添加wav头信息

👇

Blob 是浏览器内部生成的二进制数据,包括数据和类型信息

👇

File 是 Blob 的子类,除了数据和类型信息,还包括文件名和最后修改时间,通常表示用户从本地文件系统选择的文件

将base64片段转为WAV文件

/**
 * 将base64片段转为WAV文件
 * @param base64Segments
 * @returns
 */
export function base64ToAudio(base64Segments) {
  // 合并PCM数据
  const pcmData = mergeBase64SegmentsIntoPCM(base64Segments);
  // 创建WAV头
  const dataLength = pcmData.length;
  const wavHeader = createWavHeader(dataLength, 44100);
  // 合并WAV文件头和PCM数据
  const blob = new Blob([wavHeader, pcmData], { type: 'audio/wav' });
  const file = new File([blob], 'output.wav', { type: 'audio/wav' });
  return file;
}

将一系列Base64编码的音频段合并成一个PCM数据流

/**
 * 将一系列Base64编码的音频段合并成一个PCM数据流
 * @param segments 包含Base64编码音频段的数组
 * @returns
 */
function mergeBase64SegmentsIntoPCM(segments) {
  let mergedData = new Uint8Array();
  segments.forEach((base64Segment) => {
    const binarySegment = atob(base64Segment);
    const binaryArray = new Uint8Array(binarySegment.length);
    for (let i = 0; i < binarySegment.length; i++) {
      binaryArray[i] = binarySegment.charCodeAt(i);
    }
    mergedData = mergeArrays(mergedData, binaryArray);
  });
  // 合并后的PCM数据
  return mergedData;
}

合并两个TypedArray(类型化数组)

/**
 * 合并两个TypedArray(类型化数组)
 * @param segments
 * @returns
 */
function mergeArrays(a, b) {
  // 类型化数组,确保类型一致
  const c = new a.constructor(a.length + b.length);
  // 类型化数组的set方法直接在底层内存中操作,不需要逐个元素拷贝,效率高
  c.set(a, 0);
  // 保障合并后的数组在内存中是连续的,提高访问速度
  c.set(b, a.length);
  return c;
}

创建一个WAV文件的头部信息

/**
 * 创建一个WAV文件的头部信息
 * 包含了RIFF格式标识、文件大小、WAVE标识、格式子块fmt的ID和大小、音频格式、
 * 声道数、采样率、字节率、块对齐、每样本位数以及数据子块data的ID和大小
 * @param dataSize 文件大小
 * @param sampleRate 采样率
 * @returns
 */
function createWavHeader(dataSize, sampleRate) {
  // 创建一个大小为44字节的ArrayBuffer,用于存储WAV文件头
  const buffer = new ArrayBuffer(44);
  // 创建一个DataView,用于操作buffer中的数据
  const view = new DataView(buffer);

  view.setUint32(0, 0x52494646, false); // 设置Chunk ID为"RIFF"
  view.setUint32(4, dataSize + 36, true); // 设置文件大小(不包括前8个字节)
  view.setUint32(8, 0x57415645, false); // 设置格式标识为"WAVE"
  view.setUint32(12, 0x666d7420, false); // 设置第一个子块ID为"fmt "
  view.setUint32(16, 16, true); // 设置第一个子块大小为16字节
  view.setUint16(20, 1, true); // 设置音频格式为PCM(1表示PCM)
  view.setUint16(22, 1, true); // 设置声道数(单声道为1)
  view.setUint32(24, sampleRate, true); // 设置采样率
  view.setUint32(28, sampleRate * 2, true); // 设置字节率(采样率 * 每帧字节数)
  view.setUint16(32, 2, true); // 设置每帧字节数(块对齐)
  view.setUint16(34, 16, true); // 设置每样本位数
  view.setUint32(36, 0x64617461, false); // 设置第二个子块ID为"data"
  view.setUint32(40, dataSize, true); // 设置第二个子块大小(即音频数据大小)

  // 返回填充了WAV文件头信息的buffer
  return buffer;
}

异步获取音频文件的时长

/**
 * 异步获取音频文件的时长
 * @param file 音频文件
 * @returns 返回音频的时长(秒)
 */
export const getAudioDuration = async (file) => {
  try {
    const audio = new Audio(URL.createObjectURL(file));
    await new Promise((resolve) => (audio.onloadedmetadata = resolve));
    const { duration } = audio;
    return duration;
  } catch (error) {
    console.error('获取音频时长时发生错误:', error);
    return 0;
  }
};

将文件上传到oss

export const uploadFile = (data: UploadTokenData, file: File) => {
  console.log('uploadFile开始了', data, '====', file);

  const bodyFormData = new FormData();
  const url = `${data.host}/${data.dir}${file.name}`;

  bodyFormData.append('OSSAccessKeyId', data.accessId);
  bodyFormData.append('policy', data.policy);
  bodyFormData.append('signature', data.signature);
  bodyFormData.append('key', `${data.dir}${file.name}`);
  bodyFormData.append('dir', data.dir);
  bodyFormData.append('success_action_status', '200');
  bodyFormData.append('file', file);

  console.log('uploadFile上传的url: ', url);

  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();

    xhr.onerror = function error(e) {
      console.log('upload error', e);
      reject(e);
    };
    xhr.onload = async () => {
      // allow success when 2xx status see https://github.com/react-component/upload/issues/34
      if (xhr.status < 200 || xhr.status >= 300) {
        reject('上传异常');
      }
      console.log('upload success');
      resolve({
        ...data,
        ossUrl: url,
      });
    };
    xhr.open('post', data.host, true);
    xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
    xhr.send(bodyFormData);
  });
};
相关推荐
limengshi1383922 天前
通信工程学习:什么是PCM脉冲编码调制、DPCM差分脉冲编码调制、ADPCM自适应差分脉冲编码调制
网络·学习·信息与通信·pcm·adpcm
C有点难。5 天前
音频原始数据PCM
音视频·pcm
楚肽生物--多肽合成定制6 天前
特异性心肌细胞靶向肽(PCM);WLSEAGPVVTVRALRGTGSW;CAS:771479-86-8
科技·学习·pcm
跃龙客9 天前
PCM转PCMA(pcm_alaw,G711.A率)转换表 && PCM转PCMU(pcm_ulaw,G711.U率)转换表
pcm·g711
彷徨而立10 天前
音频PCM的能量dB计算
pcm
畅联云平台12 天前
美畅物联丨从模拟到数字的华丽转身:PCM及其衍生编码技术解析
pcm
lxzlife1 个月前
ffmpeg把pcm编码为aac
ffmpeg·pcm·aac
lxzlife1 个月前
ffmpeg把pcm编码为mp3
c++·ffmpeg·音视频·pcm·mpeg-1
Fre丸子_1 个月前
ffmpeg解析pcm文件进行播放
ffmpeg·pcm