diff --git a/src/components/MiniOscilloscope.tsx b/src/components/MiniOscilloscope.tsx index 994f3bd..2333b5c 100644 --- a/src/components/MiniOscilloscope.tsx +++ b/src/components/MiniOscilloscope.tsx @@ -3,6 +3,11 @@ import { useNavigate, useLocation } from 'react-router-dom'; import { useSettings } from '@/contexts/SettingsContext'; import { useAudioAnalyzer } from '@/contexts/AudioAnalyzerContext'; +// Get CSS variable value +function getCSSVar(name: string): string { + return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); +} + export function MiniOscilloscope() { const canvasRef = useRef(null); const animationRef = useRef(); @@ -23,15 +28,23 @@ export function MiniOscilloscope() { const width = canvas.width; const height = canvas.height; + // Get theme colors + const primaryHsl = getCSSVar('--primary'); + const primaryColor = primaryHsl ? `hsl(${primaryHsl})` : 'hsl(120, 100%, 50%)'; + const primaryColorDim = primaryHsl ? `hsl(${primaryHsl} / 0.3)` : 'hsl(120, 100%, 50%, 0.3)'; + const primaryColorFaint = primaryHsl ? `hsl(${primaryHsl} / 0.1)` : 'hsl(120, 100%, 50%, 0.1)'; + const bgHsl = getCSSVar('--background'); + const bgColor = bgHsl ? `hsl(${bgHsl} / 0.8)` : 'rgba(0, 0, 0, 0.6)'; + // Clear with transparent background ctx.clearRect(0, 0, width, height); - // Draw background - ctx.fillStyle = 'rgba(0, 0, 0, 0.6)'; + // Draw background with theme color + ctx.fillStyle = bgColor; ctx.fillRect(0, 0, width, height); - // Draw grid lines - ctx.strokeStyle = 'hsl(120, 100%, 50%, 0.1)'; + // Draw grid lines with theme color + ctx.strokeStyle = primaryColorFaint; ctx.lineWidth = 1; // Vertical grid lines @@ -42,31 +55,29 @@ export function MiniOscilloscope() { ctx.stroke(); } - // Center line - ctx.strokeStyle = 'hsl(120, 100%, 50%, 0.3)'; + // Center line with theme color + ctx.strokeStyle = primaryColorDim; ctx.beginPath(); ctx.moveTo(0, height / 2); ctx.lineTo(width, height / 2); ctx.stroke(); - // Draw waveform from analyzer - let hasAudio = false; - + // Draw waveform from analyzer or flat line if (analyzerNode) { const bufferLength = analyzerNode.frequencyBinCount; const dataArray = new Uint8Array(bufferLength); analyzerNode.getByteTimeDomainData(dataArray); // Check if there's actual audio (not just silence) - hasAudio = dataArray.some(v => Math.abs(v - 128) > 2); + const hasAudio = dataArray.some(v => Math.abs(v - 128) > 2); + + ctx.strokeStyle = primaryColor; + ctx.lineWidth = 2; + ctx.shadowColor = primaryColor; + ctx.shadowBlur = hasAudio ? 10 : 0; + ctx.beginPath(); if (hasAudio) { - ctx.strokeStyle = 'hsl(120, 100%, 50%)'; - ctx.lineWidth = 2; - ctx.shadowColor = 'hsl(120, 100%, 50%)'; - ctx.shadowBlur = 10; - ctx.beginPath(); - const sliceWidth = width / bufferLength; let x = 0; @@ -82,28 +93,21 @@ export function MiniOscilloscope() { x += sliceWidth; } - + } else { + // Flat line when no audio + ctx.moveTo(0, height / 2); ctx.lineTo(width, height / 2); - ctx.stroke(); - ctx.shadowBlur = 0; } - } - // Draw idle animation when no audio - if (!hasAudio) { - const time = Date.now() / 1000; - ctx.strokeStyle = 'hsl(120, 100%, 50%, 0.5)'; - ctx.lineWidth = 1; + ctx.stroke(); + ctx.shadowBlur = 0; + } else { + // Flat line when no analyzer + ctx.strokeStyle = primaryColor; + ctx.lineWidth = 2; ctx.beginPath(); - - for (let i = 0; i < width; i++) { - const y = height / 2 + Math.sin(i * 0.05 + time * 2) * 3; - if (i === 0) { - ctx.moveTo(i, y); - } else { - ctx.lineTo(i, y); - } - } + ctx.moveTo(0, height / 2); + ctx.lineTo(width, height / 2); ctx.stroke(); } @@ -153,7 +157,7 @@ export function MiniOscilloscope() { return (
diff --git a/src/components/OscilloscopeDisplay.tsx b/src/components/OscilloscopeDisplay.tsx index 21caa2f..92323ea 100755 --- a/src/components/OscilloscopeDisplay.tsx +++ b/src/components/OscilloscopeDisplay.tsx @@ -1,6 +1,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'; interface OscilloscopeDisplayProps { audioData: AudioData | null; @@ -33,6 +34,10 @@ export function OscilloscopeDisplay({ const animationRef = useRef(null); const currentSampleRef = useRef(0); const lastSeekPositionRef = useRef(0); + const { analyzerNode: sharedAnalyzer } = useSharedAudioAnalyzer(); + + // Use shared analyzer for live audio (music player, sound effects) + const liveAnalyzer = sharedAnalyzer || micAnalyzer; const drawGraticule = useCallback((ctx: CanvasRenderingContext2D) => { ctx.strokeStyle = '#00ff00'; @@ -52,7 +57,11 @@ export function OscilloscopeDisplay({ }, []); const drawFrame = useCallback(() => { - if ((!audioData && !micAnalyzer) || !canvasRef.current) return; + if (!canvasRef.current) return; + + // Always allow drawing if we have live analyzer, even without audioData + const hasLiveSource = liveAnalyzer || micAnalyzer; + if (!audioData && !hasLiveSource) return; const canvas = canvasRef.current; const ctx = canvas.getContext('2d'); @@ -63,23 +72,33 @@ export function OscilloscopeDisplay({ let endSample: number; let samplesToAdvance: number = samplesPerFrame; - if (micAnalyzer) { - // Real-time microphone data - const bufferLength = micAnalyzer.frequencyBinCount; + // Priority: micAnalyzer > liveAnalyzer (shared) > audioData (file) + const activeAnalyzer = micAnalyzer || liveAnalyzer; + + if (activeAnalyzer && !audioData) { + // Real-time audio data (mic or music player) + const bufferLength = activeAnalyzer.frequencyBinCount; const dataArray = new Uint8Array(bufferLength); - micAnalyzer.getByteTimeDomainData(dataArray); + activeAnalyzer.getByteTimeDomainData(dataArray); + + // Clear to pure black + ctx.fillStyle = '#000000'; + ctx.fillRect(0, 0, WIDTH, HEIGHT); + + // Draw graticule first + drawGraticule(ctx); // Convert to Float32Array-like for consistency - const micData = new Float32Array(dataArray.length); + const liveData = new Float32Array(dataArray.length); for (let i = 0; i < dataArray.length; i++) { - micData[i] = (dataArray[i] - 128) / 128; // Normalize to -1 to 1 + liveData[i] = (dataArray[i] - 128) / 128; // Normalize to -1 to 1 } - samplesPerFrame = micData.length; + samplesPerFrame = liveData.length; startSample = 0; - endSample = micData.length; + endSample = liveData.length; - // Draw mic data directly + // Draw live data directly ctx.strokeStyle = '#00ff00'; ctx.lineWidth = 2; ctx.beginPath(); @@ -88,7 +107,7 @@ export function OscilloscopeDisplay({ let x = 0; for (let i = 0; i < samplesPerFrame; i++) { - const v = micData[i]; + const v = liveData[i]; const y = (v * HEIGHT) / 2 + HEIGHT / 2; if (i === 0) { @@ -102,15 +121,13 @@ export function OscilloscopeDisplay({ ctx.stroke(); - // Draw graticule - drawGraticule(ctx); - // Request next frame for real-time - if (isPlaying) { - animationRef.current = requestAnimationFrame(drawFrame); - } + animationRef.current = requestAnimationFrame(drawFrame); return; } + + // File playback mode - need audioData + if (!audioData) return; // File playback mode const baseSamplesPerFrame = Math.floor(audioData.sampleRate / FPS); @@ -274,7 +291,7 @@ export function OscilloscopeDisplay({ } animationRef.current = requestAnimationFrame(drawFrame); - }, [audioData, micAnalyzer, mode, drawGraticule, onPlaybackEnd, isPlaying, playbackSpeed, isLooping, seekPosition]); + }, [audioData, micAnalyzer, liveAnalyzer, mode, drawGraticule, onPlaybackEnd, isPlaying, playbackSpeed, isLooping, seekPosition]); // Initialize canvas useEffect(() => { @@ -288,11 +305,17 @@ export function OscilloscopeDisplay({ } }, [drawGraticule]); - // Handle playback + // Handle playback - start animation for file playback or live audio useEffect(() => { + const hasLiveSource = liveAnalyzer || micAnalyzer; + if (isPlaying && audioData) { + // File playback currentSampleRef.current = 0; animationRef.current = requestAnimationFrame(drawFrame); + } else if (hasLiveSource && !audioData) { + // Live audio visualization (music player, sound effects) + animationRef.current = requestAnimationFrame(drawFrame); } else { if (animationRef.current) { cancelAnimationFrame(animationRef.current); @@ -304,7 +327,7 @@ export function OscilloscopeDisplay({ cancelAnimationFrame(animationRef.current); } }; - }, [isPlaying, audioData, drawFrame]); + }, [isPlaying, audioData, liveAnalyzer, micAnalyzer, drawFrame]); const getModeLabel = () => { switch (mode) { @@ -338,8 +361,8 @@ export function OscilloscopeDisplay({ {getModeLabel()}
- {/* Idle state */} - {!audioData && !isPlaying && ( + {/* Idle state - only show if no live audio and no file */} + {!audioData && !liveAnalyzer && !micAnalyzer && (

NO SIGNAL