Add configurable manifest-driven race audio system
This commit is contained in:
@@ -2393,3 +2393,103 @@ If the project grows further, the next useful additions to this document are:
|
||||
- SQLite table contract section
|
||||
- controller-to-view ID map for critical forms/buttons
|
||||
- browser-level smoke tests for critical create/edit workflows
|
||||
|
||||
## 20. Race Audio Architecture
|
||||
|
||||
### 20.1 Ownership and dependency direction
|
||||
|
||||
`src/race_audio.js` owns audio scheduling only. It does not own session state, laps, validation, leaderboards, penalties, pit timing, or finish rules.
|
||||
|
||||
Dependency flow:
|
||||
|
||||
```text
|
||||
AMMC WebSocket
|
||||
-> decoder_runtime.processDecoderMessageHelper
|
||||
-> existing result/leaderboard mutation
|
||||
-> triggerRaceAudio(event, metadata)
|
||||
-> race_audio.createRaceAudioEngine
|
||||
-> short HTMLAudio effect + prioritized Web Speech queue
|
||||
|
||||
Timing Start button
|
||||
-> timing_views validation
|
||||
-> raceAudioEngine.runStartSequence
|
||||
-> start-light state callbacks
|
||||
-> start tone is initiated
|
||||
-> existing commitStart callback sets session.startedAt/status
|
||||
|
||||
1-second application clock
|
||||
-> runtime_services.handleSessionTimerTickHelper
|
||||
-> existing remaining/follow-up/finish rules
|
||||
-> one-shot audio markers on the normalized session
|
||||
-> triggerRaceAudio
|
||||
```
|
||||
|
||||
The service is constructed in `src/app.js`. `src/app.js` is the composition root and injects `triggerRaceAudio`, `startSessionWithAudio`, historical-record lookup, speech, settings, and logging callbacks into the extracted modules.
|
||||
|
||||
### 20.2 Files and contracts
|
||||
|
||||
- `src/race_audio.js`: event catalog, settings normalization, manifest loading, safe effect playback, fallback, priority/cooldown queue, templates, start-light state machine, start sequence, status.
|
||||
- `rc-race-sound-pack/sound_manifest.json`: event-to-effect mapping, `sv`/`en` templates, runtime defaults, optional passing pool names.
|
||||
- `server.js`: read-only `/race-sounds` static route. JSON is `no-store`; WAV files may be cached for one day.
|
||||
- `src/decoder_runtime.js`: emits audio metadata only after existing validation/result mutation. Split timing never emits a lap; pit roles emit `entering_pits`/`leaving_pits` without adding laps.
|
||||
- `src/runtime_services.js`: legacy audio fallback, Web Speech adapter, persisted-state hydration, and one-shot 60/30/10/time-expired/final-lap markers.
|
||||
- `src/timing_views.js`: visual start lights and manual race-control cues. It retains the authoritative client-side start callback.
|
||||
- `src/settings.js`: persistent volume/language/start configuration and per-event effect/voice matrix.
|
||||
- `src/core_views.js`: optional `passSound` on driver and car records.
|
||||
|
||||
### 20.3 Persisted schema
|
||||
|
||||
`state.settings.raceAudio` is normalized on local load, backend hydration, seed, import/export, and save:
|
||||
|
||||
```js
|
||||
{
|
||||
enabled, muted, language,
|
||||
masterVolume, effectsVolume, voiceVolume, passingVolume,
|
||||
startCountdownSeconds,
|
||||
startLights, lightIntervalMs,
|
||||
randomStartDelay, minStartDelay, maxStartDelay,
|
||||
perCompetitorPassingTone,
|
||||
events: { [eventKey]: { effect, voice } },
|
||||
templates: { [eventKey]: string }
|
||||
}
|
||||
```
|
||||
|
||||
Drivers and cars optionally persist `passSound`. Sessions persist `audioAnnouncements`, a small list of one-shot clock markers reset at every start. Audio log entries are persisted in `state.audioEventLog` and bounded to 500 entries. Decoder passings reuse the normal state synchronization path, so audio logging never adds a blocking write to transponder processing; timing persistence remains the source of truth.
|
||||
|
||||
### 20.4 Priority, concurrency, and failure behavior
|
||||
|
||||
- `critical`: track call, red/yellow flag, false start, timing failure, pause.
|
||||
- `high`: start, finish, penalty, marshal, final lap.
|
||||
- `normal`: fastest lap, track record, leader, countdown, results.
|
||||
- `low`: personal best and position changes.
|
||||
- `pass`: short transponder effects.
|
||||
|
||||
Effects use independent `Audio` instances and may overlap. They never hold decoder processing. Voice is serialized by priority. A critical voice can cancel the current lower-priority browser utterance. Cooldowns are scoped by event and competitor where available.
|
||||
|
||||
All fetch, JSON, file, `Audio.play()`, speech, subscriber, and log failures are caught. A custom missing passing sound retries `effects/transponder_pass.wav`. Failure never mutates or rejects the timing path.
|
||||
|
||||
### 20.5 Start timing invariant
|
||||
|
||||
Start-light state is independent from sound and exposes:
|
||||
|
||||
```text
|
||||
IDLE -> GRID -> READY -> LIGHT_1 -> LIGHT_2 -> LIGHT_3 -> LIGHT_4 -> LIGHT_5 -> WAIT_RANDOM -> GO
|
||||
```
|
||||
|
||||
The random value is generated once inside the race-controller service, not by the visual light component. After the delay, the engine initiates the start tone and immediately invokes the existing `commitStart` callback. That callback sets `session.status = running` and `session.startedAt = Date.now()`. No HTTP request occurs between tone initiation and timestamp assignment.
|
||||
|
||||
Practice types intentionally bypass this sequence. This preserves continuously running Open Practice behavior.
|
||||
|
||||
### 20.6 Event mapping and calculations
|
||||
|
||||
Automatic decoder events are emitted only for valid main-loop laps. Personal best requires an earlier personal best. Fastest lap requires an earlier session fastest lap. Track record requires an earlier best lap from another session in the same class. Leader/position events compare the existing leaderboard immediately before and after the valid passing. These checks observe timing results; they do not recalculate standings independently.
|
||||
|
||||
Finish announcements run only after the existing follow-up/finish logic marks the session finished. Winner and podium use `buildLeaderboard(session)` after that transition. Positive time penalties and removed laps emit the penalty event after the existing judging function saves its correction.
|
||||
|
||||
### 20.7 Known boundary
|
||||
|
||||
The application currently has no authentication or role model. Race-control sound buttons therefore inherit the same local operator access as all Timing and Judging controls. Do not expose the administrative application publicly; expose only the existing public overlay endpoints. A future authorization layer should gate all race-control actions together rather than adding audio-only permissions.
|
||||
|
||||
### 20.8 Tests
|
||||
|
||||
`tests/race_audio.test.js` covers normalization, language/event mapping, custom-file fallback, critical interruption, start-light order, random delay, and tone/timestamp callback order. Existing decoder, runtime persistence, audio fallback, race timing, and server integration suites remain the regression boundary.
|
||||
|
||||
Reference in New Issue
Block a user