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 });