import { Transition, defineComponent, onMounted, reactive, watch, defineAsyncComponent } from "vue"; import { connectWebsocket, evaluatingData, handleEndBegin, handleStartBegin, handleStartEvaluat, handleViewReport, startCheckDelay, checkUseEarphone, handleCancelEvaluat } from "/src/view/evaluating"; import Earphone from "./earphone"; import styles from "./index.module.less"; import SoundEffect from "./sound-effect"; import state, { handleRessetState, resetPlaybackToStart, musicalInstrumentCodeInfo } 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_closeDelayCheck, api_finishDelayCheck, api_retryEvaluating } 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"; // const DelayCheck = defineAsyncComponent(() => // import('./delay-check') // ) // 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; 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) => { if (type !== 'start') { // const erji = await checkUseEarphone(); const res = await getEarphone(); const erji = res?.content?.checkIsWired || false; console.log('耳机状态111',res) evaluatingData.earphoneMode = true; evaluatingData.earPhoneType = res?.content?.type || ""; if (evaluatingData.earPhoneType === "有线耳机") { setTimeout(() => { evaluatingData.earphoneMode = false; }, 3000); } } console.log("检测结束,生成数据",evaluatingData.websocketState , evaluatingData.startBegin , evaluatingData.checkEnd); handleConnect(); } /** 生成评测曲谱数据 */ 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.isOpenMetronome ? 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") || localStorage.getItem("BEHAVIORID") || undefined; let rate = state.speed / state.originSpeed; rate = parseFloat(rate.toFixed(2)); console.log('速度比例',rate,'速度',state.speed) calculateInfo = formatTimes() 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: state.speed, heardLevel: state.setting.evaluationDifficulty, // beatLength: Math.round((state.fixtime * 1000) / rate), beatLength: actualBeatLength, evaluationCriteria: state.evaluationStandard, speedRate: rate, // 播放倍率 }; await connectWebsocket(content); // state.playSource = "music"; }; /** 评测结果按钮处理 */ const handleEvaluatResult = (type: "practise" | "tryagain" | "look" | "share" | "update" | "selfCancel") => { if (type === "update") { if (state.isAppPlay) { evaluatModel.evaluatUpdateAudio = true; resetPlaybackToStart() return; } else if (evaluatingData.resultData?.recordIdStr || evaluatingData.resultData?.recordId) { let rate = state.speed / state.originSpeed; rate = parseFloat(rate.toFixed(2)); // 上传云端 // evaluatModel.evaluatUpdateAudio = true; api_openAdjustRecording({ recordId: evaluatingData.resultData?.recordIdStr || evaluatingData.resultData?.recordId, title: state.examSongName || "曲谱演奏", coverImg: state.coverImg, speedRate: rate, // 播放倍率 musicRenderType: state.musicRenderType, musicSheetId: state.examSongId, }); 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() } else if (type === "selfCancel") { // 再来一次,需要手动取消评测,不生成评测记录,不显示评测结果弹窗 evaluatingData.oneselfCancleEvaluating = true; handleCancelEvaluat(); startBtnHandle() } resetPlaybackToStart() 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 () => { // 如果是异常状态,先等待500ms再执行后续流程 if (evaluatingData.isErrorState && !state.setting.soundEffect) { // console.log('异常流程1') showLoadingToast({ message: "处理中", duration: 1000, overlay: true, overlayClass: styles.scoreMode, }); await new Promise((resolve) => { setTimeout(() => { closeToast(); evaluatingData.isErrorState =false // console.log('异常流程2') resolve() }, 1000); }) } // console.log('异常流程3') // 检测APP端socket状态 const res: any = await startCheckDelay(); if (res?.checked) { handleConnect(); handleStartBegin(calculateInfo.firstNoteTime); 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) if (res?.content) { evaluatingData.checkEnd = true checkEarphoneStatus() } }; // 监听重复评测消息 const handRetryEvaluating = () => { handleEvaluatResult('tryagain') } onMounted(async () => { // 如果打开了延迟检测开关,需要先发送开始检测的消息 if (state.setting.soundEffect) { await api_startDelayCheck({}); } else { evaluatingData.checkEnd = true checkEarphoneStatus() } evaluatingData.isDisabledPlayMusic = true; // handlePerformDetection(); api_cancelDelayCheck(handleCancelDelayCheck); api_finishDelayCheck(handleFinishDelayCheck); api_retryEvaluating(handRetryEvaluating); }); return () => (
{evaluatingData.websocketState && !evaluatingData.startBegin && evaluatingData.checkEnd && ( { startBtnHandle() }} /> )} {evaluatingData.websocketState && evaluatingData.startBegin && ( <> handleEvaluatResult("selfCancel")} /> handleEndBegin()}/> )}
{/* {evaluatingData.soundEffectMode && ( { evaluatingData.soundEffectMode = false; handlePerformDetection(); }} onBack={() => handleDelayBack()} /> )} */} {/* 倒计时 */} {/* 遮罩 */} { evaluatingData.isBeginMask &&
} { evaluatingData.earphoneMode = false; // handlePerformDetection(); checkEarphoneStatus('start'); }} /> {/* 评测作业,非完整评测不显示评测结果弹窗 */} { evaluatingData.hideResultModal ? : } (evaluatModel.shareMode = false)} />
); }, });