1ee087eCymbal designer: scripted design, thickness field, mount, single strikeJeremy Magland 1// Web Audio playback of a Float32Array signal with position reporting.
3import { normalize, type LevelMode } from "./level.ts";
5export class Player {
6 private ctx: AudioContext | null = null;
7 private source: AudioBufferSourceNode | null = null;
8 private startedAt = 0;
9 private offset = 0;
10 private length = 0;
11 onEnded: (() => void) | null = null;
13 get playing(): boolean {
14 return this.source !== null;
15 }
17 /** Current playback position [s], or null when stopped. */
18 position(): number | null {
19 if (!this.source || !this.ctx) return null;
20 return Math.min(this.length, this.offset + this.ctx.currentTime - this.startedAt);
21 }
23 play(samples: Float32Array, fs: number, offset = 0, mode: LevelMode = "peak", gainDb = 0): void {
24 this.stop();
25 if (samples.length === 0) return;
26 if (!this.ctx) this.ctx = new AudioContext({ sampleRate: fs });
27 const ctx = this.ctx;
28 const buffer = ctx.createBuffer(1, samples.length, fs);
29 buffer.getChannelData(0).set(normalize(samples, fs, mode, gainDb, 0.9));
30 const src = ctx.createBufferSource();
31 src.buffer = buffer;
32 src.connect(ctx.destination);
33 src.onended = () => {
34 if (this.source === src) {
35 this.source = null;
36 this.onEnded?.();
37 }
38 };
39 this.length = samples.length / fs;
40 this.offset = Math.max(0, Math.min(offset, this.length));
41 this.startedAt = ctx.currentTime;
42 this.source = src;
43 src.start(0, this.offset);
44 void ctx.resume();
45 }
47 stop(): void {
48 if (this.source) {
49 const s = this.source;
50 this.source = null;
51 try { s.stop(); } catch { /* already stopped */ }
52 }
53 }
54}