This commit is contained in:
gpt-engineer-app[bot] 2025-12-21 14:55:19 +00:00
parent 53f2dc6e96
commit 9997558c3a
3 changed files with 128 additions and 39 deletions

View File

@ -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,13 +27,12 @@ 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);
@ -33,6 +40,10 @@ export function Oscilloscope() {
const [micGainNode, setMicGainNode] = useState<GainNode | null>(null);
const isMicActiveRef = useRef(false);
// Audio playback refs
const audioRef = useRef<HTMLAudioElement | null>(null);
const audioUrlRef = useRef<string | null>(null);
const { audioData, isLoading, fileName, originalFile, loadAudioFile, reset: resetAudio } = useAudioAnalyzer();
const { isExporting, progress, exportedUrl, exportVideo, reset: resetExport } = useVideoExporter();
const { playSound } = useSettings();
@ -48,6 +59,70 @@ export function Oscilloscope() {
const [micStream, setMicStream] = useState<MediaStream | null>(null);
const [micAnalyzer, setMicAnalyzer] = useState<AnalyserNode | null>(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);
if (isMicActive) {
@ -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() {
</div>
{/* Right Column: Control Panel */}
<ControlPanel
<OscilloscopeControls
mode={mode}
onModeChange={setMode}
canGenerate={canGenerate}
@ -251,8 +322,6 @@ export function Oscilloscope() {
onExportResolutionChange={handleExportResolutionChange}
exportFps={exportFps}
onExportFpsChange={handleExportFpsChange}
exportFormat={exportFormat}
onExportFormatChange={handleExportFormatChange}
exportQuality={exportQuality}
onExportQualityChange={handleExportQualityChange}
liveSettings={liveSettings}
@ -261,7 +330,7 @@ export function Oscilloscope() {
</div>
{/* Oscilloscope Display */}
<div className="flex justify-center">
<div className="flex justify-center flex-col items-center gap-4">
<OscilloscopeDisplay
audioData={audioData}
micAnalyzer={micAnalyzer}
@ -270,10 +339,47 @@ export function Oscilloscope() {
playbackSpeed={playbackSpeed}
isLooping={isLooping}
seekPosition={seekPosition}
onPlaybackEnd={() => setIsPlaying(false)}
onPlaybackEnd={() => {
setIsPlaying(false);
setCurrentTime(0);
setSeekPosition(0);
}}
onSeek={handleSeek}
liveSettings={liveSettings}
/>
{/* Audio Playback Controls */}
{audioData && originalFile && (
<div className="w-full max-w-3xl space-y-2 px-4">
{/* Play/Pause and Time Display */}
<div className="flex items-center gap-4">
<Button
onClick={handlePreview}
variant="outline"
size="sm"
className="font-crt border-primary/50 hover:bg-primary/10"
disabled={isExporting}
>
{isPlaying ? <Pause size={16} /> : <Play size={16} />}
</Button>
<span className="font-mono-crt text-sm text-foreground/80 min-w-[80px]">
{formatTime(currentTime)} / {formatTime(audioData.duration)}
</span>
{/* Progress Bar */}
<div className="flex-1">
<Slider
value={[seekPosition * 100]}
onValueChange={(value) => handleSeek(value[0] / 100)}
max={100}
step={0.1}
className="cursor-pointer"
/>
</div>
</div>
</div>
)}
</div>
{/* Microphone Calibration */}

View File

@ -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({
</select>
</div>
{/* Format */}
<div className="flex items-center justify-between">
<span className="font-mono-crt text-sm text-foreground/90">Format</span>
<select
value={exportFormat}
onChange={(e) => onExportFormatChange(e.target.value)}
className="bg-background border border-primary/50 text-primary font-mono-crt text-sm px-2 py-1"
>
<option value="webm">WebM</option>
<option value="mp4">MP4</option>
</select>
</div>
{/* Quality */}
<div className="flex items-center justify-between">
<span className="font-mono-crt text-sm text-foreground/90">Quality</span>
@ -338,8 +321,8 @@ export function ControlPanel({
{/* Info */}
<div className="text-xs text-muted-foreground font-mono-crt space-y-1 pt-4 border-t border-border">
<p>Output: 1920×1080 WebM</p>
<p>Frame Rate: 60 FPS</p>
<p>Output: WebM (VP9)</p>
<p>Quality affects video bitrate</p>
<p>Supports files up to 6+ hours</p>
</div>
</div>

View File

@ -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;