index.tsx 9.1 KB

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