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

react 录音功能

在这有两种录音时的动态效果

1.使用react-media-recorder库实现基础录音功能
2.添加录音计时器显示录音时长
3.实现音频可视化效果,通过Canvas绘制频谱图
4.处理麦克风权限状态检测
5.提供开始/停止录音和清除录音的控制按钮
在这里插入图片描述

先添加依赖

npm install react-media-recorder

// export default AudioRecorder;
import React, { useState, useEffect, useRef } from 'react';
import { useReactMediaRecorder } from 'react-media-recorder';
import ECGVisualizer from './component/ECGV'
import './index.less'
const EnhancedAudioRecorder = () => {const [recordingTime, setRecordingTime] = useState(0);const timerRef = useRef(null);const canvasRef = useRef(null);const audioContextRef = useRef(null);const analyserRef = useRef(null);const animationRef = useRef(null);const [permissionStatus, setPermissionStatus] = useState<'unknown' | 'granted' | 'denied'>('unknown');const {status,startRecording,stopRecording,mediaBlobUrl,previewAudioStream,clearBlobUrl,} = useReactMediaRecorder({audio: true,onStart: () => setPermissionStatus('granted'),onStop: (blobUrl, blob) => {clearInterval(timerRef.current);cancelAnimationFrame(animationRef.current);// setRecordingTime(0);}});// 检查初始权限状态useEffect(() => {const checkPermission = async () => {try {if (navigator.permissions) {const status = await navigator.permissions.query({ name: 'microphone' });setPermissionStatus(status.state);status.onchange = () => setPermissionStatus(status.state);}} catch (e) {console.log('Permissions API not supported');}};checkPermission();return () => clearInterval(timerRef.current);}, []);// 音频可视化处理useEffect(() => {if (previewAudioStream && status === 'recording') {timerRef.current = setInterval(() => {setRecordingTime(prev => prev + 1);}, 1000);setupAudioVisualizer();}return () => {cancelAnimationFrame(animationRef.current);clearInterval(timerRef.current);};}, [previewAudioStream, status]);const setupAudioVisualizer = () => {audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)();analyserRef.current = audioContextRef.current.createAnalyser();const source = audioContextRef.current.createMediaStreamSource(previewAudioStream);source.connect(analyserRef.current);analyserRef.current.fftSize = 512;const bufferLength = analyserRef.current.frequencyBinCount;const dataArray = new Uint8Array(bufferLength);const draw = () => {animationRef.current = requestAnimationFrame(draw);analyserRef.current.getByteFrequencyData(dataArray);const canvas = canvasRef.current;if (!canvas) return;const ctx = canvas.getContext('2d');const width = canvas.width;const height = canvas.height;ctx.clearRect(0, 0, width, height);const barWidth = (width / bufferLength) * 0.8;let x = 0;for (let i = 0; i < bufferLength; i++) {const barHeight = (Math.pow(dataArray[i]/255, 0.5)) * height;const hue = i / bufferLength * 360;ctx.fillStyle = `hsl(${hue}, 100%, 50%)`;ctx.fillRect(x, height - barHeight, barWidth, barHeight);x += barWidth + 1;}};draw();};const formatTime = (seconds) => {const mins = Math.floor(seconds / 60).toString().padStart(2, '0');const secs = (seconds % 60).toString().padStart(2, '0');return `${mins}:${secs}`;};// 处理开始录音逻辑const handleStartRecording = async () => {if (permissionStatus === 'denied') {alert('请在浏览器设置中允许麦克风权限后刷新页面');return;}try {await startRecording();} catch (err) {// 出错时清除定时器if (timerRef.current) {clearInterval(timerRef.current);}console.error('录音错误:', err);}};const handleClear=()=>{clearBlobUrl();   setRecordingTime(0);setupAudioVisualizer()}return (<div className="enhanced-recorder"><div className="recorder-controls"><buttononClick={handleStartRecording}disabled={status === 'recording'}>{status === 'recording' ? '录音中...' : '开始录音'}</button><buttononClick={stopRecording}disabled={status !== 'recording'}>停止录音</button><buttononClick={handleClear}>清除录音</button><div className="recording-time">录音时长: {formatTime(recordingTime)}</div></div><div className="visualizer-container"><canvasref={canvasRef}height={50}style={{  width:'100%'}}/></div><ECGVisualizer previewAudioStream={previewAudioStream} status={status} />{mediaBlobUrl && (<div className="playback-section"><h3>录音回放{mediaBlobUrl}</h3><audio src={mediaBlobUrl} controls /><ahref={mediaBlobUrl}download={`recording-${new Date().toISOString()}.wav`}className="download-btn">下载录音</a></div>)}<div className="status-info"><p>当前状态: <span className={status}>{status}</span></p></div></div>);
};export default EnhancedAudioRecorder;

第二种动态我做成了组件

import React, { useRef, useEffect } from 'react';
import { StatusMessages } from 'react-media-recorder';
import './ECGV.less'
interface ECGVisualizerProps {previewAudioStream: MediaStream | null;status: StatusMessages;
}const ECGVisualizer: React.FC<ECGVisualizerProps> = ({ previewAudioStream, status 
}) => {const canvasRef = useRef<HTMLCanvasElement>(null);const animationRef = useRef<number>();const audioContextRef = useRef<AudioContext>();const analyserRef = useRef<AnalyserNode>();const dataArrayRef = useRef<Uint8Array>();// 设置音频上下文和分析器const setupAudioContext = () => {if (!previewAudioStream || !canvasRef.current) return;audioContextRef.current = new (window.AudioContext || (window as any).webkitAudioContext)();analyserRef.current = audioContextRef.current.createAnalyser();const source = audioContextRef.current.createMediaStreamSource(previewAudioStream);source.connect(analyserRef.current);analyserRef.current.fftSize = 2048;const bufferLength = analyserRef.current.frequencyBinCount;dataArrayRef.current = new Uint8Array(bufferLength);drawECG();};// 绘制心电图效果const drawECG = () => {if (!analyserRef.current || !canvasRef.current || !dataArrayRef.current) return;const canvas = canvasRef.current;const ctx = canvas.getContext('2d');if (!ctx) return;const width = canvas.width;const height = canvas.height;analyserRef.current.getByteTimeDomainData(dataArrayRef.current);ctx.clearRect(0, 0, width, height);ctx.lineWidth = 2;ctx.strokeStyle = '#ff0000';ctx.beginPath();const sliceWidth = width / dataArrayRef.current.length;let x = 0;let lastY = height / 2;// 添加基线ctx.moveTo(0, height / 2);ctx.lineTo(width, height / 2);ctx.strokeStyle = 'rgba(255, 0, 0, 0.2)';ctx.stroke();ctx.beginPath();ctx.strokeStyle = '#ff0000';// 绘制心电图波形for (let i = 0; i < dataArrayRef.current.length; i++) {const v = dataArrayRef.current[i] / 128.0;const y = v * height / 2;if (i === 0) {ctx.moveTo(x, y);} else {if (Math.abs(y - lastY) > height / 4) {ctx.lineTo(x, y);ctx.stroke();ctx.beginPath();ctx.moveTo(x, y);} else {ctx.lineTo(x, y);}}lastY = y;x += sliceWidth;}ctx.stroke();animationRef.current = requestAnimationFrame(drawECG);};useEffect(() => {if (status === 'recording' && previewAudioStream) {setupAudioContext();}return () => {if (animationRef.current) {cancelAnimationFrame(animationRef.current);}if (audioContextRef.current && audioContextRef.current.state !== 'closed') {audioContextRef.current.close();}};}, [status, previewAudioStream]);return (<div className="ecg-container"><canvas ref={canvasRef} width={600} height={200}style={{background: '#111',display: status === 'recording' ? 'block' : 'none'}}/></div>);
};export default ECGVisualizer;
.ecg-recorder {max-width: 800px;margin: 0 auto;padding: 20px;background: #222;color: white;border-radius: 10px;font-family: 'Arial', sans-serif;
}.controls {display: flex;gap: 15px;align-items: center;margin-bottom: 20px;
}.record-btn {background: #e53935;padding: 10px 20px;border: none;border-radius: 5px;color: white;font-weight: bold;cursor: pointer;
}.record-btn:hover {background: #c62828;
}.record-btn:disabled {background: #555;cursor: not-allowed;
}.stop-btn {background: #1e88e5;padding: 10px 20px;border: none;border-radius: 5px;color: white;font-weight: bold;cursor: pointer;
}.stop-btn:hover {background: #1565c0;
}.stop-btn:disabled {background: #555;cursor: not-allowed;
}.timer {margin-left: auto;font-family: monospace;font-size: 1.2em;
}.ecg-container {margin: 20px 0;border: 1px solid #444;border-radius: 5px;overflow: hidden;
}.playback {margin-top: 20px;padding: 15px;background: #333;border-radius: 5px;
}.download-btn {display: inline-block;margin-top: 10px;padding: 8px 16px;background: #43a047;color: white;text-decoration: none;border-radius: 4px;
}.download-btn:hover {background: #2e7d32;
}audio {width: 100%;margin-top: 10px;
}
http://www.dtcms.com/a/290196.html

相关文章:

  • ODB安装与使用
  • 部署zabbix企业级分布式监控
  • 智慧场景:定制开发开源AI智能名片S2B2C商城小程序赋能零售新体验
  • SLAM实战——13章代码学习及回环检测的补充
  • STM32-第九节-ADC模数转换
  • ❗量化模型构建回测框架
  • 【2025/07/21】GitHub 今日热门项目
  • 【HTTP缓存机制深度解析:从ETag到实践策略】
  • C# Lambdab表达式 Var 类
  • 如何防止QQ浏览器录屏,盗录视频资源?
  • Apache Ignite Binary Object 调优
  • 【牛客算法】小美的排列询问
  • Linux 命令大全
  • Java基础教程(010):面向对象中的this和就近原则
  • 移星科技 modbus-tcp 转 modbus-Rtu模块
  • 安卓模拟器安装后,sdk版本详情简介及安卓sdk建议装哪几个版本
  • 突破量子仿真瓶颈:微算法科技MLGO量子算法的算术化与核操作迭代模型
  • 区块链之以太坊合约开发工具——Metamask钱包和Remix IDE
  • Android MTK平台预置多张静态壁纸
  • Freemarker生成Word文档下载到浏览器(下载word)
  • 上海GEO优化公司找哪家怎么做
  • uniapp底部导航栏凸起
  • windows电脑给iOS手机安装ipa包的方法
  • Kubernetes Pod调度基础
  • Leetcode力扣解题记录--第238题(前/后缀积)
  • 【Git#6】多人协作 企业级开发模型
  • 3D可视化模型轻量化陷阱:STL转GLTF的精度损失与压缩比平衡策略
  • 【系统全面】Linux内核原理——基础知识介绍
  • H3C路由器模拟PPPOE拨号
  • MTSC2025参会感悟:Multi-Agent RAG 应用质量保障建设