Add configurable manifest-driven race audio system
This commit is contained in:
@@ -42,6 +42,9 @@ export function renderSettings(deps) {
|
||||
refreshAmmcStatus,
|
||||
startManagedAmmc,
|
||||
stopManagedAmmc,
|
||||
normalizeRaceAudioSettings,
|
||||
raceAudioEventDefinitions,
|
||||
triggerRaceAudio,
|
||||
} = deps;
|
||||
const ammcStatus = ammc.status || {};
|
||||
const ammcOutput = Array.isArray(ammcStatus.lastOutput) ? ammcStatus.lastOutput : [];
|
||||
@@ -54,6 +57,15 @@ export function renderSettings(deps) {
|
||||
const speechVoiceOptions = speechVoices
|
||||
.map((voice) => `<option value="${escapeHtml(voice.voiceURI)}" ${state.settings.speechVoiceUri === voice.voiceURI ? "selected" : ""}>${escapeHtml(`${voice.name} (${voice.lang})`)}</option>`)
|
||||
.join("");
|
||||
const raceAudio = normalizeRaceAudioSettings(state.settings.raceAudio);
|
||||
const raceAudioEventRows = raceAudioEventDefinitions.map((definition) => `
|
||||
<tr>
|
||||
<td><strong>${escapeHtml(definition.key.replaceAll("_", " "))}</strong><small>${escapeHtml(definition.group)} · ${escapeHtml(definition.priority)}</small></td>
|
||||
<td><input type="checkbox" name="raceAudioEffect_${definition.key}" form="settingsForm" ${raceAudio.events[definition.key]?.effect ? "checked" : ""} ${definition.effect ? "" : "disabled"} /></td>
|
||||
<td><input type="checkbox" name="raceAudioVoice_${definition.key}" form="settingsForm" ${raceAudio.events[definition.key]?.voice ? "checked" : ""} ${definition.voice ? "" : "disabled"} /></td>
|
||||
<td><button type="button" class="btn" data-test-race-audio="${definition.key}">${t("audio.test")}</button></td>
|
||||
</tr>
|
||||
`).join("");
|
||||
|
||||
dom.view.innerHTML = `
|
||||
<section class="panel">
|
||||
@@ -159,7 +171,28 @@ export function renderSettings(deps) {
|
||||
</section>
|
||||
|
||||
<section class="panel mt-16">
|
||||
<div class="panel-header"><h3>${t("settings.audio")}</h3></div>
|
||||
<div class="panel-header"><h3>${t("audio.race_sounds")}</h3></div>
|
||||
<div class="panel-body form-grid cols-4 settings-grid settings-grid-toggles">
|
||||
<label class="toggle"><input type="checkbox" name="raceAudioEnabled" form="settingsForm" ${raceAudio.enabled ? "checked" : ""} /><span>${t("audio.enabled")}</span></label>
|
||||
<label class="toggle"><input type="checkbox" name="raceAudioMuted" form="settingsForm" ${raceAudio.muted ? "checked" : ""} /><span>${t("audio.mute")}</span></label>
|
||||
<label><span>${t("audio.language")}</span><select name="raceAudioLanguage" form="settingsForm"><option value="sv" ${raceAudio.language === "sv" ? "selected" : ""}>Svenska</option><option value="en" ${raceAudio.language === "en" ? "selected" : ""}>English</option></select></label>
|
||||
<label><span>${t("audio.start_countdown")}</span><select name="raceAudioCountdown" form="settingsForm">${[0,10,30,60].map((value) => `<option value="${value}" ${raceAudio.startCountdownSeconds === value ? "selected" : ""}>${value ? `${value} s` : t("common.off")}</option>`).join("")}</select></label>
|
||||
<label><span>${t("audio.master_volume")}</span><input type="range" min="0" max="1" step="0.05" name="raceAudioMaster" form="settingsForm" value="${raceAudio.masterVolume}" /></label>
|
||||
<label><span>${t("audio.effects_volume")}</span><input type="range" min="0" max="1" step="0.05" name="raceAudioEffects" form="settingsForm" value="${raceAudio.effectsVolume}" /></label>
|
||||
<label><span>${t("audio.voice_volume")}</span><input type="range" min="0" max="1" step="0.05" name="raceAudioVoice" form="settingsForm" value="${raceAudio.voiceVolume}" /></label>
|
||||
<label><span>${t("audio.passing_volume")}</span><input type="range" min="0" max="1" step="0.05" name="raceAudioPassing" form="settingsForm" value="${raceAudio.passingVolume}" /></label>
|
||||
<label class="toggle"><input type="checkbox" name="raceAudioStartLights" form="settingsForm" ${raceAudio.startLights ? "checked" : ""} /><span>${t("audio.start_lights")}</span></label>
|
||||
<label class="toggle"><input type="checkbox" name="raceAudioRandomDelay" form="settingsForm" ${raceAudio.randomStartDelay ? "checked" : ""} /><span>${t("audio.random_delay")}</span></label>
|
||||
<label><span>${t("audio.min_delay")}</span><input type="number" min="0" max="10" step="0.1" name="raceAudioMinDelay" form="settingsForm" value="${raceAudio.minStartDelay}" /></label>
|
||||
<label><span>${t("audio.max_delay")}</span><input type="number" min="0" max="10" step="0.1" name="raceAudioMaxDelay" form="settingsForm" value="${raceAudio.maxStartDelay}" /></label>
|
||||
<label class="toggle"><input type="checkbox" name="raceAudioPerCompetitor" form="settingsForm" ${raceAudio.perCompetitorPassingTone ? "checked" : ""} /><span>${t("audio.per_competitor")}</span></label>
|
||||
<button id="settingsRaceAudioTest" class="btn" type="button">${t("audio.test")}</button>
|
||||
</div>
|
||||
<div class="panel-body"><p class="hint">${t("audio.note")}</p><div class="table-wrap"><table class="audio-event-table"><thead><tr><th>Event</th><th>${t("audio.effect")}</th><th>${t("audio.voice")}</th><th></th></tr></thead><tbody>${raceAudioEventRows}</tbody></table></div></div>
|
||||
</section>
|
||||
|
||||
<section class="panel mt-16">
|
||||
<div class="panel-header"><h3>${t("settings.audio")} · Legacy</h3></div>
|
||||
<div class="panel-body form-grid cols-4 settings-grid settings-grid-toggles">
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="audioEnabled" form="settingsForm" ${state.settings.audioEnabled ? "checked" : ""} />
|
||||
@@ -429,6 +462,28 @@ export function renderSettings(deps) {
|
||||
</section>
|
||||
`;
|
||||
|
||||
const applyRaceAudioFormValues = (form) => {
|
||||
const events = {};
|
||||
raceAudioEventDefinitions.forEach((definition) => {
|
||||
events[definition.key] = {
|
||||
effect: form.get("raceAudioEffect_" + definition.key) === "on",
|
||||
voice: form.get("raceAudioVoice_" + definition.key) === "on",
|
||||
};
|
||||
});
|
||||
state.settings.raceAudio = normalizeRaceAudioSettings({
|
||||
...state.settings.raceAudio,
|
||||
enabled: form.get("raceAudioEnabled") === "on",
|
||||
muted: form.get("raceAudioMuted") === "on",
|
||||
language: String(form.get("raceAudioLanguage") || "sv"),
|
||||
masterVolume: Number(form.get("raceAudioMaster")), effectsVolume: Number(form.get("raceAudioEffects")),
|
||||
voiceVolume: Number(form.get("raceAudioVoice")), passingVolume: Number(form.get("raceAudioPassing")),
|
||||
startCountdownSeconds: Number(form.get("raceAudioCountdown")), startLights: form.get("raceAudioStartLights") === "on",
|
||||
randomStartDelay: form.get("raceAudioRandomDelay") === "on", minStartDelay: Number(form.get("raceAudioMinDelay")),
|
||||
maxStartDelay: Number(form.get("raceAudioMaxDelay")), perCompetitorPassingTone: form.get("raceAudioPerCompetitor") === "on",
|
||||
events,
|
||||
});
|
||||
};
|
||||
|
||||
const applyAudioFormValues = () => {
|
||||
const formNode = document.getElementById("settingsForm");
|
||||
if (!(formNode instanceof HTMLFormElement)) return;
|
||||
@@ -487,6 +542,8 @@ export function renderSettings(deps) {
|
||||
state.settings.backendUrl = String(form.get("backendUrl") || "").trim();
|
||||
state.settings.autoReconnect = form.get("autoReconnect") === "on";
|
||||
applyAudioFormValues();
|
||||
applyRaceAudioFormValues(form);
|
||||
state.settings.audioEnabled = state.settings.audioEnabled || state.settings.raceAudio.enabled;
|
||||
state.settings.speakerPassingCueEnabled = form.get("speakerPassingCueEnabled") === "on";
|
||||
state.settings.speakerLeaderCueEnabled = form.get("speakerLeaderCueEnabled") === "on";
|
||||
state.settings.speakerFinishCueEnabled = form.get("speakerFinishCueEnabled") === "on";
|
||||
@@ -504,6 +561,8 @@ export function renderSettings(deps) {
|
||||
});
|
||||
|
||||
document.getElementById("settingsConnect")?.addEventListener("click", connectDecoder);
|
||||
document.getElementById("settingsRaceAudioTest")?.addEventListener("click", () => triggerRaceAudio("audio_test", { force: true, interrupt: true }));
|
||||
document.querySelectorAll("[data-test-race-audio]").forEach((button) => button.addEventListener("click", () => triggerRaceAudio(button.dataset.testRaceAudio, { force: true, interrupt: true, variables: { driverName: state.drivers[0]?.name || "Driver", lapTime: "16.000", marshalPost: "1", transponder: "123", podium: "P1, P2, P3" } })));
|
||||
document.getElementById("settingsTestStart")?.addEventListener("click", () => {
|
||||
applyAudioFormValues();
|
||||
if (state.settings.startSoundEnabled) playStartCue();
|
||||
|
||||
Reference in New Issue
Block a user