index.tsx 9.4 KB

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