import styles from "./index.module.less"; import { Snackbar } from "@varlet/ui"; import { closeToast, showLoadingToast, showToast, Popup } from "vant"; import { defineComponent, onMounted, onUnmounted, reactive, ref, watch } from "vue"; import { getLeveByScore, getLeveByScoreMeasure, IEvaluatings } from "./evaluatResult"; import { cancelEvaluating, endEvaluating, endSoundCheck, getEarphone, api_proxyServiceMessage, removeResult, sendResult, startEvaluating, startSoundCheck, api_openWebView, api_startRecording, api_startRecordingCb, api_stopRecording, api_recordStartTime, api_remove_recordStartTime, api_startCapture, api_endCapture, api_getDeviceDelay, hideComplexButton, api_checkSocketStatus, addAccompanyError, removeAccompanyError, addSocketStatus, removeSocketStatus, api_disconnectSocket, api_midiMicDelay, api_cloudSetCurrentTime, api_cloudChangeSpeed, api_startDelayCheck, api_closeDelayCheck, } from "/src/helpers/communication"; import state, { IPlayState, clearSelection, handleStopPlay, onPlay, resetPlaybackToStart, togglePlay, initSetPlayRate } from "/src/state"; import { IPostMessage } from "/src/utils/native-message"; import { usePageVisibility } from "@vant/use"; import { browser } from "/src/utils"; import { getAudioCurrentTime, toggleMutePlayAudio, audioListStart, audioData } from "../audio-list"; import { handleStartTick, closeTick } from "../tick"; import AbnormalPop from "../abnormal-pop"; import { storeData } from "../../store"; import icon_bg from "../abnormal-pop/icon_bg.svg"; import icon_close from "../abnormal-pop/icon_close.svg"; import icon_btn from "../abnormal-pop/icon_btn.svg"; import icon_success from "../abnormal-pop/icon_success.svg"; import { data } from "../../page-instrument/custom-plugins/work-index"; import { startCountdown } from "/src/page-instrument/evaluat-model/countdown"; const browserInfo = browser(); let socketStartTime = 0; export const popImgs = { icon_bg, icon_close, icon_btn, icon_success, }; export const evaluatingData = reactive({ /** 评测数据 */ contentData: {} as any, /** 评测模块是否加载完成 */ rendered: false, earphone: false, // 是否插入耳机 soundEffect: false, // 是否效音 soundEffectFrequency: 0, // 效音频率 checkStep: 0, // 执行步骤 checkEnd: false, // 检测结束 earphoneMode: false, // 耳机弹窗 earPhoneType: "" as "" | "有线耳机" | "蓝牙耳机", // 耳机类型 soundEffectMode: false, // 效音弹窗 websocketState: false, // websocket连接状态 /**是否开始播放 */ startBegin: false, // 开始 backtime: 0, // 延迟时间 /** 已经评测的数据 */ evaluatings: {} as IEvaluatings, /** 评测结果 */ resultData: {} as any, /** 评测结果弹窗 */ resulstMode: false, /** 是否是完整评测 */ isComplete: false, /** */ isDisabledPlayMusic: false, /** socket异常状态弹窗 */ socketErrorPop: false, /** 异常提示 */ errorContents: '', /** socket异常状态弹窗的状态值 */ socketErrorStatus: 0, /** 延迟检测,socket状态异常 */ delayCheckSocketError: false, /** 异常状态,不生成评测记录,不调用保存接口 */ isErrorState: false, /** accompanyError,错误类型 */ accompanyErrorType: '', /** app播放结束状态,重新评测需要重置为 */ isAudioPlayEnd: false, preloadJson: true, // 预加载延迟检测的资源 jsonLoading: true, // 延迟检测的资源加载中状态 jsonLoadDone: true, // 延迟检测的动画dom加载完成状态 hideResultModal: false, // 评测作业,如果不是完整评测,需要隐藏评测结果弹窗 oneselfCancleEvaluating: false, // 是否是自主取消评测,自主取消评测,不生产评测记录 isBeginMask: false // 倒计时和系统节拍器时候的遮罩,防止用户点击 }); const sendOffsetTime = async (offsetTime: number) => { const delayData = await api_getDeviceDelay(); api_midiMicDelay({ header: { commond: "audioPlayStart", type: "SOUND_COMPARE", }, body: { offsetTime, micDelay: delayData?.content?.value, }, }); }; /** 点击开始评测按钮 */ export const handleStartEvaluat = async () => { if (state.modeType === "evaluating") { handleCancelEvaluat(); // 放下面会在异步之后执行 旋律线可能在会隐藏不了 state.modeType = "practise"; } else { // 放下面会在异步之后执行 旋律线可能在会隐藏不了 state.modeType = "evaluating"; if (state.platform !== "PC") { // 评测前先检查APP端的websocket状态 const res = await api_checkSocketStatus(); if (res?.content?.status === "connected") { handleStopPlay(); } else { // socket未连接 // evaluatingData.socketErrorPop = true } } else { handleStopPlay(); } } //state.modeType = state.modeType === "evaluating" ? "practise" : "evaluating"; if (state.modeType !== "evaluating") { // 切换到练习模式,卸载评测模块 evaluatingData.rendered = false; } }; /** 开始评测 & 延迟检测开始按钮 */ export const startCheckDelay = async () => { // 评测前先检查APP端的websocket状态 const res = await api_checkSocketStatus(); if (res?.content?.status === "connected") { // return new Promise((resolve) => { resolve({ checked: true }); }); } else { /** * socket未连接,记录此时的时间,以便于和收到socket成功链接,进行对比,对比时间小于500ms时,则连接中的状态默认显示500ms持续时间 * * */ socketStartTime = +new Date(); evaluatingData.socketErrorPop = true; evaluatingData.socketErrorStatus = 1; return new Promise((resolve) => { resolve({ checked: false }); }); } }; const check_currentTime = () => { let preTime = 0; // 选段评测模式 if (state.isSelectMeasureMode) { preTime = state.section[0].time * 1000; } const currentTime = getAudioCurrentTime() * 1000 - preTime; // console.log('播放进度music', currentTime, 'preTime:' + preTime) if (currentTime >= 500) { sendEvaluatingOffsetTime(500); return; } setTimeout(() => { check_currentTime(); }, 10); }; /** 开始播放发送延迟时间 */ export const sendEvaluatingOffsetTime = async (currentTime: number) => { // 没有开始时间点, 不处理 if (!evaluatingData.backtime) return; const nowTime = Date.now(); const delayTime = nowTime - evaluatingData.backtime - currentTime; console.error("真正播放延迟", delayTime, "currentTime:", currentTime); await api_proxyServiceMessage({ header: { commond: "audioPlayStart", type: "SOUND_COMPARE", }, body: { offsetTime: delayTime < 0 ? 0 : delayTime, micDelay: 0, }, }); }; /** 检测耳机 */ export const checkUseEarphone = async () => { const res = await getEarphone(); return res?.content?.checkIsWired || false; }; /** * 开始录音 */ const handleStartSoundCheck = () => { startSoundCheck(); }; /** 结束录音 */ export const handleEndSoundCheck = () => { endSoundCheck(); }; /** 连接websocket */ export const connectWebsocket = async (content: any) => { evaluatingData.contentData = content; evaluatingData.websocketState = true; }; /** * 执行检测 */ export const handlePerformDetection = async () => { // 检测完成不检测了 if (evaluatingData.checkEnd) return; // 延迟检测 if (evaluatingData.checkStep === 0) { evaluatingData.checkStep = 5; // 没有设备延迟数据 或 开启了效音 显示检测组件,并持续检测耳机状态 if (state.setting.soundEffect) { evaluatingData.soundEffectMode = true; return; } const delayTime = await api_getDeviceDelay(); console.log("🚀 ~ delayTime:", delayTime); if (!delayTime) { evaluatingData.soundEffectMode = true; return; } handlePerformDetection(); return; } // 检测耳机 if ((evaluatingData.checkStep = 5)) { evaluatingData.checkStep = 10; const erji = await checkUseEarphone(); if (!erji) { evaluatingData.earphoneMode = true; return; } handlePerformDetection(); return; } // 效音 // if (evaluatingData.checkStep === 7) { // // 是否需要开启效音 // evaluatingData.checkStep = 10; // if (state.setting.soundEffect && !state.isPercussion) { // evaluatingData.soundEffectMode = true; // handleStartSoundCheck(); // return // } // handlePerformDetection(); // return; // } // 效验完成 if (evaluatingData.checkStep === 10) { evaluatingData.checkEnd = true; } }; /** 记录小节分数 */ export const addMeasureScore = (measureScore: any, show = true) => { // #8720 bug修复 for (let idx in evaluatingData.evaluatings) { evaluatingData.evaluatings[idx].show = false; } evaluatingData.evaluatings[measureScore.measureRenderIndex] = { ...measureScore, leve: getLeveByScoreMeasure(measureScore.score), show, }; // console.log("🚀 ~ measureScore:", evaluatingData.evaluatings) }; const handleScoreResult = (res?: IPostMessage) => { console.log("返回", res, evaluatingData.oneselfCancleEvaluating); // 如果是手动取消评测,不生成评测记录 if (evaluatingData.oneselfCancleEvaluating) { return; } if (res?.content) { const { header, body } = res.content; // 效音返回 if (header.commond === "checking") { evaluatingData.soundEffectFrequency = body.frequency; } // 小节评分返回 if (header?.commond === "measureScore") { console.log("🚀 ~ 评测返回:", res); addMeasureScore(body); } // 评测结束返回 if (header?.commond === "overall") { console.log("🚀 ~ 评测返回:", res); console.log("评测结束", body); state.isHideEvaluatReportSaveBtn = false; setTimeout(() => { // 评测作业,如果不是完整评测,不展示评测弹窗 if (data.trainingType === "EVALUATION" && !evaluatingData.isComplete) { evaluatingData.hideResultModal = true; } else { evaluatingData.hideResultModal = false; } evaluatingData.resulstMode = evaluatingData.isErrorState ? false : true; }, 200); evaluatingData.resultData = { ...body, ...getLeveByScore(body.score), }; // console.log("🚀 ~ evaluatingData.resultData:", evaluatingData.resultData) closeToast(); } } }; /** 开始评测 */ export const handleStartBegin = async (preTimes?: number) => { if (state.isAppPlay) { await api_cloudSetCurrentTime({ currentTime: 0, songID: state.examSongId, }) } evaluatingData.isComplete = false; evaluatingData.evaluatings = {}; evaluatingData.resultData = {}; evaluatingData.backtime = 0; evaluatingData.isAudioPlayEnd = false; const res = await startEvaluating(evaluatingData.contentData); if (res?.api !== "startEvaluating") { Snackbar.error("请在APP端进行评测"); evaluatingData.startBegin = false; return; } if (res?.content?.reson) { showToast(res.content?.des); evaluatingData.startBegin = false; return; } initSetPlayRate(); resetPlaybackToStart(); evaluatingData.startBegin = true; if (evaluatingData.isDisabledPlayMusic) { evaluatingData.isBeginMask = true // 先播放倒计时 await startCountdown() state.playState = state.playState === "paused" ? "play" : "paused"; // 设置为开始播放时, 如果需要节拍,先播放节拍器 if (state.playState === "play" && (state.playType==="play"&&state.needTick)||(state.playType==="sing"&&state.needSingTick)) { // 如果是系统节拍器 等系统节拍器播完了再播,如果是mp3节拍器 直接播 if((state.playType==="play" && !state.isOpenMetronome)||(state.playType==="sing" && !state.isSingOpenMetronome)){ const tickend = await handleStartTick(); console.log("🚀 ~ tickend:", tickend) // 节拍器返回false, 取消播放 if (!tickend) { state.playState = "paused"; evaluatingData.startBegin = false; evaluatingData.isBeginMask = false return; } }else{ handleStartTick() } } evaluatingData.isBeginMask = false onPlay(); } if (evaluatingData.isErrorState) return //开始录音 // await api_startRecording({ // accompanimentState: state.setting.enableAccompaniment ? 1 : 0, // firstNoteTime: preTimes || 0, // }); let rate = state.speed / state.originSpeed; rate = parseFloat(rate.toFixed(2)); await api_startRecordingCb({ // accompanimentState: state.setting.enableAccompaniment ? 1 : 0, accompanimentState: !state.accompany ? 0 : 1, // 评测没有伴奏时,静音播放 firstNoteTime: preTimes || 0, speedRate: rate, // 播放倍率 }, () => { if (state.isAppPlay) { setTimeout(() => { sendOffsetTime(0) }, 300); } }) // 如果开启了摄像头, 开启录制视频 if (state.setting.camera) { console.log("开始录制视频"); await api_startCapture(); } // 如果是midi音频评测,需要调用cloudPlay if (state.isAppPlay) { await api_cloudChangeSpeed({ speed: state.originSpeed, originalSpeed: state.originSpeed, songID: state.examSongId, }); audioData.progress = 0 audioListStart(state.playState); } evaluatingData.oneselfCancleEvaluating = false; }; /** 播放音乐 */ const playMusic = async () => { const playState = await togglePlay("play"); // 取消播放,停止播放 if (!playState) { evaluatingData.startBegin = false; handleCancelEvaluat(); return; } // 检测播放进度, 计算延迟 check_currentTime(); // 如果开启了摄像头, 开启录制视频 if (state.setting.camera) { console.log("开始录制视频"); api_startCapture(); } }; let _audio: HTMLAudioElement; /** 录音开始,记录开始时间点 */ const recordStartTimePoint = async (res?: IPostMessage) => { console.error("开始录音"); // 没有开始评测,不处理 if (!evaluatingData.startBegin) return; let inteveral = res?.content?.inteveral || 0; if (browserInfo.ios) { inteveral *= 1000; } evaluatingData.backtime = inteveral || Date.now(); console.log("🚀 ~ 开始时间点:", evaluatingData.backtime, "已经录的时间:", Date.now() - inteveral, "记录时间点:", Date.now()); // 是否禁播 if (evaluatingData.isDisabledPlayMusic) { return; } // 开始播放 playMusic(); }; /** * 结束评测 * @param isComplete 是否完整评测 * @returns */ export const handleEndEvaluat = (isComplete = false) => { // 没有开始评测 , 不是评测模式 , 不评分 if (!evaluatingData.startBegin || state.modeType !== "evaluating") return; // 结束录音 // api_stopRecording(); // 结束评测 console.log("评测结束1"); endEvaluating({ musicScoreId: state.examSongId, }); // 评测作业如果不是完整评测,给出提示 if (!isComplete && data.trainingType === "EVALUATION") { showToast({ message: "完整演奏结束才算作业分数!", }); } else { showLoadingToast({ message: "评分中", duration: 0, overlay: true, overlayClass: styles.scoreMode, }); } setTimeout(() => { evaluatingData.startBegin = false; }, 500); evaluatingData.isComplete = isComplete; // 如果开启了摄像头, 结束录制视频 if (state.setting.camera) { console.log("结束录制视频"); api_endCapture(); } }; /** * 结束评测(手动结束评测) */ export const handleEndBegin = () => { handleEndEvaluat(); handleStopPlay(); }; /** * 取消评测 */ export const handleCancelEvaluat = (cancelType?: string) => { evaluatingData.evaluatings = {}; evaluatingData.startBegin = false; // 关闭提示 closeToast(); // 取消记录 api_proxyServiceMessage({ header: { commond: "recordCancel", type: "SOUND_COMPARE", status: 200, }, }); /** * 异常状态是取消评测(cancelEvaluating),正常结束时结束评测(endEvaluating) */ // if (cancelType === "cancel") { // // 取消评测 // cancelEvaluating(); // } else { // endEvaluating({ // musicScoreId: state.examSongId, // }); // } cancelEvaluating(); // 停止播放 handleStopPlay(); console.log("评测结束2"); endEvaluating({ musicScoreId: state.examSongId, }); // 如果开启了摄像头, 结束录制视频 if (state.setting.camera) { console.log("结束录制视频"); api_endCapture(); } }; /** 查看报告 */ export const handleViewReport = (key: "recordId" | "recordIdStr", type: "gym" | "colexiu" | "orchestra" | "instrument") => { const id = evaluatingData.resultData?.[key] || ""; let url = ""; switch (type) { case "gym": url = location.origin + location.pathname + "#/report/" + id; break; case "orchestra": url = location.origin + location.pathname + "report-share.html?id=" + id; break; case "instrument": url = location.origin + location.pathname + "#/evaluat-report?id=" + id + "&musicRenderType=" + state.musicRenderType; break; default: url = location.origin + location.pathname + "report-share.html?id=" + id; break; } api_openWebView({ url, orientation: 0, isHideTitle: true, // 此处兼容安卓,意思为隐藏全部头部 statusBarTextColor: false, isOpenLight: true, c_orientation: 0, }); }; // 隐藏存演奏按钮 const handleComplexButton = (res?: IPostMessage) => { console.log("监听是否隐藏保存按钮", res); if (res?.content) { const { header, body } = res.content; state.isHideEvaluatReportSaveBtn = true; } }; // 检测到APP发送的异常信息 const handleAccompanyError = (res?: IPostMessage) => { console.log("异常信息返回", res); if (res?.content) { const { type, reson } = res.content; switch (type) { case "enterBackground": // App退到后台 case "playError": // 播放异常 case "socketError": // socket连接断开,评测中,则取消评测 // 延迟检测中 if (evaluatingData.soundEffectMode) { evaluatingData.socketErrorStatus = 0; evaluatingData.delayCheckSocketError = true; evaluatingData.socketErrorPop = type === "socketError" ? true : false; evaluatingData.accompanyErrorType = type; // api_checkSocketStatus() return; } // 评测中 if (state.modeType === "evaluating" && evaluatingData.startBegin) { handleCancelEvaluat("cancel"); } // 关闭节拍器 closeTick(); // socketerrror,才发送关闭延迟检测的消息 if (type === "socketError") { api_closeDelayCheck({}); } evaluatingData.socketErrorStatus = 0; evaluatingData.socketErrorPop = type === "socketError" ? true : false; evaluatingData.isErrorState = true; evaluatingData.accompanyErrorType = type; resetPlaybackToStart(); break; case "recordError": // 录音异常 break; default: break; } } }; // 监测socket状态,是否已经成功连接 const handleSocketStatus = (res?: IPostMessage) => { if (res?.content?.status === "connected") { const currentTime = +new Date(); evaluatingData.delayCheckSocketError = false; const diffTime = currentTime - socketStartTime; if (diffTime < 1000) { const remainingTime = 1000 - diffTime; setTimeout(() => { evaluatingData.socketErrorStatus = 2; }, remainingTime); } } }; // 评测出现异常,再试一次 export const hanldeConfirmPop = async () => { api_checkSocketStatus(); evaluatingData.socketErrorStatus = 1; socketStartTime = +new Date(); }; // 关闭异常弹窗 export const hanldeClosePop = () => { evaluatingData.socketErrorPop = false; evaluatingData.socketErrorStatus = 0; }; export default defineComponent({ name: "evaluating", setup() { const pageVisibility = usePageVisibility(); // 需要记录的数据 const record_old_data = reactive({ /** 指法 */ finger: false, /** 原音伴奏 */ play_mode: "" as IPlayState, /** 评测是否要伴奏 */ enableAccompaniment: true, }); /** 记录状态 */ const hanlde_record = () => { // 取消指法 record_old_data.finger = state.setting.displayFingering; state.setting.displayFingering = false; // 切换为伴奏 record_old_data.play_mode = state.playSource; record_old_data.enableAccompaniment = state.setting.enableAccompaniment; // 如果关闭伴奏,评测静音 if (!record_old_data.enableAccompaniment) { console.log("关闭伴奏"); toggleMutePlayAudio(record_old_data.play_mode === "music" ? "music" : record_old_data.play_mode === "background" ? "background" : "mingSong", true); } }; /** 还原状态 */ const handle_reduction = () => { // 还原指法 state.setting.displayFingering = record_old_data.finger; state.playSource = record_old_data.play_mode; // 如果关闭伴奏, 结束评测取消静音 if (!record_old_data.enableAccompaniment) { toggleMutePlayAudio(record_old_data.play_mode === "music" ? "music" : record_old_data.play_mode === "background" ? "background" : "mingSong", false); } }; watch(pageVisibility, (value) => { if (value == "hidden" && evaluatingData.startBegin) { // handleEndBegin(); } }); watch( () => evaluatingData.socketErrorStatus, () => { if (evaluatingData.socketErrorStatus === 2) { setTimeout(() => { evaluatingData.socketErrorPop = false; // evaluatingData.socketErrorStatus = 0 }, 1000); } } ); watch( () => evaluatingData.socketErrorPop, () => { if (evaluatingData.socketErrorPop && state.setting.soundEffect) { // 监听到socket状态异常,需要关闭延迟检测 api_closeDelayCheck({}); } } ); onMounted(() => { resetPlaybackToStart(); hanlde_record(); evaluatingData.resultData = {}; // evaluatingData.resulstMode = true; // evaluatingData.resultData = {...getLeveByScore(10), score: 10, intonation: 10, cadence: 30, integrity: 40} // console.log("🚀 ~ evaluatingData.resultData:", evaluatingData.resultData) evaluatingData.evaluatings = {}; evaluatingData.soundEffectFrequency = 0; evaluatingData.checkStep = 0; evaluatingData.rendered = true; sendResult(handleScoreResult); hideComplexButton(handleComplexButton, true); api_recordStartTime(recordStartTimePoint); addAccompanyError(handleAccompanyError); addSocketStatus(handleSocketStatus); // 不是选段模式评测, 就清空已选段 if (!state.isSelectMeasureMode) { clearSelection(); } console.log("加载评测模块成功"); }); onUnmounted(() => { evaluatingData.checkEnd = false; evaluatingData.rendered = false; resetPlaybackToStart(); removeResult(handleScoreResult); hideComplexButton(() => {}, false); api_remove_recordStartTime(recordStartTimePoint); handle_reduction(); removeAccompanyError(handleAccompanyError); if (evaluatingData.socketErrorPop && state.setting.soundEffect) { console.log('延迟检测出错') } else { removeSocketStatus(handleSocketStatus); } api_disconnectSocket(); console.log("卸载评测模块成功"); }); return () => (