feat: add opt-in procedural drive audio
This commit is contained in:
91
src/composables/useAudioEngine.ts
Normal file
91
src/composables/useAudioEngine.ts
Normal 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);
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user