mirror of
https://github.com/JorySeverijnse/ui-fixer-supreme.git
synced 2026-01-29 19:58:38 +00:00
Changes
This commit is contained in:
parent
043b06d6ea
commit
8ecd8da712
@ -6,7 +6,7 @@ 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, Pause, Play } from 'lucide-react';
|
import { Pause, Play } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Slider } from '@/components/ui/slider';
|
import { Slider } from '@/components/ui/slider';
|
||||||
|
|
||||||
@ -24,6 +24,7 @@ export function Oscilloscope() {
|
|||||||
showGrid: true,
|
showGrid: true,
|
||||||
glowIntensity: 1,
|
glowIntensity: 1,
|
||||||
displayMode: 'combined',
|
displayMode: 'combined',
|
||||||
|
visualizationMode: 'waveform',
|
||||||
});
|
});
|
||||||
const [isMicActive, setIsMicActive] = useState(false);
|
const [isMicActive, setIsMicActive] = useState(false);
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
@ -339,54 +340,21 @@ export function Oscilloscope() {
|
|||||||
onExportQualityChange={handleExportQualityChange}
|
onExportQualityChange={handleExportQualityChange}
|
||||||
liveSettings={liveSettings}
|
liveSettings={liveSettings}
|
||||||
onLiveSettingsChange={setLiveSettings}
|
onLiveSettingsChange={setLiveSettings}
|
||||||
|
isMicActive={isMicActive}
|
||||||
|
onToggleMic={toggleMic}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Audio Input Row - Now at bottom */}
|
{/* Audio Uploader - below controls on desktop */}
|
||||||
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4">
|
<div className="mt-4">
|
||||||
<div className="flex-1 w-full sm:w-auto">
|
|
||||||
<AudioUploader
|
<AudioUploader
|
||||||
onFileSelect={handleFileSelect}
|
onFileSelect={handleFileSelect}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
fileName={fileName}
|
fileName={fileName}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Microphone Toggle */}
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<Button
|
|
||||||
onClick={toggleMic}
|
|
||||||
variant={isMicActive ? "default" : "outline"}
|
|
||||||
className={`flex items-center gap-2 font-crt ${
|
|
||||||
isMicActive
|
|
||||||
? 'bg-primary text-primary-foreground'
|
|
||||||
: 'border-primary/50 hover:bg-primary/10'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{isMicActive ? <MicOff size={16} /> : <Mic size={16} />}
|
|
||||||
{isMicActive ? 'STOP MIC' : 'USE MICROPHONE'}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{isMicActive && (
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="text-sm text-muted-foreground font-mono-crt">
|
|
||||||
Real-time input active
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
onClick={() => setShowMicCalibration(!showMicCalibration)}
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="font-mono-crt text-xs"
|
|
||||||
>
|
|
||||||
Calibrate
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{/* Microphone Calibration */}
|
{/* Microphone Calibration */}
|
||||||
{showMicCalibration && isMicActive && (
|
{showMicCalibration && isMicActive && (
|
||||||
<div className="bg-card border border-border rounded-lg p-4 max-w-md mx-auto">
|
<div className="bg-card border border-border rounded-lg p-4 max-w-md mx-auto">
|
||||||
@ -438,6 +406,56 @@ export function Oscilloscope() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{showMicCalibration && isMicActive && (
|
||||||
|
<div className="bg-card border border-border rounded-lg p-4 max-w-md mx-auto">
|
||||||
|
<h3 className="font-crt text-lg text-primary text-glow mb-4">Microphone Calibration</h3>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Level Indicator */}
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between text-sm font-mono-crt mb-2">
|
||||||
|
<span>Input Level</span>
|
||||||
|
<span>{Math.round(micLevel * 100)}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-secondary rounded-full h-3">
|
||||||
|
<div
|
||||||
|
className="bg-primary h-3 rounded-full transition-all duration-100"
|
||||||
|
style={{ width: `${micLevel * 100}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-xs text-muted-foreground mt-1">
|
||||||
|
<span>Quiet</span>
|
||||||
|
<span className={micLevel > 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'}
|
||||||
|
</span>
|
||||||
|
<span>Loud</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Gain Control */}
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between text-sm font-mono-crt mb-2">
|
||||||
|
<span>Gain</span>
|
||||||
|
<span>{micGain.toFixed(1)}x</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0.1"
|
||||||
|
max="3"
|
||||||
|
step="0.1"
|
||||||
|
value={micGain}
|
||||||
|
onChange={(e) => setMicGain(Number(e.target.value))}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-muted-foreground font-mono-crt">
|
||||||
|
Speak into your microphone and adjust gain until the level shows "Good" (green).
|
||||||
|
The bar should peak around 50-70% when speaking normally.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Status Info */}
|
{/* Status Info */}
|
||||||
{(isLoading || isExporting) && (
|
{(isLoading || isExporting) && (
|
||||||
|
|||||||
@ -1,15 +1,18 @@
|
|||||||
import { Play, Download, RotateCcw } from 'lucide-react';
|
import { Play, Download, RotateCcw, Mic, MicOff } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Progress } from '@/components/ui/progress';
|
import { Progress } from '@/components/ui/progress';
|
||||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||||
import type { OscilloscopeMode } from '@/hooks/useOscilloscopeRenderer';
|
import type { OscilloscopeMode } from '@/hooks/useOscilloscopeRenderer';
|
||||||
|
|
||||||
|
export type VisualizationMode = 'waveform' | 'spectrum' | 'both';
|
||||||
|
|
||||||
export interface LiveDisplaySettings {
|
export interface LiveDisplaySettings {
|
||||||
lineThickness: number;
|
lineThickness: number;
|
||||||
showGrid: boolean;
|
showGrid: boolean;
|
||||||
glowIntensity: number;
|
glowIntensity: number;
|
||||||
displayMode: OscilloscopeMode;
|
displayMode: OscilloscopeMode;
|
||||||
|
visualizationMode: VisualizationMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ControlPanelProps {
|
interface ControlPanelProps {
|
||||||
@ -36,6 +39,8 @@ interface ControlPanelProps {
|
|||||||
onExportQualityChange: (quality: string) => void;
|
onExportQualityChange: (quality: string) => void;
|
||||||
liveSettings: LiveDisplaySettings;
|
liveSettings: LiveDisplaySettings;
|
||||||
onLiveSettingsChange: (settings: LiveDisplaySettings) => void;
|
onLiveSettingsChange: (settings: LiveDisplaySettings) => void;
|
||||||
|
isMicActive: boolean;
|
||||||
|
onToggleMic: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OscilloscopeControls({
|
export function OscilloscopeControls({
|
||||||
@ -62,6 +67,8 @@ export function OscilloscopeControls({
|
|||||||
onExportQualityChange,
|
onExportQualityChange,
|
||||||
liveSettings,
|
liveSettings,
|
||||||
onLiveSettingsChange,
|
onLiveSettingsChange,
|
||||||
|
isMicActive,
|
||||||
|
onToggleMic,
|
||||||
}: ControlPanelProps) {
|
}: ControlPanelProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6 p-6 bg-card border border-border rounded-lg">
|
<div className="flex flex-col gap-6 p-6 bg-card border border-border rounded-lg">
|
||||||
@ -69,6 +76,26 @@ export function OscilloscopeControls({
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Label className="font-crt text-lg text-primary text-glow">LIVE DISPLAY</Label>
|
<Label className="font-crt text-lg text-primary text-glow">LIVE DISPLAY</Label>
|
||||||
|
|
||||||
|
{/* Visualization Mode */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="font-mono-crt text-sm text-foreground/90">Visualization</span>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{(['waveform', 'spectrum', 'both'] as VisualizationMode[]).map((vizMode) => (
|
||||||
|
<button
|
||||||
|
key={vizMode}
|
||||||
|
onClick={() => onLiveSettingsChange({ ...liveSettings, visualizationMode: vizMode })}
|
||||||
|
className={`px-2 py-1 text-xs font-mono-crt border transition-all duration-300 capitalize ${
|
||||||
|
liveSettings.visualizationMode === vizMode
|
||||||
|
? 'border-primary text-primary bg-primary/10'
|
||||||
|
: 'border-primary/50 text-primary/70 hover:border-primary hover:text-primary'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{vizMode}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Display Mode */}
|
{/* Display Mode */}
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
value={liveSettings.displayMode}
|
value={liveSettings.displayMode}
|
||||||
@ -147,6 +174,31 @@ export function OscilloscopeControls({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Audio Input */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label className="font-crt text-lg text-primary text-glow">AUDIO INPUT</Label>
|
||||||
|
|
||||||
|
{/* Microphone Toggle */}
|
||||||
|
<Button
|
||||||
|
onClick={onToggleMic}
|
||||||
|
variant={isMicActive ? "default" : "outline"}
|
||||||
|
className={`w-full flex items-center justify-center gap-2 font-crt ${
|
||||||
|
isMicActive
|
||||||
|
? 'bg-primary text-primary-foreground'
|
||||||
|
: 'border-primary/50 hover:bg-primary/10'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isMicActive ? <MicOff size={16} /> : <Mic size={16} />}
|
||||||
|
{isMicActive ? 'STOP MICROPHONE' : 'USE MICROPHONE'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{isMicActive && (
|
||||||
|
<p className="text-xs text-muted-foreground font-mono-crt text-center">
|
||||||
|
Real-time input active
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Playback Controls */}
|
{/* Playback Controls */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Label className="font-crt text-lg text-primary text-glow">PLAYBACK CONTROLS</Label>
|
<Label className="font-crt text-lg text-primary text-glow">PLAYBACK CONTROLS</Label>
|
||||||
|
|||||||
@ -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 './OscilloscopeControls';
|
import type { LiveDisplaySettings, VisualizationMode } from './OscilloscopeControls';
|
||||||
|
|
||||||
interface OscilloscopeDisplayProps {
|
interface OscilloscopeDisplayProps {
|
||||||
audioData: AudioData | null;
|
audioData: AudioData | null;
|
||||||
@ -56,6 +56,7 @@ export function OscilloscopeDisplay({
|
|||||||
const showGrid = liveSettings?.showGrid ?? true;
|
const showGrid = liveSettings?.showGrid ?? true;
|
||||||
const glowIntensity = liveSettings?.glowIntensity ?? 1;
|
const glowIntensity = liveSettings?.glowIntensity ?? 1;
|
||||||
const liveDisplayMode = liveSettings?.displayMode ?? 'combined';
|
const liveDisplayMode = liveSettings?.displayMode ?? 'combined';
|
||||||
|
const visualizationMode = liveSettings?.visualizationMode ?? 'waveform';
|
||||||
|
|
||||||
const drawGraticule = useCallback((ctx: CanvasRenderingContext2D) => {
|
const drawGraticule = useCallback((ctx: CanvasRenderingContext2D) => {
|
||||||
if (!showGrid) return;
|
if (!showGrid) return;
|
||||||
@ -80,6 +81,36 @@ export function OscilloscopeDisplay({
|
|||||||
ctx.globalAlpha = 1;
|
ctx.globalAlpha = 1;
|
||||||
}, [showGrid]);
|
}, [showGrid]);
|
||||||
|
|
||||||
|
// Draw spectrum bars
|
||||||
|
const drawSpectrum = useCallback((ctx: CanvasRenderingContext2D, frequencyData: Uint8Array, yOffset: number = 0, heightRatio: number = 1) => {
|
||||||
|
const primaryColor = getThemeColor('--primary', '#00ff00');
|
||||||
|
const accentColor = getThemeColor('--accent', '#00ccff');
|
||||||
|
|
||||||
|
const barCount = 64;
|
||||||
|
const barWidth = (WIDTH / barCount) - 2;
|
||||||
|
const maxBarHeight = (HEIGHT * heightRatio) * 0.8;
|
||||||
|
|
||||||
|
// Sample frequency data for the bar count
|
||||||
|
const step = Math.floor(frequencyData.length / barCount);
|
||||||
|
|
||||||
|
for (let i = 0; i < barCount; i++) {
|
||||||
|
const dataIndex = i * step;
|
||||||
|
const value = frequencyData[dataIndex] / 255;
|
||||||
|
const barHeight = value * maxBarHeight;
|
||||||
|
|
||||||
|
const x = i * (barWidth + 2);
|
||||||
|
const y = yOffset + (HEIGHT * heightRatio) - barHeight;
|
||||||
|
|
||||||
|
// Create gradient for each bar
|
||||||
|
const gradient = ctx.createLinearGradient(x, y + barHeight, x, y);
|
||||||
|
gradient.addColorStop(0, primaryColor);
|
||||||
|
gradient.addColorStop(1, accentColor);
|
||||||
|
|
||||||
|
ctx.fillStyle = gradient;
|
||||||
|
ctx.fillRect(x, y, barWidth, barHeight);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const drawFrame = useCallback(() => {
|
const drawFrame = useCallback(() => {
|
||||||
if (!canvasRef.current) return;
|
if (!canvasRef.current) return;
|
||||||
|
|
||||||
@ -104,20 +135,24 @@ export function OscilloscopeDisplay({
|
|||||||
if (activeAnalyzer && !audioData) {
|
if (activeAnalyzer && !audioData) {
|
||||||
// Real-time audio data (mic or music player)
|
// Real-time audio data (mic or music player)
|
||||||
const bufferLength = activeAnalyzer.frequencyBinCount;
|
const bufferLength = activeAnalyzer.frequencyBinCount;
|
||||||
const dataArray = new Uint8Array(bufferLength);
|
const timeDomainData = new Uint8Array(bufferLength);
|
||||||
activeAnalyzer.getByteTimeDomainData(dataArray);
|
const frequencyData = new Uint8Array(bufferLength);
|
||||||
|
activeAnalyzer.getByteTimeDomainData(timeDomainData);
|
||||||
|
activeAnalyzer.getByteFrequencyData(frequencyData);
|
||||||
|
|
||||||
// Clear to background color
|
// Clear to background color
|
||||||
ctx.fillStyle = backgroundColor;
|
ctx.fillStyle = backgroundColor;
|
||||||
ctx.fillRect(0, 0, WIDTH, HEIGHT);
|
ctx.fillRect(0, 0, WIDTH, HEIGHT);
|
||||||
|
|
||||||
// Draw graticule first
|
// Draw graticule first (only for waveform modes)
|
||||||
|
if (visualizationMode !== 'spectrum') {
|
||||||
drawGraticule(ctx);
|
drawGraticule(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
// Convert to Float32Array-like for consistency
|
// Convert to Float32Array-like for consistency
|
||||||
const liveData = new Float32Array(dataArray.length);
|
const liveData = new Float32Array(timeDomainData.length);
|
||||||
for (let i = 0; i < dataArray.length; i++) {
|
for (let i = 0; i < timeDomainData.length; i++) {
|
||||||
liveData[i] = (dataArray[i] - 128) / 128; // Normalize to -1 to 1
|
liveData[i] = (timeDomainData[i] - 128) / 128; // Normalize to -1 to 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply glow effect
|
// Apply glow effect
|
||||||
@ -131,7 +166,56 @@ export function OscilloscopeDisplay({
|
|||||||
ctx.strokeStyle = primaryColor;
|
ctx.strokeStyle = primaryColor;
|
||||||
ctx.lineWidth = lineThickness;
|
ctx.lineWidth = lineThickness;
|
||||||
|
|
||||||
// Draw based on live display mode
|
// Draw based on visualization mode
|
||||||
|
if (visualizationMode === 'spectrum') {
|
||||||
|
// Spectrum bars only
|
||||||
|
ctx.shadowBlur = 0;
|
||||||
|
drawSpectrum(ctx, frequencyData, 0, 1);
|
||||||
|
} else if (visualizationMode === 'both') {
|
||||||
|
// Waveform on top half, spectrum on bottom half
|
||||||
|
// Draw waveform
|
||||||
|
if (liveDisplayMode === 'all') {
|
||||||
|
// XY mode in top half
|
||||||
|
ctx.beginPath();
|
||||||
|
const centerX = WIDTH / 2;
|
||||||
|
const centerY = HEIGHT / 4;
|
||||||
|
const scale = Math.min(WIDTH, HEIGHT / 2) * 0.35;
|
||||||
|
|
||||||
|
for (let i = 0; i < liveData.length - 1; i += 2) {
|
||||||
|
const x = centerX + liveData[i] * scale;
|
||||||
|
const y = centerY - liveData[i + 1] * scale;
|
||||||
|
if (i === 0) ctx.moveTo(x, y);
|
||||||
|
else ctx.lineTo(x, y);
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
} else {
|
||||||
|
// Combined waveform in top half
|
||||||
|
ctx.beginPath();
|
||||||
|
const sliceWidth = WIDTH / liveData.length;
|
||||||
|
let x = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < liveData.length; i++) {
|
||||||
|
const v = liveData[i];
|
||||||
|
const y = (v * HEIGHT * 0.4) / 2 + HEIGHT / 4;
|
||||||
|
if (i === 0) ctx.moveTo(x, y);
|
||||||
|
else ctx.lineTo(x, y);
|
||||||
|
x += sliceWidth;
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spectrum in bottom half
|
||||||
|
ctx.shadowBlur = 0;
|
||||||
|
drawSpectrum(ctx, frequencyData, HEIGHT / 2, 0.5);
|
||||||
|
|
||||||
|
// Divider line
|
||||||
|
ctx.strokeStyle = 'rgba(255,255,255,0.1)';
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, HEIGHT / 2);
|
||||||
|
ctx.lineTo(WIDTH, HEIGHT / 2);
|
||||||
|
ctx.stroke();
|
||||||
|
} else {
|
||||||
|
// Waveform only (default)
|
||||||
if (liveDisplayMode === 'all') {
|
if (liveDisplayMode === 'all') {
|
||||||
// XY / Lissajous mode - treat odd/even samples as L/R
|
// XY / Lissajous mode - treat odd/even samples as L/R
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
@ -142,16 +226,12 @@ export function OscilloscopeDisplay({
|
|||||||
for (let i = 0; i < liveData.length - 1; i += 2) {
|
for (let i = 0; i < liveData.length - 1; i += 2) {
|
||||||
const x = centerX + liveData[i] * scale;
|
const x = centerX + liveData[i] * scale;
|
||||||
const y = centerY - liveData[i + 1] * scale;
|
const y = centerY - liveData[i + 1] * scale;
|
||||||
|
if (i === 0) ctx.moveTo(x, y);
|
||||||
if (i === 0) {
|
else ctx.lineTo(x, y);
|
||||||
ctx.moveTo(x, y);
|
|
||||||
} else {
|
|
||||||
ctx.lineTo(x, y);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
} else {
|
} else {
|
||||||
// Combined waveform mode (default)
|
// Combined waveform mode
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
const sliceWidth = WIDTH / liveData.length;
|
const sliceWidth = WIDTH / liveData.length;
|
||||||
let x = 0;
|
let x = 0;
|
||||||
@ -159,17 +239,13 @@ export function OscilloscopeDisplay({
|
|||||||
for (let i = 0; i < liveData.length; i++) {
|
for (let i = 0; i < liveData.length; i++) {
|
||||||
const v = liveData[i];
|
const v = liveData[i];
|
||||||
const y = (v * HEIGHT) / 2 + HEIGHT / 2;
|
const y = (v * HEIGHT) / 2 + HEIGHT / 2;
|
||||||
|
if (i === 0) ctx.moveTo(x, y);
|
||||||
if (i === 0) {
|
else ctx.lineTo(x, y);
|
||||||
ctx.moveTo(x, y);
|
|
||||||
} else {
|
|
||||||
ctx.lineTo(x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
x += sliceWidth;
|
x += sliceWidth;
|
||||||
}
|
}
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ctx.shadowBlur = 0;
|
ctx.shadowBlur = 0;
|
||||||
|
|
||||||
@ -346,7 +422,7 @@ export function OscilloscopeDisplay({
|
|||||||
}
|
}
|
||||||
|
|
||||||
animationRef.current = requestAnimationFrame(drawFrame);
|
animationRef.current = requestAnimationFrame(drawFrame);
|
||||||
}, [audioData, micAnalyzer, liveAnalyzer, mode, drawGraticule, onPlaybackEnd, isPlaying, playbackSpeed, isLooping, lineThickness, glowIntensity, liveDisplayMode, audioElementRef]);
|
}, [audioData, micAnalyzer, liveAnalyzer, mode, drawGraticule, drawSpectrum, onPlaybackEnd, isPlaying, playbackSpeed, isLooping, lineThickness, glowIntensity, liveDisplayMode, visualizationMode, audioElementRef]);
|
||||||
|
|
||||||
// Initialize canvas
|
// Initialize canvas
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -61,41 +61,29 @@ export const useOfflineVideoExport = () => {
|
|||||||
|
|
||||||
setState(prev => ({ ...prev, stage: 'rendering', progress: 5 }));
|
setState(prev => ({ ...prev, stage: 'rendering', progress: 5 }));
|
||||||
|
|
||||||
// Load intro video
|
// Load intro video - always use webm
|
||||||
console.log('📹 Loading intro video...');
|
console.log('📹 Loading intro video...');
|
||||||
const introVideo = document.createElement('video');
|
const introVideo = document.createElement('video');
|
||||||
introVideo.muted = true;
|
introVideo.muted = true;
|
||||||
introVideo.playsInline = true;
|
introVideo.playsInline = true;
|
||||||
|
introVideo.preload = 'auto';
|
||||||
// Try webm first, fallback to mp4
|
|
||||||
let introLoaded = false;
|
|
||||||
let introDuration = 0;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
|
||||||
introVideo.onloadedmetadata = () => {
|
|
||||||
introDuration = introVideo.duration;
|
|
||||||
console.log(`✅ Intro video loaded: ${introDuration.toFixed(2)}s`);
|
|
||||||
introLoaded = true;
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
introVideo.onerror = () => {
|
|
||||||
console.warn('⚠️ Could not load intro.webm, trying intro.mp4');
|
|
||||||
introVideo.src = '/intro.mp4';
|
|
||||||
};
|
|
||||||
introVideo.src = '/intro.webm';
|
introVideo.src = '/intro.webm';
|
||||||
|
|
||||||
// Timeout after 5 seconds
|
let introDuration = 0;
|
||||||
setTimeout(() => {
|
|
||||||
if (!introLoaded) {
|
// Wait for video to be fully loaded
|
||||||
console.warn('⚠️ Intro video loading timed out');
|
await new Promise<void>((resolve) => {
|
||||||
|
introVideo.onloadeddata = () => {
|
||||||
|
introDuration = introVideo.duration;
|
||||||
|
console.log(`✅ Intro video loaded: ${introDuration.toFixed(2)}s, ${introVideo.videoWidth}x${introVideo.videoHeight}`);
|
||||||
resolve();
|
resolve();
|
||||||
}
|
};
|
||||||
}, 5000);
|
introVideo.onerror = (e) => {
|
||||||
|
console.error('❌ Failed to load intro video:', e);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
introVideo.load();
|
||||||
});
|
});
|
||||||
} catch (introError) {
|
|
||||||
console.warn('⚠️ Could not load intro video:', introError);
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(prev => ({ ...prev, progress: 10 }));
|
setState(prev => ({ ...prev, progress: 10 }));
|
||||||
|
|
||||||
@ -215,7 +203,7 @@ export const useOfflineVideoExport = () => {
|
|||||||
recorder.start(1000); // 1 second chunks
|
recorder.start(1000); // 1 second chunks
|
||||||
|
|
||||||
// Calculate total frames including intro
|
// Calculate total frames including intro
|
||||||
const introFrames = introLoaded ? Math.ceil(introDuration * fps) : 0;
|
const introFrames = introDuration > 0 ? Math.ceil(introDuration * fps) : 0;
|
||||||
const mainFrames = Math.ceil(duration * fps);
|
const mainFrames = Math.ceil(duration * fps);
|
||||||
const fadeFrames = Math.ceil(fps * 0.5); // 0.5 second fade
|
const fadeFrames = Math.ceil(fps * 0.5); // 0.5 second fade
|
||||||
const totalFrames = introFrames + mainFrames;
|
const totalFrames = introFrames + mainFrames;
|
||||||
@ -223,22 +211,31 @@ export const useOfflineVideoExport = () => {
|
|||||||
|
|
||||||
console.log(`🎬 Total frames: ${totalFrames} (intro: ${introFrames}, main: ${mainFrames}, fade: ${fadeFrames})`);
|
console.log(`🎬 Total frames: ${totalFrames} (intro: ${introFrames}, main: ${mainFrames}, fade: ${fadeFrames})`);
|
||||||
|
|
||||||
// Render intro frames first (if loaded)
|
// Render intro frames first
|
||||||
if (introLoaded && introFrames > 0) {
|
if (introFrames > 0) {
|
||||||
console.log('📹 Rendering intro frames...');
|
console.log('📹 Rendering intro frames...');
|
||||||
introVideo.currentTime = 0;
|
|
||||||
await introVideo.play();
|
|
||||||
|
|
||||||
for (let frameIndex = 0; frameIndex < introFrames; frameIndex++) {
|
for (let frameIndex = 0; frameIndex < introFrames; frameIndex++) {
|
||||||
if (cancelledRef.current) {
|
if (cancelledRef.current) {
|
||||||
introVideo.pause();
|
|
||||||
recorder.stop();
|
recorder.stop();
|
||||||
setState({ isExporting: false, progress: 0, error: 'Cancelled', stage: 'idle', fps: 0 });
|
setState({ isExporting: false, progress: 0, error: 'Cancelled', stage: 'idle', fps: 0 });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seek to correct time
|
// Seek to correct time and wait for frame
|
||||||
introVideo.currentTime = frameIndex / fps;
|
const targetTime = frameIndex / fps;
|
||||||
|
introVideo.currentTime = targetTime;
|
||||||
|
|
||||||
|
// Wait for the seek to complete
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
const onSeeked = () => {
|
||||||
|
introVideo.removeEventListener('seeked', onSeeked);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
introVideo.addEventListener('seeked', onSeeked);
|
||||||
|
// Fallback timeout
|
||||||
|
setTimeout(resolve, 50);
|
||||||
|
});
|
||||||
|
|
||||||
// Draw intro video frame scaled to canvas
|
// Draw intro video frame scaled to canvas
|
||||||
ctx.fillStyle = '#0a0f0a';
|
ctx.fillStyle = '#0a0f0a';
|
||||||
@ -268,7 +265,6 @@ export const useOfflineVideoExport = () => {
|
|||||||
await new Promise(resolve => setTimeout(resolve, 1000 / fps));
|
await new Promise(resolve => setTimeout(resolve, 1000 / fps));
|
||||||
}
|
}
|
||||||
|
|
||||||
introVideo.pause();
|
|
||||||
console.log('✅ Intro frames complete');
|
console.log('✅ Intro frames complete');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -338,7 +334,7 @@ export const useOfflineVideoExport = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply fade-in effect from intro (first fadeFrames of main content)
|
// Apply fade-in effect from intro (first fadeFrames of main content)
|
||||||
if (introLoaded && frameIndex < fadeFrames) {
|
if (introDuration > 0 && frameIndex < fadeFrames) {
|
||||||
const fadeProgress = frameIndex / fadeFrames;
|
const fadeProgress = frameIndex / fadeFrames;
|
||||||
// Draw a semi-transparent black overlay that fades out
|
// Draw a semi-transparent black overlay that fades out
|
||||||
ctx.fillStyle = `rgba(10, 15, 10, ${1 - fadeProgress})`;
|
ctx.fillStyle = `rgba(10, 15, 10, ${1 - fadeProgress})`;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user