Files
GMW/design/system/16-sound-design.md
T
asepharyana d59b59a7a7 feat: migrate frontend to Astro + expand AI moderation + backend admin/runtime config
Frontend:
- migrate from Vite to Astro (astro.config.mjs, pages/, layouts/)
- add admin panel, settings page, command palette, error boundary
- refactor App.tsx, MascotChatbot, Sidebar, Header, DashboardLayout
- update API client, WebSocket, auth, dashboard features

Backend:
- add admin module and config routes
- refactor middlewares, Redis connection, WebSocket server/bridge
- add runtime config loader

Discord Gateway:
- refactor AI moderation: circuit breaker, concurrency limiter, fallback processor
- add media analysis client, Seaxng search, user profile learner
- add new drizzle migration

Shared:
- extend database schema, add new config fields
2026-07-02 00:02:41 +07:00

7.3 KiB

Sound Design — The Audio Soul

"Sound is the vocabulary of nature. It speaks to us on a primal level." — Randy Thom, Sound Designer


🎯 Filosofi Audio

Suara di BETE bukan sekadar efek — ia adalah layer konfirmasi non-visual:

  1. Subtle & non-intrusive — Volume rendah, durasi pendek
  2. Meaningful — Setiap suara punya makna spesifik
  3. Context-aware — Suara yang berbeda untuk konteks berbeda
  4. Opt-out — Semua suara bisa dimatikan

🔔 Sound Catalog

UI Feedback Sounds

Event Sound Type Duration Volume Description
Button click Pop 80ms 0.3 Subtle tick
Toggle on Click 100ms 0.3 Switch engage
Toggle off Click 100ms 0.2 Switch release
Modal open Whoosh 200ms 0.2 Soft slide
Modal close Whoosh 150ms 0.15 Quick retreat
Toast appear Ding 300ms 0.3 Notification
Error toast Buzz 200ms 0.4 Warning

Moderation Sounds

Event Sound Duration Volume Description
Message flagged Chime 400ms 0.3 Attention tone
Critical alert Siren 1s 0.5 Urgent pattern
Analysis complete Ping 200ms 0.2 Completion

Voice Channel Sounds

Event Sound Duration Volume
User joins Connect 150ms 0.2
User leaves Disconnect 150ms 0.2
Recording start Record-on 200ms 0.3
Recording stop Record-off 200ms 0.2

🎵 Audio Implementation

Sound Manager

// shared/lib/sound.ts
class SoundManager {
  private static instance: SoundManager;
  private enabled = true;
  private volume = 0.5;
  private audioCache = new Map<string, HTMLAudioElement>();

  static getInstance(): SoundManager {
    if (!this.instance) this.instance = new SoundManager();
    return this.instance;
  }

  async play(soundId: string): Promise<void> {
    if (!this.enabled) return;

    let audio = this.audioCache.get(soundId);
    if (!audio) {
      audio = new Audio(`/sounds/${soundId}.mp3`);
      this.audioCache.set(soundId, audio);
    }

    audio.volume = this.volume;
    audio.currentTime = 0;
    await audio.play().catch(() => {}); // Swallow autoplay errors
  }

  setEnabled(enabled: boolean): void { this.enabled = enabled; }
  setVolume(volume: number): void { this.volume = Math.max(0, Math.min(1, volume)); }
}

export const sound = SoundManager.getInstance();

Preloading Strategy

// Preload critical sounds on app init
function preloadSounds(): void {
  const criticalSounds = ['click', 'toggle', 'notification'];
  criticalSounds.forEach(id => {
    const audio = new Audio(`/sounds/${id}.mp3`);
    audio.preload = 'auto';
  });
}

// Call on app bootstrap
document.addEventListener('DOMContentLoaded', preloadSounds);

🎚️ Sound Settings

