// In-memory record of the audio-rate normal acceleration of every node, quantized to int16 // against one float32 scale per sample, exactly the encoding of the .cymsim file (see // cymsim/docs/format.md, section 6), so that exporting copies blocks without re-encoding and // loading keeps the file's blocks as they are. Recording is opt-in (output.record_acceleration): // at the default mesh it costs about 0.4 GB per simulated second. export interface AccelerationBlock { first: number; count: number; data: Int16Array; scale: Float32Array } export class AccelerationStore { readonly nodes: number; readonly fs: number; readonly blocks: AccelerationBlock[] = []; nSamples = 0; constructor(nodes: number, fs: number) { this.nodes = nodes; this.fs = fs; } get bytes() { return this.nSamples * (2 * this.nodes + 4); } /** Bytes a full record of ``nSamples`` samples takes in memory and in the file. */ static estimateBytes(nSamples: number, nodes: number): number { return nSamples * (2 * nodes + 4); } /** Encode ``count`` rows of float accelerations (row-major, N per row) that continue the record at ``first``. */ push(rows: Float32Array, count: number, first: number) { if (count <= 0) return; if (first !== this.nSamples) throw new Error(`acceleration record is not contiguous: got sample ${first}, expected ${this.nSamples}`); const N = this.nodes, data = new Int16Array(count * N), scale = new Float32Array(count); for (let r = 0; r < count; r++) { let peak = 0; const b = r * N; for (let i = 0; i < N; i++) { const a = Math.abs(rows[b + i]); if (a > peak) peak = a; } const s = peak > 0 ? peak / 32767 : 0; scale[r] = s; if (s > 0) for (let i = 0; i < N; i++) data[b + i] = Math.round(rows[b + i] / s); } this.blocks.push({ first, count, data, scale }); this.nSamples += count; } /** Append an already encoded block (from a file). */ pushEncoded(data: Int16Array, scale: Float32Array) { const count = scale.length; if (count === 0) return; if (data.length !== count * this.nodes) throw new Error("encoded acceleration block has the wrong size"); this.blocks.push({ first: this.nSamples, count, data, scale }); this.nSamples += count; } /** Decoded accelerations [m/s^2] of samples [j0, j1), row-major. */ read(j0: number, j1: number): Float32Array { j0 = Math.max(0, j0); j1 = Math.min(this.nSamples, j1); const N = this.nodes, out = new Float32Array(Math.max(0, j1 - j0) * N); for (const b of this.blocks) { const lo = Math.max(j0, b.first), hi = Math.min(j1, b.first + b.count); for (let j = lo; j < hi; j++) { const s = b.scale[j - b.first], src = (j - b.first) * N, dst = (j - j0) * N; for (let i = 0; i < N; i++) out[dst + i] = b.data[src + i] * s; } } return out; } } /** The transverse displacement frames kept for the .cymsim file (a strobe for display, not a signal). */ export class DisplacementStore { readonly nodes: number; readonly rate: number; readonly times: number[] = []; readonly frames: Float32Array[] = []; constructor(nodes: number, rate: number) { this.nodes = nodes; this.rate = rate; } push(w: Float32Array, time: number) { if (w.length === this.nodes) { this.frames.push(w); this.times.push(time); } } get bytes() { return this.frames.length * this.nodes * 4; } }