mirror of
https://github.com/JorySeverijnse/ui-fixer-supreme.git
synced 2026-01-29 19:58:38 +00:00
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:
parent
ad6587978a
commit
1495a9a5c5
175
src/components/Oscilloscope.tsx
Normal file
175
src/components/Oscilloscope.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -4,6 +4,7 @@ import type { OscilloscopeMode } from '@/hooks/useOscilloscopeRenderer';
|
||||
|
||||
interface OscilloscopeDisplayProps {
|
||||
audioData: AudioData | null;
|
||||
micAnalyzer: AnalyserNode | null;
|
||||
mode: OscilloscopeMode;
|
||||
isPlaying: boolean;
|
||||
onPlaybackEnd?: () => void;
|
||||
@ -15,6 +16,7 @@ const FPS = 60;
|
||||
|
||||
export function OscilloscopeDisplay({
|
||||
audioData,
|
||||
micAnalyzer,
|
||||
mode,
|
||||
isPlaying,
|
||||
onPlaybackEnd
|
||||
@ -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(() => {
|
||||
|
||||
15
src/components/OscilloscopeScreen.tsx
Normal file
15
src/components/OscilloscopeScreen.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -30,6 +30,9 @@ const commands: Record<string, string> = {
|
||||
'/a': '/achievements',
|
||||
'/credits': '/credits',
|
||||
'/cred': '/credits',
|
||||
'/oscilloscope': '/oscilloscope',
|
||||
'/oscope': '/oscilloscope',
|
||||
'/o': '/oscilloscope',
|
||||
};
|
||||
|
||||
const helpText = `Available commands:
|
||||
@ -47,6 +50,7 @@ const helpText = `Available commands:
|
||||
/music, /m - Navigate to Music Player
|
||||
/ai, /chat - Navigate to AI Chat
|
||||
/achievements /a - View achievements
|
||||
/oscilloscope /o - Audio oscilloscope
|
||||
/credits /cred - View credits
|
||||
/help, /h - Show this help message
|
||||
/clear, /c - Clear terminal output`;
|
||||
|
||||
@ -12,12 +12,14 @@ export function useAudioAnalyzer() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = 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 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,
|
||||
};
|
||||
|
||||
@ -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 (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
@ -82,18 +9,6 @@ const OscilloscopePage = () => {
|
||||
transition={{ duration: 0.5 }}
|
||||
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 />
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user