Stop previews when arrowing or selecting
This commit is contained in:
@@ -48,6 +48,9 @@ export class AudioEngine {
|
||||
private readonly sampleLoaders = new Map<string, Promise<AudioBuffer>>();
|
||||
private readonly activeSpatialSamples = new Set<ActiveSpatialSampleRuntime>();
|
||||
|
||||
private previewSourceNode: AudioBufferSourceNode | null = null;
|
||||
private previewGainNode: GainNode | null = null;
|
||||
|
||||
private outboundSource: MediaStreamAudioSourceNode | null = null;
|
||||
private outboundInputGain: GainNode | null = null;
|
||||
private outboundInputGainValue = 1;
|
||||
@@ -491,6 +494,47 @@ export class AudioEngine {
|
||||
}
|
||||
}
|
||||
|
||||
stopPreviewSample(): void {
|
||||
if (this.previewSourceNode) {
|
||||
try { this.previewSourceNode.stop(); } catch { /* already ended */ }
|
||||
try { this.previewSourceNode.disconnect(); } catch { /* ignore */ }
|
||||
this.previewSourceNode = null;
|
||||
}
|
||||
if (this.previewGainNode) {
|
||||
try { this.previewGainNode.disconnect(); } catch { /* ignore */ }
|
||||
this.previewGainNode = null;
|
||||
}
|
||||
}
|
||||
|
||||
async playPreviewSample(url: string, gain = 1): Promise<void> {
|
||||
this.stopPreviewSample();
|
||||
await this.ensureContext();
|
||||
const { audioCtx, sfxGainNode } = this;
|
||||
if (!audioCtx || !sfxGainNode) return;
|
||||
if (gain <= 0) return;
|
||||
try {
|
||||
const buffer = await this.getSampleBuffer(url);
|
||||
this.stopPreviewSample();
|
||||
const source = audioCtx.createBufferSource();
|
||||
source.buffer = buffer;
|
||||
const gainNode = audioCtx.createGain();
|
||||
gainNode.gain.setValueAtTime(0, audioCtx.currentTime);
|
||||
gainNode.gain.setTargetAtTime(gain, audioCtx.currentTime, ONE_SHOT_ATTACK_SECONDS);
|
||||
source.connect(gainNode).connect(sfxGainNode);
|
||||
this.previewSourceNode = source;
|
||||
this.previewGainNode = gainNode;
|
||||
source.onended = () => {
|
||||
if (this.previewSourceNode === source) {
|
||||
this.previewSourceNode = null;
|
||||
this.previewGainNode = null;
|
||||
}
|
||||
};
|
||||
source.start();
|
||||
} catch {
|
||||
// Ignore decode/load errors.
|
||||
}
|
||||
}
|
||||
|
||||
/** Starts a looping sample and returns a stop callback for explicit teardown. */
|
||||
async startLoopingSample(url: string, gain = 1): Promise<(() => void) | null> {
|
||||
await this.ensureContext();
|
||||
|
||||
@@ -49,6 +49,7 @@ type EditorDeps = {
|
||||
sfxUiCancel: () => void;
|
||||
openSoundPropertyPicker?: (item: WorldItem, key: string) => void;
|
||||
previewSound?: (soundPath: string) => void;
|
||||
stopPreviewSound?: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -408,6 +409,7 @@ export function createItemPropertyEditor(deps: EditorDeps): {
|
||||
}
|
||||
|
||||
if (control.type === 'select') {
|
||||
deps.stopPreviewSound?.();
|
||||
const selectedValue = deps.state.itemPropertyOptionValues[deps.state.itemPropertyOptionIndex];
|
||||
deps.signalingSend({ type: 'item_update', itemId, params: { [propertyKey]: selectedValue } });
|
||||
const item = deps.state.items.get(itemId);
|
||||
@@ -422,6 +424,7 @@ export function createItemPropertyEditor(deps: EditorDeps): {
|
||||
}
|
||||
|
||||
if (control.type === 'cancel') {
|
||||
deps.stopPreviewSound?.();
|
||||
deps.state.mode = 'itemProperties';
|
||||
deps.state.editingPropertyKey = null;
|
||||
deps.state.itemPropertyOptionValues = [];
|
||||
|
||||
@@ -1027,17 +1027,27 @@ let previewDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
/** Plays a sound preview with debounce, for use while navigating sound picker. */
|
||||
function previewSound(soundPath: string): void {
|
||||
audio.stopPreviewSample();
|
||||
if (previewDebounceTimer !== null) {
|
||||
clearTimeout(previewDebounceTimer);
|
||||
}
|
||||
previewDebounceTimer = setTimeout(() => {
|
||||
previewDebounceTimer = null;
|
||||
if (soundPath) {
|
||||
void audio.playSample(soundPath, 0.7);
|
||||
void audio.playPreviewSample(soundPath, 0.7);
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
/** Stops any in-progress preview sound and clears the debounce timer. */
|
||||
function stopPreviewSound(): void {
|
||||
if (previewDebounceTimer !== null) {
|
||||
clearTimeout(previewDebounceTimer);
|
||||
previewDebounceTimer = null;
|
||||
}
|
||||
audio.stopPreviewSample();
|
||||
}
|
||||
|
||||
/** Opens the sound picker for a sound-typed item property, falling back to text edit if no sounds are found. */
|
||||
async function openSoundPropertyPicker(item: WorldItem, key: string): Promise<void> {
|
||||
updateStatus('Loading sounds...');
|
||||
@@ -2629,6 +2639,7 @@ const itemPropertyEditor = createItemPropertyEditor({
|
||||
sfxUiCancel: () => audio.sfxUiCancel(),
|
||||
openSoundPropertyPicker: (item, key) => { void openSoundPropertyPicker(item, key); },
|
||||
previewSound,
|
||||
stopPreviewSound,
|
||||
});
|
||||
|
||||
/** Handles nickname edit mode submission/cancel and text editing keys. */
|
||||
|
||||
Reference in New Issue
Block a user