import { useCallback, useEffect, useRef, useState } from 'react'; import { DEFAULTS, loadParams, saveParams, type WoodblockParams } from './params'; import { DEFAULT_MFILES, type MFile } from './mfiles'; import { useRenderer } from './hooks/useRenderer'; import { encodeWav } from './lib/wav'; import ParamPanel from './components/ParamPanel'; import CodePanel from './components/CodePanel'; 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()); // Editable copy of the MATLAB implementation. Deliberately not persisted: // a reload restores the code the app shipped with. const [mfiles, setMfiles] = useState(() => DEFAULT_MFILES.map((f) => ({ ...f }))); const [codeOpen, setCodeOpen] = useState(false); const { result, error, busy, stage, progress, renderFull } = useRenderer(params, mfiles); 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 onChangeFile = useCallback((name: string, source: string) => { setMfiles((fs) => fs.map((f) => (f.name === name ? { ...f, source } : f))); }, []); const onResetFile = useCallback((name: string) => { setMfiles((fs) => fs.map((f) => f.name === name ? { ...f, source: DEFAULT_MFILES.find((d) => d.name === name)?.source ?? f.source } : f, ), ); }, []); const onResetAllFiles = useCallback(() => setMfiles(DEFAULT_MFILES.map((f) => ({ ...f }))), []); const codeEdited = mfiles.some( (f) => f.source !== DEFAULT_MFILES.find((d) => d.name === f.name)?.source, ); 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

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

MATLAB error

{error.file ? `${error.file}${error.line ? `:${error.line}` : ''}` : 'error'} {' '} {error.message}
Showing the last successful render. Open Code to fix the source, or revert it.
)}

Geometry — block, strike, slit, microphone

{result && ( <>

Diagnostics

Contact force

Waveform

Spectrum

Spectrogram

Mode shapes

)}
{codeOpen && ( setCodeOpen(false)} onRenderFull={renderFull} error={error} busy={busy} /> )}
); }