index.tsx 19 KB

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