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 { CLOCK_RING_BUFFER_SECONDS } from './constants';
import { stopSource, disconnectNode } from './cleanup-nodes';
import { adStyles } from './audio-demo-styles';
import { inputValue } from './utils';
function ClockDemoInner() {
const { ensureResumed } = useAudioContext();
const [isRunning, setIsRunning] = useState(false);
const [bpm, setBpm] = useState(120);
const [timeScale, setTimeScale] = useState(1);
const periodMs = 60000 / bpm;
const pulseSourceRef = useRef<ConstantSourceNode | null>(null);
const pulseGainRef = useRef<GainNode | null>(null);
const monitorGainRef = useRef<GainNode | null>(null);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const bpmRef = useRef(bpm);
const scope = useScopeWorklet(CLOCK_RING_BUFFER_SECONDS);
useEffect(() => {
bpmRef.current = bpm;
}, [bpm]);
const handleTrigger = useCallback(async () => {
const ctx = await ensureResumed();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'square';
osc.frequency.setValueAtTime(1200, ctx.currentTime);
gain.gain.setValueAtTime(0, ctx.currentTime);
gain.gain.linearRampToValueAtTime(0.4, ctx.currentTime + 0.005);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.08);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start();
osc.stop(ctx.currentTime + 0.1);
osc.onended = () => {
osc.disconnect();
gain.disconnect();
};
}, [ensureResumed]);
const clearTimer = useCallback(() => {
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
}, []);
const schedulePulse = useCallback(() => {
const pulseGainNode = pulseGainRef.current;
if (pulseGainNode) {
const now = pulseGainNode.context.currentTime;
const pulseWidth = Math.min(0.3, Math.max(0.02, 60000 / bpmRef.current / 1000 / 2));
pulseGainNode.gain.cancelScheduledValues(now);
pulseGainNode.gain.setValueAtTime(0, now);
pulseGainNode.gain.setValueAtTime(1, now + 0.0005);
pulseGainNode.gain.setValueAtTime(0, now + pulseWidth);
}
void handleTrigger();
}, [handleTrigger]);
const startClock = useCallback(async () => {
const ctx = await ensureResumed();
const pulseSource = ctx.createConstantSource();
const pulseGain = ctx.createGain();
const workletNode = await scope.ensureWorklet(ctx);
const monitorGain = ctx.createGain();
pulseSource.offset.setValueAtTime(1, ctx.currentTime);
pulseGain.gain.setValueAtTime(0, ctx.currentTime);
monitorGain.gain.setValueAtTime(0, ctx.currentTime);
pulseSource.connect(pulseGain);
pulseGain.connect(workletNode);
workletNode.connect(monitorGain);
monitorGain.connect(ctx.destination);
pulseSource.start();
pulseSourceRef.current = pulseSource;
pulseGainRef.current = pulseGain;
monitorGainRef.current = monitorGain;
clearTimer();
timerRef.current = setInterval(schedulePulse, Math.round(60000 / bpm));
setIsRunning(true);
}, [bpm, ensureResumed, clearTimer, schedulePulse, scope]);
const stopClock = useCallback(() => {
stopSource(pulseSourceRef);
disconnectNode(pulseGainRef);
disconnectNode(monitorGainRef);
scope.disconnect();
clearTimer();
setIsRunning(false);
}, [clearTimer, scope]);
const handleToggle = useCallback(() => {
if (isRunning) {
stopClock();
} else {
void startClock();
}
}, [isRunning, startClock, stopClock]);
const handleBpmChange = useCallback(
(e: Event) => {
const nextBpm = inputValue(e);
setBpm(nextBpm);
if (timerRef.current) {
clearTimer();
timerRef.current = setInterval(schedulePulse, Math.round(60000 / nextBpm));
}
},
[clearTimer, schedulePulse],
);
useEffect(() => () => stopClock(), [stopClock]);
return (
<div className={adStyles.demoContainer}>
<div className={adStyles.controlsSection}>
<div className={adStyles.controlGroup}>
<label htmlFor="clock-tempo" className={adStyles.controlLabel}>
Tempo: <span className={adStyles.valueDisplay}>{bpm} BPM</span>
</label>
<input
id="clock-tempo"
type="range"
min="40"
max="200"
value={bpm}
onChange={handleBpmChange}
className={adStyles.slider}
/>
</div>
<button
type="button"
aria-pressed={isRunning}
className={`${adStyles.playButton} ${isRunning ? adStyles.playButtonActive : ''}`}
onClick={handleToggle}
>
{isRunning ? 'Stop Clock' : 'Start Clock'}
</button>
<TimeScaleControls value={timeScale} onChange={setTimeScale} />
</div>
<WaveformDisplay
isPlaying={isRunning}
refreshMs={30}
timeScale={timeScale}
timeWindowMs={Math.min(30000, Math.max(200, periodMs * 4 * timeScale))}
scope={scope}
triggerEnabled
triggerMode="auto"
triggerLevel={0.5}
triggerEdge="rising"
triggerHoldMs={Math.max(200, periodMs)}
triggerPosition={0.05}
/>
</div>
);
}
export default function ClockDemo() {
return <BrowserOnly>{() => <ClockDemoInner />}</BrowserOnly>;
}