mirror of
https://github.com/arthur-pbty/pomodoro.git
synced 2026-06-03 15:07:32 +02:00
b933c6040c
feat: add useAmbientSound hook for ambient sound management feat: create useLocalStorage hook for persistent state management feat: develop useTheme hook for theme switching functionality feat: implement useTimer hook for Pomodoro timer logic feat: create useTodos hook for managing todo list functionality style: add global styles and custom scrollbar for better UI experience chore: set up main entry point for the application feat: define types for Timer, Todo, and Statistics feat: create utility function for class name merging chore: configure Tailwind CSS for styling chore: set up TypeScript configuration for the project chore: configure Vite for development and build process
170 lines
5.0 KiB
TypeScript
170 lines
5.0 KiB
TypeScript
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
|
|
export type SoundType = 'none' | 'rain' | 'cafe' | 'whitenoise' | 'forest' | 'ocean';
|
|
|
|
export function useAmbientSound() {
|
|
const [currentSound, setCurrentSound] = useState<SoundType>('none');
|
|
const [volume, setVolume] = useState(0.5);
|
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
|
|
const audioContextRef = useRef<AudioContext | null>(null);
|
|
const nodesRef = useRef<{
|
|
sources: AudioBufferSourceNode[];
|
|
gains: GainNode[];
|
|
masterGain: GainNode | null;
|
|
}>({ sources: [], gains: [], masterGain: null });
|
|
|
|
const stopSound = useCallback(() => {
|
|
nodesRef.current.sources.forEach(source => {
|
|
try { source.stop(); } catch (e) {}
|
|
});
|
|
nodesRef.current.sources = [];
|
|
nodesRef.current.gains = [];
|
|
setIsPlaying(false);
|
|
}, []);
|
|
|
|
const createNoiseBuffer = useCallback((ctx: AudioContext, type: 'white' | 'brown' | 'pink') => {
|
|
const bufferSize = ctx.sampleRate * 2;
|
|
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
|
|
const output = buffer.getChannelData(0);
|
|
|
|
if (type === 'white') {
|
|
for (let i = 0; i < bufferSize; i++) {
|
|
output[i] = Math.random() * 2 - 1;
|
|
}
|
|
} else if (type === 'brown') {
|
|
let lastOut = 0;
|
|
for (let i = 0; i < bufferSize; i++) {
|
|
const white = Math.random() * 2 - 1;
|
|
output[i] = (lastOut + 0.02 * white) / 1.02;
|
|
lastOut = output[i];
|
|
output[i] *= 3.5;
|
|
}
|
|
} else if (type === 'pink') {
|
|
let b0 = 0, b1 = 0, b2 = 0, b3 = 0, b4 = 0, b5 = 0, b6 = 0;
|
|
for (let i = 0; i < bufferSize; i++) {
|
|
const white = Math.random() * 2 - 1;
|
|
b0 = 0.99886 * b0 + white * 0.0555179;
|
|
b1 = 0.99332 * b1 + white * 0.0750759;
|
|
b2 = 0.96900 * b2 + white * 0.1538520;
|
|
b3 = 0.86650 * b3 + white * 0.3104856;
|
|
b4 = 0.55000 * b4 + white * 0.5329522;
|
|
b5 = -0.7616 * b5 - white * 0.0168980;
|
|
output[i] = b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362;
|
|
output[i] *= 0.11;
|
|
b6 = white * 0.115926;
|
|
}
|
|
}
|
|
|
|
return buffer;
|
|
}, []);
|
|
|
|
const playSound = useCallback((type: SoundType) => {
|
|
stopSound();
|
|
|
|
if (type === 'none') {
|
|
setCurrentSound('none');
|
|
return;
|
|
}
|
|
|
|
if (!audioContextRef.current) {
|
|
audioContextRef.current = new AudioContext();
|
|
}
|
|
const ctx = audioContextRef.current;
|
|
|
|
const masterGain = ctx.createGain();
|
|
masterGain.gain.value = volume;
|
|
masterGain.connect(ctx.destination);
|
|
nodesRef.current.masterGain = masterGain;
|
|
|
|
const createLoopingSource = (buffer: AudioBuffer, gainValue: number = 1) => {
|
|
const source = ctx.createBufferSource();
|
|
const gain = ctx.createGain();
|
|
source.buffer = buffer;
|
|
source.loop = true;
|
|
gain.gain.value = gainValue;
|
|
source.connect(gain);
|
|
gain.connect(masterGain);
|
|
source.start();
|
|
nodesRef.current.sources.push(source);
|
|
nodesRef.current.gains.push(gain);
|
|
return { source, gain };
|
|
};
|
|
|
|
switch (type) {
|
|
case 'rain': {
|
|
// Bruit de pluie = bruit rose + bruit brun
|
|
const pinkBuffer = createNoiseBuffer(ctx, 'pink');
|
|
const brownBuffer = createNoiseBuffer(ctx, 'brown');
|
|
createLoopingSource(pinkBuffer, 0.6);
|
|
createLoopingSource(brownBuffer, 0.3);
|
|
break;
|
|
}
|
|
case 'cafe': {
|
|
// Ambiance café = bruit brun léger + variations
|
|
const brownBuffer = createNoiseBuffer(ctx, 'brown');
|
|
const pinkBuffer = createNoiseBuffer(ctx, 'pink');
|
|
createLoopingSource(brownBuffer, 0.4);
|
|
createLoopingSource(pinkBuffer, 0.15);
|
|
break;
|
|
}
|
|
case 'whitenoise': {
|
|
const whiteBuffer = createNoiseBuffer(ctx, 'white');
|
|
createLoopingSource(whiteBuffer, 0.3);
|
|
break;
|
|
}
|
|
case 'forest': {
|
|
// Forêt = bruit rose doux
|
|
const pinkBuffer = createNoiseBuffer(ctx, 'pink');
|
|
const brownBuffer = createNoiseBuffer(ctx, 'brown');
|
|
createLoopingSource(pinkBuffer, 0.25);
|
|
createLoopingSource(brownBuffer, 0.15);
|
|
break;
|
|
}
|
|
case 'ocean': {
|
|
// Océan = bruit brun
|
|
const brownBuffer = createNoiseBuffer(ctx, 'brown');
|
|
createLoopingSource(brownBuffer, 0.7);
|
|
break;
|
|
}
|
|
}
|
|
|
|
setCurrentSound(type);
|
|
setIsPlaying(true);
|
|
}, [volume, stopSound, createNoiseBuffer]);
|
|
|
|
const changeVolume = useCallback((newVolume: number) => {
|
|
setVolume(newVolume);
|
|
if (nodesRef.current.masterGain) {
|
|
nodesRef.current.masterGain.gain.value = newVolume;
|
|
}
|
|
}, []);
|
|
|
|
const toggleSound = useCallback(() => {
|
|
if (isPlaying) {
|
|
stopSound();
|
|
} else if (currentSound !== 'none') {
|
|
playSound(currentSound);
|
|
}
|
|
}, [isPlaying, currentSound, playSound, stopSound]);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
stopSound();
|
|
if (audioContextRef.current) {
|
|
audioContextRef.current.close();
|
|
}
|
|
};
|
|
}, [stopSound]);
|
|
|
|
return {
|
|
currentSound,
|
|
volume,
|
|
isPlaying,
|
|
playSound,
|
|
stopSound,
|
|
changeVolume,
|
|
toggleSound,
|
|
};
|
|
}
|