| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298 |
- 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>
- );
- },
- });
|