# Audio Playback Feature - E2E Documentation ## Overview The Tour Builder Platform implements a comprehensive audio playback system supporting background audio on pages, element-triggered audio effects (hover/click sounds), and media player elements. Key features: - **User Interaction Unlock**: All audio waits for first user click/tap before playing (consistent across all browsers) - **Sound Effects Layering**: Hover/click effects play over background audio (no interruption) - **Media Player Ducking**: AudioPlayer and VideoPlayer elements pause background audio when playing - **Constructor Modes**: Edit mode silences audio; Interact mode enables full audio playback --- ## Architecture ``` ┌─────────────────────────────────────────────────────────────────┐ │ Audio Types │ │ Background Audio │ Element Audio Effects │ │ (page-level) │ (hover/click sounds) │ └─────────────────────────────────────────────────────────────────┘ │ ┌─────────────────────────────────────────────────────────────────┐ │ Playback Hooks │ │ useBackgroundAudioPlayback │ useAudioEffects │ useAudioEventMgr│ └─────────────────────────────────────────────────────────────────┘ │ ┌─────────────────────────────────────────────────────────────────┐ │ Controller Layer │ │ backgroundAudioController (singleton for audio ducking) │ └─────────────────────────────────────────────────────────────────┘ │ ┌─────────────────────────────────────────────────────────────────┐ │ Rendering │ │ CanvasBackground │ UiElements (hover/click audio) │ └─────────────────────────────────────────────────────────────────┘ ``` --- ## Audio Types ### 1. Background Audio Page-level ambient audio that plays behind all content with configurable playback settings. ```tsx // Using useBackgroundAudioPlayback hook for controlled playback const { audioRef } = useBackgroundAudioPlayback({ audioUrl: page.background_audio_url, autoplay: page.background_audio_autoplay ?? true, loop: page.background_audio_loop ?? true, startTime: page.background_audio_start_time ?? null, endTime: page.background_audio_end_time ?? null, }); ``` **Configurable Properties (stored in `tour_pages`):** | Property | Type | Default | Description | |----------|------|---------|-------------| | `background_audio_autoplay` | boolean | true | Start playback automatically | | `background_audio_loop` | boolean | true | Loop continuously | | `background_audio_start_time` | DECIMAL(10,1) \| null | null | Start playback at this time (seconds) | | `background_audio_end_time` | DECIMAL(10,1) \| null | null | Stop/loop at this time (seconds) | **DECIMAL Parsing (Critical):** Sequelize DECIMAL fields return strings from the database (e.g., `"2.5"` not `2.5`). In runtime code, these must be parsed with `parseFloat(String(value))` before passing to the hook. **Note:** When `background_audio_end_time` is set, looping is handled via JavaScript (`timeupdate` event) to properly seek back to `startTime`. ### 2. Element Audio Effects Interactive audio triggered by user hover and click on UI elements. ```tsx // Using useAudioEffects hook const { playClickAudio, stopAll } = useAudioEffects({ hoverAudioUrl: element.hoverAudioUrl, clickAudioUrl: element.clickAudioUrl, volume: element.audioVolume ?? 1, isHovered, isActive, resolveUrl: preloadOrchestrator.getReadyBlobUrl, }); ``` **Configurable Properties (stored in element's `ui_schema_json`):** | Property | Type | Description | |----------|------|-------------| | `hoverAudioUrl` | string | Audio to play on mouse hover | | `clickAudioUrl` | string | Audio to play on click/tap | | `audioVolume` | number (0-1) | Volume level (iOS ignores this) | --- ## User Interaction Unlock All audio waits for first user interaction (click/tap) before playing. This ensures consistent behavior across all browsers (Safari, Chrome, Firefox). ### backgroundAudioController Module-level singleton that manages audio unlock and ducking: ```typescript // frontend/src/lib/backgroundAudioController.ts class BackgroundAudioController { private audioElement: HTMLAudioElement | null = null; private waitingForInteraction = false; private hasUserInteracted = false; // Register background audio element register(audio: HTMLAudioElement | null): void; // Mark audio as waiting for interaction setWaitingForInteraction(waiting: boolean): void; // Called on first user click/tap - unlocks all audio notifyUserInteraction(): void; // Check if audio is unlocked hasInteracted(): boolean; // Ducking methods for media players notifyForegroundStart(): void; notifyForegroundEnd(): void; } ``` ### Unlock Flow ``` ┌──────────────────────────────────────────────────────────────┐ │ 1. Page loads │ │ └── backgroundAudioController.setWaitingForInteraction() │ │ └── All audio is silent │ └──────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ 2. User clicks/taps anywhere on canvas │ │ └── handleCanvasInteraction() triggered │ │ └── backgroundAudioController.notifyUserInteraction() │ └──────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ 3. Audio unlocked │ │ └── Background audio starts playing │ │ └── Hover/click effects now work │ │ └── hasInteracted() returns true │ └──────────────────────────────────────────────────────────────┘ ``` --- ## Audio Layering & Ducking Different audio types have different behaviors: | Audio Type | Background Audio Behavior | |------------|---------------------------| | **Hover/click effects** | Layers over (plays simultaneously) | | **AudioPlayer element** | Pauses background (ducking) | | **VideoPlayer element** | Pauses background (ducking) | ### Sound Effects (No Ducking) Hover and click effects play over background audio without interruption: ```typescript // useAudioEffects.ts - no ducking calls if (backgroundAudioController.hasInteracted()) { audio.play().catch(() => undefined); } ``` ### Media Players (With Ducking) AudioPlayer and VideoPlayer elements pause background audio: ```typescript // AudioPlayerElement.tsx / VideoPlayerElement.tsx useEffect(() => { const audio = audioRef.current; if (!audio) return; const handlePlay = () => backgroundAudioController.notifyForegroundStart(); const handlePause = () => backgroundAudioController.notifyForegroundEnd(); const handleEnded = () => backgroundAudioController.notifyForegroundEnd(); audio.addEventListener('play', handlePlay); audio.addEventListener('pause', handlePause); audio.addEventListener('ended', handleEnded); return () => { /* cleanup */ }; }, []); ``` ### Reference Counting The controller uses reference counting for multiple media players: ```typescript notifyForegroundStart(); // count = 1, background pauses notifyForegroundStart(); // count = 2, still paused notifyForegroundEnd(); // count = 1, still paused notifyForegroundEnd(); // count = 0, background resumes ``` --- ## Browser Autoplay Policy All major browsers restrict audio autoplay. Our implementation handles this uniformly. ### Browser Policies | Browser | Policy | |---------|--------| | **Safari** | Strictest - blocks ALL audio until user interaction | | **Chrome** | Blocks until user interaction or high Media Engagement Index | | **Firefox** | Blocks by default, user-configurable | ### Our Strategy: Always Wait for Interaction Instead of trying autoplay and handling failures, we consistently wait for user interaction: ```typescript // useBackgroundAudioPlayback.ts if (!shouldBlockAutoplay) { // Always wait for user interaction before playing backgroundAudioController.setWaitingForInteraction(true); } // RuntimePresentation.tsx & constructor.tsx const handleCanvasInteraction = useCallback(() => { backgroundAudioController.notifyUserInteraction(); }, []);