/ simulated_instruments / cymbal2
Sign in
simulated_instruments / cymbal2
cymbal2 / webapp / src / tutorial / figparts.tsx
76 lines · 2.3 KBCodeBlameHistory
17959acInteractive tutorial on the theory and the solverJeremy Magland 1// Shared scaffolding for the tutorial figures: a framed figure with a
2// caption, a labeled slider row, and a one-shot audio player for the demos
3// that make sound.
5import { useCallback, useRef, type ReactNode } from 'react';
7export function Figure({ caption, children }: { caption: ReactNode; children: ReactNode }) {
8 return (
9 <figure className="tut-fig">
10 {children}
11 <figcaption>{caption}</figcaption>
12 </figure>
13 );
16interface SliderProps {
17 label: string;
18 value: number;
19 min: number;
20 max: number;
21 step?: number;
22 log?: boolean;
23 format?: (v: number) => string;
24 onChange: (v: number) => void;
27export function FigSlider({ label, value, min, max, step, log, format, onChange }: SliderProps) {
28 const fmt = format ?? ((v: number) => v.toPrecision(3));
29 const toSlider = (v: number) => (log ? Math.log(v) : v);
30 const fromSlider = (v: number) => (log ? Math.exp(v) : v);
31 return (
32 <label className="tut-slider">
33 <span className="tut-slider-name">{label}</span>
34 <input
35 type="range"
36 min={toSlider(min)}
37 max={toSlider(max)}
38 step={log ? (Math.log(max) - Math.log(min)) / 400 : (step ?? (max - min) / 400)}
39 value={toSlider(value)}
40 onChange={(e) => onChange(fromSlider(Number(e.target.value)))}
41 />
42 <span className="tut-slider-val">{fmt(value)}</span>
43 </label>
44 );
47export function FigControls({ children }: { children: ReactNode }) {
48 return <div className="tut-controls">{children}</div>;
51// Play a mono Float32Array once. Each call replaces the previous source, so
52// mashing the button restarts rather than stacks.
53export function usePlayer() {
54 const ctxRef = useRef<AudioContext | null>(null);
55 const srcRef = useRef<AudioBufferSourceNode | null>(null);
56 return useCallback((samples: Float32Array<ArrayBuffer>, sampleRate: number) => {
57 let ctx = ctxRef.current;
58 if (!ctx || ctx.state === 'closed') {
59 ctx = new AudioContext();
60 ctxRef.current = ctx;
61 }
62 void ctx.resume();
63 try {
64 srcRef.current?.stop();
65 } catch {
66 // already stopped
67 }
68 const buf = ctx.createBuffer(1, samples.length, sampleRate);
69 buf.copyToChannel(samples, 0);
70 const src = ctx.createBufferSource();
71 src.buffer = buf;
72 src.connect(ctx.destination);
73 src.start();
74 srcRef.current = src;
75 }, []);
↑↓ move↵openescclose