当前位置: 首页 > 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.lryc.cn/news/594862.html

相关文章:

  • 《Linux 环境下 Nginx 多站点综合实践:域名解析、访问控制与 HTTPS 加密部署》​
  • 大模型——Prompt 优化还是模型微调
  • Ubuntu 22.04 安装 Docker (安装包形式)
  • WPF 项目设置应用程序图标和设置程序集图标
  • 移星科技 modbus-tcp 转 modbus-Rtu模块
  • (数据结构)线性表(中):SLIst单链表
  • tcpdump 命令解析(随手记)
  • IOPaint+CPolar:零公网IP也能搭建专属AI图像编辑平台
  • 高级技术【Java】【反射】【注解】【动态代理】
  • 复习博客:JVM
  • 【Project】ELK 7.17.16 日志分析系统部署
  • 阿里云平台使用的ack创建的pod与服务器中的MongoDB不在同一网段如何解决
  • 【图像处理基石】什么是相机的内外参数?
  • 单表查询-分页提前获取数据
  • 自动化与安全 - 将 Terraform 集成到 CI/CD
  • 安装pytorch(cpu版)
  • 电科金仓2025发布会,国产数据库的AI融合进化与智领未来
  • 【Lucene】SimScorer
  • 【Spring AI】Advisors API—顾问(即拦截器)
  • 轨迹优化 | 基于边界中间值问题(BIVP)的路径平滑求解器(附C++/Python仿真)
  • 6.String、StringBuffer、StringBuilder区别及使用场景
  • C++学习笔记(六:数组)
  • AI Agent与MCP Service技术进展结构化分析报告(2025Q2)
  • 解决win10下Vmware虚拟机在笔记本睡眠唤醒后ssh连接不上的问题
  • 项目研发进度安排
  • 音视频学习(四十二):H264帧间压缩技术
  • 【时时三省】(C语言基础)使用字符指针变量和字符数组的比较
  • Electron使用WebAssembly实现CRC-16 原理校验
  • Java 二叉树
  • C++11之右值引用与移动语义(提高效率)重要