mirror of
https://github.com/remvze/moodist.git
synced 2025-12-17 08:54:13 +00:00
feat: add binaural beat generator without styles
This commit is contained in:
parent
d2e289e5d5
commit
f40e8206f8
6 changed files with 232 additions and 0 deletions
1
src/components/modals/binaural/binaural.module.css
Normal file
1
src/components/modals/binaural/binaural.module.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
/* WIP */
|
||||
211
src/components/modals/binaural/binaural.tsx
Normal file
211
src/components/modals/binaural/binaural.tsx
Normal file
|
|
@ -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<number>(440); // Default to A4 note
|
||||
const [beatFrequency, setBeatFrequency] = useState<number>(10); // Default to 10 Hz difference
|
||||
const [volume, setVolume] = useState<number>(0.5); // Default volume at 50%
|
||||
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||||
const [selectedPreset, setSelectedPreset] = useState<string>('Custom');
|
||||
|
||||
const audioContextRef = useRef<AudioContext | null>(null);
|
||||
const leftOscillatorRef = useRef<OscillatorNode | null>(null);
|
||||
const rightOscillatorRef = useRef<OscillatorNode | null>(null);
|
||||
const gainNodeRef = useRef<GainNode | null>(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<HTMLSelectElement>) => {
|
||||
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 (
|
||||
<Modal show={show} onClose={onClose}>
|
||||
<h2 className={styles.title}>Binaural Beats</h2>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Presets:
|
||||
<select value={selectedPreset} onChange={handlePresetChange}>
|
||||
{presets.map(preset => (
|
||||
<option key={preset.name} value={preset.name}>
|
||||
{preset.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
{selectedPreset === 'Custom' && (
|
||||
<>
|
||||
<div>
|
||||
<label>
|
||||
Base Frequency (Hz):
|
||||
<input
|
||||
max="1500"
|
||||
min="20"
|
||||
step="0.1"
|
||||
type="number"
|
||||
value={baseFrequency}
|
||||
onChange={e => setBaseFrequency(parseFloat(e.target.value))}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
Beat Frequency (Hz):
|
||||
<input
|
||||
max="40"
|
||||
min="0.1"
|
||||
step="0.1"
|
||||
type="number"
|
||||
value={beatFrequency}
|
||||
onChange={e => setBeatFrequency(parseFloat(e.target.value))}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div>
|
||||
<label>
|
||||
Volume:
|
||||
<input
|
||||
max="1"
|
||||
min="0"
|
||||
step="0.01"
|
||||
type="range"
|
||||
value={volume}
|
||||
onChange={e => setVolume(parseFloat(e.target.value))}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<button disabled={isPlaying} onClick={startSound}>
|
||||
Start
|
||||
</button>
|
||||
<button disabled={!isPlaying} onClick={stopSound}>
|
||||
Stop
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
1
src/components/modals/binaural/index.ts
Normal file
1
src/components/modals/binaural/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { BinauralModal } from './binaural';
|
||||
13
src/components/toolbar/menu/items/binaural.tsx
Normal file
13
src/components/toolbar/menu/items/binaural.tsx
Normal file
|
|
@ -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 (
|
||||
<Item icon={<RiPlayListFill />} label="Binaural Beats" onClick={open} />
|
||||
);
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<ShareItem open={() => open('shareLink')} />
|
||||
<ShuffleItem />
|
||||
<SleepTimerItem open={() => open('sleepTimer')} />
|
||||
<BinauralItem open={() => open('binaural')} />
|
||||
|
||||
<Divider />
|
||||
<CountdownItem open={() => open('countdown')} />
|
||||
|
|
@ -163,6 +167,7 @@ export function Menu() {
|
|||
show={modals.sleepTimer}
|
||||
onClose={() => close('sleepTimer')}
|
||||
/>
|
||||
<BinauralModal show={modals.binaural} onClose={() => close('binaural')} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue