From 9997558c3af71f4ea815ffc61fe3b2496b8fbc71 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sun, 21 Dec 2025 14:55:19 +0000 Subject: [PATCH] Changes --- src/components/Oscilloscope.tsx | 142 +++++++++++++++--- ...trolPanel.tsx => OscilloscopeControls.tsx} | 23 +-- src/components/OscilloscopeDisplay.tsx | 2 +- 3 files changed, 128 insertions(+), 39 deletions(-) rename src/components/{ControlPanel.tsx => OscilloscopeControls.tsx} (94%) diff --git a/src/components/Oscilloscope.tsx b/src/components/Oscilloscope.tsx index 0bf0904..c343024 100644 --- a/src/components/Oscilloscope.tsx +++ b/src/components/Oscilloscope.tsx @@ -1,13 +1,21 @@ import { useState, useCallback, useEffect, useRef } from 'react'; import { AudioUploader } from './AudioUploader'; -import { ControlPanel, LiveDisplaySettings } from './ControlPanel'; +import { OscilloscopeControls, LiveDisplaySettings } from './OscilloscopeControls'; 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 { Mic, MicOff, Pause, Play } from 'lucide-react'; import { Button } from '@/components/ui/button'; +import { Slider } from '@/components/ui/slider'; + +// Format time in mm:ss format +const formatTime = (seconds: number): string => { + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${mins}:${secs.toString().padStart(2, '0')}`; +}; export function Oscilloscope() { const [mode, setMode] = useState<'combined' | 'separate' | 'all'>('combined'); @@ -19,19 +27,22 @@ export function Oscilloscope() { }); const [isMicActive, setIsMicActive] = useState(false); const [isPlaying, setIsPlaying] = useState(false); - const [currentSample, setCurrentSample] = useState(0); + const [currentTime, setCurrentTime] = useState(0); const [playbackSpeed, setPlaybackSpeed] = useState(1); const [isLooping, setIsLooping] = useState(false); const [seekPosition, setSeekPosition] = useState(0); const [exportResolution, setExportResolution] = useState<'640x480' | '1280x720' | '1920x1080'>('1920x1080'); const [exportFps, setExportFps] = useState<24 | 30 | 60>(60); - const [exportFormat, setExportFormat] = useState<'webm' | 'mp4'>('webm'); const [exportQuality, setExportQuality] = useState<'low' | 'medium' | 'high'>('medium'); const [showMicCalibration, setShowMicCalibration] = useState(false); const [micLevel, setMicLevel] = useState(0); const [micGain, setMicGain] = useState(1); const [micGainNode, setMicGainNode] = useState(null); const isMicActiveRef = useRef(false); + + // Audio playback refs + const audioRef = useRef(null); + const audioUrlRef = useRef(null); const { audioData, isLoading, fileName, originalFile, loadAudioFile, reset: resetAudio } = useAudioAnalyzer(); const { isExporting, progress, exportedUrl, exportVideo, reset: resetExport } = useVideoExporter(); @@ -47,6 +58,70 @@ export function Oscilloscope() { // Real-time microphone input const [micStream, setMicStream] = useState(null); const [micAnalyzer, setMicAnalyzer] = useState(null); + + // Create audio element when file is loaded + useEffect(() => { + if (originalFile) { + // Clean up previous audio URL + if (audioUrlRef.current) { + URL.revokeObjectURL(audioUrlRef.current); + } + + // Create new audio element + const url = URL.createObjectURL(originalFile); + audioUrlRef.current = url; + + const audio = new Audio(url); + audio.loop = isLooping; + audio.playbackRate = playbackSpeed; + + audio.addEventListener('timeupdate', () => { + setCurrentTime(audio.currentTime); + if (audioData) { + const position = audio.currentTime / audioData.duration; + setSeekPosition(position); + } + }); + + audio.addEventListener('ended', () => { + if (!isLooping) { + setIsPlaying(false); + setCurrentTime(0); + setSeekPosition(0); + } + }); + + audioRef.current = audio; + + return () => { + audio.pause(); + audio.src = ''; + if (audioUrlRef.current) { + URL.revokeObjectURL(audioUrlRef.current); + audioUrlRef.current = null; + } + }; + } + }, [originalFile, audioData]); + + // Update audio playback state + useEffect(() => { + if (audioRef.current) { + audioRef.current.loop = isLooping; + audioRef.current.playbackRate = playbackSpeed; + } + }, [isLooping, playbackSpeed]); + + // Handle play/pause + useEffect(() => { + if (audioRef.current) { + if (isPlaying) { + audioRef.current.play().catch(console.error); + } else { + audioRef.current.pause(); + } + } + }, [isPlaying]); const handleFileSelect = useCallback((file: File) => { loadAudioFile(file); @@ -118,9 +193,10 @@ export function Oscilloscope() { }, [isPlaying, playSound]); const handleSeek = useCallback((position: number) => { - if (audioData && position >= 0) { - const newSample = Math.floor(position * audioData.leftChannel.length); - setCurrentSample(newSample); + if (audioData && audioRef.current && position >= 0) { + const newTime = position * audioData.duration; + audioRef.current.currentTime = newTime; + setCurrentTime(newTime); setSeekPosition(position); } }, [audioData]); @@ -141,10 +217,6 @@ export function Oscilloscope() { setExportFps(fps as 24 | 30 | 60); }, []); - const handleExportFormatChange = useCallback((format: string) => { - setExportFormat(format as 'webm' | 'mp4'); - }, []); - const handleExportQualityChange = useCallback((quality: string) => { setExportQuality(quality as 'low' | 'medium' | 'high'); }, []); @@ -160,10 +232,9 @@ export function Oscilloscope() { fps: exportFps, mode, audioFile: originalFile, - format: exportFormat, quality: exportQuality, }); - }, [audioData, originalFile, exportVideo, mode, exportResolution, exportFps, exportFormat, exportQuality]); + }, [audioData, originalFile, exportVideo, mode, exportResolution, exportFps, exportQuality]); const handleReset = useCallback(() => { playSound('click'); @@ -231,7 +302,7 @@ export function Oscilloscope() { {/* Right Column: Control Panel */} - {/* Oscilloscope Display */} -
+
setIsPlaying(false)} + onPlaybackEnd={() => { + setIsPlaying(false); + setCurrentTime(0); + setSeekPosition(0); + }} onSeek={handleSeek} liveSettings={liveSettings} /> + + {/* Audio Playback Controls */} + {audioData && originalFile && ( +
+ {/* Play/Pause and Time Display */} +
+ + + + {formatTime(currentTime)} / {formatTime(audioData.duration)} + + + {/* Progress Bar */} +
+ handleSeek(value[0] / 100)} + max={100} + step={0.1} + className="cursor-pointer" + /> +
+
+
+ )}
{/* Microphone Calibration */} diff --git a/src/components/ControlPanel.tsx b/src/components/OscilloscopeControls.tsx similarity index 94% rename from src/components/ControlPanel.tsx rename to src/components/OscilloscopeControls.tsx index d5874bd..010ca1f 100755 --- a/src/components/ControlPanel.tsx +++ b/src/components/OscilloscopeControls.tsx @@ -32,15 +32,13 @@ interface ControlPanelProps { onExportResolutionChange: (resolution: string) => void; exportFps: number; onExportFpsChange: (fps: number) => void; - exportFormat: string; - onExportFormatChange: (format: string) => void; exportQuality: string; onExportQualityChange: (quality: string) => void; liveSettings: LiveDisplaySettings; onLiveSettingsChange: (settings: LiveDisplaySettings) => void; } -export function ControlPanel({ +export function OscilloscopeControls({ mode, onModeChange, canGenerate, @@ -60,8 +58,6 @@ export function ControlPanel({ onExportResolutionChange, exportFps, onExportFpsChange, - exportFormat, - onExportFormatChange, exportQuality, onExportQualityChange, liveSettings, @@ -251,19 +247,6 @@ export function ControlPanel({
- {/* Format */} -
- Format - -
- {/* Quality */}
Quality @@ -338,8 +321,8 @@ export function ControlPanel({ {/* Info */}
-

Output: 1920×1080 WebM

-

Frame Rate: 60 FPS

+

Output: WebM (VP9)

+

Quality affects video bitrate

Supports files up to 6+ hours

diff --git a/src/components/OscilloscopeDisplay.tsx b/src/components/OscilloscopeDisplay.tsx index 0a39a02..cf7eb3c 100755 --- a/src/components/OscilloscopeDisplay.tsx +++ b/src/components/OscilloscopeDisplay.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useCallback } from 'react'; import type { AudioData } from '@/hooks/useAudioAnalyzer'; import type { OscilloscopeMode } from '@/hooks/useOscilloscopeRenderer'; import { useAudioAnalyzer as useSharedAudioAnalyzer } from '@/contexts/AudioAnalyzerContext'; -import type { LiveDisplaySettings } from './ControlPanel'; +import type { LiveDisplaySettings } from './OscilloscopeControls'; interface OscilloscopeDisplayProps { audioData: AudioData | null;