From f40e8206f8126f1988e0e39ca522ac3c5eb8139f Mon Sep 17 00:00:00 2001 From: MAZE Date: Fri, 13 Sep 2024 14:55:04 +0330 Subject: [PATCH] feat: add binaural beat generator without styles --- .../modals/binaural/binaural.module.css | 1 + src/components/modals/binaural/binaural.tsx | 211 ++++++++++++++++++ src/components/modals/binaural/index.ts | 1 + .../toolbar/menu/items/binaural.tsx | 13 ++ src/components/toolbar/menu/items/index.ts | 1 + src/components/toolbar/menu/menu.tsx | 5 + 6 files changed, 232 insertions(+) create mode 100644 src/components/modals/binaural/binaural.module.css create mode 100644 src/components/modals/binaural/binaural.tsx create mode 100644 src/components/modals/binaural/index.ts create mode 100644 src/components/toolbar/menu/items/binaural.tsx diff --git a/src/components/modals/binaural/binaural.module.css b/src/components/modals/binaural/binaural.module.css new file mode 100644 index 0000000..fdbd99d --- /dev/null +++ b/src/components/modals/binaural/binaural.module.css @@ -0,0 +1 @@ +/* WIP */ diff --git a/src/components/modals/binaural/binaural.tsx b/src/components/modals/binaural/binaural.tsx new file mode 100644 index 0000000..98cfc03 --- /dev/null +++ b/src/components/modals/binaural/binaural.tsx @@ -0,0 +1,211 @@ +import { useEffect, useState, useRef, useCallback } from 'react'; + +import { Modal } from '@/components/modal'; + +import styles from './binaural.module.css'; + +interface BinauralProps { + 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 BinauralModal({ onClose, show }: BinauralProps) { + const [baseFrequency, setBaseFrequency] = useState(440); // Default to A4 note + const [beatFrequency, setBeatFrequency] = useState(10); // Default to 10 Hz difference + const [volume, setVolume] = useState(0.5); // Default volume at 50% + const [isPlaying, setIsPlaying] = useState(false); + const [selectedPreset, setSelectedPreset] = useState('Custom'); + + const audioContextRef = useRef(null); + const leftOscillatorRef = useRef(null); + const rightOscillatorRef = useRef(null); + const gainNodeRef = useRef(null); + + const startSound = () => { + if (isPlaying) return; + + // Initialize the AudioContext + audioContextRef.current = new window.AudioContext(); + const audioContext = audioContextRef.current; + + if (!audioContext) return; + + // Create a gain node for volume control + gainNodeRef.current = audioContext.createGain(); + gainNodeRef.current.gain.value = volume; // Set volume based on state + + // Create oscillators for left and right channels + leftOscillatorRef.current = audioContext.createOscillator(); + rightOscillatorRef.current = audioContext.createOscillator(); + + if ( + !leftOscillatorRef.current || + !rightOscillatorRef.current || + !gainNodeRef.current + ) + return; + + leftOscillatorRef.current.frequency.value = + baseFrequency - beatFrequency / 2; + rightOscillatorRef.current.frequency.value = + baseFrequency + beatFrequency / 2; + + // Pan oscillators to left and right + const leftPanner = audioContext.createStereoPanner(); + leftPanner.pan.value = -1; + + const rightPanner = audioContext.createStereoPanner(); + rightPanner.pan.value = 1; + + // Connect nodes + leftOscillatorRef.current.connect(leftPanner).connect(gainNodeRef.current); + rightOscillatorRef.current + .connect(rightPanner) + .connect(gainNodeRef.current); + gainNodeRef.current.connect(audioContext.destination); + + // Start oscillators + leftOscillatorRef.current.start(); + rightOscillatorRef.current.start(); + + setIsPlaying(true); + }; + + const stopSound = useCallback(() => { + if (!isPlaying) return; + + leftOscillatorRef.current?.stop(); + rightOscillatorRef.current?.stop(); + audioContextRef.current?.close(); + + setIsPlaying(false); + }, [isPlaying]); + + useEffect(() => { + // Update gain node when volume changes + if (gainNodeRef.current) { + gainNodeRef.current.gain.value = volume; + } + }, [volume]); + + 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 ( + +

Binaural Beats

+ +
+ +
+ {selectedPreset === 'Custom' && ( + <> +
+ +
+
+ +
+ + )} +
+ +
+
+ + +
+
+ ); +} diff --git a/src/components/modals/binaural/index.ts b/src/components/modals/binaural/index.ts new file mode 100644 index 0000000..92188e9 --- /dev/null +++ b/src/components/modals/binaural/index.ts @@ -0,0 +1 @@ +export { BinauralModal } from './binaural'; diff --git a/src/components/toolbar/menu/items/binaural.tsx b/src/components/toolbar/menu/items/binaural.tsx new file mode 100644 index 0000000..a62c503 --- /dev/null +++ b/src/components/toolbar/menu/items/binaural.tsx @@ -0,0 +1,13 @@ +import { RiPlayListFill } from 'react-icons/ri/index'; + +import { Item } from '../item'; + +interface BinauralProps { + open: () => void; +} + +export function Binaural({ open }: BinauralProps) { + return ( + } label="Binaural Beats" onClick={open} /> + ); +} diff --git a/src/components/toolbar/menu/items/index.ts b/src/components/toolbar/menu/items/index.ts index 7c63287..162b7d4 100644 --- a/src/components/toolbar/menu/items/index.ts +++ b/src/components/toolbar/menu/items/index.ts @@ -10,3 +10,4 @@ export { Pomodoro as PomodoroItem } from './pomodoro'; export { Notepad as NotepadItem } from './notepad'; export { Todo as TodoItem } from './todo'; export { Countdown as CountdownItem } from './countdown'; +export { Binaural as BinauralItem } from './binaural'; diff --git a/src/components/toolbar/menu/menu.tsx b/src/components/toolbar/menu/menu.tsx index f2c36b2..0e2efb0 100644 --- a/src/components/toolbar/menu/menu.tsx +++ b/src/components/toolbar/menu/menu.tsx @@ -17,6 +17,7 @@ import { NotepadItem, TodoItem, CountdownItem, + BinauralItem, } from './items'; import { Divider } from './divider'; import { ShareLinkModal } from '@/components/modals/share-link'; @@ -24,6 +25,7 @@ import { PresetsModal } from '@/components/modals/presets'; 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 { Pomodoro, Notepad, Todo, Countdown } from '@/components/toolbox'; import { fade, mix, slideY } from '@/lib/motion'; import { useSoundStore } from '@/stores/sound'; @@ -39,6 +41,7 @@ export function Menu() { const initial = useMemo( () => ({ + binaural: false, breathing: false, countdown: false, notepad: false, @@ -116,6 +119,7 @@ export function Menu() { open('shareLink')} /> open('sleepTimer')} /> + open('binaural')} /> open('countdown')} /> @@ -163,6 +167,7 @@ export function Menu() { show={modals.sleepTimer} onClose={() => close('sleepTimer')} /> + close('binaural')} /> ); }