feat(audio): passing tone only during races and remove legacy audio

Adds 'Calls during a race' (default: passing tone only, like timing
systems) that drops name/personal best/leader/position calls in race
sessions; practice keeps them. Removes the legacy audio panel and
fallbacks, including its 'Sound on' switch that silently muted every
race-audio voice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
larssand
2026-09-30 21:47:40 +02:00
parent 5bfb67ea02
commit aaca5e8c63
13 changed files with 108 additions and 205 deletions

View File

@@ -98,9 +98,6 @@ export function renderSettings(deps) {
const running = Boolean(ammcStatus.running);
const speechVoices = getSpeechVoices();
const raceAudio = normalizeRaceAudioSettings(state.settings.raceAudio);
const speechVoiceOptions = speechVoices
.map((voice) => `<option value="${escapeHtml(voice.voiceURI)}" ${state.settings.speechVoiceUri === voice.voiceURI ? "selected" : ""}>${escapeHtml(`${voice.name} (${voice.lang})`)}</option>`)
.join("");
const raceAudioVoiceOptions = speechVoices
.map((voice) => `<option value="${escapeHtml(voice.voiceURI)}" ${raceAudio.voiceUri === voice.voiceURI ? "selected" : ""}>${escapeHtml(`${voice.name} (${voice.lang})`)}</option>`)
.join("");
@@ -243,6 +240,7 @@ export function renderSettings(deps) {
<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.race_callouts")}</span><select name="raceAudioRaceCallouts" form="settingsForm"><option value="passing" ${raceAudio.raceCallouts === "passing" ? "selected" : ""}>${t("audio.race_callouts_passing")}</option><option value="full" ${raceAudio.raceCallouts === "full" ? "selected" : ""}>${t("audio.race_callouts_full")}</option></select></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.voice_select")}</span><select name="raceAudioVoiceUri" form="settingsForm"><option value="">${t("audio.voice_auto")}</option>${raceAudioVoiceOptions}</select></label>
<label><span>${t("audio.voice_rate")}</span><input type="range" min="0.65" max="1.35" step="0.05" name="raceAudioVoiceRate" form="settingsForm" value="${raceAudio.voiceRate}" /></label>
@@ -262,78 +260,6 @@ export function renderSettings(deps) {
<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" : ""} />
<span>${t("settings.audio_enabled")}</span>
</label>
<select name="passingSoundMode" form="settingsForm">
<option value="off" ${state.settings.passingSoundMode === "off" ? "selected" : ""}>${t("settings.passing_sound_off")}</option>
<option value="beep" ${state.settings.passingSoundMode === "beep" ? "selected" : ""}>${t("settings.passing_sound_beep")}</option>
<option value="name" ${state.settings.passingSoundMode === "name" ? "selected" : ""}>${t("settings.passing_sound_name")}</option>
</select>
<label class="toggle">
<input type="checkbox" name="finishVoiceEnabled" form="settingsForm" ${state.settings.finishVoiceEnabled ? "checked" : ""} />
<span>${t("settings.finish_voice")}</span>
</label>
<label class="toggle">
<input type="checkbox" name="startSoundEnabled" form="settingsForm" ${state.settings.startSoundEnabled ? "checked" : ""} />
<span>${t("settings.start_sound")}</span>
</label>
<label class="toggle">
<input type="checkbox" name="finishSoundEnabled" form="settingsForm" ${state.settings.finishSoundEnabled ? "checked" : ""} />
<span>${t("settings.finish_sound")}</span>
</label>
<label class="settings-audio-wide">
<span>${t("settings.speech_voice")}</span>
<select name="speechVoiceUri" form="settingsForm">
<option value="">${t("settings.system_default_voice")}</option>
${speechVoiceOptions}
</select>
</label>
<label>
<span>${t("settings.speech_rate")}</span>
<input type="range" min="0.65" max="1.35" step="0.05" name="speechRate" form="settingsForm" value="${state.settings.speechRate || 0.95}" />
</label>
<label>
<span>${t("settings.speech_pitch")}</span>
<input type="range" min="0.7" max="1.3" step="0.05" name="speechPitch" form="settingsForm" value="${state.settings.speechPitch || 1}" />
</label>
<button id="settingsTestStart" class="btn" type="button">${t("settings.test_start")}</button>
<button id="settingsTestFinish" class="btn" type="button">${t("settings.test_finish")}</button>
<button id="settingsTestDriverVoice" class="btn" type="button">${t("settings.test_driver_voice")}</button>
<label class="toggle">
<input type="checkbox" name="speakerPassingCueEnabled" form="settingsForm" ${state.settings.speakerPassingCueEnabled ? "checked" : ""} />
<span>${t("settings.speaker_passing_cue")}</span>
</label>
<label class="toggle">
<input type="checkbox" name="speakerLeaderCueEnabled" form="settingsForm" ${state.settings.speakerLeaderCueEnabled ? "checked" : ""} />
<span>${t("settings.speaker_leader_cue")}</span>
</label>
<label class="toggle">
<input type="checkbox" name="speakerFinishCueEnabled" form="settingsForm" ${state.settings.speakerFinishCueEnabled ? "checked" : ""} />
<span>${t("settings.speaker_finish_cue")}</span>
</label>
<label class="toggle">
<input type="checkbox" name="speakerBestLapCueEnabled" form="settingsForm" ${state.settings.speakerBestLapCueEnabled ? "checked" : ""} />
<span>${t("settings.speaker_bestlap_cue")}</span>
</label>
<label class="toggle">
<input type="checkbox" name="speakerTop3CueEnabled" form="settingsForm" ${state.settings.speakerTop3CueEnabled ? "checked" : ""} />
<span>${t("settings.speaker_top3_cue")}</span>
</label>
<label class="toggle">
<input type="checkbox" name="speakerSessionStartCueEnabled" form="settingsForm" ${state.settings.speakerSessionStartCueEnabled ? "checked" : ""} />
<span>${t("settings.speaker_start_cue")}</span>
</label>
</div>
<div class="panel-body">
<p>${t("settings.audio_note")}</p>
<p class="hint">${t("settings.voice_note")}</p>
</div>
</section>
</div>
<div data-settings-panel="appearance">
@@ -668,6 +594,7 @@ Erik Eriksson;654321;4WD;Associated;;</code></pre>
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",
raceCallouts: String(form.get("raceAudioRaceCallouts") || "passing"),
events,
});
};
@@ -676,7 +603,6 @@ Erik Eriksson;654321;4WD;Associated;;</code></pre>
const formNode = document.getElementById("settingsForm");
if (!(formNode instanceof HTMLFormElement)) return false;
applyRaceAudioFormValues(new FormData(formNode));
state.settings.audioEnabled = state.settings.audioEnabled || state.settings.raceAudio.enabled;
return true;
};
@@ -686,19 +612,6 @@ Erik Eriksson;654321;4WD;Associated;;</code></pre>
return true;
};
const applyAudioFormValues = () => {
const formNode = document.getElementById("settingsForm");
if (!(formNode instanceof HTMLFormElement)) return;
const form = new FormData(formNode);
state.settings.audioEnabled = form.get("audioEnabled") === "on";
state.settings.passingSoundMode = String(form.get("passingSoundMode") || "beep");
state.settings.finishVoiceEnabled = form.get("finishVoiceEnabled") === "on";
state.settings.startSoundEnabled = form.get("startSoundEnabled") === "on";
state.settings.finishSoundEnabled = form.get("finishSoundEnabled") === "on";
state.settings.speechVoiceUri = String(form.get("speechVoiceUri") || "");
state.settings.speechRate = Math.max(0.65, Math.min(1.35, Number(form.get("speechRate") || 0.95) || 0.95));
state.settings.speechPitch = Math.max(0.7, Math.min(1.3, Number(form.get("speechPitch") || 1) || 1));
};
const splitDecoderToggle = document.getElementById("settingsSplitEnabled");
const pitDecoderToggle = document.getElementById("settingsPitEnabled");
@@ -743,15 +656,7 @@ Erik Eriksson;654321;4WD;Associated;;</code></pre>
state.settings.pitExitWsUrl = String(form.get("pitExitWsUrl") || "").trim();
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";
state.settings.speakerBestLapCueEnabled = form.get("speakerBestLapCueEnabled") === "on";
state.settings.speakerTop3CueEnabled = form.get("speakerTop3CueEnabled") === "on";
state.settings.speakerSessionStartCueEnabled = form.get("speakerSessionStartCueEnabled") === "on";
state.settings.clubName = String(form.get("clubName") || "").trim() || "JMK RB";
state.settings.clubTagline = String(form.get("clubTagline") || "").trim() || "Live Timing & Race Control";
state.settings.pdfFooter = String(form.get("pdfFooter") || "").trim() || "Generated by RC RaceCore";
@@ -781,25 +686,6 @@ Erik Eriksson;654321;4WD;Associated;;</code></pre>
};
document.getElementById("settingsRaceAudioTest")?.addEventListener("click", () => testRaceAudio("audio_test"));
document.querySelectorAll("[data-test-race-audio]").forEach((button) => button.addEventListener("click", () => testRaceAudio(button.dataset.testRaceAudio)));
document.getElementById("settingsTestStart")?.addEventListener("click", () => {
applyAudioFormValues();
if (state.settings.startSoundEnabled) playStartCue();
if (state.settings.finishVoiceEnabled) {
setTimeout(() => speakText(t("timing.race_started_voice"), { interrupt: true }), state.settings.startSoundEnabled ? 650 : 0);
}
});
document.getElementById("settingsTestFinish")?.addEventListener("click", () => {
applyAudioFormValues();
if (state.settings.finishSoundEnabled) playFinishSiren();
if (state.settings.finishVoiceEnabled) {
setTimeout(() => speakText(t("timing.race_finished_voice"), { interrupt: true }), state.settings.finishSoundEnabled ? 1150 : 0);
}
});
document.getElementById("settingsTestDriverVoice")?.addEventListener("click", () => {
applyAudioFormValues();
const driver = state.drivers[0];
speakText(driver?.speechName || driver?.name || t("settings.driver_voice_sample"), { interrupt: true });
});
document.getElementById("settingsLaunchObs")?.addEventListener("click", () => openOverlayWindow("obs", { public: true }));
document.getElementById("settingsCopyObsUrl")?.addEventListener("click", async () => {
const url = buildOverlayUrl("obs", { public: true });