1/**
2 * One-shot playback of a sample range with a playhead callback, shared by
3 * the explorer (runs) and the clip editor (recorded clips). Only one
4 * range plays per Player; starting again stops what was playing.
5 */
6export class Player {
7 private ctx: () => AudioContext;
8 private cur: { src: AudioBufferSourceNode; ctxT0: number; startT: number; endT: number; raf: number } | null = null;
9 /** Playhead position in seconds while playing, then null once stopped. */
10 onTick: ((t: number | null) => void) | null = null;
11 /** Fired on every start and stop, for button labels. */
12 onStateChange: (() => void) | null = null;
14 constructor(ctx: () => AudioContext) {
15 this.ctx = ctx;
16 }
18 get playing(): boolean {
19 return this.cur !== null;
20 }
22 stop(): void {
23 if (!this.cur) return;
24 cancelAnimationFrame(this.cur.raf);
25 try { this.cur.src.stop(); } catch { /* already ended */ }
26 this.cur = null;
27 this.onTick?.(null);
28 this.onStateChange?.();
29 }
31 /** Play samples[startT*sr .. endT*sr); times are in seconds. */
32 start(samples: Float32Array, sr: number, startT: number, endT: number): void {
33 this.stop();
34 const s0 = Math.max(0, Math.round(startT * sr));
35 const s1 = Math.min(samples.length, Math.round(endT * sr));
36 if (s1 - s0 < 1) return;
37 const ctx = this.ctx();
38 const buf = ctx.createBuffer(1, s1 - s0, sr);
39 buf.getChannelData(0).set(samples.subarray(s0, s1));
40 const src = ctx.createBufferSource();
41 src.buffer = buf;
42 src.connect(ctx.destination);
43 src.onended = () => { if (this.cur?.src === src) this.stop(); };
44 const begin = () => {
45 src.start();
46 this.cur = { src, ctxT0: ctx.currentTime, startT, endT, raf: 0 };
47 const tick = () => {
48 if (this.cur?.src !== src) return;
49 this.onTick?.(Math.min(this.cur.endT, this.cur.startT + (ctx.currentTime - this.cur.ctxT0)));
50 this.cur.raf = requestAnimationFrame(tick);
51 };
52 this.cur.raf = requestAnimationFrame(tick);
53 this.onStateChange?.();
54 };
55 if (ctx.state === 'suspended') void ctx.resume().then(begin);
56 else begin();
57 }
58}