import { AudioPlayer } from '@/components/ui/audio-player'; import { AudioWaveform } from '@/components/ui/audio-waveform'; import { Button } from '@/components/ui/button'; import { Text } from '@/components/ui/text'; import { useColor } from '@/hooks/useColor'; import { BORDER_RADIUS } from '@/theme/globals'; import { AudioModule, RecordingOptions, RecordingPresets, useAudioRecorder, } from 'expo-audio'; import { Circle, Download, Mic, Square, Trash2 } from 'lucide-react-native'; import React, { useEffect, useRef, useState } from 'react'; import { Alert, Platform, StyleSheet, View, ViewStyle } from 'react-native'; import Animated, { cancelAnimation, Easing, useAnimatedStyle, useSharedValue, withRepeat, withTiming, } from 'react-native-reanimated'; export interface AudioRecorderProps { style?: ViewStyle; quality?: 'high' | 'low'; showWaveform?: boolean; showTimer?: boolean; maxDuration?: number; // in seconds onRecordingComplete?: (uri: string) => void; onRecordingStart?: () => void; onRecordingStop?: () => void; customRecordingOptions?: RecordingOptions; } export function AudioRecorder({ style, quality = 'high', showWaveform = true, showTimer = true, maxDuration, onRecordingComplete, onRecordingStart, onRecordingStop, customRecordingOptions, }: AudioRecorderProps) { const recordingOptions = customRecordingOptions || (quality === 'high' ? RecordingPresets.HIGH_QUALITY : RecordingPresets.LOW_QUALITY); const recorder = useAudioRecorder(recordingOptions); const [permissionGranted, setPermissionGranted] = useState(false); const [duration, setDuration] = useState(0); const [recordingUri, setRecordingUri] = useState(null); const [isRecording, setIsRecording] = useState(false); // Waveform data for real-time visualization const [waveformData, setWaveformData] = useState( Array.from({ length: 30 }, () => 0.2) ); // Theme colors const primaryColor = useColor('primary'); const secondaryColor = useColor('secondary'); const textColor = useColor('text'); const mutedColor = useColor('textMuted'); const redColor = useColor('red'); const greenColor = useColor('green'); // Animation values using react-native-reanimated const recordingPulse = useSharedValue(1); const durationInterval = useRef(null); const meteringInterval = useRef(null); // Request permissions on mount useEffect(() => { (async () => { try { const status = await AudioModule.requestRecordingPermissionsAsync(); setPermissionGranted(status.granted); if (!status.granted) { Alert.alert( 'Permission Required', 'Please grant microphone permission to record audio.', [{ text: 'OK' }] ); } } catch (error) { console.error('Error requesting permissions:', error); setPermissionGranted(false); } })(); }, []); // Recording pulse animation using react-native-reanimated useEffect(() => { if (isRecording) { // Start the pulse animation recordingPulse.value = withRepeat( withTiming(1.2, { duration: 600, easing: Easing.inOut(Easing.ease) }), -1, // Infinite loop true // Reverse the animation (yoyo effect) ); } else { // Stop the animation and reset the scale cancelAnimation(recordingPulse); recordingPulse.value = withTiming(1, { duration: 300 }); } return () => { // Ensure animation is cancelled on unmount cancelAnimation(recordingPulse); }; }, [isRecording, recordingPulse]); // Create animated style for the record button const animatedRecordButtonStyle = useAnimatedStyle(() => { return { transform: [{ scale: recordingPulse.value }], }; }); // Real-time waveform updates during recording useEffect(() => { if (isRecording) { meteringInterval.current = setInterval(async () => { try { // Try to get metering data from recorder const status = recorder.getStatus(); let level = 0.3; // Default fallback level if (status && typeof status.metering === 'number') { // Convert dB to normalized value (typical range -160 to 0 dB) const dbLevel = status.metering; level = Math.max(0.1, Math.min(1.0, (dbLevel + 50) / 50)); } else { // Generate more realistic simulated audio levels const time = Date.now() / 1000; const baseLevel = 0.3 + Math.sin(time * 2) * 0.2; // Sine wave base const variation = (Math.random() - 0.5) * 0.4; // Random variation const spike = Math.random() < 0.1 ? Math.random() * 0.3 : 0; // Occasional spikes level = Math.max(0.1, Math.min(0.9, baseLevel + variation + spike)); } // Update waveform data by shifting array and adding new value setWaveformData((prevData) => { const newData = [...prevData.slice(1), level]; return newData; }); } catch (error) { console.log('Using simulated audio data'); // Fallback to realistic simulated data const time = Date.now() / 1000; const baseLevel = 0.4 + Math.sin(time * 3) * 0.2; const noise = (Math.random() - 0.5) * 0.3; const level = Math.max(0.15, Math.min(0.85, baseLevel + noise)); setWaveformData((prevData) => [...prevData.slice(1), level]); } }, 80); // Update every 80ms for smooth animation return () => { if (meteringInterval.current) { clearInterval(meteringInterval.current); meteringInterval.current = null; } }; } else { // Reset to quiet state when not recording setWaveformData(Array.from({ length: 30 }, () => 0.2)); if (meteringInterval.current) { clearInterval(meteringInterval.current); meteringInterval.current = null; } } }, [isRecording, recorder]); // Auto-stop recording when max duration is reached useEffect(() => { if (maxDuration && duration >= maxDuration && isRecording) { handleStopRecording(); } }, [duration, maxDuration, isRecording]); const startDurationTimer = () => { setDuration(0); durationInterval.current = setInterval(() => { setDuration((prev) => prev + 0.1); }, 100); }; const stopDurationTimer = () => { if (durationInterval.current) { clearInterval(durationInterval.current); durationInterval.current = null; } }; const handleStartRecording = async () => { if (!permissionGranted) { Alert.alert( 'Permission Required', 'Microphone permission is required to record audio.' ); return; } try { console.log('Starting recording...'); setRecordingUri(null); setIsRecording(true); startDurationTimer(); // Enable metering in recording options const meteringOptions = { ...recordingOptions, isMeteringEnabled: true, }; await recorder.prepareToRecordAsync(meteringOptions); await recorder.record(); onRecordingStart?.(); console.log('Recording started successfully'); } catch (error) { console.error('Error starting recording:', error); setIsRecording(false); stopDurationTimer(); Alert.alert('Error', 'Failed to start recording. Please try again.'); } }; const handleStopRecording = async () => { try { console.log('Stopping recording...'); setIsRecording(false); stopDurationTimer(); await recorder.stop(); const uri = recorder.uri; console.log('Recording stopped, URI:', uri); if (uri) { setRecordingUri(uri); onRecordingComplete?.(uri); } onRecordingStop?.(); } catch (error) { console.error('Error stopping recording:', error); Alert.alert('Error', 'Failed to stop recording. Please try again.'); } }; const handleDeleteRecording = () => { Alert.alert( 'Delete Recording', 'Are you sure you want to delete this recording?', [ { text: 'Cancel', style: 'cancel' }, { text: 'Delete', style: 'destructive', onPress: () => { setRecordingUri(null); setDuration(0); }, }, ] ); }; const handleSaveRecording = () => { if (recordingUri && onRecordingComplete) { onRecordingComplete(recordingUri); } }; const formatTime = (seconds: number) => { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); const centisecs = Math.floor((seconds % 1) * 100); return `${mins}:${secs.toString().padStart(2, '0')}.${centisecs .toString() .padStart(2, '0')}`; }; if (!permissionGranted) { return ( Microphone permission is required to record audio. ); } return ( {recordingUri && !isRecording ? ( ) : ( {/* Recording Status */} {isRecording ? ( Recording ) : ( )} {/* Waveform Visualization */} {showWaveform && ( )} {/* Timer */} {showTimer && ( {formatTime(duration)} {maxDuration && ( Max: {formatTime(maxDuration)} )} )} {/* Controls */} {!isRecording && !recordingUri && ( )} {isRecording && ( )} )} ); } const styles = StyleSheet.create({ container: { borderRadius: BORDER_RADIUS, padding: 20, alignItems: 'center', }, recordingStatus: { height: 36, }, recordingIndicator: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', }, waveformContainer: { alignItems: 'center', marginBottom: 16, }, timerContainer: { alignItems: 'center', marginBottom: 20, }, controlsContainer: { alignItems: 'center', marginBottom: 12, }, recordButton: { width: 80, height: 80, borderRadius: 40, }, stopButton: { width: 80, height: 80, borderRadius: 40, }, playbackControls: { flexDirection: 'row', alignItems: 'center', gap: 16, marginTop: 16, }, controlButton: { width: 48, height: 48, }, saveButton: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 24, }, });