Audio System
How audio content is generated, composed, and delivered.
Overview
The LokkoLabs audio system uses a segment-based composition approach. Instead of generating unique audio files for every variant (team names, ranks, etc.), we generate atomic segments and combine them at runtime.
Cost Savings: This approach reduces ElevenLabs API usage by approximately 80% compared to generating all variants upfront.
Segment Types
Audio is organized into different segment types:
| Type | Example | Purpose |
|---|---|---|
| Base Clips | "Welcome to Training..." | Complete narration segments |
| Team Segments | "Team One", "Team Two" | Team name announcements |
| Action Suffixes | "...has completed training" | Reusable action phrases |
| Rank Prefixes | "In first place..." | Ranking announcements |
Team Segments
Eight pre-generated team name segments:
Important:Use "Team One" not "Team" + "One". Full phrases maintain tonal consistency in the TTS output.
Template Composition
Templates define how segments combine for team-specific announcements:
// Template definition
const AUDIO_TEMPLATES = {
TRAIN_DONE: {
pattern: ["{TEAM}", "SEG_TRAIN_DONE_SUFFIX"],
// Resolves to: "Team Three" + "has completed training"
},
MISSION_WINNER: {
pattern: ["SEG_FIRST_PLACE", "{TEAM}"],
// Resolves to: "In first place" + "Team One"
},
};
// Usage at runtime
resolveTemplate("TRAIN_DONE", { team: 3 });
// -> Returns array of audio URLs to play in sequenceResolution Flow
- Event triggers template (e.g., team completes training)
- System looks up template pattern
- Replaces
{TEAM}with appropriate segment - Returns list of audio URLs
- Audio player queues and plays in sequence
Inter-Clip Delay
Audio clips can have configurable pauses between them:
| Layer | Field Name | Type |
|---|---|---|
| Database | pause_after_seconds | INTEGER NULL |
| API/Types | pause_after_seconds | number | null |
| ContentAudio | pauseAfter | number | null (camelCase) |
Duration Calculations
When calculating total duration, always include pauseAfter:
// ✅ CORRECT - use getSegmentDuration which includes pauseAfter
import { getSegmentDuration } from "@/lib/domain/unified-content";
const totalDuration = segments.reduce(
(sum, s) => sum + getSegmentDuration(s), 0
);
// ❌ WRONG - missing inter-clip gaps
const totalDuration = segments.reduce(
(sum, s) => sum + s.duration, 0
);Audio Generation
Audio is generated using ElevenLabs Text-to-Speech:
Generation Process
- Admin creates or updates script content
- System detects missing audio clips
- Calls ElevenLabs API with script text
- Stores MP3 in Supabase Storage
- Links clip metadata in database
API Endpoint
POST /api/admin/audio/generate
{
"scripts": ["briefing-intro", "training-start"],
"generateSegments": true // Include team segments
}Voice Configuration
ElevenLabs voice settings for CONTROL narrator:
- Voice: Professional, authoritative tone
- Stability: 0.75 (consistent delivery)
- Clarity: 0.80 (clear enunciation)
- Style: 0.40 (moderate expressiveness)
Delivery Modes
Events support three content delivery modes:
Broadcast
Organizer plays audio through room speakers or video call. Attendees see synchronized visuals.
Central audio + visual sync
Synchronized
Audio plays on attendee phones. Organizer controls timing. All phones progress together.
Distributed audio + central control
Independent
Each attendee controls their own progression. Useful for async or self-paced scenarios.
Full attendee control
Audio Player Hook
The useAudioPlayer hook manages playback:
const audioPlayer = useAudioPlayer({
onClipComplete: (clipId) => {
// Track progress
},
onQueueComplete: () => {
// All clips finished
},
});
// Usage
audioPlayer.play(clipUrl);
audioPlayer.queue([clip1, clip2, clip3]);
audioPlayer.pause();
audioPlayer.resume();
audioPlayer.stop();Player State
| Property | Type | Description |
|---|---|---|
isPlaying | boolean | Audio is currently playing |
currentClip | AudioClip | null | Currently playing clip |
queue | AudioClip[] | Upcoming clips |
progress | number | Current clip progress (0-1) |
isInPostDelay | boolean | In pauseAfter countdown |
postDelayRemaining | number | Seconds left in delay |
Browser Autoplay
Modern browsers require user interaction before playing audio. The system handles this with an interaction gate:
Implementation: First user tap enables audio. Button click or screen tap before event starts unlocks playback.
// Gate audio behind user interaction
const [audioEnabled, setAudioEnabled] = useState(false);
const handleInteraction = () => {
// Play silent audio to unlock
const audio = new Audio();
audio.src = silentMp3;
audio.play().then(() => {
setAudioEnabled(true);
});
};Storage Structure
Audio files are stored in Supabase Storage:
audio/
├── clips/
│ ├── briefing-intro.mp3
│ ├── training-start.mp3
│ └── ...
├── segments/
│ ├── team-one.mp3
│ ├── team-two.mp3
│ └── ...
└── generated/
└── {timestamp}-{hash}.mp3File References
| File | Purpose |
|---|---|
src/lib/domain/audio-segments.ts | Segment definitions, templates |
src/lib/domain/audio-scripts.ts | Base script content |
src/lib/hooks/useAudioPlayer.ts | Playback management hook |
src/lib/hooks/useAudioSequencer.ts | Runtime segment composition |
src/lib/services/elevenlabs.ts | TTS API client |
Next Steps
- Review the Architecture overview
- Explore the API Reference
- Read the Organizer Guide for usage context