| |
|
| | import React, { useState, useRef, useCallback, useEffect } from 'react'; |
| |
|
| | interface AudioInputProps { |
| | onAudioSubmit: (blob: Blob, mimeType: string) => void; |
| | isProcessing: boolean; |
| | } |
| |
|
| | const MicrophoneIcon: React.FC<{className?: string}> = ({ className }) => ( |
| | <svg xmlns="http://www.w3.org/2000/svg" className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" /> |
| | </svg> |
| | ); |
| |
|
| | const UploadIcon: React.FC<{className?: string}> = ({ className }) => ( |
| | <svg xmlns="http://www.w3.org/2000/svg" className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" /> |
| | </svg> |
| | ); |
| |
|
| |
|
| | export const AudioInput: React.FC<AudioInputProps> = ({ onAudioSubmit, isProcessing }) => { |
| | const [isRecording, setIsRecording] = useState(false); |
| | const [audioURL, setAudioURL] = useState<string | null>(null); |
| | const [audioBlob, setAudioBlob] = useState<Blob | null>(null); |
| | const [mimeType, setMimeType] = useState<string>('audio/webm'); |
| | const mediaRecorderRef = useRef<MediaRecorder | null>(null); |
| | const audioChunksRef = useRef<Blob[]>([]); |
| | const [recordTime, setRecordTime] = useState(0); |
| | const timerIntervalRef = useRef<number | null>(null); |
| |
|
| | const formatTime = (seconds: number) => { |
| | const minutes = Math.floor(seconds / 60); |
| | const secs = seconds % 60; |
| | return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; |
| | }; |
| | |
| | useEffect(() => { |
| | return () => { |
| | if(timerIntervalRef.current) clearInterval(timerIntervalRef.current); |
| | } |
| | }, []); |
| |
|
| | const handleStartRecording = async () => { |
| | try { |
| | const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); |
| | setIsRecording(true); |
| | setAudioURL(null); |
| | setAudioBlob(null); |
| | |
| | let options = { mimeType: 'audio/webm' }; |
| | if (!MediaRecorder.isTypeSupported('audio/webm')) { |
| | options = { mimeType: 'audio/mp4' }; |
| | } |
| | setMimeType(options.mimeType); |
| |
|
| | mediaRecorderRef.current = new MediaRecorder(stream, options); |
| | mediaRecorderRef.current.ondataavailable = (event) => { |
| | audioChunksRef.current.push(event.data); |
| | }; |
| | mediaRecorderRef.current.onstop = () => { |
| | const blob = new Blob(audioChunksRef.current, { type: options.mimeType }); |
| | const url = URL.createObjectURL(blob); |
| | setAudioBlob(blob); |
| | setAudioURL(url); |
| | audioChunksRef.current = []; |
| | stream.getTracks().forEach(track => track.stop()); |
| | }; |
| | mediaRecorderRef.current.start(); |
| | setRecordTime(0); |
| | timerIntervalRef.current = window.setInterval(() => { |
| | setRecordTime(prev => prev + 1); |
| | }, 1000); |
| |
|
| | } catch (err) { |
| | console.error("Error accessing microphone:", err); |
| | alert("Could not access microphone. Please check your browser permissions."); |
| | } |
| | }; |
| |
|
| | const handleStopRecording = () => { |
| | if (mediaRecorderRef.current) { |
| | mediaRecorderRef.current.stop(); |
| | setIsRecording(false); |
| | if (timerIntervalRef.current) clearInterval(timerIntervalRef.current); |
| | } |
| | }; |
| | |
| | const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => { |
| | const file = event.target.files?.[0]; |
| | if (file && file.type.startsWith('audio/')) { |
| | const url = URL.createObjectURL(file); |
| | setAudioBlob(file); |
| | setMimeType(file.type); |
| | setAudioURL(url); |
| | } else { |
| | alert("Please upload a valid audio file."); |
| | } |
| | }; |
| | |
| | const handleSubmit = () => { |
| | if(audioBlob) { |
| | onAudioSubmit(audioBlob, mimeType); |
| | } |
| | }; |
| | |
| | const handleReset = () => { |
| | setAudioBlob(null); |
| | setAudioURL(null); |
| | setIsRecording(false); |
| | setRecordTime(0); |
| | if(mediaRecorderRef.current && mediaRecorderRef.current.state === 'recording') { |
| | mediaRecorderRef.current.stop(); |
| | } |
| | if (timerIntervalRef.current) clearInterval(timerIntervalRef.current); |
| | }; |
| |
|
| |
|
| | return ( |
| | <div className="p-6 md:p-8 space-y-6"> |
| | <div> |
| | <h2 className="text-2xl font-semibold text-center text-gray-100">Submit Your Voice Sample</h2> |
| | <p className="text-center text-gray-400 mt-2">Record up to 3 minutes of audio or upload a file.</p> |
| | </div> |
| | <div className="flex flex-col md:flex-row items-center justify-center gap-4"> |
| | {isRecording ? ( |
| | <button |
| | onClick={handleStopRecording} |
| | className="w-full md:w-auto flex-1 flex items-center justify-center gap-3 px-6 py-3 bg-red-600 text-white font-semibold rounded-lg hover:bg-red-700 transition-colors" |
| | > |
| | <span className="relative flex h-3 w-3"> |
| | <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-white opacity-75"></span> |
| | <span className="relative inline-flex rounded-full h-3 w-3 bg-white"></span> |
| | </span> |
| | Stop Recording ({formatTime(recordTime)}) |
| | </button> |
| | ) : ( |
| | <button |
| | onClick={handleStartRecording} |
| | disabled={!!audioBlob} |
| | className="w-full md:w-auto flex-1 flex items-center justify-center gap-3 px-6 py-3 bg-brand-blue text-white font-semibold rounded-lg hover:bg-opacity-80 transition-all disabled:bg-gray-600 disabled:cursor-not-allowed" |
| | > |
| | <MicrophoneIcon className="w-6 h-6"/> |
| | Record Voice |
| | </button> |
| | )} |
| | <span className="text-gray-500">OR</span> |
| | <label className={`w-full md:w-auto flex-1 flex items-center justify-center gap-3 px-6 py-3 bg-gray-700 text-white font-semibold rounded-lg transition-colors ${!!audioBlob ? 'cursor-not-allowed bg-gray-600' : 'cursor-pointer hover:bg-gray-600'}`}> |
| | <UploadIcon className="w-6 h-6"/> |
| | Upload File |
| | <input type="file" accept="audio/*" className="hidden" onChange={handleFileUpload} disabled={isRecording || !!audioBlob} /> |
| | </label> |
| | </div> |
| | |
| | {audioURL && ( |
| | <div className="mt-6 p-4 bg-gray-900/50 rounded-lg flex flex-col items-center gap-4"> |
| | <p className="font-semibold">Your Audio Sample:</p> |
| | <audio controls src={audioURL} className="w-full max-w-md"></audio> |
| | <div className="flex gap-4 mt-2"> |
| | <button |
| | onClick={handleSubmit} |
| | disabled={isProcessing} |
| | className="px-8 py-2 bg-gradient-to-r from-brand-blue to-brand-purple text-white font-bold rounded-lg hover:opacity-90 transition-opacity disabled:opacity-50 disabled:cursor-wait" |
| | > |
| | {isProcessing ? 'Processing...' : 'Generate EQ'} |
| | </button> |
| | <button |
| | onClick={handleReset} |
| | disabled={isProcessing} |
| | className="px-6 py-2 bg-gray-600 text-white font-semibold rounded-lg hover:bg-gray-500 transition-colors disabled:opacity-50" |
| | > |
| | Reset |
| | </button> |
| | </div> |
| | </div> |
| | )} |
| | </div> |
| | ); |
| | }; |
| |
|