From ed3e11b64bfd3383c0c1aa3c53777adc73d6f2be Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Sun, 7 Jun 2026 00:21:27 +0330 Subject: [PATCH] Music mute everywhere + card-draw SFX - MusicToggle: global floating button (enable/disable music from any screen; hidden on the table, which has its own audio control in its HUD). Uses sound-store toggleMusic. - Card sounds now use a synthesized card-draw "swish" (filtered noise burst with a downward sweep) for cardPlay (+ soft landing tap) and deal (a flurry), replacing the old beep tones. Verified: tsc + next build pass. Co-Authored-By: Claude Opus 4.8 --- src/app/page.tsx | 2 ++ src/components/online/MusicToggle.tsx | 38 +++++++++++++++++++++++++++ src/lib/sound.ts | 33 +++++++++++++++++++++-- 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 src/components/online/MusicToggle.tsx diff --git a/src/app/page.tsx b/src/app/page.tsx index c34a590..0ac69eb 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -20,6 +20,7 @@ import { DailyRewardModal } from "@/components/online/DailyRewardModal"; import { NotificationToaster } from "@/components/online/NotificationToaster"; import { ResumeGameBar } from "@/components/online/ResumeGameBar"; import { CelebrationOverlay } from "@/components/online/CelebrationOverlay"; +import { MusicToggle } from "@/components/online/MusicToggle"; import { PublicProfileModal } from "@/components/online/PublicProfileModal"; import { CapacitorBack } from "@/components/CapacitorBack"; import { useSessionStore } from "@/lib/session-store"; @@ -171,6 +172,7 @@ export default function Page() { + {loading && null} diff --git a/src/components/online/MusicToggle.tsx b/src/components/online/MusicToggle.tsx new file mode 100644 index 0000000..89767af --- /dev/null +++ b/src/components/online/MusicToggle.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { Music } from "lucide-react"; +import { useSoundStore } from "@/lib/sound-store"; +import { useUIStore } from "@/lib/ui-store"; +import { useI18n } from "@/lib/i18n"; + +/** + * Always-available music mute toggle (enable/disable from anywhere). Floats in a + * corner on every screen except the game table, which has its own audio control + * in its HUD. + */ +export function MusicToggle() { + const { t } = useI18n(); + const music = useSoundStore((s) => s.music); + const toggleMusic = useSoundStore((s) => s.toggleMusic); + const screen = useUIStore((s) => s.screen); + + if (screen === "game") return null; + + return ( + + ); +} diff --git a/src/lib/sound.ts b/src/lib/sound.ts index bf1375d..009f1d0 100644 --- a/src/lib/sound.ts +++ b/src/lib/sound.ts @@ -115,6 +115,32 @@ class SoundManager { notes.forEach(([freq, dur], i) => this.tone(freq, t0 + i * gap, dur, opts)); } + /** Filtered noise burst with a downward sweep — a card "swish" / draw sound. */ + private swish(start: number, dur = 0.13, opts: { gain?: number; from?: number; to?: number } = {}) { + if (!this.ctx || !this.master) return; + const ctx = this.ctx; + const buf = ctx.createBuffer(1, Math.ceil(ctx.sampleRate * dur), ctx.sampleRate); + const data = buf.getChannelData(0); + for (let i = 0; i < data.length; i++) data[i] = Math.random() * 2 - 1; + const src = ctx.createBufferSource(); + src.buffer = buf; + const bp = ctx.createBiquadFilter(); + bp.type = "bandpass"; + bp.Q.value = 0.9; + bp.frequency.setValueAtTime(opts.from ?? 3200, start); + bp.frequency.exponentialRampToValueAtTime(opts.to ?? 900, start + dur); + const g = ctx.createGain(); + const peak = opts.gain ?? 0.28; + g.gain.setValueAtTime(0.0001, start); + g.gain.exponentialRampToValueAtTime(peak, start + 0.008); + g.gain.exponentialRampToValueAtTime(0.0001, start + dur); + src.connect(bp); + bp.connect(g); + g.connect(this.master); + src.start(start); + src.stop(start + dur + 0.02); + } + play(name: Sfx) { if (!this.sfxEnabled) return; this.init(); @@ -125,11 +151,14 @@ class SoundManager { this.tone(520, t, 0.06, { type: "square", gain: 0.12 }); break; case "cardPlay": - this.tone(360, t, 0.09, { type: "triangle", gain: 0.18, to: 220 }); + // Draw-card swish + a soft low tap as it lands on the felt. + this.swish(t, 0.13, { gain: 0.3, from: 3200, to: 800 }); + this.tone(150, t + 0.08, 0.06, { type: "sine", gain: 0.12, to: 90 }); break; case "deal": + // A flurry of card-draw swishes (dealing). for (let i = 0; i < 4; i++) - this.tone(320 + i * 20, t + i * 0.08, 0.07, { type: "triangle", gain: 0.14, to: 200 }); + this.swish(t + i * 0.1, 0.1, { gain: 0.22, from: 3400, to: 1000 }); break; case "trump": this.seq([[440, 0.12], [660, 0.18]], 0.1, { type: "sine", gain: 0.25 });