// Shared scaffolding for the tutorial figures: a framed figure with a
// caption, a labeled slider row, and a one-shot audio player for the demos
// that make sound.
import { useCallback, useRef, type ReactNode } from 'react';
export function Figure({ caption, children }: { caption: ReactNode; children: ReactNode }) {
return (
{children}
{caption}
);
}
interface SliderProps {
label: string;
value: number;
min: number;
max: number;
step?: number;
log?: boolean;
format?: (v: number) => string;
onChange: (v: number) => void;
}
export function FigSlider({ label, value, min, max, step, log, format, onChange }: SliderProps) {
const fmt = format ?? ((v: number) => v.toPrecision(3));
const toSlider = (v: number) => (log ? Math.log(v) : v);
const fromSlider = (v: number) => (log ? Math.exp(v) : v);
return (
);
}
export function FigControls({ children }: { children: ReactNode }) {
return
{children}
;
}
// Play a mono Float32Array once. Each call replaces the previous source, so
// mashing the button restarts rather than stacks.
export function usePlayer() {
const ctxRef = useRef(null);
const srcRef = useRef(null);
return useCallback((samples: Float32Array, sampleRate: number) => {
let ctx = ctxRef.current;
if (!ctx || ctx.state === 'closed') {
ctx = new AudioContext();
ctxRef.current = ctx;
}
void ctx.resume();
try {
srcRef.current?.stop();
} catch {
// already stopped
}
const buf = ctx.createBuffer(1, samples.length, sampleRate);
buf.copyToChannel(samples, 0);
const src = ctx.createBufferSource();
src.buffer = buf;
src.connect(ctx.destination);
src.start();
srcRef.current = src;
}, []);
}