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:
@@ -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;
|
||||
@@ -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<HTMLCanvasElement>(null);
|
||||
const animationRef = useRef<number | null>(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(() => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -28,9 +28,12 @@ const commands: Record<string, string> = {
|
||||
'/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 = `
|
||||
|
||||
Reference in New Issue
Block a user