perf(align): render previews on worker threads in parallel chunks, send Opus
The MIDI preview render (~2 s for a 5 min song) and auto-align ran synchronously on the server's event loop, and each preview was a 12.4 MB WAV. Renders now run as 4 parallel time chunks (2 s pre-roll, 10 ms cross-fade) on worker threads, auto-align on its own worker, and previews are mono Ogg Opus 48 kb/s via ffmpeg (1.5 MB, same timing as the WAV in Chrome). ?format=wav keeps the PCM output. Swimming: render 2.0 s -> 0.65 s + 0.6 s encode; event loop blocked 2.0 s -> <0.1 s per render. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7b7a1cc6c9
commit
3ce58e1e05
@@ -0,0 +1,52 @@
|
||||
// MIDI preview rendering split into time chunks that render in parallel (one per worker thread), then stitched.
|
||||
// Each chunk is rendered with a pre-roll (notes already sounding are re-attacked inside it and it is discarded) and a
|
||||
// short overlap that is cross-faded into the next chunk, so seams are inaudible for a preview. Pure: no I/O.
|
||||
|
||||
export interface Chunk {
|
||||
/** Audio time of the chunk's first output sample (includes the pre-roll). */
|
||||
renderStart: number;
|
||||
/** Seconds to render from `renderStart`. */
|
||||
renderDur: number;
|
||||
/** The part of the timeline this chunk owns: [from, to). */
|
||||
from: number; to: number;
|
||||
}
|
||||
|
||||
export const PREROLL_SEC = 2;
|
||||
export const XFADE_SEC = 0.01;
|
||||
/** Chunks shorter than this are not worth a worker (synth start-up, pre-roll overhead). */
|
||||
export const MIN_CHUNK_SEC = 20;
|
||||
|
||||
/** Splits [0, durSec) into at most `maxChunks` equal chunks of at least MIN_CHUNK_SEC (always at least one). */
|
||||
export function planChunks(durSec: number, maxChunks: number): Chunk[] {
|
||||
const n = Math.max(1, Math.min(maxChunks, Math.floor(durSec / MIN_CHUNK_SEC)));
|
||||
const out: Chunk[] = [];
|
||||
for (let k = 0; k < n; k++) {
|
||||
const from = (durSec * k) / n, to = k === n - 1 ? durSec : (durSec * (k + 1)) / n;
|
||||
const renderStart = k === 0 ? 0 : from - PREROLL_SEC;
|
||||
const end = k === n - 1 ? durSec : to + XFADE_SEC;
|
||||
out.push({ renderStart, renderDur: end - renderStart, from, to });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins rendered chunks (mono, `sampleRate`, as planned by `planChunks`) into one signal of round(durSec·sr) samples:
|
||||
* each chunk contributes [from, to), and its XFADE_SEC overlap past `to` fades out while the next chunk fades in.
|
||||
*/
|
||||
export function stitchChunks(chunks: Chunk[], pcm: Float32Array[], sampleRate: number, durSec: number): Float32Array {
|
||||
const n = Math.round(durSec * sampleRate), out = new Float32Array(n);
|
||||
const xf = Math.max(1, Math.round(XFADE_SEC * sampleRate));
|
||||
chunks.forEach((c, k) => {
|
||||
const x = pcm[k]!, s0 = Math.round(c.from * sampleRate), s1 = k === chunks.length - 1 ? n : Math.round(c.to * sampleRate);
|
||||
const off = Math.round(c.renderStart * sampleRate); // output index of x[0]
|
||||
for (let i = s0; i < s1; i++) {
|
||||
const v = x[i - off] ?? 0;
|
||||
// first xf samples of a later chunk fade in; the previous chunk's overlap was already added (fading out)
|
||||
out[i] = k > 0 && i - s0 < xf ? out[i]! + v * ((i - s0) / xf) : v;
|
||||
}
|
||||
if (k < chunks.length - 1) {
|
||||
for (let j = 0; j < xf && s1 + j < n; j++) out[s1 + j] = (x[s1 + j - off] ?? 0) * (1 - j / xf);
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
Reference in New Issue
Block a user