mirror of
https://github.com/JorySeverijnse/ui-fixer-supreme.git
synced 2026-01-29 21:48:37 +00:00
Refactor oscilloscope UI
- Rename ControlPanel to OscilloscopeControls for clarity - Integrate live audio playback with a progress bar and time display - Remove MP4 export; WebM only with updated export options - Align oscilloscope components to use shared audio analyzer context - Enable immediate oscilloscope initialization and live display modes (Combined/XY) - Improve styling and responsive sizing of mini/full oscilloscope views X-Lovable-Edit-ID: edt-28a85a31-f522-4420-8353-3be1a784d5c4
This commit is contained in:
commit
8fd0ef4f53
@ -1,13 +1,21 @@
|
|||||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
import { useState, useCallback, useEffect, useRef } from 'react';
|
||||||
import { AudioUploader } from './AudioUploader';
|
import { AudioUploader } from './AudioUploader';
|
||||||
import { ControlPanel, LiveDisplaySettings } from './ControlPanel';
|
import { OscilloscopeControls, LiveDisplaySettings } from './OscilloscopeControls';
|
||||||
import { OscilloscopeDisplay } from './OscilloscopeDisplay';
|
import { OscilloscopeDisplay } from './OscilloscopeDisplay';
|
||||||
import { useAudioAnalyzer } from '@/hooks/useAudioAnalyzer';
|
import { useAudioAnalyzer } from '@/hooks/useAudioAnalyzer';
|
||||||
import { useOscilloscopeRenderer } from '@/hooks/useOscilloscopeRenderer';
|
import { useOscilloscopeRenderer } from '@/hooks/useOscilloscopeRenderer';
|
||||||
import { useVideoExporter } from '@/hooks/useVideoExporter';
|
import { useVideoExporter } from '@/hooks/useVideoExporter';
|
||||||
import { useSettings } from '@/contexts/SettingsContext';
|
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 { 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() {
|
export function Oscilloscope() {
|
||||||
const [mode, setMode] = useState<'combined' | 'separate' | 'all'>('combined');
|
const [mode, setMode] = useState<'combined' | 'separate' | 'all'>('combined');
|
||||||
@ -19,19 +27,22 @@ export function Oscilloscope() {
|
|||||||
});
|
});
|
||||||
const [isMicActive, setIsMicActive] = useState(false);
|
const [isMicActive, setIsMicActive] = useState(false);
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [currentSample, setCurrentSample] = useState(0);
|
const [currentTime, setCurrentTime] = useState(0);
|
||||||
const [playbackSpeed, setPlaybackSpeed] = useState(1);
|
const [playbackSpeed, setPlaybackSpeed] = useState(1);
|
||||||
const [isLooping, setIsLooping] = useState(false);
|
const [isLooping, setIsLooping] = useState(false);
|
||||||
const [seekPosition, setSeekPosition] = useState(0);
|
const [seekPosition, setSeekPosition] = useState(0);
|
||||||
const [exportResolution, setExportResolution] = useState<'640x480' | '1280x720' | '1920x1080'>('1920x1080');
|
const [exportResolution, setExportResolution] = useState<'640x480' | '1280x720' | '1920x1080'>('1920x1080');
|
||||||
const [exportFps, setExportFps] = useState<24 | 30 | 60>(60);
|
const [exportFps, setExportFps] = useState<24 | 30 | 60>(60);
|
||||||
const [exportFormat, setExportFormat] = useState<'webm' | 'mp4'>('webm');
|
|
||||||
const [exportQuality, setExportQuality] = useState<'low' | 'medium' | 'high'>('medium');
|
const [exportQuality, setExportQuality] = useState<'low' | 'medium' | 'high'>('medium');
|
||||||
const [showMicCalibration, setShowMicCalibration] = useState(false);
|
const [showMicCalibration, setShowMicCalibration] = useState(false);
|
||||||
const [micLevel, setMicLevel] = useState(0);
|
const [micLevel, setMicLevel] = useState(0);
|
||||||
const [micGain, setMicGain] = useState(1);
|
const [micGain, setMicGain] = useState(1);
|
||||||
const [micGainNode, setMicGainNode] = useState<GainNode | null>(null);
|
const [micGainNode, setMicGainNode] = useState<GainNode | null>(null);
|
||||||
const isMicActiveRef = useRef(false);
|
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 { audioData, isLoading, fileName, originalFile, loadAudioFile, reset: resetAudio } = useAudioAnalyzer();
|
||||||
const { isExporting, progress, exportedUrl, exportVideo, reset: resetExport } = useVideoExporter();
|
const { isExporting, progress, exportedUrl, exportVideo, reset: resetExport } = useVideoExporter();
|
||||||
@ -47,6 +58,70 @@ export function Oscilloscope() {
|
|||||||
// Real-time microphone input
|
// Real-time microphone input
|
||||||
const [micStream, setMicStream] = useState<MediaStream | null>(null);
|
const [micStream, setMicStream] = useState<MediaStream | null>(null);
|
||||||
const [micAnalyzer, setMicAnalyzer] = useState<AnalyserNode | 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) => {
|
const handleFileSelect = useCallback((file: File) => {
|
||||||
loadAudioFile(file);
|
loadAudioFile(file);
|
||||||
@ -118,9 +193,10 @@ export function Oscilloscope() {
|
|||||||
}, [isPlaying, playSound]);
|
}, [isPlaying, playSound]);
|
||||||
|
|
||||||
const handleSeek = useCallback((position: number) => {
|
const handleSeek = useCallback((position: number) => {
|
||||||
if (audioData && position >= 0) {
|
if (audioData && audioRef.current && position >= 0) {
|
||||||
const newSample = Math.floor(position * audioData.leftChannel.length);
|
const newTime = position * audioData.duration;
|
||||||
setCurrentSample(newSample);
|
audioRef.current.currentTime = newTime;
|
||||||
|
setCurrentTime(newTime);
|
||||||
setSeekPosition(position);
|
setSeekPosition(position);
|
||||||
}
|
}
|
||||||
}, [audioData]);
|
}, [audioData]);
|
||||||
@ -141,10 +217,6 @@ export function Oscilloscope() {
|
|||||||
setExportFps(fps as 24 | 30 | 60);
|
setExportFps(fps as 24 | 30 | 60);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleExportFormatChange = useCallback((format: string) => {
|
|
||||||
setExportFormat(format as 'webm' | 'mp4');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleExportQualityChange = useCallback((quality: string) => {
|
const handleExportQualityChange = useCallback((quality: string) => {
|
||||||
setExportQuality(quality as 'low' | 'medium' | 'high');
|
setExportQuality(quality as 'low' | 'medium' | 'high');
|
||||||
}, []);
|
}, []);
|
||||||
@ -160,10 +232,9 @@ export function Oscilloscope() {
|
|||||||
fps: exportFps,
|
fps: exportFps,
|
||||||
mode,
|
mode,
|
||||||
audioFile: originalFile,
|
audioFile: originalFile,
|
||||||
format: exportFormat,
|
|
||||||
quality: exportQuality,
|
quality: exportQuality,
|
||||||
});
|
});
|
||||||
}, [audioData, originalFile, exportVideo, mode, exportResolution, exportFps, exportFormat, exportQuality]);
|
}, [audioData, originalFile, exportVideo, mode, exportResolution, exportFps, exportQuality]);
|
||||||
|
|
||||||
const handleReset = useCallback(() => {
|
const handleReset = useCallback(() => {
|
||||||
playSound('click');
|
playSound('click');
|
||||||
@ -231,7 +302,7 @@ export function Oscilloscope() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Column: Control Panel */}
|
{/* Right Column: Control Panel */}
|
||||||
<ControlPanel
|
<OscilloscopeControls
|
||||||
mode={mode}
|
mode={mode}
|
||||||
onModeChange={setMode}
|
onModeChange={setMode}
|
||||||
canGenerate={canGenerate}
|
canGenerate={canGenerate}
|
||||||
@ -251,8 +322,6 @@ export function Oscilloscope() {
|
|||||||
onExportResolutionChange={handleExportResolutionChange}
|
onExportResolutionChange={handleExportResolutionChange}
|
||||||
exportFps={exportFps}
|
exportFps={exportFps}
|
||||||
onExportFpsChange={handleExportFpsChange}
|
onExportFpsChange={handleExportFpsChange}
|
||||||
exportFormat={exportFormat}
|
|
||||||
onExportFormatChange={handleExportFormatChange}
|
|
||||||
exportQuality={exportQuality}
|
exportQuality={exportQuality}
|
||||||
onExportQualityChange={handleExportQualityChange}
|
onExportQualityChange={handleExportQualityChange}
|
||||||
liveSettings={liveSettings}
|
liveSettings={liveSettings}
|
||||||
@ -261,7 +330,7 @@ export function Oscilloscope() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Oscilloscope Display */}
|
{/* Oscilloscope Display */}
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center flex-col items-center gap-4">
|
||||||
<OscilloscopeDisplay
|
<OscilloscopeDisplay
|
||||||
audioData={audioData}
|
audioData={audioData}
|
||||||
micAnalyzer={micAnalyzer}
|
micAnalyzer={micAnalyzer}
|
||||||
@ -270,10 +339,47 @@ export function Oscilloscope() {
|
|||||||
playbackSpeed={playbackSpeed}
|
playbackSpeed={playbackSpeed}
|
||||||
isLooping={isLooping}
|
isLooping={isLooping}
|
||||||
seekPosition={seekPosition}
|
seekPosition={seekPosition}
|
||||||
onPlaybackEnd={() => setIsPlaying(false)}
|
onPlaybackEnd={() => {
|
||||||
|
setIsPlaying(false);
|
||||||
|
setCurrentTime(0);
|
||||||
|
setSeekPosition(0);
|
||||||
|
}}
|
||||||
onSeek={handleSeek}
|
onSeek={handleSeek}
|
||||||
liveSettings={liveSettings}
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Microphone Calibration */}
|
{/* Microphone Calibration */}
|
||||||
|
|||||||
@ -32,15 +32,13 @@ interface ControlPanelProps {
|
|||||||
onExportResolutionChange: (resolution: string) => void;
|
onExportResolutionChange: (resolution: string) => void;
|
||||||
exportFps: number;
|
exportFps: number;
|
||||||
onExportFpsChange: (fps: number) => void;
|
onExportFpsChange: (fps: number) => void;
|
||||||
exportFormat: string;
|
|
||||||
onExportFormatChange: (format: string) => void;
|
|
||||||
exportQuality: string;
|
exportQuality: string;
|
||||||
onExportQualityChange: (quality: string) => void;
|
onExportQualityChange: (quality: string) => void;
|
||||||
liveSettings: LiveDisplaySettings;
|
liveSettings: LiveDisplaySettings;
|
||||||
onLiveSettingsChange: (settings: LiveDisplaySettings) => void;
|
onLiveSettingsChange: (settings: LiveDisplaySettings) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ControlPanel({
|
export function OscilloscopeControls({
|
||||||
mode,
|
mode,
|
||||||
onModeChange,
|
onModeChange,
|
||||||
canGenerate,
|
canGenerate,
|
||||||
@ -60,8 +58,6 @@ export function ControlPanel({
|
|||||||
onExportResolutionChange,
|
onExportResolutionChange,
|
||||||
exportFps,
|
exportFps,
|
||||||
onExportFpsChange,
|
onExportFpsChange,
|
||||||
exportFormat,
|
|
||||||
onExportFormatChange,
|
|
||||||
exportQuality,
|
exportQuality,
|
||||||
onExportQualityChange,
|
onExportQualityChange,
|
||||||
liveSettings,
|
liveSettings,
|
||||||
@ -251,19 +247,6 @@ export function ControlPanel({
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</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 */}
|
{/* Quality */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="font-mono-crt text-sm text-foreground/90">Quality</span>
|
<span className="font-mono-crt text-sm text-foreground/90">Quality</span>
|
||||||
@ -338,8 +321,8 @@ export function ControlPanel({
|
|||||||
|
|
||||||
{/* Info */}
|
{/* Info */}
|
||||||
<div className="text-xs text-muted-foreground font-mono-crt space-y-1 pt-4 border-t border-border">
|
<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>Output: WebM (VP9)</p>
|
||||||
<p>Frame Rate: 60 FPS</p>
|
<p>Quality affects video bitrate</p>
|
||||||
<p>Supports files up to 6+ hours</p>
|
<p>Supports files up to 6+ hours</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -2,7 +2,7 @@ import { useEffect, useRef, useCallback } from 'react';
|
|||||||
import type { AudioData } from '@/hooks/useAudioAnalyzer';
|
import type { AudioData } from '@/hooks/useAudioAnalyzer';
|
||||||
import type { OscilloscopeMode } from '@/hooks/useOscilloscopeRenderer';
|
import type { OscilloscopeMode } from '@/hooks/useOscilloscopeRenderer';
|
||||||
import { useAudioAnalyzer as useSharedAudioAnalyzer } from '@/contexts/AudioAnalyzerContext';
|
import { useAudioAnalyzer as useSharedAudioAnalyzer } from '@/contexts/AudioAnalyzerContext';
|
||||||
import type { LiveDisplaySettings } from './ControlPanel';
|
import type { LiveDisplaySettings } from './OscilloscopeControls';
|
||||||
|
|
||||||
interface OscilloscopeDisplayProps {
|
interface OscilloscopeDisplayProps {
|
||||||
audioData: AudioData | null;
|
audioData: AudioData | null;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user