|
@@ -0,0 +1,298 @@
|
|
|
+import { Popup, Snackbar } from "@varlet/ui";
|
|
|
+import { defineComponent, onMounted, reactive, watch } from "vue";
|
|
|
+import {
|
|
|
+ connectWebsocket,
|
|
|
+ evaluatingData,
|
|
|
+ handleEndBegin,
|
|
|
+ handleEndSoundCheck,
|
|
|
+ handlePerformDetection,
|
|
|
+ handleStartBegin,
|
|
|
+ handleStartEvaluat,
|
|
|
+ handleViewReport,
|
|
|
+} from "/src/view/evaluating";
|
|
|
+import Earphone from "./earphone";
|
|
|
+import styles from "./index.module.less";
|
|
|
+import SoundEffect from "./sound-effect";
|
|
|
+import state from "/src/state";
|
|
|
+import { storeData } from "/src/store";
|
|
|
+import { browser } from "/src/utils";
|
|
|
+import { getNoteByMeasuresSlursStart } from "/src/helpers/formateMusic";
|
|
|
+import { Icon, NoticeBar, showToast, Swipe, SwipeItem } from "vant";
|
|
|
+import iconStudent from "./icons/student.png";
|
|
|
+import EvaluatResult from "./evaluat-result";
|
|
|
+import EvaluatAudio from "./evaluat-audio";
|
|
|
+import { api_openWebView, api_proxyServiceMessage, api_videoUpdate } from "/src/helpers/communication";
|
|
|
+import EvaluatShare from "./evaluat-share";
|
|
|
+
|
|
|
+// frequency 频率, amplitude 振幅, decibels 分贝
|
|
|
+type TCriteria = "frequency" | "amplitude" | "decibels";
|
|
|
+
|
|
|
+export default defineComponent({
|
|
|
+ name: "evaluat-model",
|
|
|
+ setup() {
|
|
|
+ const evaluatModel = reactive({
|
|
|
+ tips: true,
|
|
|
+ evaluatUpdateAudio: false,
|
|
|
+ isSaveVideo: state.setting.camera && state.setting.saveToAlbum,
|
|
|
+ shareMode: false,
|
|
|
+ });
|
|
|
+ /**
|
|
|
+ * 木管(长笛 萨克斯 单簧管)乐器一级的2、3、6测评要放原音音频
|
|
|
+ * 铜管乐器一级的1a,1b,5,6测评要放原音音频
|
|
|
+ */
|
|
|
+ const getMusicMode = () => {
|
|
|
+ const muguan = [2, 4, 5, 6];
|
|
|
+ const tongguan = [12, 13, 14, 15, 17];
|
|
|
+ if (muguan.includes(state.subjectId) && (state.examSongName || "").search(/[^\u0000-\u00FF](1-2|1-3|1-6)/gi) > -1) {
|
|
|
+ return "music";
|
|
|
+ }
|
|
|
+ if (tongguan.includes(state.subjectId) && (state.examSongName || "").search(/[^\u0000-\u00FF](1-1-1|1-1-2|1-5|1-6)/gi) > -1) {
|
|
|
+ return "music";
|
|
|
+ }
|
|
|
+ if ([23, 113, 121].includes(state.subjectId)) {
|
|
|
+ return "music";
|
|
|
+ }
|
|
|
+ return "background";
|
|
|
+ };
|
|
|
+ const browserInfo = browser();
|
|
|
+ /** 是否是节奏练习 */
|
|
|
+ const isRhythmicExercises = () => {
|
|
|
+ const examSongName = state.examSongName || "";
|
|
|
+ return examSongName.indexOf("节奏练习") > -1;
|
|
|
+ };
|
|
|
+
|
|
|
+ /** 获取评测标准 */
|
|
|
+ const getEvaluationCriteria = () => {
|
|
|
+ let criteria: TCriteria = "frequency";
|
|
|
+ // 声部打击乐
|
|
|
+ if ([23, 113, 121].includes(state.subjectId)) {
|
|
|
+ criteria = "amplitude";
|
|
|
+ } else if (isRhythmicExercises()) {
|
|
|
+ // 分类为节奏练习
|
|
|
+ criteria = "decibels";
|
|
|
+ }
|
|
|
+ return criteria;
|
|
|
+ };
|
|
|
+
|
|
|
+ /** 生成评测曲谱数据 */
|
|
|
+ const formatTimes = () => {
|
|
|
+ let ListenMode = false;
|
|
|
+ let dontEvaluatingMode = false;
|
|
|
+ let skip = false;
|
|
|
+ const datas = [];
|
|
|
+ for (let index = 0; index < state.times.length; index++) {
|
|
|
+ const item = state.times[index];
|
|
|
+ const note = getNoteByMeasuresSlursStart(item);
|
|
|
+ const rate = state.speed / state.originSpeed;
|
|
|
+ const difftime = item.difftime;
|
|
|
+ const start = difftime + (item.sourceRelativeTime || item.relativeTime);
|
|
|
+ const end = difftime + (item.sourceRelaEndtime || item.relaEndtime);
|
|
|
+ const isStaccato = note.noteElement.voiceEntry.isStaccato();
|
|
|
+ const noteRate = isStaccato ? 0.5 : 1;
|
|
|
+ if (note.formatLyricsEntries.contains("Play") || note.formatLyricsEntries.contains("Play...")) {
|
|
|
+ ListenMode = false;
|
|
|
+ }
|
|
|
+ if (note.formatLyricsEntries.contains("Listen")) {
|
|
|
+ ListenMode = true;
|
|
|
+ }
|
|
|
+ if (note.formatLyricsEntries.contains("纯律结束")) {
|
|
|
+ dontEvaluatingMode = false;
|
|
|
+ }
|
|
|
+ if (note.formatLyricsEntries.contains("纯律")) {
|
|
|
+ dontEvaluatingMode = true;
|
|
|
+ }
|
|
|
+ const nextNote = state.times[index + 1];
|
|
|
+ // console.log("noteinfo", note.noteElement.isRestFlag && !!note.stave && !!nextNote)
|
|
|
+ if (skip && (note.stave || !item.noteElement.isRestFlag || (nextNote && !nextNote.noteElement.isRestFlag))) {
|
|
|
+ skip = false;
|
|
|
+ }
|
|
|
+ if (note.noteElement.isRestFlag && !!note.stave && !!nextNote && nextNote.noteElement.isRestFlag) {
|
|
|
+ skip = true;
|
|
|
+ }
|
|
|
+ // console.log(note.measureOpenIndex, item.measureOpenIndex, note);
|
|
|
+ // console.log("skip", skip)
|
|
|
+ const data = {
|
|
|
+ timeStamp: (start * 1000) / rate,
|
|
|
+ duration: ((end * 1000) / rate - (start * 1000) / rate) * noteRate,
|
|
|
+ frequency: item.frequency,
|
|
|
+ nextFrequency: item.nextFrequency,
|
|
|
+ prevFrequency: item.prevFrequency,
|
|
|
+ // 重复的情况index会自然累加,render的index是谱面渲染的index
|
|
|
+ measureIndex: note.measureOpenIndex,
|
|
|
+ measureRenderIndex: item.measureListIndex,
|
|
|
+ dontEvaluating: ListenMode || dontEvaluatingMode,
|
|
|
+ musicalNotesIndex: item.i,
|
|
|
+ denominator: note.noteElement?.Length.denominator,
|
|
|
+ };
|
|
|
+ datas.push(data);
|
|
|
+ }
|
|
|
+ return datas;
|
|
|
+ };
|
|
|
+ /** 连接websocket */
|
|
|
+ const handleConnect = async () => {
|
|
|
+ const behaviorId = localStorage.getItem("behaviorId") || undefined;
|
|
|
+ const rate = state.speed / state.originSpeed;
|
|
|
+ const content = {
|
|
|
+ musicXmlInfos: formatTimes(),
|
|
|
+ id: state.examSongId,
|
|
|
+ subjectId: state.subjectId,
|
|
|
+ detailId: state.detailId,
|
|
|
+ examSongId: state.examSongId,
|
|
|
+ xmlUrl: state.xmlUrl,
|
|
|
+ partIndex: state.partIndex,
|
|
|
+ behaviorId,
|
|
|
+ tenantId: storeData.user.tenantId,
|
|
|
+ platform: browserInfo.ios ? "IOS" : browserInfo.android ? "ANDROID" : "WEB",
|
|
|
+ clientId: storeData.platformType === "STUDENT" ? "student" : storeData.platformType === "TEACHER" ? "teacher" : "education",
|
|
|
+ speed: state.speed,
|
|
|
+ heardLevel: state.setting.evaluationDifficulty,
|
|
|
+ beatLength: Math.round((state.fixtime * 1000) / rate),
|
|
|
+ campId: sessionStorage.getItem("campId") || "",
|
|
|
+ evaluationCriteria: getEvaluationCriteria(),
|
|
|
+ };
|
|
|
+ const result = await connectWebsocket(content);
|
|
|
+ state.playSource = getMusicMode();
|
|
|
+ };
|
|
|
+
|
|
|
+ /** 评测结果按钮处理 */
|
|
|
+ const handleEvaluatResult = (type: "practise" | "tryagain" | "look" | "share" | "update") => {
|
|
|
+ if (type === "update") {
|
|
|
+ // 上传云端
|
|
|
+ evaluatModel.evaluatUpdateAudio = true;
|
|
|
+ return;
|
|
|
+ } else if (type === "share") {
|
|
|
+ // 分享
|
|
|
+ evaluatModel.shareMode = true;
|
|
|
+ return;
|
|
|
+ } else if (type === "look") {
|
|
|
+ // 跳转
|
|
|
+ handleViewReport();
|
|
|
+ return;
|
|
|
+ } else if (type === "practise") {
|
|
|
+ // 去练习
|
|
|
+ handleStartEvaluat();
|
|
|
+ } else if (type === "tryagain") {
|
|
|
+ // 再来一次
|
|
|
+ handleStartBegin();
|
|
|
+ }
|
|
|
+ evaluatingData.resulstMode = false;
|
|
|
+ };
|
|
|
+ /** 上传音视频 */
|
|
|
+ const hanldeUpdateVideoAndAudio = async (update = false) => {
|
|
|
+ if (!update) {
|
|
|
+ evaluatModel.evaluatUpdateAudio = false;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ let res = null;
|
|
|
+ if (evaluatModel.isSaveVideo) {
|
|
|
+ res = await api_videoUpdate();
|
|
|
+ }
|
|
|
+ api_proxyServiceMessage({
|
|
|
+ header: {
|
|
|
+ commond: "videoUpload",
|
|
|
+ status: 200,
|
|
|
+ type: "SOUND_COMPARE",
|
|
|
+ },
|
|
|
+ body: {
|
|
|
+ filePath: res?.content?.filePath,
|
|
|
+ recordId: res?.recordId,
|
|
|
+ },
|
|
|
+ });
|
|
|
+ Snackbar.success("上传成功");
|
|
|
+ evaluatModel.evaluatUpdateAudio = false;
|
|
|
+ };
|
|
|
+
|
|
|
+ onMounted(() => {
|
|
|
+ handlePerformDetection();
|
|
|
+ });
|
|
|
+ watch(
|
|
|
+ () => evaluatingData.checkEnd,
|
|
|
+ () => {
|
|
|
+ if (evaluatingData.checkEnd) {
|
|
|
+ console.log("检测结束,连接websocket");
|
|
|
+ handleConnect();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ );
|
|
|
+ watch(
|
|
|
+ () => evaluatingData.startBegin,
|
|
|
+ () => {
|
|
|
+ if (evaluatingData.startBegin) {
|
|
|
+ evaluatModel.tips = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ );
|
|
|
+ return () => (
|
|
|
+ <div>
|
|
|
+ {evaluatingData.websocketState && (
|
|
|
+ <>
|
|
|
+ {!evaluatingData.startBegin && (
|
|
|
+ <div class={styles.btn} onClick={handleStartBegin}>
|
|
|
+ 开始演奏
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ {evaluatingData.startBegin && (
|
|
|
+ <div class={[styles.btn, styles.endBtn]} onClick={() => handleEndBegin(false)}>
|
|
|
+ <Icon name="success" />
|
|
|
+ <span>结束演奏</span>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ {evaluatModel.tips && (
|
|
|
+ <>
|
|
|
+ <div class={styles.notice}>
|
|
|
+ <img src={iconStudent} />
|
|
|
+ <NoticeBar
|
|
|
+ scrollable={false}
|
|
|
+ style="background: #fff;color: #01C1B5;"
|
|
|
+ mode="closeable"
|
|
|
+ onClose={() => {
|
|
|
+ evaluatModel.tips = false;
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <Swipe style="height: 32px;" show-indicators={false} autoplay={3000} vertical>
|
|
|
+ <SwipeItem>请在周围安静的环境下演奏,减少杂音</SwipeItem>
|
|
|
+ <SwipeItem>请选择稳定、良好的网络环境,避免信号中断</SwipeItem>
|
|
|
+ <SwipeItem>演奏前请调试好乐器,保证最佳演奏状态</SwipeItem>
|
|
|
+ <SwipeItem>演奏时请佩戴耳机,评测收音更精准</SwipeItem>
|
|
|
+ </Swipe>
|
|
|
+ </NoticeBar>
|
|
|
+ </div>
|
|
|
+ <div style={{ height: "40px" }}></div>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ <Popup teleport="body" closeOnClickOverlay={false} defaultStyle={false} v-model:show={evaluatingData.earphoneMode}>
|
|
|
+ <Earphone
|
|
|
+ onClose={() => {
|
|
|
+ evaluatingData.earphoneMode = false;
|
|
|
+ handlePerformDetection();
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </Popup>
|
|
|
+ <Popup teleport="body" closeOnClickOverlay={false} defaultStyle={false} v-model:show={evaluatingData.soundEffectMode}>
|
|
|
+ <SoundEffect
|
|
|
+ onClose={(value: any) => {
|
|
|
+ evaluatingData.soundEffectMode = false;
|
|
|
+ if (value) {
|
|
|
+ state.setting.soundEffect = false;
|
|
|
+ }
|
|
|
+ handleEndSoundCheck();
|
|
|
+ handlePerformDetection();
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </Popup>
|
|
|
+
|
|
|
+ <Popup teleport="body" closeOnClickOverlay={false} defaultStyle={false} v-model:show={evaluatingData.resulstMode}>
|
|
|
+ <EvaluatResult onClose={handleEvaluatResult} />
|
|
|
+ </Popup>
|
|
|
+ <Popup teleport="body" closeOnClickOverlay={false} defaultStyle={false} v-model:show={evaluatModel.evaluatUpdateAudio}>
|
|
|
+ <EvaluatAudio onClose={hanldeUpdateVideoAndAudio} />
|
|
|
+ </Popup>
|
|
|
+ <Popup teleport="body" closeOnClickOverlay={false} defaultStyle={false} v-model:show={evaluatModel.shareMode}>
|
|
|
+ <EvaluatShare onClose={() => (evaluatModel.shareMode = false)} />
|
|
|
+ </Popup>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ },
|
|
|
+});
|