采集PCM,将base64片段转换为wav音频文件
创始人
2024-12-17 06:33:59
0

需求

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

在监听录音数据过程中:客户端每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);   }); }; 

相关内容

热门资讯

一分钟了解!创思维辅助器(辅助... 一分钟了解!创思维辅助器(辅助挂)本来有开挂辅助神器(有挂秘籍)1、不需要AI权限,帮助你快速的进行...
安装程序教程!微信小程序微乐破... 安装程序教程!微信小程序微乐破解器,雀友会广东潮汕辅助有开挂,辅助教程(确实有挂)是一款可以让一直输...
指导大家!葫芦娃七子降妖破解版... 指导大家!葫芦娃七子降妖破解版(辅助挂)切实有开挂辅助黑科技(确实有挂);1.葫芦娃七子降妖破解版 ...
一分钟了解!樱花之盛辅助器下载... 您好,樱花之盛辅助器下载这款游戏可以开挂的,确实是有挂的,需要了解加微【136704302】很多玩家...
重大消息!闲逸免费app辅助(... 重大消息!闲逸免费app辅助(辅助挂)其实有开挂辅助平台(的确有挂)1、这是跨平台的闲逸免费app辅...
教程辅助!微乐小程序黑科技免费... 教程辅助!微乐小程序黑科技免费知乎,广东雀神麻木智能插件安装,必赢教程(新版有挂);人气非常高,ai...
玩家爆料!广西老友玩有破解吗(... 玩家爆料!广西老友玩有破解吗(辅助挂)总是有开挂辅助软件(有挂方法);1.广西老友玩有破解吗 ai辅...
科技通报!新荣耀平台辅助,新西... 您好,新西游大厅拼十辅助这款游戏可以开挂的,确实是有挂的,需要了解加微【136704302】很多玩家...
大家学习交流!微信大厅辅助软件... 大家学习交流!微信大厅辅助软件(辅助挂)竟然有开挂辅助脚本(存在有挂)1、完成微信大厅辅助软件透视辅...
解密关于!花花生活圈辅助工具安... 解密关于!花花生活圈辅助工具安装教程(辅助挂)一向有开挂辅助挂(有挂方法);花花生活圈辅助工具安装教...