This commit is contained in:
gpt-engineer-app[bot] 2025-12-21 14:09:40 +00:00
parent cdc0f6d45e
commit d4f544168d
2 changed files with 84 additions and 57 deletions

View File

@ -3,6 +3,11 @@ import { useNavigate, useLocation } from 'react-router-dom';
import { useSettings } from '@/contexts/SettingsContext'; import { useSettings } from '@/contexts/SettingsContext';
import { useAudioAnalyzer } from '@/contexts/AudioAnalyzerContext'; import { useAudioAnalyzer } from '@/contexts/AudioAnalyzerContext';
// Get CSS variable value
function getCSSVar(name: string): string {
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
}
export function MiniOscilloscope() { export function MiniOscilloscope() {
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const animationRef = useRef<number>(); const animationRef = useRef<number>();
@ -23,15 +28,23 @@ export function MiniOscilloscope() {
const width = canvas.width; const width = canvas.width;
const height = canvas.height; 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 // Clear with transparent background
ctx.clearRect(0, 0, width, height); ctx.clearRect(0, 0, width, height);
// Draw background // Draw background with theme color
ctx.fillStyle = 'rgba(0, 0, 0, 0.6)'; ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, width, height); ctx.fillRect(0, 0, width, height);
// Draw grid lines // Draw grid lines with theme color
ctx.strokeStyle = 'hsl(120, 100%, 50%, 0.1)'; ctx.strokeStyle = primaryColorFaint;
ctx.lineWidth = 1; ctx.lineWidth = 1;
// Vertical grid lines // Vertical grid lines
@ -42,31 +55,29 @@ export function MiniOscilloscope() {
ctx.stroke(); ctx.stroke();
} }
// Center line // Center line with theme color
ctx.strokeStyle = 'hsl(120, 100%, 50%, 0.3)'; ctx.strokeStyle = primaryColorDim;
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(0, height / 2); ctx.moveTo(0, height / 2);
ctx.lineTo(width, height / 2); ctx.lineTo(width, height / 2);
ctx.stroke(); ctx.stroke();
// Draw waveform from analyzer // Draw waveform from analyzer or flat line
let hasAudio = false;
if (analyzerNode) { if (analyzerNode) {
const bufferLength = analyzerNode.frequencyBinCount; const bufferLength = analyzerNode.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength); const dataArray = new Uint8Array(bufferLength);
analyzerNode.getByteTimeDomainData(dataArray); analyzerNode.getByteTimeDomainData(dataArray);
// Check if there's actual audio (not just silence) // 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);
if (hasAudio) { ctx.strokeStyle = primaryColor;
ctx.strokeStyle = 'hsl(120, 100%, 50%)';
ctx.lineWidth = 2; ctx.lineWidth = 2;
ctx.shadowColor = 'hsl(120, 100%, 50%)'; ctx.shadowColor = primaryColor;
ctx.shadowBlur = 10; ctx.shadowBlur = hasAudio ? 10 : 0;
ctx.beginPath(); ctx.beginPath();
if (hasAudio) {
const sliceWidth = width / bufferLength; const sliceWidth = width / bufferLength;
let x = 0; let x = 0;
@ -82,28 +93,21 @@ export function MiniOscilloscope() {
x += sliceWidth; x += sliceWidth;
} }
} else {
// Flat line when no audio
ctx.moveTo(0, height / 2);
ctx.lineTo(width, height / 2); ctx.lineTo(width, height / 2);
}
ctx.stroke(); ctx.stroke();
ctx.shadowBlur = 0; 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.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 { } else {
ctx.lineTo(i, y); // Flat line when no analyzer
} ctx.strokeStyle = primaryColor;
} ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(0, height / 2);
ctx.lineTo(width, height / 2);
ctx.stroke(); ctx.stroke();
} }
@ -153,7 +157,7 @@ export function MiniOscilloscope() {
return ( return (
<div <div
onClick={handleClick} onClick={handleClick}
className="fixed bottom-4 left-1/2 -translate-x-1/2 w-[350px] md:w-[500px] h-[70px] z-50 cursor-pointer group" className="fixed bottom-6 left-1/2 -translate-x-1/2 w-[400px] md:w-[600px] h-[80px] z-50 cursor-pointer group"
title="Open Oscilloscope" title="Open Oscilloscope"
> >
<div className="relative w-full h-full rounded-lg border border-primary/50 overflow-hidden bg-background/80 backdrop-blur-sm transition-all duration-300 group-hover:border-primary group-hover:shadow-[0_0_20px_hsl(var(--primary)/0.4)]"> <div className="relative w-full h-full rounded-lg border border-primary/50 overflow-hidden bg-background/80 backdrop-blur-sm transition-all duration-300 group-hover:border-primary group-hover:shadow-[0_0_20px_hsl(var(--primary)/0.4)]">

View File

@ -1,6 +1,7 @@
import { useEffect, useRef, useCallback } from 'react'; 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';
interface OscilloscopeDisplayProps { interface OscilloscopeDisplayProps {
audioData: AudioData | null; audioData: AudioData | null;
@ -33,6 +34,10 @@ export function OscilloscopeDisplay({
const animationRef = useRef<number | null>(null); const animationRef = useRef<number | null>(null);
const currentSampleRef = useRef(0); const currentSampleRef = useRef(0);
const lastSeekPositionRef = 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) => { const drawGraticule = useCallback((ctx: CanvasRenderingContext2D) => {
ctx.strokeStyle = '#00ff00'; ctx.strokeStyle = '#00ff00';
@ -52,7 +57,11 @@ export function OscilloscopeDisplay({
}, []); }, []);
const drawFrame = useCallback(() => { 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 canvas = canvasRef.current;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
@ -63,23 +72,33 @@ export function OscilloscopeDisplay({
let endSample: number; let endSample: number;
let samplesToAdvance: number = samplesPerFrame; let samplesToAdvance: number = samplesPerFrame;
if (micAnalyzer) { // Priority: micAnalyzer > liveAnalyzer (shared) > audioData (file)
// Real-time microphone data const activeAnalyzer = micAnalyzer || liveAnalyzer;
const bufferLength = micAnalyzer.frequencyBinCount;
if (activeAnalyzer && !audioData) {
// Real-time audio data (mic or music player)
const bufferLength = activeAnalyzer.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength); 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 // 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++) { 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; startSample = 0;
endSample = micData.length; endSample = liveData.length;
// Draw mic data directly // Draw live data directly
ctx.strokeStyle = '#00ff00'; ctx.strokeStyle = '#00ff00';
ctx.lineWidth = 2; ctx.lineWidth = 2;
ctx.beginPath(); ctx.beginPath();
@ -88,7 +107,7 @@ export function OscilloscopeDisplay({
let x = 0; let x = 0;
for (let i = 0; i < samplesPerFrame; i++) { for (let i = 0; i < samplesPerFrame; i++) {
const v = micData[i]; const v = liveData[i];
const y = (v * HEIGHT) / 2 + HEIGHT / 2; const y = (v * HEIGHT) / 2 + HEIGHT / 2;
if (i === 0) { if (i === 0) {
@ -102,16 +121,14 @@ export function OscilloscopeDisplay({
ctx.stroke(); ctx.stroke();
// Draw graticule
drawGraticule(ctx);
// Request next frame for real-time // Request next frame for real-time
if (isPlaying) {
animationRef.current = requestAnimationFrame(drawFrame); animationRef.current = requestAnimationFrame(drawFrame);
}
return; return;
} }
// File playback mode - need audioData
if (!audioData) return;
// File playback mode // File playback mode
const baseSamplesPerFrame = Math.floor(audioData.sampleRate / FPS); const baseSamplesPerFrame = Math.floor(audioData.sampleRate / FPS);
samplesPerFrame = Math.floor(baseSamplesPerFrame * playbackSpeed); samplesPerFrame = Math.floor(baseSamplesPerFrame * playbackSpeed);
@ -274,7 +291,7 @@ export function OscilloscopeDisplay({
} }
animationRef.current = requestAnimationFrame(drawFrame); 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 // Initialize canvas
useEffect(() => { useEffect(() => {
@ -288,11 +305,17 @@ export function OscilloscopeDisplay({
} }
}, [drawGraticule]); }, [drawGraticule]);
// Handle playback // Handle playback - start animation for file playback or live audio
useEffect(() => { useEffect(() => {
const hasLiveSource = liveAnalyzer || micAnalyzer;
if (isPlaying && audioData) { if (isPlaying && audioData) {
// File playback
currentSampleRef.current = 0; currentSampleRef.current = 0;
animationRef.current = requestAnimationFrame(drawFrame); animationRef.current = requestAnimationFrame(drawFrame);
} else if (hasLiveSource && !audioData) {
// Live audio visualization (music player, sound effects)
animationRef.current = requestAnimationFrame(drawFrame);
} else { } else {
if (animationRef.current) { if (animationRef.current) {
cancelAnimationFrame(animationRef.current); cancelAnimationFrame(animationRef.current);
@ -304,7 +327,7 @@ export function OscilloscopeDisplay({
cancelAnimationFrame(animationRef.current); cancelAnimationFrame(animationRef.current);
} }
}; };
}, [isPlaying, audioData, drawFrame]); }, [isPlaying, audioData, liveAnalyzer, micAnalyzer, drawFrame]);
const getModeLabel = () => { const getModeLabel = () => {
switch (mode) { switch (mode) {
@ -338,8 +361,8 @@ export function OscilloscopeDisplay({
{getModeLabel()} {getModeLabel()}
</div> </div>
{/* Idle state */} {/* Idle state - only show if no live audio and no file */}
{!audioData && !isPlaying && ( {!audioData && !liveAnalyzer && !micAnalyzer && (
<div className="absolute inset-0 flex items-center justify-center"> <div className="absolute inset-0 flex items-center justify-center">
<p className="font-crt text-2xl text-primary/40 text-glow animate-pulse"> <p className="font-crt text-2xl text-primary/40 text-glow animate-pulse">
NO SIGNAL NO SIGNAL