From 614811167f33534de14ff0b7ad1f671a81d2045b Mon Sep 17 00:00:00 2001 From: JorySeverijnse Date: Sun, 21 Dec 2025 14:29:17 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=85=20Audio=20Playback=20Controls=20-=20P?= =?UTF-8?q?layback=20Speed=20Control:=200.5x,=201x,=201.5x,=20and=202x=20s?= =?UTF-8?q?peed=20options=20-=20Looping=20Toggle:=20Enable/disable=20autom?= =?UTF-8?q?atic=20looping=20of=20audio=20playback=20-=20Seeking:=20Click?= =?UTF-8?q?=20anywhere=20on=20the=20oscilloscope=20display=20to=20jump=20t?= =?UTF-8?q?o=20that=20position=20in=20the=20audio=20=E2=9C=85=20Export=20O?= =?UTF-8?q?ptions=20-=20Resolution=20Selection:=20Choose=20from=20640?= =?UTF-8?q?=C3=97480,=201280=C3=97720,=20or=201920=C3=971080=20-=20Frame?= =?UTF-8?q?=20Rate=20Options:=2024,=2030,=20or=2060=20FPS=20-=20Format=20S?= =?UTF-8?q?election:=20WebM=20or=20MP4=20(if=20supported)=20-=20Quality=20?= =?UTF-8?q?Settings:=20Low,=20Medium,=20or=20High=20quality=20presets=20?= =?UTF-8?q?=E2=9C=85=20Microphone=20Calibration=20-=20Real-time=20Level=20?= =?UTF-8?q?Monitoring:=20Visual=20indicator=20showing=20current=20micropho?= =?UTF-8?q?ne=20input=20level=20-=20Gain=20Control:=20Adjustable=20gain=20?= =?UTF-8?q?slider=20(0.1x=20to=203x)=20to=20optimize=20input=20levels=20-?= =?UTF-8?q?=20Visual=20Feedback:=20Color-coded=20level=20indicator=20(red?= =?UTF-8?q?=3Dtoo=20loud,=20yellow=3Dgood,=20green=3Dtoo=20quiet)=20-=20Ca?= =?UTF-8?q?libration=20Mode:=20Dedicated=20calibration=20panel=20that=20ap?= =?UTF-8?q?pears=20when=20mic=20is=20active?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ControlPanel.tsx | 126 ++++++++++++++++++ src/components/Oscilloscope.tsx | 174 +++++++++++++++++++++++-- src/components/OscilloscopeDisplay.tsx | 51 +++++++- src/hooks/useVideoExporter.ts | 2 + 4 files changed, 337 insertions(+), 16 deletions(-) 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