Files
filmcraft/apps/filmcraft-web/web/audio-worklet.js
Brandon ThomasandClaude Opus 5.5 5f613aff25 M13.2: FilmCraft web app (apps/filmcraft-web) with Blob media, OPFS recovery, WebAudio, WebCodecs
- eframe web runner on WebGPU (WebGL2 fallback; GPU compositor on WebGPU, CPU on WebGL2).
- fs: virtual file table of File/Blob handles, 1 MiB chunked async range reads with an
  LRU cache and read-ahead (pending + retry instead of blocking), downloads for written
  files, /opfs/ paths written to OPFS.
- import: File System Access picker / <input type=file>, drag-and-drop anywhere,
  container index prefetch (moov wherever it is), retried imports; project open/save.
- recovery: OPFS auto-save snapshot while dirty, media copies, reopened on next visit.
- audio: AudioWorklet output fed ahead from the UI thread; played-frame audio clock.
- webcodecs: H.264/HEVC/VP9/AV1 MP4/MOV decoded by the browser VideoDecoder via a
  reader opener (fallback to our decoders on any error), probed with isConfigSupported.
- api: window.filmcraft (execute/request/commands/screenshot/importFiles/importUrl/
  openProject/files/readFile/info).
- cargo xtask web [--dev] [--serve PORT] (wasm-bindgen CLI); xtask wasm also checks
  filmcraft-ui-egui and filmcraft-web; layer L6 for the web app.
- Original web icon (favicon.svg) with attribution sidecar; docs/web.md; headless-Chrome
  smoke test (tests/smoke.mjs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:52:35 -04:00

60 lines
1.8 KiB
JavaScript

// FilmCraft web audio output (AudioWorklet). The UI thread mixes audio ahead and posts
// interleaved stereo Float32Array blocks; this processor plays them in order and reports how many
// frames it actually played (with its currentTime), which drives the playback clock.
class FilmcraftOutput extends AudioWorkletProcessor {
constructor() {
super();
this.queue = [];
this.offset = 0;
this.gen = 0;
this.played = 0;
this.quanta = 0;
this.port.onmessage = (e) => {
const m = e.data;
if (m.type === "reset") {
this.gen = m.gen;
this.queue = [];
this.offset = 0;
this.played = 0;
this.port.postMessage({ gen: this.gen, played: 0, time: currentTime });
} else if (m.type === "data" && m.gen === this.gen) {
this.queue.push(m.samples);
}
};
}
process(_inputs, outputs) {
const out = outputs[0];
const left = out[0];
const right = out[1] || out[0];
const n = left.length;
let i = 0;
while (i < n && this.queue.length) {
const b = this.queue[0];
const k = Math.min(b.length / 2 - this.offset, n - i);
for (let j = 0; j < k; j++) {
left[i + j] = b[2 * (this.offset + j)];
right[i + j] = b[2 * (this.offset + j) + 1];
}
i += k;
this.offset += k;
if (this.offset * 2 >= b.length) {
this.queue.shift();
this.offset = 0;
}
}
// only real frames advance the clock; a starved queue plays silence and holds it
this.played += i;
for (; i < n; i++) {
left[i] = 0;
right[i] = 0;
}
if (++this.quanta % 4 === 0) {
this.port.postMessage({ gen: this.gen, played: this.played, time: currentTime });
}
return true;
}
}
registerProcessor("filmcraft-output", FilmcraftOutput);