// features/settings/SoundSettings.tsx
function SoundSettings() {
  const [soundEnabled, setSoundEnabled] = useState(true);
  const [soundVolume, setSoundVolume] = useState(0.5);

  return (
    <Card>
      <CardHeader>
        <CardTitle>Sound</CardTitle>
      </CardHeader>
      <CardContent className="space-y-4">
        <ToggleGroup>
          <Toggle
            pressed={soundEnabled}
            onPressedChange={(v) => {
              setSoundEnabled(v);
              sound.setEnabled(v);
            }}
            label="Sound Effects"
          />
        </ToggleGroup>

        {soundEnabled && (
          <div>
            <Label>Volume</Label>
            <Slider
              value={[soundVolume]}
              onValueChange={([v]) => {
                setSoundVolume(v);
                sound.setVolume(v);
              }}
              min={0}
              max={1}
              step={0.1}
            />
          </div>
        )}
      </CardContent>
    </Card>
  );
}

🔊 Audio Feedback Implementation

React Hook

// hooks/useSound.ts
function useSound(soundId: string) {
  const play = useCallback(() => {
    sound.play(soundId);
  }, [soundId]);

  return play;
}

// Usage
function DeleteButton({ onClick }: { onClick: () => void }) {
  const playClick = useSound('click');
  const playError = useSound('error');

  const handleClick = async () => {
    playClick();
    try {
      await onClick();
    } catch {
      playError();
    }
  };

  return <Button onClick={handleClick}>Delete</Button>;
}

Toast + Sound Integration

function useToastWithSound() {
  const { toast } = useToast();

  return useCallback((t: ToastInput) => {
    toast(t);

    switch (t.type) {
      case 'success': sound.play('success'); break;
      case 'error':   sound.play('error');   break;
      case 'warning': sound.play('warning'); break;
      case 'info':    sound.play('info');    break;
    }
  }, [toast]);
}

📄 Sound File Structure

public/sounds/
├── ui/
│   ├── click.mp3           # 80ms
│   ├── toggle-on.mp3       # 100ms
│   ├── toggle-off.mp3      # 100ms
│   ├── modal-open.mp3      # 200ms
│   ├── modal-close.mp3     # 150ms
│   └── notification.mp3    # 300ms
├── moderation/
│   ├── flagged.mp3         # 400ms
│   ├── critical.mp3        # 1s
│   └── analysis-done.mp3   # 200ms
├── voice/
│   ├── user-join.mp3       # 150ms
│   ├── user-leave.mp3      # 150ms
│   ├── recording-start.mp3 # 200ms
│   └── recording-stop.mp3  # 200ms
└── _index.json             # Sound metadata

Sound Metadata

{
  "ui/click": {
    "duration": 80,
    "volume": 0.3,
    "category": "feedback",
    "critical": true
  },
  "moderation/critical": {
    "duration": 1000,
    "volume": 0.5,
    "category": "alert",
    "critical": true
  }
}

Accessibility & Sound

// Respect system accessibility settings
function shouldPlaySound(): boolean {
  // iOS: silent switch
  if (navigator.mediaSession?.playbackState === 'none') return false;
  return true;
}

// Before playing:
if (!shouldPlaySound()) return;

// User preference always wins
if (!userSettings.soundEnabled) return;

⚠️ Sound Anti-Patterns

Mandatory sounds

// ❌ JANGAN — user tidak bisa mematikan
sound.play('loud-intro-music');

// ✅ Always respect user preference
if (userSettings.soundEnabled) sound.play('subtle-click');

Long or repetitive sounds

// ❌ JANGAN — 5 detik sound effect mengganggu
sound.play('complex-jingle');

// ✅ Durasi pendek, sekali main
sound.play('quick-chime');

No audio context check

// ❌ JANGAN — play tanpa cek autoplay policy
new Audio('/sounds/click.mp3').play();

// ✅ Handle autoplay rejection
const audio = new Audio('/sounds/click.mp3');
await audio.play().catch(() => {});  // Silently fail

🔗 Referensi

Sumber Konsep
Web Audio API Audio playback
Game UX Sound Design Sound design patterns
WCAG Auditory Audio accessibility

"Suara adalah gaung ingatan — setiap klik adalah bisikan dari masa lalu." ❄️🩵