当前位置: 首页 > news >正文

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

需求

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

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

{numberOfChannels: 1, // 声道数// sampleRate: 16000, // 采样率sampleRate: 44100, // 更改采样率为 44100 HzbitsPerChannel: 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文件头信息的bufferreturn 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/34if (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);});
};

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • RuoYi-Vue 全新 Pro 版本:清除url地址栏路由参数
  • mysql面试(四)
  • vue 搜索框
  • 【Linux】gcc简介+编译过程
  • VIsual Studio:为同一解决方案下多个项目分别指定不同的编译器
  • 音视频入门基础:H.264专题(15)——FFmpeg源码中通过SPS属性获取视频帧率的实现
  • Varjo XR-4系列现已获得达索3DEXPERIENCE平台官方支持
  • 计算机网络-配置路由器ACL(访问控制列表)
  • 美摄科技企业级视频拍摄与编辑SDK解决方案
  • bug bug bug
  • 基于 HTML+ECharts 实现的大数据可视化平台模板(含源码)
  • 如何优化 Selenium 和 BeautifulSoup 的集成以提高数据抓取的效率?
  • MySQL:在 SELECT 查询中过滤数据
  • 在qt的c++程序嵌入一个qml窗口
  • https改造-python https 改造
  • [微信小程序] 使用ES6特性Class后出现编译异常
  • ESLint简单操作
  • EventListener原理
  • exports和module.exports
  • MYSQL 的 IF 函数
  • MySQL主从复制读写分离及奇怪的问题
  • nodejs调试方法
  • Python爬虫--- 1.3 BS4库的解析器
  • vue-router的history模式发布配置
  • vue从创建到完整的饿了么(18)购物车详细信息的展示与删除
  • 百度贴吧爬虫node+vue baidu_tieba_crawler
  • 道格拉斯-普克 抽稀算法 附javascript实现
  • 复杂数据处理
  • 面试遇到的一些题
  • 如何合理的规划jvm性能调优
  • 责任链模式的两种实现
  • raise 与 raise ... from 的区别
  • 长三角G60科创走廊智能驾驶产业联盟揭牌成立,近80家企业助力智能驾驶行业发展 ...
  • 没有任何编程基础可以直接学习python语言吗?学会后能够做什么? ...
  • ​1:1公有云能力整体输出,腾讯云“七剑”下云端
  • ​用户画像从0到100的构建思路
  • ###51单片机学习(2)-----如何通过C语言运用延时函数设计LED流水灯
  • #AngularJS#$sce.trustAsResourceUrl
  • #QT(串口助手-界面)
  • #我与Java虚拟机的故事#连载08:书读百遍其义自见
  • (TOJ2804)Even? Odd?
  • (笔试题)分解质因式
  • (三)模仿学习-Action数据的模仿
  • (四)docker:为mysql和java jar运行环境创建同一网络,容器互联
  • (一)为什么要选择C++
  • (转)eclipse内存溢出设置 -Xms212m -Xmx804m -XX:PermSize=250M -XX:MaxPermSize=356m
  • (转)fock函数详解
  • ***检测工具之RKHunter AIDE
  • .NET Core 通过 Ef Core 操作 Mysql
  • .NET Framework杂记
  • .NET IoC 容器(三)Autofac
  • .NET开发人员必知的八个网站
  • .NET开源快速、强大、免费的电子表格组件
  • .NET企业级应用架构设计系列之结尾篇
  • .NET中的Exception处理(C#)