Förbättrat ljudsystemet
This commit is contained in:
@@ -104,6 +104,11 @@ export function applyPersistedStateHelper(persisted, deps) {
|
||||
audioEnabled: persisted.settings?.audioEnabled !== false,
|
||||
passingSoundMode: persisted.settings?.passingSoundMode || state.settings.passingSoundMode || "beep",
|
||||
finishVoiceEnabled: persisted.settings?.finishVoiceEnabled !== false,
|
||||
startSoundEnabled: persisted.settings?.startSoundEnabled !== false,
|
||||
finishSoundEnabled: persisted.settings?.finishSoundEnabled !== false,
|
||||
speechVoiceUri: String(persisted.settings?.speechVoiceUri || state.settings.speechVoiceUri || ""),
|
||||
speechRate: Math.max(0.65, Math.min(1.35, Number(persisted.settings?.speechRate || state.settings.speechRate || 0.95) || 0.95)),
|
||||
speechPitch: Math.max(0.7, Math.min(1.3, Number(persisted.settings?.speechPitch || state.settings.speechPitch || 1) || 1)),
|
||||
speakerPassingCueEnabled: persisted.settings?.speakerPassingCueEnabled === true,
|
||||
speakerLeaderCueEnabled: persisted.settings?.speakerLeaderCueEnabled !== false,
|
||||
speakerFinishCueEnabled: persisted.settings?.speakerFinishCueEnabled !== false,
|
||||
@@ -255,18 +260,21 @@ function playTone({ ensureAudioContext, type, startFreq, ramps = [], attack = 0.
|
||||
osc.connect(gain); gain.connect(ctx.destination); osc.start(); osc.stop(ctx.currentTime + stopAt);
|
||||
}
|
||||
|
||||
export function playPassingBeepHelper({ ensureAudioContext }) { playTone({ ensureAudioContext, type: 'square', startFreq: 1320, peak: 0.08, release: 0.14, stopAt: 0.16 }); }
|
||||
export function playFinishSirenHelper({ ensureAudioContext }) { playTone({ ensureAudioContext, type: 'sawtooth', startFreq: 720, peak: 0.12, release: 1.2, stopAt: 1.22, attack: 0.03, ramps: [[1280,0.28,'linear'],[720,0.56,'linear'],[1280,0.84,'linear'],[720,1.12,'linear']] }); }
|
||||
export function playPassingBeepHelper({ ensureAudioContext }) { playTone({ ensureAudioContext, type: 'sine', startFreq: 1480, peak: 0.07, release: 0.12, stopAt: 0.14, ramps: [[1760,0.08,'linear']] }); }
|
||||
export function playFinishSirenHelper({ ensureAudioContext }) {
|
||||
playTone({ ensureAudioContext, type: 'triangle', startFreq: 980, peak: 0.12, release: 0.58, stopAt: 0.62, attack: 0.02, ramps: [[620,0.56,'linear']] });
|
||||
setTimeout(() => playTone({ ensureAudioContext, type: 'triangle', startFreq: 980, peak: 0.1, release: 0.72, stopAt: 0.76, attack: 0.02, ramps: [[520,0.68,'linear']] }), 520);
|
||||
}
|
||||
export function playLeaderCueHelper({ ensureAudioContext }) { playTone({ ensureAudioContext, type: 'triangle', startFreq: 880, peak: 0.09, release: 0.26, stopAt: 0.28, ramps: [[1320,0.12,'linear'],[1760,0.24,'linear']] }); }
|
||||
export function playStartCueHelper({ ensureAudioContext }) { playTone({ ensureAudioContext, type: 'triangle', startFreq: 520, peak: 0.08, release: 0.4, stopAt: 0.42, ramps: [[1040,0.4,'linear']] }); }
|
||||
export function playStartCueHelper({ ensureAudioContext }) { playTone({ ensureAudioContext, type: 'square', startFreq: 760, peak: 0.1, release: 0.62, stopAt: 0.66, attack: 0.015, ramps: [[1520,0.16,'linear'],[1520,0.54,'linear']] }); }
|
||||
export function playBestLapCueHelper({ ensureAudioContext }) { playTone({ ensureAudioContext, type: 'sine', startFreq: 1540, peak: 0.07, release: 0.22, stopAt: 0.24, ramps: [[1980,0.2,'linear']] }); }
|
||||
|
||||
export function pushOverlayEventHelper(type, label, deps) {
|
||||
export function pushOverlayEventHelper(type, label, deps, options = {}) {
|
||||
const { uid, state, t, getOverlayEvents, setOverlayEvents, getOverlayMode, getCurrentView, getOverlayViewMode, playLeaderCue, playPassingBeep, playFinishSiren, playStartCue, playBestLapCue } = deps;
|
||||
const events = getOverlayEvents();
|
||||
events.unshift({ id: uid("overlay"), type, label, ts: Date.now() });
|
||||
setOverlayEvents(events.length > 12 ? events.slice(0, 12) : events);
|
||||
if (getOverlayMode() && getCurrentView() === "overlay" && getOverlayViewMode() === "speaker") {
|
||||
if (!options.silentCue && getOverlayMode() && getCurrentView() === "overlay" && getOverlayViewMode() === "speaker") {
|
||||
if (type === "leader" && state.settings.speakerLeaderCueEnabled) playLeaderCue();
|
||||
else if (type === "passing" && state.settings.speakerPassingCueEnabled) playPassingBeep();
|
||||
else if (type === "finish" && state.settings.speakerFinishCueEnabled) playFinishSiren();
|
||||
@@ -276,30 +284,74 @@ export function pushOverlayEventHelper(type, label, deps) {
|
||||
}
|
||||
}
|
||||
|
||||
export function speakTextHelper(text, { state, currentLanguage }) {
|
||||
if (!state.settings.audioEnabled || !("speechSynthesis" in window) || !text) return;
|
||||
export function getSpeechVoicesHelper() {
|
||||
if (!("speechSynthesis" in window)) return [];
|
||||
return window.speechSynthesis.getVoices().map((voice) => ({
|
||||
name: voice.name,
|
||||
lang: voice.lang,
|
||||
voiceURI: voice.voiceURI,
|
||||
localService: voice.localService,
|
||||
}));
|
||||
}
|
||||
|
||||
export function selectSpeechVoice(voices, voiceUri, language) {
|
||||
const items = Array.isArray(voices) ? voices : [];
|
||||
const selected = items.find((voice) => voice.voiceURI === voiceUri);
|
||||
if (selected) return selected;
|
||||
const prefix = String(language || "").toLowerCase().split("-")[0];
|
||||
return items.find((voice) => String(voice.lang || "").toLowerCase().startsWith(prefix) && voice.localService !== false)
|
||||
|| items.find((voice) => String(voice.lang || "").toLowerCase().startsWith(prefix))
|
||||
|| null;
|
||||
}
|
||||
|
||||
export function getSessionAnnouncementText(session, phase, { t }) {
|
||||
const practice = ["open_practice", "free_practice", "practice"].includes(String(session?.type || "").toLowerCase());
|
||||
if (phase === "start") return t(practice ? "timing.practice_started_voice" : "timing.race_started_voice");
|
||||
return t(practice ? "timing.practice_finished_voice" : "timing.race_finished_voice");
|
||||
}
|
||||
|
||||
export function speakTextHelper(text, { state, currentLanguage }, options = {}) {
|
||||
if (!state.settings.audioEnabled || !("speechSynthesis" in window) || !text) return false;
|
||||
const synth = window.speechSynthesis;
|
||||
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";
|
||||
utterance.rate = 1;
|
||||
window.speechSynthesis.cancel();
|
||||
window.speechSynthesis.speak(utterance);
|
||||
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));
|
||||
const voice = selectSpeechVoice(synth.getVoices(), String(state.settings.speechVoiceUri || ""), utterance.lang);
|
||||
if (voice) utterance.voice = voice;
|
||||
synth.speak(utterance);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function announcePassingHelper(entry, { state, t, playPassingBeep, speakText }) {
|
||||
if (!state.settings.audioEnabled) return;
|
||||
if (state.settings.passingSoundMode === "beep") { playPassingBeep(); return; }
|
||||
if (state.settings.passingSoundMode === "name") speakText(entry?.displayName || entry?.driverName || t("common.unknown_driver"));
|
||||
if (state.settings.passingSoundMode === "name") {
|
||||
const spoken = speakText(entry?.speechName || entry?.displayName || entry?.driverName || t("common.unknown_driver"), { interrupt: false });
|
||||
if (!spoken) playPassingBeep();
|
||||
}
|
||||
}
|
||||
|
||||
export function announceRaceFinishedHelper({ state, t, getActiveSession, pushOverlayEvent, playFinishSiren }) {
|
||||
if (!state.settings.audioEnabled || !state.settings.finishVoiceEnabled) {
|
||||
const session = getActiveSession();
|
||||
if (session) pushOverlayEvent("finish", `${session.name} • ${t("timing.race_finished")}`);
|
||||
return;
|
||||
export function announceRaceStartedHelper(session, { state, t, pushOverlayEvent, playStartCue, speakText }) {
|
||||
if (!session) return;
|
||||
pushOverlayEvent("start", `${session.name} • ${t("timing.start")}`, { silentCue: true });
|
||||
if (!state.settings.audioEnabled) return;
|
||||
if (state.settings.startSoundEnabled !== false) playStartCue();
|
||||
if (state.settings.finishVoiceEnabled !== false) {
|
||||
setTimeout(() => speakText(getSessionAnnouncementText(session, "start", { t }), { interrupt: true }), state.settings.startSoundEnabled === false ? 0 : 650);
|
||||
}
|
||||
}
|
||||
|
||||
export function announceRaceFinishedHelper({ state, t, getActiveSession, pushOverlayEvent, playFinishSiren, speakText }) {
|
||||
const session = getActiveSession();
|
||||
if (session) pushOverlayEvent("finish", `${session.name} • ${t("timing.race_finished")}`);
|
||||
playFinishSiren();
|
||||
if (session) pushOverlayEvent("finish", `${session.name} • ${t("timing.race_finished")}`, { silentCue: true });
|
||||
if (!session || !state.settings.audioEnabled) return;
|
||||
if (state.settings.finishSoundEnabled !== false) playFinishSiren();
|
||||
if (state.settings.finishVoiceEnabled !== false) {
|
||||
setTimeout(() => speakText(getSessionAnnouncementText(session, "finish", { t }), { interrupt: true }), state.settings.finishSoundEnabled === false ? 0 : 1150);
|
||||
}
|
||||
}
|
||||
|
||||
export function handleSessionTimerTickHelper({ getActiveSession, isUntimedSession, getSessionTiming, saveState, getLastFinishAnnouncementSessionId, setLastFinishAnnouncementSessionId, announceRaceFinished }) {
|
||||
|
||||
Reference in New Issue
Block a user