Add configurable manifest-driven race audio system
This commit is contained in:
@@ -96,13 +96,14 @@ export async function stopManagedAmmcHelper({ ammc, t, getBackendUrl }) {
|
||||
}
|
||||
|
||||
export function applyPersistedStateHelper(persisted, deps) {
|
||||
const { state, getDefaultBackendUrl, DEFAULT_LANGUAGE, DEFAULT_THEME, AVAILABLE_THEMES, normalizeDriver, normalizeCar, normalizeEvent, normalizeSession, normalizeStoredRacePreset, normalizeObsOverlaySettings, normalizeTrainingState, applyTheme } = deps;
|
||||
const { state, getDefaultBackendUrl, DEFAULT_LANGUAGE, DEFAULT_THEME, AVAILABLE_THEMES, normalizeDriver, normalizeCar, normalizeEvent, normalizeSession, normalizeStoredRacePreset, normalizeObsOverlaySettings, normalizeTrainingState, normalizeRaceAudioSettings = (value = {}) => value || {}, applyTheme } = deps;
|
||||
state.classes = persisted.classes || [];
|
||||
state.drivers = (persisted.drivers || []).map((driver) => normalizeDriver(driver)).filter((driver) => driver.name);
|
||||
state.cars = (persisted.cars || []).map((car) => normalizeCar(car)).filter((car) => car.name);
|
||||
state.events = (persisted.events || []).map((event) => normalizeEvent(event));
|
||||
state.sessions = (persisted.sessions || []).map((session) => normalizeSession(session));
|
||||
state.resultsBySession = persisted.resultsBySession || {};
|
||||
state.audioEventLog = Array.isArray(persisted.audioEventLog) ? persisted.audioEventLog.slice(0, 500) : [];
|
||||
state.training = normalizeTrainingState(persisted.training);
|
||||
state.activeSessionId = persisted.activeSessionId || null;
|
||||
state.settings = {
|
||||
@@ -135,6 +136,7 @@ export function applyPersistedStateHelper(persisted, deps) {
|
||||
pdfFooter: persisted.settings?.pdfFooter || state.settings.pdfFooter || "Generated by JMK RB RaceController",
|
||||
pdfTheme: persisted.settings?.pdfTheme || state.settings.pdfTheme || "classic",
|
||||
logoDataUrl: persisted.settings?.logoDataUrl || state.settings.logoDataUrl || "",
|
||||
raceAudio: normalizeRaceAudioSettings(persisted.settings?.raceAudio || state.settings?.raceAudio),
|
||||
racePresets: Array.isArray(persisted.settings?.racePresets) ? persisted.settings.racePresets.map((preset) => normalizeStoredRacePreset(preset)).filter((preset) => preset.name) : Array.isArray(state.settings?.racePresets) ? state.settings.racePresets.map((preset) => normalizeStoredRacePreset(preset)).filter((preset) => preset.name) : [],
|
||||
obsOverlay: normalizeObsOverlaySettings(persisted.settings?.obsOverlay || state.settings?.obsOverlay),
|
||||
};
|
||||
@@ -331,9 +333,13 @@ export function speakTextHelper(text, { state, currentLanguage }, options = {})
|
||||
if (!options.interrupt && (synth.speaking || synth.pending)) return false;
|
||||
if (options.interrupt) synth.cancel();
|
||||
const utterance = new SpeechSynthesisUtterance(text);
|
||||
utterance.lang = currentLanguage() === "sv" ? "sv-SE" : "en-US";
|
||||
const language = options.language || currentLanguage();
|
||||
utterance.lang = String(language).toLowerCase().startsWith("sv") ? "sv-SE" : "en-US";
|
||||
utterance.rate = Math.max(0.65, Math.min(1.35, Number(state.settings.speechRate || 0.95) || 0.95));
|
||||
utterance.pitch = Math.max(0.7, Math.min(1.3, Number(state.settings.speechPitch || 1) || 1));
|
||||
utterance.volume = Math.max(0, Math.min(1, Number(options.volume ?? 1)));
|
||||
if (typeof options.onEnd === "function") utterance.onend = options.onEnd;
|
||||
if (typeof options.onError === "function") utterance.onerror = options.onError;
|
||||
const voice = selectSpeechVoice(synth.getVoices(), String(state.settings.speechVoiceUri || ""), utterance.lang);
|
||||
if (voice) utterance.voice = voice;
|
||||
synth.speak(utterance);
|
||||
@@ -352,6 +358,7 @@ export function announcePassingHelper(entry, { state, t, playPassingBeep, speakT
|
||||
export function announceRaceStartedHelper(session, { state, t, pushOverlayEvent, playStartCue, speakText }) {
|
||||
if (!session) return;
|
||||
pushOverlayEvent("start", `${session.name} • ${t("timing.start")}`, { silentCue: true });
|
||||
if (state.settings.raceAudio?.enabled) return;
|
||||
if (!state.settings.audioEnabled) return;
|
||||
if (state.settings.startSoundEnabled !== false) playStartCue();
|
||||
if (state.settings.finishVoiceEnabled !== false) {
|
||||
@@ -362,6 +369,7 @@ export function announceRaceStartedHelper(session, { state, t, pushOverlayEvent,
|
||||
export function announceRaceFinishedHelper({ state, t, getActiveSession, pushOverlayEvent, playFinishSiren, speakText }) {
|
||||
const session = getActiveSession();
|
||||
if (session) pushOverlayEvent("finish", `${session.name} • ${t("timing.race_finished")}`, { silentCue: true });
|
||||
if (state.settings.raceAudio?.enabled) return;
|
||||
if (!session || !state.settings.audioEnabled) return;
|
||||
if (state.settings.finishSoundEnabled !== false) playFinishSiren();
|
||||
if (state.settings.finishVoiceEnabled !== false) {
|
||||
@@ -369,15 +377,31 @@ export function announceRaceFinishedHelper({ state, t, getActiveSession, pushOve
|
||||
}
|
||||
}
|
||||
|
||||
export function handleSessionTimerTickHelper({ getActiveSession, isUntimedSession, getSessionTiming, saveState, getLastFinishAnnouncementSessionId, setLastFinishAnnouncementSessionId, announceRaceFinished }) {
|
||||
export function handleSessionTimerTickHelper({ getActiveSession, isUntimedSession, getSessionTiming, saveState, getLastFinishAnnouncementSessionId, setLastFinishAnnouncementSessionId, announceRaceFinished, announceRaceClockEvent = () => {} }) {
|
||||
const active = getActiveSession();
|
||||
if (!active || active.status !== "running") return { changed: false };
|
||||
if (isUntimedSession(active)) return { changed: false };
|
||||
const timing = getSessionTiming(active);
|
||||
if (timing.remainingMs > 0) return { changed: false };
|
||||
if (!Array.isArray(active.audioAnnouncements)) active.audioAnnouncements = [];
|
||||
let changed = false;
|
||||
const announceOnce = (marker, eventKey) => {
|
||||
if (active.audioAnnouncements.includes(marker)) return;
|
||||
active.audioAnnouncements.push(marker);
|
||||
announceRaceClockEvent(eventKey, active);
|
||||
changed = true;
|
||||
};
|
||||
if (timing.remainingMs > 0 && timing.remainingMs <= 60000) announceOnce("remaining_60", "one_minute_remaining");
|
||||
if (timing.remainingMs > 0 && timing.remainingMs <= 30000) announceOnce("remaining_30", "thirty_seconds_remaining");
|
||||
if (timing.remainingMs > 0 && timing.remainingMs <= 10000) announceOnce("remaining_10", "ten_seconds_remaining");
|
||||
if (timing.remainingMs > 0) {
|
||||
if (changed) saveState();
|
||||
return { changed };
|
||||
}
|
||||
announceOnce("time_expired", "time_expired");
|
||||
if (!["open_practice", "free_practice", "practice"].includes(String(active.type || ""))) announceOnce("final_lap", "final_lap");
|
||||
if (Number(active.followUpSec || 0) > 0) {
|
||||
if (!active.followUpStartedAt) { active.followUpStartedAt = Date.now(); saveState(); return { changed: true }; }
|
||||
if (timing.followUpRemainingMs > 0) return { changed: false };
|
||||
if (timing.followUpRemainingMs > 0) { if (changed) saveState(); return { changed }; }
|
||||
}
|
||||
active.status = "finished";
|
||||
active.endedAt = Date.now();
|
||||
|
||||
Reference in New Issue
Block a user