index.tsx 9.6 KB

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