import { externalRefer } from '@/constants/external-links'; import { AudioOutlined } from '@ant-design/icons'; import { useIntl } from '@umijs/max'; import { Button, Space, Tooltip } from 'antd'; import dayjs from 'dayjs'; import lamejs from 'lamejs'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; interface AudioInputProps { onAudioData: (audioData: { chunks: any[]; url: string; name: string; type: string; duration: number; }) => void; onAnalyse?: (analyseData: any, frequencyBinCount: any) => void; onAudioPermission: (audioPermission: boolean) => void; onRecord?: (isRecording: boolean) => void; onStop?: () => void; voiceActivity?: boolean; type?: 'text' | 'primary' | 'default'; } const audioFormat: Record = { 'audio/mpeg': 'mp3', 'audio/mp4': 'mp4', 'audio/wav': 'wav', 'audio/ogg': 'ogg', 'audio/webm': 'webm', 'audio/aac': 'aac', 'audio/x-flac': 'flac', 'audio/pcm': 'pcm', 'audio/flac': 'flac', 'audio/x-wav': 'wav', 'audio/L16': 'pcm', 'audio/opus': 'opus' }; const recordingFormat = { type: 'audio/mpeg', suffix: '.mp3' }; const AudioInput: React.FC = (props) => { const intl = useIntl(); const [audioOn, setAudioOn] = useState(false); const [isRecording, setIsRecording] = useState(false); const [audioPermission, setAudioPermission] = useState(true); const audioStream = useRef(null); const audioRecorder = useRef(null); const startTime = useRef(0); const audioContext = useRef(null); const analyser = useRef(null); const dataArray = useRef(null); const audioUrl = useRef(''); const scriptProcessor = useRef(null); const mp3Encoder = useRef(null); const mp3Data = useRef(null); const mediaStreamSource = useRef(null); const sampleRate = 44100; // mp3 const initAudioContext = useCallback(() => { audioContext.current = new (window.AudioContext || window.webkitAudioContext)(); analyser.current = audioContext.current.createAnalyser(); analyser.current.fftSize = 512; dataArray.current = new Uint8Array(analyser.current.frequencyBinCount); }, []); const generateVisualData = useCallback(() => { mediaStreamSource.current = audioContext.current.createMediaStreamSource( audioStream.current ); mediaStreamSource.current.connect(analyser.current); }, []); const initMp3Converter = useCallback(() => { scriptProcessor.current = audioContext.current.createScriptProcessor( 4096, 1, 1 ); mp3Encoder.current = new lamejs.Mp3Encoder(1, sampleRate, 128); mp3Data.current = []; scriptProcessor.current.onaudioprocess = (event: any) => { const pcmData = event.inputBuffer.getChannelData(0); const samples = new Int16Array(pcmData.length); for (let i = 0; i < pcmData.length; i++) { samples[i] = Math.max(-1, Math.min(1, pcmData[i])) * 32767; } const mp3Chunk = mp3Encoder.current.encodeBuffer(samples); if (mp3Chunk.length > 0) { mp3Data.current.push(mp3Chunk); } }; mediaStreamSource.current.connect(scriptProcessor.current); scriptProcessor.current.connect(audioContext.current.destination); }, []); // stop all audio tracks const stopAudioTracks = () => { audioStream.current?.getTracks().forEach((track: any) => { track.stop(); }); scriptProcessor.current?.disconnect(); mediaStreamSource.current?.disconnect(); audioContext.current?.close(); }; const handleStopRecording = () => { setIsRecording(false); audioRecorder.current?.stop(); stopAudioTracks(); }; const getAudioFormat = (type?: string) => { const mimeType = type || audioRecorder.current?.mimeType; let resultFormat = recordingFormat; Object.keys(audioFormat).forEach((key: string) => { if (mimeType.includes(key)) { resultFormat = { type: mimeType, suffix: `.${audioFormat[key]}` }; } }); return resultFormat; }; const generateMp3Data = useCallback(() => { const finalChunk = mp3Encoder.current.flush(); if (finalChunk.length > 0) { mp3Data.current.push(finalChunk); } const mp3Blob = new Blob(mp3Data.current, { type: 'audio/mpeg' }); return mp3Blob; }, []); // get all audio tracks const getAudioTracks = () => { const audioTracks = audioStream.current.getAudioTracks(); audioTracks.forEach((track: any) => { track.onended = () => { setAudioPermission(false); }; }); }; // check if microphone is on const isMicrophoneOn = () => { return ( audioStream.current && audioStream.current .getTracks() .some((track: any) => track.readyState === 'live') ); }; const microphonePermissionDenied = async () => { const permissionStatus = await navigator.permissions.query({ name: 'microphone' as any }); return permissionStatus.state === 'denied'; }; const checkMicrophonePermission = async () => { try { const permissionStatus = await navigator.permissions.query({ name: 'microphone' as any }); if (permissionStatus.state === 'granted') { setAudioPermission(true); props.onAudioPermission(true); } else if (permissionStatus.state === 'denied') { setAudioPermission(false); props.onAudioPermission(false); handleStopRecording(); } permissionStatus.onchange = () => { console.log('permission changed'); checkMicrophonePermission(); }; } catch (error) { // todo } }; // open audio const EnableAudio = async () => { try { audioStream.current = await navigator.mediaDevices?.getUserMedia({ audio: true }); getAudioTracks(); setAudioOn(true); setAudioPermission(true); initAudioContext(); generateVisualData(); initMp3Converter(); } catch (error) { console.log('enable+++++++++', error); } }; // close audio const disableAudio = () => { stopAudioTracks(); setAudioOn(false); handleStopRecording(); audioStream.current = null; }; const stopRecording = () => { audioRecorder.current?.stop(); setIsRecording(false); props.onRecord?.(false); }; const handleAudioData = (audioData: any) => { props.onAudioData?.(audioData); }; const generateFileNameByTime = (type?: string) => { const format = getAudioFormat(type); return `recording-${dayjs().format('YYYY-MM-DD-HH_mm_ss')}${format.suffix}`; }; // start recording const StartRecording = async () => { if (isRecording) { stopRecording(); return; } try { await EnableAudio(); URL.revokeObjectURL(audioUrl.current); audioRecorder.current = new MediaRecorder(audioStream.current); audioRecorder.current.ondataavailable = (event: any) => { if (props.voiceActivity) { analyser.current?.getByteFrequencyData(dataArray.current); props.onAnalyse?.(dataArray.current, analyser); } }; // stop recording audioRecorder.current.onstop = async () => { handleStopRecording(); props.onAnalyse?.([], null); const mp3Blob = generateMp3Data(); audioUrl.current = mp3Blob.size ? URL.createObjectURL(mp3Blob) : ''; handleAudioData({ chunks: mp3Blob, size: mp3Blob.size, type: mp3Blob.type, url: audioUrl.current, name: generateFileNameByTime(mp3Blob.type), duration: Math.floor((Date.now() - startTime.current) / 1000) }); }; setIsRecording(true); props.onRecord?.(true); startTime.current = Date.now(); audioRecorder.current.start(100); } catch (error) { console.log('error====', error); } }; const renderRecordButtonTips = useMemo(() => { if (!audioPermission) { return ( {intl.formatMessage({ id: 'playground.audio.enablemic' })} ); } return isRecording ? intl.formatMessage({ id: 'playground.audio.stoprecord' }) : intl.formatMessage({ id: 'playground.audio.startrecord' }); }, [audioPermission, isRecording, intl]); const noAudioPermissionButtonStyle = useMemo(() => { if (!audioPermission) { return { backgroundColor: 'var(--ant-color-error-bg-filled-hover)', color: 'var(--ant-color-error-border-hover)', border: 'none' }; } return {}; }, [audioPermission]); useEffect(() => { return () => { handleStopRecording(); stopAudioTracks(); URL.revokeObjectURL(audioUrl.current); }; }, []); useEffect(() => { checkMicrophonePermission(); }, []); return (
{
}
); }; export default React.memo(AudioInput);