Explorar o código

Merge branch 'feature-tianyong'

TIANYONG hai 1 ano
pai
achega
9f43fb5e14

+ 66 - 1
src/helpers/formateMusic.ts

@@ -591,6 +591,9 @@ export const formatXML = (xml: string): string => {
 	if (!xml) return "";
 	const xmlParse = new DOMParser().parseFromString(xml, "text/xml");
 	const measures = Array.from(xmlParse.getElementsByTagName("measure"));
+	const repeats: any = Array.from(xmlParse.querySelectorAll('repeat'))
+	// 处理重复小节信息
+	parseXmlToRepeat(repeats)
 	// const words: any = xmlParse.getElementsByTagName("words");
 	// for (const word of words) {
 	// 	if (word && word.textContent?.trim() === "筒音作5") {
@@ -976,6 +979,7 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 				relaMeasureLength,
 				id: svgElement?.attrs.id,
 				note: note.halfTone + 12, // see issue #224
+				fixtime,
 				relativeTime: retain(relativeTime),
 				time: retain(relativeTime + fixtime),
 				endtime: retain(relaEndtime + fixtime),
@@ -1033,11 +1037,72 @@ export const getNoteByMeasuresSlursStart = (note: any) => {
 		tieNote = note.noteElement.tie.StartNote;
 	}
 	if (activeNote && tieNote && tieNote !== activeNote.noteElement) {
+		const arr: any = []
 		for (const note of state.times) {
 			if (tieNote === note.noteElement) {
-				return note;
+				arr.push(note)
 			}
 		}
+		if (arr.length) {
+			return arr.find((n: any) => n.i === (note.i - 1))
+		}
 	}
 	return activeNote;
 };
+
+
+// 通过xml信息获取重播的小节信息
+const parseXmlToRepeat = (repeats: any) => {
+	if (!repeats.length) return
+	let repeatInfo: any = []
+	// 重复开始小节,结束小节
+	let start = 0, end = 0
+	for (let i = 0; i < repeats.length; i++) {
+		const element = repeats[i];
+		const direction = element.getAttribute('direction')
+		let parentElement = element.parentNode
+		while (parentElement && parentElement.tagName !== 'measure') {
+			parentElement = parentElement.parentNode
+		}
+		let notesNumber = parentElement.getAttribute('number') // 第多少小节,从1开始
+		notesNumber = notesNumber ? Number(notesNumber) : 0
+		if (direction === 'forward') {
+			start = notesNumber
+		} else if (direction === 'backward') {
+			end = notesNumber
+			repeatInfo.push({
+				start,
+				end
+			})
+		}
+	}
+	state.repeatInfo = repeatInfo
+	console.log('重播',repeatInfo)
+}
+
+// 校验当前选段是否满足重播条件
+export const verifyCanRepeat = (startNum: number, endNum: number) => {
+	let repeatIdx = -1
+	if (state.repeatInfo.length) {
+		for (let i = state.repeatInfo.length - 1; i >= 0; i--) {
+			const { start, end } = state.repeatInfo[i];
+			if (startNum <= start && endNum >= end) {
+				repeatIdx = i
+				return {
+					repeatIdx,
+					canRepeat: true
+				}
+				break;
+			}
+		}
+		return {
+			repeatIdx,
+			canRepeat: false
+		}
+	} else {
+		return {
+			repeatIdx,
+			canRepeat: false
+		}
+	}
+}

+ 11 - 5
src/page-instrument/custom-plugins/work-ealuating/index.tsx

@@ -2,7 +2,7 @@ import { defineComponent, onMounted, reactive, watch } from "vue";
 import { useRoute } from "vue-router";
 // import { verifyMembershipServices } from "../vip-verify";
 import { api_lessonTrainingSubmitTraining } from "../../api";
-import state, { IDifficulty, handleSetSpeed } from "/src/state";
+import state, { IDifficulty, handleSetSpeed, hanldeDirectSelection, setSection } from "/src/state";
 import { getQuery } from "/src/utils/queryString";
 import { evaluatingData } from "/src/view/evaluating";
 
