Add isPlaying state to MusicSlider

This commit is contained in:
Mathias Wagner 2025-03-01 15:47:12 +01:00
parent e633a5e004
commit 33a1715adc
5 changed files with 221 additions and 215 deletions

View File

@ -48,6 +48,7 @@ export const Game = () => {
const [allSongs, setAllSongs] = useState([]); const [allSongs, setAllSongs] = useState([]);
const [songsLoading, setSongsLoading] = useState(false); const [songsLoading, setSongsLoading] = useState(false);
const [composerIsPlaying, setComposerIsPlaying] = useState(false);
useEffect(() => { useEffect(() => {
if (!connected) return; if (!connected) return;
@ -129,6 +130,7 @@ export const Game = () => {
if (phase === "composing") { if (phase === "composing") {
console.log("Received frequency update:", data.frequency); console.log("Received frequency update:", data.frequency);
setFrequency(data.frequency); setFrequency(data.frequency);
setComposerIsPlaying(data.isPlaying); // Make sure isPlaying is handled
} }
}, },
"phase-changed": (data) => { "phase-changed": (data) => {
@ -223,10 +225,11 @@ export const Game = () => {
} }
}, [allSongs, songOptions]); }, [allSongs, songOptions]);
const handleFrequencyChange = useCallback((newFrequency) => { const handleFrequencyChange = useCallback((newFrequency, isPlaying) => {
setFrequency(newFrequency); setFrequency(newFrequency);
setComposerIsPlaying(isPlaying);
if (role === "composer") { if (role === "composer") {
send("submit-frequency", { frequency: newFrequency }); send("submit-frequency", { frequency: newFrequency, isPlaying });
} }
}, [role, send]); }, [role, send]);
@ -267,22 +270,10 @@ export const Game = () => {
setTimeLeft(0); setTimeLeft(0);
}, [send]); }, [send]);
const handlePlayerReady = useCallback((player) => { const handlePlayerReady = useCallback(() => {
console.log("Player ready"); 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 = () => { const renderPhaseContent = () => {
switch (phase) { switch (phase) {
case "waiting": case "waiting":
@ -543,6 +534,7 @@ export const Game = () => {
isReadOnly={role !== "composer"} isReadOnly={role !== "composer"}
onFrequencyChange={handleFrequencyChange} onFrequencyChange={handleFrequencyChange}
frequency={frequency} frequency={frequency}
composerIsPlaying={composerIsPlaying}
/> />
)} )}

View File

@ -1,222 +1,215 @@
import {useEffect, useRef, useState} from "react"; import { useState, useEffect, useRef, useCallback } from 'react';
import "./styles.sass"; import "./styles.sass";
export const MusicSlider = ({ isReadOnly = false, onFrequencyChange, frequency: externalFrequency }) => { export const MusicSlider = ({ isReadOnly, onFrequencyChange, frequency: externalFrequency, composerIsPlaying }) => {
const [frequency, setFrequency] = useState(externalFrequency || 440); const [audioContext, setAudioContext] = useState(null);
const [isPlaying, setIsPlaying] = useState(false); const [oscillator, setOscillator] = useState(null);
const [dragging, setDragging] = useState(false); const [gainNode, setGainNode] = useState(null);
const audioContextRef = useRef(null);
const oscillatorRef = useRef(null);
const gainNodeRef = useRef(null);
const sliderRef = useRef(null); const sliderRef = useRef(null);
const hasInteractedRef = useRef(false); const frequency = useRef(externalFrequency || 440);
const isPressed = useRef(false);
const isDragging = useRef(false);
useEffect(() => { const initAudio = useCallback(() => {
const initAudioContext = () => { if (audioContext) return;
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);
}
};
document.addEventListener('click', initAudioContext);
document.addEventListener('touchstart', initAudioContext);
document.addEventListener('keydown', initAudioContext);
return () => {
document.removeEventListener('click', initAudioContext);
document.removeEventListener('touchstart', initAudioContext);
document.removeEventListener('keydown', initAudioContext);
};
}, []);
useEffect(() => {
if (externalFrequency !== undefined && !dragging) {
setFrequency(externalFrequency);
if (audioContextRef.current) {
if (!isPlaying) {
startAudio();
} else if (oscillatorRef.current) {
oscillatorRef.current.frequency.setValueAtTime(
externalFrequency,
audioContextRef.current.currentTime
);
}
}
}
}, [externalFrequency, dragging, isPlaying]);
const handleMouseDown = (e) => {
if (isReadOnly) return;
setDragging(true);
handleFrequencyChange(e);
if (!audioContextRef.current) {
audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)();
}
if (!isPlaying) {
startAudio();
}
};
const handleMouseUp = () => {
setDragging(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);
if (onFrequencyChange) {
onFrequencyChange(newFrequency);
}
};
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 { try {
audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)(); const ctx = new (window.AudioContext || window.webkitAudioContext)();
} catch (e) { const osc = ctx.createOscillator();
console.error("AudioContext could not be created:", e); const gain = ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(frequency.current, ctx.currentTime);
gain.gain.setValueAtTime(0.00001, ctx.currentTime);
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; return;
} }
}
if (audioContextRef.current.state === 'suspended') { if (oscillator && gainNode) {
audioContextRef.current.resume().catch(err => { frequency.current = externalFrequency;
console.error("Could not resume AudioContext:", err); oscillator.frequency.setValueAtTime(frequency.current, audioContext.currentTime);
});
}
try { if (composerIsPlaying) {
if (!oscillatorRef.current) { gainNode.gain.setTargetAtTime(0.5, audioContext.currentTime, 0.01);
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 { } else {
oscillatorRef.current.frequency.setValueAtTime( gainNode.gain.setTargetAtTime(0.00001, audioContext.currentTime, 0.05);
frequency,
audioContextRef.current.currentTime
);
} }
setIsPlaying(true); }
} catch (e) { }, [externalFrequency, composerIsPlaying, oscillator, audioContext, gainNode, isReadOnly, initAudio]);
console.error("Error starting audio:", e);
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 stopAudio = () => { const handleMouseDown = (e) => {
if (oscillatorRef.current) { const target = e.target;
try { const isSlider = target.classList.contains('otamatone-neck');
oscillatorRef.current.stop(); const isIndicator = target.classList.contains('frequency-indicator') ||
oscillatorRef.current.disconnect(); target.closest('.frequency-indicator');
oscillatorRef.current = null;
setIsPlaying(false); if (!isSlider && !isIndicator) return;
console.log("Audio stopped");
} catch (e) { e.preventDefault();
console.error("Error stopping audio:", e); e.stopPropagation();
}
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 = (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);
};
document.addEventListener('mousemove', handleMouseMove, true);
document.addEventListener('mouseup', handleMouseUp, true);
const slider = sliderRef.current;
if (slider) {
slider.addEventListener('mousedown', handleMouseDown, true);
}
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(() => { useEffect(() => {
return () => { return () => {
if (oscillatorRef.current) { if (gainNode) gainNode.gain.setValueAtTime(0, audioContext.currentTime);
if (oscillator) {
try { try {
oscillatorRef.current.stop(); oscillator.stop();
oscillatorRef.current.disconnect();
} catch (e) { } catch (e) {
console.error("Error cleaning up oscillator:", e); console.warn("Oscillator already stopped");
}
}
if (audioContextRef.current && audioContextRef.current.state !== 'closed') {
try {
audioContextRef.current.close();
} catch (e) {
console.error("Error closing AudioContext:", e);
} }
} }
if (audioContext) audioContext.close();
}; };
}, []); }, [oscillator, audioContext, gainNode]);
useEffect(() => { const getFrequencyPosition = () => {
return () => { const pos = ((frequency.current - 220) / 880) * 100;
if (isPlaying) { return `${Math.max(0, Math.min(pos, 100))}%`;
stopAudio();
}
}; };
}, [isPlaying]);
return ( return (
<div className={`otamatone-container ${isReadOnly ? 'read-only' : ''}`}> <div className="otamatone-container">
<div <div className="otamatone">
className="otamatone"
onMouseDown={handleMouseDown}
style={{ cursor: isReadOnly ? 'default' : 'pointer' }}
>
<div className="otamatone-face"> <div className="otamatone-face">
<div className="otamatone-mouth" style={{
height: `${10 + (frequency / 2000) * 40}px`,
width: `${10 + (frequency / 2000) * 40}px`
}}></div>
<div className="otamatone-eyes"> <div className="otamatone-eyes">
<div className="eye left-eye"></div> <div className="eye left-eye"></div>
<div className="eye right-eye"></div> <div className="eye right-eye"></div>
</div> </div>
<div className="otamatone-mouth"></div>
</div>
<div
ref={sliderRef}
className={`otamatone-neck ${!isReadOnly ? 'interactive' : ''}`}
style={{ cursor: isReadOnly ? 'default' : 'pointer' }}
>
<div
className={`frequency-indicator ${isReadOnly ? 'read-only' : ''} ${composerIsPlaying ? 'active' : ''}`}
style={{ left: getFrequencyPosition() }}
>
<div className="note-marker">
{(!isReadOnly && isDragging.current) && Math.round(frequency.current)}
</div>
</div> </div>
<div className="otamatone-neck" ref={sliderRef}>
<div className="frequency-indicator" style={{left: `${(frequency - 20) / 1980 * 100}%`}}></div>
<div className="note-marker" style={{left: '10%', pointerEvents: 'none'}}></div>
<div className="note-marker" style={{left: '50%', pointerEvents: 'none'}}></div>
<div className="note-marker" style={{left: '90%', pointerEvents: 'none'}}></div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -63,6 +63,10 @@
background: rgba(255, 255, 255, 0.2) background: rgba(255, 255, 255, 0.2)
pointer-events: none pointer-events: none
&.interactive
.frequency-indicator:hover
transform: translateX(-50%) scale(1.1)
.frequency-indicator .frequency-indicator
position: absolute position: absolute
top: -20px top: -20px
@ -84,6 +88,19 @@
cursor: grabbing cursor: grabbing
transform: translateX(-50%) scale(0.95) transform: translateX(-50%) scale(0.95)
&.read-only
pointer-events: none
cursor: default
opacity: 0.8
box-shadow: 0 0 15px rgba(255, 107, 179, 0.4)
&.active
opacity: 1
box-shadow: 0 0 30px rgba(255, 107, 179, 0.8)
transform: translateX(-50%) scale(1.1)
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275)
.note-marker .note-marker
position: absolute position: absolute
top: -50px top: -50px

View File

@ -780,6 +780,10 @@
width: 100% width: 100%
height: auto height: auto
aspect-ratio: 16 / 9 aspect-ratio: 16 / 9
transition: pointer-events 0.1s ease
&:hover .song-embedded-player
pointer-events: auto !important
.results-phase .results-phase
display: flex display: flex

View File

@ -221,7 +221,7 @@ module.exports = (io) => (socket) => {
if (currentRoomId) socket.emit("room-code", currentRoomId); if (currentRoomId) socket.emit("room-code", currentRoomId);
}); });
socket.on("submit-frequency", ({ frequency }) => { socket.on("submit-frequency", ({ frequency, isPlaying }) => {
const roomId = roomController.getUserRoom(socket.id); const roomId = roomController.getUserRoom(socket.id);
if (!roomId) return; if (!roomId) return;
@ -229,7 +229,7 @@ module.exports = (io) => (socket) => {
if (userRole !== 'composer') return; if (userRole !== 'composer') return;
if (gameController.updateFrequency(roomId, frequency)) { if (gameController.updateFrequency(roomId, frequency)) {
socket.to(roomId).emit("frequency-update", { frequency }); socket.to(roomId).emit("frequency-update", { frequency, isPlaying });
} }
}); });