diff --git a/components/ItemEditor.tsx b/components/ItemEditor.tsx index f5ff230..a08aeea 100644 --- a/components/ItemEditor.tsx +++ b/components/ItemEditor.tsx @@ -8,6 +8,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from ". import { SOUND_CATEGORIES, EMOJIS } from "@/lib/bingo-data"; import { Badge } from "./ui/badge"; import { ResourceBrowser, DragData } from "./ResourceBrowser"; +import { playSoundOnce } from "@/lib/sounds"; type Item = { id: string; @@ -200,10 +201,6 @@ export function ItemEditor({ campaign }: { campaign: Campaign }) { setUploading(false); }; - const playSoundOnce = (url: string) => { - try { const a = new Audio(url); a.preload = "auto"; a.volume = 0.4; a.play().catch(() => {}); } catch {} - }; - // ─── Drag & Drop handlers ─────────────────────────────────────── const handleDragOver = useCallback((e: React.DragEvent, idx: number) => { diff --git a/components/ResourceBrowser.tsx b/components/ResourceBrowser.tsx index 97363ec..1993742 100644 --- a/components/ResourceBrowser.tsx +++ b/components/ResourceBrowser.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback, useRef } from "react"; import { useDropzone } from "react-dropzone"; -import { Howl } from "howler"; +import { playSoundOnce } from "@/lib/sounds"; // ─── Types ────────────────────────────────────────────────────────── @@ -33,19 +33,6 @@ export type DragData = | { type: "image"; url: string; originalName: string } | { type: "emoji"; emoji: string }; -// ─── Howl helper ──────────────────────────────────────────────────── - -const howlCache = new Map(); - -function playPreview(url: string) { - let h = howlCache.get(url); - if (!h) { - h = new Howl({ src: [url], format: ["mp3", "ogg"], volume: 0.4, preload: true }); - howlCache.set(url, h); - } - h.play(); -} - // ─── Emoji Picker (inline, searchable) ────────────────────────────── const EMOJI_LIST = [ @@ -211,7 +198,7 @@ function SoundsTab() { label={s.originalName} duration={s.duration} url={s.url} - onPlay={() => playPreview(s.url)} + onPlay={() => playSoundOnce(s.url)} onDelete={() => deleteSound(filename)} /> ); @@ -244,7 +231,7 @@ function SoundsTab() { key={r.id} name={r.name} duration={r.duration} - onPreview={() => r.previewUrl && playPreview(r.previewUrl)} + onPreview={() => r.previewUrl && playSoundOnce(r.previewUrl)} onImport={() => importSound(r)} /> ))} diff --git a/lib/sounds.ts b/lib/sounds.ts index 602ff49..9083662 100644 --- a/lib/sounds.ts +++ b/lib/sounds.ts @@ -1,3 +1,5 @@ +import { Howl } from "howler"; + let audioCtx: AudioContext | null = null; function getCtx(): AudioContext { @@ -107,13 +109,50 @@ export function playChaosRiser() { }); } +// ─── Howl-based URL playback ────────────────────────────────────── + +const howlCache = new Map(); + +/** + * Play a sound file URL (MP3/OGG) using howler.js. + * Howler handles autoplay policy, cross-browser codec support, + * and AudioContext resume better than raw new Audio(). + */ export function playSoundUrl(url: string) { - try { - const audio = new Audio(url); - audio.preload = "auto"; - audio.volume = 0.4; - audio.play().catch(() => {}); - } catch {} + let h = howlCache.get(url); + if (!h) { + h = new Howl({ + src: [url], + format: url.endsWith(".mp3") ? ["mp3"] : url.endsWith(".ogg") ? ["ogg"] : ["mp3", "ogg"], + volume: 0.4, + onloaderror: (_id: number, err: unknown) => { + console.error("[BaraBingo] Howl load error:", url, err); + }, + onplayerror: (_id: number, err: unknown) => { + console.error("[BaraBingo] Howl play error:", url, err); + }, + }); + howlCache.set(url, h); + } + h.play(); +} + +/** + * One-shot play of a sound URL. Same as playSoundUrl but always creates a fresh instance. + * Useful for preview/editor buttons where you want it to play every click. + */ +export function playSoundOnce(url: string) { + new Howl({ + src: [url], + format: url.endsWith(".mp3") ? ["mp3"] : url.endsWith(".ogg") ? ["ogg"] : ["mp3", "ogg"], + volume: 0.4, + onloaderror: (_id: number, err: unknown) => { + console.error("[BaraBingo] Howl load error:", url, err); + }, + onplayerror: (_id: number, err: unknown) => { + console.error("[BaraBingo] Howl play error:", url, err); + }, + }).play(); } export function playSound(category: string, soundUrl?: string | null) {