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:

TypeExamplePurpose
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:

Team One
Team Two
Team Three
Team Four
Team Five
Team Six
Team Seven
Team Eight

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 sequence

Resolution Flow

  1. Event triggers template (e.g., team completes training)
  2. System looks up template pattern
  3. Replaces {TEAM} with appropriate segment
  4. Returns list of audio URLs
  5. Audio player queues and plays in sequence

Inter-Clip Delay

Audio clips can have configurable pauses between them:

LayerField NameType
Databasepause_after_secondsINTEGER NULL
API/Typespause_after_secondsnumber | null
ContentAudiopauseAfternumber | 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

  1. Admin creates or updates script content
  2. System detects missing audio clips
  3. Calls ElevenLabs API with script text
  4. Stores MP3 in Supabase Storage
  5. 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

PropertyTypeDescription
isPlayingbooleanAudio is currently playing
currentClipAudioClip | nullCurrently playing clip
queueAudioClip[]Upcoming clips
progressnumberCurrent clip progress (0-1)
isInPostDelaybooleanIn pauseAfter countdown
postDelayRemainingnumberSeconds 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}.mp3

File References

FilePurpose
src/lib/domain/audio-segments.tsSegment definitions, templates
src/lib/domain/audio-scripts.tsBase script content
src/lib/hooks/useAudioPlayer.tsPlayback management hook
src/lib/hooks/useAudioSequencer.tsRuntime segment composition
src/lib/services/elevenlabs.tsTTS API client

Next Steps