index.tsx 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. import { Popup, Snackbar } from "@varlet/ui";
  2. import { Transition, defineComponent, onMounted, reactive, watch } from "vue";
  3. import {
  4. connectWebsocket,
  5. evaluatingData,
  6. handleEndBegin,
  7. handleEndSoundCheck,
  8. handlePerformDetection,
  9. handleStartBegin,
  10. handleStartEvaluat,
  11. handleViewReport,
  12. } from "/src/view/evaluating";
  13. import Earphone from "./earphone";
  14. import styles from "./index.module.less";
  15. import SoundEffect from "./sound-effect";
  16. import state from "/src/state";
  17. import { storeData } from "/src/store";
  18. import { browser } from "/src/utils";
  19. import { getNoteByMeasuresSlursStart } from "/src/helpers/formateMusic";
  20. import EvaluatResult from "./evaluat-result";
  21. import EvaluatAudio from "./evaluat-audio";
  22. import { api_openWebView, api_proxyServiceMessage, api_videoUpdate } from "/src/helpers/communication";
  23. import EvaluatShare from "./evaluat-share";
  24. import { Vue3Lottie } from "vue3-lottie";
  25. import startData from "./data/start.json";
  26. import startingData from "./data/starting.json";
  27. import iconEvaluat from "./icons/evaluating.json";
  28. import { getQuery } from "/src/utils/queryString";
  29. // frequency 频率, amplitude 振幅, decibels 分贝
  30. type TCriteria = "frequency" | "amplitude" | "decibels";
  31. export default defineComponent({
  32. name: "evaluat-model",
  33. setup() {
  34. const evaluatModel = reactive({
  35. tips: true,
  36. evaluatUpdateAudio: false,
  37. isSaveVideo: state.setting.camera && state.setting.saveToAlbum,
  38. shareMode: false,
  39. });
  40. const browserInfo = browser();
  41. /** 是否是节奏练习 */
  42. const isRhythmicExercises = () => {
  43. const examSongName = state.examSongName || "";
  44. return examSongName.indexOf("节奏练习") > -1;
  45. };
  46. /** 获取评测标准 */
  47. const getEvaluationCriteria = () => {
  48. let criteria: TCriteria = "frequency";
  49. // 声部打击乐
  50. if ([23, 113, 121].includes(state.subjectId)) {
  51. criteria = "amplitude";
  52. } else if (isRhythmicExercises()) {
  53. // 分类为节奏练习
  54. criteria = "decibels";
  55. }
  56. return criteria;
  57. };
  58. /** 生成评测曲谱数据 */
  59. const formatTimes = () => {
  60. const difftime = state.times[0]?.difftime || 0;
  61. let starTime = 0;
  62. let ListenMode = false;
  63. let dontEvaluatingMode = false;
  64. let skip = false;
  65. const datas = [];
  66. let times = state.times;
  67. /** 如果为选段模式,评测 */
  68. if (state.isSelectMeasureMode) {
  69. const startIndex = state.times.findIndex((n: any) => n.noteId == state.section[0].noteId);
  70. const endIndex = state.times.findIndex((n: any) => n.noteId == state.section[1].noteId);
  71. times = state.times.filter((n: any, index: number) => {
  72. return index >= startIndex && index <= endIndex;
  73. });
  74. starTime = times[0].sourceRelativeTime || times[0].relativeTime;
  75. console.log("🚀 ~ times", times);
  76. }
  77. let measureIndex = -1;
  78. let recordMeasure = -1;
  79. for (let index = 0; index < times.length; index++) {
  80. const item = times[index];
  81. const note = getNoteByMeasuresSlursStart(item);
  82. const rate = state.speed / state.originSpeed;
  83. const start = difftime + (item.sourceRelativeTime || item.relativeTime) - starTime;
  84. const end = difftime + (item.sourceRelaEndtime || item.relaEndtime) - starTime;
  85. const isStaccato = note.noteElement.voiceEntry.isStaccato();
  86. const noteRate = isStaccato ? 0.5 : 1;
  87. if (note.formatLyricsEntries.contains("Play") || note.formatLyricsEntries.contains("Play...")) {
  88. ListenMode = false;
  89. }
  90. if (note.formatLyricsEntries.contains("Listen")) {
  91. ListenMode = true;
  92. }
  93. if (note.formatLyricsEntries.contains("纯律结束")) {
  94. dontEvaluatingMode = false;
  95. }
  96. if (note.formatLyricsEntries.contains("纯律")) {
  97. dontEvaluatingMode = true;
  98. }
  99. const nextNote = state.times[index + 1];
  100. // console.log("noteinfo", note.noteElement.isRestFlag && !!note.stave && !!nextNote)
  101. if (skip && (note.stave || !item.noteElement.isRestFlag || (nextNote && !nextNote.noteElement.isRestFlag))) {
  102. skip = false;
  103. }
  104. if (note.noteElement.isRestFlag && !!note.stave && !!nextNote && nextNote.noteElement.isRestFlag) {
  105. skip = true;
  106. }
  107. // console.log(note.measureOpenIndex, item.measureOpenIndex, note);
  108. // console.log("skip", skip)
  109. if (note.measureOpenIndex != recordMeasure) {
  110. measureIndex++
  111. recordMeasure = note.measureOpenIndex
  112. }
  113. const data = {
  114. timeStamp: (start * 1000) / rate,
  115. duration: ((end * 1000) / rate - (start * 1000) / rate) * noteRate,
  116. frequency: item.frequency,
  117. nextFrequency: item.nextFrequency,
  118. prevFrequency: item.prevFrequency,
  119. // 重复的情况index会自然累加,render的index是谱面渲染的index
  120. measureIndex: measureIndex,
  121. measureRenderIndex: item.measureListIndex,
  122. dontEvaluating: ListenMode || dontEvaluatingMode || item.skipMode,
  123. musicalNotesIndex: index,
  124. denominator: note.noteElement?.Length.denominator,
  125. isOrnament: !!note?.voiceEntry?.ornamentContainer,
  126. };
  127. datas.push(data);
  128. }
  129. return datas;
  130. };
  131. /** 连接websocket */
  132. const handleConnect = async () => {
  133. const query = getQuery()
  134. const behaviorId = localStorage.getItem("behaviorId") || undefined;
  135. const rate = state.speed / state.originSpeed;
  136. const content = {
  137. musicXmlInfos: formatTimes(),
  138. subjectId: state.subjectId,
  139. detailId: state.detailId,
  140. examSongId: state.examSongId,
  141. xmlUrl: state.xmlUrl,
  142. partIndex: state.partIndex,
  143. behaviorId,
  144. platform: browserInfo.ios ? "IOS" : browserInfo.android ? "ANDROID" : "WEB",
  145. clientId: storeData.platformType === "STUDENT" ? "student" : storeData.platformType === "TEACHER" ? "teacher" : "education",
  146. hertz: state.setting.frequency,
  147. reactionTimeMs: state.setting.reactionTimeMs,
  148. speed: state.speed,
  149. heardLevel: state.setting.evaluationDifficulty,
  150. beatLength: Math.round((state.fixtime * 1000) / rate),
  151. practiceSource: query.unitId ? 'UNIT_TEST' : 'PRACTICE',
  152. // evaluationCriteria: getEvaluationCriteria(),
  153. };
  154. await connectWebsocket(content);
  155. state.playSource = "music";
  156. console.log("连接成功");
  157. };
  158. /** 评测结果按钮处理 */
  159. const handleEvaluatResult = (type: "practise" | "tryagain" | "look" | "share" | "update") => {
  160. if (type === "update") {
  161. // 上传云端
  162. evaluatModel.evaluatUpdateAudio = true;
  163. return;
  164. } else if (type === "share") {
  165. // 分享
  166. evaluatModel.shareMode = true;
  167. return;
  168. } else if (type === "look") {
  169. // 跳转
  170. handleViewReport();
  171. return;
  172. } else if (type === "practise") {
  173. // 去练习
  174. handleStartEvaluat();
  175. } else if (type === "tryagain") {
  176. // 再来一次
  177. handleStartBegin();
  178. }
  179. evaluatingData.resulstMode = false;
  180. };
  181. /** 上传音视频 */
  182. const hanldeUpdateVideoAndAudio = async (update = false) => {
  183. if (!update) {
  184. evaluatModel.evaluatUpdateAudio = false;
  185. return;
  186. }
  187. let res = null;
  188. if (evaluatModel.isSaveVideo) {
  189. res = await api_videoUpdate();
  190. }
  191. api_proxyServiceMessage({
  192. header: {
  193. commond: "videoUpload",
  194. status: 200,
  195. type: "SOUND_COMPARE",
  196. },
  197. body: {
  198. filePath: res?.content?.filePath,
  199. recordId: res?.recordId,
  200. },
  201. });
  202. Snackbar.success("上传成功");
  203. evaluatModel.evaluatUpdateAudio = false;
  204. };
  205. onMounted(() => {
  206. handlePerformDetection();
  207. });
  208. watch(
  209. () => evaluatingData.checkEnd,
  210. () => {
  211. if (evaluatingData.checkEnd) {
  212. console.log("检测结束,连接websocket");
  213. handleConnect();
  214. }
  215. }
  216. );
  217. return () => (
  218. <div>
  219. <Transition name="pop-center">
  220. {evaluatingData.websocketState && !evaluatingData.startBegin && (
  221. <div class={styles.startBtn} onClick={handleStartBegin}>
  222. <img src={iconEvaluat.evaluatingStart} />
  223. </div>
  224. )}
  225. </Transition>
  226. <Transition name="pop-center">
  227. {evaluatingData.websocketState && evaluatingData.startBegin && (
  228. <div class={styles.endBtn} onClick={() => handleEndBegin()}>
  229. <img src={iconEvaluat.evaluatingEnd} />
  230. </div>
  231. )}
  232. </Transition>
  233. <div style={{ display: !evaluatingData.startBegin ? "" : "none" }} class={styles.dialogueBox} key="start">
  234. <div class={styles.dialogue}>
  235. <img class={styles.dialoguebg} src={iconEvaluat["task-bg"]} />
  236. <div>演奏前请调整好乐器,保证最佳演奏状态。</div>
  237. </div>
  238. <Vue3Lottie class={styles.dialogueIcon} animationData={startData}></Vue3Lottie>
  239. </div>
  240. <div style={{ display: evaluatingData.startBegin ? "" : "none" }} class={styles.dialogueBox} key="start">
  241. <div class={styles.dialogueing}>收音中...</div>
  242. <Vue3Lottie class={styles.dialogueIcon} animationData={startingData}></Vue3Lottie>
  243. </div>
  244. <Popup teleport="body" closeOnClickOverlay={false} defaultStyle={false} v-model:show={evaluatingData.earphoneMode}>
  245. <Earphone
  246. onClose={() => {
  247. evaluatingData.earphoneMode = false;
  248. handlePerformDetection();
  249. }}
  250. />
  251. </Popup>
  252. <Popup teleport="body" closeOnClickOverlay={false} defaultStyle={false} v-model:show={evaluatingData.soundEffectMode}>
  253. <SoundEffect
  254. onClose={(value: any) => {
  255. evaluatingData.soundEffectMode = false;
  256. if (value) {
  257. state.setting.soundEffect = false;
  258. }
  259. handleEndSoundCheck();
  260. handlePerformDetection();
  261. }}
  262. />
  263. </Popup>
  264. <Popup teleport="body" closeOnClickOverlay={false} defaultStyle={false} v-model:show={evaluatingData.resulstMode}>
  265. <EvaluatResult onClose={handleEvaluatResult} />
  266. </Popup>
  267. <Popup teleport="body" closeOnClickOverlay={false} defaultStyle={false} v-model:show={evaluatModel.evaluatUpdateAudio}>
  268. <EvaluatAudio onClose={hanldeUpdateVideoAndAudio} />
  269. </Popup>
  270. <Popup teleport="body" defaultStyle={false} v-model:show={evaluatModel.shareMode}>
  271. <EvaluatShare onClose={() => (evaluatModel.shareMode = false)} />
  272. </Popup>
  273. </div>
  274. );
  275. },
  276. });