import { playedSoundById } from '../../core/properties/playedSound'; import { Button, HStack, Label, Theme } from '../../ui'; import { soundLibrary } from '../../viewport/SoundLibrary'; import { TOOLBAR_ICON_SVG } from '../panels/toolbarIcons'; import type { EditorState } from '../state'; /** * What a sound asset sounds like: a button that plays it and the length the decoder reports. Both * are of the stretch the asset is cut to (#343); the waveform is in its tab. * * Playing here goes through the same library a running game uses, so what the inspector plays is * exactly what the graph will: same decode, same device, and `Stop` reaches both. */ export class SoundPreview extends HStack { private readonly length: Label; private shown = ''; private readonly stopWatching: () => void; constructor( private readonly state: EditorState, private readonly assetId: string, ) { super({ gap: Theme.gap, align: 'center' }); const play = new Button('Play', { icon: TOOLBAR_ICON_SVG.play }); play.onClick = () => soundLibrary.play(this.played(), 1, false); this.add(play); const stop = new Button('Stop', { icon: TOOLBAR_ICON_SVG.stop }); stop.onClick = () => soundLibrary.stop(this.played()); this.add(stop); this.length = new Label('', { style: 'muted' }); this.add(this.length, 1); this.showLength(); this.stopWatching = state.on('project', () => this.showLength()); } private played(): string { return playedSoundById(this.state.project, this.assetId); } /** * The length is only known once the bytes are decoded, and decoding needs an audio device. So it * is asked for rather than waited on: a preview nobody has played yet says nothing about length, * which is more honest than a number guessed from the file size. */ private showLength(): void { const name = this.played(); if (name === this.shown) return; this.shown = name; const known = soundLibrary.duration(name); if (known !== null) { this.length.setText(`${known.toFixed(2)} s`); return; } void soundLibrary.load(name).then((buffer) => { if (this.destroyed || this.shown !== name) return; this.length.setText(buffer ? `${buffer.duration.toFixed(2)} s` : 'Could not be decoded.'); }); } override destroy(options?: Parameters[0]): void { this.stopWatching(); super.destroy(options); } }