feat: add opt-in procedural drive audio

This commit is contained in:
2026-09-02 05:22:13 +03:00
parent 190ac1ce62
commit 130ff2d8bb
9 changed files with 1155 additions and 3 deletions

View File

@@ -0,0 +1,91 @@
import {
onMounted,
onUnmounted,
readonly,
shallowRef,
watch,
type Ref,
} from 'vue';
import { createAudioController } from '@/audio/create-audio-controller';
import { createBrowserSynthEngine } from '@/audio/synth-engine';
import type { AudioController, AudioUiState } from '@/audio/types';
export interface AudioEngineSources {
intensity: Readonly<Ref<number>>;
volume: Readonly<Ref<number>>;
setVolume(value: number): void;
}
export interface AudioEngineBinding {
state: Readonly<Ref<AudioUiState>>;
muted: Readonly<Ref<boolean>>;
startFromGesture(): void;
resumeFromGesture(): void;
toggleMuted(): void;
setVolume(value: number): void;
}
export function useAudioEngine(sources: AudioEngineSources): AudioEngineBinding {
const state = shallowRef<AudioUiState>({ kind: 'idle' });
const muted = shallowRef(false);
let controller: AudioController | null = null;
let removeSubscription = (): void => {};
let removePageListeners = (): void => {};
const ensureController = (): AudioController => {
if (controller) return controller;
controller = createAudioController({
createGraph: createBrowserSynthEngine,
initialVolume: sources.volume.value,
initiallyVisible: document.visibilityState !== 'hidden',
});
removeSubscription = controller.subscribe((next) => { state.value = next; });
controller.setIntensity(sources.intensity.value);
return controller;
};
watch(sources.intensity, (value) => controller?.setIntensity(value));
watch(sources.volume, (value) => controller?.setVolume(value));
onMounted(() => {
ensureController();
const onVisibilityChange = (): void => {
void controller?.setVisible(document.visibilityState !== 'hidden');
};
const onPageHide = (event: PageTransitionEvent): void => { void controller?.pageHide(event.persisted); };
const onPageShow = (event: PageTransitionEvent): void => { void controller?.pageShow(event.persisted); };
document.addEventListener('visibilitychange', onVisibilityChange);
window.addEventListener('pagehide', onPageHide);
window.addEventListener('pageshow', onPageShow);
removePageListeners = () => {
document.removeEventListener('visibilitychange', onVisibilityChange);
window.removeEventListener('pagehide', onPageHide);
window.removeEventListener('pageshow', onPageShow);
};
});
onUnmounted(() => {
removePageListeners();
removeSubscription();
controller?.dispose();
controller = null;
});
return {
state: readonly(state),
muted: readonly(muted),
startFromGesture() {
// Keep this call direct: the controller constructs AudioContext synchronously here.
void ensureController().startFromGesture();
},
resumeFromGesture() { void ensureController().resumeFromGesture(); },
toggleMuted() {
muted.value = !muted.value;
ensureController().setMuted(muted.value);
},
setVolume(value) {
sources.setVolume(value);
ensureController().setVolume(value);
},
};
}