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 sampleLoaders = new Map<string, Promise<AudioBuffer>>();
|
||||||
private readonly activeSpatialSamples = new Set<ActiveSpatialSampleRuntime>();
|
private readonly activeSpatialSamples = new Set<ActiveSpatialSampleRuntime>();
|
||||||
|
|
||||||
|
private previewSourceNode: AudioBufferSourceNode | null = null;
|
||||||
|
private previewGainNode: GainNode | null = null;
|
||||||
|
|
||||||
private outboundSource: MediaStreamAudioSourceNode | null = null;
|
private outboundSource: MediaStreamAudioSourceNode | null = null;
|
||||||
private outboundInputGain: GainNode | null = null;
|
private outboundInputGain: GainNode | null = null;
|
||||||
private outboundInputGainValue = 1;
|
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. */
|
/** Starts a looping sample and returns a stop callback for explicit teardown. */
|
||||||
async startLoopingSample(url: string, gain = 1): Promise<(() => void) | null> {
|
async startLoopingSample(url: string, gain = 1): Promise<(() => void) | null> {
|
||||||
await this.ensureContext();
|
await this.ensureContext();
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ type EditorDeps = {
|
|||||||
sfxUiCancel: () => void;
|
sfxUiCancel: () => void;
|
||||||
openSoundPropertyPicker?: (item: WorldItem, key: string) => void;
|
openSoundPropertyPicker?: (item: WorldItem, key: string) => void;
|
||||||
previewSound?: (soundPath: string) => void;
|
previewSound?: (soundPath: string) => void;
|
||||||
|
stopPreviewSound?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -408,6 +409,7 @@ export function createItemPropertyEditor(deps: EditorDeps): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (control.type === 'select') {
|
if (control.type === 'select') {
|
||||||
|
deps.stopPreviewSound?.();
|
||||||
const selectedValue = deps.state.itemPropertyOptionValues[deps.state.itemPropertyOptionIndex];
|
const selectedValue = deps.state.itemPropertyOptionValues[deps.state.itemPropertyOptionIndex];
|
||||||
deps.signalingSend({ type: 'item_update', itemId, params: { [propertyKey]: selectedValue } });
|
deps.signalingSend({ type: 'item_update', itemId, params: { [propertyKey]: selectedValue } });
|
||||||
const item = deps.state.items.get(itemId);
|
const item = deps.state.items.get(itemId);
|
||||||
@@ -422,6 +424,7 @@ export function createItemPropertyEditor(deps: EditorDeps): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (control.type === 'cancel') {
|
if (control.type === 'cancel') {
|
||||||
|
deps.stopPreviewSound?.();
|
||||||
deps.state.mode = 'itemProperties';
|
deps.state.mode = 'itemProperties';
|
||||||
deps.state.editingPropertyKey = null;
|
deps.state.editingPropertyKey = null;
|
||||||
deps.state.itemPropertyOptionValues = [];
|
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. */
|
/** Plays a sound preview with debounce, for use while navigating sound picker. */
|
||||||
function previewSound(soundPath: string): void {
|
function previewSound(soundPath: string): void {
|
||||||
|
audio.stopPreviewSample();
|
||||||
if (previewDebounceTimer !== null) {
|
if (previewDebounceTimer !== null) {
|
||||||
clearTimeout(previewDebounceTimer);
|
clearTimeout(previewDebounceTimer);
|
||||||
}
|
}
|
||||||
previewDebounceTimer = setTimeout(() => {
|
previewDebounceTimer = setTimeout(() => {
|
||||||
previewDebounceTimer = null;
|
previewDebounceTimer = null;
|
||||||
if (soundPath) {
|
if (soundPath) {
|
||||||
void audio.playSample(soundPath, 0.7);
|
void audio.playPreviewSample(soundPath, 0.7);
|
||||||
}
|
}
|
||||||
}, 200);
|
}, 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. */
|
/** 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> {
|
async function openSoundPropertyPicker(item: WorldItem, key: string): Promise<void> {
|
||||||
updateStatus('Loading sounds...');
|
updateStatus('Loading sounds...');
|
||||||
@@ -2629,6 +2639,7 @@ const itemPropertyEditor = createItemPropertyEditor({
|
|||||||
sfxUiCancel: () => audio.sfxUiCancel(),
|
sfxUiCancel: () => audio.sfxUiCancel(),
|
||||||
openSoundPropertyPicker: (item, key) => { void openSoundPropertyPicker(item, key); },
|
openSoundPropertyPicker: (item, key) => { void openSoundPropertyPicker(item, key); },
|
||||||
previewSound,
|
previewSound,
|
||||||
|
stopPreviewSound,
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Handles nickname edit mode submission/cancel and text editing keys. */
|
/** Handles nickname edit mode submission/cancel and text editing keys. */
|
||||||
|
|||||||
Reference in New Issue
Block a user