@mrmartineau/use-sound
Version:
The web needs more (tasteful) sounds!
1 lines • 11.7 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../src/use-on-mount.ts"],"sourcesContent":["import React from 'react';\n\nimport useOnMount from './use-on-mount';\n\nimport { HookOptions, PlayFunction, PlayOptions, ReturnedValue } from './types';\n\nexport default function useSound<T = any>(\n src: string | string[],\n {\n id,\n volume = 1,\n playbackRate = 1,\n soundEnabled = true,\n interrupt = false,\n onload,\n ...delegated\n }: HookOptions<T> = {} as HookOptions\n) {\n const HowlConstructor = React.useRef<typeof Howl | null>(null);\n const howlerGlobalRef = React.useRef<HowlerGlobal | null>(null);\n const isHowlerLoaded = React.useRef(false);\n const latestLoadId = React.useRef(0);\n const activeSoundRef = React.useRef<Howl | null>(null);\n const activeSpritePlaybackIds = React.useRef<Map<string, Set<number>>>(\n new Map()\n );\n const isMounted = React.useRef(false);\n const hasSprite = Boolean(delegated.sprite);\n\n const [duration, setDuration] = React.useState<number | null>(null);\n const [isReady, setIsReady] = React.useState(false);\n\n const [sound, setSound] = React.useState<Howl | null>(null);\n\n // We want to lazy-load Howler, since sounds can't play on load anyway.\n useOnMount(() => {\n isMounted.current = true;\n\n import('howler').then(mod => {\n if (!isMounted.current) {\n return;\n }\n\n // Depending on the module system used, `mod` might hold\n // the export directly, or it might be under `default`.\n HowlConstructor.current = mod.Howl ?? mod.default.Howl;\n howlerGlobalRef.current = mod.Howler ?? mod.default.Howler;\n isHowlerLoaded.current = true;\n setIsReady(true);\n });\n\n return () => {\n isMounted.current = false;\n isHowlerLoaded.current = false;\n latestLoadId.current += 1;\n if (activeSoundRef.current) {\n activeSoundRef.current.stop();\n activeSoundRef.current.unload();\n activeSoundRef.current = null;\n }\n activeSpritePlaybackIds.current.clear();\n };\n });\n\n // When the `src` changes, we have to do a whole thing where we recreate\n // the Howl instance. This is because Howler doesn't expose a way to\n // tweak the sound\n React.useEffect(() => {\n if (!HowlConstructor.current || !isHowlerLoaded.current) {\n return;\n }\n\n const currentLoadId = latestLoadId.current + 1;\n latestLoadId.current = currentLoadId;\n\n const handleLoad = function(this: Howl) {\n if (typeof onload === 'function') {\n onload.call(this);\n }\n\n if (!isMounted.current || currentLoadId !== latestLoadId.current) {\n return;\n }\n\n setDuration(this.duration() * 1000);\n };\n\n const nextSound = new HowlConstructor.current({\n src: Array.isArray(src) ? src : [src],\n volume,\n rate: playbackRate,\n onload: handleLoad,\n ...delegated,\n });\n\n const previousSound = activeSoundRef.current;\n if (previousSound) {\n previousSound.stop();\n previousSound.unload();\n }\n // Any tracked playback ids belong to the previous Howl instance.\n // Clear them so stop/pause lookups never target stale ids.\n activeSpritePlaybackIds.current.clear();\n setSound(nextSound);\n activeSoundRef.current = nextSound;\n\n // The linter wants to run this effect whenever ANYTHING changes,\n // but very specifically I only want to recreate the Howl instance\n // when the `src` changes. Other changes should have no effect.\n // Passing array to the useEffect dependencies list will result in\n // ifinite loop so we need to stringify it, for more details check\n // https://github.com/facebook/react/issues/14476#issuecomment-471199055\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [JSON.stringify(src), isReady]);\n\n // Whenever volume/playbackRate are changed, change those properties\n // on the sound instance.\n React.useEffect(() => {\n if (sound) {\n sound.volume(volume);\n\n // HACK: When a sprite is defined, `sound.rate()` throws an error, because Howler tries to reset the \"_default\" sprite, which doesn't exist. This is likely a bug within Howler, but I don’t have the bandwidth to investigate, so instead, we’re ignoring playbackRate changes when a sprite is defined.\n if (!hasSprite) {\n sound.rate(playbackRate);\n }\n }\n }, [sound, volume, playbackRate, hasSprite]);\n\n const play: PlayFunction = React.useCallback(\n (options?: PlayOptions) => {\n if (typeof options === 'undefined') {\n options = {};\n }\n\n if (!sound || (!soundEnabled && !options.forceSoundEnabled)) {\n return;\n }\n\n if (interrupt) {\n sound.stop();\n activeSpritePlaybackIds.current.clear();\n }\n\n if (typeof options.playbackRate !== 'undefined' && !hasSprite) {\n sound.rate(options.playbackRate);\n }\n\n const spriteKey = options.id ?? id;\n const playbackId = sound.play(spriteKey);\n\n if (typeof spriteKey === 'string' && typeof playbackId === 'number') {\n const playbackIdsForKey =\n activeSpritePlaybackIds.current.get(spriteKey) ?? new Set<number>();\n\n playbackIdsForKey.add(playbackId);\n activeSpritePlaybackIds.current.set(spriteKey, playbackIdsForKey);\n\n sound.once(\n 'end',\n () => {\n const ids = activeSpritePlaybackIds.current.get(spriteKey);\n if (!ids) {\n return;\n }\n ids.delete(playbackId);\n if (ids.size === 0) {\n activeSpritePlaybackIds.current.delete(spriteKey);\n }\n },\n playbackId\n );\n }\n },\n [sound, soundEnabled, interrupt, hasSprite, id]\n );\n\n const stop = React.useCallback(\n (id?: string) => {\n if (!sound) {\n return;\n }\n\n if (typeof id === 'string') {\n const playbackIds = activeSpritePlaybackIds.current.get(id);\n if (playbackIds && playbackIds.size > 0) {\n playbackIds.forEach(playbackId => {\n sound.stop(playbackId);\n });\n activeSpritePlaybackIds.current.delete(id);\n return;\n }\n }\n\n sound.stop(id as any);\n\n if (typeof id === 'undefined') {\n activeSpritePlaybackIds.current.clear();\n }\n },\n [sound]\n );\n\n const pause = React.useCallback(\n (id?: string) => {\n if (!sound) {\n return;\n }\n\n if (typeof id === 'string') {\n const playbackIds = activeSpritePlaybackIds.current.get(id);\n if (playbackIds && playbackIds.size > 0) {\n playbackIds.forEach(playbackId => {\n sound.pause(playbackId);\n });\n return;\n }\n }\n\n sound.pause(id as any);\n },\n [sound]\n );\n\n const unlock = React.useCallback(() => {\n if (!howlerGlobalRef.current || !howlerGlobalRef.current.ctx) {\n return;\n }\n\n if (howlerGlobalRef.current.ctx.state === 'suspended') {\n try {\n void howlerGlobalRef.current.ctx.resume().catch(() => {\n // Ignore resume failures; unlock is a best-effort helper.\n });\n } catch {\n // Ignore sync resume errors; unlock is a best-effort helper.\n }\n }\n }, []);\n\n const returnedValue: ReturnedValue = [\n play,\n {\n sound,\n stop,\n pause,\n unlock,\n duration,\n },\n ];\n\n return returnedValue;\n}\n\nexport { useSound };\n","import * as React from 'react';\n\nexport default function useOnMount(callback: React.EffectCallback) {\n React.useEffect(callback, []);\n}\n"],"mappings":"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","names":["React","React","id"]}