import { useState, useRef, useCallback, useEffect } from 'preact/hooks';
import BrowserOnly from '../browser-only';
import { useAudioContext } from './use-audio-context';
import WaveformDisplay from './waveform-display';
import TimeScaleControls from './time-scale-controls';
import useScopeWorklet from './use-scope-worklet';
import { DEFAULT_FFT_SIZE } from './constants';
import { stopSource, disconnectNode } from './cleanup-nodes';
import ButtonGroup from './button-group';
import { adStyles } from './audio-demo-styles';
import { inputValue } from './utils';
type WaveformType = 'sine' | 'triangle' | 'sawtooth' | 'square';
const WAVEFORM_TYPES: WaveformType[] = ['sine', 'triangle', 'sawtooth', 'square'];
function OscillatorDemoInner() {
const { ensureResumed } = useAudioContext();
const [isPlaying, setIsPlaying] = useState(false);
const [waveform, setWaveform] = useState<WaveformType>('sine');
const [frequency, setFrequency] = useState(440);
const [volume, setVolume] = useState(50);
const [timeScale, setTimeScale] = useState(1);
const oscRef = useRef<OscillatorNode | null>(null);
const gainRef = useRef<GainNode | null>(null);
const analyserRef = useRef<AnalyserNode | null>(null);
const scope = useScopeWorklet();
const handleToggle = useCallback(async () => {
if (isPlaying) {
stopSource(oscRef);
disconnectNode(gainRef);
disconnectNode(analyserRef);
scope.disconnect();
setIsPlaying(false);
return;
}
const ctx = await ensureResumed();
const oscillator = ctx.createOscillator();
const gainNode = ctx.createGain();
const analyser = ctx.createAnalyser();
analyser.fftSize = DEFAULT_FFT_SIZE;
oscillator.type = waveform;
oscillator.frequency.setValueAtTime(frequency, ctx.currentTime);
gainNode.gain.setValueAtTime(volume / 100, ctx.currentTime);
oscillator.connect(gainNode);
gainNode.connect(analyser);
const worklet = await scope.ensureWorklet(ctx);
gainNode.connect(worklet);
analyser.connect(ctx.destination);
oscillator.start();
oscRef.current = oscillator;
gainRef.current = gainNode;
analyserRef.current = analyser;
setIsPlaying(true);
}, [isPlaying, waveform, frequency, volume, ensureResumed, scope]);
const handleWaveformChange = useCallback((type: WaveformType) => {
setWaveform(type);
if (oscRef.current) {
oscRef.current.type = type;
}
}, []);
const handleFrequencyChange = useCallback((e: Event) => {
const freq = inputValue(e);
setFrequency(freq);
if (oscRef.current) {
oscRef.current.frequency.setValueAtTime(freq, oscRef.current.context.currentTime);
}
}, []);
const handleVolumeChange = useCallback((e: Event) => {
const vol = inputValue(e);
setVolume(vol);
if (gainRef.current) {
gainRef.current.gain.setValueAtTime(vol / 100, gainRef.current.context.currentTime);
}
}, []);
useEffect(() => {
return () => {
stopSource(oscRef);
disconnectNode(gainRef);
disconnectNode(analyserRef);
scope.disconnect();
};
}, [scope]);
return (
<div className={adStyles.demoContainer}>
<div className={adStyles.controlsSection}>
<ButtonGroup
label="Waveform"
options={WAVEFORM_TYPES}
value={waveform}
onChange={handleWaveformChange}
/>
<div className={adStyles.controlGroup}>
<label htmlFor="oscillator-frequency" className={adStyles.controlLabel}>
Frequency: <span className={adStyles.valueDisplay}>{frequency} Hz</span>
</label>
<input
id="oscillator-frequency"
type="range"
min="20"
max="2000"
value={frequency}
onChange={handleFrequencyChange}
className={adStyles.slider}
/>
</div>
<div className={adStyles.controlGroup}>
<label htmlFor="oscillator-volume" className={adStyles.controlLabel}>
Volume: <span className={adStyles.valueDisplay}>{volume}%</span>
</label>
<input
id="oscillator-volume"
type="range"
min="0"
max="100"
value={volume}
onChange={handleVolumeChange}
className={adStyles.slider}
/>
</div>
<button
type="button"
aria-pressed={isPlaying}
className={`${adStyles.playButton} ${isPlaying ? adStyles.playButtonActive : ''}`}
onClick={handleToggle}
>
{isPlaying ? 'Stop' : 'Play'}
</button>
<TimeScaleControls value={timeScale} onChange={setTimeScale} />
</div>
<WaveformDisplay isPlaying={isPlaying} timeScale={timeScale} scope={scope} />
</div>
);
}
export default function OscillatorDemo() {
return <BrowserOnly>{() => <OscillatorDemoInner />}</BrowserOnly>;
}