/ proof-of-concept / cymplay
Sign in
proof-of-concept / cymplay
cymplay / src / render.ts
67 lines · 3.2 KBCodeBlameHistory
9c9fa54Guide the first visit, and separate recording from playingJeremy Magland 1// Rendering a recorded performance offline.
2//
3// The WAV you can download is not a recording of what came out of the speakers: it is the same
4// hits played again through the same AudioWorklet in an OfflineAudioContext, which runs as fast
5// as the machine allows and is exactly reproducible. The whole performance is handed to the
6// processor in processorOptions, because a port message posted before startRendering is not
7// guaranteed to arrive before the first block.
8import type { SimulationConfig } from "./config/schema.ts";
9import type { Derived } from "./derived.ts";
10import type { LoggedHit } from "./hits.ts";
11import type { ModesData } from "./modes.ts";
12import { buildHitPatch } from "./patch.ts";
14export interface RenderOptions {
15 sampleRate: number;
16 leadIn: number; // silence before the first hit [s]
17 tail: number; // ring-out after the last hit [s]
18 maxPoints: number;
19 unbaffled: boolean;
22/** The hits as worklet messages, with release frames relative to ``baseFrame``. */
23export function hitMessages(modes: ModesData, derived: Derived, hits: LoggedHit[], sampleRate: number,
24 baseFrame: number, maxPointsOf: (h: LoggedHit) => number) {
25 const sorted = hits.filter((h) => h.time >= 0).slice().sort((a, b) => a.time - b.time);
26 const t0 = sorted.length ? sorted[0].time : 0;
27 const out: Record<string, unknown>[] = [];
28 for (const h of sorted) {
29 const patch = buildHitPatch(modes, h.exciter, h.r, h.phi, h.velocity, maxPointsOf(h), derived.phiFactor);
30 if (!patch) continue;
31 out.push({
32 type: "hit", id: h.id, phi: patch.phi, offsets: patch.offsets, kPoint: patch.kPoint, alpha: patch.alpha,
33 mass: patch.mass, gap: patch.gap, velocity: h.velocity,
34 atFrame: baseFrame + Math.round((h.time - t0) * sampleRate), silent: true,
35 });
36 }
37 return { messages: out, span: sorted.length ? sorted[sorted.length - 1].time - t0 : 0 };
40export function gainArray(modes: ModesData, unbaffled: boolean): Float32Array {
41 const g = new Float32Array(2 * modes.nModes);
42 for (let i = 0; i < g.length; i++) g[i] = modes.gainFront[i] - (unbaffled ? modes.gainBack[i] : 0);
43 return g;
46export async function renderRecording(workletUrl: string, modes: ModesData, _config: SimulationConfig,
47 derived: Derived, hits: LoggedHit[], opts: RenderOptions): Promise<Float32Array> {
48 const fs = opts.sampleRate;
49 const lead = Math.round(opts.leadIn * fs);
50 const { messages, span } = hitMessages(modes, derived, hits, fs, lead, () => opts.maxPoints);
51 if (!messages.length) throw new Error("there are no hits to render");
52 const frames = lead + Math.ceil(span * fs) + Math.ceil(opts.tail * fs);
53 const ctx = new OfflineAudioContext({ numberOfChannels: 1, length: frames, sampleRate: fs });
54 await ctx.audioWorklet.addModule(workletUrl);
55 const node = new AudioWorkletNode(ctx, "cymbal-modal", {
56 numberOfInputs: 0,
57 numberOfOutputs: 1,
58 outputChannelCount: [1],
59 processorOptions: {
60 init: { type: "init", freq: derived.freq, sigma: derived.sigma, gain: gainArray(modes, opts.unbaffled) },
61 hits: messages,
62 },
63 });
64 node.connect(ctx.destination);
65 const buffer = await ctx.startRendering();
66 return buffer.getChannelData(0).slice();
↑↓ move↵openescclose