/ simulated_instruments / woodblock
Sign in
simulated_instruments / woodblock
woodblock / webapp / src / App.tsx
252 lines · 8.9 KBCodeBlameHistory
982850fWoodblock strike simulator: Python reference model and interactive web appJeremy Magland 1import { useCallback, useEffect, useRef, useState } from 'react';
2import { DEFAULTS, loadParams, saveParams, type WoodblockParams } from './params';
c5866daReimplement the algorithm in MATLAB syntax, run it with numblJeremy Magland 3import { DEFAULT_MFILES, type MFile } from './mfiles';
982850fWoodblock strike simulator: Python reference model and interactive web appJeremy Magland 4import { useRenderer } from './hooks/useRenderer';
5import { encodeWav } from './lib/wav';
6import ParamPanel from './components/ParamPanel';
c5866daReimplement the algorithm in MATLAB syntax, run it with numblJeremy Magland 7import CodePanel from './components/CodePanel';
982850fWoodblock strike simulator: Python reference model and interactive web appJeremy Magland 8import Scene3D from './components/Scene3D';
9import DiagnosticsPanel from './components/DiagnosticsPanel';
10import WaveformPlot from './components/plots/WaveformPlot';
11import SpectrumPlot from './components/plots/SpectrumPlot';
12import SpectrogramPlot from './components/plots/SpectrogramPlot';
13import ContactForcePlot from './components/plots/ContactForcePlot';
14import ModeShapes from './components/plots/ModeShapes';
16export default function App() {
17 const [params, setParams] = useState<WoodblockParams>(() => loadParams());
c5866daReimplement the algorithm in MATLAB syntax, run it with numblJeremy Magland 18 // Editable copy of the MATLAB implementation. Deliberately not persisted:
19 // a reload restores the code the app shipped with.
20 const [mfiles, setMfiles] = useState<MFile[]>(() => DEFAULT_MFILES.map((f) => ({ ...f })));
21 const [codeOpen, setCodeOpen] = useState(false);
22 const { result, error, busy, stage, progress, renderFull } = useRenderer(params, mfiles);
982850fWoodblock strike simulator: Python reference model and interactive web appJeremy Magland 23 const audioCtxRef = useRef<AudioContext | null>(null);
25 useEffect(() => saveParams(params), [params]);
27 const onChange = useCallback((key: keyof WoodblockParams, value: number | boolean) => {
28 setParams((p) => ({ ...p, [key]: value }));
29 }, []);
31 const reset = useCallback(() => setParams({ ...DEFAULTS }), []);
c5866daReimplement the algorithm in MATLAB syntax, run it with numblJeremy Magland 33 const onChangeFile = useCallback((name: string, source: string) => {
34 setMfiles((fs) => fs.map((f) => (f.name === name ? { ...f, source } : f)));
35 }, []);
37 const onResetFile = useCallback((name: string) => {
38 setMfiles((fs) =>
39 fs.map((f) =>
40 f.name === name
41 ? { ...f, source: DEFAULT_MFILES.find((d) => d.name === name)?.source ?? f.source }
42 : f,
43 ),
44 );
45 }, []);
47 const onResetAllFiles = useCallback(() => setMfiles(DEFAULT_MFILES.map((f) => ({ ...f }))), []);
48 const codeEdited = mfiles.some(
49 (f) => f.source !== DEFAULT_MFILES.find((d) => d.name === f.name)?.source,
50 );
982850fWoodblock strike simulator: Python reference model and interactive web appJeremy Magland 52 const [looping, setLooping] = useState(false);
53 const loopSrcRef = useRef<AudioBufferSourceNode | null>(null);
55 const getAudioCtx = useCallback((sampleRate: number) => {
56 let ctx = audioCtxRef.current;
57 if (!ctx || ctx.state === 'closed') {
58 ctx = new AudioContext({ sampleRate });
59 audioCtxRef.current = ctx;
60 }
61 void ctx.resume();
62 return ctx;
63 }, []);
65 const makeBuffer = useCallback((ctx: AudioContext, r: NonNullable<typeof result>) => {
66 const buf = ctx.createBuffer(1, r.y.length, r.sampleRate);
67 buf.copyToChannel(new Float32Array(r.y), 0);
68 return buf;
69 }, []);
71 const play = useCallback(() => {
72 if (!result) return;
73 const ctx = getAudioCtx(result.sampleRate);
74 const src = ctx.createBufferSource();
75 src.buffer = makeBuffer(ctx, result);
76 src.connect(ctx.destination);
77 src.start();
78 }, [result, getAudioCtx, makeBuffer]);
80 // Loop mode: repeat the strike continuously, swapping in each new render as
81 // parameters change so edits are audible without pressing Play.
82 useEffect(() => {
83 if (!looping || !result) return;
84 const ctx = getAudioCtx(result.sampleRate);
85 const src = ctx.createBufferSource();
86 src.buffer = makeBuffer(ctx, result);
87 src.loop = true;
88 src.connect(ctx.destination);
89 src.start();
90 loopSrcRef.current = src;
91 return () => {
92 loopSrcRef.current = null;
93 try {
94 src.stop();
95 } catch {
96 // already stopped
97 }
98 src.disconnect();
99 };
100 }, [looping, result, getAudioCtx, makeBuffer]);
102 const download = useCallback(() => {
103 if (!result) return;
104 const blob = encodeWav(result.y, result.sampleRate, params.bit_depth === 16 ? 16 : 24);
105 const url = URL.createObjectURL(blob);
106 const a = document.createElement('a');
107 a.href = url;
108 a.download = result.quality === 'full' ? 'woodblock.wav' : 'woodblock_preview.wav';
109 a.click();
110 setTimeout(() => URL.revokeObjectURL(url), 5000);
111 }, [result, params.bit_depth]);
113 return (
114 <div className="app">
115 <div className="topbar">
116 <h1>Woodblock Simulator</h1>
117 <button className="primary" onClick={renderFull} disabled={busy}>
118 Render
119 </button>
120 <button
121 className={`play ${result?.quality === 'preview' ? 'preview' : ''}`}
122 onClick={play}
123 disabled={!result}
124 title={
125 result?.quality === 'preview'
126 ? 'Playing the fast preview render; press Render for full quality'
127 : 'Play the rendered strike'
128 }
129 >
130 ▶ Play{result?.quality === 'preview' ? ' (preview)' : ''}
131 </button>
132 <button
133 className={`loop ${looping ? 'active' : ''}`}
134 onClick={() => setLooping((v) => !v)}
135 disabled={!result && !looping}
136 title="Repeat the strike continuously so parameter changes are audible immediately"
137 >
138 ⟳ Loop{looping ? ' on' : ''}
139 </button>
140 <button onClick={download} disabled={!result}>
141 Download .wav{result?.quality === 'preview' ? ' (preview)' : ''}
142 </button>
143 <button onClick={reset}>Reset to defaults</button>
145 className={codeOpen ? 'code active' : 'code'}
146 onClick={() => setCodeOpen((v) => !v)}
147 title="View and edit the MATLAB source that numbl executes"
148 >
149 {'{ }'} Code{codeEdited ? ' •' : ''}
150 </button>
c5866daReimplement the algorithm in MATLAB syntax, run it with numblJeremy Magland 152 {error && <span className="badge err">code error</span>}
154 <span className={`badge ${result.quality}`}>
155 {result.quality === 'full' ? 'full quality' : 'preview quality — press Render'}
156 </span>
157 )}
158 {busy ? (
159 <>
160 <span>{stage}…</span>
161 <div className="progressbar">
162 <div style={{ width: `${Math.round(progress * 100)}%` }} />
163 </div>
164 </>
165 ) : (
166 <span>ready</span>
167 )}
168 </div>
169 </div>
170 <div className="main">
171 <div className="sidebar">
172 <ParamPanel params={params} onChange={onChange} />
173 </div>
174 <div className="content">
176 <div className="card wide error-card">
177 <h2>MATLAB error</h2>
178 <div>
179 <strong>
180 {error.file ? `${error.file}${error.line ? `:${error.line}` : ''}` : 'error'}
181 </strong>{' '}
182 {error.message}
183 </div>
184 <div className="code-hint">
185 Showing the last successful render. Open Code to fix the source, or revert it.
186 </div>
187 </div>
188 )}
190 <h2>Geometry — block, strike, slit, microphone</h2>
191 <Scene3D params={params} />
192 </div>
193 {result && (
194 <>
195 <div className="card">
196 <h2>Diagnostics</h2>
197 <DiagnosticsPanel result={result} />
198 </div>
199 <div className="card">
200 <h2>Contact force</h2>
201 <ContactForcePlot
202 force={result.force}
203 forceFsHz={result.forceFsHz}
204 peakForceN={result.diag.peakForceN}
205 contactMs={result.diag.contactMs}
206 />
207 </div>
208 <div className="card">
209 <h2>Waveform</h2>
210 <WaveformPlot y={result.y} sampleRate={result.sampleRate} />
211 </div>
212 <div className="card">
213 <h2>Spectrum</h2>
214 <SpectrumPlot
215 y={result.y}
216 sampleRate={result.sampleRate}
217 partialsHz={result.diag.partialsHz}
218 helmholtzHz={result.diag.helmholtzHz}
219 />
220 </div>
221 <div className="card">
222 <h2>Spectrogram</h2>
223 <SpectrogramPlot y={result.y} sampleRate={result.sampleRate} />
224 </div>
225 <div className="card">
226 <h2>Mode shapes</h2>
227 <ModeShapes
228 modeShapes={result.modeShapes}
229 shapePts={result.shapePts}
230 partialsHz={result.diag.partialsHz}
231 strikePosition={params.strike_position}
232 />
233 </div>
234 </>
235 )}
236 </div>
237 </div>
239 <CodePanel
240 mfiles={mfiles}
241 onChangeFile={onChangeFile}
242 onResetFile={onResetFile}
243 onResetAll={onResetAllFiles}
244 onClose={() => setCodeOpen(false)}
245 onRenderFull={renderFull}
246 error={error}
247 busy={busy}
248 />
249 )}
251 );
↑↓ move↵openescclose