/ simulated_instruments / maraca
Sign in
simulated_instruments / maraca
maraca / webapp / src / hooks / useRenderer.ts
129 lines · 4.5 KBBlameHistoryRaw
1// Hook owning the render requests. Renders are explicit: nothing starts
2// until Render preview or Render full is pressed, so changing a parameter
3// or a .m source never kicks off work on its own. Rendering goes through
4// the repo's instrument/ module (the same track-render interface midiboot
5// uses), with the full parameter surface, the edited .m sources, and the
6// diagnostics payload passed through the request's app-facing fields.
7// Cancelling is cooperative: the worker checks between render stages, and
8// the instrument module tears the worker down only if it never responds.
10import { useCallback, useEffect, useRef, useState } from 'react';
11import type { MaracaParams } from '../params';
12import type { MFile } from '../mfiles';
13import { maracaInstrument } from '../../../instrument/index.ts';
14import type { MaracaExploreDiag } from '../../../instrument/core.ts';
16/** The old worker response shape, reassembled from the instrument result
17 * so the plot components are unchanged. */
18export type RenderResult = MaracaExploreDiag & {
19 y: Float32Array;
20 sampleRate: number;
21};
23export interface RenderError {
24 message: string;
25 file?: string;
26 line?: number;
29export interface RenderState {
30 result: RenderResult | null;
31 error: RenderError | null;
32 busy: boolean;
33 stage: string;
34 progress: number; // 0..1 through the render
35 elapsedS: number;
38export function useRenderer(params: MaracaParams, mfiles: MFile[]) {
39 const abortRef = useRef<AbortController | null>(null);
40 const startedAt = useRef(0);
41 const [state, setState] = useState<RenderState>({
42 result: null,
43 error: null,
44 busy: false,
45 stage: '',
46 progress: 0,
47 elapsedS: 0,
48 });
50 useEffect(() => () => abortRef.current?.abort(), []);
52 // elapsed-time ticker while a render is in flight
53 useEffect(() => {
54 if (!state.busy) return;
55 const t = setInterval(
56 () => setState((s) => ({ ...s, elapsedS: (performance.now() - startedAt.current) / 1000 })),
57 250,
58 );
59 return () => clearInterval(t);
60 }, [state.busy]);
62 const render = useCallback(
63 (quality: 'preview' | 'full') => {
64 if (abortRef.current) return; // a request is already in flight
65 const ctrl = new AbortController();
66 abortRef.current = ctrl;
67 startedAt.current = performance.now();
68 setState((s) => ({ ...s, busy: true, stage: 'starting', progress: 0, elapsedS: 0 }));
69 const sampleRate = Number(params.sample_rate);
70 void maracaInstrument.renderTrack({
71 notes: [],
72 sampleRate,
73 lengthSamples: 1,
74 quality,
75 params: params as unknown as Record<string, number | boolean>,
76 mfiles,
77 wantDiagnostics: true,
78 signal: ctrl.signal,
79 onProgress: (p) => setState((s) => ({ ...s, stage: p.stage, progress: p.frac })),
80 }).then((r) => {
81 if (abortRef.current === ctrl) abortRef.current = null;
82 if (ctrl.signal.aborted) return;
83 const diag = r.diag as MaracaExploreDiag;
84 setState((s) => ({
85 ...s,
86 result: { ...diag, y: r.audio, sampleRate },
87 error: null,
88 busy: false,
89 stage: '',
90 progress: 0,
91 }));
92 }).catch((err: unknown) => {
93 if (abortRef.current === ctrl) abortRef.current = null;
94 if (err instanceof DOMException && err.name === 'AbortError') {
95 setState((s) => ({ ...s, busy: false, stage: '', progress: 0 }));
96 return;
97 }
98 // A failed render leaves nothing playable: the previous result is
99 // dropped, so Play and Download cannot hand back audio that no
100 // longer corresponds to the code or parameters on screen.
101 const e = err as { message?: string; file?: string; line?: number };
102 setState((s) => ({
103 ...s,
104 result: null,
105 error: {
106 message: e?.message ? String(e.message) : String(err),
107 file: typeof e?.file === 'string' ? e.file : undefined,
108 line: typeof e?.line === 'number' ? e.line : undefined,
109 },
110 busy: false,
111 stage: '',
112 progress: 0,
113 }));
114 });
115 },
116 [params, mfiles],
117 );
119 const cancel = useCallback(() => {
120 abortRef.current?.abort();
121 abortRef.current = null;
122 setState((s) => ({ ...s, busy: false, stage: '', progress: 0, elapsedS: 0 }));
123 }, []);
125 const renderPreview = useCallback(() => render('preview'), [render]);
126 const renderFull = useCallback(() => render('full'), [render]);
128 return { ...state, renderPreview, renderFull, cancel };
↑↓ move↵openescclose