Implemented Oscilloscope correctly now

Have tested it with a 5 minute long audio file
Bigger files and different formats still needs testing
This commit is contained in:
JorySeverijnse 2025-12-21 13:51:16 +01:00
parent ad6587978a
commit 1495a9a5c5
6 changed files with 269 additions and 100 deletions

View File

@ -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<MediaStream | null>(null);
const [micAnalyzer, setMicAnalyzer] = useState<AnalyserNode | null>(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 (
<div className="space-y-6 max-w-6xl mx-auto">
{/* Header */}
<div className="text-center space-y-4">
<h2 className="font-minecraft text-2xl md:text-3xl text-primary text-glow-strong">
Audio Oscilloscope
</h2>
<p className="font-pixel text-foreground/80">
Visualize audio waveforms in real-time
</p>
</div>
{/* Controls Row */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Left Column: Audio Input */}
<div className="space-y-4">
<AudioUploader
onFileSelect={handleFileSelect}
isLoading={isLoading}
fileName={fileName}
/>
{/* Microphone Toggle */}
<div className="flex items-center gap-4">
<Button
onClick={toggleMic}
variant={isMicActive ? "default" : "outline"}
className={`flex items-center gap-2 font-crt ${
isMicActive
? 'bg-primary text-primary-foreground'
: 'border-primary/50 hover:bg-primary/10'
}`}
>
{isMicActive ? <MicOff size={16} /> : <Mic size={16} />}
{isMicActive ? 'STOP MIC' : 'USE MICROPHONE'}
</Button>
{isMicActive && (
<div className="text-sm text-muted-foreground font-mono-crt">
Real-time input active
</div>
)}
</div>
</div>
{/* Right Column: Control Panel */}
<ControlPanel
mode={mode}
onModeChange={setMode}
canGenerate={canGenerate}
isGenerating={isExporting}
progress={progress}
exportedUrl={exportedUrl}
onGenerate={handleGenerate}
onReset={handleReset}
isPlaying={isPlaying}
onPreview={handlePreview}
canPreview={canPreview}
/>
</div>
{/* Oscilloscope Display */}
<div className="flex justify-center">
<OscilloscopeDisplay
audioData={audioData}
micAnalyzer={micAnalyzer}
mode={mode}
isPlaying={isPlaying}
onPlaybackEnd={() => setIsPlaying(false)}
/>
</div>
{/* Status Info */}
{(isLoading || isExporting) && (
<div className="text-center text-muted-foreground font-mono-crt text-sm">
{isLoading && "Loading audio..."}
{isExporting && `Exporting video... ${progress}%`}
</div>
)}
</div>
);
}

View File

