import type { OoSoundCue, SoundDriver } from "@rakazo/core"; import { CUE_RECIPES, PcmWavSynthesizer } from "@rakazo/core"; import { File, Paths } from "expo-file-system"; export class ExpoMobileSoundDriver implements SoundDriver { private cacheDir = `${Paths.cache}/earcons`; private cachedUris = new Map(); isSupported(): boolean { return true; } async play(cue: OoSoundCue): Promise { if (!CUE_RECIPES[cue]) return; // Check if HTML5 audio is present (mobile web fallback / test environment) const AudioCtor = (globalThis as { Audio?: typeof Audio }).Audio; if (typeof AudioCtor === "function" && typeof window !== "undefined") { const wav = PcmWavSynthesizer.renderCueWav(cue); const blob = new Blob([wav as Uint8Array], { type: "audio/wav" }); const url = URL.createObjectURL(blob); try { const audio = new AudioCtor(url); await audio.play().catch(() => undefined); } finally { setTimeout(() => URL.revokeObjectURL(url), 2000); } return; } // Native Expo Audio playback try { const { createAudioPlayer, setAudioModeAsync } = await import("expo-audio"); await setAudioModeAsync({ playsInSilentMode: true, interruptionMode: "mixWithOthers", shouldPlayInBackground: false, }); let uri = this.cachedUris.get(cue); if (!uri) { const wav = PcmWavSynthesizer.renderCueWav(cue); const file = new File(this.cacheDir, `earcon-${cue}.wav`); file.create({ overwrite: true }); file.write(this.bytesToBase64(wav), { encoding: "base64" }); uri = file.uri; this.cachedUris.set(cue, uri); } const player = createAudioPlayer({ uri }); player.play(); setTimeout(() => player.release(), 2000); } catch { // Best-effort degradation } } private bytesToBase64(bytes: Uint8Array): string { if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64"); let binary = ""; const chunk = 0x8000; for (let i = 0; i < bytes.length; i += chunk) { binary += String.fromCharCode(...bytes.subarray(i, i + chunk)); } return btoa(binary); } } export const mobileSound = new ExpoMobileSoundDriver();