165 lines
4.1 KiB
TypeScript
165 lines
4.1 KiB
TypeScript
import { SoundStorage } from '@/storage/storages'
|
|
|
|
interface CachedSound {
|
|
id: string
|
|
name: string
|
|
base64: string
|
|
}
|
|
|
|
// Core storage instances
|
|
const soundStorage = new SoundStorage()
|
|
const activeSounds = new Map<string, HTMLAudioElement[]>()
|
|
const audioCache = new Map<string, HTMLAudioElement>()
|
|
|
|
export function useSoundComposable() {
|
|
/**
|
|
* Converts a sound URL to base64 format
|
|
*/
|
|
async function soundToBase64(url: string): Promise<string> {
|
|
try {
|
|
const response = await fetch(url)
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`)
|
|
}
|
|
|
|
const blob = await response.blob()
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader()
|
|
reader.onloadend = () => resolve((reader.result as string).split(',')[1])
|
|
reader.onerror = reject
|
|
reader.readAsDataURL(blob)
|
|
})
|
|
} catch (error) {
|
|
console.error('Error converting sound to base64:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Preloads a sound file and caches it for future use
|
|
*/
|
|
async function preloadSound(soundUrl: string): Promise<HTMLAudioElement> {
|
|
if (audioCache.has(soundUrl)) {
|
|
return audioCache.get(soundUrl)!
|
|
}
|
|
|
|
let audio: HTMLAudioElement
|
|
const cachedSound = await soundStorage.getById(soundUrl)
|
|
|
|
if (cachedSound) {
|
|
audio = new Audio(`data:audio/mpeg;base64,${cachedSound.base64}`)
|
|
} else {
|
|
try {
|
|
const base64 = await soundToBase64(soundUrl)
|
|
const soundData: CachedSound = {
|
|
id: soundUrl,
|
|
name: soundUrl.split('/').pop() || soundUrl,
|
|
base64
|
|
}
|
|
await soundStorage.add(soundData)
|
|
audio = new Audio(`data:audio/mpeg;base64,${base64}`)
|
|
} catch (error) {
|
|
console.error(`Failed to load and cache sound ${soundUrl}:`, error)
|
|
audio = new Audio(soundUrl)
|
|
}
|
|
}
|
|
|
|
audio.load()
|
|
audioCache.set(soundUrl, audio)
|
|
return audio
|
|
}
|
|
|
|
/**
|
|
* Plays a sound with optional looping and duplicate prevention
|
|
*/
|
|
async function playSound(soundUrl: string, loop: boolean = false, ignoreIfPlaying: boolean = false): Promise<void> {
|
|
try {
|
|
const playingSounds = activeSounds.get(soundUrl) || []
|
|
|
|
if (ignoreIfPlaying && playingSounds.some((audio) => !audio.paused)) {
|
|
return
|
|
}
|
|
|
|
stopSound(soundUrl)
|
|
|
|
const audio = await preloadSound(soundUrl)
|
|
const playingAudio = audio.cloneNode() as HTMLAudioElement
|
|
playingAudio.loop = loop
|
|
|
|
if (!activeSounds.has(soundUrl)) {
|
|
activeSounds.set(soundUrl, [])
|
|
}
|
|
|
|
activeSounds.get(soundUrl)!.push(playingAudio)
|
|
|
|
// Cleanup when sound ends
|
|
playingAudio.addEventListener(
|
|
'ended',
|
|
() => {
|
|
const sounds = activeSounds.get(soundUrl)
|
|
if (!sounds) return
|
|
|
|
const index = sounds.indexOf(playingAudio)
|
|
if (index > -1) {
|
|
sounds.splice(index, 1)
|
|
}
|
|
if (sounds.length === 0) {
|
|
activeSounds.delete(soundUrl)
|
|
}
|
|
},
|
|
{ once: true }
|
|
)
|
|
|
|
await playingAudio.play()
|
|
} catch (error) {
|
|
console.error(`Failed to play sound ${soundUrl}:`, error)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stops all instances of a specific sound
|
|
*/
|
|
function stopSound(soundUrl: string): void {
|
|
const sounds = activeSounds.get(soundUrl)
|
|
if (!sounds) return
|
|
|
|
sounds.forEach((audio) => {
|
|
audio.pause()
|
|
audio.currentTime = 0
|
|
})
|
|
activeSounds.delete(soundUrl)
|
|
}
|
|
|
|
/**
|
|
* Stops all currently playing sounds
|
|
*/
|
|
function stopAllSounds(): void {
|
|
activeSounds.forEach((_, url) => stopSound(url))
|
|
activeSounds.clear()
|
|
}
|
|
|
|
/**
|
|
* Clears all cached sounds and stops playback
|
|
*/
|
|
async function clearSoundCache(): Promise<void> {
|
|
stopAllSounds()
|
|
audioCache.clear()
|
|
await soundStorage.reset()
|
|
}
|
|
|
|
/**
|
|
* Preloads multiple sounds simultaneously
|
|
*/
|
|
async function preloadSounds(soundUrls: string[]): Promise<HTMLAudioElement[]> {
|
|
return Promise.all(soundUrls.map(preloadSound))
|
|
}
|
|
|
|
return {
|
|
playSound,
|
|
stopSound,
|
|
stopAllSounds,
|
|
clearSoundCache,
|
|
preloadSounds
|
|
}
|
|
}
|