@ -4,6 +4,7 @@ import type { OscilloscopeMode } from '@/hooks/useOscilloscopeRenderer';
interface OscilloscopeDisplayProps { interface OscilloscopeDisplayProps {
audioData: AudioData | null; audioData: AudioData | null;
micAnalyzer: AnalyserNode | null;
mode: OscilloscopeMode; mode: OscilloscopeMode;
isPlaying: boolean; isPlaying: boolean;
onPlaybackEnd?: () => void; onPlaybackEnd?: () => void;
@ -13,11 +14,12 @@ const WIDTH = 800;
const HEIGHT = 600; const HEIGHT = 600;
const FPS = 60; const FPS = 60;
export function OscilloscopeDisplay({ export function OscilloscopeDisplay({
audioData, audioData,
mode, micAnalyzer,
mode,
isPlaying, isPlaying,
onPlaybackEnd onPlaybackEnd
}: OscilloscopeDisplayProps) { }: OscilloscopeDisplayProps) {
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const animationRef = useRef<number | null>(null); const animationRef = useRef<number | null>(null);
@ -41,15 +43,69 @@ export function OscilloscopeDisplay({
}, []); }, []);
const drawFrame = useCallback(() => { const drawFrame = useCallback(() => {
if (!audioData || !canvasRef.current) return; if ((!audioData && !micAnalyzer) || !canvasRef.current) return;
const canvas = canvasRef.current; const canvas = canvasRef.current;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
if (!ctx) return; if (!ctx) return;
const samplesPerFrame = Math.floor(audioData.sampleRate / FPS); let samplesPerFrame: number;
const startSample = currentSampleRef.current; let startSample: number;
const endSample = Math.min(startSample + samplesPerFrame, audioData.leftChannel.length); 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 // Clear to pure black
ctx.fillStyle = '#000000'; ctx.fillStyle = '#000000';
@ -189,7 +245,7 @@ export function OscilloscopeDisplay({
} }
animationRef.current = requestAnimationFrame(drawFrame); animationRef.current = requestAnimationFrame(drawFrame);
}, [audioData, mode, drawGraticule, onPlaybackEnd]); }, [audioData, micAnalyzer, mode, drawGraticule, onPlaybackEnd, isPlaying]);
// Initialize canvas // Initialize canvas
useEffect(() => { useEffect(() => {

View File

@ -0,0 +1,15 @@
import { ReactNode } from 'react';
interface OscilloscopeScreenProps {
children: ReactNode;
}
export function OscilloscopeScreen({ children }: OscilloscopeScreenProps) {
return (
<div className="crt-bezel">
<div className="screen-curve relative bg-black border border-primary/20">
{children}
</div>
</div>
);
}

View File

@ -28,9 +28,12 @@ const commands: Record<string, string> = {
'/achievements': '/achievements', '/achievements': '/achievements',
'/ach': '/achievements', '/ach': '/achievements',
'/a': '/achievements', '/a': '/achievements',
'/credits': '/credits', '/credits': '/credits',
'/cred': '/credits', '/cred': '/credits',
}; '/oscilloscope': '/oscilloscope',
'/oscope': '/oscilloscope',
'/o': '/oscilloscope',
};
const helpText = `Available commands: const helpText = `Available commands:
/home - Navigate to Home /home - Navigate to Home
@ -46,9 +49,10 @@ const helpText = `Available commands:
/breakout - Play Breakout /breakout - Play Breakout
/music, /m - Navigate to Music Player /music, /m - Navigate to Music Player
/ai, /chat - Navigate to AI Chat /ai, /chat - Navigate to AI Chat
/achievements /a - View achievements /achievements /a - View achievements
/credits /cred - View credits /oscilloscope /o - Audio oscilloscope
/help, /h - Show this help message /credits /cred - View credits
/help, /h - Show this help message
/clear, /c - Clear terminal output`; /clear, /c - Clear terminal output`;
const helpHint = ` const helpHint = `

View File

@ -12,12 +12,14 @@ export function useAudioAnalyzer() {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [fileName, setFileName] = useState<string | null>(null); const [fileName, setFileName] = useState<string | null>(null);
const [originalFile, setOriginalFile] = useState<File | null>(null);
const audioContextRef = useRef<AudioContext | null>(null); const audioContextRef = useRef<AudioContext | null>(null);
const loadAudioFile = useCallback(async (file: File) => { const loadAudioFile = useCallback(async (file: File) => {
setIsLoading(true); setIsLoading(true);
setError(null); setError(null);
setFileName(file.name); setFileName(file.name);
setOriginalFile(file);
try { try {
// Create or reuse AudioContext // Create or reuse AudioContext
@ -55,6 +57,7 @@ export function useAudioAnalyzer() {
const reset = useCallback(() => { const reset = useCallback(() => {
setAudioData(null); setAudioData(null);
setFileName(null); setFileName(null);
setOriginalFile(null);
setError(null); setError(null);
}, []); }, []);
@ -63,6 +66,7 @@ export function useAudioAnalyzer() {
isLoading, isLoading,
error, error,
fileName, fileName,
originalFile,
loadAudioFile, loadAudioFile,
reset, reset,
}; };

View File

@ -1,80 +1,7 @@
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Oscilloscope } from '@/components/Oscilloscope'; import { Oscilloscope } from '@/components/Oscilloscope';
import { useEffect } from 'react';
const OscilloscopePage = () => { 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 ( return (
<motion.div <motion.div
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
@ -82,18 +9,6 @@ const OscilloscopePage = () => {
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
className="space-y-6" className="space-y-6"
> >
<div className="text-center space-y-2">
<h1 className="font-minecraft text-3xl md:text-4xl text-primary text-glow-strong">
Audio Oscilloscope
</h1>
<p className="font-pixel text-foreground/80">
Visualize audio waveforms in real-time with microphone input or audio files.
</p>
<p className="text-sm text-muted-foreground mt-2">
🔬 Auto-testing video export in 3 seconds...
</p>
</div>
<Oscilloscope /> <Oscilloscope />
</motion.div> </motion.div>
); );