1import { useEffect, useMemo, useRef, useState } from "react";
2import { prepareAudio, toAudioBuffer } from "@plate-lab/core";
4export interface AudioPlayerProps {
5 /** Pressure signal [Pa] */
6 p: Float64Array;
7 fs: number;
8 label?: string;
9}
11/**
12 * Plays the radiated-pressure estimate once, exactly as long as it is. Playback
13 * level is normalized; the displayed peak is not. A clip of a few milliseconds
14 * sounds like a click, and is labelled as such rather than repeated.
15 */
16export function AudioPlayer({ p, fs, label }: AudioPlayerProps) {
17 const prep = useMemo(() => prepareAudio(p, fs), [p, fs]);
18 const ctxRef = useRef<AudioContext | null>(null);
19 const srcRef = useRef<AudioBufferSourceNode | null>(null);
20 const [playing, setPlaying] = useState(false);
21 useEffect(() => () => stop(), []);
23 const stop = () => {
24 srcRef.current?.stop();
25 srcRef.current = null;
26 setPlaying(false);
27 };
28 const play = () => {
29 if (!ctxRef.current) ctxRef.current = new AudioContext();
30 const ctx = ctxRef.current;
31 void ctx.resume();
32 stop();
33 const src = ctx.createBufferSource();
34 src.buffer = toAudioBuffer(ctx, prep);
35 src.connect(ctx.destination);
36 src.start();
37 src.onended = () => setPlaying(false);
38 srcRef.current = src;
39 setPlaying(true);
40 };
41 const short = prep.durationS < 0.1;
42 return (
43 <div className="pl-row pl-small">
44 <button className="pl-btn" onClick={playing ? stop : play} disabled={p.length < 2}>
45 {playing ? "Stop" : `Play${label ? ` ${label}` : ""}`}
46 </button>
47 <span className="pl-muted">
48 {(prep.durationS * 1000).toFixed(1)} ms at {fs} Hz, peak {prep.peakPa.toExponential(2)} Pa; normalized for playback, played once
49 {short ? ". This excerpt is too short to hear as a note; lengthen the simulated window for a longer clip" : ""}
50 </span>
51 </div>
52 );
53}