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

js录制音频

整体思路

1、监听用户onTouchStart事件,设置一个定时器记录是否长按,然后调用JSBridge开始录制事件
2、通过JSBridge监听录音过程,拿到录音的数据,前端用数组变量存放
3、监听用户onTouchEnd松开事件,清除定时器,处理录音数组转换成一个文件上传到oss

难点

难点一:将base64录音片段转为WAV文件
首先将一系列Base64编码的音频段合并成一个PCM数据流;
然后创建一个WAV文件的头部信息;
最后合并WAV文件头和PCM数据

难点二:TypedArray数据的合并
TypedArray: 存储的是固定类型的数值数据,如整数或浮点数。
Array: 可以存储任何类型的数据,包括数字、字符串、对象等

开始录音

  /*** 开始录音*/const handleTouchStart = (event) => {event.preventDefault();timerId = setTimeout(() => {setLongPress(true);console.log('handleTouchStart 长按了');JSBridge(XX.startRecording', {numberOfChannels: 1, // 声道数// sampleRate: 16000, // 采样率sampleRate: 44100, // 更改采样率为 44100 HzbitsPerChannel: 16, // 位深format: 'PCM',}).then(() => {setRecordStatus('dialog_listening');});}, 100); // 长按时长,这里设置为100ms};

监听录音过程

 const onRecordChange = (event) => {console.log(event);const { error, param } = event || {};const { pcm } = param || {};const { errorCode, errorMsg } = error || {};if (errorCode) {Toast.show({type: 'error',content: `录制失败,${errorMsg}`,});baseArrayRef.current = [];} else {baseArrayRef.current.push(pcm);}};useEffect(() => {document.addEventListener('RecordingDataBufferTransfer', onRecordChange);return () => {// 清除长按定时器if (timerId !== null) clearTimeout(timerId);};}, []);

结束录制

/*** 结束录音* @returns*/const handleTouchEnd = (event) => {if (timerId !== null) {clearTimeout(timerId)timerId = null}if (!longPress) return;setLongPress(false);console.log('handleTouchEnd 松开了');JSBridge('XX.stopRecording').then(() => {// 移除事件监听器document.removeEventListener('RecordingDataBufferTransfer',onRecordChange,);setRecordStatus('dialog_sleep');onMerge();});};

音频波动动画

VoiceAnimation/index.tsx

import cls from 'classnames';
import debounce from 'lodash/debounce';
import { useLayoutEffect, useMemo, useRef } from 'react';
import styles from './index.module.less';interface IProps {status: string;
}
export default function (props: IProps) {const { status = 'dialog_sleep' } = props;const list = useMemo(() => new Array(5).fill(true), []);return (<div className={cls(styles.voice, status)}>{list.map((_, index) => (<AnimationItem status={status} index={index} />))}</div>);
}function getTransationByStatus(status: string, index?) {return {dialog_sleep: {transition: 'all 0.3s',height: '8px',transform: 'translateY(0)',},dialog_idle: {transition: 'all 0.3s',height: '8px',transform: 'translateY(0)',},dialog_listening: {transition: 'all 0.3s',height: '24px',transform: index % 2 ? 'translateY(8px)' : 'translateY(-8px)',onTransitionEnd: debounce((event) => {if (event.target.parentElement.className.indexOf('dialog_listening') ===-1)return;event.target.style.transitionDuration = '0.5s';event.target.style.height = '24px';event.target.style.transform =event.target.style.transform === 'translateY(8px)'? 'translateY(-8px)': 'translateY(8px)';},{leading: true,trailing: false,},),},dialog_thinking: {transition: 'all 0.3s',height: `${[52, 44, 36, 28, 24][index]}px`,transform: 'translateY(0)',onTransitionEnd: debounce((event) => {if (event.target.parentElement.className.indexOf('dialog_thinking') ===-1)return;event.target.style.transitionDuration = '0.5s';event.target.style.height = {'52px': '24px','44px': '28px','36px': '32px','32px': '36px','28px': '44px','24px': '52px',}[event.target.style.height];},{leading: true,trailing: false,},),},dialog_responding: {transition: 'all 0.2s',height: `${Math.random() * (index + 1) * 10 + 24}px`,transform: 'translateY(0)',onTransitionEnd: debounce((event) => {if (event.target.parentElement.className.indexOf('dialog_responding',) === -1)return;event.target.style.transitionDuration = '0.15s';event.target.style.height = `${Math.random() * (index + 1) * 10 + 24}px`;},{leading: true,trailing: false,},),},}[status];
}function AnimationItem({ status, index }: { status: string; index?: number }) {const div = useRef<any>();useLayoutEffect(() => {const container = div.current as HTMLDivElement;function reset() {container.ontransitionend = (e) => {};container.style.transition = 'all .1s';container.style.height = '24px';container.style.transform = 'translateY(0)';}reset();const { onTransitionEnd = () => {}, ...style } =getTransationByStatus(status, index) || {};container.ontransitionend = onTransitionEnd;for (let prop in style) {container.style[prop] = style[prop];}return () => {};}, [status]);return (<div ref={div} className={styles.item} style={{ width: 24, height: 24 }} />);
}

VoiceAnimation/index.module.less

.voice {display: flex;justify-content: center;align-items: center;height: 56px;.item {// width: 24px;// height: 24px;background-color: var(--TY-Text-Brand-1);border-radius: 20px;margin: 0 4px;transform: translateY(0);}
}.loop(@n, @i: 0) when (@i <= @n) {&:nth-child(@{i}) {animation-delay: (@i * 0.2s);}.loop(@n, (@i + 1));
}

一个完整的音频录制——播放的例子

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>pcmtowav</title>
</head><body><div>getUserMedia需要https,使用localhost或127.0.0.1时,可用http。</div><button id="start">开始录音</button><button id="end">结束录音</button><button id="play">播放录音</button>
</body>
<script>var context = null,inputData = [],size = 0,audioInput = null,recorder = null,dataArray;document.getElementById('start').addEventListener('click', function () {context = new (window.AudioContext || window.webkitAudioContext)();// 清空数据inputData = [];// 录音节点recorder = context.createScriptProcessor(4096, 1, 1);recorder.onaudioprocess = function (e) {var data = e.inputBuffer.getChannelData(0);inputData.push(new Float32Array(data));size += data.length;}navigator.mediaDevices.getUserMedia({audio: true}).then((stream) => {audioInput = context.createMediaStreamSource(stream);}).catch((err) => {console.log('error');}).then(function () {audioInput.connect(recorder);recorder.connect(context.destination);});});document.getElementById('end').addEventListener('click', function () {recorder.disconnect();});document.getElementById('play').addEventListener('click', function () {recorder.disconnect();if (0 !== size) {// 组合数据// var data = combine(inputData, size);		inputSampleRate = context.sampleRate;context.decodeAudioData(encodeWAV().buffer, function (buffer) {// decodeAudioData,是支持promise,三参数的知识兼容老的playSound(buffer);}, function () {console.log('error');});// console.log(data.buffer);}});// ----------------------// 以下是增加的内容var inputSampleRate = 0;   // 输入采样率var oututSampleBits = 16;  // 输出采样数位// 数据简单处理function decompress() {// 合并var data = new Float32Array(size);var offset = 0; // 偏移量计算// 将二维数据,转成一维数据for (var i = 0; i < inputData.length; i++) {data.set(inputData[i], offset);offset += inputData[i].length;}return data;};function encodePCM() {let bytes = decompress(),sampleBits = oututSampleBits,offset = 0,dataLength = bytes.length * (sampleBits / 8),buffer = new ArrayBuffer(dataLength),data = new DataView(buffer);// 写入采样数据 if (sampleBits === 8) {for (var i = 0; i < bytes.length; i++, offset++) {// 范围[-1, 1]var s = Math.max(-1, Math.min(1, bytes[i]));// 8位采样位划分成2^8=256份,它的范围是0-255; 16位的划分的是2^16=65536份,范围是-32768到32767// 因为我们收集的数据范围在[-1,1],那么你想转换成16位的话,只需要对负数*32768,对正数*32767,即可得到范围在[-32768,32767]的数据。// 对于8位的话,负数*128,正数*127,然后整体向上平移128(+128),即可得到[0,255]范围的数据。var val = s < 0 ? s * 128 : s * 127;val = parseInt(val + 128);data.setInt8(offset, val, true);}} else {for (var i = 0; i < bytes.length; i++, offset += 2) {var s = Math.max(-1, Math.min(1, bytes[i]));// 16位直接乘就行了data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);}}return data;}function encodeWAV() {var sampleRate = inputSampleRate;var sampleBits = oututSampleBits;var bytes = encodePCM();var buffer = new ArrayBuffer(44 + bytes.byteLength);var data = new DataView(buffer);var channelCount = 1;   // 单声道var offset = 0;// 资源交换文件标识符 writeString(data, offset, 'RIFF'); offset += 4;// 下个地址开始到文件尾总字节数,即文件大小-8 data.setUint32(offset, 36 + bytes.byteLength, true); offset += 4;// WAV文件标志writeString(data, offset, 'WAVE'); offset += 4;// 波形格式标志 writeString(data, offset, 'fmt '); offset += 4;// 过滤字节,一般为 0x10 = 16 data.setUint32(offset, 16, true); offset += 4;// 格式类别 (PCM形式采样数据) data.setUint16(offset, 1, true); offset += 2;// 通道数 data.setUint16(offset, channelCount, true); offset += 2;// 采样率,每秒样本数,表示每个通道的播放速度 data.setUint32(offset, sampleRate, true); offset += 4;// 波形数据传输率 (每秒平均字节数) 单声道×每秒数据位数×每样本数据位/8 data.setUint32(offset, channelCount * sampleRate * (sampleBits / 8), true); offset += 4;// 快数据调整数 采样一次占用字节数 单声道×每样本的数据位数/8 data.setUint16(offset, channelCount * (sampleBits / 8), true); offset += 2;// 每样本数据位数 data.setUint16(offset, sampleBits, true); offset += 2;// 数据标识符 writeString(data, offset, 'data'); offset += 4;// 采样数据总数,即数据总大小-44 data.setUint32(offset, bytes.byteLength, true); offset += 4;// 给wav头增加pcm体for (let i = 0; i < bytes.byteLength; ++i) {data.setUint8(offset, bytes.getUint8(i, true), true);offset++;}return data;}function getWAVBlob() {return new Blob([encodeWAV()], { type: 'audio/wav' });}function playSound(buffer) {var source = context.createBufferSource();// 设置数据source.buffer = buffer;// connect到扬声器source.connect(context.destination);source.start();}function writeString(data, offset, str) {for (var i = 0; i < str.length; i++) {data.setUint8(offset + i, str.charCodeAt(i));}}function combineDataView(resultConstructor, ...arrays) {let totalLength = 0,offset = 0;// 统计长度for (let arr of arrays) {totalLength += arr.length || arr.byteLength;}// 创建新的存放变量let buffer = new ArrayBuffer(totalLength),result = new resultConstructor(buffer);// 设置数据for (let arr of arrays) {// dataview合并for (let i = 0, len = arr.byteLength; i < len; ++i) {result.setInt8(offset, arr.getInt8(i));offset += 1;}}return result;}
</script></html>

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • httpx,一个网络请求的 Python 新宠儿
  • 24年电赛——自动行驶小车(H题)基于 CCS Theia -陀螺仪 JY60 代码移植到 MSPM0G3507(附代码)
  • 校园选课助手【5】-解决Elasticsearch和MySQL同步
  • 无人机之保护野生动物篇
  • JS+H5打字练习器
  • JAVA-案列练习-ATM项目
  • DDOS攻击有哪些攻击手段?
  • linux内核,中断上下文,中断下半部的处理2种方法: tasklet_schedule,工作队列workqueue
  • docker安装与container基本使用
  • android 音频播放器,(一)SoundPool音频播放实例
  • linux 原子操作,信号量,互斥锁mutex,自旋锁splock
  • 嵌入式数据库 sqlite3
  • 中央处理器CPU
  • 力扣第五十题——Pow(x,n)
  • Linux中yum、rpm、apt-get、wget的区别,yum、rpm、apt-get常用命令,CentOS、Ubuntu中安装wget
  • 9月CHINA-PUB-OPENDAY技术沙龙——IPHONE
  • 【108天】Java——《Head First Java》笔记(第1-4章)
  • Angular6错误 Service: No provider for Renderer2
  • Angularjs之国际化
  • Dubbo 整合 Pinpoint 做分布式服务请求跟踪
  • emacs初体验
  • java B2B2C 源码多租户电子商城系统-Kafka基本使用介绍
  • k个最大的数及变种小结
  • SQLServer插入数据
  • STAR法则
  • Vue 2.3、2.4 知识点小结
  • webgl (原生)基础入门指南【一】
  • 复杂数据处理
  • 什么软件可以剪辑音乐?
  • RDS-Mysql 物理备份恢复到本地数据库上
  • SAP CRM里Lead通过工作流自动创建Opportunity的原理讲解 ...
  • 昨天1024程序员节,我故意写了个死循环~
  • ​【C语言】长篇详解,字符系列篇3-----strstr,strtok,strerror字符串函数的使用【图文详解​】
  • ​Benvista PhotoZoom Pro 9.0.4新功能介绍
  • ​LeetCode解法汇总2696. 删除子串后的字符串最小长度
  • ​如何在iOS手机上查看应用日志
  • ###51单片机学习(1)-----单片机烧录软件的使用,以及如何建立一个工程项目
  • #[Composer学习笔记]Part1:安装composer并通过composer创建一个项目
  • (十)T检验-第一部分
  • (四)模仿学习-完成后台管理页面查询
  • (未解决)jmeter报错之“请在微信客户端打开链接”
  • (一) springboot详细介绍
  • (一)基于IDEA的JAVA基础1
  • .net 程序发生了一个不可捕获的异常
  • .NET 给NuGet包添加Readme
  • .NET开源全面方便的第三方登录组件集合 - MrHuo.OAuth
  • .NET下ASPX编程的几个小问题
  • @AliasFor 使用
  • @Autowired注解的实现原理
  • [ CTF ] WriteUp- 2022年第三届“网鼎杯”网络安全大赛(白虎组)
  • [ element-ui:table ] 设置table中某些行数据禁止被选中,通过selectable 定义方法解决
  • [2019/05/17]解决springboot测试List接口时JSON传参异常
  • [Android View] 可绘制形状 (Shape Xml)
  • [android]-如何在向服务器发送request时附加已保存的cookie数据
  • [Cesium学习]