diff --git a/README.md b/README.md index e7e7125..dd074e8 100644 --- a/README.md +++ b/README.md @@ -540,7 +540,7 @@ Example message: The repository includes `rc-race-sound-pack/`. `sound_manifest.json` is the source of truth for event-to-WAV and Swedish/English announcement mappings. The server exposes it at `/race-sounds/sound_manifest.json`; WAV files are served from `/race-sounds/sounds/effects/`. 1. Open `Settings -> Race sounds and announcements`. -2. Select `sv` or `en`, then set master, effects, voice, and passing volume. +2. Select `sv` or `en`, choose an installed Race voice, then set its speed, pitch, and master/effects/voice/passing volume. The offline race-controller defaults are speed `1.08` and pitch `0.90`; prerecorded or neural clips are still required for genuinely human announcer quality. 3. Enable or disable effect and voice independently for each event and use its Test button. Changes save automatically, and the section also has its own Save button. WAV effects are prerecorded; voice announcements use the operating system Web Speech voices. 4. Select a 0/10/30/60-second pre-start, start lights, and random delay limits. For timed race sessions the clock starts at the same callback that initiates the start tone. Open/Free Practice starts immediately. 5. Use the Race Control buttons in `Timing` for Track call, Track clear, Red flag, False start, and Marshal warning. diff --git a/README.sv.md b/README.sv.md index 11c14fc..7d073cd 100644 --- a/README.sv.md +++ b/README.sv.md @@ -529,7 +529,7 @@ Exempelmeddelande: Repot innehåller `rc-race-sound-pack/`. `sound_manifest.json` är källan för kopplingen mellan event, WAV-filer och svenska/engelska utrop. Servern publicerar manifestet på `/race-sounds/sound_manifest.json` och WAV-filerna under `/race-sounds/sounds/effects/`. 1. Öppna `Inställningar -> Raceljud och utrop`. -2. Välj `sv` eller `en` och ställ master-, effekt-, röst- och passing-volym. +2. Välj `sv` eller `en`, välj en installerad Race-röst och ställ dess hastighet, tonhöjd samt master-, effekt-, röst- och passing-volym. Offline-standard för race-controller-stil är hastighet `1.08` och tonhöjd `0.90`; för verkligt mänsklig speakerkvalitet krävs fortfarande förinspelade eller neurala klipp. 3. Slå på/av effekt och röst separat för varje event och använd radens Testa-knapp. Ändringar sparas automatiskt och sektionen har även en egen Spara-knapp. WAV-effekter är förinspelade; röstutrop använder operativsystemets Web Speech-röster. 4. Välj 0/10/30/60 sekunders förstart, startljus och gränser för slumpad startfördröjning. För tidsstyrda race startar klockan i samma callback som starttonen initieras. Open/Free Practice startar direkt. 5. Använd Race Control-knapparna i `Tidtagning` för Stopp på banan, Banan fri, Röd flagg, Felaktig start och Banvaktsvarning. diff --git a/docs/javascript-architecture.md b/docs/javascript-architecture.md index 6395526..4365729 100644 --- a/docs/javascript-architecture.md +++ b/docs/javascript-architecture.md @@ -2444,6 +2444,7 @@ The service is constructed in `src/app.js`. `src/app.js` is the composition root ```js { enabled, muted, language, + voiceUri, voiceRate, voicePitch, masterVolume, effectsVolume, voiceVolume, passingVolume, startCountdownSeconds, startLights, lightIntervalMs, diff --git a/src/app.js b/src/app.js index 3c81830..40890f9 100644 --- a/src/app.js +++ b/src/app.js @@ -1013,6 +1013,10 @@ const TRANSLATIONS = { "audio.enabled": "Aktivera nya raceljud", "audio.mute": "Tysta allt", "audio.language": "Utropsspråk", + "audio.voice_select": "Race-röst", + "audio.voice_auto": "Automatisk röst för valt språk", + "audio.voice_rate": "Race-röstens hastighet", + "audio.voice_pitch": "Race-röstens tonhöjd", "audio.start_countdown": "Nedräkning före start", "audio.master_volume": "Mastervolym", "audio.effects_volume": "Effektvolym", @@ -2120,6 +2124,10 @@ const TRANSLATIONS = { "audio.enabled": "Enable new race audio", "audio.mute": "Mute all", "audio.language": "Announcement language", + "audio.voice_select": "Race voice", + "audio.voice_auto": "Automatic voice for selected language", + "audio.voice_rate": "Race voice speed", + "audio.voice_pitch": "Race voice pitch", "audio.start_countdown": "Pre-start countdown", "audio.master_volume": "Master volume", "audio.effects_volume": "Effects volume", @@ -2744,7 +2752,7 @@ const raceAudioEngine = createRaceAudioEngine({ speak: (text, options = {}) => new Promise((resolve) => { let settled = false; const done = (value) => { if (!settled) { settled = true; resolve(value); } }; - const started = speakText(text, { ...options, voiceUri: "", onEnd: () => done(true), onError: () => done(false) }); + const started = speakText(text, { ...options, voiceUri: state.settings.raceAudio?.voiceUri || "", rate: state.settings.raceAudio?.voiceRate, pitch: state.settings.raceAudio?.voicePitch, onEnd: () => done(true), onError: () => done(false) }); if (!started) done(false); else setTimeout(() => done(true), 15000); }), diff --git a/src/race_audio.js b/src/race_audio.js index 86db196..23a0c96 100644 --- a/src/race_audio.js +++ b/src/race_audio.js @@ -64,6 +64,9 @@ export function normalizeRaceAudioSettings(value = {}) { enabled: source.enabled !== false, muted: source.muted === true, language: source.language === "en" ? "en" : "sv", + voiceUri: String(source.voiceUri || ""), + voiceRate: clamp(source.voiceRate, 1.08, 0.65, 1.35), + voicePitch: clamp(source.voicePitch, 0.9, 0.7, 1.3), masterVolume: clamp(source.masterVolume ?? source.volume?.master, 0.8), effectsVolume: clamp(source.effectsVolume ?? source.volume?.effects, 0.8), voiceVolume: clamp(source.voiceVolume ?? source.volume?.voice, 0.9), diff --git a/src/runtime_services.js b/src/runtime_services.js index b3534b3..ea07300 100644 --- a/src/runtime_services.js +++ b/src/runtime_services.js @@ -335,8 +335,8 @@ export function speakTextHelper(text, { state, currentLanguage }, options = {}) const utterance = new SpeechSynthesisUtterance(text); 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.rate = Math.max(0.65, Math.min(1.35, Number(options.rate ?? state.settings.speechRate ?? 0.95) || 0.95)); + utterance.pitch = Math.max(0.7, Math.min(1.3, Number(options.pitch ?? 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; diff --git a/src/settings.js b/src/settings.js index 3e4a721..b3e828f 100644 --- a/src/settings.js +++ b/src/settings.js @@ -54,10 +54,13 @@ export function renderSettings(deps) { const suggestedPitExitWsUrl = getManagedWsUrl("pit_exit"); const running = Boolean(ammcStatus.running); const speechVoices = getSpeechVoices(); + const raceAudio = normalizeRaceAudioSettings(state.settings.raceAudio); const speechVoiceOptions = speechVoices .map((voice) => ``) .join(""); - const raceAudio = normalizeRaceAudioSettings(state.settings.raceAudio); + const raceAudioVoiceOptions = speechVoices + .map((voice) => ``) + .join(""); const raceAudioEventRows = raceAudioEventDefinitions.map((definition) => `