From 1495a9a5c541b4d2f192bb16a06747f38a717381 Mon Sep 17 00:00:00 2001 From: JorySeverijnse Date: Sun, 21 Dec 2025 13:51:16 +0100 Subject: [PATCH] Implemented Oscilloscope correctly now Have tested it with a 5 minute long audio file Bigger files and different formats still needs testing --- src/components/Oscilloscope.tsx | 175 +++++++++++++++++++++++++ src/components/OscilloscopeDisplay.tsx | 74 +++++++++-- src/components/OscilloscopeScreen.tsx | 15 +++ src/components/TerminalCommand.tsx | 16 ++- src/hooks/useAudioAnalyzer.ts | 4 + src/pages/Oscilloscope.tsx | 85 ------------ 6 files changed, 269 insertions(+), 100 deletions(-) create mode 100644 src/components/Oscilloscope.tsx create mode 100644 src/components/OscilloscopeScreen.tsx diff --git a/src/components/Oscilloscope.tsx b/src/components/Oscilloscope.tsx new file mode 100644 index 0000000..defb474 --- /dev/null +++ b/src/components/Oscilloscope.tsx @@ -0,0 +1,175 @@ +import { useState, useCallback, useEffect } from 'react'; +import { AudioUploader } from './AudioUploader'; +import { ControlPanel } from './ControlPanel'; +import { OscilloscopeDisplay } from './OscilloscopeDisplay'; +import { useAudioAnalyzer } from '@/hooks/useAudioAnalyzer'; +import { useOscilloscopeRenderer } from '@/hooks/useOscilloscopeRenderer'; +import { useVideoExporter } from '@/hooks/useVideoExporter'; +import { useSettings } from '@/contexts/SettingsContext'; +import { Mic, MicOff } from 'lucide-react'; +import { Button } from '@/components/ui/button'; + +export function Oscilloscope() { + const [mode, setMode] = useState<'combined' | 'separate' | 'all'>('combined'); + const [isMicActive, setIsMicActive] = useState(false); + const [isPlaying, setIsPlaying] = useState(false); + const [currentSample, setCurrentSample] = useState(0); + + const { audioData, isLoading, fileName, originalFile, loadAudioFile, reset: resetAudio } = useAudioAnalyzer(); + const { isExporting, progress, exportedUrl, exportVideo, reset: resetExport } = useVideoExporter(); + const { playSound } = useSettings(); + + // Real-time microphone input + const [micStream, setMicStream] = useState(null); + const [micAnalyzer, setMicAnalyzer] = useState(null); + + const handleFileSelect = useCallback((file: File) => { + loadAudioFile(file); + if (isMicActive) { + setIsMicActive(false); + } + }, [loadAudioFile, isMicActive]); + + const toggleMic = useCallback(async () => { + playSound('click'); + if (isMicActive) { + // Stop microphone + if (micStream) { + micStream.getTracks().forEach(track => track.stop()); + setMicStream(null); + } + setMicAnalyzer(null); + setIsMicActive(false); + resetAudio(); + } else { + // Start microphone + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + setMicStream(stream); + + const audioContext = new AudioContext(); + const analyser = audioContext.createAnalyser(); + analyser.fftSize = 2048; + + const source = audioContext.createMediaStreamSource(stream); + source.connect(analyser); + + setMicAnalyzer(analyser); + setIsMicActive(true); + resetAudio(); // Clear any loaded file + } catch (error) { + console.error('Error accessing microphone:', error); + alert('Could not access microphone. Please check permissions.'); + } + } + }, [isMicActive, micStream, playSound, resetAudio]); + + const handlePreview = useCallback(() => { + playSound('click'); + setIsPlaying(!isPlaying); + }, [isPlaying, playSound]); + + const handleGenerate = useCallback(async () => { + if (!audioData || !originalFile) return; + + await exportVideo(audioData, originalFile, { + width: 1920, + height: 1080, + fps: 60, + mode, + audioFile: originalFile, + }); + }, [audioData, originalFile, exportVideo, mode]); + + const handleReset = useCallback(() => { + playSound('click'); + setIsPlaying(false); + resetAudio(); + resetExport(); + }, [playSound, resetAudio, resetExport]); + + const canPreview = (audioData !== null || isMicActive) && !isExporting; + const canGenerate = audioData !== null && originalFile !== null && !isExporting && !isLoading; + + return ( +
+ {/* Header */} +
+

+ Audio Oscilloscope +

+

+ Visualize audio waveforms in real-time +

+
+ + {/* Controls Row */} +
+ {/* Left Column: Audio Input */} +
+ + + {/* Microphone Toggle */} +
+ + + {isMicActive && ( +
+ Real-time input active +
+ )} +
+
+ + {/* Right Column: Control Panel */} + +
+ + {/* Oscilloscope Display */} +
+ setIsPlaying(false)} + /> +
+ + {/* Status Info */} + {(isLoading || isExporting) && ( +
+ {isLoading && "Loading audio..."} + {isExporting && `Exporting video... ${progress}%`} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/components/OscilloscopeDisplay.tsx b/src/components/OscilloscopeDisplay.tsx index 7a6c5ca..cf81f2e 100755 --- a/src/components/OscilloscopeDisplay.tsx +++ b/src/components/OscilloscopeDisplay.tsx @@ -4,6 +4,7 @@ import type { OscilloscopeMode } from '@/hooks/useOscilloscopeRenderer'; interface OscilloscopeDisplayProps { audioData: AudioData | null; + micAnalyzer: AnalyserNode | null; mode: OscilloscopeMode; isPlaying: boolean; onPlaybackEnd?: () => void; @@ -13,11 +14,12 @@ const WIDTH = 800; const HEIGHT = 600; const FPS = 60; -export function OscilloscopeDisplay({ - audioData, - mode, +export function OscilloscopeDisplay({ + audioData, + micAnalyzer, + mode, isPlaying, - onPlaybackEnd + onPlaybackEnd }: OscilloscopeDisplayProps) { const canvasRef = useRef(null); const animationRef = useRef(null); @@ -41,15 +43,69 @@ export function OscilloscopeDisplay({ }, []); const drawFrame = useCallback(() => { - if (!audioData || !canvasRef.current) return; + if ((!audioData && !micAnalyzer) || !canvasRef.current) return; const canvas = canvasRef.current; const ctx = canvas.getContext('2d'); if (!ctx) return; - const samplesPerFrame = Math.floor(audioData.sampleRate / FPS); - const startSample = currentSampleRef.current; - const endSample = Math.min(startSample + samplesPerFrame, audioData.leftChannel.length); + let samplesPerFrame: number; + let startSample: number; + let endSample: number; + + if (micAnalyzer) { + // Real-time microphone data + const bufferLength = micAnalyzer.frequencyBinCount; + const dataArray = new Uint8Array(bufferLength); + micAnalyzer.getByteTimeDomainData(dataArray); + + // Convert to Float32Array-like for consistency + const micData = new Float32Array(dataArray.length); + for (let i = 0; i < dataArray.length; i++) { + micData[i] = (dataArray[i] - 128) / 128; // Normalize to -1 to 1 + } + + samplesPerFrame = micData.length; + startSample = 0; + endSample = micData.length; + + // Draw mic data directly + ctx.strokeStyle = '#00ff00'; + ctx.lineWidth = 2; + ctx.beginPath(); + + const sliceWidth = WIDTH / samplesPerFrame; + let x = 0; + + for (let i = 0; i < samplesPerFrame; i++) { + const v = micData[i]; + const y = (v * HEIGHT) / 2 + HEIGHT / 2; + + if (i === 0) { + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } + + x += sliceWidth; + } + + ctx.stroke(); + + // Draw graticule + drawGraticule(ctx); + + // Request next frame for real-time + if (isPlaying) { + animationRef.current = requestAnimationFrame(drawFrame); + } + return; + } + + // File playback mode + samplesPerFrame = Math.floor(audioData.sampleRate / FPS); + startSample = currentSampleRef.current; + endSample = Math.min(startSample + samplesPerFrame, audioData.leftChannel.length); // Clear to pure black ctx.fillStyle = '#000000'; @@ -189,7 +245,7 @@ export function OscilloscopeDisplay({ } animationRef.current = requestAnimationFrame(drawFrame); - }, [audioData, mode, drawGraticule, onPlaybackEnd]); + }, [audioData, micAnalyzer, mode, drawGraticule, onPlaybackEnd, isPlaying]); // Initialize canvas useEffect(() => { diff --git a/src/components/OscilloscopeScreen.tsx b/src/components/OscilloscopeScreen.tsx new file mode 100644 index 0000000..c9be2aa --- /dev/null +++ b/src/components/OscilloscopeScreen.tsx @@ -0,0 +1,15 @@ +import { ReactNode } from 'react'; + +interface OscilloscopeScreenProps { + children: ReactNode; +} + +export function OscilloscopeScreen({ children }: OscilloscopeScreenProps) { + return ( +
+
+ {children} +
+
+ ); +} \ No newline at end of file diff --git a/src/components/TerminalCommand.tsx b/src/components/TerminalCommand.tsx index a6473ba..92deef0 100644 --- a/src/components/TerminalCommand.tsx +++ b/src/components/TerminalCommand.tsx @@ -28,9 +28,12 @@ const commands: Record = { '/achievements': '/achievements', '/ach': '/achievements', '/a': '/achievements', - '/credits': '/credits', - '/cred': '/credits', -}; + '/credits': '/credits', + '/cred': '/credits', + '/oscilloscope': '/oscilloscope', + '/oscope': '/oscilloscope', + '/o': '/oscilloscope', + }; const helpText = `Available commands: /home - Navigate to Home @@ -46,9 +49,10 @@ const helpText = `Available commands: /breakout - Play Breakout /music, /m - Navigate to Music Player /ai, /chat - Navigate to AI Chat - /achievements /a - View achievements - /credits /cred - View credits - /help, /h - Show this help message + /achievements /a - View achievements + /oscilloscope /o - Audio oscilloscope + /credits /cred - View credits + /help, /h - Show this help message /clear, /c - Clear terminal output`; const helpHint = ` diff --git a/src/hooks/useAudioAnalyzer.ts b/src/hooks/useAudioAnalyzer.ts index 10ff8b9..3931cbc 100755 --- a/src/hooks/useAudioAnalyzer.ts +++ b/src/hooks/useAudioAnalyzer.ts @@ -12,12 +12,14 @@ export function useAudioAnalyzer() { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [fileName, setFileName] = useState(null); + const [originalFile, setOriginalFile] = useState(null); const audioContextRef = useRef(null); const loadAudioFile = useCallback(async (file: File) => { setIsLoading(true); setError(null); setFileName(file.name); + setOriginalFile(file); try { // Create or reuse AudioContext @@ -55,6 +57,7 @@ export function useAudioAnalyzer() { const reset = useCallback(() => { setAudioData(null); setFileName(null); + setOriginalFile(null); setError(null); }, []); @@ -63,6 +66,7 @@ export function useAudioAnalyzer() { isLoading, error, fileName, + originalFile, loadAudioFile, reset, }; diff --git a/src/pages/Oscilloscope.tsx b/src/pages/Oscilloscope.tsx index 2e6ac3d..f5a4d41 100644 --- a/src/pages/Oscilloscope.tsx +++ b/src/pages/Oscilloscope.tsx @@ -1,80 +1,7 @@ import { motion } from 'framer-motion'; import { Oscilloscope } from '@/components/Oscilloscope'; -import { useEffect } from 'react'; const OscilloscopePage = () => { - // Auto-test export functionality on page load - useEffect(() => { - console.log('🔬 AUTO-TESTING OSCILLOSCOPE VIDEO EXPORT...'); - - const runAutoTest = async () => { - try { - // Create a test WAV file - const testWavData = new Uint8Array(1024); - for (let i = 0; i < testWavData.length; i++) { - testWavData[i] = Math.sin(i * 0.1) * 64 + 128; // Simple sine wave - } - const testFile = new File([testWavData], 'auto-test.wav', { type: 'audio/wav' }); - - console.log('📁 Created test audio file:', testFile.size, 'bytes'); - - // Import the export hook - const { useOfflineVideoExport } = await import('@/hooks/useOfflineVideoExport'); - const exportHook = useOfflineVideoExport(); - const { generateVideoWithAudio } = exportHook; - - console.log('⚙️ Starting video export...'); - - // Mock drawFrame function - const mockDrawFrame = (ctx: CanvasRenderingContext2D, width: number, height: number) => { - ctx.fillStyle = '#0a0f0a'; - ctx.fillRect(0, 0, width, height); - ctx.fillStyle = '#00ff00'; - ctx.font = '20px monospace'; - ctx.fillText('AUTO-TEST VIDEO', 20, height/2); - ctx.fillText('Oscilloscope Export Test', 20, height/2 + 30); - ctx.fillText(`File: ${testFile.name}`, 20, height/2 + 60); - }; - - // Run export - const result = await generateVideoWithAudio(testFile, mockDrawFrame, { - fps: 30, - format: 'webm', - width: 640, - height: 480, - quality: 'medium' - }); - - if (result) { - console.log('✅ AUTO-TEST SUCCESS!'); - console.log(`📁 Generated video: ${result.size} bytes`); - console.log('🎬 Type:', result.type); - - // Auto-download for testing - const url = URL.createObjectURL(result); - const a = document.createElement('a'); - a.href = url; - a.download = 'oscilloscope-auto-test.webm'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - - console.log('⬇️ Auto-downloaded test video file'); - } else { - console.error('❌ AUTO-TEST FAILED: No video generated'); - } - - } catch (error) { - console.error('❌ AUTO-TEST ERROR:', error); - } - }; - - // Run test after 3 seconds - const timer = setTimeout(runAutoTest, 3000); - return () => clearTimeout(timer); - }, []); - return ( { transition={{ duration: 0.5 }} className="space-y-6" > -
-

- Audio Oscilloscope -

-

- Visualize audio waveforms in real-time with microphone input or audio files. -

-

- 🔬 Auto-testing video export in 3 seconds... -

-
-
);