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 Hz bitsPerChannel: 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 ( {list.map((_, index) => ( ))} ); } 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(); 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 ( { 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)); }
一个完整的音频录制——播放的例子
pcmtowav getUserMedia需要https,使用localhost或127.0.0.1时,可用http。
相关内容