import { Transition, defineComponent, onMounted, reactive, watch, defineAsyncComponent, computed, onUnmounted } from "vue"; import { connectWebsocket, evaluatingData, handleEndBegin, handleStartBegin, handleStartEvaluat, handleViewReport, startCheckDelay, checkUseEarphone, handleCancelEvaluat, checkMinInterval, handleEndEvaluat } from "/src/view/evaluating"; import Earphone from "./earphone"; import styles from "./index.module.less"; import SoundEffect from "./sound-effect"; import state, { handleRessetState, resetPlaybackToStart, clearSelection, initSetPlayRate, resetBaseRate } from "/src/state"; import { storeData } from "/src/store"; import { browser } from "/src/utils"; import { getNoteByMeasuresSlursStart } from "/src/helpers/formateMusic"; import { Icon, Popup, showToast, closeToast, showLoadingToast } from "vant"; import EvaluatResult from "./evaluat-result"; import EvaluatAudio from "./evaluat-audio"; import { api_getDeviceDelay, api_openAdjustRecording, api_proxyServiceMessage, api_videoUpdate, getEarphone, api_back, api_startDelayCheck, api_cancelDelayCheck, api_remove_cancelDelayCheck, api_closeDelayCheck, api_finishDelayCheck, api_retryEvaluating, api_remove_finishDelayCheck, api_workUpdate } 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 { headImg } from "/src/page-instrument/header-top/image"; import { api_musicPracticeRecordVideoUpload } from "../api"; import { headTopData } from "../header-top/index"; import { getQuery } from "/src/utils/queryString"; import Countdown from "./countdown"; import { IPostMessage } from "/src/utils/native-message"; import tipErjiBg from "./icons/tip_erji.png" import tipErjiBtn from "./icons/tip_btn.png" import SubmitNoDonePop from "./submit-nodone"; import { selfSubmitWorkHome } from "../custom-plugins/work-index"; // const DelayCheck = defineAsyncComponent(() => // import('./delay-check') // ) // frequency 频率, amplitude 振幅, decibels 分贝 type TCriteria = "frequency" | "amplitude" | "decibels"; /** * 节拍器时长 * 评测模式时,应该传节拍器时长 * 阶段评测时,判断是否从第一小节开始,并且曲子本身含有节拍器,需要传节拍器时长,否则传0 */ let actualBeatLength = 0; let calculateInfo: any = {}; let checkErjiTimer: any = null export const reCheckDelay = () => { evaluatingData.onceErjiPopShow = false; evaluatingData.needCheckErjiStatus = true; headTopData.settingMode = false state.setting.soundEffect = false api_startDelayCheck({}); } 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, showNoDonePop: false, // 提交作业显示未达标确认弹窗 }); /** * 检测返回 */ const handleDelayBack = () => { if (query.workRecord) { evaluatingData.soundEffectMode = false; api_back(); } else { evaluatingData.soundEffectMode = false; // handleRessetState(); // headTopData.modeType = "init"; } }; /** * 执行检测 */ const handlePerformDetection = async () => { console.log(evaluatingData.checkStep, evaluatingData, "检测123"); // 检测完成不检测了 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) { const erji = await checkUseEarphone(); if (!erji) { evaluatingData.earphoneMode = true; } 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 checkEarphoneStatus = async (type?: string) => { clearTimeout(checkErjiTimer); checkErjiTimer = null; if (type !== "start") { // const erji = await checkUseEarphone(); const res = await getEarphone(); const erji = res?.content?.checkIsWired || false; // 是否已经提示过耳机弹窗,重新进入评测页面,重置该状态为false,手动关掉耳机弹窗,改变该状态为true,本次评测都不在提示耳机状态弹窗 if (!evaluatingData.onceErjiPopShow) { evaluatingData.earphoneMode = true; } else { clearTimeout(checkErjiTimer); checkErjiTimer = null; return; } evaluatingData.earPhoneType = res?.content?.type || ""; if (evaluatingData.earPhoneType === "有线耳机") { clearTimeout(checkErjiTimer); checkErjiTimer = null; setTimeout(() => { evaluatingData.earphoneMode = false; }, 1500); } else { // 如果没有佩戴有限耳机,需要持续检测耳机状态 checkErjiTimer = setTimeout(() => { checkEarphoneStatus(); }, 1000); } } console.log("检测结束,生成数据", evaluatingData.websocketState, evaluatingData.startBegin, evaluatingData.checkEnd); handleConnect(); }; /** 生成评测曲谱数据 */ const formatTimes = () => { console.log('评测111') let starTime = 0; let ListenMode = false; let dontEvaluatingMode = false; let skip = false; const datas = []; let selectTimes = state.times; // 选段评测前面小节的listen、play标识 let preLyricsContent = '' 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.isOpenMetronome ? actualBeatLength : 0; // 妙极客的曲子,选择的第一小节,beatLength需要传递fixtime if (state.isEvxml && startIndex == 0) { actualBeatLength = Math.round((state.times[0].fixtime * 1000) / 1); } 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; // 如果是弱起,并且预备小节是第一节 if (state.section.length && state.sectionFirst && state.sectionFirst.measureListIndex == 0 && !state.isEvxml) { // actualBeatLength = actualBeatLength < Math.round((state.times[0].fixtime * 1000) / 1) ? Math.round((state.times[0].fixtime * 1000) / 1) : actualBeatLength; } let firstNoteTime = unitTestIdx > 1 ? preTime : 0; let measureIndex = -1; let recordMeasure = -1; // 如果有mp3节拍器,并且预备小节是第一节,并且从0开始播放,actualBeatLength需要加上mp3节拍器时间 if (state.section.length === 2 && firstNoteTime === 0 && state.section[0]?.MeasureNumberXML === state.firstMeasureNumber + 1 && state.times[0].fixtime) { actualBeatLength = actualBeatLength + Math.round((state.times[0].fixtime * 1000) / 1) } // 找到选段评测,开始小节前面最近的是play或者listen的小节 if (preTimes.length) { for (let index = preTimes.length-1; index >= 0; index--) { const item = preTimes[index] const note = getNoteByMeasuresSlursStart(item) if (note.formatLyricsEntries.contains('Play') || note.formatLyricsEntries.contains('Play...')) { preLyricsContent = 'Play' break } if (note.formatLyricsEntries.contains('Listen')) { preLyricsContent = 'Listen' break } } preLyricsContent = preLyricsContent ? preLyricsContent : 'Play' } 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 = state.basePlayRate * state.originAudioPlayRate; // 播放倍率 // const difftime = item.difftime; const difftime = 0; 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 (index == 0 && !note.formatLyricsEntries.length) { ListenMode = preLyricsContent === 'Play' ? false : preLyricsContent === 'Listen' ? true : false } 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; } // 是否是需要延续、不停顿演奏的音符 let isTenutoSound = false; if (item?.noteElement?.tie && item.noteElement.tie?.StartNote) { const startId = item.noteElement.tie?.StartNote?.NoteToGraphicalNoteObjectId isTenutoSound = item.NoteToGraphicalNoteObjectId === startId ? false : true } // 音符是否不需要评测 let noteNeedEvaluat = item.hasGraceNote || ListenMode || dontEvaluatingMode || !!item?.voiceEntry?.ornamentContainer || !!item.noteElement?.speedInfo?.startWord?.includes('rit.') || item.skipMode noteNeedEvaluat = noteNeedEvaluat == true ? true : false; 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, // item.MeasureNumberXML >= 1 ? item.MeasureNumberXML - 1 : note.noteElement.sourceMeasure.measureListIndex, dontEvaluating: noteNeedEvaluat, musicalNotesIndex: index, denominator: note.noteElement?.Length.denominator, // isOrnament: !!note?.voiceEntry?.ornamentContainer, isTenutoSound, isStaccato: item?.voiceEntry?.isStaccato ? true : false, // 是否是重音 frequencyList: item.frequencyList, // 如果是和弦音符,需要添加多个音符的频率,用于评测 }; datas.push(data); } return { datas, firstNoteTime, }; }; /** 连接websocket */ const handleConnect = async () => { const behaviorId = localStorage.getItem("behaviorId") || localStorage.getItem("BEHAVIORID") || undefined; // let rate = state.speed / state.originSpeed; const rate = state.basePlayRate * state.originAudioPlayRate; // 播放倍率 // rate = parseFloat(rate.toFixed(2)); console.log("速度比例", rate, "速度", state.speed); calculateInfo = formatTimes(); // 评测的速度,如果是选段,则选选段开头小节的速度 const evaluatSpeed = state.sectionStatus && state.section.length === 2 && state.section[0].measureSpeed ? state.section[0].measureSpeed * state.basePlayRate : state.speed; evaluatingData.evaluatSpeed = evaluatSpeed; const content = { musicXmlInfos: calculateInfo.datas, subjectId: state.musicalCode, 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 ? Number(state.setting.reactionTimeMs) : 0, speed: evaluatSpeed, heardLevel: state.setting.evaluationDifficulty, // beatLength: Math.round((state.fixtime * 1000) / rate), beatLength: actualBeatLength / rate, evaluationCriteria: state.evaluationStandard, speedRate: parseFloat(rate.toFixed(2)), // 播放倍率 }; await connectWebsocket(content); // state.playSource = "music"; }; /** 评测结果按钮处理 */ const handleEvaluatResult = (type: "practise" | "tryagain" | "look" | "share" | "update" | "selfCancel" | "submitWork") => { if (type === "update") { if (state.isAppPlay) { evaluatModel.evaluatUpdateAudio = true; resetPlaybackToStart(); return; } else if (evaluatingData.resultData?.recordIdStr || evaluatingData.resultData?.recordId) { const rate = state.basePlayRate * state.originAudioPlayRate; // 播放倍率 // 上传云端 // evaluatModel.evaluatUpdateAudio = true; api_openAdjustRecording({ recordId: evaluatingData.resultData?.recordIdStr || evaluatingData.resultData?.recordId, title: state.examSongName || "曲谱演奏", coverImg: state.coverImg, speedRate: parseFloat(rate.toFixed(2)), // 播放倍率 musicRenderType: state.musicRenderType, musicSheetId: state.examSongId, 'part-index': state.partIndex }); 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") { /** * TODO: 2025.01.20 再来一次改为只是关闭结果弹窗,需要用户手动点击评测按钮触发评测,不自动进行评测,故注释掉下方自动评测的方法 */ // startBtnHandle(); } else if (type === "selfCancel") { // 再来一次,需要手动取消评测,不生成评测记录,不显示评测结果弹窗 evaluatingData.oneselfCancleEvaluating = true; // handleCancelEvaluat(); handleEndEvaluat(false, 'selfCancel'); // evaluatingData.isBeginMask = true; evaluatingData.evaluatings = {}; state.playState = "paused"; } else if (type === "submitWork") { // 作业模式,提交作业,作业没有达标时,提交作业需要弹窗提醒 if (!state.isWorkDone) { evaluatModel.showNoDonePop = true; return; } else { submitWorkHome(); } } resetPlaybackToStart(); evaluatingData.resulstMode = false; }; // 关闭提交作业确认弹窗 const handleCloseSubmitPop = (type: "again" | "confirm") => { evaluatModel.showNoDonePop = false; if (type === "again") { handleEvaluatResult("tryagain"); } else { submitWorkHome(); resetPlaybackToStart(); evaluatingData.resulstMode = false; } } // 提交作业 const submitWorkHome = async () => { // 分为开了摄像头和没开摄像头的情况 if (state.setting.camera) { const res = await api_workUpdate(); console.log('提交作业回调',res) if (res) { if (res?.content?.type === "success") { handleSaveResult({ id: evaluatingData.resultData?.recordId, videoFilePath: res?.content?.filePath, }); // 手动提交评测作业 selfSubmitWorkHome(); } else if (res?.content?.type === "error") { showToast({ message: res.content?.message || "上传失败", }); } } } else { // 手动提交评测作业 selfSubmitWorkHome(); } } /** 上传音视频 */ 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 () => { // 如果打开了延迟检测开关,需要先发送开始检测的消息 const delayData = await api_getDeviceDelay(); console.log('设备的延迟值',delayData.content?.value) if (delayData && delayData.content?.value <= 0) { await api_startDelayCheck({}); return; } evaluatingData.needReplayEvaluat = false; // 选段未完成时,清除选段状态 if (state.sectionStatus && state.section.length < 2) { clearSelection(); } // 如果是异常状态,先等待500ms再执行后续流程 if (evaluatingData.isErrorState && !state.setting.soundEffect) { // console.log('异常流程1') // showLoadingToast({ // message: "处理中", // duration: 1000, // overlay: true, // overlayClass: styles.scoreMode, // }); state.loadingText = "处理中…"; state.isLoading = true; await new Promise((resolve) => { setTimeout(() => { // closeToast(); state.isLoading = false; evaluatingData.isErrorState = false; // console.log('异常流程2') resolve(); }, 1000); }); } // console.log('异常流程3') // 非选段状态,从头开始评测,重置速度 if (!state.sectionStatus && state.section.length === 0) { state.activeNoteIndex = 0; state.speed = state.times[0].measureSpeed * state.basePlayRate // console.log('速度',7,state.speed) } initSetPlayRate(); // 检测APP端socket状态 const res: any = await startCheckDelay(); if (res?.checked) { handleConnect(); handleStartBegin(calculateInfo.firstNoteTime); evaluatingData.resulstMode = false; if (evaluatingData.isErrorState) { evaluatingData.isErrorState = false; // evaluatingData.resulstMode = false; } } }; // 监听到APP取消延迟检测 const handleCancelDelayCheck = async (res?: IPostMessage) => { console.log("监听取消延迟检测", res); if (res?.content) { // 关闭延迟检测页面,并返回到模式选择页面 // await api_closeDelayCheck({}); handleDelayBack(); } }; // 监听APP延迟成功的回调 const handleFinishDelayCheck = async (res?: IPostMessage) => { console.log("监听延迟检测成功", res); evaluatingData.socketErrorPop = false; if (res?.content) { evaluatingData.checkEnd = true; state.setting.soundEffect = false; evaluatingData.tipErjiShow = true; } }; // 监听重复评测消息 const handRetryEvaluating = () => { handleEvaluatResult("tryagain"); }; const earPhonePopShow = computed(() => { return evaluatingData.earphoneMode && !state.isLoading && !state.hasDriverPop && !evaluatingData.showOpenCameraPop; }); const tipErjiPopShow = computed(() => { return evaluatingData.tipErjiShow && !state.isLoading && !state.hasDriverPop && !evaluatingData.showOpenCameraPop; }); // watch( // () => state.setting.soundEffect, // (val) => { // if (val) { // headTopData.settingMode = false // api_startDelayCheck({}); // state.setting.soundEffect = false // } // } // ); // 手动取消评测,需要自动再次评测 // watch( // () => evaluatingData.needReplayEvaluat, // (val) => { // if (val && evaluatingData.oneselfCancleEvaluating) { // setTimeout(() => { // startBtnHandle(); // }, 500); // } // } // ); onMounted(async () => { // 如果打开了延迟检测开关,需要先发送开始检测的消息 const delayData = await api_getDeviceDelay(); console.log('设备的延迟值',delayData.content?.value) if (delayData && delayData.content?.value <= 0) { await api_startDelayCheck({}); } else { evaluatingData.checkEnd = true; // 点击评测模式进入评测模块的需要检测耳机状态,通过返回按钮进入评测模块的,不检测耳机状态 if (evaluatingData.needCheckErjiStatus) { checkEarphoneStatus(); } } evaluatingData.isDisabledPlayMusic = true; // handlePerformDetection(); api_cancelDelayCheck(handleCancelDelayCheck); api_finishDelayCheck(handleFinishDelayCheck); api_retryEvaluating(handRetryEvaluating); }); onUnmounted(() => { api_remove_finishDelayCheck(handleFinishDelayCheck); api_remove_cancelDelayCheck(handleCancelDelayCheck); clearTimeout(checkErjiTimer); checkErjiTimer = null; }); // 资源类型 const isPad = navigator?.userAgent?.includes("UAWEIVRD-W09") || browserInfo?.iPad || browserInfo.isTablet; return () => (
{!evaluatingData.startBegin && ( { startBtnHandle(); }} /> )} {evaluatingData.startBegin && ( <> { // 校验评测最小间隔时间 const currentTime = +new Date(); // 开始评测和结束评测的间隔时间小于800毫秒,则不处理 if (currentTime - evaluatingData.recordingTime < 800) { return; } handleEvaluatResult("selfCancel") }} /> { // 校验评测最小间隔时间 const currentTime = +new Date(); // 开始评测和结束评测的间隔时间小于800毫秒,则不处理 if (currentTime - evaluatingData.recordingTime < 800) { return; } handleEndBegin() }} /> )}
{/* {evaluatingData.soundEffectMode && ( { evaluatingData.soundEffectMode = false; handlePerformDetection(); }} onBack={() => handleDelayBack()} /> )} */} {/* 倒计时 */} {/* 遮罩 */} { evaluatingData.isBeginMask &&
}
{ evaluatingData.tipErjiShow = false; checkEarphoneStatus(); }} />
{ evaluatingData.onceErjiPopShow = true; clearTimeout(checkErjiTimer); checkErjiTimer = null; // #11035,可能刚好关闭耳机弹窗的时候,第二次又出现了弹窗 setTimeout(() => { evaluatingData.earphoneMode = false; }, 300); // handlePerformDetection(); checkEarphoneStatus("start"); }} /> {/* 评测作业,非完整评测不显示评测结果弹窗 */} { evaluatingData.resulstMode && <> {evaluatingData.hideResultModal ? ( ) : ( )} } (evaluatModel.shareMode = false)} />
); }, });