index.tsx 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. import { Popup, Snackbar } from "@varlet/ui";
  2. import { 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, NoticeBar, showToast, Swipe, SwipeItem } from "vant";
  21. import iconStudent from "./icons/student.png";
  22. import EvaluatResult from "./evaluat-result";
  23. import EvaluatAudio from "./evaluat-audio";
  24. import { api_openWebView, api_proxyServiceMessage, api_videoUpdate } from "/src/helpers/communication";
  25. import EvaluatShare from "./evaluat-share";
  26. // frequency 频率, amplitude 振幅, decibels 分贝
  27. type TCriteria = "frequency" | "amplitude" | "decibels";
  28. export default defineComponent({
  29. name: "evaluat-model",
  30. setup() {
  31. const evaluatModel = reactive({
  32. tips: true,
  33. evaluatUpdateAudio: false,
  34. isSaveVideo: state.setting.camera && state.setting.saveToAlbum,
  35. shareMode: false,
  36. });
  37. /**
  38. * 木管(长笛 萨克斯 单簧管)乐器一级的2、3、6测评要放原音音频
  39. * 铜管乐器一级的1a,1b,5,6测评要放原音音频
  40. */
  41. const getMusicMode = () => {
  42. const muguan = [2, 4, 5, 6];
  43. const tongguan = [12, 13, 14, 15, 17];
  44. if (muguan.includes(state.subjectId) && (state.examSongName || "").search(/[^\u0000-\u00FF](1-2|1-3|1-6)/gi) > -1) {
  45. return "music";
  46. }
  47. if (tongguan.includes(state.subjectId) && (state.examSongName || "").search(/[^\u0000-\u00FF](1-1-1|1-1-2|1-5|1-6)/gi) > -1) {
  48. return "music";
  49. }
  50. if ([23, 113, 121].includes(state.subjectId)) {
  51. return "music";
  52. }
  53. return "background";
  54. };
  55. const browserInfo = browser();
  56. /** 是否是节奏练习 */
  57. const isRhythmicExercises = () => {
  58. const examSongName = state.examSongName || "";
  59. return examSongName.indexOf("节奏练习") > -1;
  60. };
  61. /** 获取评测标准 */
  62. const getEvaluationCriteria = () => {
  63. let criteria: TCriteria = "frequency";
  64. // 声部打击乐
  65. if ([23, 113, 121].includes(state.subjectId)) {
  66. criteria = "amplitude";
  67. } else if (isRhythmicExercises()) {
  68. // 分类为节奏练习
  69. criteria = "decibels";
  70. }
  71. return criteria;
  72. };
  73. /** 生成评测曲谱数据 */
  74. const formatTimes = () => {
  75. let ListenMode = false;
  76. let dontEvaluatingMode = false;
  77. let skip = false;
  78. const datas = [];
  79. for (let index = 0; index < state.times.length; index++) {
  80. const item = state.times[index];
  81. const note = getNoteByMeasuresSlursStart(item);
  82. const rate = state.speed / state.originSpeed;
  83. const difftime = item.difftime;
  84. const start = difftime + (item.sourceRelativeTime || item.relativeTime);
  85. const end = difftime + (item.sourceRelaEndtime || item.relaEndtime);
  86. const isStaccato = note.noteElement.voiceEntry.isStaccato();
  87. const noteRate = isStaccato ? 0.5 : 1;
  88. if (note.formatLyricsEntries.contains("Play") || note.formatLyricsEntries.contains("Play...")) {
  89. ListenMode = false;
  90. }
  91. if (note.formatLyricsEntries.contains("Listen")) {
  92. ListenMode = true;
  93. }
  94. if (note.formatLyricsEntries.contains("纯律结束")) {
  95. dontEvaluatingMode = false;
  96. }
  97. if (note.formatLyricsEntries.contains("纯律")) {
  98. dontEvaluatingMode = true;
  99. }
  100. const nextNote = state.times[index + 1];
  101. // console.log("noteinfo", note.noteElement.isRestFlag && !!note.stave && !!nextNote)
  102. if (skip && (note.stave || !item.noteElement.isRestFlag || (nextNote && !nextNote.noteElement.isRestFlag))) {
  103. skip = false;
  104. }
  105. if (note.noteElement.isRestFlag && !!note.stave && !!nextNote && nextNote.noteElement.isRestFlag) {
  106. skip = true;
  107. }
  108. // console.log(note.measureOpenIndex, item.measureOpenIndex, note);
  109. // console.log("skip", skip)
  110. const data = {
  111. timeStamp: (start * 1000) / rate,
  112. duration: ((end * 1000) / rate - (start * 1000) / rate) * noteRate,
  113. frequency: item.frequency,
  114. nextFrequency: item.nextFrequency,
  115. prevFrequency: item.prevFrequency,
  116. // 重复的情况index会自然累加,render的index是谱面渲染的index
  117. measureIndex: note.measureOpenIndex,
  118. measureRenderIndex: item.measureListIndex,
  119. dontEvaluating: ListenMode || dontEvaluatingMode,
  120. musicalNotesIndex: item.i,
  121. denominator: note.noteElement?.Length.denominator,
  122. };
  123. datas.push(data);
  124. }
  125. return datas;
  126. };
  127. /** 连接websocket */
  128. const handleConnect = async () => {
  129. const behaviorId = localStorage.getItem("behaviorId") || undefined;
  130. const rate = state.speed / state.originSpeed;
  131. const content = {
  132. musicXmlInfos: formatTimes(),
  133. id: state.examSongId,
  134. subjectId: state.subjectId,
  135. detailId: state.detailId,
  136. examSongId: state.examSongId,
  137. xmlUrl: state.xmlUrl,
  138. partIndex: state.partIndex,
  139. behaviorId,
  140. tenantId: storeData.user.tenantId,
  141. platform: browserInfo.ios ? "IOS" : browserInfo.android ? "ANDROID" : "WEB",
  142. clientId: storeData.platformType === "STUDENT" ? "student" : storeData.platformType === "TEACHER" ? "teacher" : "education",
  143. speed: state.speed,
  144. heardLevel: state.setting.evaluationDifficulty,
  145. beatLength: Math.round((state.fixtime * 1000) / rate),
  146. campId: sessionStorage.getItem("campId") || "",
  147. evaluationCriteria: getEvaluationCriteria(),
  148. };
  149. const result = await connectWebsocket(content);
  150. state.playSource = getMusicMode();
  151. };
  152. /** 评测结果按钮处理 */
  153. const handleEvaluatResult = (type: "practise" | "tryagain" | "look" | "share" | "update") => {
  154. if (type === "update") {
  155. // 上传云端
  156. evaluatModel.evaluatUpdateAudio = true;
  157. return;
  158. } else if (type === "share") {
  159. // 分享
  160. evaluatModel.shareMode = true;
  161. return;
  162. } else if (type === "look") {
  163. // 跳转
  164. handleViewReport();
  165. return;
  166. } else if (type === "practise") {
  167. // 去练习
  168. handleStartEvaluat();
  169. } else if (type === "tryagain") {
  170. // 再来一次
  171. handleStartBegin();
  172. }
  173. evaluatingData.resulstMode = false;
  174. };
  175. /** 上传音视频 */
  176. const hanldeUpdateVideoAndAudio = async (update = false) => {
  177. if (!update) {
  178. evaluatModel.evaluatUpdateAudio = false;
  179. return;
  180. }
  181. let res = null;
  182. if (evaluatModel.isSaveVideo) {
  183. res = await api_videoUpdate();
  184. }
  185. api_proxyServiceMessage({
  186. header: {
  187. commond: "videoUpload",
  188. status: 200,
  189. type: "SOUND_COMPARE",
  190. },
  191. body: {
  192. filePath: res?.content?.filePath,
  193. recordId: res?.recordId,
  194. },
  195. });
  196. Snackbar.success("上传成功");
  197. evaluatModel.evaluatUpdateAudio = false;
  198. };
  199. onMounted(() => {
  200. handlePerformDetection();
  201. });
  202. watch(
  203. () => evaluatingData.checkEnd,
  204. () => {
  205. if (evaluatingData.checkEnd) {
  206. console.log("检测结束,连接websocket");
  207. handleConnect();
  208. }
  209. }
  210. );
  211. watch(
  212. () => evaluatingData.startBegin,
  213. () => {
  214. if (evaluatingData.startBegin) {
  215. evaluatModel.tips = false;
  216. }
  217. }
  218. );
  219. return () => (
  220. <div>
  221. {evaluatingData.websocketState && (
  222. <>
  223. {!evaluatingData.startBegin && (
  224. <div class={styles.btn} onClick={handleStartBegin}>
  225. 开始演奏
  226. </div>
  227. )}
  228. {evaluatingData.startBegin && (
  229. <div class={[styles.btn, styles.endBtn]} onClick={() => handleEndBegin(false)}>
  230. <Icon name="success" />
  231. <span>结束演奏</span>
  232. </div>
  233. )}
  234. </>
  235. )}
  236. {evaluatModel.tips && (
  237. <>
  238. <div class={styles.notice}>
  239. <img src={iconStudent} />
  240. <NoticeBar
  241. scrollable={false}
  242. style="background: #fff;color: #01C1B5;"
  243. mode="closeable"
  244. onClose={() => {
  245. evaluatModel.tips = false;
  246. }}
  247. >
  248. <Swipe style="height: 32px;" show-indicators={false} autoplay={3000} vertical>
  249. <SwipeItem>请在周围安静的环境下演奏,减少杂音</SwipeItem>
  250. <SwipeItem>请选择稳定、良好的网络环境,避免信号中断</SwipeItem>
  251. <SwipeItem>演奏前请调试好乐器,保证最佳演奏状态</SwipeItem>
  252. <SwipeItem>演奏时请佩戴耳机,评测收音更精准</SwipeItem>
  253. </Swipe>
  254. </NoticeBar>
  255. </div>
  256. <div style={{ height: "40px" }}></div>
  257. </>
  258. )}
  259. <Popup teleport="body" closeOnClickOverlay={false} defaultStyle={false} v-model:show={evaluatingData.earphoneMode}>
  260. <Earphone
  261. onClose={() => {
  262. evaluatingData.earphoneMode = false;
  263. handlePerformDetection();
  264. }}
  265. />
  266. </Popup>
  267. <Popup teleport="body" closeOnClickOverlay={false} defaultStyle={false} v-model:show={evaluatingData.soundEffectMode}>
  268. <SoundEffect
  269. onClose={(value: any) => {
  270. evaluatingData.soundEffectMode = false;
  271. if (value) {
  272. state.setting.soundEffect = false;
  273. }
  274. handleEndSoundCheck();
  275. handlePerformDetection();
  276. }}
  277. />
  278. </Popup>
  279. <Popup teleport="body" closeOnClickOverlay={false} defaultStyle={false} v-model:show={evaluatingData.resulstMode}>
  280. <EvaluatResult onClose={handleEvaluatResult} />
  281. </Popup>
  282. <Popup teleport="body" closeOnClickOverlay={false} defaultStyle={false} v-model:show={evaluatModel.evaluatUpdateAudio}>
  283. <EvaluatAudio onClose={hanldeUpdateVideoAndAudio} />
  284. </Popup>
  285. <Popup teleport="body" closeOnClickOverlay={false} defaultStyle={false} v-model:show={evaluatModel.shareMode}>
  286. <EvaluatShare onClose={() => (evaluatModel.shareMode = false)} />
  287. </Popup>
  288. </div>
  289. );
  290. },
  291. });