Skip to content

Latest commit

Β 

History

History
233 lines (215 loc) Β· 20.1 KB

File metadata and controls

233 lines (215 loc) Β· 20.1 KB

Race Replay Audio-Visual Alignment System

Distance-Based Checkpoint Architecture

Track Geometry

  • Track Type: 200m Banked Indoor
  • Race Distance: 800m
  • Laps: 4 (200m Γ— 4 = 800m)
  • Checkpoints: 0m (start), 200m, 400m, 600m, 800m (finish)

Data Structure Design

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    COMMENTARY CHECKPOINT SYSTEM                             β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                             β”‚
β”‚  CHECKPOINT QUEUE (One per runner Γ— 5 checkpoints = max 55 events)         β”‚
β”‚                                                                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚ RUNNER: Melodi (id=1)  -  Splits: [0, 32.94, 68.67, 105.05, 141.58]β”‚   β”‚
β”‚  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€   β”‚
β”‚  β”‚ Distance β”‚ Time      β”‚ Status β”‚ Audio Clips                          β”‚   β”‚
β”‚  │──────────┼───────────┼────────┼─────────────────────────────────────│   β”‚
β”‚  β”‚    0m    β”‚ 0.00s     β”‚   βœ“    β”‚ [00] Intro, [01] "Runners set..."   β”‚   β”‚
β”‚  β”‚   200m   β”‚ 32.94s    β”‚   β—‹    β”‚ [04] "32.9 for Melodi at 200m"      β”‚   β”‚
β”‚  β”‚   400m   β”‚ 68.67s    β”‚   β—‹    β”‚ [07] "1:08 at the bell"             β”‚   β”‚
β”‚  β”‚   600m   β”‚ 105.05s   β”‚   β—‹    β”‚ [11] "1:45 through 600m"            β”‚   β”‚
β”‚  β”‚   800m   β”‚ 141.58s   β”‚   β—‹    β”‚ [15] "2:21.58 for Melodi"           β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                                                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚ RUNNER: Skye (id=8)    -  Splits: [0, 41.00, 88.55, 136.83, 184.27]β”‚   β”‚
β”‚  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€   β”‚
β”‚  β”‚ Distance β”‚ Time      β”‚ Status β”‚ Audio Clips                          β”‚   β”‚
β”‚  │──────────┼───────────┼────────┼─────────────────────────────────────│   β”‚
β”‚  β”‚    0m    β”‚ 0.00s     β”‚   βœ“    β”‚ (no start clip - shared)            β”‚   β”‚
β”‚  β”‚   200m   β”‚ 41.00s    β”‚   β—‹    β”‚ [05] "41 flat for Skye"             β”‚   β”‚
β”‚  β”‚   400m   β”‚ 88.55s    β”‚   β—‹    β”‚ [09] "1:28 for Skye"                β”‚   β”‚
β”‚  β”‚   600m   β”‚ 136.83s   β”‚   β—‹    β”‚ [13] "2:16 through 600m"            β”‚   β”‚
β”‚  β”‚   800m   β”‚ 184.27s   β”‚   β—‹    β”‚ [18] "3:04.27 PR for Skye"          β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                                                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚ RUNNER: Margeaux (id=9) - Splits: [0, 33.18, 70.18, 109.24, 190.55]β”‚   β”‚
β”‚  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€   β”‚
β”‚  β”‚ Distance β”‚ Time      β”‚ Status β”‚ Audio Clips                          β”‚   β”‚
β”‚  │──────────┼───────────┼────────┼─────────────────────────────────────│   β”‚
β”‚  β”‚    0m    β”‚ 0.00s     β”‚   βœ“    β”‚ (no start clip)                     β”‚   β”‚
β”‚  β”‚   200m   β”‚ 33.18s    β”‚   β—‹    β”‚ (no clip)                           β”‚   β”‚
β”‚  β”‚   400m   β”‚ 70.18s    β”‚   β—‹    β”‚ (no clip)                           β”‚   β”‚
β”‚  β”‚   600m   β”‚ 109.24s   β”‚   β—‹    β”‚ (no clip)                           β”‚   β”‚
β”‚  β”‚   800m   β”‚ 190.55s   β”‚   β—‹    β”‚ [20] "Margeaux crosses in 3:10"     β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

