// Web Audio playback of a Float32Array signal with position reporting. import { normalize, type LevelMode } from "./level.ts"; export class Player { private ctx: AudioContext | null = null; private source: AudioBufferSourceNode | null = null; private startedAt = 0; private offset = 0; private length = 0; onEnded: (() => void) | null = null; get playing(): boolean { return this.source !== null; } /** Current playback position [s], or null when stopped. */ position(): number | null { if (!this.source || !this.ctx) return null; return Math.min(this.length, this.offset + this.ctx.currentTime - this.startedAt); } play(samples: Float32Array, fs: number, offset = 0, mode: LevelMode = "peak"): void { this.stop(); if (samples.length === 0) return; if (!this.ctx) this.ctx = new AudioContext({ sampleRate: fs }); const ctx = this.ctx; const buffer = ctx.createBuffer(1, samples.length, fs); buffer.getChannelData(0).set(normalize(samples, fs, mode, 0.9)); const src = ctx.createBufferSource(); src.buffer = buffer; src.connect(ctx.destination); src.onended = () => { if (this.source === src) { this.source = null; this.onEnded?.(); } }; this.length = samples.length / fs; this.offset = Math.max(0, Math.min(offset, this.length)); this.startedAt = ctx.currentTime; this.source = src; src.start(0, this.offset); void ctx.resume(); } stop(): void { if (this.source) { const s = this.source; this.source = null; try { s.stop(); } catch { /* already stopped */ } } } }