@@ -19,6 +19,9 @@ export default defineComponent({
 		const evaluatingWorkData = reactive({
 			difficulty: "" as IDifficulty,
 			evaluatingRecord: props.workeData?.id,
+			start: "" as any,
+			end: "" as any,
+			evaluateSpeed: 0,
 		});
 		/** 隐藏评测功能 */
 		const handleHide = () => {
@@ -42,10 +45,13 @@ export default defineComponent({
 			if (["BEGINNER", "ADVANCED", "PERFORMER"].includes(trainingContent.evaluateDifficult)) {
 				evaluatingWorkData.difficulty = trainingContent.evaluateDifficult;
 				state.setting.evaluationDifficulty = trainingContent.evaluateDifficult;
-				//设置速度
-				if (trainingContent.evaluateSpeed) {
-					handleSetSpeed(trainingContent.evaluateSpeed);
-				}
+
+				evaluatingWorkData.evaluateSpeed = trainingContent.evaluateSpeed;
+				evaluatingWorkData.start = Number(trainingContent.practiceChapterBegin);
+				evaluatingWorkData.end = Number(trainingContent.practiceChapterEnd);
+				state.userChooseEndIndex = evaluatingWorkData.end
+				// 设置小节
+				setSection(evaluatingWorkData.start, evaluatingWorkData.end, evaluatingWorkData.evaluateSpeed);
 			}
 		};
 		/** 添加记录 */

+ 5 - 24
src/page-instrument/custom-plugins/work-home/index.tsx

@@ -2,7 +2,7 @@ import { defineComponent, onMounted, reactive, watch } from "vue";
 import styles from "./index.module.less";
 // import { verifyMembershipServices } from "../vip-verify";
 import { api_lessonTrainingSubmitTraining, api_lessonTrainingTrainingStudentDetail } from "../../api";
-import state, { handleSetSpeed } from "/src/state";
+import state, { handleSetSpeed, hanldeDirectSelection, setSection } from "/src/state";
 
 export default defineComponent({
 	name: "HomeWork",
@@ -51,33 +51,14 @@ export default defineComponent({
 				training.trainingSpeed = trainingContent.practiceSpeed;
 				training.start = Number(trainingContent.practiceChapterBegin);
 				training.end = Number(trainingContent.practiceChapterEnd);
+				state.userChooseEndIndex = training.end
 				if (training.isAddOk === 0) {
-					
-					setSection();
+					// 设置小节
+					setSection(training.start, training.end, training.trainingSpeed);
 				}
 			}
 		};
-		/**设置小节 */
-		const setSection = () => {
-			const startNotes = state.times.filter(
-			  (n: any) => n.noteElement.sourceMeasure.MeasureNumberXML == training.start
-			)
-			const endNotes = state.times.filter(
-			  (n: any) => n.noteElement.sourceMeasure.MeasureNumberXML == training.end
-			)
-			const startNote = startNotes[0]
-			const endNote = endNotes[endNotes.length - 1]
-			//   console.log('🚀 ~ activeNote', startNote, endNote, questionExtendsInfo.value.end)
-			if (startNote && endNote) {
-			  // 设置小节
-			  state.sectionStatus = true
-			  state.section = [startNote, endNote]
-			  //设置速度
-			  if (training.trainingSpeed) {
-				handleSetSpeed(training.trainingSpeed);
-			  }
-			}
-		  }
+
 		const getWorkDetail = async () => {
 			const res = await api_lessonTrainingTrainingStudentDetail(props.workeData.id);
 			if (res?.code === 200) {

+ 72 - 12
src/page-instrument/evaluat-model/index.tsx

@@ -24,6 +24,14 @@ import { headTopData } from "../header-top/index";
 // frequency 频率, amplitude 振幅, decibels 分贝
 type TCriteria = "frequency" | "amplitude" | "decibels";
 
+/**
+ * 节拍器时长
+ * 评测模式时,应该传节拍器时长
+ * 阶段评测时,判断是否从第一小节开始,并且曲子本身含有节拍器,需要传节拍器时长,否则传0
+ */
+let actualBeatLength = 0
+let calculateInfo: any = {}
+
 export default defineComponent({
   name: "evaluat-model",
   setup() {
@@ -100,19 +108,56 @@ export default defineComponent({
 
     /** 生成评测曲谱数据 */
     const formatTimes = () => {
+      let starTime = 0
       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];
+      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);
-        const end = difftime + (item.sourceRelaEndtime || item.relaEndtime);
+        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...")) {
@@ -127,7 +172,7 @@ export default defineComponent({
         if (note.formatLyricsEntries.contains("纯律")) {
           dontEvaluatingMode = true;
         }
-        const nextNote = state.times[index + 1];
+        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;
@@ -138,6 +183,10 @@ export default defineComponent({
         // 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,
@@ -145,23 +194,27 @@ export default defineComponent({
           nextFrequency: item.nextFrequency,
           prevFrequency: item.prevFrequency,
           // 重复的情况index会自然累加,render的index是谱面渲染的index
-          measureIndex: note.measureOpenIndex,
+          measureIndex: measureIndex,
           measureRenderIndex: item.measureListIndex,
           dontEvaluating: ListenMode || dontEvaluatingMode || item.skipMode,
-          musicalNotesIndex: item.i,
+          musicalNotesIndex: index,
           denominator: note.noteElement?.Length.denominator,
           isOrnament: !!note?.voiceEntry?.ornamentContainer,
         };
         datas.push(data);
       }
-      return datas;
+			return {
+				datas,
+				firstNoteTime
+			}
     };
     /** 连接websocket */
     const handleConnect = async () => {
       const behaviorId = localStorage.getItem("behaviorId") || undefined;
       const rate = state.speed / state.originSpeed;
+      calculateInfo = formatTimes()
       const content = {
-        musicXmlInfos: formatTimes(),
+        musicXmlInfos: calculateInfo.datas,
         subjectId: state.subjectId,
         detailId: state.detailId,
         examSongId: state.examSongId,
@@ -174,7 +227,8 @@ export default defineComponent({
         reactionTimeMs: state.setting.reactionTimeMs,
         speed: state.speed,
         heardLevel: state.setting.evaluationDifficulty,
-        beatLength: Math.round((state.fixtime * 1000) / rate),
+        // beatLength: Math.round((state.fixtime * 1000) / rate),
+        beatLength: actualBeatLength,
         evaluationCriteria: getEvaluationCriteria(),
       };
       await connectWebsocket(content);
@@ -207,7 +261,7 @@ export default defineComponent({
         handleStartEvaluat();
       } else if (type === "tryagain") {
         // 再来一次
-        handleStartBegin();
+        startBtnHandle()
       }
       evaluatingData.resulstMode = false;
     };
@@ -244,6 +298,10 @@ export default defineComponent({
       showToast("上传成功");
     };
 
+    const startBtnHandle = () => {
+      handleConnect();
+      handleStartBegin(calculateInfo.firstNoteTime);
+    }
     onMounted(() => {
       evaluatingData.isDisabledPlayMusic = true;
       handlePerformDetection();
@@ -252,7 +310,9 @@ export default defineComponent({
       <div>
         <Transition name="pop-center">
           {evaluatingData.websocketState && !evaluatingData.startBegin && evaluatingData.checkEnd && (
-            <div class={styles.startBtn} onClick={handleStartBegin}>
+            <div class={styles.startBtn} onClick={() => {
+              startBtnHandle()
+            }}>
               <img src={iconEvaluat.evaluatingStart} />
             </div>
           )}

+ 2 - 1
src/page-instrument/view-detail/index.tsx

@@ -163,7 +163,8 @@ export default defineComponent({
       state.enableNotation = data.notation ? true : false;
       // 是否是合奏,先根据background判断
       state.isConcert = data.background?.length > 1;
-
+      // 开启预备小节
+      state.isOpenPrepare = true;
       // console.log("🚀 ~ state.subjectId:", state.subjectId, state.track as any , state.subjectId)
       // 是否打击乐
       // state.isPercussion =

+ 58 - 3
src/state.ts

@@ -10,6 +10,7 @@ import { audioListStart, getAudioCurrentTime, getAudioDuration, setAudioCurrentT
 import { toggleFollow } from "./view/follow-practice";
 import { browser, setStorageSpeed } from "./utils";
 import { api_createMusicPlayer } from "./helpers/communication";
+import { verifyCanRepeat } from "./helpers/formateMusic";
 
 /** 入门 | 进阶 | 大师 */
 export type IDifficulty = "BEGINNER" | "ADVANCED" | "PERFORMER";
@@ -180,7 +181,11 @@ const state = reactive({
   /** 是否隐藏评测报告弹窗,保存演奏按钮,默认不隐藏 */
   isHideEvaluatReportSaveBtn: false,
   /** 是否是合奏 */
-  isConcert: false
+  isConcert: false,
+  /** 用户选择的结束小节数 */
+  userChooseEndIndex: 0,
+  /** 重播小节集合信息 */
+  repeatInfo: [],  
 });
 const browserInfo = browser();
 let offset_duration = 0;
@@ -254,7 +259,7 @@ const handlePlaying = () => {
   const duration = getAudioDuration();
   state.playProgress = (currentTime / duration) * 100;
   let item = getNote(currentTime);
-
+  // console.log(11111,currentTime,duration,state.playSource, item.i)
   if (item) {
     // 选段状态下
     if (state.sectionStatus && state.section.length === 2) {
@@ -374,6 +379,7 @@ const setCursorPosition = (note: any, cursor: any) => {
       if (!bbox) return;
       const baseW = state.platform === IPlatform.PC ? 29 : 18;
       const width = (bbox.width - baseW) / 3;
+      // console.log(555555,bbox.left,width)
       cursor.cursorElement.style.left = bbox.left + "px";
       cursor.cursorElement.style.transform = `translateX(${width}px)`;
     });
@@ -381,6 +387,7 @@ const setCursorPosition = (note: any, cursor: any) => {
 };
 /** 跳转到下一个音符 */
 export const gotoNext = (note: any) => {
+  // console.log(33333333333,state.activeNoteIndex,note.i)
   const num = note.i;
 
   if (state.activeNoteIndex === note.i) {
@@ -391,6 +398,7 @@ export const gotoNext = (note: any) => {
     }
     return;
   }
+  
   const osmd = state.osmd;
   let prev = state.activeNoteIndex;
   state.activeNoteIndex = num;
@@ -525,11 +533,58 @@ export const handleSelection = (item: any) => {
   }
 };
 
+/** 阶段练习、阶段评测设置选段小节 */
+export const setSection = (start: number, end: number, userSpeed?: number) => {
+  const startNotes = state.times.filter(
+    (n: any) => n.noteElement.sourceMeasure.MeasureNumberXML == start
+  )
+  const endNotes = state.times.filter(
+    (n: any) => n.noteElement.sourceMeasure.MeasureNumberXML == end
+  )
+
+  const lastEndId = endNotes[endNotes.length - 1].noteId
+  let lastEndNotes = endNotes.filter((n: any) => n.noteId === lastEndId)
+  // 是否符合重播规则
+  const canRepeatInfo = verifyCanRepeat(start, end)
+  const isCanRepeat = canRepeatInfo.canRepeat
+  // 如果符合重播规则,但是lastEndNotes长度为1,则需要向前找,直到找到lastEndNotes长度为2
+  let currentEndNum: number = end
+  const lastEndIndex = canRepeatInfo.repeatIdx
+  while (isCanRepeat && lastEndNotes.length === 1 && lastEndNotes[0].MeasureNumberXML <= lastEndIndex) {
+    currentEndNum = currentEndNum - 1
+    const newEndNotes = state.times.filter(
+      (n: any) => n.noteElement.sourceMeasure.MeasureNumberXML == currentEndNum
+    )
+    const newLastEndId = newEndNotes[newEndNotes.length - 1].noteId
+    lastEndNotes = newEndNotes.filter((n: any) => n.noteId === newLastEndId)
+  }
+
+  const endIdx: any = isCanRepeat ? lastEndNotes.length - 1 : 0
+  const startNote = startNotes[0]
+  // const endNote = endNotes[endNotes.length - 1]
+  const endNote = lastEndNotes[endIdx]			
+  
+  if (startNote && endNote) {
+    state.isSelectMeasureMode = true;
+    // 设置小节
+    hanldeDirectSelection([startNote, endNote]);
+
+    //设置速度
+    if (userSpeed) {
+      handleSetSpeed(userSpeed);
+    }
+  }
+}	
+
+
 /** 直接设置选段 */
 export const hanldeDirectSelection = (list: any[]) => {
   if (!Array.isArray(list) || list.length !== 2) return;
   state.sectionStatus = true;
-  state.section = formateSelectMearure(list);
+  setTimeout(() => {
+    state.section = formateSelectMearure(list);
+    console.log(333333333,state.section)
+  }, 500);
 };
 let offsetTop = 0;
 /**

+ 5 - 0
src/view/audio-list/index.tsx

@@ -56,8 +56,10 @@ export const getAudioCurrentTime = () => {
 		const c = getMidiCurrentTime();
 		return c;
 	}
+	// console.log('返回的时间',audioData.songEle?.currentTime,audioData.progress)
 	if (state.playSource === "music") return audioData.songEle?.currentTime || audioData.progress;
 	if (state.playSource === "background") return audioData.backgroundEle?.currentTime || audioData.progress;
+	
 	return audioData.songEle?.currentTime || audioData.progress;
 };
 /** 获取曲谱的总时间 */
@@ -72,6 +74,7 @@ export const getAudioDuration = () => {
 
 /** 设置播放的开始时间 */
 export const setAudioCurrentTime = (time: number, index = 0) => {
+	// console.log('开始时间12345',time)
 	// 如果是midi播放
 	if (audioData.midiRender) {
 		setMidiCurrentTime(index);
@@ -149,6 +152,8 @@ export default defineComponent({
 			const total = res?.totalDuration || res?.content?.totalDuration;
 			const time = currentTime / 1000;
 			audioData.progress = time;
+			audioData.songEle && (audioData.songEle.currentTime = time);
+			audioData.backgroundEle && (audioData.backgroundEle.currentTime = time);
 			audioData.duration = total / 1000;
 			if (
 				res?.content?.totalDuration > 1000 &&

+ 3 - 2
src/view/evaluating/index.tsx

@@ -248,7 +248,7 @@ const handleScoreResult = (res?: IPostMessage) => {
 };
 
 /** 开始评测 */
-export const handleStartBegin = async () => {
+export const handleStartBegin = async (preTimes?: number) => {
 	evaluatingData.isComplete = false;
 	evaluatingData.evaluatings = {};
 	evaluatingData.resultData = {};
@@ -283,7 +283,8 @@ export const handleStartBegin = async () => {
 	}
 	//开始录音
 	await api_startRecording({
-		accompanimentState: state.setting.enableAccompaniment ? 1 : 0
+		accompanimentState: state.setting.enableAccompaniment ? 1 : 0,
+		firstNoteTime: preTimes || 0,
 	});
 
 	// 如果开启了摄像头, 开启录制视频

+ 6 - 4
src/view/selection/index.tsx

@@ -137,7 +137,7 @@ const calcNoteData = () => {
 			}
 		}
 	}
-	// console.log("🚀 ~ selectData.notes:", selectData.staves);
+	console.log("🚀 ~ selectData.notes:", selectData.notes, selectData.staves);
 };
 
 /** 重新计算 */
@@ -168,24 +168,26 @@ export default defineComponent({
 						}
 					}
 					if (state.section.length === 2) {
+						// 实际的结束位置
+						const actualEndIndex = state.userChooseEndIndex
 						// 选段预备拍背景
 						if (state.sectionFirst && item.MeasureNumberXML === state.sectionFirst.MeasureNumberXML) {
 							return styles.prepareStaveBox;
 						}
 						if (
 							item.MeasureNumberXML >= state.section[0].MeasureNumberXML &&
-							item.MeasureNumberXML <= state.section[1].MeasureNumberXML
+							item.MeasureNumberXML <= actualEndIndex
 						) {
 							if (
 								item.MeasureNumberXML == state.section[0].MeasureNumberXML &&
-								item.MeasureNumberXML == state.section[1].MeasureNumberXML
+								item.MeasureNumberXML == actualEndIndex
 							) {
 								return styles.centerStaveBox;
 							}
 							if (item.MeasureNumberXML == state.section[0].MeasureNumberXML) {
 								return styles.leftStaveBox;
 							}
-							if (item.MeasureNumberXML == state.section[1].MeasureNumberXML) {
+							if (item.MeasureNumberXML == actualEndIndex) {
 								return styles.rightStaveBox;
 							}
 							return styles.staveBox;