diff --git a/src/components/ControlPanel.tsx b/src/components/ControlPanel.tsx index fe847c4..866ea07 100755 --- a/src/components/ControlPanel.tsx +++ b/src/components/ControlPanel.tsx @@ -17,6 +17,18 @@ interface ControlPanelProps { isPlaying: boolean; onPreview: () => void; canPreview: boolean; + playbackSpeed: number; + onPlaybackSpeedChange: (speed: number) => void; + isLooping: boolean; + onLoopingChange: (looping: boolean) => void; + exportResolution: string; + onExportResolutionChange: (resolution: string) => void; + exportFps: number; + onExportFpsChange: (fps: number) => void; + exportFormat: string; + onExportFormatChange: (format: string) => void; + exportQuality: string; + onExportQualityChange: (quality: string) => void; } export function ControlPanel({ @@ -31,6 +43,18 @@ export function ControlPanel({ isPlaying, onPreview, canPreview, + playbackSpeed, + onPlaybackSpeedChange, + isLooping, + onLoopingChange, + exportResolution, + onExportResolutionChange, + exportFps, + onExportFpsChange, + exportFormat, + onExportFormatChange, + exportQuality, + onExportQualityChange, }: ControlPanelProps) { return (
@@ -63,6 +87,48 @@ export function ControlPanel({
+ {/* Playback Controls */} +
+ + + {/* Playback Speed */} +
+ Speed: {playbackSpeed}x +
+ {[0.5, 1, 1.5, 2].map((speed) => ( + + ))} +
+
+ + {/* Looping Toggle */} +
+ Looping + +
+
+ {/* Preview Button */} {isMicActive && ( -
- Real-time input active +
+
+ Real-time input active +
+
)}
@@ -149,6 +237,18 @@ export function Oscilloscope() { isPlaying={isPlaying} onPreview={handlePreview} canPreview={canPreview} + playbackSpeed={playbackSpeed} + onPlaybackSpeedChange={handlePlaybackSpeedChange} + isLooping={isLooping} + onLoopingChange={handleLoopingChange} + exportResolution={exportResolution} + onExportResolutionChange={handleExportResolutionChange} + exportFps={exportFps} + onExportFpsChange={handleExportFpsChange} + exportFormat={exportFormat} + onExportFormatChange={handleExportFormatChange} + exportQuality={exportQuality} + onExportQualityChange={handleExportQualityChange} /> @@ -159,10 +259,66 @@ export function Oscilloscope() { micAnalyzer={micAnalyzer} mode={mode} isPlaying={isPlaying} + playbackSpeed={playbackSpeed} + isLooping={isLooping} + seekPosition={seekPosition} onPlaybackEnd={() => setIsPlaying(false)} + onSeek={handleSeek} /> + {/* Microphone Calibration */} + {showMicCalibration && isMicActive && ( +
+

Microphone Calibration

+ +
+ {/* Level Indicator */} +
+
+ Input Level + {Math.round(micLevel * 100)}% +
+
+
+
+
+ Quiet + 0.7 ? 'text-red-400' : micLevel > 0.5 ? 'text-yellow-400' : 'text-green-400'}> + {micLevel > 0.7 ? 'Too Loud' : micLevel > 0.5 ? 'Good' : 'Too Quiet'} + + Loud +
+
+ + {/* Gain Control */} +
+
+ Gain + {micGain.toFixed(1)}x +
+ setMicGain(Number(e.target.value))} + className="w-full" + /> +
+ +

+ Speak into your microphone and adjust gain until the level shows "Good" (green). + The bar should peak around 50-70% when speaking normally. +

+
+
+ )} + {/* Status Info */} {(isLoading || isExporting) && (
diff --git a/src/components/OscilloscopeDisplay.tsx b/src/components/OscilloscopeDisplay.tsx index cf81f2e..21caa2f 100755 --- a/src/components/OscilloscopeDisplay.tsx +++ b/src/components/OscilloscopeDisplay.tsx @@ -7,7 +7,11 @@ interface OscilloscopeDisplayProps { micAnalyzer: AnalyserNode | null; mode: OscilloscopeMode; isPlaying: boolean; + playbackSpeed: number; + isLooping: boolean; + seekPosition: number; onPlaybackEnd?: () => void; + onSeek?: (position: number) => void; } const WIDTH = 800; @@ -19,11 +23,16 @@ export function OscilloscopeDisplay({ micAnalyzer, mode, isPlaying, - onPlaybackEnd + playbackSpeed, + isLooping, + seekPosition, + onPlaybackEnd, + onSeek }: OscilloscopeDisplayProps) { const canvasRef = useRef(null); const animationRef = useRef(null); const currentSampleRef = useRef(0); + const lastSeekPositionRef = useRef(0); const drawGraticule = useCallback((ctx: CanvasRenderingContext2D) => { ctx.strokeStyle = '#00ff00'; @@ -52,6 +61,7 @@ export function OscilloscopeDisplay({ let samplesPerFrame: number; let startSample: number; let endSample: number; + let samplesToAdvance: number = samplesPerFrame; if (micAnalyzer) { // Real-time microphone data @@ -103,8 +113,23 @@ export function OscilloscopeDisplay({ } // File playback mode - samplesPerFrame = Math.floor(audioData.sampleRate / FPS); - startSample = currentSampleRef.current; + const baseSamplesPerFrame = Math.floor(audioData.sampleRate / FPS); + samplesPerFrame = Math.floor(baseSamplesPerFrame * playbackSpeed); + samplesToAdvance = samplesPerFrame; + + // Handle seeking + if (seekPosition > 0 && seekPosition !== lastSeekPositionRef.current) { + startSample = Math.floor(seekPosition * audioData.leftChannel.length); + currentSampleRef.current = startSample; + lastSeekPositionRef.current = seekPosition; + // Reset after one frame + setTimeout(() => { + lastSeekPositionRef.current = 0; + }, 1000 / FPS); + } else { + startSample = currentSampleRef.current; + } + endSample = Math.min(startSample + samplesPerFrame, audioData.leftChannel.length); // Clear to pure black @@ -240,12 +265,16 @@ export function OscilloscopeDisplay({ currentSampleRef.current = endSample; if (endSample >= audioData.leftChannel.length) { - onPlaybackEnd?.(); - return; + if (isLooping) { + currentSampleRef.current = 0; // Loop back to start + } else { + onPlaybackEnd?.(); + return; + } } animationRef.current = requestAnimationFrame(drawFrame); - }, [audioData, micAnalyzer, mode, drawGraticule, onPlaybackEnd, isPlaying]); + }, [audioData, micAnalyzer, mode, drawGraticule, onPlaybackEnd, isPlaying, playbackSpeed, isLooping, seekPosition]); // Initialize canvas useEffect(() => { @@ -293,7 +322,15 @@ export function OscilloscopeDisplay({ ref={canvasRef} width={WIDTH} height={HEIGHT} - className="w-full h-auto" + className="w-full h-auto cursor-pointer" + onClick={(e) => { + if (!audioData) return; + const rect = canvasRef.current?.getBoundingClientRect(); + if (!rect) return; + const x = e.clientX - rect.left; + const clickPosition = x / rect.width; + onSeek?.(Math.max(0, Math.min(1, clickPosition))); + }} /> {/* Mode indicator */} diff --git a/src/hooks/useVideoExporter.ts b/src/hooks/useVideoExporter.ts index 815d011..2cfab11 100755 --- a/src/hooks/useVideoExporter.ts +++ b/src/hooks/useVideoExporter.ts @@ -8,6 +8,8 @@ interface ExportOptions { fps: number; mode: OscilloscopeMode; audioFile: File; + format?: 'webm' | 'mp4'; + quality?: 'low' | 'medium' | 'high'; } // WebGL shaders