Skip to content

Commit

Permalink
useAudioSystem
Browse files Browse the repository at this point in the history
  • Loading branch information
yhattav committed Jan 10, 2025
1 parent 5aabd84 commit e5dd63c
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 203 deletions.
185 changes: 19 additions & 166 deletions src/components/GravitySimulator/GravitySimulator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,9 @@ import {
toSimulatorPath,
} from "../../utils/types/path";
import { PaperCanvas } from "../PaperCanvas/PaperCanvas";
import { AudioManager } from "../../utils/audio/AudioManager";
import { VolumeSettings } from "../../utils/audio/AudioManager";
import { SimulatorRenderer } from "../SimulatorRenderer/SimulatorRenderer";
import { SimulatorControls } from "../SimulatorControls/SimulatorControls";
import { useAudioSystem } from "../../hooks/useAudioSystem";

const generatePastelColor = () => {
const r = Math.floor(Math.random() * 75 + 180);
Expand Down Expand Up @@ -179,175 +178,29 @@ export const GravitySimulator: React.FC<GravitySimulatorProps> = ({
[]
);

// Initialize audio manager with current settings
const [audioManager] = useState(() =>
disableSound
? null
: AudioManager.getInstance({
masterVolume: physicsConfig.MASTER_VOLUME,
ambientVolume: physicsConfig.AMBIENT_VOLUME,
particleVolume: physicsConfig.PARTICLE_VOLUME,
})
);
const [isAudioLoaded, setIsAudioLoaded] = useState(false);
const [isAudioPlaying, setIsAudioPlaying] = useState(false);

// Initialize audio files
useEffect(() => {
if (disableSound || !audioManager) return;

const initAudio = async () => {
await audioManager.initialize(audioFiles);
};
initAudio();

return () => {
audioManager.cleanup();
};
}, [audioManager, disableSound, audioFiles]);

// Handle first interaction
useEffect(() => {
if (disableSound || !audioManager) return;

if (firstInteractionDetected) {
audioManager.notifyFirstInteraction();
if (audioManager.getIsLoaded()) {
setIsAudioLoaded(audioManager.getIsLoaded());
audioManager.play();
setIsAudioPlaying(true);
}
}
}, [firstInteractionDetected, audioManager, disableSound]);

// Update audio manager when volume settings change
useEffect(() => {
if (disableSound || !audioManager) return;

const updateVolumes = throttle(
(settings: VolumeSettings) => {
audioManager.updateVolumeSettings(settings);
},
100,
{ leading: true, trailing: true }
);

updateVolumes({
// Use the audio system hook
const {
isAudioLoaded,
isAudioPlaying,
handleAudioToggle,
notifyFirstInteraction,
} = useAudioSystem({
disableSound,
particles,
volumeSettings: {
masterVolume: physicsConfig.MASTER_VOLUME,
ambientVolume: physicsConfig.AMBIENT_VOLUME,
particleVolume: physicsConfig.PARTICLE_VOLUME,
});

return () => {
updateVolumes.cancel();
};
}, [
audioManager,
disableSound,
physicsConfig.MASTER_VOLUME,
physicsConfig.AMBIENT_VOLUME,
physicsConfig.PARTICLE_VOLUME,
]);

// Track particle sound effects
useEffect(() => {
if (disableSound || !audioManager) return;

// Get current particle IDs
const currentParticleIds = new Set(
particles.map((p) => p.id).filter(Boolean)
);

// Get existing sound effect IDs
const existingSoundEffectIds = new Set(
audioManager.getActiveSoundEffectIds()
);

// Remove sound effects for particles that no longer exist
existingSoundEffectIds.forEach((effectId) => {
if (!currentParticleIds.has(effectId)) {
audioManager.removeSoundEffect(effectId);
}
});

// Add sound effects for new particles
particles.forEach((particle) => {
if (!particle.id) return;
if (!existingSoundEffectIds.has(particle.id)) {
audioManager.addParticleSoundEffect(particle.id, {
volume: -100, // Start silent
frequency: 0, // Start with no frequency
});
}
});

// Cleanup on unmount or when disableSound changes
return () => {
if (disableSound) {
// Remove all sound effects when sound is disabled
audioManager.getActiveSoundEffectIds().forEach((id) => {
audioManager.removeSoundEffect(id);
});
}
};
}, [particles, audioManager, disableSound]); // Run whenever particles array changes
},
audioFiles,
});

// Update sound effect parameters based on particle properties
// Handle first interaction
useEffect(() => {
if (disableSound || !audioManager) return;

requestAnimationFrame(() => {
particles.forEach((particle) => {
if (!particle.id) return;

// Calculate velocity magnitude
const velocityMagnitude = particle.velocity.length;

// Define velocity thresholds
const VELOCITY_THRESHOLD = 0.1; // Minimum velocity for sound
const NORMAL_VELOCITY = 200; // Velocity at which noise is at 4000Hz

if (velocityMagnitude < VELOCITY_THRESHOLD) {
// Complete silence when nearly stationary
audioManager.updateSoundEffect(particle.id, {
frequency: 0,
volume: -100,
});
return;
}

// Calculate normalized velocity ratio (0 to 1)
const normalizedVelocity = Math.min(
velocityMagnitude / NORMAL_VELOCITY,
5
);

// Calculate noise frequency (0Hz to 8000Hz)
// At normal velocity (ratio = 1), frequency will be 4000Hz
const frequency = normalizedVelocity * 4000;

// Calculate volume (-70dB to -40dB)
// More velocity = louder, but with a soft cap
const volume = -50 + Math.min(normalizedVelocity, 1) * 20;

// Update sound effect parameters
audioManager.updateSoundEffect(particle.id, {
frequency,
volume,
});
});
});
}, [particles, audioManager, disableSound]);

const handleAudioToggle = useCallback(
async (e: React.MouseEvent<HTMLButtonElement>) => {
if (disableSound || !audioManager) return;
e.stopPropagation();
await audioManager.togglePlayback();
setIsAudioPlaying(audioManager.getIsPlaying());
},
[audioManager, disableSound]
);
if (firstInteractionDetected) {
notifyFirstInteraction();
}
}, [firstInteractionDetected, notifyFirstInteraction]);

