import { useCallback, useEffect, useRef, useState } from 'react'; import { DEFAULTS, loadParams, saveParams, type WoodblockParams } from './params'; import { useRenderer } from './hooks/useRenderer'; import { encodeWav } from './lib/wav'; import ParamPanel from './components/ParamPanel'; import Scene3D from './components/Scene3D'; import DiagnosticsPanel from './components/DiagnosticsPanel'; import WaveformPlot from './components/plots/WaveformPlot'; import SpectrumPlot from './components/plots/SpectrumPlot'; import SpectrogramPlot from './components/plots/SpectrogramPlot'; import ContactForcePlot from './components/plots/ContactForcePlot'; import ModeShapes from './components/plots/ModeShapes'; export default function App() { const [params, setParams] = useState(() => loadParams()); const { result, busy, stage, progress, renderFull } = useRenderer(params); const audioCtxRef = useRef(null); useEffect(() => saveParams(params), [params]); const onChange = useCallback((key: keyof WoodblockParams, value: number | boolean) => { setParams((p) => ({ ...p, [key]: value })); }, []); const reset = useCallback(() => setParams({ ...DEFAULTS }), []); const [looping, setLooping] = useState(false); const loopSrcRef = useRef(null); const getAudioCtx = useCallback((sampleRate: number) => { let ctx = audioCtxRef.current; if (!ctx || ctx.state === 'closed') { ctx = new AudioContext({ sampleRate }); audioCtxRef.current = ctx; } void ctx.resume(); return ctx; }, []); const makeBuffer = useCallback((ctx: AudioContext, r: NonNullable) => { const buf = ctx.createBuffer(1, r.y.length, r.sampleRate); buf.copyToChannel(new Float32Array(r.y), 0); return buf; }, []); const play = useCallback(() => { if (!result) return; const ctx = getAudioCtx(result.sampleRate); const src = ctx.createBufferSource(); src.buffer = makeBuffer(ctx, result); src.connect(ctx.destination); src.start(); }, [result, getAudioCtx, makeBuffer]); // Loop mode: repeat the strike continuously, swapping in each new render as // parameters change so edits are audible without pressing Play. useEffect(() => { if (!looping || !result) return; const ctx = getAudioCtx(result.sampleRate); const src = ctx.createBufferSource(); src.buffer = makeBuffer(ctx, result); src.loop = true; src.connect(ctx.destination); src.start(); loopSrcRef.current = src; return () => { loopSrcRef.current = null; try { src.stop(); } catch { // already stopped } src.disconnect(); }; }, [looping, result, getAudioCtx, makeBuffer]); const download = useCallback(() => { if (!result) return; const blob = encodeWav(result.y, result.sampleRate, params.bit_depth === 16 ? 16 : 24); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = result.quality === 'full' ? 'woodblock.wav' : 'woodblock_preview.wav'; a.click(); setTimeout(() => URL.revokeObjectURL(url), 5000); }, [result, params.bit_depth]); return (

Woodblock Simulator

{result && ( {result.quality === 'full' ? 'full quality' : 'preview quality — press Render'} )} {busy ? ( <> {stage}…
) : ( ready )}

Geometry — block, strike, slit, microphone

{result && ( <>

Diagnostics

Contact force

Waveform

Spectrum

Spectrogram

Mode shapes

)}
); }