- commit
- e02d208e32297a4e82ff0d2f0385b67d91628567
- parent
- c42cf423db5a9c6614451f91d51c556be59fc240
- author
- Dami <dami.etoile@gmail.com>
| M | public/ | 4 | ++ |
| A | public/ | bin | |
| M | src/ | 67 | +++++++++++++++++++++++++++++- |
Binary file.
@@ -24,6 +24,7 @@ const DEFAULT_CLIPS = Object.freeze({
2424 confirm: ['confirm.ogg'],2525 back: ['back.ogg'],2626 scene: ['scene.ogg'],27+ music: ['music-flowing-rocks.ogg'],2728 });2829 2930 const ALIASES = Object.freeze({@@ -70,6 +71,14 @@ function writeEnabled(storage, key, enabled) {
7071 }7172 }7273 74+function readOptIn(storage, key) {75+ try {76+ return storage?.getItem(key) === '1';77+ } catch {78+ return false;79+ }80+}81+7382 function choose(list, random = Math.random) {7483 return list[Math.floor(random() * list.length)] || list[0];7584 }@@ -84,15 +93,18 @@ export function createAudioSystem({
8493 basePath = '/audio/',8594 storage = availableStorage(),8695 storageKey = 'miau.sound.enabled',96+ musicStorageKey = 'miau.music.enabled',8797 clips = DEFAULT_CLIPS,8898 AudioContextCtor = globalThis.AudioContext || globalThis.webkitAudioContext,8999 fetchImpl = globalThis.fetch,90100 random = Math.random,91101 } = {}) {92102 let enabled = readEnabled(storage, storageKey);93- let musicEnabled = false;103+ let musicEnabled = readOptIn(storage, musicStorageKey);94104 let context = null;95105 let masterGain = null;106+ let musicGain = null;107+ let musicSource = null;96108 let unlocking = null;97109 let listenersTarget = null;98110 let removeGestureListeners = null;@@ -124,6 +136,9 @@ export function createAudioSystem({
124136 context ||= new AudioContextCtor();125137 masterGain ||= context.createGain();126138 masterGain.connect(context.destination);139+ musicGain ||= context.createGain();140+ musicGain.gain.value = 1;141+ musicGain.connect(masterGain);127142 setMasterGain();128143 if (context.state === 'suspended') await context.resume();129144 await preload;@@ -156,6 +171,7 @@ export function createAudioSystem({
156171 listenersTarget = target;157172 const onGesture = () => {158173 void unlock().then(() => {174+ if (musicEnabled) playMusic();159175 if (!listenersTarget) return;160176 for (const event of ['pointerdown', 'touchstart', 'keydown']) listenersTarget.removeEventListener(event, onGesture, true);161177 listenersTarget = null;@@ -203,8 +219,53 @@ export function createAudioSystem({
203219 return mute(enabled);204220 }205221 222+ function stopMusic() {223+ if (!musicSource) return false;224+ try { musicSource.stop(); } catch { /* already stopped */ }225+ musicSource.disconnect?.();226+ musicSource = null;227+ return true;228+ }229+230+ function playMusic(name = 'music', { volume = 0.126, rate = 1, offset = 0 } = {}) {231+ if (!musicEnabled || disposed) return false;232+ const resolved = ALIASES[name] || name;233+ if (!context || !musicGain) {234+ void unlock().then(() => playMusic(name, { volume, rate, offset }));235+ return false;236+ }237+ const available = buffers.get(resolved);238+ if (!available?.length) return false;239+ stopMusic();240+ const source = context.createBufferSource();241+ source.buffer = choose(available, random);242+ source.playbackRate.value = rate;243+ source.loop = true;244+ const gain = context.createGain();245+ gain.gain.value = volume;246+ source.connect(gain).connect(musicGain);247+ source.start(0, offset);248+ musicSource = source;249+ return true;250+ }251+252+ function setMusicEnabled(value, { track = 'music' } = {}) {253+ musicEnabled = Boolean(value);254+ try {255+ if (musicEnabled) storage?.setItem(musicStorageKey, '1');256+ else storage?.removeItem(musicStorageKey);257+ } catch {258+ // Storage may be unavailable in private browsing or embedded previews.259+ }260+ if (musicEnabled) {261+ if (context) void unlock().then(() => playMusic(track));262+ } else stopMusic();263+ return musicEnabled;264+ }265+206266 function dispose() {207267 disposed = true;268+ stopMusic();208269 removeGestureListeners?.();209270 return context?.close?.();210271 }@@ -214,11 +275,13 @@ export function createAudioSystem({
214275 unlock,215276 installGestureListeners,216277 play,278+ playMusic,279+ stopMusic,217280 mute,218281 toggleMute,219282 isMuted: () => !enabled,220283 isMusicEnabled: () => musicEnabled,221- setMusicEnabled: value => { musicEnabled = Boolean(value); return musicEnabled; },284+ setMusicEnabled,222285 getStatus: () => ({ enabled, muted: !enabled, unlocked: Boolean(context), ready: buffers.size > 0, errors: [...loadErrors.keys()] }),223286 dispose,224287 };