diff --git a/src/components/modals/isochronic/index.ts b/src/components/modals/isochronic/index.ts new file mode 100644 index 0000000..e4512eb --- /dev/null +++ b/src/components/modals/isochronic/index.ts @@ -0,0 +1 @@ +export { IsochronicModal } from './isochronic'; diff --git a/src/components/modals/isochronic/isochornic.module.css b/src/components/modals/isochronic/isochornic.module.css new file mode 100644 index 0000000..fdbd99d --- /dev/null +++ b/src/components/modals/isochronic/isochornic.module.css @@ -0,0 +1 @@ +/* WIP */ diff --git a/src/components/modals/isochronic/isochronic.tsx b/src/components/modals/isochronic/isochronic.tsx new file mode 100644 index 0000000..d99eaa9 --- /dev/null +++ b/src/components/modals/isochronic/isochronic.tsx @@ -0,0 +1,245 @@ +import { useEffect, useState, useRef, useCallback } from 'react'; + +import { Modal } from '@/components/modal'; + +import styles from './isochornic.module.css'; + +interface IsochronicProps { + onClose: () => void; + show: boolean; +} + +interface Preset { + baseFrequency: number; + beatFrequency: number; + name: string; +} + +const presets: Preset[] = [ + { baseFrequency: 100, beatFrequency: 2, name: 'Delta (Deep Sleep) 2 Hz' }, + { baseFrequency: 100, beatFrequency: 5, name: 'Theta (Meditation) 5 Hz' }, + { baseFrequency: 100, beatFrequency: 10, name: 'Alpha (Relaxation) 10 Hz' }, + { baseFrequency: 100, beatFrequency: 20, name: 'Beta (Focus) 20 Hz' }, + { baseFrequency: 100, beatFrequency: 40, name: 'Gamma (Cognition) 40 Hz' }, + { baseFrequency: 440, beatFrequency: 10, name: 'Custom' }, +]; + +export function IsochronicModal({ onClose, show }: IsochronicProps) { + const [baseFrequency, setBaseFrequency] = useState(440); // Default A4 note + const [beatFrequency, setBeatFrequency] = useState(10); // Default 10 Hz beat + const [volume, setVolume] = useState(0.5); // Default volume at 50% + const [waveform, setWaveform] = useState('sine'); // Default waveform + const [isPlaying, setIsPlaying] = useState(false); + const [selectedPreset, setSelectedPreset] = useState('Custom'); + + const audioContextRef = useRef(null); + const oscillatorRef = useRef(null); + const gainNodeRef = useRef(null); + const beatGainRef = useRef(null); + const modulatorRef = useRef(null); + + const startSound = () => { + if (isPlaying) return; + + audioContextRef.current = new window.AudioContext(); + const audioContext = audioContextRef.current; + + if (!audioContext) return; + + // Main gain node for volume control + gainNodeRef.current = audioContext.createGain(); + gainNodeRef.current.gain.value = volume; + + // Oscillator for the base tone + oscillatorRef.current = audioContext.createOscillator(); + oscillatorRef.current.frequency.value = baseFrequency; + oscillatorRef.current.type = waveform; + + // Gain node to create isochronic beats + beatGainRef.current = audioContext.createGain(); + beatGainRef.current.gain.value = 0; // Start with silence + + // Oscillator for modulation + modulatorRef.current = audioContext.createOscillator(); + modulatorRef.current.frequency.value = beatFrequency; + modulatorRef.current.type = 'square'; // Square wave for on/off effect + + // Modulator gain to adjust modulation depth + const modulatorGain = audioContext.createGain(); + modulatorGain.gain.value = 0.5; // Modulation depth + + // Connect modulator to the beat gain node + modulatorRef.current + .connect(modulatorGain) + .connect(beatGainRef.current.gain); + + // Connect oscillator through beat gain and main gain to destination + oscillatorRef.current + .connect(beatGainRef.current) + .connect(gainNodeRef.current) + .connect(audioContext.destination); + + // Start oscillators + oscillatorRef.current.start(); + modulatorRef.current.start(); + + setIsPlaying(true); + }; + + const stopSound = useCallback(() => { + if (!isPlaying) return; + + oscillatorRef.current?.stop(); + modulatorRef.current?.stop(); + audioContextRef.current?.close(); + + setIsPlaying(false); + }, [isPlaying]); + + useEffect(() => { + // Update gain when volume changes + if (gainNodeRef.current) { + gainNodeRef.current.gain.value = volume; + } + }, [volume]); + + useEffect(() => { + // Update base frequency when it changes + if (oscillatorRef.current) { + oscillatorRef.current.frequency.value = baseFrequency; + } + }, [baseFrequency]); + + useEffect(() => { + // Update beat frequency when it changes + if (modulatorRef.current) { + modulatorRef.current.frequency.value = beatFrequency; + } + }, [beatFrequency]); + + useEffect(() => { + // Update waveform when it changes + if (oscillatorRef.current) { + oscillatorRef.current.type = waveform; + } + }, [waveform]); + + useEffect(() => { + // Cleanup when component unmounts + return () => { + if (isPlaying) { + stopSound(); + } + }; + }, [isPlaying, stopSound]); + + useEffect(() => { + // Update frequencies when a preset is selected + if (selectedPreset !== 'Custom') { + const preset = presets.find(p => p.name === selectedPreset); + if (preset) { + setBaseFrequency(preset.baseFrequency); + setBeatFrequency(preset.beatFrequency); + } + } + }, [selectedPreset]); + + const handlePresetChange = (e: React.ChangeEvent) => { + const selected = e.target.value; + setSelectedPreset(selected); + + if (selected === 'Custom') { + // Allow user to input custom frequencies + return; + } + + const preset = presets.find(p => p.name === selected); + if (preset) { + setBaseFrequency(preset.baseFrequency); + setBeatFrequency(preset.beatFrequency); + } + }; + + return ( + +

Isochronic Tones

+
+ +
+ {selectedPreset === 'Custom' && ( + <> +
+ +
+
+ +
+
+ +
+ + )} +
+ +
+
+ + +
+
+ ); +} diff --git a/src/components/toolbar/menu/items/index.ts b/src/components/toolbar/menu/items/index.ts index 162b7d4..158ff5f 100644 --- a/src/components/toolbar/menu/items/index.ts +++ b/src/components/toolbar/menu/items/index.ts @@ -11,3 +11,4 @@ export { Notepad as NotepadItem } from './notepad'; export { Todo as TodoItem } from './todo'; export { Countdown as CountdownItem } from './countdown'; export { Binaural as BinauralItem } from './binaural'; +export { Isochronic as IsochronicItem } from './isochronic'; diff --git a/src/components/toolbar/menu/items/isochronic.tsx b/src/components/toolbar/menu/items/isochronic.tsx new file mode 100644 index 0000000..f5b0988 --- /dev/null +++ b/src/components/toolbar/menu/items/isochronic.tsx @@ -0,0 +1,13 @@ +import { RiPlayListFill } from 'react-icons/ri/index'; + +import { Item } from '../item'; + +interface IsochronicProps { + open: () => void; +} + +export function Isochronic({ open }: IsochronicProps) { + return ( + } label="Isochronic Tones" onClick={open} /> + ); +} diff --git a/src/components/toolbar/menu/menu.tsx b/src/components/toolbar/menu/menu.tsx index 0e2efb0..a985c30 100644 --- a/src/components/toolbar/menu/menu.tsx +++ b/src/components/toolbar/menu/menu.tsx @@ -18,6 +18,7 @@ import { TodoItem, CountdownItem, BinauralItem, + IsochronicItem, } from './items'; import { Divider } from './divider'; import { ShareLinkModal } from '@/components/modals/share-link'; @@ -26,6 +27,7 @@ import { ShortcutsModal } from '@/components/modals/shortcuts'; import { SleepTimerModal } from '@/components/modals/sleep-timer'; import { BreathingExerciseModal } from '@/components/modals/breathing'; import { BinauralModal } from '@/components/modals/binaural'; +import { IsochronicModal } from '@/components/modals/isochronic'; import { Pomodoro, Notepad, Todo, Countdown } from '@/components/toolbox'; import { fade, mix, slideY } from '@/lib/motion'; import { useSoundStore } from '@/stores/sound'; @@ -44,6 +46,7 @@ export function Menu() { binaural: false, breathing: false, countdown: false, + isochronic: false, notepad: false, pomodoro: false, presets: false, @@ -120,6 +123,7 @@ export function Menu() { open('sleepTimer')} /> open('binaural')} /> + open('isochronic')} /> open('countdown')} /> @@ -168,6 +172,10 @@ export function Menu() { onClose={() => close('sleepTimer')} /> close('binaural')} /> + close('isochronic')} + /> ); }