### Global Commentary Queue (Non-Runner-Specific)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ GLOBAL EVENTS β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ PRE-RACE (Time-based, triggers at raceTime = -46s) β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ [00] "Welcome to the 800m championship heat..." β”‚ β”‚ β”‚ β”‚ Trigger: raceTime >= -46 β”‚ β”‚ β”‚ β”‚ Duration: ~18s β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ FIELD SEPARATION (Distance-based, triggers at ~130m when gap forms) β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ [03] "Field stringing out..." β”‚ β”‚ β”‚ β”‚ Trigger: leader distance >= 130m β”‚ β”‚ β”‚ β”‚ Subject: None (general observation) β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ MIDDLE PACK ANALYSIS (Distance-based, triggers at ~250m) β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ [06] "Middle pack feeling the gap..." β”‚ β”‚ β”‚ β”‚ Trigger: leader distance >= 250m β”‚ β”‚ β”‚ β”‚ Subject: Parvati (id=2) - mentioned in audio β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ RACE SUMMARY (Time-based, triggers at raceTime = 198s) β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ [21] "What a race... two different strategies" β”‚ β”‚ β”‚ β”‚ Trigger: raceTime >= 198 β”‚ β”‚ β”‚ β”‚ Subject: None β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜


### Audio-Visual Synchronization Logic

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ TRIGGER EVALUATION PER FRAME β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ β”‚ EACH ANIMATION FRAME (60fps): β”‚ β”‚ β”‚ β”‚ 1. UPDATE raceTime β”‚ β”‚ raceTime += (1/60) * speed; β”‚ β”‚ β”‚ β”‚ 2. UPDATE all runner positions β”‚ β”‚ For each runner: β”‚ β”‚ dist = interpolateDistance(runner.splits, raceTime) β”‚ β”‚ (x, y) = getTrackCoordinates(dist) β”‚ β”‚ β”‚ β”‚ 3. EVALUATE COMMENTARY QUEUE β”‚ β”‚ β”‚ β”‚ A. Check PRE-RACE queue (time-based) β”‚ β”‚ if raceTime >= -46 and intro not played: β”‚ β”‚ playAudio([00]) β”‚ β”‚ β”‚ β”‚ B. Check RUNNER-SPECIFIC queues (distance-based) β”‚ β”‚ For each runner with pending audio: β”‚ β”‚ checkpoint = nextPendingCheckpoint(runner) β”‚ β”‚ actualDist = getDistanceAtTime(runner.splits, raceTime) β”‚ β”‚ β”‚ β”‚ if actualDist >= checkpoint.distance: β”‚ β”‚ // RUNNER HAS CROSSED THE CHECKPOINT! β”‚ β”‚ if not isAudioPlaying: β”‚ β”‚ playAudio(checkpoint.audioClip) β”‚ β”‚ checkpoint.status = "PLAYED" β”‚ β”‚ focusIndicator.target = runner.id β”‚ β”‚ β”‚ β”‚ C. Check GLOBAL events (mixed triggers) β”‚ β”‚ For each global event: β”‚ β”‚ if event.triggerType == "time" and raceTime >= event.triggerValue β”‚ β”‚ OR β”‚ β”‚ if event.triggerType == "distance" and leaderDist >= event.trigger β”‚ β”‚ if not isAudioPlaying: β”‚ β”‚ playAudio(event.audioClip) β”‚ β”‚ β”‚ β”‚ 4. UPDATE VISUAL INDICATOR β”‚ β”‚ if currentFocusRunnerId and isAudioPlaying: β”‚ β”‚ target = document.getElementById(runner-${currentFocusRunnerId}) β”‚ β”‚ focusIndicator.style.top = target.style.top β”‚ β”‚ focusIndicator.style.left = target.style.left β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜


### Checkpoint Data Structure (JavaScript)