useEffect(() => {
const updateOffset = () => {
Expand Down
105 changes: 68 additions & 37 deletions src/hooks/useAudioSystem.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,35 @@
import { useState, useEffect } from "react";
import { throttle } from "lodash";
import { useState, useEffect, useCallback } from "react";
import { AudioManager, VolumeSettings } from "../utils/audio/AudioManager";
import { throttle } from "lodash";
import { Particle } from "../types/particle";
import { PhysicsSettings } from "../constants/physics";

export const useAudioSystem = (
disableSound: boolean,
physicsConfig: PhysicsSettings,
particles: Particle[],
audioFiles: string[]
) => {

interface UseAudioSystemProps {
disableSound: boolean;
particles: Particle[];
volumeSettings: VolumeSettings;
audioFiles: string[];
}

interface UseAudioSystemReturn {
isAudioLoaded: boolean;
isAudioPlaying: boolean;
handleAudioToggle: (e: React.MouseEvent) => Promise<void>;
notifyFirstInteraction: () => void;
}

export const useAudioSystem = ({
disableSound,
particles,
volumeSettings,
audioFiles,
}: UseAudioSystemProps): UseAudioSystemReturn => {
const [audioManager] = useState(() =>
disableSound
? null
: AudioManager.getInstance({
masterVolume: physicsConfig.MASTER_VOLUME,
ambientVolume: physicsConfig.AMBIENT_VOLUME,
particleVolume: physicsConfig.PARTICLE_VOLUME,
masterVolume: volumeSettings.masterVolume,
ambientVolume: volumeSettings.ambientVolume,
particleVolume: volumeSettings.particleVolume,
})
);
const [isAudioLoaded, setIsAudioLoaded] = useState(false);
Expand All @@ -28,17 +41,15 @@ export const useAudioSystem = (

const initAudio = async () => {
await audioManager.initialize(audioFiles);
setIsAudioLoaded(true);
};
initAudio();

return () => {
audioManager.cleanup();
setIsAudioLoaded(false);
};
}, [audioManager, disableSound, audioFiles]);

// Update volume settings when they change
// Update audio manager when volume settings change
useEffect(() => {
if (disableSound || !audioManager) return;

Expand All @@ -50,30 +61,23 @@ export const useAudioSystem = (
{ leading: true, trailing: true }
);

updateVolumes({
masterVolume: physicsConfig.MASTER_VOLUME,
ambientVolume: physicsConfig.AMBIENT_VOLUME,
particleVolume: physicsConfig.PARTICLE_VOLUME,
});
updateVolumes(volumeSettings);

return () => {
updateVolumes.cancel();
};
}, [
audioManager,
disableSound,
physicsConfig.MASTER_VOLUME,
physicsConfig.AMBIENT_VOLUME,
physicsConfig.PARTICLE_VOLUME,
]);
}, [audioManager, disableSound, volumeSettings]);

// Track particle sound effects
useEffect(() => {
if (disableSound || !audioManager) return;

// Get current particle IDs
const currentParticleIds = new Set(
particles.map((p) => p.id).filter(Boolean)
);

// Get existing sound effect IDs
const existingSoundEffectIds = new Set(
audioManager.getActiveSoundEffectIds()
);
Expand All @@ -96,8 +100,10 @@ export const useAudioSystem = (
}
});

// Cleanup on unmount or when disableSound changes
return () => {
if (disableSound) {
// Remove all sound effects when sound is disabled
audioManager.getActiveSoundEffectIds().forEach((id) => {
audioManager.removeSoundEffect(id);
});
Expand All @@ -113,25 +119,37 @@ export const useAudioSystem = (
particles.forEach((particle) => {
if (!particle.id) return;

// Calculate velocity magnitude
const velocityMagnitude = particle.velocity.length;
const VELOCITY_THRESHOLD = 0.1;
const NORMAL_VELOCITY = 200;

// Define velocity thresholds
const VELOCITY_THRESHOLD = 0.1; // Minimum velocity for sound
const NORMAL_VELOCITY = 200; // Velocity at which noise is at 4000Hz

if (velocityMagnitude < VELOCITY_THRESHOLD) {
// Complete silence when nearly stationary
audioManager.updateSoundEffect(particle.id, {
frequency: 0,
volume: -100,
});
return;
}

// Calculate normalized velocity ratio (0 to 1)
const normalizedVelocity = Math.min(
velocityMagnitude / NORMAL_VELOCITY,
5
);

// Calculate noise frequency (0Hz to 8000Hz)
// At normal velocity (ratio = 1), frequency will be 4000Hz
const frequency = normalizedVelocity * 4000;

// Calculate volume (-70dB to -40dB)
// More velocity = louder, but with a soft cap
const volume = -50 + Math.min(normalizedVelocity, 1) * 20;

// Update sound effect parameters
audioManager.updateSoundEffect(particle.id, {
frequency,
volume,
Expand All @@ -140,18 +158,31 @@ export const useAudioSystem = (
});
}, [particles, audioManager, disableSound]);

const handleAudioToggle = async (e: React.MouseEvent<HTMLButtonElement>) => {
const handleAudioToggle = useCallback(
async (e: React.MouseEvent) => {
if (disableSound || !audioManager) return;
e.stopPropagation();
await audioManager.togglePlayback();
setIsAudioPlaying(audioManager.getIsPlaying());
},
[audioManager, disableSound]
);

const notifyFirstInteraction = useCallback(() => {
if (disableSound || !audioManager) return;
e.stopPropagation();
await audioManager.togglePlayback();
setIsAudioPlaying(audioManager.getIsPlaying());
};

audioManager.notifyFirstInteraction();
if (audioManager.getIsLoaded()) {
setIsAudioLoaded(audioManager.getIsLoaded());
audioManager.play();
setIsAudioPlaying(true);
}
}, [audioManager, disableSound]);

return {
audioManager,
isAudioLoaded,
isAudioPlaying,
setIsAudioPlaying,
handleAudioToggle,
notifyFirstInteraction,
};
};

0 comments on commit e5dd63c

Please sign in to comment.