4152426Explicit preview and full renders, as in the cymbal2 appJeremy Magland 1// Hook owning the render worker. Renders are explicit: nothing starts until
2// Render preview or Render full is pressed, so changing a parameter or a .m
3// source never kicks off work on its own. A request in flight can be
4// cancelled, which tears the worker down and builds a fresh one.
6import { useCallback, useEffect, useRef, useState } from 'react';
7import type { WoodblockParams } from '../params';
c5866daReimplement the algorithm in MATLAB syntax, run it with numblJeremy Magland 8import type { MFile } from '../mfiles';
9import type { ErrorMessage, ProgressMessage, RenderResponse } from '../worker/renderWorker';
11export interface RenderError {
12 message: string;
13 file?: string;
14 line?: number;
15}
17export interface RenderState {
18 result: RenderResponse | null;
c5866daReimplement the algorithm in MATLAB syntax, run it with numblJeremy Magland 19 error: RenderError | null;
982850fWoodblock strike simulator: Python reference model and interactive web appJeremy Magland 20 busy: boolean;
21 stage: string;
4152426Explicit preview and full renders, as in the cymbal2 appJeremy Magland 22 progress: number; // 0..1 through the render
23 elapsedS: number;
26export function useRenderer(params: WoodblockParams, mfiles: MFile[]) {
982850fWoodblock strike simulator: Python reference model and interactive web appJeremy Magland 27 const workerRef = useRef<Worker | null>(null);
28 const nextId = useRef(1);
29 const inFlight = useRef<number | null>(null);
4152426Explicit preview and full renders, as in the cymbal2 appJeremy Magland 30 const startedAt = useRef(0);
982850fWoodblock strike simulator: Python reference model and interactive web appJeremy Magland 31 const [state, setState] = useState<RenderState>({
32 result: null,
982850fWoodblock strike simulator: Python reference model and interactive web appJeremy Magland 34 busy: false,
35 stage: '',
36 progress: 0,
982850fWoodblock strike simulator: Python reference model and interactive web appJeremy Magland 38 });
4152426Explicit preview and full renders, as in the cymbal2 appJeremy Magland 40 const attachHandlers = useCallback((worker: Worker) => {
c5866daReimplement the algorithm in MATLAB syntax, run it with numblJeremy Magland 41 worker.onmessage = (ev: MessageEvent<RenderResponse | ProgressMessage | ErrorMessage>) => {
982850fWoodblock strike simulator: Python reference model and interactive web appJeremy Magland 42 const msg = ev.data;
43 if (msg.type === 'progress') {
44 setState((s) => ({ ...s, stage: msg.stage, progress: msg.frac }));
45 return;
46 }
4152426Explicit preview and full renders, as in the cymbal2 appJeremy Magland 47 inFlight.current = null;
c5866daReimplement the algorithm in MATLAB syntax, run it with numblJeremy Magland 48 if (msg.type === 'error') {
4152426Explicit preview and full renders, as in the cymbal2 appJeremy Magland 49 // A failed render leaves nothing playable: the previous result is
50 // dropped, so Play and Download cannot hand back audio that no
51 // longer corresponds to the code or parameters on screen.
52 setState((s) => ({
c5866daReimplement the algorithm in MATLAB syntax, run it with numblJeremy Magland 55 error: { message: msg.message, file: msg.file, line: msg.line },
56 busy: false,
57 stage: '',
58 progress: 0,
59 }));
60 return;
982850fWoodblock strike simulator: Python reference model and interactive web appJeremy Magland 61 }
4152426Explicit preview and full renders, as in the cymbal2 appJeremy Magland 62 setState((s) => ({ ...s, result: msg, error: null, busy: false, stage: '', progress: 0 }));
982850fWoodblock strike simulator: Python reference model and interactive web appJeremy Magland 63 };
66 const makeWorker = useCallback(() => {
67 const worker = new Worker(new URL('../worker/renderWorker.ts', import.meta.url), {
68 type: 'module',
69 });
70 attachHandlers(worker);
71 workerRef.current = worker;
72 return worker;
73 }, [attachHandlers]);
75 useEffect(() => {
76 const worker = makeWorker();
982850fWoodblock strike simulator: Python reference model and interactive web appJeremy Magland 77 return () => {
78 worker.terminate();
79 workerRef.current = null;
c5866daReimplement the algorithm in MATLAB syntax, run it with numblJeremy Magland 80 inFlight.current = null;
982850fWoodblock strike simulator: Python reference model and interactive web appJeremy Magland 81 };
84 // elapsed-time ticker while a render is in flight
85 useEffect(() => {
86 if (!state.busy) return;
87 const t = setInterval(
88 () => setState((s) => ({ ...s, elapsedS: (performance.now() - startedAt.current) / 1000 })),
89 250,
90 );
91 return () => clearInterval(t);
92 }, [state.busy]);
4152426Explicit preview and full renders, as in the cymbal2 appJeremy Magland 94 const render = useCallback(
95 (quality: 'preview' | 'full') => {
96 const worker = workerRef.current;
97 if (!worker || inFlight.current !== null) return;
98 const id = nextId.current++;
99 inFlight.current = id;
100 startedAt.current = performance.now();
101 setState((s) => ({ ...s, busy: true, stage: 'starting', progress: 0, elapsedS: 0 }));
102 worker.postMessage({ type: 'render', id, params, mfiles, quality });
103 },
104 [params, mfiles],
4152426Explicit preview and full renders, as in the cymbal2 appJeremy Magland 107 // Cancelling terminates the worker outright: the .m interpreter runs a tight
108 // loop that never yields, so there is nothing to poll a flag from. The
109 // replacement worker starts with empty stage caches.
110 const cancel = useCallback(() => {
111 const worker = workerRef.current;
112 if (worker) worker.terminate();
113 inFlight.current = null;
114 makeWorker();
115 setState((s) => ({ ...s, busy: false, stage: '', progress: 0, elapsedS: 0 }));
116 }, [makeWorker]);
118 const renderPreview = useCallback(() => render('preview'), [render]);
119 const renderFull = useCallback(() => render('full'), [render]);
4152426Explicit preview and full renders, as in the cymbal2 appJeremy Magland 121 return { ...state, renderPreview, renderFull, cancel };