import { Transition, defineComponent, onMounted, reactive, watch } from "vue"; import { connectWebsocket, evaluatingData, handleEndBegin, handleStartBegin, handleStartEvaluat, handleViewReport, startCheckDelay } from "/src/view/evaluating"; import Earphone from "./earphone"; import styles from "./index.module.less"; import SoundEffect from "./sound-effect"; import state, { handleRessetState } from "/src/state"; import { storeData } from "/src/store"; import { browser } from "/src/utils"; import { getNoteByMeasuresSlursStart } from "/src/helpers/formateMusic"; import { Icon, Popup, showToast } from "vant"; import EvaluatResult from "./evaluat-result"; import EvaluatAudio from "./evaluat-audio"; import { api_getDeviceDelay, api_openAdjustRecording, api_proxyServiceMessage, api_videoUpdate, getEarphone } from "/src/helpers/communication"; import EvaluatShare from "./evaluat-share"; import { Vue3Lottie } from "vue3-lottie"; import startData from "./data/start.json"; import startingData from "./data/starting.json"; import iconTastBg from "./icons/task-bg.svg"; import iconEvaluat from "./icons/evaluating.json"; import { api_musicPracticeRecordVideoUpload } from "../api"; import DelayCheck from "./delay-check"; import { headTopData } from "../header-top/index"; import { getQuery } from "/src/utils/queryString"; // frequency 频率, amplitude 振幅, decibels 分贝 type TCriteria = "frequency" | "amplitude" | "decibels"; /** * 节拍器时长 * 评测模式时,应该传节拍器时长 * 阶段评测时,判断是否从第一小节开始,并且曲子本身含有节拍器,需要传节拍器时长,否则传0 */ let actualBeatLength = 0 let calculateInfo: any = {} export default defineComponent({ name: "evaluat-model", setup() { const query = getQuery(); const evaluatModel = reactive({ tips: true, evaluatUpdateAudio: false, isSaveVideo: state.setting.camera && state.setting.saveToAlbum, shareMode: false, }); /** * 检测返回 */ const handleDelayBack = () => { if (query.workRecord) { evaluatingData.soundEffectMode = false; } else { evaluatingData.soundEffectMode = false; handleRessetState(); headTopData.modeType = "init"; } } /** * 执行检测 */ const handlePerformDetection = async () => { console.log(evaluatingData.checkStep, evaluatingData, "检测"); // 检测完成不检测了 if (evaluatingData.checkEnd) return; // 延迟检测 if (evaluatingData.checkStep === 0) { evaluatingData.checkStep = 10; // 没有设备延迟数据 或 开启了效音 显示检测组件,并持续检测耳机状态 if (state.setting.soundEffect) { evaluatingData.soundEffectMode = true; return; } // 判断只有开始了设备检测之后才去调用api if (state.setting.soundEffect) { const delayData = await api_getDeviceDelay(); // console.log("🚀 ~ delayTime:", delayData); if (delayData && delayData.content?.value < 0) { evaluatingData.soundEffectMode = true; return; } } handlePerformDetection(); return; } // 效验完成 if (evaluatingData.checkStep === 10) { evaluatingData.checkEnd = true; console.log("检测结束,生成数据"); handleConnect(); } }; 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 starTime = 0 let ListenMode = false; let dontEvaluatingMode = false; let skip = false; const datas = []; let selectTimes = state.times let unitTestIdx = 0 let preTime = 0 let preTimes = [] // 系统节拍器时长 actualBeatLength = Math.round(state.times[0].fixtime * 1000 / 1) // 如果是阶段评测,选取该阶段的times if (state.isSelectMeasureMode && state.section.length) { const startIndex = state.times.findIndex( (n: any) => n.noteId == state.section[0].noteId ) let endIndex = state.times.findIndex( (n: any) => n.noteId == state.section[1].noteId ) endIndex = endIndex < state.section[1].i ? state.section[1].i : endIndex if (startIndex > 1) { // firstNoteTime应该取预备小节的第一个音符的开始播放的时间 const idx = startIndex - 1 - (state.times[startIndex-1].si) preTime = state.times[idx] ? state.times[idx].time * 1000 : 0 } actualBeatLength = startIndex == 0 && !state.needTick ? actualBeatLength : 0 selectTimes = state.times.filter((n: any, index: number) => { return index >= startIndex && index <= endIndex }) preTimes = state.times.filter((n: any, index: number) => { return index < startIndex }) unitTestIdx = startIndex starTime = selectTimes[0].sourceRelativeTime || selectTimes[0].relativeTime } // 阶段评测beatLength需要加上预备小节的持续时长 actualBeatLength = preTimes.length ? actualBeatLength + preTimes[preTimes.length - 1].relaMeasureLength * 1000 : actualBeatLength let firstNoteTime = unitTestIdx > 1 ? preTime : 0 let measureIndex = -1 let recordMeasure = -1 for (let index = 0; index < selectTimes.length; index++) { const item = selectTimes[index]; const note = getNoteByMeasuresSlursStart(item); // #8701 bug: 评测模式,是以曲谱本身的速度进行评测,所以rate取1,不需要转换 // const rate = state.speed / state.originSpeed; const rate = 1; const difftime = item.difftime; const start = difftime + (item.sourceRelativeTime || item.relativeTime) - starTime; const end = difftime + (item.sourceRelaEndtime || item.relaEndtime) - starTime; 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 = selectTimes[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) // console.log(end,start,rate,noteRate, '评测') if (note.measureOpenIndex != recordMeasure) { measureIndex++ recordMeasure = note.measureOpenIndex } 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: measureIndex, measureRenderIndex: item.measureListIndex, dontEvaluating: ListenMode || dontEvaluatingMode || item.skipMode, musicalNotesIndex: index, denominator: note.noteElement?.Length.denominator, isOrnament: !!note?.voiceEntry?.ornamentContainer, }; datas.push(data); } return { datas, firstNoteTime } }; /** 连接websocket */ const handleConnect = async () => { const behaviorId = localStorage.getItem("behaviorId") || undefined; const rate = state.speed / state.originSpeed; calculateInfo = formatTimes() const content = { musicXmlInfos: calculateInfo.datas, subjectId: state.subjectId, detailId: state.detailId, examSongId: state.examSongId, xmlUrl: state.xmlUrl, partIndex: state.partIndex, behaviorId, platform: browserInfo.ios ? "IOS" : browserInfo.android ? "ANDROID" : "WEB", clientId: storeData.platformType === "STUDENT" ? "student" : storeData.platformType === "TEACHER" ? "teacher" : "education", hertz: state.setting.frequency, reactionTimeMs: state.setting.reactionTimeMs, speed: state.speed, heardLevel: state.setting.evaluationDifficulty, // beatLength: Math.round((state.fixtime * 1000) / rate), beatLength: actualBeatLength, evaluationCriteria: getEvaluationCriteria(), }; await connectWebsocket(content); // state.playSource = "music"; }; /** 评测结果按钮处理 */ const handleEvaluatResult = (type: "practise" | "tryagain" | "look" | "share" | "update") => { if (type === "update") { if (evaluatingData.resultData?.recordIdStr || evaluatingData.resultData?.recordId) { // 上传云端 // evaluatModel.evaluatUpdateAudio = true; api_openAdjustRecording({ recordId: evaluatingData.resultData?.recordIdStr || evaluatingData.resultData?.recordId, title: state.examSongName || "曲谱演奏", coverImg: state.coverImg, }); return; } } else if (type === "share") { // 分享 evaluatModel.shareMode = true; return; } else if (type === "look") { // 跳转 handleViewReport("recordId", "instrument"); return; } else if (type === "practise") { // 去练习 handleStartEvaluat(); } else if (type === "tryagain") { // 再来一次 startBtnHandle() } evaluatingData.resulstMode = false; }; /** 上传音视频 */ const hanldeUpdateVideoAndAudio = async (update = false) => { if (!update) { evaluatModel.evaluatUpdateAudio = false; return; } if (state.setting.camera && state.setting.saveToAlbum) { evaluatModel.evaluatUpdateAudio = false; api_videoUpdate((res: any) => { if (res) { if (res?.content?.type === "success") { handleSaveResult({ id: evaluatingData.resultData?.recordId, videoFilePath: res?.content?.filePath, }); } else if (res?.content?.type === "error") { showToast({ message: res.content?.message || "上传失败", }); } } }); return; } evaluatModel.evaluatUpdateAudio = false; showToast("上传成功"); }; const handleSaveResult = async (_body: any) => { await api_musicPracticeRecordVideoUpload(_body); showToast("上传成功"); }; const startBtnHandle = async () => { // 检测APP端socket状态 const res: any = await startCheckDelay(); if (res?.checked) { handleConnect(); handleStartBegin(calculateInfo.firstNoteTime); } } onMounted(() => { evaluatingData.isDisabledPlayMusic = true; handlePerformDetection(); }); return () => (