index.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. import { Transition, defineComponent, onMounted, reactive, watch } from "vue";
  2. import {
  3. connectWebsocket,
  4. evaluatingData,
  5. handleEndBegin,
  6. handleStartBegin,
  7. handleStartEvaluat,
  8. handleViewReport,
  9. } from "/src/view/evaluating";
  10. import Earphone from "./earphone";
  11. import styles from "./index.module.less";
  12. import SoundEffect from "./sound-effect";
  13. import state from "/src/state";
  14. import { storeData } from "/src/store";
  15. import { browser } from "/src/utils";
  16. import { getNoteByMeasuresSlursStart } from "/src/helpers/formateMusic";
  17. import { Icon, Popup, showToast } from "vant";
  18. import EvaluatResult from "./evaluat-result";
  19. import EvaluatAudio from "./evaluat-audio";
  20. import { api_getDeviceDelay, api_proxyServiceMessage, api_videoUpdate, getEarphone } from "/src/helpers/communication";
  21. import EvaluatShare from "./evaluat-share";
  22. import { Vue3Lottie } from "vue3-lottie";
  23. import startData from "./data/start.json";
  24. import startingData from "./data/starting.json";
  25. import iconTastBg from "./icons/task-bg.svg";
  26. import iconEvaluat from "./icons/evaluating.json";
  27. import { api_musicPracticeRecordVideoUpload } from "../api";
  28. import DelayCheck from "./delay-check";
  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. /**
  41. * 执行检测
  42. */
  43. const handlePerformDetection = async () => {
  44. // 检测完成不检测了
  45. if (evaluatingData.checkEnd) return;
  46. // 延迟检测
  47. if (evaluatingData.checkStep === 0) {
  48. evaluatingData.checkStep = 10;
  49. // 没有设备延迟数据 或 开启了效音 显示检测组件,并持续检测耳机状态
  50. if (state.setting.soundEffect) {
  51. evaluatingData.soundEffectMode = true;
  52. return;
  53. }
  54. const delayData = await api_getDeviceDelay();
  55. // console.log("🚀 ~ delayTime:", delayData);
  56. if (delayData && delayData.content?.value < 0) {
  57. evaluatingData.soundEffectMode = true;
  58. return;
  59. }
  60. handlePerformDetection();
  61. return;
  62. }
  63. // 效验完成
  64. if (evaluatingData.checkStep === 10) {
  65. evaluatingData.checkEnd = true;
  66. console.log("检测结束,生成数据");
  67. handleConnect();
  68. }
  69. };
  70. const browserInfo = browser();
  71. /** 是否是节奏练习 */
  72. const isRhythmicExercises = () => {
  73. const examSongName = state.examSongName || "";
  74. return examSongName.indexOf("节奏练习") > -1;
  75. };
  76. /** 获取评测标准 */
  77. const getEvaluationCriteria = () => {
  78. let criteria: TCriteria = "frequency";
  79. // 声部打击乐
  80. if ([23, 113, 121].includes(state.subjectId)) {
  81. criteria = "amplitude";
  82. } else if (isRhythmicExercises()) {
  83. // 分类为节奏练习
  84. criteria = "decibels";
  85. }
  86. return criteria;
  87. };
  88. /** 生成评测曲谱数据 */
  89. const formatTimes = () => {
  90. let ListenMode = false;
  91. let dontEvaluatingMode = false;
  92. let skip = false;
  93. const datas = [];
  94. for (let index = 0; index < state.times.length; index++) {
  95. const item = state.times[index];
  96. const note = getNoteByMeasuresSlursStart(item);
  97. const rate = state.speed / state.originSpeed;
  98. const difftime = item.difftime;
  99. const start = difftime + (item.sourceRelativeTime || item.relativeTime);
  100. const end = difftime + (item.sourceRelaEndtime || item.relaEndtime);
  101. const isStaccato = note.noteElement.voiceEntry.isStaccato();
  102. const noteRate = isStaccato ? 0.5 : 1;
  103. if (note.formatLyricsEntries.contains("Play") || note.formatLyricsEntries.contains("Play...")) {
  104. ListenMode = false;
  105. }
  106. if (note.formatLyricsEntries.contains("Listen")) {
  107. ListenMode = true;
  108. }
  109. if (note.formatLyricsEntries.contains("纯律结束")) {
  110. dontEvaluatingMode = false;
  111. }
  112. if (note.formatLyricsEntries.contains("纯律")) {
  113. dontEvaluatingMode = true;
  114. }
  115. const nextNote = state.times[index + 1];
  116. // console.log("noteinfo", note.noteElement.isRestFlag && !!note.stave && !!nextNote)
  117. if (
  118. skip &&
  119. (note.stave || !item.noteElement.isRestFlag || (nextNote && !nextNote.noteElement.isRestFlag))
  120. ) {
  121. skip = false;
  122. }
  123. if (
  124. note.noteElement.isRestFlag &&
  125. !!note.stave &&
  126. !!nextNote &&
  127. nextNote.noteElement.isRestFlag
  128. ) {
  129. skip = true;
  130. }
  131. // console.log(note.measureOpenIndex, item.measureOpenIndex, note);
  132. // console.log("skip", skip)
  133. const data = {
  134. timeStamp: (start * 1000) / rate,
  135. duration: ((end * 1000) / rate - (start * 1000) / rate) * noteRate,
  136. frequency: item.frequency,
  137. nextFrequency: item.nextFrequency,
  138. prevFrequency: item.prevFrequency,
  139. // 重复的情况index会自然累加,render的index是谱面渲染的index
  140. measureIndex: note.measureOpenIndex,
  141. measureRenderIndex: item.measureListIndex,
  142. dontEvaluating: ListenMode || dontEvaluatingMode || item.skipMode,
  143. musicalNotesIndex: item.i,
  144. denominator: note.noteElement?.Length.denominator,
  145. isOrnament: !!note?.voiceEntry?.ornamentContainer,
  146. };
  147. datas.push(data);
  148. }
  149. return datas;
  150. };
  151. /** 连接websocket */
  152. const handleConnect = async () => {
  153. const behaviorId = localStorage.getItem("behaviorId") || undefined;
  154. const rate = state.speed / state.originSpeed;
  155. const content = {
  156. musicXmlInfos: formatTimes(),
  157. subjectId: state.subjectId,
  158. detailId: state.detailId,
  159. examSongId: state.examSongId,
  160. xmlUrl: state.xmlUrl,
  161. partIndex: state.partIndex,
  162. behaviorId,
  163. platform: browserInfo.ios ? "IOS" : browserInfo.android ? "ANDROID" : "WEB",
  164. clientId:
  165. storeData.platformType === "STUDENT"
  166. ? "student"
  167. : storeData.platformType === "TEACHER"
  168. ? "teacher"
  169. : "education",
  170. hertz: state.setting.frequency,
  171. reactionTimeMs: state.setting.reactionTimeMs,
  172. speed: state.speed,
  173. heardLevel: state.setting.evaluationDifficulty,
  174. beatLength: Math.round((state.fixtime * 1000) / rate),
  175. evaluationCriteria: getEvaluationCriteria(),
  176. };
  177. await connectWebsocket(content);
  178. // state.playSource = "music";
  179. };
  180. /** 评测结果按钮处理 */
  181. const handleEvaluatResult = (type: "practise" | "tryagain" | "look" | "share" | "update") => {
  182. if (type === "update") {
  183. // 上传云端
  184. evaluatModel.evaluatUpdateAudio = true;
  185. return;
  186. } else if (type === "share") {
  187. // 分享
  188. evaluatModel.shareMode = true;
  189. return;
  190. } else if (type === "look") {
  191. // 跳转
  192. handleViewReport("recordId", "instrument");
  193. return;
  194. } else if (type === "practise") {
  195. // 去练习
  196. handleStartEvaluat();
  197. } else if (type === "tryagain") {
  198. // 再来一次
  199. handleStartBegin();
  200. }
  201. evaluatingData.resulstMode = false;
  202. };
  203. /** 上传音视频 */
  204. const hanldeUpdateVideoAndAudio = async (update = false) => {
  205. if (!update) {
  206. evaluatModel.evaluatUpdateAudio = false;
  207. return;
  208. }
  209. if (state.setting.camera && state.setting.saveToAlbum) {
  210. evaluatModel.evaluatUpdateAudio = false;
  211. api_videoUpdate((res: any) => {
  212. if (res) {
  213. if (res?.content?.type === "success") {
  214. handleSaveResult({
  215. id: evaluatingData.resultData?.recordId,
  216. videoFilePath: res?.content?.filePath,
  217. });
  218. } else if (res?.content?.type === "error") {
  219. showToast({
  220. message: res.content?.message || "上传失败",
  221. });
  222. }
  223. }
  224. });
  225. return;
  226. }
  227. evaluatModel.evaluatUpdateAudio = false;
  228. showToast("上传成功");
  229. };
  230. const handleSaveResult = async (_body: any) => {
  231. await api_musicPracticeRecordVideoUpload(_body);
  232. showToast("上传成功");
  233. };
  234. onMounted(() => {
  235. handlePerformDetection();
  236. });
  237. return () => (
  238. <div>
  239. <Transition name="pop-center">
  240. {evaluatingData.websocketState && !evaluatingData.startBegin && evaluatingData.checkEnd && (
  241. <div class={styles.startBtn} onClick={handleStartBegin}>
  242. <img src={iconEvaluat.evaluatingStart} />
  243. </div>
  244. )}
  245. </Transition>
  246. <Transition name="pop-center">
  247. {evaluatingData.websocketState && evaluatingData.startBegin && (
  248. <div class={styles.endBtn} onClick={() => handleEndBegin()}>
  249. <img src={iconEvaluat.evaluatingEnd} />
  250. </div>
  251. )}
  252. </Transition>
  253. <div
  254. style={{ display: !evaluatingData.startBegin ? "" : "none" }}
  255. class={styles.dialogueBox}
  256. key="start"
  257. >
  258. <div class={styles.dialogue}>
  259. <img class={styles.dialoguebg} src={iconTastBg} />
  260. <div>演奏前请调整好乐器,保证最佳演奏状态。</div>
  261. </div>
  262. <Vue3Lottie class={styles.dialogueIcon} animationData={startData}></Vue3Lottie>
  263. </div>
  264. <div
  265. style={{ display: evaluatingData.startBegin ? "" : "none" }}
  266. class={styles.dialogueBox}
  267. key="start"
  268. >
  269. <div class={styles.dialogueing}>收音中...</div>
  270. <Vue3Lottie class={styles.dialogueIcon} animationData={startingData}></Vue3Lottie>
  271. </div>
  272. {evaluatingData.soundEffectMode && <DelayCheck onClose={() => {
  273. evaluatingData.soundEffectMode = false;
  274. handlePerformDetection();
  275. }} />}
  276. <Popup
  277. teleport="body"
  278. closeOnClickOverlay={false}
  279. class={["popup-custom", "van-scale"]}
  280. transition="van-scale"
  281. v-model:show={evaluatingData.earphoneMode}
  282. >
  283. <Earphone
  284. onClose={() => {
  285. evaluatingData.earphoneMode = false;
  286. handlePerformDetection();
  287. }}
  288. />
  289. </Popup>
  290. {/* <Popup
  291. teleport="body"
  292. closeOnClickOverlay={false}
  293. class={["popup-custom", "van-scale"]}
  294. transition="van-scale"
  295. v-model:show={evaluatingData.soundEffectMode}
  296. >
  297. <SoundEffect
  298. onClose={(value: any) => {
  299. evaluatingData.soundEffectMode = false;
  300. if (value) {
  301. state.setting.soundEffect = false;
  302. }
  303. handleEndSoundCheck();
  304. handlePerformDetection();
  305. }}
  306. />
  307. </Popup> */}
  308. <Popup
  309. teleport="body"
  310. closeOnClickOverlay={false}
  311. class={["popup-custom", "van-scale"]}
  312. transition="van-scale"
  313. v-model:show={evaluatingData.resulstMode}
  314. >
  315. <EvaluatResult onClose={handleEvaluatResult} />
  316. </Popup>
  317. <Popup
  318. teleport="body"
  319. closeOnClickOverlay={false}
  320. class={["popup-custom", "van-scale"]}
  321. transition="van-scale"
  322. v-model:show={evaluatModel.evaluatUpdateAudio}
  323. >
  324. <EvaluatAudio onClose={hanldeUpdateVideoAndAudio} />
  325. </Popup>
  326. <Popup
  327. teleport="body"
  328. class={["popup-custom", "van-scale"]}
  329. transition="van-scale"
  330. v-model:show={evaluatModel.shareMode}
  331. >
  332. <EvaluatShare onClose={() => (evaluatModel.shareMode = false)} />
  333. </Popup>
  334. </div>
  335. );
  336. },
  337. });