diff --git a/client/src/pages/Game/Game.jsx b/client/src/pages/Game/Game.jsx index 548bb0c..74c3a77 100644 --- a/client/src/pages/Game/Game.jsx +++ b/client/src/pages/Game/Game.jsx @@ -48,6 +48,7 @@ export const Game = () => { const [allSongs, setAllSongs] = useState([]); const [songsLoading, setSongsLoading] = useState(false); + const [composerIsPlaying, setComposerIsPlaying] = useState(false); useEffect(() => { if (!connected) return; @@ -129,6 +130,7 @@ export const Game = () => { if (phase === "composing") { console.log("Received frequency update:", data.frequency); setFrequency(data.frequency); + setComposerIsPlaying(data.isPlaying); // Make sure isPlaying is handled } }, "phase-changed": (data) => { @@ -223,10 +225,11 @@ export const Game = () => { } }, [allSongs, songOptions]); - const handleFrequencyChange = useCallback((newFrequency) => { + const handleFrequencyChange = useCallback((newFrequency, isPlaying) => { setFrequency(newFrequency); + setComposerIsPlaying(isPlaying); if (role === "composer") { - send("submit-frequency", { frequency: newFrequency }); + send("submit-frequency", { frequency: newFrequency, isPlaying }); } }, [role, send]); @@ -267,22 +270,10 @@ export const Game = () => { setTimeLeft(0); }, [send]); - const handlePlayerReady = useCallback((player) => { + const handlePlayerReady = useCallback(() => { console.log("Player ready"); }, []); - const togglePlayback = useCallback(() => { - const audioElement = document.querySelector('audio'); - - if (audioElement) { - if (audioElement.paused) { - audioElement.play().catch(err => console.error("Play error:", err)); - } else { - audioElement.pause(); - } - } - }, []); - const renderPhaseContent = () => { switch (phase) { case "waiting": @@ -543,6 +534,7 @@ export const Game = () => { isReadOnly={role !== "composer"} onFrequencyChange={handleFrequencyChange} frequency={frequency} + composerIsPlaying={composerIsPlaying} /> )} diff --git a/client/src/pages/Game/components/MusicSlider/MusicSlider.jsx b/client/src/pages/Game/components/MusicSlider/MusicSlider.jsx index 802381a..63045de 100644 --- a/client/src/pages/Game/components/MusicSlider/MusicSlider.jsx +++ b/client/src/pages/Game/components/MusicSlider/MusicSlider.jsx @@ -1,224 +1,217 @@ -import {useEffect, useRef, useState} from "react"; +import { useState, useEffect, useRef, useCallback } from 'react'; import "./styles.sass"; -export const MusicSlider = ({ isReadOnly = false, onFrequencyChange, frequency: externalFrequency }) => { - const [frequency, setFrequency] = useState(externalFrequency || 440); - const [isPlaying, setIsPlaying] = useState(false); - const [dragging, setDragging] = useState(false); - const audioContextRef = useRef(null); - const oscillatorRef = useRef(null); - const gainNodeRef = useRef(null); - const sliderRef = useRef(null); - const hasInteractedRef = useRef(false); +export const MusicSlider = ({ isReadOnly, onFrequencyChange, frequency: externalFrequency, composerIsPlaying }) => { + const [audioContext, setAudioContext] = useState(null); + const [oscillator, setOscillator] = useState(null); + const [gainNode, setGainNode] = useState(null); + const sliderRef = useRef(null); + const frequency = useRef(externalFrequency || 440); + const isPressed = useRef(false); + const isDragging = useRef(false); - useEffect(() => { - const initAudioContext = () => { - if (!audioContextRef.current && !hasInteractedRef.current) { - hasInteractedRef.current = true; - audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)(); - if (!oscillatorRef.current) { - startAudio(); - } - document.removeEventListener('click', initAudioContext); - document.removeEventListener('touchstart', initAudioContext); - document.removeEventListener('keydown', initAudioContext); - } - }; + const initAudio = useCallback(() => { + if (audioContext) return; - document.addEventListener('click', initAudioContext); - document.addEventListener('touchstart', initAudioContext); - document.addEventListener('keydown', initAudioContext); + try { + const ctx = new (window.AudioContext || window.webkitAudioContext)(); + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); - return () => { - document.removeEventListener('click', initAudioContext); - document.removeEventListener('touchstart', initAudioContext); - document.removeEventListener('keydown', initAudioContext); - }; - }, []); + osc.type = 'sine'; + osc.frequency.setValueAtTime(frequency.current, ctx.currentTime); - useEffect(() => { - if (externalFrequency !== undefined && !dragging) { - setFrequency(externalFrequency); + gain.gain.setValueAtTime(0.00001, ctx.currentTime); - if (audioContextRef.current) { - if (!isPlaying) { - startAudio(); - } else if (oscillatorRef.current) { - oscillatorRef.current.frequency.setValueAtTime( - externalFrequency, - audioContextRef.current.currentTime - ); - } - } - } - }, [externalFrequency, dragging, isPlaying]); + osc.connect(gain); + gain.connect(ctx.destination); + osc.start(); + + setAudioContext(ctx); + setOscillator(osc); + setGainNode(gain); + + if (isReadOnly) { + gain.gain.setValueAtTime(0.00001, ctx.currentTime); + } + } catch (error) { + console.error("Audio initialization error:", error); + } + }, [audioContext, isReadOnly]); + + useEffect(() => { + if (!isReadOnly || !externalFrequency) return; + + // Initialize audio if not already done + if (!audioContext) { + initAudio(); + return; + } + + if (oscillator && gainNode) { + frequency.current = externalFrequency; + oscillator.frequency.setValueAtTime(frequency.current, audioContext.currentTime); + + if (composerIsPlaying) { + gainNode.gain.setTargetAtTime(0.5, audioContext.currentTime, 0.01); + } else { + gainNode.gain.setTargetAtTime(0.00001, audioContext.currentTime, 0.05); + } + } + }, [externalFrequency, composerIsPlaying, oscillator, audioContext, gainNode, isReadOnly, initAudio]); + + useEffect(() => { + if (isReadOnly) return; + + const calculateFrequency = (clientX) => { + const slider = sliderRef.current; + const rect = slider.getBoundingClientRect(); + const width = rect.width; + + // Calculate relative X position using window coordinates + let x = clientX - rect.left; + + // For positions outside the slider, calculate relative to slider bounds + if (clientX < rect.left) x = 0; + if (clientX > rect.right) x = width; + + const percentage = x / width; + return Math.round(220 + percentage * 880); + }; + + const handleMouseMove = (e) => { + if (!isPressed.current) return; + + e.preventDefault(); + e.stopPropagation(); + + const newFreq = calculateFrequency(e.clientX); + frequency.current = newFreq; + onFrequencyChange(newFreq, true); + + if (oscillator && gainNode) { + oscillator.frequency.setValueAtTime(newFreq, audioContext.currentTime); + gainNode.gain.setTargetAtTime(0.5, audioContext.currentTime, 0.01); + } + }; const handleMouseDown = (e) => { - if (isReadOnly) return; - - setDragging(true); - handleFrequencyChange(e); + const target = e.target; + const isSlider = target.classList.contains('otamatone-neck'); + const isIndicator = target.classList.contains('frequency-indicator') || + target.closest('.frequency-indicator'); + + if (!isSlider && !isIndicator) return; - if (!audioContextRef.current) { - audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)(); - } + e.preventDefault(); + e.stopPropagation(); - if (!isPlaying) { - startAudio(); - } + if (!audioContext) initAudio(); + isPressed.current = true; + isDragging.current = true; + + if (gainNode) { + gainNode.gain.setTargetAtTime(0.5, audioContext.currentTime, 0.01); + } + + document.body.style.userSelect = 'none'; + const iframes = document.querySelectorAll('iframe'); + iframes.forEach(iframe => { + iframe.style.pointerEvents = 'none'; + }); + + onFrequencyChange(frequency.current, true); + handleMouseMove(e); }; - const handleMouseUp = () => { - setDragging(false); + const handleMouseUp = (e) => { + if (!isPressed.current) return; + + e.preventDefault(); + e.stopPropagation(); + + isPressed.current = false; + isDragging.current = false; + + if (gainNode) { + gainNode.gain.setTargetAtTime(0.00001, audioContext.currentTime, 0.05); + } + + document.body.style.userSelect = ''; + const iframes = document.querySelectorAll('iframe'); + iframes.forEach(iframe => { + iframe.style.pointerEvents = 'auto'; + }); + + onFrequencyChange(frequency.current, false); }; - const handleFrequencyChange = (e) => { - if (isReadOnly) return; - - const rect = sliderRef.current.getBoundingClientRect(); - const x = e.clientX - rect.left; - const clampedX = Math.max(0, Math.min(x, rect.width)); - const newFrequency = 20 + (clampedX / rect.width) * 1980; - setFrequency(newFrequency); + document.addEventListener('mousemove', handleMouseMove, true); + document.addEventListener('mouseup', handleMouseUp, true); + + const slider = sliderRef.current; + if (slider) { + slider.addEventListener('mousedown', handleMouseDown, true); + } - if (onFrequencyChange) { - onFrequencyChange(newFrequency); - } + return () => { + document.removeEventListener('mousemove', handleMouseMove, true); + document.removeEventListener('mouseup', handleMouseUp, true); + if (slider) { + slider.removeEventListener('mousedown', handleMouseDown, true); + } + document.body.style.userSelect = ''; + const iframes = document.querySelectorAll('iframe'); + iframes.forEach(iframe => { + iframe.style.pointerEvents = 'auto'; + }); }; + }, [isReadOnly, onFrequencyChange, audioContext, oscillator, gainNode, initAudio]); - useEffect(() => { - const handleMouseMove = (e) => dragging && handleFrequencyChange(e); - const handleMouseUpGlobal = () => dragging && handleMouseUp(); - - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUpGlobal); - - return () => { - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUpGlobal); - }; - }, [dragging]); - - useEffect(() => { - if (isPlaying && oscillatorRef.current && audioContextRef.current) { - oscillatorRef.current.frequency.setValueAtTime( - frequency, - audioContextRef.current.currentTime - ); - } - }, [frequency, isPlaying]); - - const startAudio = () => { - if (!audioContextRef.current) { - try { - audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)(); - } catch (e) { - console.error("AudioContext could not be created:", e); - return; - } - } - - if (audioContextRef.current.state === 'suspended') { - audioContextRef.current.resume().catch(err => { - console.error("Could not resume AudioContext:", err); - }); - } - + useEffect(() => { + return () => { + if (gainNode) gainNode.gain.setValueAtTime(0, audioContext.currentTime); + if (oscillator) { try { - if (!oscillatorRef.current) { - oscillatorRef.current = audioContextRef.current.createOscillator(); - oscillatorRef.current.type = 'sine'; - oscillatorRef.current.frequency.setValueAtTime( - frequency, - audioContextRef.current.currentTime - ); - - gainNodeRef.current = audioContextRef.current.createGain(); - gainNodeRef.current.gain.setValueAtTime(0.5, audioContextRef.current.currentTime); - - oscillatorRef.current.connect(gainNodeRef.current); - gainNodeRef.current.connect(audioContextRef.current.destination); - oscillatorRef.current.start(); - console.log("Audio started successfully"); - } else { - oscillatorRef.current.frequency.setValueAtTime( - frequency, - audioContextRef.current.currentTime - ); - } - setIsPlaying(true); + oscillator.stop(); } catch (e) { - console.error("Error starting audio:", e); + console.warn("Oscillator already stopped"); } + } + if (audioContext) audioContext.close(); }; + }, [oscillator, audioContext, gainNode]); - const stopAudio = () => { - if (oscillatorRef.current) { - try { - oscillatorRef.current.stop(); - oscillatorRef.current.disconnect(); - oscillatorRef.current = null; - setIsPlaying(false); - console.log("Audio stopped"); - } catch (e) { - console.error("Error stopping audio:", e); - } - } - }; + const getFrequencyPosition = () => { + const pos = ((frequency.current - 220) / 880) * 100; + return `${Math.max(0, Math.min(pos, 100))}%`; + }; - useEffect(() => { - return () => { - if (oscillatorRef.current) { - try { - oscillatorRef.current.stop(); - oscillatorRef.current.disconnect(); - } catch (e) { - console.error("Error cleaning up oscillator:", e); - } - } - - if (audioContextRef.current && audioContextRef.current.state !== 'closed') { - try { - audioContextRef.current.close(); - } catch (e) { - console.error("Error closing AudioContext:", e); - } - } - }; - }, []); - - useEffect(() => { - return () => { - if (isPlaying) { - stopAudio(); - } - }; - }, [isPlaying]); - - return ( -