mirror of
https://github.com/remvze/moodist.git
synced 2025-09-29 15:30:49 -04:00
feat: add isochronic tone generator without styles
This commit is contained in:
parent
f40e8206f8
commit
d759064373
1
src/components/modals/isochronic/index.ts
Normal file
1
src/components/modals/isochronic/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { IsochronicModal } from './isochronic';
|
1
src/components/modals/isochronic/isochornic.module.css
Normal file
1
src/components/modals/isochronic/isochornic.module.css
Normal file
@ -0,0 +1 @@
|
|||||||
|
/* WIP */
|
245
src/components/modals/isochronic/isochronic.tsx
Normal file
245
src/components/modals/isochronic/isochronic.tsx
Normal file
@ -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<number>(440); // Default A4 note
|
||||||
|
const [beatFrequency, setBeatFrequency] = useState<number>(10); // Default 10 Hz beat
|
||||||
|
const [volume, setVolume] = useState<number>(0.5); // Default volume at 50%
|
||||||
|
const [waveform, setWaveform] = useState<OscillatorType>('sine'); // Default waveform
|
||||||
|
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||||||
|
const [selectedPreset, setSelectedPreset] = useState<string>('Custom');
|
||||||
|
|
||||||
|
const audioContextRef = useRef<AudioContext | null>(null);
|
||||||
|
const oscillatorRef = useRef<OscillatorNode | null>(null);
|
||||||
|
const gainNodeRef = useRef<GainNode | null>(null);
|
||||||
|
const beatGainRef = useRef<GainNode | null>(null);
|
||||||
|
const modulatorRef = useRef<OscillatorNode | null>(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<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}>Isochronic Tones</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="2000"
|
||||||
|
min="20"
|
||||||
|
step="0.1"
|
||||||
|
type="number"
|
||||||
|
value={baseFrequency}
|
||||||
|
onChange={e => setBaseFrequency(parseFloat(e.target.value))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
Tone 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>
|
||||||
|
Waveform:
|
||||||
|
<select
|
||||||
|
value={waveform}
|
||||||
|
onChange={e => setWaveform(e.target.value as OscillatorType)}
|
||||||
|
>
|
||||||
|
<option value="sine">Sine</option>
|
||||||
|
<option value="square">Square</option>
|
||||||
|
<option value="sawtooth">Sawtooth</option>
|
||||||
|
<option value="triangle">Triangle</option>
|
||||||
|
</select>
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
@ -11,3 +11,4 @@ export { Notepad as NotepadItem } from './notepad';
|
|||||||
export { Todo as TodoItem } from './todo';
|
export { Todo as TodoItem } from './todo';
|
||||||
export { Countdown as CountdownItem } from './countdown';
|
export { Countdown as CountdownItem } from './countdown';
|
||||||
export { Binaural as BinauralItem } from './binaural';
|
export { Binaural as BinauralItem } from './binaural';
|
||||||
|
export { Isochronic as IsochronicItem } from './isochronic';
|
||||||
|
13
src/components/toolbar/menu/items/isochronic.tsx
Normal file
13
src/components/toolbar/menu/items/isochronic.tsx
Normal file
@ -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 (
|
||||||
|
<Item icon={<RiPlayListFill />} label="Isochronic Tones" onClick={open} />
|
||||||
|
);
|
||||||
|
}
|
@ -18,6 +18,7 @@ import {
|
|||||||
TodoItem,
|
TodoItem,
|
||||||
CountdownItem,
|
CountdownItem,
|
||||||
BinauralItem,
|
BinauralItem,
|
||||||
|
IsochronicItem,
|
||||||
} from './items';
|
} from './items';
|
||||||
import { Divider } from './divider';
|
import { Divider } from './divider';
|
||||||
import { ShareLinkModal } from '@/components/modals/share-link';
|
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 { SleepTimerModal } from '@/components/modals/sleep-timer';
|
||||||
import { BreathingExerciseModal } from '@/components/modals/breathing';
|
import { BreathingExerciseModal } from '@/components/modals/breathing';
|
||||||
import { BinauralModal } from '@/components/modals/binaural';
|
import { BinauralModal } from '@/components/modals/binaural';
|
||||||
|
import { IsochronicModal } from '@/components/modals/isochronic';
|
||||||
import { Pomodoro, Notepad, Todo, Countdown } from '@/components/toolbox';
|
import { Pomodoro, Notepad, Todo, Countdown } from '@/components/toolbox';
|
||||||
import { fade, mix, slideY } from '@/lib/motion';
|
import { fade, mix, slideY } from '@/lib/motion';
|
||||||
import { useSoundStore } from '@/stores/sound';
|
import { useSoundStore } from '@/stores/sound';
|
||||||
@ -44,6 +46,7 @@ export function Menu() {
|
|||||||
binaural: false,
|
binaural: false,
|
||||||
breathing: false,
|
breathing: false,
|
||||||
countdown: false,
|
countdown: false,
|
||||||
|
isochronic: false,
|
||||||
notepad: false,
|
notepad: false,
|
||||||
pomodoro: false,
|
pomodoro: false,
|
||||||
presets: false,
|
presets: false,
|
||||||
@ -120,6 +123,7 @@ export function Menu() {
|
|||||||
<ShuffleItem />
|
<ShuffleItem />
|
||||||
<SleepTimerItem open={() => open('sleepTimer')} />
|
<SleepTimerItem open={() => open('sleepTimer')} />
|
||||||
<BinauralItem open={() => open('binaural')} />
|
<BinauralItem open={() => open('binaural')} />
|
||||||
|
<IsochronicItem open={() => open('isochronic')} />
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
<CountdownItem open={() => open('countdown')} />
|
<CountdownItem open={() => open('countdown')} />
|
||||||
@ -168,6 +172,10 @@ export function Menu() {
|
|||||||
onClose={() => close('sleepTimer')}
|
onClose={() => close('sleepTimer')}
|
||||||
/>
|
/>
|
||||||
<BinauralModal show={modals.binaural} onClose={() => close('binaural')} />
|
<BinauralModal show={modals.binaural} onClose={() => close('binaural')} />
|
||||||
|
<IsochronicModal
|
||||||
|
show={modals.isochronic}
|
||||||
|
onClose={() => close('isochronic')}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user