```javascript
// Runner-specific checkpoint queue
const runnerCheckpoints = {
  1: { // Melodi
    checkpoints: [
      { distance: 0,   time: 0,     audio: [1],  played: false },   // Start
      { distance: 200, time: 32.94, audio: [4],  played: false },   // 200m
      { distance: 400, time: 68.67, audio: [7, 10], played: false }, // 400m (bell + 600m approaching)
      { distance: 600, time: 105.05, audio: [11], played: false },  // 600m
      { distance: 800, time: 141.58, audio: [14, 15], played: false } // Finish
    ]
  },
  8: { // Skye
    checkpoints: [
      { distance: 0,   time: 0,     audio: [],   played: false },   // Start (no specific audio)
      { distance: 200, time: 41.00, audio: [5],  played: false },   // 200m
      { distance: 400, time: 88.55, audio: [9],  played: false },   // 400m
      { distance: 600, time: 136.83, audio: [12, 16], played: false }, // 600m
      { distance: 800, time: 184.27, audio: [18], played: false }  // Finish
    ]
  },
  9: { // Margeaux
    checkpoints: [
      { distance: 800, time: 190.55, audio: [19], played: false }   // Finish only
    ]
  }
};

// Global events queue
const globalEvents = [
  { type: "time",     trigger: -46,  audio: [0],  played: false }, // Pre-race intro
  { type: "distance", trigger: 130,  audio: [3],  played: false }, // Field separation
  { type: "distance", trigger: 250,  audio: [6],  played: false }, // Middle pack
  { type: "distance", trigger: 450,  audio: [8],  played: false }, // Field strung out
  { type: "time",     trigger: 198,  audio: [20], played: false }  // Race summary
];

Audio Clip Inventory (Aligned to Checkpoints)

β”Œβ”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Clip β”‚ Checkpoint  β”‚ Runner           β”‚ Transcript                                 β”‚
β”œβ”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ [00] β”‚ Pre-race    β”‚ None             β”‚ "Welcome to the 800m championship..."      β”‚
β”‚ [01] β”‚ 0m          β”‚ All (start)      β”‚ "Runners set. Clean start..."              β”‚
β”‚ [02] β”‚ 100m        β”‚ Melodi           β”‚ "Melodi out fast through the first 100"    β”‚
β”‚ [03] β”‚ 130m        β”‚ Leader (general) β”‚ "Field stringing out..."                   β”‚
β”‚ [04] β”‚ 200m        β”‚ Melodi           β”‚ "32.9 for Melodi at the 200"               β”‚
β”‚ [05] β”‚ 200m        β”‚ Skye             β”‚ "41 flat for Skye at the 200"              β”‚
β”‚ [06] β”‚ 250m        β”‚ Leader (general) β”‚ "Middle pack feeling the gap..."           β”‚
β”‚ [07] β”‚ 380m        β”‚ Melodi           β”‚ "400m. Melodi commanding at 1:08"          β”‚
β”‚ [08] β”‚ 450m        β”‚ Leader (general) β”‚ "Look at the separation..."                β”‚
β”‚ [09] β”‚ 400m        β”‚ Skye             β”‚ "1:28 for Skye... negative split pacing"   β”‚
β”‚ [10] β”‚ 580m        β”‚ Leader (general) β”‚ "600m mark. This is where it's decided"    β”‚
β”‚ [11] β”‚ 600m        β”‚ Melodi           β”‚ "1:45 for Melodi through 600m"             β”‚
β”‚ [12] β”‚ 600m        β”‚ Skye             β”‚ "2:16 for Skye... running people down"     β”‚
β”‚ [13] β”‚ 700m        β”‚ Leader (general) β”‚ "100m to go for Melodi"                    β”‚
β”‚ [14] β”‚ 720m        β”‚ Skye             β”‚ "Skye flying now... passing people"        β”‚
β”‚ [15] β”‚ 800m        β”‚ Melodi           β”‚ "2:21.58 for Melodi... outstanding"        β”‚
β”‚ [16] β”‚ 750m        β”‚ Skye             β”‚ "Watch Skye coming home... 47s final lap"  β”‚
β”‚ [17] β”‚ 750m        β”‚ Skye             β”‚ "50m to go for Skye"                       β”‚
β”‚ [18] β”‚ 800m        β”‚ Skye             β”‚ "3:04.27 PR for Skye"                      β”‚
β”‚ [19] β”‚ 800m        β”‚ Margeaux         β”‚ "Margeaux crosses in 3:10"                 β”‚
β”‚ [20] β”‚ 198s        β”‚ None (summary)   β”‚ "What a race... two different strategies"  β”‚
β””β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Implementation Notes

  1. Audio Overlap Prevention: Only ONE audio clip plays at a time. If a checkpoint is crossed while another clip is playing, it queues (or cancels if too late).

  2. Visual Indicator: The yellow ring follows the runner mentioned in the CURRENTLY PLAYING audio clip, not just any runner crossing a checkpoint.

  3. Reset Behavior: All played: false flags reset to allow replay.

  4. Multiple Clips at Same Checkpoint: Some checkpoints have multiple clips (e.g., Melodi at 400m has clips [7] and [10]). They play sequentially.

  5. Distance Calculation: Uses linear interpolation between known split times:

    distance = checkpoint * 200 + (timeInSegment / segmentDuration * 200)