update:语音优化

This commit is contained in:
2026-08-19 14:04:12 +08:00
parent 75cfcd66ee
commit bb224c9f29
13 changed files with 153 additions and 86 deletions
+12
View File
@@ -20,6 +20,12 @@ export const audioFils = {
"https://static.shelingxingqiu.com/attachment/2025-09-17/dcutwrda0amn5kqr4j.mp3",
距离不足:
"https://static.shelingxingqiu.com/attachment/2025-11-12/de6hr2faw28t0ianh0.mp3",
"靶纸不符!请更换靶纸":
"https://static.shelingxingqiu.com/shootmini/static/audio/%E9%9D%B6%E7%BA%B8%E4%B8%8D%E7%AC%A6%EF%BC%81%E8%AF%B7%E6%9B%B4%E6%8D%A2%E9%9D%B6%E7%BA%B8.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/%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",
轮到你了:
@@ -54,6 +60,8 @@ export const audioFils = {
"https://static.shelingxingqiu.com/shootmini/static/audio/%E5%B0%84%E7%AE%AD%E6%97%A0%E6%95%88%E6%A3%80%E6%9F%A5%E8%B7%9D%E7%A6%BB%E5%92%8C%E9%9D%B6%E7%BA%B8.mp3",
"射箭无效,距离不足":
"https://static.shelingxingqiu.com/shootmini/static/audio/%E5%B0%84%E7%AE%AD%E6%97%A0%E6%95%88%EF%BC%8C%E8%B7%9D%E7%A6%BB%E4%B8%8D%E8%B6%B3.MP3",
"射箭无效,距离不足且靶纸错误":
"https://static.shelingxingqiu.com/shootmini/static/audio/%E5%B0%84%E7%AE%AD%E6%97%A0%E6%95%88%EF%BC%8C%E8%B7%9D%E7%A6%BB%E4%B8%8D%E8%B6%B3%E4%B8%94%E9%9D%B6%E7%BA%B8%E9%94%99%E8%AF%AF.MP3",
"射箭无效,未识别到靶纸":
"https://static.shelingxingqiu.com/shootmini/static/audio/%E5%B0%84%E7%AE%AD%E6%97%A0%E6%95%88%EF%BC%8C%E6%9C%AA%E8%AF%86%E5%88%AB%E5%88%B0%E9%9D%B6%E7%BA%B8.MP3",
"射箭无效,靶纸错误":
@@ -126,8 +134,12 @@ const AUDIO_WARM_PRIORITY_KEYS = [
"比赛结束",
"射击无效",
"射箭无效,距离不足",
"射箭无效,距离不足且靶纸错误",
"射箭无效,未识别到靶纸",
"射箭无效,靶纸错误",
"靶纸不符!请更换靶纸",
"站距与靶纸合格",
"请射箭!测试站距与靶纸",
"中场休息",
"下半场开始",
"决金箭轮",
+6 -2
View File
@@ -2,7 +2,11 @@
import { ref, watch, onMounted, onBeforeUnmount } from "vue";
import audioManager from "@/audioManager";
import { MESSAGETYPESV2 } from "@/constants";
import { getDirectionText, getInvalidShotAudioKey } from "@/util";
import {
getDirectionText,
getInvalidShotAudioKey,
getInvalidShotText,
} from "@/util";
import useStore from "@/store";
import { storeToRefs } from "pinia";
@@ -88,7 +92,7 @@ async function onReceiveMessage(message) {
currentRoundEnded.value = true;
} else if (type === MESSAGETYPESV2.InvalidShot) {
uni.showToast({
title: "距离不足,无效",
title: getInvalidShotText(shootData),
icon: "none",
});
audioManager.play(getInvalidShotAudioKey(shootData));
+6 -2
View File
@@ -2,7 +2,11 @@
import { ref, watch, onMounted, onBeforeUnmount, computed } from "vue";
import audioManager from "@/audioManager";
import { MESSAGETYPESV2 } from "@/constants";
import { getDirectionText, getInvalidShotAudioKey } from "@/util";
import {
getDirectionText,
getInvalidShotAudioKey,
getInvalidShotText,
} from "@/util";
import useStore from "@/store";
import { storeToRefs } from "pinia";
@@ -171,7 +175,7 @@ async function onReceiveMessage(msg) {
audioManager.play("中场休息");
} else if (msg.type === MESSAGETYPESV2.InvalidShot) {
uni.showToast({
title: "距离不足,无效",
title: getInvalidShotText(msg.shootData),
icon: "none",
});
audioManager.play(getInvalidShotAudioKey(msg.shootData));
+10 -11
View File
@@ -6,6 +6,7 @@ import Avatar from "@/components/Avatar.vue";
import audioManager from "@/audioManager";
import { simulShootAPI } from "@/apis";
import { MESSAGETYPESV2 } from "@/constants";
import { getDistanceCheckAudioKey, getDistanceCheckText } from "@/util";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
@@ -26,6 +27,7 @@ const props = defineProps({
});
const arrow = ref({});
const distance = ref(0);
const statusText = ref("");
const showsimul = ref(false);
const count = ref(props.count);
const timer = ref(null);
@@ -34,7 +36,7 @@ const updateTimer = (value) => {
count.value = Math.round(value);
};
onMounted(() => {
audioManager.play("请射箭测试距离");
audioManager.play("请射箭测试站距与靶纸");
if (props.isBattle) {
timer.value = setInterval(() => {
count.value -= 1;
@@ -51,14 +53,12 @@ onBeforeUnmount(() => {
async function onReceiveMessage(msg) {
if (Array.isArray(msg)) return;
if (msg.type === MESSAGETYPESV2.TestDistance) {
const rawDistance = Number(msg.shootData?.distance);
const rawDistance = Number(msg.shootData?.distance ?? msg.shootData?.dst);
distance.value = Number.isFinite(rawDistance)
? Number((rawDistance / 100).toFixed(2))
: 0;
if (rawDistance === 0) {
audioManager.play("未发现靶纸,请瞄准靶纸射箭");
} else if (distance.value >= 5) audioManager.play("距离合格");
else audioManager.play("距离不足");
statusText.value = getDistanceCheckText(msg.shootData);
audioManager.play(getDistanceCheckAudioKey(msg.shootData));
}
}
@@ -101,13 +101,12 @@ onBeforeUnmount(() => {
模拟射箭
</button>
<view class="warnning-text">
<block v-if="distance > 0">
<text>当前距离{{ distance }}</text>
<text v-if="distance >= 5">已达到距离要求</text>
<text v-else>请调整站位</text>
<block v-if="statusText">
<text v-if="distance > 0">当前距离{{ distance }}</text>
<text>{{ statusText }}</text>
</block>
<block v-else>
<text>请射箭测试站距</text>
<text>请射箭测试站距与靶纸</text>
</block>
</view>
<view class="user-row">
+11 -25
View File
@@ -9,7 +9,11 @@ import {
getServerMessageTypeName,
} from "@/utils/matchProtocol";
import { MESSAGETYPESV2 } from "@/constants";
import { getDirectionText, getInvalidShotAudioKey } from "@/util";
import {
getDirectionText,
getDistanceCheckAudioKey,
getInvalidShotAudioKey,
} from "@/util";
import {
normalizeId,
normalizeMatchInfo,
@@ -49,20 +53,6 @@ const ENABLE_REALTIME_MESSAGE_LOG = (() => {
}
})();
function getServerMessageLogSummary(message = {}) {
const practiceInfo = message.practice_info || message.practiceInfo || {};
const matchInfo = message.match_info || message.matchInfo || {};
return {
type: message.type,
matchId: normalizeId(pickField(message, "matchId", "match_id")),
sequence: message.sequence,
practiceDetailCount: Array.isArray(practiceInfo.details)
? practiceInfo.details.length
: 0,
roundCount: Array.isArray(matchInfo.rounds) ? matchInfo.rounds.length : 0,
};
}
// 比赛服消息类型先映射成项目里已有的 V2 业务消息,页面仍然复用原来的 socket-inbox 流程。
const BUSINESS_TYPE_BY_SERVER_TYPE = {
[ServerMessageType.SERVER_MSG_MATCH_READY]: MESSAGETYPESV2.AboutToStart,
@@ -87,6 +77,9 @@ function normalizeShootData(shootData) {
if (normalized.distance === undefined && normalized.dst !== undefined) {
normalized.distance = normalized.dst;
}
if (normalized.targetOk === undefined && normalized.target_ok !== undefined) {
normalized.targetOk = normalized.target_ok;
}
return normalized;
}
@@ -368,10 +361,8 @@ function getShootResultAudioKeys(shootData) {
}
function getTestDistanceAudioKeys(shootData) {
const distance = Number(shootData?.distance ?? shootData?.dst);
if (Number.isNaN(distance)) return [];
if (distance === 0) return ["未发现靶纸,请瞄准靶纸射箭"];
return [distance / 100 >= 5 ? "\u8ddd\u79bb\u5408\u683c" : "\u8ddd\u79bb\u4e0d\u8db3"];
if (!shootData) return [];
return [getDistanceCheckAudioKey(shootData)];
}
function getAckAudioKeys(message, businessMessage) {
@@ -805,13 +796,8 @@ function handleMessage(data) {
return;
}
const typeName = getServerMessageTypeName(message.type);
if (ENABLE_REALTIME_MESSAGE_LOG) {
console.log(
"收到比赛服 WebSocket 消息",
typeName,
getServerMessageLogSummary(message)
);
console.log("收到比赛服 WebSocket 消息", message);
}
const decodedMatchId = normalizeId(pickField(message, "matchId", "match_id"));
+11 -9
View File
@@ -22,7 +22,12 @@ import {
} from "@/apis";
import { connectMatchWebSocket, closeMatchWebSocket } from "@/matchWebsocket";
import { sharePractiseData } from "@/canvas";
import { wxShare, debounce } from "@/util";
import {
wxShare,
debounce,
getDistanceCheckAudioKey,
getShootValidation,
} from "@/util";
import { MESSAGETYPESV2 } from "@/constants";
import useStore from "@/store";
import { storeToRefs } from "pinia";
@@ -154,14 +159,11 @@ async function onReceiveMessage(msg) {
} else if (msg.type === MESSAGETYPESV2.BattleEnd) {
setTimeout(() => onOver(msg), 1500);
} else if (msg.type === MESSAGETYPESV2.TestDistance && step.value === 3) {
const rawDistance = Number(msg.shootData?.distance);
if (rawDistance === 0) {
audioManager.play("未发现靶纸,请瞄准靶纸射箭");
} else if (rawDistance / 100 >= 5) {
audioManager.play("距离合格");
btnDisabled.value = false;
showGuide.value = true;
} else audioManager.play("距离不足");
const result = getShootValidation(msg.shootData);
const isQualified = result.distanceOk && result.targetOk;
audioManager.play(getDistanceCheckAudioKey(msg.shootData));
btnDisabled.value = !isQualified;
showGuide.value = isQualified;
}
// messages.forEach((msg) => {
// if (msg.constructor === MESSAGETYPES.ShootSyncMeArrowID) {
@@ -6,6 +6,7 @@ import Avatar from "./Avatar.vue";
import audioManager from "@/audioManager";
import { simulShootAPI } from "@/apis";
import { MESSAGETYPESV2 } from "@/constants";
import { getDistanceCheckAudioKey, getDistanceCheckText } from "@/util";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
@@ -26,6 +27,7 @@ const props = defineProps({
});
const arrow = ref({});
const distance = ref(0);
const statusText = ref("");
const showsimul = ref(false);
const count = ref(props.count);
const timer = ref(null);
@@ -34,7 +36,7 @@ const updateTimer = (value) => {
count.value = Math.round(value);
};
onMounted(() => {
audioManager.play("请射箭测试距离");
audioManager.play("请射箭测试站距与靶纸");
if (props.isBattle) {
timer.value = setInterval(() => {
count.value -= 1;
@@ -51,14 +53,12 @@ onBeforeUnmount(() => {
async function onReceiveMessage(msg) {
if (Array.isArray(msg)) return;
if (msg.type === MESSAGETYPESV2.TestDistance) {
const rawDistance = Number(msg.shootData?.distance);
const rawDistance = Number(msg.shootData?.distance ?? msg.shootData?.dst);
distance.value = Number.isFinite(rawDistance)
? Number((rawDistance / 100).toFixed(2))
: 0;
if (rawDistance === 0) {
audioManager.play("未发现靶纸,请瞄准靶纸射箭");
} else if (distance.value >= 5) audioManager.play("距离合格");
else audioManager.play("距离不足");
statusText.value = getDistanceCheckText(msg.shootData);
audioManager.play(getDistanceCheckAudioKey(msg.shootData));
}
}
@@ -101,13 +101,12 @@ onBeforeUnmount(() => {
模拟射箭
</button>
<view class="warnning-text">
<block v-if="distance > 0">
<text>当前距离{{ distance }}</text>
<text v-if="distance >= 5">已达到距离要求</text>
<text v-else>请调整站位</text>
<block v-if="statusText">
<text v-if="distance > 0">当前距离{{ distance }}</text>
<text>{{ statusText }}</text>
</block>
<block v-else>
<text>请射箭测试站距</text>
<text>请射箭测试站距与靶纸</text>
</block>
</view>
<view class="user-row">
+6 -2
View File
@@ -20,7 +20,11 @@ import {
MATCH_WS_STATE_EVENT,
} from "@/matchWebsocket";
import { MESSAGETYPESV2 } from "@/constants";
import { getDirectionText, getInvalidShotAudioKey } from "@/util";
import {
getDirectionText,
getInvalidShotAudioKey,
getInvalidShotText,
} from "@/util";
import { takeMatchReturnSnapshot } from "@/utils/matchReturn";
import audioManager, {
AUDIO_INTERRUPTION_BEGIN_EVENT,
@@ -1133,7 +1137,7 @@ async function runBattleEndTask(task, runId) {
async function runInvalidShotTask(task, runId) {
if (!isQueueAlive(runId)) return;
uni.showToast({
title: "距离不足,无效",
title: getInvalidShotText(task.message?.shootData),
icon: "none",
});
await playAudioKeys(getInvalidShotAudioKey(task.message?.shootData), {
@@ -2,7 +2,11 @@
import { ref, watch, onMounted, onBeforeUnmount, computed } from "vue";
import audioManager from "@/audioManager";
import { MESSAGETYPESV2 } from "@/constants";
import { getDirectionText, getInvalidShotAudioKey } from "@/util";
import {
getDirectionText,
getInvalidShotAudioKey,
getInvalidShotText,
} from "@/util";
import Avatar from "@/components/Avatar.vue";
import useStore from "@/store";
@@ -238,7 +242,7 @@ async function onReceiveMessage(msg) {
audioManager.play("中场休息");
} else if (msg.type === MESSAGETYPESV2.InvalidShot) {
uni.showToast({
title: "距离不足,无效",
title: getInvalidShotText(msg.shootData),
icon: "none",
});
audioManager.play(getInvalidShotAudioKey(msg.shootData));
+10 -11
View File
@@ -6,6 +6,7 @@ import Avatar from "@/components/Avatar.vue";
import audioManager from "@/audioManager";
import { simulShootAPI } from "@/apis";
import { MESSAGETYPESV2 } from "@/constants";
import { getDistanceCheckAudioKey, getDistanceCheckText } from "@/util";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
@@ -30,6 +31,7 @@ const props = defineProps({
});
const arrow = ref({});
const distance = ref(0);
const statusText = ref("");
const showsimul = ref(false);
const count = ref(props.count);
const timer = ref(null);
@@ -38,7 +40,7 @@ const updateTimer = (value) => {
count.value = Math.round(value);
};
onMounted(() => {
audioManager.play("请射箭测试距离");
audioManager.play("请射箭测试站距与靶纸");
if (props.isBattle) {
timer.value = setInterval(() => {
count.value -= 1;
@@ -55,14 +57,12 @@ onBeforeUnmount(() => {
async function onReceiveMessage(msg) {
if (Array.isArray(msg)) return;
if (msg.type === MESSAGETYPESV2.TestDistance) {
const rawDistance = Number(msg.shootData?.distance);
const rawDistance = Number(msg.shootData?.distance ?? msg.shootData?.dst);
distance.value = Number.isFinite(rawDistance)
? Number((rawDistance / 100).toFixed(2))
: 0;
if (rawDistance === 0) {
audioManager.play("未发现靶纸,请瞄准靶纸射箭");
} else if (distance.value >= 5) audioManager.play("距离合格");
else audioManager.play("距离不足");
statusText.value = getDistanceCheckText(msg.shootData);
audioManager.play(getDistanceCheckAudioKey(msg.shootData));
}
}
@@ -100,13 +100,12 @@ onBeforeUnmount(() => {
</button>
<view class="warnning-text">
<view class="target-tip">当前靶纸为<text class="text-yellow">{{ targetType }}cm</text>全环靶</view>
<block v-if="distance > 0">
<text>当前距离<text class="text-yellow">{{ distance }}</text></text>
<text v-if="distance >= 5">已达到距离要求</text>
<text v-else>请调整站位</text>
<block v-if="statusText">
<text v-if="distance > 0">当前距离<text class="text-yellow">{{ distance }}</text></text>
<text>{{ statusText }}</text>
</block>
<block v-else>
<text>请射箭测试站距</text>
<text>请射箭测试站距与靶纸</text>
</block>
</view>
<view class="user-row">
+60 -8
View File
@@ -329,18 +329,70 @@ export const getDirectionText = (angle = 0) => {
}
};
// 正式射箭阶段的距离单位为厘米,按距离异常类型选择对应语音
export const getInvalidShotAudioKey = (shootData) => {
if (!shootData || typeof shootData !== "object") return "射击无效";
// protobuf 会省略值为 0 的标量字段;消息体存在且距离缺失时按 0 处理。
// 射箭数据中的距离单位为厘米;旧版本会明确上报 target_ok=true
export const getShootValidation = (shootData) => {
if (!shootData || typeof shootData !== "object") {
return {
rawDistance: 0,
distance: 0,
distanceOk: false,
targetOk: false,
targetDetected: false,
};
}
const rawDistance = Number(shootData.distance ?? shootData.dst ?? 0);
if (!Number.isFinite(rawDistance)) return "射击无效";
if (rawDistance < 0) return "射箭无效,靶纸错误";
if (rawDistance === 0) return "射箭无效,未识别到靶纸";
if (rawDistance / 100 < 5) return "射箭无效,距离不足";
const finiteDistance = Number.isFinite(rawDistance) ? rawDistance : 0;
const targetFlag = shootData.targetOk ?? shootData.target_ok;
const targetDetected = finiteDistance !== 0;
const targetOk = finiteDistance > 0 && targetFlag === true;
return {
rawDistance: finiteDistance,
distance: Math.max(0, finiteDistance / 100),
// 旧固件以负距离表示靶纸错误,此时只提示靶纸问题,不叠加距离不足。
distanceOk: finiteDistance < 0 || finiteDistance >= 500,
targetOk: targetDetected && targetOk,
targetDetected,
};
};
// 测距阶段提示音:同时校验站距和靶纸,保留旧版本距离不足音频。
export const getDistanceCheckAudioKey = (shootData) => {
const result = getShootValidation(shootData);
if (!result.targetDetected) return "未发现靶纸,请瞄准靶纸射箭";
if (!result.distanceOk && !result.targetOk) {
return "射箭无效,距离不足且靶纸错误";
}
if (!result.targetOk) return "靶纸不符!请更换靶纸";
if (result.distanceOk) return "站距与靶纸合格";
return "距离不足";
};
export const getDistanceCheckText = (shootData) => {
const result = getShootValidation(shootData);
if (!result.targetDetected) return "未识别到靶纸,请瞄准靶纸射箭";
if (result.rawDistance < 0) return "靶纸错误";
return `${result.distanceOk ? "站距合格" : "站距过近"}${
result.targetOk ? "靶纸正确" : "靶纸错误"
}`;
};
// 正式射箭阶段的距离和靶纸异常类型选择对应语音。
export const getInvalidShotAudioKey = (shootData) => {
const result = getShootValidation(shootData);
if (!result.targetDetected) return "射箭无效,未识别到靶纸";
if (!result.distanceOk && !result.targetOk) {
return "射箭无效,距离不足且靶纸错误";
}
if (!result.targetOk) return "射箭无效,靶纸错误";
if (!result.distanceOk) return "射箭无效,距离不足";
return "射击无效";
};
export const getInvalidShotText = (shootData) =>
getInvalidShotAudioKey(shootData);
export const wxLogin = () => {
return new Promise((resolve, reject) => {
uni.login({
+1 -1
View File
File diff suppressed because one or more lines are too long
+4 -2
View File
@@ -7,8 +7,8 @@ const { Reader, Writer } = protobuf;
// 所以这里使用 minimal Reader/Writer 做静态字段解码和客户端消息编码。
// <match-schema-generated>
// 此区块由 scripts/generate-match-schema.mjs 自动生成,请勿手动修改。
// 来源:src/utils/match.min.jssha256: 150812589c2e6f7a
// 协议命名空间:rpc;消息数:12;字段数:151
// 来源:src/utils/match.min.jssha256: a7ab92ae43e405e7
// 协议命名空间:rpc;消息数:12;字段数:153
export const ServerMessageType = {
SERVER_MSG_UNKNOWN: 0,
@@ -51,6 +51,7 @@ const SCHEMAS = {
8: { name: "distance", kind: "float" },
9: { name: "three_consecutive_10_rings", kind: "bool" },
10: { name: "ok", kind: "bool" },
11: { name: "target_ok", kind: "bool" },
},
MatchShootList: {
1: { name: "items", kind: "message", type: "MatchShoot", repeated: true },
@@ -139,6 +140,7 @@ const SCHEMAS = {
7: { name: "device_id", kind: "string" },
8: { name: "shoot_id", kind: "string" },
9: { name: "three_consecutive_10_rings", kind: "bool" },
10: { name: "target_ok", kind: "bool" },
},
PracticeInfo: {
1: { name: "id", kind: "int64" },