3534482Linear spectrogram axis by default; capture and replay the displacement movie at a chosen slowdownJeremy Magland 1// Store of the displacement movie: the frames captured during the run, quantized to 16 bits per
2// node against a running peak, plus playback timing. A frame holds w at every node at one instant
3// of simulated time; replaying them at a slowdown factor shows the plate moving.
5export interface MovieOptions {
6 /** Frames per second of *simulated* time. */
7 fps: number;
8 /** Refuse to store more than this many bytes; capture stops when the budget is reached. */
9 budgetBytes: number;
10}
12export const DEFAULT_MOVIE_FPS = 2000;
13export const DEFAULT_MOVIE_BUDGET = 256e6;
15export class Movie {
16 readonly nodes: number; readonly fps: number; readonly budgetBytes: number; readonly fs: number;
17 private readonly frames: Int16Array[] = [];
18 private readonly scales: number[] = []; // metres per unit of the quantized value
19 private readonly times: number[] = []; // simulated time of each frame [s]
20 full = false;
22 constructor(nodes: number, fs: number, opts: MovieOptions) {
23 this.nodes = nodes; this.fs = fs; this.fps = opts.fps; this.budgetBytes = opts.budgetBytes;
24 }
26 get length() { return this.frames.length; }
27 get bytes() { return this.frames.length * this.nodes * 2; }
28 /** Simulated duration covered by the frames [s]. */
29 get duration() { return this.times.length ? this.times[this.times.length - 1] : 0; }
30 frameTime(i: number) { return this.times[Math.min(Math.max(i, 0), this.times.length - 1)] ?? 0; }
32 /** Store one frame (w in metres at every node) taken at audio sample ``sample``. */
33 push(w: Float32Array, sample: number) {
34 if (this.full || w.length !== this.nodes) return;
35 if (this.bytes + this.nodes * 2 > this.budgetBytes) { this.full = true; return; }
36 let peak = 0;
37 for (let i = 0; i < w.length; i++) { const a = Math.abs(w[i]); if (a > peak) peak = a; }
38 const scale = peak > 0 ? peak / 32767 : 1;
39 const q = new Int16Array(this.nodes);
40 for (let i = 0; i < w.length; i++) q[i] = Math.round(w[i] / scale);
41 this.frames.push(q); this.scales.push(scale); this.times.push(sample / this.fs);
42 }
44 /** Dequantized frame i, written into ``out`` (allocated when omitted). */
45 read(i: number, out?: Float32Array): Float32Array {
46 const dst = out && out.length === this.nodes ? out : new Float32Array(this.nodes);
47 const k = Math.min(Math.max(i, 0), this.frames.length - 1);
48 if (k < 0) return dst;
49 const q = this.frames[k], s = this.scales[k];
50 for (let n = 0; n < this.nodes; n++) dst[n] = q[n] * s;
51 return dst;
52 }
54 /** Index of the frame nearest a simulated time [s]. */
55 indexAtTime(t: number): number {
56 if (!this.times.length) return 0;
57 let lo = 0, hi = this.times.length - 1;
58 while (lo < hi) { const mid = (lo + hi) >> 1; if (this.times[mid] < t) lo = mid + 1; else hi = mid; }
59 if (lo > 0 && Math.abs(this.times[lo - 1] - t) <= Math.abs(this.times[lo] - t)) lo--;
60 return lo;
61 }
62}
64/** Audio samples between captured frames for a target simulated frame rate (at least 1). */
65export function movieEvery(fs: number, fps: number): number {
66 return Math.max(1, Math.round(fs / Math.max(fps, 1)));
67}
69/** Drives frame playback in real time at a slowdown factor (20 = 20x slower than reality). */
70export class MoviePlayer {
71 private raf = 0; private last = 0; time = 0; // simulated time being shown [s]
72 playing = false; slowdown = 50;
73 readonly movie: Movie; private readonly onFrame: (index: number, time: number) => void;
75 constructor(movie: Movie, onFrame: (index: number, time: number) => void) { this.movie = movie; this.onFrame = onFrame; }
77 play(fromSeconds = this.time) {
78 this.stop();
79 this.time = fromSeconds >= this.movie.duration ? 0 : fromSeconds;
80 this.playing = true; this.last = performance.now();
81 const tick = () => {
82 if (!this.playing) return;
83 const now = performance.now();
84 this.time += (now - this.last) / 1000 / this.slowdown;
85 this.last = now;
86 if (this.time >= this.movie.duration) { this.time = this.movie.duration; this.seek(this.time); this.stop(); return; }
87 this.seek(this.time);
88 this.raf = requestAnimationFrame(tick);
89 };
90 this.raf = requestAnimationFrame(tick);
91 }
93 stop() { this.playing = false; if (this.raf) cancelAnimationFrame(this.raf); this.raf = 0; }
94 seek(t: number) { this.time = t; this.onFrame(this.movie.indexAtTime(t), t); }
95}