index.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  1. import { Transition, defineComponent, onMounted, reactive, watch, defineAsyncComponent, computed, onUnmounted } from "vue";
  2. import { connectWebsocket, evaluatingData, handleEndBegin, handleStartBegin, handleStartEvaluat, handleViewReport, startCheckDelay, checkUseEarphone, handleCancelEvaluat } from "/src/view/evaluating";
  3. import Earphone from "./earphone";
  4. import styles from "./index.module.less";
  5. import SoundEffect from "./sound-effect";
  6. import state, { handleRessetState, resetPlaybackToStart, musicalInstrumentCodeInfo, clearSelection } from "/src/state";
  7. import { storeData } from "/src/store";
  8. import { browser } from "/src/utils";
  9. import { getNoteByMeasuresSlursStart } from "/src/helpers/formateMusic";
  10. import { Icon, Popup, showToast, closeToast, showLoadingToast } from "vant";
  11. import EvaluatResult from "./evaluat-result";
  12. import EvaluatAudio from "./evaluat-audio";
  13. import { api_getDeviceDelay, api_openAdjustRecording, api_proxyServiceMessage, api_videoUpdate, getEarphone, api_back, api_startDelayCheck, api_cancelDelayCheck, api_closeDelayCheck, api_finishDelayCheck, api_retryEvaluating, api_remove_finishDelayCheck } from "/src/helpers/communication";
  14. import EvaluatShare from "./evaluat-share";
  15. import { Vue3Lottie } from "vue3-lottie";
  16. import startData from "./data/start.json";
  17. import startingData from "./data/starting.json";
  18. import iconTastBg from "./icons/task-bg.svg";
  19. import iconEvaluat from "./icons/evaluating.json";
  20. import { headImg } from "/src/page-instrument/header-top/image";
  21. import { api_musicPracticeRecordVideoUpload } from "../api";
  22. import { headTopData } from "../header-top/index";
  23. import { getQuery } from "/src/utils/queryString";
  24. import Countdown from "./countdown";
  25. import { IPostMessage } from "/src/utils/native-message";
  26. // const DelayCheck = defineAsyncComponent(() =>
  27. // import('./delay-check')
  28. // )
  29. // frequency 频率, amplitude 振幅, decibels 分贝
  30. type TCriteria = "frequency" | "amplitude" | "decibels";
  31. /**
  32. * 节拍器时长
  33. * 评测模式时,应该传节拍器时长
  34. * 阶段评测时,判断是否从第一小节开始,并且曲子本身含有节拍器,需要传节拍器时长,否则传0
  35. */
  36. let actualBeatLength = 0;
  37. let calculateInfo: any = {};
  38. let checkErjiTimer: any = null
  39. export default defineComponent({
  40. name: "evaluat-model",
  41. setup() {
  42. const query = getQuery();
  43. const evaluatModel = reactive({
  44. tips: true,
  45. evaluatUpdateAudio: false,
  46. isSaveVideo: state.setting.camera && state.setting.saveToAlbum,
  47. shareMode: false,
  48. });
  49. /**
  50. * 检测返回
  51. */
  52. const handleDelayBack = () => {
  53. if (query.workRecord) {
  54. evaluatingData.soundEffectMode = false;
  55. api_back();
  56. } else {
  57. evaluatingData.soundEffectMode = false;
  58. handleRessetState();
  59. headTopData.modeType = "init";
  60. }
  61. };
  62. /**
  63. * 执行检测
  64. */
  65. const handlePerformDetection = async () => {
  66. console.log(evaluatingData.checkStep, evaluatingData, "检测123");
  67. // 检测完成不检测了
  68. if (evaluatingData.checkEnd) return;
  69. // 延迟检测
  70. if (evaluatingData.checkStep === 0) {
  71. evaluatingData.checkStep = 10;
  72. // 没有设备延迟数据 或 开启了效音 显示检测组件,并持续检测耳机状态
  73. if (state.setting.soundEffect) {
  74. evaluatingData.soundEffectMode = true;
  75. return;
  76. }
  77. // 判断只有开始了设备检测之后才去调用api
  78. if (state.setting.soundEffect) {
  79. const delayData = await api_getDeviceDelay();
  80. // console.log("🚀 ~ delayTime:", delayData);
  81. if (delayData && delayData.content?.value < 0) {
  82. evaluatingData.soundEffectMode = true;
  83. return;
  84. }
  85. }
  86. handlePerformDetection();
  87. return;
  88. }
  89. // 效验完成
  90. if (evaluatingData.checkStep === 10) {
  91. const erji = await checkUseEarphone();
  92. if (!erji) {
  93. evaluatingData.earphoneMode = true;
  94. }
  95. evaluatingData.checkEnd = true;
  96. console.log("检测结束,生成数据");
  97. handleConnect();
  98. }
  99. };
  100. const browserInfo = browser();
  101. /** 是否是节奏练习 */
  102. const isRhythmicExercises = () => {
  103. const examSongName = state.examSongName || "";
  104. return examSongName.indexOf("节奏练习") > -1;
  105. };
  106. /** 获取评测标准 */
  107. const getEvaluationCriteria = () => {
  108. let criteria: TCriteria = "frequency";
  109. // 声部打击乐
  110. if ([23, 113, 121].includes(state.subjectId)) {
  111. criteria = "amplitude";
  112. } else if (isRhythmicExercises()) {
  113. // 分类为节奏练习
  114. criteria = "decibels";
  115. }
  116. return criteria;
  117. };
  118. /** 校验耳机状态 */
  119. const checkEarphoneStatus = async (type?: string) => {
  120. clearTimeout(checkErjiTimer);
  121. checkErjiTimer = null;
  122. if (type !== "start") {
  123. // const erji = await checkUseEarphone();
  124. const res = await getEarphone();
  125. const erji = res?.content?.checkIsWired || false;
  126. console.log("耳机状态111", res);
  127. evaluatingData.earphoneMode = true;
  128. evaluatingData.earPhoneType = res?.content?.type || "";
  129. if (evaluatingData.earPhoneType === "有线耳机") {
  130. clearTimeout(checkErjiTimer);
  131. checkErjiTimer = null;
  132. setTimeout(() => {
  133. evaluatingData.earphoneMode = false;
  134. }, 3000);
  135. } else {
  136. // 如果没有佩戴有限耳机,需要持续检测耳机状态
  137. checkErjiTimer = setTimeout(() => {
  138. checkEarphoneStatus();
  139. }, 1000);
  140. }
  141. }
  142. console.log("检测结束,生成数据", evaluatingData.websocketState, evaluatingData.startBegin, evaluatingData.checkEnd);
  143. handleConnect();
  144. };
  145. /** 生成评测曲谱数据 */
  146. const formatTimes = () => {
  147. let starTime = 0;
  148. let ListenMode = false;
  149. let dontEvaluatingMode = false;
  150. let skip = false;
  151. const datas = [];
  152. let selectTimes = state.times;
  153. let unitTestIdx = 0;
  154. let preTime = 0;
  155. let preTimes = [];
  156. // 系统节拍器时长
  157. actualBeatLength = Math.round((state.times[0].fixtime * 1000) / 1);
  158. // 如果是阶段评测,选取该阶段的times
  159. if (state.isSelectMeasureMode && state.section.length) {
  160. const startIndex = state.times.findIndex((n: any) => n.noteId == state.section[0].noteId);
  161. let endIndex = state.times.findIndex((n: any) => n.noteId == state.section[1].noteId);
  162. endIndex = endIndex < state.section[1].i ? state.section[1].i : endIndex;
  163. if (startIndex > 1) {
  164. // firstNoteTime应该取预备小节的第一个音符的开始播放的时间
  165. const idx = startIndex - 1 - state.times[startIndex - 1].si;
  166. preTime = state.times[idx] ? state.times[idx].time * 1000 : 0;
  167. }
  168. actualBeatLength = startIndex == 0 && state.isOpenMetronome ? actualBeatLength : 0;
  169. selectTimes = state.times.filter((n: any, index: number) => {
  170. return index >= startIndex && index <= endIndex;
  171. });
  172. preTimes = state.times.filter((n: any, index: number) => {
  173. return index < startIndex;
  174. });
  175. unitTestIdx = startIndex;
  176. starTime = selectTimes[0].sourceRelativeTime || selectTimes[0].relativeTime;
  177. }
  178. // 阶段评测beatLength需要加上预备小节的持续时长
  179. actualBeatLength = preTimes.length ? actualBeatLength + preTimes[preTimes.length - 1].relaMeasureLength * 1000 : actualBeatLength;
  180. let firstNoteTime = unitTestIdx > 1 ? preTime : 0;
  181. let measureIndex = -1;
  182. let recordMeasure = -1;
  183. for (let index = 0; index < selectTimes.length; index++) {
  184. const item = selectTimes[index];
  185. const note = getNoteByMeasuresSlursStart(item);
  186. // #8701 bug: 评测模式,是以曲谱本身的速度进行评测,所以rate取1,不需要转换
  187. // const rate = state.speed / state.originSpeed;
  188. const rate = 1;
  189. const difftime = item.difftime;
  190. const start = difftime + (item.sourceRelativeTime || item.relativeTime) - starTime;
  191. const end = difftime + (item.sourceRelaEndtime || item.relaEndtime) - starTime;
  192. const isStaccato = note.noteElement.voiceEntry.isStaccato();
  193. const noteRate = isStaccato ? 0.5 : 1;
  194. if (note.formatLyricsEntries.contains("Play") || note.formatLyricsEntries.contains("Play...")) {
  195. ListenMode = false;
  196. }
  197. if (note.formatLyricsEntries.contains("Listen")) {
  198. ListenMode = true;
  199. }
  200. if (note.formatLyricsEntries.contains("纯律结束")) {
  201. dontEvaluatingMode = false;
  202. }
  203. if (note.formatLyricsEntries.contains("纯律")) {
  204. dontEvaluatingMode = true;
  205. }
  206. const nextNote = selectTimes[index + 1];
  207. // console.log("noteinfo", note.noteElement.isRestFlag && !!note.stave && !!nextNote)
  208. if (skip && (note.stave || !item.noteElement.isRestFlag || (nextNote && !nextNote.noteElement.isRestFlag))) {
  209. skip = false;
  210. }
  211. if (note.noteElement.isRestFlag && !!note.stave && !!nextNote && nextNote.noteElement.isRestFlag) {
  212. skip = true;
  213. }
  214. // console.log(note.measureOpenIndex, item.measureOpenIndex, note);
  215. // console.log("skip", skip)
  216. // console.log(end,start,rate,noteRate, '评测')
  217. if (note.measureOpenIndex != recordMeasure) {
  218. measureIndex++;
  219. recordMeasure = note.measureOpenIndex;
  220. }
  221. // 是否是需要延续、不停顿演奏的音符
  222. let isTenutoSound = false;
  223. if (item?.noteElement?.tie && item.noteElement.tie?.StartNote) {
  224. const startId = item.noteElement.tie?.StartNote?.NoteToGraphicalNoteObjectId
  225. isTenutoSound = item.NoteToGraphicalNoteObjectId === startId ? false : true
  226. }
  227. const data = {
  228. timeStamp: (start * 1000) / rate,
  229. duration: ((end * 1000) / rate - (start * 1000) / rate) * noteRate,
  230. frequency: item.frequency,
  231. nextFrequency: item.nextFrequency,
  232. prevFrequency: item.prevFrequency,
  233. // 重复的情况index会自然累加,render的index是谱面渲染的index
  234. measureIndex: measureIndex,
  235. measureRenderIndex: item.measureListIndex,
  236. dontEvaluating: item.hasGraceNote || ListenMode || dontEvaluatingMode || !!item?.voiceEntry?.ornamentContainer || !!item.noteElement?.speedInfo?.startWord?.includes('rit.') || item.skipMode,
  237. musicalNotesIndex: index,
  238. denominator: note.noteElement?.Length.denominator,
  239. // isOrnament: !!note?.voiceEntry?.ornamentContainer,
  240. isTenutoSound,
  241. isStaccato: item?.voiceEntry?.isStaccato, // 是否是重音
  242. };
  243. datas.push(data);
  244. }
  245. return {
  246. datas,
  247. firstNoteTime,
  248. };
  249. };
  250. /** 连接websocket */
  251. const handleConnect = async () => {
  252. const behaviorId = localStorage.getItem("behaviorId") || localStorage.getItem("BEHAVIORID") || undefined;
  253. let rate = state.speed / state.originSpeed;
  254. rate = parseFloat(rate.toFixed(2));
  255. console.log("速度比例", rate, "速度", state.speed);
  256. calculateInfo = formatTimes();
  257. const content = {
  258. musicXmlInfos: calculateInfo.datas,
  259. subjectId: state.musicalCode,
  260. detailId: state.detailId,
  261. examSongId: state.examSongId,
  262. xmlUrl: state.xmlUrl,
  263. partIndex: state.partIndex,
  264. behaviorId,
  265. platform: browserInfo.ios ? "IOS" : browserInfo.android ? "ANDROID" : "WEB",
  266. clientId: storeData.platformType === "STUDENT" ? "student" : storeData.platformType === "TEACHER" ? "teacher" : "education",
  267. hertz: state.setting.frequency,
  268. reactionTimeMs: state.setting.reactionTimeMs ? Number(state.setting.reactionTimeMs) : 0,
  269. speed: state.speed,
  270. heardLevel: state.setting.evaluationDifficulty,
  271. // beatLength: Math.round((state.fixtime * 1000) / rate),
  272. beatLength: actualBeatLength,
  273. evaluationCriteria: state.evaluationStandard,
  274. speedRate: rate, // 播放倍率
  275. };
  276. await connectWebsocket(content);
  277. // state.playSource = "music";
  278. };
  279. /** 评测结果按钮处理 */
  280. const handleEvaluatResult = (type: "practise" | "tryagain" | "look" | "share" | "update" | "selfCancel") => {
  281. if (type === "update") {
  282. if (state.isAppPlay) {
  283. evaluatModel.evaluatUpdateAudio = true;
  284. resetPlaybackToStart();
  285. return;
  286. } else if (evaluatingData.resultData?.recordIdStr || evaluatingData.resultData?.recordId) {
  287. let rate = state.speed / state.originSpeed;
  288. rate = parseFloat(rate.toFixed(2));
  289. // 上传云端
  290. // evaluatModel.evaluatUpdateAudio = true;
  291. api_openAdjustRecording({
  292. recordId: evaluatingData.resultData?.recordIdStr || evaluatingData.resultData?.recordId,
  293. title: state.examSongName || "曲谱演奏",
  294. coverImg: state.coverImg,
  295. speedRate: rate, // 播放倍率
  296. musicRenderType: state.musicRenderType,
  297. musicSheetId: state.examSongId,
  298. 'part-index': state.partIndex
  299. });
  300. return;
  301. }
  302. } else if (type === "share") {
  303. // 分享
  304. evaluatModel.shareMode = true;
  305. return;
  306. } else if (type === "look") {
  307. // 跳转
  308. handleViewReport("recordId", "instrument");
  309. return;
  310. } else if (type === "practise") {
  311. // 去练习
  312. handleStartEvaluat();
  313. } else if (type === "tryagain") {
  314. startBtnHandle();
  315. } else if (type === "selfCancel") {
  316. // 再来一次,需要手动取消评测,不生成评测记录,不显示评测结果弹窗
  317. evaluatingData.oneselfCancleEvaluating = true;
  318. handleCancelEvaluat();
  319. startBtnHandle();
  320. }
  321. resetPlaybackToStart();
  322. evaluatingData.resulstMode = false;
  323. };
  324. /** 上传音视频 */
  325. const hanldeUpdateVideoAndAudio = async (update = false) => {
  326. if (!update) {
  327. evaluatModel.evaluatUpdateAudio = false;
  328. return;
  329. }
  330. if (state.setting.camera && state.setting.saveToAlbum) {
  331. evaluatModel.evaluatUpdateAudio = false;
  332. api_videoUpdate((res: any) => {
  333. if (res) {
  334. if (res?.content?.type === "success") {
  335. handleSaveResult({
  336. id: evaluatingData.resultData?.recordId,
  337. videoFilePath: res?.content?.filePath,
  338. });
  339. } else if (res?.content?.type === "error") {
  340. showToast({
  341. message: res.content?.message || "上传失败",
  342. });
  343. }
  344. }
  345. });
  346. return;
  347. }
  348. evaluatModel.evaluatUpdateAudio = false;
  349. showToast("上传成功");
  350. };
  351. const handleSaveResult = async (_body: any) => {
  352. await api_musicPracticeRecordVideoUpload(_body);
  353. showToast("上传成功");
  354. };
  355. const startBtnHandle = async () => {
  356. // 选段未完成时,清除选段状态
  357. if (state.sectionStatus && state.section.length < 2) {
  358. clearSelection();
  359. }
  360. // 如果是异常状态,先等待500ms再执行后续流程
  361. if (evaluatingData.isErrorState && !state.setting.soundEffect) {
  362. // console.log('异常流程1')
  363. showLoadingToast({
  364. message: "处理中",
  365. duration: 1000,
  366. overlay: true,
  367. overlayClass: styles.scoreMode,
  368. });
  369. await new Promise<void>((resolve) => {
  370. setTimeout(() => {
  371. closeToast();
  372. evaluatingData.isErrorState = false;
  373. // console.log('异常流程2')
  374. resolve();
  375. }, 1000);
  376. });
  377. }
  378. // console.log('异常流程3')
  379. // 检测APP端socket状态
  380. const res: any = await startCheckDelay();
  381. if (res?.checked) {
  382. handleConnect();
  383. handleStartBegin(calculateInfo.firstNoteTime);
  384. if (evaluatingData.isErrorState) {
  385. evaluatingData.isErrorState = false;
  386. evaluatingData.resulstMode = false;
  387. }
  388. }
  389. };
  390. // 监听到APP取消延迟检测
  391. const handleCancelDelayCheck = async (res?: IPostMessage) => {
  392. console.log("监听取消延迟检测", res);
  393. if (res?.content) {
  394. // 关闭延迟检测页面,并返回到模式选择页面
  395. // await api_closeDelayCheck({});
  396. handleDelayBack();
  397. }
  398. };
  399. // 监听APP延迟成功的回调
  400. const handleFinishDelayCheck = async (res?: IPostMessage) => {
  401. console.log("监听延迟检测成功", res);
  402. evaluatingData.socketErrorPop = false;
  403. if (res?.content) {
  404. evaluatingData.checkEnd = true;
  405. checkEarphoneStatus();
  406. }
  407. };
  408. // 监听重复评测消息
  409. const handRetryEvaluating = () => {
  410. handleEvaluatResult("tryagain");
  411. };
  412. const earPhonePopShow = computed(() => {
  413. return evaluatingData.earphoneMode && !state.isLoading && !state.hasDriverPop;
  414. });
  415. onMounted(async () => {
  416. // 如果打开了延迟检测开关,需要先发送开始检测的消息
  417. if (state.setting.soundEffect) {
  418. await api_startDelayCheck({});
  419. } else {
  420. evaluatingData.checkEnd = true;
  421. checkEarphoneStatus();
  422. }
  423. evaluatingData.isDisabledPlayMusic = true;
  424. // handlePerformDetection();
  425. api_cancelDelayCheck(handleCancelDelayCheck);
  426. api_finishDelayCheck(handleFinishDelayCheck);
  427. api_retryEvaluating(handRetryEvaluating);
  428. });
  429. onUnmounted(() => {
  430. api_remove_finishDelayCheck(handleFinishDelayCheck);
  431. clearTimeout(checkErjiTimer);
  432. checkErjiTimer = null;
  433. });
  434. return () => (
  435. <div>
  436. <div class={styles.operatingBtn}>
  437. {evaluatingData.websocketState && !evaluatingData.startBegin && evaluatingData.checkEnd && (
  438. <img
  439. class={[styles.iconBtn, "evaluting-1"]}
  440. src={headImg("icon_play.png")}
  441. onClick={() => {
  442. startBtnHandle();
  443. }}
  444. />
  445. )}
  446. {evaluatingData.websocketState && evaluatingData.startBegin && (
  447. <>
  448. <img class={styles.iconBtn} src={headImg("icon_reset.png")} onClick={() => handleEvaluatResult("selfCancel")} />
  449. <img class={styles.iconBtn} src={headImg("submit.png")} onClick={() => handleEndBegin()} />
  450. </>
  451. )}
  452. </div>
  453. {/* {evaluatingData.soundEffectMode && (
  454. <DelayCheck
  455. onClose={() => {
  456. evaluatingData.soundEffectMode = false;
  457. handlePerformDetection();
  458. }}
  459. onBack={() => handleDelayBack()}
  460. />
  461. )} */}
  462. {/* 倒计时 */}
  463. <Countdown />
  464. {/* 遮罩 */}
  465. {
  466. evaluatingData.isBeginMask && <div class={styles.beginMask}></div>
  467. }
  468. <Popup teleport="body" closeOnClickOverlay={false} class={["popup-custom", "van-scale"]} transition="van-scale" v-model:show={earPhonePopShow.value}>
  469. <Earphone
  470. earphoneType={evaluatingData.earPhoneType}
  471. onClose={() => {
  472. clearTimeout(checkErjiTimer);
  473. checkErjiTimer = null;
  474. evaluatingData.earphoneMode = false;
  475. // handlePerformDetection();
  476. checkEarphoneStatus("start");
  477. }}
  478. />
  479. </Popup>
  480. {/* 评测作业,非完整评测不显示评测结果弹窗 */}
  481. {evaluatingData.hideResultModal ? (
  482. <EvaluatResult onClose={handleEvaluatResult} />
  483. ) : (
  484. <Popup teleport="body" closeOnClickOverlay={false} class={["popup-custom", "van-scale"]} transition="van-scale" v-model:show={evaluatingData.resulstMode}>
  485. <EvaluatResult onClose={handleEvaluatResult} />
  486. </Popup>
  487. )}
  488. <Popup teleport="body" closeOnClickOverlay={false} class={["popup-custom", "van-scale"]} transition="van-scale" v-model:show={evaluatModel.evaluatUpdateAudio}>
  489. <EvaluatAudio onClose={hanldeUpdateVideoAndAudio} />
  490. </Popup>
  491. <Popup teleport="body" class={["popup-custom", "van-scale"]} transition="van-scale" v-model:show={evaluatModel.shareMode}>
  492. <EvaluatShare onClose={() => (evaluatModel.shareMode = false)} />
  493. </Popup>
  494. </div>
  495. );
  496. },
  497. });