// 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; }, []); }