/ simulated_instruments / cymbal2
Sign in
simulated_instruments / cymbal2
cymbal2 / webapp / src / hooks / useRenderer.ts
123 lines · 4.0 KBCodeBlameHistory
1793cecTime-domain nonlinear shell cymbal after Samejima (2021)Jeremy Magland 1// Hook owning the render worker. Unlike the sibling cymbal app there is no
2// debounced auto-preview: a cymbal2 render is a full time-domain integration
fc81645Open on the paper-demo stroke, and fail renders loudlyJeremy Magland 3// and takes from under a minute (preview) to minutes (full quality) in the
1793cecTime-domain nonlinear shell cymbal after Samejima (2021)Jeremy Magland 4// browser, so renders are explicit. A request in flight can be cancelled,
5// which tears the worker down and builds a fresh one.
7import { useCallback, useEffect, useRef, useState } from 'react';
8import type { Cymbal2Params } from '../params';
9import type { MFile } from '../mfiles';
10import type { ErrorMessage, ProgressMessage, RenderResponse } from '../worker/renderWorker';
12export interface RenderError {
13 message: string;
14 file?: string;
15 line?: number;
18export interface RenderState {
19 result: RenderResponse | null;
20 error: RenderError | null;
21 busy: boolean;
22 stage: string;
a22298bShow a progress bar alongside the elapsed time while renderingJeremy Magland 23 progress: number; // 0..1 through the render
1793cecTime-domain nonlinear shell cymbal after Samejima (2021)Jeremy Magland 24 elapsedS: number;
27export function useRenderer(params: Cymbal2Params, mfiles: MFile[]) {
28 const workerRef = useRef<Worker | null>(null);
29 const nextId = useRef(1);
30 const inFlight = useRef<number | null>(null);
31 const startedAt = useRef(0);
32 const [state, setState] = useState<RenderState>({
33 result: null,
34 error: null,
35 busy: false,
36 stage: '',
39 });
41 const attachHandlers = useCallback((worker: Worker) => {
42 worker.onmessage = (ev: MessageEvent<RenderResponse | ProgressMessage | ErrorMessage>) => {
43 const msg = ev.data;
44 if (msg.type === 'progress') {
a22298bShow a progress bar alongside the elapsed time while renderingJeremy Magland 45 setState((s) => ({ ...s, stage: msg.stage, progress: msg.frac }));
47 }
48 inFlight.current = null;
49 if (msg.type === 'error') {
fc81645Open on the paper-demo stroke, and fail renders loudlyJeremy Magland 50 // A failed render leaves nothing playable: the previous result is
51 // dropped, so Play and Download cannot hand back audio that no
52 // longer corresponds to the code or parameters on screen.
1793cecTime-domain nonlinear shell cymbal after Samejima (2021)Jeremy Magland 53 setState((s) => ({
54 ...s,
1793cecTime-domain nonlinear shell cymbal after Samejima (2021)Jeremy Magland 56 error: { message: msg.message, file: msg.file, line: msg.line },
57 busy: false,
58 stage: '',
61 return;
62 }
a22298bShow a progress bar alongside the elapsed time while renderingJeremy Magland 63 setState((s) => ({ ...s, result: msg, error: null, busy: false, stage: '', progress: 0 }));
65 }, []);
67 const makeWorker = useCallback(() => {
68 const worker = new Worker(new URL('../worker/renderWorker.ts', import.meta.url), {
69 type: 'module',
70 });
71 attachHandlers(worker);
72 workerRef.current = worker;
73 return worker;
74 }, [attachHandlers]);
76 useEffect(() => {
77 const worker = makeWorker();
78 return () => {
79 worker.terminate();
80 workerRef.current = null;
81 inFlight.current = null;
82 };
83 }, [makeWorker]);
85 // elapsed-time ticker while a render is in flight
86 useEffect(() => {
87 if (!state.busy) return;
88 const t = setInterval(
89 () => setState((s) => ({ ...s, elapsedS: (performance.now() - startedAt.current) / 1000 })),
90 250,
91 );
92 return () => clearInterval(t);
93 }, [state.busy]);
95 const render = useCallback(
96 (quality: 'preview' | 'full') => {
97 const worker = workerRef.current;
98 if (!worker || inFlight.current !== null) return;
99 const id = nextId.current++;
100 inFlight.current = id;
101 startedAt.current = performance.now();
a22298bShow a progress bar alongside the elapsed time while renderingJeremy Magland 102 setState((s) => ({ ...s, busy: true, stage: 'starting', progress: 0, elapsedS: 0 }));
1793cecTime-domain nonlinear shell cymbal after Samejima (2021)Jeremy Magland 103 worker.postMessage({ type: 'render', id, params, mfiles, quality });
104 },
105 [params, mfiles],
106 );
a22298bShow a progress bar alongside the elapsed time while renderingJeremy Magland 108 // Cancelling terminates the worker outright: the .m interpreter runs a tight
109 // loop that never yields, so there is nothing to poll a flag from. The
110 // replacement worker starts with empty stage caches.
1793cecTime-domain nonlinear shell cymbal after Samejima (2021)Jeremy Magland 111 const cancel = useCallback(() => {
112 const worker = workerRef.current;
113 if (worker) worker.terminate();
114 inFlight.current = null;
115 makeWorker();
a22298bShow a progress bar alongside the elapsed time while renderingJeremy Magland 116 setState((s) => ({ ...s, busy: false, stage: '', progress: 0, elapsedS: 0 }));
1793cecTime-domain nonlinear shell cymbal after Samejima (2021)Jeremy Magland 117 }, [makeWorker]);
119 const renderPreview = useCallback(() => render('preview'), [render]);
120 const renderFull = useCallback(() => render('full'), [render]);
122 return { ...state, renderPreview, renderFull, cancel };
↑↓ move↵openescclose