update: 语音优化

This commit is contained in:
2026-08-20 16:56:34 +08:00
parent bb224c9f29
commit 6363dda204
5 changed files with 128 additions and 25 deletions
+53 -4
View File
@@ -1,6 +1,34 @@
export const AUDIO_INTERRUPTION_BEGIN_EVENT = "audio-interruption-begin"; export const AUDIO_INTERRUPTION_BEGIN_EVENT = "audio-interruption-begin";
export const AUDIO_INTERRUPTION_END_EVENT = "audio-interruption-end"; export const AUDIO_INTERRUPTION_END_EVENT = "audio-interruption-end";
const TRAINING_START_AUDIO_KEY_MAP = Object.freeze({
base: "请于计时前完成指定环数对应箭量",
endurance: "请于计时前完成累计环数和箭量",
precision: "请射箭命中高亮区域",
});
export const getTrainingStartAudioKey = (trainingType, fallback = "") =>
TRAINING_START_AUDIO_KEY_MAP[trainingType] || fallback;
export const getPrecisionShotAudioKeys = (shootData, directionText = "") => {
if (!shootData || typeof shootData !== "object") return [];
const ring = Number(shootData.ring);
const directionAudioKey =
shootData.angle !== null &&
shootData.angle !== undefined &&
directionText
? `${directionText}调整`
: "";
if (!Number.isFinite(ring) || ring <= 0) {
return ["未上靶", directionAudioKey].filter(Boolean);
}
const ringAudioKey = shootData.ringX === true ? "X环" : `${ring}`;
// protobuf 的 false 可能不编码;只要不是明确命中,本箭就反馈未命中。
const hitAudioKey = shootData.ok === true ? "Bingo命中目标" : "未命中";
return [ringAudioKey, directionAudioKey, hitAudioKey].filter(Boolean);
};
export const audioFils = { export const audioFils = {
tententen: "https://static.shelingxingqiu.com/shootmini/static/audio/tententen.mp3", tententen: "https://static.shelingxingqiu.com/shootmini/static/audio/tententen.mp3",
点击按钮: "https://static.shelingxingqiu.com/shootmini/static/audio/%E7%82%B9%E5%87%BB%E6%8C%89%E9%92%AE.mp3", 点击按钮: "https://static.shelingxingqiu.com/shootmini/static/audio/%E7%82%B9%E5%87%BB%E6%8C%89%E9%92%AE.mp3",
@@ -26,6 +54,16 @@ export const audioFils = {
"https://static.shelingxingqiu.com/shootmini/static/audio/%E7%AB%99%E8%B7%9D%E4%B8%8E%E9%9D%B6%E7%BA%B8%E5%90%88%E6%A0%BC.MP3", "https://static.shelingxingqiu.com/shootmini/static/audio/%E7%AB%99%E8%B7%9D%E4%B8%8E%E9%9D%B6%E7%BA%B8%E5%90%88%E6%A0%BC.MP3",
"请射箭!测试站距与靶纸": "请射箭!测试站距与靶纸":
"https://static.shelingxingqiu.com/shootmini/static/audio/%E8%AF%B7%E5%B0%84%E7%AE%AD%EF%BC%81%E6%B5%8B%E8%AF%95%E7%AB%99%E8%B7%9D%E4%B8%8E%E9%9D%B6%E7%BA%B8.MP3", "https://static.shelingxingqiu.com/shootmini/static/audio/%E8%AF%B7%E5%B0%84%E7%AE%AD%EF%BC%81%E6%B5%8B%E8%AF%95%E7%AB%99%E8%B7%9D%E4%B8%8E%E9%9D%B6%E7%BA%B8.MP3",
"站距过近,靶纸正确":
"https://static.shelingxingqiu.com/shootmini/static/audio0820/%E7%AB%99%E8%B7%9D%E8%BF%87%E8%BF%91%EF%BC%8C%E9%9D%B6%E7%BA%B8%E6%AD%A3%E7%A1%AE.MP3",
"站距过近,靶纸错误":
"https://static.shelingxingqiu.com/shootmini/static/audio0820/%E7%AB%99%E8%B7%9D%E8%BF%87%E8%BF%91%EF%BC%8C%E9%9D%B6%E7%BA%B8%E9%94%99%E8%AF%AF.MP3",
"站距合格,靶纸正确":
"https://static.shelingxingqiu.com/shootmini/static/audio0820/%E7%AB%99%E8%B7%9D%E5%90%88%E6%A0%BC%EF%BC%8C%E9%9D%B6%E7%BA%B8%E6%AD%A3%E7%A1%AE.MP3",
"站距合格,靶纸错误":
"https://static.shelingxingqiu.com/shootmini/static/audio0820/%E7%AB%99%E8%B7%9D%E5%90%88%E6%A0%BC%EF%BC%8C%E9%9D%B6%E7%BA%B8%E9%94%99%E8%AF%AF.MP3",
"未识别到靶纸,请瞄准靶纸射箭":
"https://static.shelingxingqiu.com/shootmini/static/audio/%E6%9C%AA%E8%AF%86%E5%88%AB%E5%88%B0%E9%9D%B6%E7%BA%B8%EF%BC%8C%E8%AF%B7%E7%9E%84%E5%87%86%E9%9D%B6%E7%BA%B8%E5%B0%84%E7%AE%AD.MP3",
"未发现靶纸,请瞄准靶纸射箭": "未发现靶纸,请瞄准靶纸射箭":
"https://static.shelingxingqiu.com/shootmini/static/audio/%E6%9C%AA%E5%8F%91%E7%8E%B0%E9%9D%B6%E7%BA%B8%EF%BC%8C%E8%AF%B7%E7%9E%84%E5%87%86%E9%9D%B6%E7%BA%B8%E5%B0%84%E7%AE%AD.MP3", "https://static.shelingxingqiu.com/shootmini/static/audio/%E6%9C%AA%E5%8F%91%E7%8E%B0%E9%9D%B6%E7%BA%B8%EF%BC%8C%E8%AF%B7%E7%9E%84%E5%87%86%E9%9D%B6%E7%BA%B8%E5%B0%84%E7%AE%AD.MP3",
轮到你了: 轮到你了:
@@ -109,6 +147,16 @@ export const audioFils = {
"https://static.shelingxingqiu.com/attachment/2025-11-13/de7kzzllq0futwynso.mp3", "https://static.shelingxingqiu.com/attachment/2025-11-13/de7kzzllq0futwynso.mp3",
练习开始: 练习开始:
"https://static.shelingxingqiu.com/attachment/2025-11-14/de88w0lmmt43nnfmoi.mp3", "https://static.shelingxingqiu.com/attachment/2025-11-14/de88w0lmmt43nnfmoi.mp3",
请于计时前完成指定环数对应箭量:
"https://static.shelingxingqiu.com/shootmini/static/audio0820/%E8%AF%B7%E4%BA%8E%E8%AE%A1%E6%97%B6%E5%89%8D%E5%AE%8C%E6%88%90%E6%8C%87%E5%AE%9A%E7%8E%AF%E6%95%B0%E5%AF%B9%E5%BA%94%E7%AE%AD%E9%87%8F.MP3",
请于计时前完成累计环数和箭量:
"https://static.shelingxingqiu.com/shootmini/static/audio0820/%E8%AF%B7%E4%BA%8E%E8%AE%A1%E6%97%B6%E5%89%8D%E5%AE%8C%E6%88%90%E7%B4%AF%E8%AE%A1%E7%8E%AF%E6%95%B0%E5%92%8C%E7%AE%AD%E9%87%8F.MP3",
请射箭命中高亮区域:
"https://static.shelingxingqiu.com/shootmini/static/audio0820/%E8%AF%B7%E5%B0%84%E7%AE%AD%E5%91%BD%E4%B8%AD%E9%AB%98%E4%BA%AE%E5%8C%BA%E5%9F%9F.MP3",
Bingo命中目标:
"https://static.shelingxingqiu.com/shootmini/static/audio0820/Bingo%E5%91%BD%E4%B8%AD%E7%9B%AE%E6%A0%87.MP3",
未命中:
"https://static.shelingxingqiu.com/shootmini/static/audio0820/%E6%9C%AA%E5%91%BD%E4%B8%AD.MP3",
练习结束: 练习结束:
"https://static.shelingxingqiu.com/shootmini/static/audio/%E7%BB%83%E4%B9%A0%E7%BB%93%E6%9D%9F.mp3", "https://static.shelingxingqiu.com/shootmini/static/audio/%E7%BB%83%E4%B9%A0%E7%BB%93%E6%9D%9F.mp3",
射箭声音: 射箭声音:
@@ -137,17 +185,18 @@ const AUDIO_WARM_PRIORITY_KEYS = [
"射箭无效,距离不足且靶纸错误", "射箭无效,距离不足且靶纸错误",
"射箭无效,未识别到靶纸", "射箭无效,未识别到靶纸",
"射箭无效,靶纸错误", "射箭无效,靶纸错误",
"靶纸不符!请更换靶纸",
"站距与靶纸合格",
"请射箭!测试站距与靶纸", "请射箭!测试站距与靶纸",
"站距过近,靶纸正确",
"站距过近,靶纸错误",
"站距合格,靶纸正确",
"站距合格,靶纸错误",
"未识别到靶纸,请瞄准靶纸射箭",
"中场休息", "中场休息",
"下半场开始", "下半场开始",
"决金箭轮", "决金箭轮",
"请蓝方射箭", "请蓝方射箭",
"请红方射箭", "请红方射箭",
"距离合格", "距离合格",
"距离不足",
"未发现靶纸,请瞄准靶纸射箭",
"未上靶", "未上靶",
"X环", "X环",
]; ];
+43 -1
View File
@@ -14,6 +14,10 @@ import {
getDistanceCheckAudioKey, getDistanceCheckAudioKey,
getInvalidShotAudioKey, getInvalidShotAudioKey,
} from "@/util"; } from "@/util";
import {
getPrecisionShotAudioKeys,
getTrainingStartAudioKey,
} from "@/audioManager";
import { import {
normalizeId, normalizeId,
normalizeMatchInfo, normalizeMatchInfo,
@@ -145,6 +149,15 @@ function buildBusinessMessage(message) {
matchInfo.status = 0; matchInfo.status = 0;
} }
const practiceInfo = normalizePracticeInfo(message.practice_info); const practiceInfo = normalizePracticeInfo(message.practice_info);
const trainingType = String(
practiceInfo.trainingType ||
matchInfo.trainingType ||
currentContext?.trainingType ||
""
).trim();
if (currentContext && trainingType) {
currentContext.trainingType = trainingType;
}
const businessType = getBusinessType(message, matchInfo); const businessType = getBusinessType(message, matchInfo);
if (!businessType) return null; if (!businessType) return null;
@@ -197,11 +210,15 @@ function buildBusinessMessage(message) {
matchWsType: message.type, matchWsType: message.type,
matchWsTypeName: getServerMessageTypeName(message.type), matchWsTypeName: getServerMessageTypeName(message.type),
isSecondHalfStart, isSecondHalfStart,
...(trainingType ? { trainingType } : {}),
}; };
} }
function buildPracticeSyncMessage(message) { function buildPracticeSyncMessage(message) {
const practiceInfo = normalizePracticeInfo(message.practice_info); const practiceInfo = normalizePracticeInfo(message.practice_info);
if (currentContext && practiceInfo.trainingType) {
currentContext.trainingType = String(practiceInfo.trainingType).trim();
}
const matchId = normalizeId( const matchId = normalizeId(
pickField(message, "matchId", "match_id") || pickField(message, "matchId", "match_id") ||
practiceInfo.id || practiceInfo.id ||
@@ -368,7 +385,16 @@ function getTestDistanceAudioKeys(shootData) {
function getAckAudioKeys(message, businessMessage) { function getAckAudioKeys(message, businessMessage) {
switch (message.type) { switch (message.type) {
case ServerMessageType.SERVER_MSG_MATCH_START: case ServerMessageType.SERVER_MSG_MATCH_START:
return [businessMessage?.isSecondHalfStart ? "下半场开始" : "比赛开始"]; if (businessMessage?.isSecondHalfStart) return ["下半场开始"];
// 个人训练在开始接口成功后主动播报,不再等待 MATCH_START 触发语音。
if (
getTrainingStartAudioKey(
businessMessage?.trainingType || currentContext?.trainingType
)
) {
return [];
}
return ["比赛开始"];
case ServerMessageType.SERVER_MSG_NOW_YOU: case ServerMessageType.SERVER_MSG_NOW_YOU:
if (isMeleeMessage(message, businessMessage)) return []; if (isMeleeMessage(message, businessMessage)) return [];
return getNowYouAudioKeys(businessMessage); return getNowYouAudioKeys(businessMessage);
@@ -379,6 +405,17 @@ function getAckAudioKeys(message, businessMessage) {
) { ) {
return []; return [];
} }
if (
(businessMessage?.trainingType || currentContext?.trainingType) ===
"precision"
) {
const shootData = businessMessage?.shootData;
const directionText =
shootData?.angle !== null && shootData?.angle !== undefined
? getDirectionText(shootData.angle)
: "";
return getPrecisionShotAudioKeys(shootData, directionText);
}
return getShootResultAudioKeys(businessMessage?.shootData); return getShootResultAudioKeys(businessMessage?.shootData);
case ServerMessageType.SERVER_MSG_MATCH_END: case ServerMessageType.SERVER_MSG_MATCH_END:
return ["比赛结束"]; return ["比赛结束"];
@@ -865,6 +902,7 @@ export function connectMatchWebSocket(options = {}) {
requestPracticeInfoOnOpen = false, requestPracticeInfoOnOpen = false,
appHideResumable = false, appHideResumable = false,
practiceEndAudioKey = "", practiceEndAudioKey = "",
trainingType = "",
force = false, force = false,
reconnecting = false, reconnecting = false,
reconnectReason = "", reconnectReason = "",
@@ -872,6 +910,7 @@ export function connectMatchWebSocket(options = {}) {
const normalizedMatchId = normalizeId(matchId); const normalizedMatchId = normalizeId(matchId);
const normalizedUserId = normalizeId(userId); const normalizedUserId = normalizeId(userId);
const normalizedMode = Number(mode); const normalizedMode = Number(mode);
const normalizedTrainingType = String(trainingType || "").trim();
const incomingUrl = normalizeServerUrl(serverAddr, token); const incomingUrl = normalizeServerUrl(serverAddr, token);
const currentUrlToken = getUrlToken(currentContext?.url); const currentUrlToken = getUrlToken(currentContext?.url);
const shouldKeepAuthenticatedUrl = !!( const shouldKeepAuthenticatedUrl = !!(
@@ -938,6 +977,9 @@ export function connectMatchWebSocket(options = {}) {
requestPracticeInfoOnOpen: requestPracticeInfoOnOpen === true, requestPracticeInfoOnOpen: requestPracticeInfoOnOpen === true,
appHideResumable: appHideResumable === true, appHideResumable: appHideResumable === true,
practiceEndAudioKey: String(practiceEndAudioKey || "").trim(), practiceEndAudioKey: String(practiceEndAudioKey || "").trim(),
trainingType:
normalizedTrainingType ||
(isSameContext ? currentContext?.trainingType || "" : ""),
meleeHalfRest: isSameContext meleeHalfRest: isSameContext
? currentContext?.meleeHalfRest === true ? currentContext?.meleeHalfRest === true
: false, : false,
@@ -1,6 +1,6 @@
<script setup> <script setup>
import { ref, watch, onMounted, onBeforeUnmount, computed } from "vue"; import { ref, watch, onMounted, onBeforeUnmount, computed } from "vue";
import audioManager from "@/audioManager"; import audioManager, { getTrainingStartAudioKey } from "@/audioManager";
import { MESSAGETYPESV2 } from "@/constants"; import { MESSAGETYPESV2 } from "@/constants";
import { import {
getDirectionText, getDirectionText,
@@ -207,7 +207,10 @@ async function onReceiveMessage(msg) {
if (Array.isArray(msg)) return; if (Array.isArray(msg)) return;
if (msg.type === MESSAGETYPESV2.BattleStart) { if (msg.type === MESSAGETYPESV2.BattleStart) {
halfTime.value = false; halfTime.value = false;
audioManager.play("比赛开始"); // 已实现的个人训练由页面播放专属提示,避免重复播报。
if (!getTrainingStartAudioKey(props.trainingType)) {
audioManager.play("比赛开始");
}
} else if (msg.type === MESSAGETYPESV2.BattleEnd) { } else if (msg.type === MESSAGETYPESV2.BattleEnd) {
audioManager.play("练习结束", false); audioManager.play("练习结束", false);
} else if (msg.type === MESSAGETYPESV2.ShootResult) { } else if (msg.type === MESSAGETYPESV2.ShootResult) {
+21 -12
View File
@@ -10,7 +10,10 @@ import Avatar from "@/components/Avatar.vue";
import BowPower from "@/components/BowPower.vue"; import BowPower from "@/components/BowPower.vue";
import TestDistance from "./components/TestDistance.vue"; import TestDistance from "./components/TestDistance.vue";
import BubbleTip from "./components/BubbleTip.vue"; import BubbleTip from "./components/BubbleTip.vue";
import audioManager from "@/audioManager"; import audioManager, {
getPrecisionShotAudioKeys,
getTrainingStartAudioKey,
} from "@/audioManager";
import { import {
createPractiseV2API, createPractiseV2API,
@@ -466,16 +469,11 @@ const buildShootResultAudioKeys = (message = {}) => {
return []; return [];
} }
const keys = [ const directionText =
arrow.ring ? `${arrow.ringX ? "X" : arrow.ring}` : "未上靶", arrow.angle !== null && arrow.angle !== undefined
]; ? getDirectionText(arrow.angle)
if (arrow.angle !== null && arrow.angle !== undefined) { : "";
keys.push(`${getDirectionText(arrow.angle)}调整`); return getPrecisionShotAudioKeys(arrow, directionText);
}
if (arrow.threeConsecutive10Rings === true) {
keys.push("tententen");
}
return keys;
}; };
const playAudioKeysAndWait = (keys) => { const playAudioKeysAndWait = (keys) => {
@@ -790,6 +788,7 @@ const connectPracticeServer = ({
requestPracticeInfoOnOpen: true, requestPracticeInfoOnOpen: true,
appHideResumable: appHideResumable.value, appHideResumable: appHideResumable.value,
practiceEndAudioKey: "练习结束", practiceEndAudioKey: "练习结束",
trainingType: trainingType.value,
}); });
connectionClosed.value = false; connectionClosed.value = false;
return true; return true;
@@ -1022,6 +1021,13 @@ onLoad((options = {}) => {
}; };
practiseId.value = trainingContext.practiceId || ""; practiseId.value = trainingContext.practiceId || "";
serverAddr.value = trainingContext.serverAddr || ""; serverAddr.value = trainingContext.serverAddr || "";
const startAudioKey = getTrainingStartAudioKey(trainingParams.value.type);
const warmupKeys = [startAudioKey];
if (trainingParams.value.type === "precision") {
warmupKeys.push("Bingo命中目标", "未命中");
}
void audioManager.warmKeys(warmupKeys.filter(Boolean));
}); });
const onReady = async () => { const onReady = async () => {
@@ -1051,7 +1057,10 @@ const onReady = async () => {
start.value = true; start.value = true;
pageStage.value = pageStages.SHOOTING; pageStage.value = pageStages.SHOOTING;
setPracticeAppHideResumable(true); setPracticeAppHideResumable(true);
audioManager.play("练习开始"); // 开始接口成功即进入正式训练,直接播放对应提示,避免依赖 BattleStart 消息。
audioManager.play(
getTrainingStartAudioKey(trainingType.value, "练习开始")
);
} catch (error) { } catch (error) {
start.value = false; start.value = false;
pageStage.value = pageStages.DISTANCE; pageStage.value = pageStages.DISTANCE;
+6 -6
View File
@@ -357,16 +357,16 @@ export const getShootValidation = (shootData) => {
}; };
}; };
// 测距阶段提示音:同时校验站距靶纸,保留旧版本距离不足音频 // 测距阶段提示音:站距靶纸组合播放新版测距语音
export const getDistanceCheckAudioKey = (shootData) => { export const getDistanceCheckAudioKey = (shootData) => {
const result = getShootValidation(shootData); const result = getShootValidation(shootData);
if (!result.targetDetected) return "未发现靶纸,请瞄准靶纸射箭"; if (!result.targetDetected) return "未识别到靶纸,请瞄准靶纸射箭";
if (!result.distanceOk && !result.targetOk) { if (!result.distanceOk && !result.targetOk) {
return "射箭无效,距离不足且靶纸错误"; return "站距过近,靶纸错误";
} }
if (!result.targetOk) return "靶纸不符!请更换靶纸"; if (!result.distanceOk) return "站距过近,靶纸正确";
if (result.distanceOk) return "站距与靶纸合格"; if (!result.targetOk) return "站距合格,靶纸错误";
return "距离不足"; return "站距合格,靶纸正确";
}; };
export const getDistanceCheckText = (shootData) => { export const getDistanceCheckText = (shootData) => {