liushengqiang 1 year ago
parent
commit
2e61851432
45 changed files with 2152 additions and 1554 deletions
  1. 1 1
      osmd-extended
  2. 52 0
      package-lock.json
  3. 5 0
      package.json
  4. 0 0
      src/constant/audios.ts
  5. 1 1
      src/helpers/calcSpeed.ts
  6. 453 501
      src/helpers/formateMusic.ts
  7. 385 0
      src/helpers/metronome.ts
  8. 382 0
      src/helpers/multiple-audio.ts
  9. 0 948
      src/helpers/runtime.ts
  10. 36 0
      src/page-gym/detail/index.module.less
  11. 48 3
      src/page-gym/detail/index.tsx
  12. 9 0
      src/page-gym/header-top/image/arrow.svg
  13. 11 0
      src/page-gym/header-top/image/icon-back.svg
  14. 14 0
      src/page-gym/header-top/image/icon-background.svg
  15. 14 0
      src/page-gym/header-top/image/icon-evaluating.svg
  16. 13 0
      src/page-gym/header-top/image/icon-music.svg
  17. 12 0
      src/page-gym/header-top/image/icon-pause.svg
  18. 12 0
      src/page-gym/header-top/image/icon-play.svg
  19. BIN
      src/page-gym/header-top/image/iconStep.png
  20. 5 0
      src/page-gym/header-top/image/index.ts
  21. 14 0
      src/page-gym/header-top/image/menu.svg
  22. BIN
      src/page-gym/header-top/image/minus.png
  23. BIN
      src/page-gym/header-top/image/music.png
  24. BIN
      src/page-gym/header-top/image/plus.png
  25. 15 0
      src/page-gym/header-top/image/replay.svg
  26. 15 0
      src/page-gym/header-top/image/section0.svg
  27. 15 0
      src/page-gym/header-top/image/section1.svg
  28. 15 0
      src/page-gym/header-top/image/section2.svg
  29. 21 0
      src/page-gym/header-top/image/speed.svg
  30. BIN
      src/page-gym/header-top/image/tickoff.png
  31. BIN
      src/page-gym/header-top/image/tickon.png
  32. 70 0
      src/page-gym/header-top/index.module.less
  33. 134 0
      src/page-gym/header-top/index.tsx
  34. 35 0
      src/page-gym/header-top/speed/index.module.less
  35. 86 0
      src/page-gym/header-top/speed/index.tsx
  36. 25 0
      src/page-gym/header-top/title/index.module.less
  37. 39 0
      src/page-gym/header-top/title/index.tsx
  38. 74 61
      src/state.ts
  39. 21 0
      src/type.ts
  40. 8 0
      src/utils/index.ts
  41. 7 0
      src/view/audio-list/index.module.less
  42. 32 0
      src/view/audio-list/index.tsx
  43. 0 14
      src/view/music-score/index.module.less
  44. 63 24
      src/view/music-score/index.tsx
  45. 10 1
      tsconfig.json

+ 1 - 1
osmd-extended

@@ -1 +1 @@
-Subproject commit 73105d788fbdc1d6586ab0e999158c06e19fbb25
+Subproject commit 5924dd1d9de7ff3e0a646fafb1e8a7da6ac8434e

+ 52 - 0
package-lock.json

@@ -11,14 +11,19 @@
         "clean-deep": "^3.4.0",
         "consola": "^2.15.3",
         "dayjs": "^1.11.7",
+        "lodash": "^4.17.21",
         "query-string": "^8.1.0",
+        "store": "^2.0.12",
         "umi-request": "^1.4.0",
         "vant": "^4.1.2",
         "vue": "^3.2.47",
         "vue-router": "^4.1.6"
       },
       "devDependencies": {
+        "@types/lodash": "^4.14.192",
         "@types/node": "^18.15.11",
+        "@types/store": "^2.0.2",
+        "@vant/use": "^1.5.1",
         "@vitejs/plugin-legacy": "^4.0.2",
         "@vitejs/plugin-vue": "^4.1.0",
         "@vitejs/plugin-vue-jsx": "^3.0.1",
@@ -2181,12 +2186,24 @@
       "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==",
       "dev": true
     },
+    "node_modules/@types/lodash": {
+      "version": "4.14.192",
+      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.192.tgz",
+      "integrity": "sha512-km+Vyn3BYm5ytMO13k9KTp27O75rbQ0NFw+U//g+PX7VZyjCioXaRFisqSIJRECljcTv73G3i6BpglNGHgUQ5A==",
+      "dev": true
+    },
     "node_modules/@types/node": {
       "version": "18.15.11",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
       "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==",
       "dev": true
     },
+    "node_modules/@types/store": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@types/store/-/store-2.0.2.tgz",
+      "integrity": "sha512-ZPHnXkzmGMfk+pHqAGzTSpA9CbsHmJLgkvOl5w52LZ0XTxB1ZIHWZzQ7lEtjTNWScBbsQekg8TjApMXkMe4nkw==",
+      "dev": true
+    },
     "node_modules/@vant/popperjs": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/@vant/popperjs/-/popperjs-1.3.0.tgz",
@@ -3345,6 +3362,11 @@
         "url": "https://github.com/sponsors/antfu"
       }
     },
+    "node_modules/lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+    },
     "node_modules/lodash.debounce": {
       "version": "4.0.8",
       "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -3979,6 +4001,14 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/store": {
+      "version": "2.0.12",
+      "resolved": "https://registry.npmjs.org/store/-/store-2.0.12.tgz",
+      "integrity": "sha512-eO9xlzDpXLiMr9W1nQ3Nfp9EzZieIQc10zPPMP5jsVV7bLOziSFFBP0XoDXACEIFtdI+rIz0NwWVA/QVJ8zJtw==",
+      "engines": {
+        "node": "*"
+      }
+    },
     "node_modules/supports-color": {
       "version": "5.5.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -5835,12 +5865,24 @@
       "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==",
       "dev": true
     },
+    "@types/lodash": {
+      "version": "4.14.192",
+      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.192.tgz",
+      "integrity": "sha512-km+Vyn3BYm5ytMO13k9KTp27O75rbQ0NFw+U//g+PX7VZyjCioXaRFisqSIJRECljcTv73G3i6BpglNGHgUQ5A==",
+      "dev": true
+    },
     "@types/node": {
       "version": "18.15.11",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
       "integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==",
       "dev": true
     },
+    "@types/store": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@types/store/-/store-2.0.2.tgz",
+      "integrity": "sha512-ZPHnXkzmGMfk+pHqAGzTSpA9CbsHmJLgkvOl5w52LZ0XTxB1ZIHWZzQ7lEtjTNWScBbsQekg8TjApMXkMe4nkw==",
+      "dev": true
+    },
     "@vant/popperjs": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/@vant/popperjs/-/popperjs-1.3.0.tgz",
@@ -6738,6 +6780,11 @@
       "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==",
       "dev": true
     },
+    "lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+    },
     "lodash.debounce": {
       "version": "4.0.8",
       "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -7212,6 +7259,11 @@
       "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz",
       "integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA=="
     },
+    "store": {
+      "version": "2.0.12",
+      "resolved": "https://registry.npmjs.org/store/-/store-2.0.12.tgz",
+      "integrity": "sha512-eO9xlzDpXLiMr9W1nQ3Nfp9EzZieIQc10zPPMP5jsVV7bLOziSFFBP0XoDXACEIFtdI+rIz0NwWVA/QVJ8zJtw=="
+    },
     "supports-color": {
       "version": "5.5.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",

+ 5 - 0
package.json

@@ -12,14 +12,19 @@
     "clean-deep": "^3.4.0",
     "consola": "^2.15.3",
     "dayjs": "^1.11.7",
+    "lodash": "^4.17.21",
     "query-string": "^8.1.0",
+    "store": "^2.0.12",
     "umi-request": "^1.4.0",
     "vant": "^4.1.2",
     "vue": "^3.2.47",
     "vue-router": "^4.1.6"
   },
   "devDependencies": {
+    "@types/lodash": "^4.14.192",
     "@types/node": "^18.15.11",
+    "@types/store": "^2.0.2",
+    "@vant/use": "^1.5.1",
     "@vitejs/plugin-legacy": "^4.0.2",
     "@vitejs/plugin-vue": "^4.1.0",
     "@vitejs/plugin-vue-jsx": "^3.0.1",

File diff suppressed because it is too large
+ 0 - 0
src/constant/audios.ts


+ 1 - 1
src/helpers/calcSpeed.ts

@@ -1,4 +1,4 @@
-import { OpenSheetMusicDisplay, SourceMeasure } from '/osmd-extended/src'
+import { OpenSheetMusicDisplay, SourceMeasure } from '../../accompany/osmd-extended/src'
 
 export const noteDuration = {
   '1/2': 2,

+ 453 - 501
src/helpers/formateMusic.ts

@@ -1,16 +1,32 @@
 import dayjs from "dayjs";
 import duration from "dayjs/plugin/duration";
-import state, { GradualVersion } from "../state";
+import state from "../state";
 // import appState from "/src/state";
 import { browser } from "../utils/index";
-import runtime, { getFixTime } from "./runtime";
-// @ts-ignore
 import { isSpecialMark, isSpeedKeyword, Fraction, SourceMeasure, isGradientWords, GRADIENT_SPEED_RESET_TAG, StringUtil, OpenSheetMusicDisplay } from "/osmd-extended/src";
 import { GradualChange, speedInfo } from "./calcSpeed";
 import { useRoute } from "vue-router";
 const browserInfo = browser();
 dayjs.extend(duration);
 
+/**
+ * 获取节拍器的时间
+ * @param speed 速度
+ * @param firstMeasure 曲谱第一个小节
+ * @returns 节拍器的时间
+ */
+export const getFixTime = (speed: number) => {
+	const duration: any = getDuration(state.osmd as unknown as OpenSheetMusicDisplay);
+	let numerator = duration.numerator || 0;
+	let denominator = duration.denominator || 4;
+	const beatUnit = duration.beatUnit || "quarter";
+	// if (state.repeatedBeats) {
+	// 	// 音频制作问题仅2拍不重复
+	// 	numerator = numerator === 2 ? 4 : numerator;
+	// }
+	return !state.needTick && !state.skipTick ? (60 / speed) * formatBeatUnit(beatUnit) * (numerator / denominator) : 0;
+};
+
 const getLinkId = (): string => {
 	return location.hash.split("?")[0].split("/").pop() || "";
 };
@@ -83,437 +99,6 @@ const tranTime = (str: string = "") => {
 	return `1970-01-01 00:${result}0`;
 };
 
-export const getAllNodes = (osmd: any) => {
-	const route = useRoute();
-	const detailId = getLinkId();
-	let fixtime = browserInfo.huawei ? 0.08 : 0; //getFixTime()
-	const allNotes: any[] = [];
-	const allNoteId: string[] = [];
-	const allMeasures: any[] = [];
-	const { baseSpeed = 90 } = state;
-	const formatRealKey = (realKey: number, detail: any) => {
-		// 长笛的LEVEL 2-5-1条练习是泛音练习,以每小节第一个音的指法为准,高音不变变指法。
-		const olnyOneIds = ["906"];
-		if (olnyOneIds.includes(detailId)) {
-			return detail.measures[0]?.realKey || realKey;
-		}
-		// 圆号的LEVEL 2-5条练习是泛音练习,最后四小节指法以连音线第一个小节为准
-		const olnyOneIds2 = ["782", "784"];
-		if (olnyOneIds2.includes(detailId)) {
-			const measureNumbers = [14, 16, 30, 32];
-			if (measureNumbers.includes(detail.firstVerticalMeasure?.measureNumber)) {
-				return allNotes[allNotes.length - 1]?.realKey || realKey;
-			}
-		}
-		// 2-6 第三小节指法按照第一个音符显示
-		const filterIds = ["900", "901", "640", "641", "739", "740", "800", "801", "773", "774", "869", "872", "714", "715"];
-		if (filterIds.includes(detailId)) {
-			if (detail.firstVerticalMeasure?.measureNumber === 3 || detail.firstVerticalMeasure?.measureNumber === 9) {
-				return detail.measures[0]?.realKey || realKey;
-			}
-		}
-		return realKey;
-	};
-	if (osmd?.cursor) {
-		try {
-			osmd.cursor.reset();
-		} catch (error) {}
-		const iterator = osmd.cursor.iterator;
-		let i = 0;
-		let si = 0;
-		let measures: any[] = [];
-		let stepSpeeds: number[] = [];
-		/** 弱起时间 */
-		let difftime = 0;
-		let usetime = 0;
-		let useGradualTime = 0;
-		let relaMeasureLength = 0;
-		let beatUnit = "quarter";
-		let gradualSpeed;
-		let gradualChange: GradualChange | undefined;
-		let gradualChangeIndex = 0;
-		const _notes = [] as any[];
-
-		if (state.gradualTimes) {
-			if (["12280"].includes(detailId) && route.query["part-index"] === "24") {
-				state.gradualTimes["8"] = "00:25:63";
-				state.gradualTimes["66"] = "01:53:35";
-				state.gradualTimes["90"] = "02:41:40";
-			}
-			console.log("合奏速度", state.gradual, state.gradualTimes);
-		}
-		 
-		let currentTimeStamp = iterator.currentTimeStamp.realValue
-		const currentTimes = [] as any[]
-		let isSetNextNoteReal = false
-		let differFrom = 0
-		while (!iterator.endReached) {
-			const voiceEntries = iterator.currentVoiceEntries?.[0] ? [iterator.currentVoiceEntries?.[0]] : [];
-			let currentVoiceEntries: any[] = [];
-			// 单声部多声轨
-			if (state.multitrack > 0) {
-				currentVoiceEntries = [...iterator.currentVoiceEntries];
-			} else {
-				currentVoiceEntries = [...iterator.currentVoiceEntries].filter((n) => {
-					return n && n?.ParentVoice?.voiceId != 1;
-				});
-			}
-			let currentTime = 0;
-			let isDouble = false;
-			let isMutileSubject = false
-			
-			
-			// console.log(iterator.currentMeasureIndex, [...iterator.currentVoiceEntries])
-			// iterator.currentMeasureIndex == 45 && console.log(currentTimeStamp, [...iterator.currentVoiceEntries])
-			if (currentVoiceEntries.length && !isSetNextNoteReal) {
-				// iterator.currentMeasureIndex == 15 &&  console.log(iterator.currentMeasureIndex, isSetNextNoteReal)
-				isDouble = true;
-				let voiceNotes = [...iterator.currentVoiceEntries].reduce((notes, n) => {
-					notes.push(...n.notes);
-					return notes;
-				}, []);
-				voiceNotes = voiceNotes.sort((a: any, b: any) => a?.length?.realValue - b?.length?.realValue);
-				currentTime = voiceNotes?.[0]?.length?.realValue || 0;
-				// iterator.currentMeasureIndex == 15 && console.log("🚀 ~ currentTime:", currentTime)
-
-				if (state.multitrack > 0 && currentVoiceEntries.length === 2) {
-					const min = voiceNotes[0]?.length?.realValue || 0
-					const max = voiceNotes[voiceNotes.length - 1]?.length?.realValue || 0
-					differFrom = max - min
-					isSetNextNoteReal = differFrom === 0 ? false : true
-					// iterator.currentMeasureIndex == 15 &&  console.log("🚀 ~ differFrom:", differFrom, isSetNextNoteReal)
-					// console.log(iterator.currentMeasureIndex, min, max)
-				}
-			}
-			// 多声部上下音符没对齐,光标多走一拍
-			if (_notes[_notes.length - 1]?.isDouble && !currentVoiceEntries.length) {
-				isMutileSubject = true
-			}
-			if (state.multitrack > 0 && !isDouble && isSetNextNoteReal) {
-				isDouble = true;
-				currentTime = differFrom
-				isSetNextNoteReal = false
-				differFrom = 0
-				// iterator.currentMeasureIndex == 7 && console.log("🚀 ~ currentTime:",iterator.currentMeasure, currentTime)
-			}
-			currentTimes.push(iterator.currentTimeStamp.realValue - currentTimeStamp)
-			currentTimeStamp = iterator.currentTimeStamp.realValue
-			for (const v of voiceEntries) {
-				const note = v.notes[0];
-				note.fixedKey = note.ParentVoiceEntry.ParentVoice.Parent.SubInstruments[0].fixedKey || 0;
-				// 有倚音
-				if (note?.voiceEntry?.isGrace) {
-					isDouble = true;
-					let ns = [...iterator.currentVoiceEntries].reduce((notes, n) => {
-						notes.push(...n.notes);
-						return notes;
-					}, []);
-					ns = ns.sort((a: any, b: any) => b?.length?.realValue - a?.length?.realValue);
-					currentTime = currentTime != 0 ? Math.min(ns?.[0]?.length?.realValue, currentTime) : ns?.[0]?.length?.realValue;
-				}
-				if (state.multitrack > 0 && currentTime > note.length.realValue) {
-					// console.log(iterator.currentMeasureIndex, currentTime , note.length.realValue)
-					currentTime = note.length.realValue
-
-				}
-				_notes.push({
-					note,
-					iterator: { ...iterator },
-					currentTime,
-					isDouble,
-					isMutileSubject,
-				});
-			}
-
-			iterator.moveToNextVisibleVoiceEntry(false);
-		}
-		for (let { note, iterator, currentTime, isDouble,isMutileSubject } of _notes) {
-			if (note) {
-				if (si === 0) {
-					allMeasures.push(note.sourceMeasure);
-				}
-				if (si === 0 && state.isSpecialBookCategory) {
-					for (const expression of (note.sourceMeasure as SourceMeasure)?.TempoExpressions) {
-						if (expression?.InstantaneousTempo?.beatUnit) {
-							// 取最后一个有效的tempo
-							// activeInstantaneousTempo = expression.InstantaneousTempo
-							beatUnit = expression.InstantaneousTempo.beatUnit;
-							// beatUnit = expression.InstantaneousTempo.beatUnit
-						}
-					}
-				}
-				let measureSpeed = note.sourceMeasure.tempoInBPM;
-				const { metronomeNoteIndex } = iterator.currentMeasure;
-				if (metronomeNoteIndex !== 0 && metronomeNoteIndex > si) {
-					measureSpeed = allNotes[allNotes.length - 1]?.speed || 100;
-				}
-				const activeVerticalMeasureList = [note.sourceMeasure.verticalMeasureList?.[0]] || [];
-				// console.log([...activeVerticalMeasureList])
-				const { realValue } = iterator.currentTimeStamp;
-				// console.log({...iterator}, i)
-				const { RealValue: vRealValue, Denominator: vDenominator } = formatDuration(iterator.currentMeasure.activeTimeSignature, iterator.currentMeasure.duration);
-				let { wholeValue, numerator, denominator, realValue: NoteRealValue } = note.length;
-				if (i === 0 && ["2670"].includes(detailId)) {
-					NoteRealValue = 0.03125;
-				}
-				if (isDouble && currentTime > 0) {
-					if (currentTime != NoteRealValue) {
-						console.log(`小节 ${note.sourceMeasure.MeasureNumberXML} 替换: noteLength: ${NoteRealValue}, 最小: ${currentTime}`);
-						NoteRealValue = currentTime;
-					}
-				}
-				// note.sourceMeasure.MeasureNumberXML === 8 && console.error(`小节 ${note.sourceMeasure.MeasureNumberXML}`, NoteRealValue)
-				if (['12667', '12673'].includes(detailId)) {
-					if (isMutileSubject && currentTimes[i + 1] > 0 && NoteRealValue > currentTimes[i + 1]) {
-						NoteRealValue = currentTimes[i + 1]
-					}
-				}
-				if (["12673"].includes(detailId) && route.query["part-index"] === "22" && i == 208) {
-					console.log(note.sourceMeasure.MeasureNumberXML, i)
-					NoteRealValue = 0.125
-				}
-				let relativeTime = usetime; //realValue * 4 * (60 / measureSpeed)
-				// if (!useedmeasures.has(iterator.currentMeasureIndex)) {
-				// usetime += vRealValue * 4 * (60 / measureSpeed)
-				// }
-				// useedmeasures.add(iterator.currentMeasureIndex)
-				// console.table({
-				//   currentMeasureIndex: iterator.currentMeasureIndex,
-				//   i,
-				//   vRealValue,
-				//   measureSpeed,
-				//   usetime,
-				//   relativeTime,
-				// })
-				// console.log({...iterator}, {...iterator.currentTimeStamp})
-				// console.log('relativeTime', relativeTime, 'realValue', realValue, 'baseSpeed', baseSpeed)
-				// 速度不能为0
-				// 此处的速度应该是按照设置的速度而不是校准后的速度,否则mp3速度不对
-				let beatSpeed = (state.isSpecialBookCategory ? measureSpeed : baseSpeed) || 1;
-				// if(['2590'].includes(detailId)){
-				//   beatSpeed = 160
-				// }
-				// console.log(getTimeByBeatUnit(beatUnit, measureSpeed, iterator.currentMeasure.activeTimeSignature.Denominator))
-				let gradualLength = 0;
-				// console.log("metronomeNoteIndex", i, iterator.currentMeasure.metronomeNoteIndex)
-				let speed = (state.isSpecialBookCategory ? measureSpeed : baseSpeed) || 1;
-				gradualChange = iterator.currentMeasure.speedInfo || gradualChange;
-				gradualSpeed = osmd.sheet.soundTempos?.get(note.sourceMeasure.measureListIndex) || gradualSpeed;
-				if (!gradualSpeed || gradualSpeed.length < 2) {
-					gradualSpeed = createSpeedInfo(gradualChange, speed);
-				}
-				const measureListIndex = iterator.currentMeasure.measureListIndex;
-				// 计算相差时间按照比例分配到所有音符上
-				if (state.gradualTimes && Object.keys(state.gradualTimes).length > 0) {
-					const withInRangeNote = state.gradual.find((item, index) => {
-						const nextItem: any = state.gradual[index + 1];
-						return item[0].measureIndex <= measureListIndex && item[1]?.measureIndex! >= measureListIndex && (!nextItem || nextItem?.[0].measureIndex !== measureListIndex);
-					});
-					if (!withInRangeNote) {
-						useGradualTime = 0;
-					}
-					const [first, last] = withInRangeNote || [];
-					if (first && last) {
-						// 小节数量
-						const continuous = last.measureIndex - first.measureIndex;
-						// 开始小节内
-						const inTheFirstMeasure = first.closedMeasureIndex == measureListIndex && si >= first.noteInMeasureIndex;
-						// 结束小节内
-						const inTheLastMeasure = last.closedMeasureIndex === measureListIndex && si < last.noteInMeasureIndex;
-						// 范围内小节
-						const inFiestOrLastMeasure = first.closedMeasureIndex !== measureListIndex && last.closedMeasureIndex !== measureListIndex;
-						if (inTheFirstMeasure || inTheLastMeasure || inFiestOrLastMeasure) {
-							const startTime = state.gradualTimes[first.measureIndex];
-							const endTime = state.gradualTimes[last.measureIndex];
-							if (startTime && endTime) {
-								const times = continuous - first.leftDuration / first.allDuration + last.leftDuration / last.allDuration;
-								const diff = dayjs(tranTime(endTime)).diff(dayjs(tranTime(startTime)), "millisecond");
-								gradualLength = ((NoteRealValue / vRealValue / times) * diff) / 1000;
-								useGradualTime += gradualLength;
-							}
-						}
-					}
-				} else if (gradualChange && gradualSpeed && (gradualChange.startXmlNoteIndex === si || gradualChangeIndex > 0)) {
-					// const tmpNoteLength = (wholeValue + numerator / denominator) * vDenominator * (60 / beatSpeed)
-					// const tmpMeasureLength = vRealValue * 4 * (60 / beatSpeed)
-					const startSpeed = gradualSpeed[0] - (gradualSpeed[1] - gradualSpeed[0]);
-					// console.log((gradualSpeed[gradualSpeed.length - 1] - startSpeed) * tmpNoteLength/tmpMeasureLength)
-					// console.log(gradualChange, gradualSpeed, startSpeed, gradualChange.startXmlNoteIndex, si, gradualChangeIndex)
-					// gradualChangeIndex = 0
-					const { resetXmlNoteIndex, endXmlNoteIndex } = gradualChange;
-					const noteDiff = endXmlNoteIndex;
-					let stepSpeed = (gradualSpeed[gradualSpeed.length - 1] - startSpeed) / noteDiff;
-					stepSpeed = note.DotsXml ? stepSpeed / 1.5 : stepSpeed;
-					// console.log(gradualChangeIndex, stepSpeed, stepSpeed * gradualChangeIndex, stepSpeed * (gradualChangeIndex + 1), noteDiff, resetXmlNoteIndex)
-					if (gradualChangeIndex < noteDiff) {
-						// stepSpeeds.push((gradualSpeed[gradualSpeed.length - 1] - startSpeed) * tmpNoteLength/tmpMeasureLength)
-						// speed += Math.ceil((stepSpeed) * (gradualChangeIndex + 1))
-						const tempSpeed = Math.ceil(speed + stepSpeed * gradualChangeIndex);
-						let tmpSpeed = getTimeByBeatUnit(beatUnit, tempSpeed, iterator.currentMeasure.activeTimeSignature.Denominator);
-						const maxLength = (wholeValue + numerator / denominator) * vDenominator * (60 / tmpSpeed);
-						// speed += stepSpeeds.reduce((a, b) => a + b, 0)
-						speed += Math.ceil(stepSpeed * (gradualChangeIndex + 1));
-						tmpSpeed = getTimeByBeatUnit(beatUnit, speed, iterator.currentMeasure.activeTimeSignature.Denominator);
-						const minLength = (wholeValue + numerator / denominator) * vDenominator * (60 / tmpSpeed);
-						gradualLength = (maxLength + minLength) / 2;
-						// console.table({maxLength, minLength, gradualLength, tempSpeed, speed, tmpSpeed, dot: note.DotsXml})
-					} else if (resetXmlNoteIndex > gradualChangeIndex) {
-						speed = allNotes[i - 1]?.speed;
-						// console.log('resetXmlNoteIndex', resetXmlNoteIndex, 'gradualChangeIndex', gradualChangeIndex, allNotes[i -1]?.speed)
-					}
-					beatSpeed = (state.isSpecialBookCategory ? getTimeByBeatUnit(beatUnit, speed, iterator.currentMeasure.activeTimeSignature.Denominator) : baseSpeed) || 1;
-					const isEnd = !(gradualChangeIndex < noteDiff) && !(resetXmlNoteIndex > gradualChangeIndex);
-					gradualChangeIndex++;
-					// console.log(gradualChangeIndex)
-					if (isEnd) {
-						gradualChangeIndex = 0;
-						gradualChange = undefined;
-						gradualSpeed = undefined;
-						stepSpeeds = [];
-					}
-				}
-				if (i === 0) {
-					// console.log(getFixTime(speed))
-					// if(['2590'].includes(detailId)){
-					//   fixtime += getFixTime(90)
-					// } else {
-					fixtime += getFixTime(beatSpeed);
-					// }
-				}
-				// console.log(speed, beatSpeed)
-				// const vDenominator = 8
-				// console.log(NoteRealValue)
-				// console.log(activeInstantaneousTempo)
-				// console.log({vDenominator, NoteRealValue, denominator, numerator, wholeValue, realValue, vRealValue, measureSpeed, speed, beatSpeed})
-				// console.log(gradualLength)
-				let noteLength = gradualLength ? gradualLength : Math.min(vRealValue, NoteRealValue) * formatBeatUnit(beatUnit) * (60 / beatSpeed);
-				const measureLength = vRealValue * vDenominator * (60 / beatSpeed);
-				// console.table({value: iterator.currentTimeStamp.realValue, vRealValue,NoteRealValue, noteLength,measureLength, MeasureNumberXML: note.sourceMeasure.MeasureNumberXML})
-				usetime += noteLength;
-				relaMeasureLength += noteLength;
-				let relaEndtime = noteLength + relativeTime;
-				const fixedKey = note.fixedKey || 0;
-				const svgElelent = activeVerticalMeasureList[0]?.vfVoices["1"]?.tickables[si];
-				// console.log(note.sourceMeasure.MeasureNumberXML,note,svgElelent, NoteRealValue, measureLength)
-				if (allNotes.length && allNotes[allNotes.length - 1].relativeTime === relativeTime) {
-					continue;
-				}
-				// console.log(iterator.currentMeasure)
-				// 如果是弱起就补齐缺省的时长
-				if (i === 0) {
-					const diff = getMeasureDurationDiff(iterator.currentMeasure);
-					if (diff > 0) {
-						difftime = diff * formatBeatUnit(beatUnit) * (60 / beatSpeed);
-						fixtime += difftime;
-					}
-					// diff获取不准确时, 弱起补齐
-					if (["2589", "2561", "2560", "2559", "2558", "2556", "2555", "2554"].includes(detailId)) {
-						difftime = iterator.currentTimeStamp.realValue * formatBeatUnit(beatUnit) * (60 / beatSpeed);
-						fixtime += difftime;
-					}
-				}
-				// console.log(note.tie)
-				const nodeDetail = {
-					difftime,
-					octaveOffset: activeVerticalMeasureList[0]?.octaveOffset,
-					frequency: note.pitch?.frequency,
-					speed,
-					beatSpeed,
-					i,
-					si,
-					stepSpeeds,
-					measureOpenIndex: allMeasures.length - 1,
-					measures,
-					tempoInBPM: note.sourceMeasure.tempoInBPM,
-					measureLength,
-					relaMeasureLength,
-					id: svgElelent?.attrs.id,
-					note: note.halfTone + 12, // see issue #224
-					relativeTime: retain(relativeTime),
-					time: retain(relativeTime + fixtime),
-					endtime: retain(relaEndtime + fixtime),
-					relaEndtime: retain(relaEndtime),
-					realValue,
-					halfTone: note.halfTone,
-					noteElement: note,
-					svgElelent,
-					fixedKey,
-					realKey: 0,
-					duration: 0,
-					formatLyricsEntries: formatLyricsEntries(note),
-					stave: activeVerticalMeasureList[0]?.stave,
-					firstVerticalMeasure: activeVerticalMeasureList[0],
-					noteLength: 1,
-					osdmContext: osmd,
-					speedbeatUnit: beatUnit,
-				};
-				nodeDetail.realKey = formatRealKey(note.halfTone - fixedKey * 12, nodeDetail);
-				nodeDetail.duration = nodeDetail.endtime - nodeDetail.time;
-				// iterator.currentMeasureIndex == 61 &&  console.log("🚀 ~ differFrom:",svgElelent, note)
-				// if (nodeDetail.id && allNoteId.includes(nodeDetail.id)) {
-				//   continue
-				// }
-				// console.log(nodeDetail.noteElement.sourceMeasure?.staffLinkedExpressions)
-				let tickables = activeVerticalMeasureList[0]?.vfVoices["1"]?.tickables || [];
-				if ([121].includes(state.subjectId)){
-					tickables= note.sourceMeasure.verticalSourceStaffEntryContainers
-				}
-				// console.log(note.sourceMeasure.MeasureNumberXML, note.sourceMeasure.verticalSourceStaffEntryContainers.length)
-				nodeDetail.noteLength = tickables.length || 1;
-				allNotes.push(nodeDetail);
-				allNoteId.push(nodeDetail.id);
-				measures.push(nodeDetail);
-				if (si < tickables.length - 1) {
-					si++;
-				} else {
-					// usetime += measureLength - relaMeasureLength
-					// console.log(relaMeasureLength, measureLength)
-					si = 0;
-					relaMeasureLength = 0;
-					measures = [];
-				}
-			}
-			i++;
-		}
-	}
-	// 按照时间轴排序
-	const sortArray = allNotes.sort((a, b) => a.relativeTime - b.relativeTime).map((item, index) => ({ ...item, i: index }));
-	// for (let i = 0; i < sortArray.length; i++) {
-	//   const note = { ...sortArray[i] }
-	//   const prevNote = sortArray[i - 1]
-	//   const tieNote =
-	//     note?.noteElement?.tie?.notes.map(
-	//       (_n: any) => _n.NoteToGraphicalNoteObjectId
-	//     ) || []
-	//   const isNotNeedStop =
-	//     note.noteElement.tie &&
-	//     prevNote?.noteElement.tie &&
-	//     note.halfTone === prevNote?.halfTone &&
-	//     tieNote.includes(prevNote?.noteElement?.NoteToGraphicalNoteObjectId)
-	//   const isOvertone = false //note.noteElement.slurs.length && note.noteElement.slurs[0].endNote === note.noteElement
-	//   if (prevNote) {
-	//     if (isNotNeedStop || isOvertone) {
-	//       note.sourceStartTime = note.time
-	//       note.sourceRelativeTime = note.relativeTime
-	//       note.sourceRealValue = note.realValue
-	//       note.sourceEndTime = note.endtime
-	//       note.sourceRelaEndtime = note.relaEndtime
-	//       note.relativeTime = prevNote.relativeTime
-	//       note.realValue = prevNote.realValue
-	//       note.time = prevNote.time
-	//       note.endtime = prevNote.endtime
-	//       note.relaEndtime = prevNote.relaEndtime
-	//     }
-	//     // 此处会导致休止符继续上一个音的指法
-	//     if (note.halfTone === 0) {
-	//       note.realKey = prevNote.realKey
-	//     }
-	//   }
-	//   sortArray[i] = note
-	// }
-	return sortArray;
-};
-
 export const getAllNoteElements = (osmd: any) => {
 	const list: any[] = [];
 	const listById: {
@@ -711,7 +296,7 @@ export const getBoundingBoxByverticalNote = (note: any) => {
 	measures = !measures || !measures[0] ? note?.noteElement?.isRestFlag && getPrevHasSourceNote(note)?.noteElement?.sourceMeasure?.verticalMeasureList : measures;
 	let height = 0;
 	if (measures) {
-		const firstMeasure = measures[runtime.partIndex];
+		const firstMeasure = measures[state.partIndex];
 		for (let index = 0; index < measures.length; index++) {
 			const measure = measures[index];
 			if (measure?.stave) {
@@ -763,7 +348,6 @@ export type Duration = FractionDefault & {
 
 export const getDuration = (osmd?: OpenSheetMusicDisplay): Duration => {
 	if (osmd) {
-		// console.log(osmd.GraphicSheet.MeasureList[0][0]?.parentSourceMeasure)
 		const { Duration, TempoInBPM, ActiveTimeSignature, TempoExpressions } = osmd.GraphicSheet.MeasureList[0][0]?.parentSourceMeasure;
 		if (Duration) {
 			let beatUnit = "quarter";
@@ -921,44 +505,6 @@ export const setPrefix = (url: string): string => {
 	return "";
 };
 
-export const formatXML = (xml: string): string => {
-	if (!xml) return "";
-	const xmlParse = new DOMParser().parseFromString(xml, "text/xml");
-	const measures = xmlParse.getElementsByTagName("measure");
-	// let speed = -1
-	let beats = -1;
-	let beatType = -1;
-	// 小节中如果没有节点默认为休止符
-	for (const measure of measures) {
-		if (beats === -1 && measure.getElementsByTagName("beats").length) {
-			beats = parseInt(measure.getElementsByTagName("beats")[0].textContent || "4");
-		}
-		if (beatType === -1 && measure.getElementsByTagName("beat-type").length) {
-			beatType = parseInt(measure.getElementsByTagName("beat-type")[0].textContent || "4");
-		}
-		// if (speed === -1 && measure.getElementsByTagName('per-minute').length) {
-		//   speed = parseInt(measure.getElementsByTagName('per-minute')[0].textContent || this.firstLib?.speed)
-		// }
-		const divisions = parseInt(measure.getElementsByTagName("divisions")[0]?.textContent || "256");
-		if (measure.getElementsByTagName("note").length === 0) {
-			const forwardTimeElement = measure.getElementsByTagName("forward")[0]?.getElementsByTagName("duration")[0];
-			if (forwardTimeElement) {
-				forwardTimeElement.textContent = "0";
-			}
-			measure.innerHTML =
-				measure.innerHTML +
-				`
-        <note>
-          <rest measure="yes"/>
-          <duration>${divisions * beats}</duration>
-          <voice>1</voice>
-          <type>whole</type>
-        </note>`;
-		}
-	}
-	return new XMLSerializer().serializeToString(xmlParse);
-};
-
 export type CustomInfo = {
 	showSpeed: boolean;
 	parsedXML: string;
@@ -971,7 +517,7 @@ export const getCustomInfo = (xml: string): CustomInfo => {
 		parsedXML: xml,
 	};
 	const xmlParse = new DOMParser().parseFromString(xml, "text/xml");
-	const words = xmlParse.getElementsByTagName("words");
+	const words: any = xmlParse.getElementsByTagName("words");
 	for (const word of words) {
 		if (word && word.textContent?.trim() === "隐藏速度") {
 			data.showSpeed = false;
@@ -989,7 +535,7 @@ export const getCustomInfo = (xml: string): CustomInfo => {
  * 替换文本标签中的内容
  */
 const replaceTextConent = (beforeText: string, afterText: string, ele: Element): Element => {
-	const words = ele?.getElementsByTagName("words");
+	const words: any = ele?.getElementsByTagName("words");
 	for (const word of words) {
 		if (word && word.textContent?.trim() === beforeText) {
 			word.textContent = afterText;
@@ -1055,23 +601,7 @@ export const isRepeatWord = (text: string): boolean => {
 		const dsRegEx: string = "d\\s?\\.s\\.";
 		const dcRegEx: string = "d\\.\\s?c\\.";
 
-		return (
-			innerText === "@" ||
-			StringUtil.StringContainsSeparatedWord(innerText, dsRegEx + " al fine", true) ||
-			StringUtil.StringContainsSeparatedWord(innerText, dsRegEx + " al coda", true) ||
-			StringUtil.StringContainsSeparatedWord(innerText, dcRegEx + " al fine", true) ||
-			StringUtil.StringContainsSeparatedWord(innerText, dcRegEx + " al coda", true) ||
-			StringUtil.StringContainsSeparatedWord(innerText, dcRegEx) ||
-			StringUtil.StringContainsSeparatedWord(innerText, "da\\s?capo", true) ||
-			StringUtil.StringContainsSeparatedWord(innerText, dsRegEx, true) ||
-			StringUtil.StringContainsSeparatedWord(innerText, "dal\\s?segno", true) ||
-			StringUtil.StringContainsSeparatedWord(innerText, "al\\s?coda", true) ||
-			StringUtil.StringContainsSeparatedWord(innerText, "to\\s?coda", true) ||
-			StringUtil.StringContainsSeparatedWord(innerText, "a (la )?coda", true) ||
-			StringUtil.StringContainsSeparatedWord(innerText, "fine", true) ||
-			StringUtil.StringContainsSeparatedWord(innerText, "coda", true) ||
-			StringUtil.StringContainsSeparatedWord(innerText, "segno", true)
-		);
+		return innerText === "@" || StringUtil.StringContainsSeparatedWord(innerText, dsRegEx + " al fine", true) || StringUtil.StringContainsSeparatedWord(innerText, dsRegEx + " al coda", true) || StringUtil.StringContainsSeparatedWord(innerText, dcRegEx + " al fine", true) || StringUtil.StringContainsSeparatedWord(innerText, dcRegEx + " al coda", true) || StringUtil.StringContainsSeparatedWord(innerText, dcRegEx) || StringUtil.StringContainsSeparatedWord(innerText, "da\\s?capo", true) || StringUtil.StringContainsSeparatedWord(innerText, dsRegEx, true) || StringUtil.StringContainsSeparatedWord(innerText, "dal\\s?segno", true) || StringUtil.StringContainsSeparatedWord(innerText, "al\\s?coda", true) || StringUtil.StringContainsSeparatedWord(innerText, "to\\s?coda", true) || StringUtil.StringContainsSeparatedWord(innerText, "a (la )?coda", true) || StringUtil.StringContainsSeparatedWord(innerText, "fine", true) || StringUtil.StringContainsSeparatedWord(innerText, "coda", true) || StringUtil.StringContainsSeparatedWord(innerText, "segno", true);
 	}
 	return false;
 };
@@ -1082,7 +612,7 @@ export const onlyVisible = (xml: string, partIndex: number): string => {
 	const xmlParse = new DOMParser().parseFromString(xml, "text/xml");
 	const partList = xmlParse.getElementsByTagName("part-list")?.[0]?.getElementsByTagName("score-part") || [];
 	const partListNames = Array.from(partList).map((item) => item.getElementsByTagName("part-name")?.[0].textContent || "");
-	const parts = xmlParse.getElementsByTagName("part");
+	const parts: any = xmlParse.getElementsByTagName("part");
 	// const firstTimeInfo = parts[0]?.getElementsByTagName('metronome')[0]?.parentElement?.parentElement?.cloneNode(true)
 	const firstMeasures = [...parts[0]?.getElementsByTagName("measure")];
 	const metronomes = [...parts[0]?.getElementsByTagName("metronome")];
@@ -1100,7 +630,7 @@ export const onlyVisible = (xml: string, partIndex: number): string => {
 	state.partListNames = partListNames;
 	if (visiblePartInfo) {
 		const id = visiblePartInfo.getAttribute("id");
-		Array.from(parts).forEach((part) => {
+		Array.from(parts).forEach((part: any) => {
 			if (part && part.getAttribute("id") !== id) {
 				part.parentNode?.removeChild(part);
 				// 不等于第一行才添加避免重复添加
@@ -1118,8 +648,8 @@ export const onlyVisible = (xml: string, partIndex: number): string => {
 					}
 				}
 				Object.values(metronomeData).forEach((metronome) => {
-					const metronomeContainer = metronome.parentElement?.parentElement;
-					const parentMeasure = metronomeContainer?.parentElement;
+					const metronomeContainer: any = metronome.parentElement?.parentElement;
+					const parentMeasure: any = metronomeContainer?.parentElement;
 					const measureMetronomes = [...(parentMeasure?.childNodes || [])];
 					const metronomesIndex = metronomeContainer ? measureMetronomes.indexOf(metronomeContainer) : -1;
 					// console.log(parentMeasure)
@@ -1148,7 +678,7 @@ export const onlyVisible = (xml: string, partIndex: number): string => {
 							// 找当前小节是否包含word标签
 							const _words = Array.from(activeMeasure?.getElementsByTagName("words") || []);
 							// 遍历word标签,检查是否和第一小节重复,如果有重复则不平移word
-							const total = _words.reduce((total: any, _word) => {
+							const total = _words.reduce((total: any, _word: any) => {
 								if (_word.textContent?.includes(text)) {
 									total++;
 								}
@@ -1256,7 +786,7 @@ export const onlyVisible = (xml: string, partIndex: number): string => {
 // 倚音后连音线
 export const appoggianceFormate = (xmlParse: Document): Document => {
 	if (!xmlParse) return xmlParse;
-	const graces = xmlParse.querySelectorAll("grace");
+	const graces: any = xmlParse.querySelectorAll("grace");
 	if (!graces.length) return xmlParse;
 	const getNextElement = (el: HTMLElement): HTMLElement => {
 		if (el.querySelector("grace")) {
@@ -1285,7 +815,8 @@ export const appoggianceFormate = (xmlParse: Document): Document => {
 };
 
 export const getVoicePartInfo = () => {
-	const { MusicalInstrumentClassification, chinesePartName } = appState;
+	// const { MusicalInstrumentClassification, chinesePartName } = appState;
+	const { MusicalInstrumentClassification, chinesePartName } = { MusicalInstrumentClassification: {}, chinesePartName: [] as any[] };
 
 	let subjectId = -1;
 	const { partListNames, partIndex } = state;
@@ -1293,7 +824,7 @@ export const getVoicePartInfo = () => {
 	if (filterPartNames.length) {
 		for (const Classification of Object.entries(MusicalInstrumentClassification)) {
 			const [key, value] = Classification as [string, string[]];
-			const activePart = partListNames[partIndex];
+			const activePart: any = partListNames[partIndex];
 			// console.log({activePart, value, partListNames})
 			const filterValue = value.filter((item) => item && (activePart || "").indexOf(item || "") > -1);
 			if (activePart && (filterValue.length || value.includes(activePart))) {
@@ -1400,3 +931,424 @@ export const getNotesByid = (id: string): NoteList => {
 	const notes = new NoteList(state.times.filter((item) => item.id === id));
 	return notes;
 };
+
+/** 格式化当前曲谱缩放比例 */
+export const formatZoom = (num = 1) => {
+	return num * state.zoom;
+};
+
+/** 格式化曲谱
+ * 1.全休止符的小节,没有音符默认加个全休止符
+ */
+export const formatXML = (xml: string): string => {
+	if (!xml) return "";
+	const xmlParse = new DOMParser().parseFromString(xml, "text/xml");
+	const measures = Array.from(xmlParse.getElementsByTagName("measure"));
+	// let speed = -1
+	let beats = -1;
+	let beatType = -1;
+	// 小节中如果没有节点默认为休止符
+	for (const measure of measures) {
+		if (beats === -1 && measure.getElementsByTagName("beats").length) {
+			beats = parseInt(measure.getElementsByTagName("beats")[0].textContent || "4");
+		}
+		if (beatType === -1 && measure.getElementsByTagName("beat-type").length) {
+			beatType = parseInt(measure.getElementsByTagName("beat-type")[0].textContent || "4");
+		}
+		// if (speed === -1 && measure.getElementsByTagName('per-minute').length) {
+		//   speed = parseInt(measure.getElementsByTagName('per-minute')[0].textContent || this.firstLib?.speed)
+		// }
+		const divisions = parseInt(measure.getElementsByTagName("divisions")[0]?.textContent || "256");
+		if (measure.getElementsByTagName("note").length === 0) {
+			const forwardTimeElement = measure.getElementsByTagName("forward")[0]?.getElementsByTagName("duration")[0];
+			if (forwardTimeElement) {
+				forwardTimeElement.textContent = "0";
+			}
+			measure.innerHTML =
+				measure.innerHTML +
+				`
+        <note>
+          <rest measure="yes"/>
+          <duration>${divisions * beats}</duration>
+          <voice>1</voice>
+          <type>whole</type>
+        </note>`;
+		}
+	}
+	return new XMLSerializer().serializeToString(xmlParse);
+};
+
+export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
+	const detailId = state.examSongId?.toString();
+	const partIndex = state.partIndex + "";
+	let fixtime = browserInfo.huawei ? 0.08 : 0; //getFixTime()
+	const allNotes: any[] = [];
+	const allNoteId: string[] = [];
+	const allMeasures: any[] = [];
+	const { baseSpeed = 90 } = state;
+	const formatRealKey = (realKey: number, detail: any) => {
+		// 长笛的LEVEL 2-5-1条练习是泛音练习,以每小节第一个音的指法为准,高音不变变指法。
+		const olnyOneIds = ["906"];
+		if (olnyOneIds.includes(detailId)) {
+			return detail.measures[0]?.realKey || realKey;
+		}
+		// 圆号的LEVEL 2-5条练习是泛音练习,最后四小节指法以连音线第一个小节为准
+		const olnyOneIds2 = ["782", "784"];
+		if (olnyOneIds2.includes(detailId)) {
+			const measureNumbers = [14, 16, 30, 32];
+			if (measureNumbers.includes(detail.firstVerticalMeasure?.measureNumber)) {
+				return allNotes[allNotes.length - 1]?.realKey || realKey;
+			}
+		}
+		// 2-6 第三小节指法按照第一个音符显示
+		const filterIds = ["900", "901", "640", "641", "739", "740", "800", "801", "773", "774", "869", "872", "714", "715"];
+		if (filterIds.includes(detailId)) {
+			if (detail.firstVerticalMeasure?.measureNumber === 3 || detail.firstVerticalMeasure?.measureNumber === 9) {
+				return detail.measures[0]?.realKey || realKey;
+			}
+		}
+		return realKey;
+	};
+	if (!osmd.cursor) return [];
+	try {
+		// osmd.cursor.reset();
+	} catch (error) {}
+	const iterator: any = osmd.cursor.Iterator;
+	// console.log("🚀 ~ iterator:", iterator)
+	console.time("音符跑完时间");
+
+	let i = 0;
+	let si = 0;
+	let measures: any[] = [];
+	let stepSpeeds: number[] = [];
+	/** 弱起时间 */
+	let difftime = 0;
+	let usetime = 0;
+	let useGradualTime = 0;
+	let relaMeasureLength = 0;
+	let beatUnit = "quarter";
+	let gradualSpeed;
+	let gradualChange: GradualChange | undefined;
+	let gradualChangeIndex = 0;
+	const _notes = [] as any[];
+
+	if (state.gradualTimes) {
+		if (["12280"].includes(detailId) && ["24"].includes(partIndex)) {
+			state.gradualTimes["8"] = "00:25:63";
+			state.gradualTimes["66"] = "01:53:35";
+			state.gradualTimes["90"] = "02:41:40";
+		}
+		console.log("合奏速度", state.gradual, state.gradualTimes);
+	}
+
+	let currentTimeStamp = iterator.currentTimeStamp.RealValue;
+	const currentTimes = [] as any[];
+	let isSetNextNoteReal = false;
+	let differFrom = 0;
+	while (!iterator.EndReached) {
+		console.log({ ...iterator });
+		const voiceEntries = iterator.CurrentVoiceEntries?.[0] ? [iterator.CurrentVoiceEntries?.[0]] : [];
+		let currentVoiceEntries: any[] = [];
+		// 单声部多声轨
+		if (state.multitrack > 0) {
+			currentVoiceEntries = [...iterator.CurrentVoiceEntries];
+		} else {
+			currentVoiceEntries = [...iterator.CurrentVoiceEntries].filter((n) => {
+				return n && n?.ParentVoice?.VoiceId != 1;
+			});
+		}
+		let currentTime = 0;
+		let isDouble = false;
+		let isMutileSubject = false;
+
+		// console.log(iterator.currentMeasureIndex, [...iterator.currentVoiceEntries])
+		// iterator.currentMeasureIndex == 45 && console.log(currentTimeStamp, [...iterator.currentVoiceEntries])
+		if (currentVoiceEntries.length && !isSetNextNoteReal) {
+			// iterator.currentMeasureIndex == 15 &&  console.log(iterator.currentMeasureIndex, isSetNextNoteReal)
+			isDouble = true;
+			let voiceNotes = [...iterator.CurrentVoiceEntries].reduce((notes, n) => {
+				notes.push(...n.Notes);
+				return notes;
+			}, [] as any);
+			voiceNotes = voiceNotes.sort((a: any, b: any) => a?.length?.realValue - b?.length?.realValue);
+			currentTime = voiceNotes?.[0]?.length?.realValue || 0;
+			// iterator.currentMeasureIndex == 15 && console.log("🚀 ~ currentTime:", currentTime)
+
+			if (state.multitrack > 0 && currentVoiceEntries.length === 2) {
+				const min = voiceNotes[0]?.length?.realValue || 0;
+				const max = voiceNotes[voiceNotes.length - 1]?.length?.realValue || 0;
+				differFrom = max - min;
+				isSetNextNoteReal = differFrom === 0 ? false : true;
+				// iterator.currentMeasureIndex == 15 &&  console.log("🚀 ~ differFrom:", differFrom, isSetNextNoteReal)
+				// console.log(iterator.currentMeasureIndex, min, max)
+			}
+		}
+		// 多声部上下音符没对齐,光标多走一拍
+		if (_notes[_notes.length - 1]?.isDouble && !currentVoiceEntries.length) {
+			isMutileSubject = true;
+		}
+		if (state.multitrack > 0 && !isDouble && isSetNextNoteReal) {
+			isDouble = true;
+			currentTime = differFrom;
+			isSetNextNoteReal = false;
+			differFrom = 0;
+			// iterator.currentMeasureIndex == 7 && console.log("🚀 ~ currentTime:",iterator.currentMeasure, currentTime)
+		}
+		currentTimes.push(iterator.currentTimeStamp.realValue - currentTimeStamp);
+		currentTimeStamp = iterator.currentTimeStamp.realValue;
+		for (const v of voiceEntries) {
+			const note = v.notes[0];
+			note.fixedKey = note.ParentVoiceEntry.ParentVoice.Parent.SubInstruments[0].fixedKey || 0;
+			// 有倚音
+			if (note?.voiceEntry?.isGrace) {
+				isDouble = true;
+				let ns = [...iterator.currentVoiceEntries].reduce((notes, n) => {
+					notes.push(...n.notes);
+					return notes;
+				}, []);
+				ns = ns.sort((a: any, b: any) => b?.length?.realValue - a?.length?.realValue);
+				currentTime = currentTime != 0 ? Math.min(ns?.[0]?.length?.realValue, currentTime) : ns?.[0]?.length?.realValue;
+			}
+			if (state.multitrack > 0 && currentTime > note.length.realValue) {
+				// console.log(iterator.currentMeasureIndex, currentTime , note.length.realValue)
+				currentTime = note.length.realValue;
+			}
+			_notes.push({
+				note,
+				iterator: { ...iterator },
+				currentTime,
+				isDouble,
+				isMutileSubject,
+			});
+		}
+
+		iterator.moveToNextVisibleVoiceEntry(false);
+	}
+	for (let { note, iterator, currentTime, isDouble, isMutileSubject } of _notes) {
+		if (note) {
+			// console.log("🚀 ~ note:", note)
+			if (si === 0) {
+				allMeasures.push(note.sourceMeasure);
+			}
+			if (si === 0 && state.isSpecialBookCategory) {
+				for (const expression of (note.sourceMeasure as SourceMeasure)?.TempoExpressions) {
+					if (expression?.InstantaneousTempo?.beatUnit) {
+						// 取最后一个有效的tempo
+						// activeInstantaneousTempo = expression.InstantaneousTempo
+						beatUnit = expression.InstantaneousTempo.beatUnit;
+						// beatUnit = expression.InstantaneousTempo.beatUnit
+					}
+				}
+			}
+			let measureSpeed = note.sourceMeasure.tempoInBPM;
+			const { metronomeNoteIndex } = iterator.currentMeasure;
+			if (metronomeNoteIndex !== 0 && metronomeNoteIndex > si) {
+				measureSpeed = allNotes[allNotes.length - 1]?.speed || 100;
+			}
+
+			const activeVerticalMeasureList = [note.sourceMeasure.verticalMeasureList?.[0]] || [];
+			// console.log([...activeVerticalMeasureList])
+			const { realValue } = iterator.currentTimeStamp;
+			// console.log({...iterator}, i)
+			const { RealValue: vRealValue, Denominator: vDenominator } = formatDuration(iterator.currentMeasure.activeTimeSignature, iterator.currentMeasure.duration);
+			let { wholeValue, numerator, denominator, realValue: NoteRealValue } = note.length;
+			if (i === 0 && ["2670"].includes(detailId)) {
+				NoteRealValue = 0.03125;
+			}
+			if (isDouble && currentTime > 0) {
+				if (currentTime != NoteRealValue) {
+					console.log(`小节 ${note.sourceMeasure.MeasureNumberXML} 替换: noteLength: ${NoteRealValue}, 最小: ${currentTime}`);
+					NoteRealValue = currentTime;
+				}
+			}
+			// note.sourceMeasure.MeasureNumberXML === 8 && console.error(`小节 ${note.sourceMeasure.MeasureNumberXML}`, NoteRealValue)
+			if (["12667", "12673"].includes(detailId)) {
+				if (isMutileSubject && currentTimes[i + 1] > 0 && NoteRealValue > currentTimes[i + 1]) {
+					NoteRealValue = currentTimes[i + 1];
+				}
+			}
+			if (["12673"].includes(detailId) && ["22"].includes(partIndex) && i == 208) {
+				console.log(note.sourceMeasure.MeasureNumberXML, i);
+				NoteRealValue = 0.125;
+			}
+			let relativeTime = usetime;
+			// 速度不能为0 此处的速度应该是按照设置的速度而不是校准后的速度,否则mp3速度不对
+			let beatSpeed = (state.isSpecialBookCategory ? measureSpeed : baseSpeed) || 1;
+			// 如果有节拍器,需要将节拍器的时间算出来
+			if (i === 0) {
+				fixtime += getFixTime(beatSpeed);
+			}
+			// console.log(getTimeByBeatUnit(beatUnit, measureSpeed, iterator.currentMeasure.activeTimeSignature.Denominator))
+			let gradualLength = 0;
+			let speed = (state.isSpecialBookCategory ? measureSpeed : baseSpeed) || 1;
+			gradualChange = iterator.currentMeasure.speedInfo || gradualChange;
+			gradualSpeed = osmd.Sheet.SoundTempos?.get(note.sourceMeasure.measureListIndex) || gradualSpeed;
+			if (!gradualSpeed || gradualSpeed.length < 2) {
+				gradualSpeed = createSpeedInfo(gradualChange, speed);
+			}
+			const measureListIndex = iterator.currentMeasure.measureListIndex;
+			// 计算相差时间按照比例分配到所有音符上
+			if (state.gradualTimes && Object.keys(state.gradualTimes).length > 0) {
+				const withInRangeNote = state.gradual.find((item, index) => {
+					const nextItem: any = state.gradual[index + 1];
+					return item[0].measureIndex <= measureListIndex && item[1]?.measureIndex! >= measureListIndex && (!nextItem || nextItem?.[0].measureIndex !== measureListIndex);
+				});
+				if (!withInRangeNote) {
+					useGradualTime = 0;
+				}
+				const [first, last] = withInRangeNote || [];
+				if (first && last) {
+					// 小节数量
+					const continuous = last.measureIndex - first.measureIndex;
+					// 开始小节内
+					const inTheFirstMeasure = first.closedMeasureIndex == measureListIndex && si >= first.noteInMeasureIndex;
+					// 结束小节内
+					const inTheLastMeasure = last.closedMeasureIndex === measureListIndex && si < last.noteInMeasureIndex;
+					// 范围内小节
+					const inFiestOrLastMeasure = first.closedMeasureIndex !== measureListIndex && last.closedMeasureIndex !== measureListIndex;
+					if (inTheFirstMeasure || inTheLastMeasure || inFiestOrLastMeasure) {
+						const startTime = state.gradualTimes[first.measureIndex];
+						const endTime = state.gradualTimes[last.measureIndex];
+						if (startTime && endTime) {
+							const times = continuous - first.leftDuration / first.allDuration + last.leftDuration / last.allDuration;
+							const diff = dayjs(tranTime(endTime)).diff(dayjs(tranTime(startTime)), "millisecond");
+							gradualLength = ((NoteRealValue / vRealValue / times) * diff) / 1000;
+							useGradualTime += gradualLength;
+						}
+					}
+				}
+			} else if (gradualChange && gradualSpeed && (gradualChange.startXmlNoteIndex === si || gradualChangeIndex > 0)) {
+				// const tmpNoteLength = (wholeValue + numerator / denominator) * vDenominator * (60 / beatSpeed)
+				// const tmpMeasureLength = vRealValue * 4 * (60 / beatSpeed)
+				const startSpeed = gradualSpeed[0] - (gradualSpeed[1] - gradualSpeed[0]);
+				// console.log((gradualSpeed[gradualSpeed.length - 1] - startSpeed) * tmpNoteLength/tmpMeasureLength)
+				// console.log(gradualChange, gradualSpeed, startSpeed, gradualChange.startXmlNoteIndex, si, gradualChangeIndex)
+				// gradualChangeIndex = 0
+				const { resetXmlNoteIndex, endXmlNoteIndex } = gradualChange;
+				const noteDiff = endXmlNoteIndex;
+				let stepSpeed = (gradualSpeed[gradualSpeed.length - 1] - startSpeed) / noteDiff;
+				stepSpeed = note.DotsXml ? stepSpeed / 1.5 : stepSpeed;
+				// console.log(gradualChangeIndex, stepSpeed, stepSpeed * gradualChangeIndex, stepSpeed * (gradualChangeIndex + 1), noteDiff, resetXmlNoteIndex)
+				if (gradualChangeIndex < noteDiff) {
+					// stepSpeeds.push((gradualSpeed[gradualSpeed.length - 1] - startSpeed) * tmpNoteLength/tmpMeasureLength)
+					// speed += Math.ceil((stepSpeed) * (gradualChangeIndex + 1))
+					const tempSpeed = Math.ceil(speed + stepSpeed * gradualChangeIndex);
+					let tmpSpeed = getTimeByBeatUnit(beatUnit, tempSpeed, iterator.currentMeasure.activeTimeSignature.Denominator);
+					const maxLength = (wholeValue + numerator / denominator) * vDenominator * (60 / tmpSpeed);
+					// speed += stepSpeeds.reduce((a, b) => a + b, 0)
+					speed += Math.ceil(stepSpeed * (gradualChangeIndex + 1));
+					tmpSpeed = getTimeByBeatUnit(beatUnit, speed, iterator.currentMeasure.activeTimeSignature.Denominator);
+					const minLength = (wholeValue + numerator / denominator) * vDenominator * (60 / tmpSpeed);
+					gradualLength = (maxLength + minLength) / 2;
+					// console.table({maxLength, minLength, gradualLength, tempSpeed, speed, tmpSpeed, dot: note.DotsXml})
+				} else if (resetXmlNoteIndex > gradualChangeIndex) {
+					speed = allNotes[i - 1]?.speed;
+					// console.log('resetXmlNoteIndex', resetXmlNoteIndex, 'gradualChangeIndex', gradualChangeIndex, allNotes[i -1]?.speed)
+				}
+				beatSpeed = (state.isSpecialBookCategory ? getTimeByBeatUnit(beatUnit, speed, iterator.currentMeasure.activeTimeSignature.Denominator) : baseSpeed) || 1;
+				const isEnd = !(gradualChangeIndex < noteDiff) && !(resetXmlNoteIndex > gradualChangeIndex);
+				gradualChangeIndex++;
+				// console.log(gradualChangeIndex)
+				if (isEnd) {
+					gradualChangeIndex = 0;
+					gradualChange = undefined;
+					gradualSpeed = undefined;
+					stepSpeeds = [];
+				}
+			}
+			// console.log(speed, beatSpeed)
+			// const vDenominator = 8
+			// console.log(NoteRealValue)
+			// console.log(activeInstantaneousTempo)
+			// console.log({vDenominator, NoteRealValue, denominator, numerator, wholeValue, realValue, vRealValue, measureSpeed, speed, beatSpeed})
+			// console.log(gradualLength)
+			let noteLength = gradualLength ? gradualLength : Math.min(vRealValue, NoteRealValue) * formatBeatUnit(beatUnit) * (60 / beatSpeed);
+			const measureLength = vRealValue * vDenominator * (60 / beatSpeed);
+			// console.table({value: iterator.currentTimeStamp.realValue, vRealValue,NoteRealValue, noteLength,measureLength, MeasureNumberXML: note.sourceMeasure.MeasureNumberXML})
+			usetime += noteLength;
+			relaMeasureLength += noteLength;
+			let relaEndtime = noteLength + relativeTime;
+			const fixedKey = note.fixedKey || 0;
+			const svgElelent = activeVerticalMeasureList[0]?.vfVoices["1"]?.tickables[si];
+			// console.log(note.sourceMeasure.MeasureNumberXML,note,svgElelent, NoteRealValue, measureLength)
+			if (allNotes.length && allNotes[allNotes.length - 1].relativeTime === relativeTime) {
+				continue;
+			}
+			// console.log(iterator.currentMeasure)
+			// 如果是弱起就补齐缺省的时长
+			if (i === 0) {
+				const diff = getMeasureDurationDiff(iterator.currentMeasure);
+				if (diff > 0) {
+					difftime = diff * formatBeatUnit(beatUnit) * (60 / beatSpeed);
+					fixtime += difftime;
+				}
+				// diff获取不准确时, 弱起补齐
+				if (["2589", "2561", "2560", "2559", "2558", "2556", "2555", "2554"].includes(detailId)) {
+					difftime = iterator.currentTimeStamp.realValue * formatBeatUnit(beatUnit) * (60 / beatSpeed);
+					fixtime += difftime;
+				}
+			}
+			// console.log(note.tie)
+			const nodeDetail = {
+				difftime,
+				octaveOffset: activeVerticalMeasureList[0]?.octaveOffset,
+				frequency: note.pitch?.frequency,
+				speed,
+				beatSpeed,
+				i,
+				si,
+				stepSpeeds,
+				measureOpenIndex: allMeasures.length - 1,
+				measures,
+				tempoInBPM: note.sourceMeasure.tempoInBPM,
+				measureLength,
+				relaMeasureLength,
+				id: svgElelent?.attrs.id,
+				note: note.halfTone + 12, // see issue #224
+				relativeTime: retain(relativeTime),
+				time: retain(relativeTime + fixtime),
+				endtime: retain(relaEndtime + fixtime),
+				relaEndtime: retain(relaEndtime),
+				realValue,
+				halfTone: note.halfTone,
+				noteElement: note,
+				svgElelent,
+				fixedKey,
+				realKey: 0,
+				duration: 0,
+				formatLyricsEntries: formatLyricsEntries(note),
+				stave: activeVerticalMeasureList[0]?.stave,
+				firstVerticalMeasure: activeVerticalMeasureList[0],
+				noteLength: 1,
+				osdmContext: osmd,
+				speedbeatUnit: beatUnit,
+			};
+			nodeDetail.realKey = formatRealKey(note.halfTone - fixedKey * 12, nodeDetail);
+			nodeDetail.duration = nodeDetail.endtime - nodeDetail.time;
+			let tickables = activeVerticalMeasureList[0]?.vfVoices["1"]?.tickables || [];
+			if ([121].includes(state.subjectId)) {
+				tickables = note.sourceMeasure.verticalSourceStaffEntryContainers;
+			}
+			// console.log(note.sourceMeasure.MeasureNumberXML, note.sourceMeasure.verticalSourceStaffEntryContainers.length)
+			nodeDetail.noteLength = tickables.length || 1;
+			allNotes.push(nodeDetail);
+			allNoteId.push(nodeDetail.id);
+			measures.push(nodeDetail);
+			if (si < tickables.length - 1) {
+				si++;
+			} else {
+				si = 0;
+				relaMeasureLength = 0;
+				measures = [];
+			}
+		}
+		i++;
+	}
+	// 按照时间轴排序
+	const sortArray = allNotes.sort((a, b) => a.relativeTime - b.relativeTime).map((item, index) => ({ ...item, i: index }));
+	console.timeEnd("音符跑完时间");
+	try {
+		osmd.cursor.reset();
+	} catch (error) {}
+	return sortArray;
+};

+ 385 - 0
src/helpers/metronome.ts

@@ -0,0 +1,385 @@
+/**
+ * 曲谱节拍器
+ * auth: lsq
+ * time: 2022.11.14
+ */
+import { reactive, watch } from "vue";
+import {tickUrl as tick, tockUrl as tock} from "../constant/audios";
+import { browser } from "../utils";
+import state from "../state";
+type IOptions = {
+	speed: number;
+};
+const ac = window.AudioContext || (window as any).webkitAudioContext || (window as any).mozAudioContext || (window as any).msAudioContext;
+const browserInfo = browser()
+export const metronomeData = reactive({
+	disable: true,
+	lineShow: false,
+	isClick: false,
+	metro: null as unknown as Metronome,
+	metroList: [] as number[],
+	activeList: [] as number[],
+	metroMeasure: [] as any[],
+	activeIndex: null as unknown as number,
+	activeMetro: {} as any,
+});
+watch(
+	() => metronomeData.lineShow,
+	() => {
+		const img: HTMLElement = document.querySelector("#cursorImg-0")!;
+		if (img) {
+			if (metronomeData.lineShow) {
+				img.classList.add("lineHide");
+			} else {
+				img.classList.remove("lineHide");
+			}
+		}
+	}
+);
+
+class Metronome {
+	ctx = new ac();
+	playType = "tick";
+	source = null as any; // 创建音频源头
+	source1 = null as any;
+	source2 = null as any;
+
+	constructor(option?: IOptions) {}
+	init(times: any[]) {
+		this.calculation(times);
+		metronomeData.activeList = [];
+		return new Promise(async (resolve) => {
+			if (this.source1 && this.source2) return resolve(true);
+			this.source1 = await this.loadAudio1();
+			this.source2 = await this.loadAudio2();
+			resolve(true);
+		});
+	}
+
+
+	// 播放
+	sound = (currentTime: number) => {
+		// console.log("🚀 ~ currentTime", currentTime)
+		currentTime = setCurrentTime(currentTime)
+		let index = -1;
+		let activeMetro = -1;
+		for (let i = 0; i < metronomeData.metroList.length; i++) {
+			const item = metronomeData.metroList[i];
+
+			if (currentTime >= item) {
+				// console.log(currentTime , item)
+				index = i;
+				activeMetro = item;
+			} else {
+				break;
+			}
+		}
+		if (index > -1 && metronomeData.activeIndex !== index) {
+			metronomeData.activeIndex = index;
+			// console.log("播放", metronomeData.activeIndex);
+			metronomeData.activeMetro = this.getStep(activeMetro);
+			// console.log("🚀 ~ metronomeData.activeMetro",metronomeData.activeMetro.measureNumberIndex, metronomeData.activeMetro.index)
+			this.playAudio();
+			metronomeData.isClick = false;
+			return;
+		}
+		metronomeData.isClick = false;
+	};
+	// 播放
+	playAudio = () => {
+		this.source = this.ctx.createBufferSource();
+		this.source.buffer = metronomeData.activeMetro?.index === 0 ? this.source1 : this.source2;
+		const gainNode = this.ctx.createGain();
+		gainNode.gain.value = metronomeData.disable ? 0 : 0.4;
+		this.source.connect(gainNode);
+		gainNode.connect(this.ctx.destination);
+		this.source.start(0); //立即播放
+	};
+
+	// 切换
+	selectPlay() {}
+
+	loadAudio1 = async () => {
+		const audioUrl = tick; // "/tick.wav";
+		const res = await fetch(audioUrl);
+		const arrayBuffer = await res.arrayBuffer(); // byte array字节数组
+		// console.log("🚀 ~ arrayBuffer", arrayBuffer)
+		const audioBuffer = await this.ctx.decodeAudioData(arrayBuffer, function (decodeData) {
+			return decodeData;
+		});
+		return audioBuffer;
+	};
+	loadAudio2 = async () => {
+		const audioUrl = tock; //"/tock.wav";
+		const res = await fetch(audioUrl);
+		const arrayBuffer = await res.arrayBuffer(); // byte array字节数组
+		const audioBuffer = await this.ctx.decodeAudioData(arrayBuffer, function (decodeData) {
+			return decodeData;
+		});
+		return audioBuffer;
+	};
+	getStep(time: number) {
+		for (let i = 0; i < metronomeData.metroMeasure.length; i++) {
+			const list = metronomeData.metroMeasure[i];
+			const item = list.find((n: any) => n.time === time);
+			if (item) {
+				// console.log('index',item)
+				return item;
+			}
+		}
+		return {};
+	}
+
+	// 计算 所有的拍子的时间
+	calculation(times: any[]) {
+		// console.log("🚀 ~ times", times);
+		// 1.统计有多少小节
+		const measures: any[] = [];
+		let xmlNumber = -1;
+		for (let i = 0; i < times.length; i++) {
+			const note = times[i];
+			const measureNumberXML = note?.noteElement?.sourceMeasure?.MeasureNumberXML || -1;
+			// console.log("🚀 ~ note?.noteElement?.sourceMeasure", note?.noteElement?.sourceMeasure)
+			// console.log("🚀 ~ measureNumberXML", measureNumberXML, note)
+			// console.log("🚀 ~ measureNumberXML", note)
+			const measureListIndex = note?.noteElement?.sourceMeasure?.measureListIndex;
+			if (measureNumberXML > -1) {
+				if (measureNumberXML != xmlNumber) {
+					const m = {
+						measureNumberXML: measureNumberXML,
+						measureNumberIndex: measureListIndex,
+						numerator: note?.noteElement?.sourceMeasure?.ActiveTimeSignature?.numerator || 0,
+						start: note.measures[0].time,
+						end: note.measures[note.measures.length - 1].endtime,
+						time: note.measures[note.measures.length - 1].endtime - note.measures[0].time,
+						stave_x: note?.noteElement?.sourceMeasure?.verticalMeasureList?.[0]?.stave?.x || 0,
+						stepList: [] as number[],
+						svgs: [] as any[]
+					};
+					// 2.统计小节的拍数
+					// 3.统计小节的时长, 开始时间,结束时间
+					// console.log(measureNumberXML,note.measures, times.filter((n: any) => n?.noteElement?.sourceMeasure?.measureListIndex == measureListIndex))
+					if ([121].includes(state.subjectId)){
+						const _measures = times.filter((n: any) => n?.noteElement?.sourceMeasure?.measureListIndex == measureListIndex)
+						note.measures = _measures
+						m.start = note.measures[0].time;
+						m.end = note.measures[note.measures.length - 1].endtime;
+						m.time = note.measures[note.measures.length - 1].endtime - note.measures[0].time;
+						try {
+							const tickables = note.noteElement.sourceMeasure.verticalMeasureList.reduce((arr: any[], value: any) => {
+								arr.push(...value.vfVoices['1'].tickables)
+								return arr
+							},[])
+							const xList: any[] = [];
+							m.svgs = tickables.map((n: any) => {
+								const x = n.getBoundingBox().x
+								if (!xList.includes(x) && n.duration !== 'w'){
+									xList.push(x)
+									n._start_x = x
+									return n
+								}
+							}).filter(Boolean).sort((a: any, b: any) => a._start_x - b._start_x)
+							// console.log(measureNumberXML, m.svgs)
+						} catch (error) {
+							console.log(error)
+						}
+						m.stepList = calculateMutilpleMetroStep(note.measures, m);
+					} else {
+						m.stepList = calculateMetroStep(note.measures, m);
+					}
+					measures.push(m);
+					xmlNumber = measureNumberXML;
+				}
+			}
+		}
+		// console.log(measures, measures.length);
+
+		let metroList: number[] = [];
+		const metroMeasure: any[] = [];
+		// 4.按照拍数将时长平均分配
+		try {
+			for (let i = 0; i < measures.length; i++) {
+				const measure = measures[i];
+				const noteStep = measure.time / measure.numerator;
+				// console.log("🚀 ~ measure.measureNumberXML",measure.measureNumberXML, noteStep)
+				const WIDTH = [121].includes(state.subjectId) ? 95 : 100
+				const widthStep = WIDTH / (measure.numerator + 1);
+				metroMeasure[i] = [] as number[];
+				// console.log('stepList', [...measure.stepList], measure.measureNumberXML)
+				for (let j = 0; j < measure.numerator; j++) {
+					const time = noteStep * j + measure.start;
+					metroList.push(time);
+					let left = '';
+					if (measure.stepList[j]) {
+						left = measure.stepList[j] + 'px'
+					} else {
+						const preLeft = measure.stepList[j - 1]
+						left = !preLeft ? `${widthStep}%` : preLeft.toString().indexOf('%') > -1 ? `${preLeft} + ${widthStep}%` : `${preLeft}px + ${widthStep}%`;
+						measure.stepList[j] = left
+					}
+					metroMeasure[i].push({
+						index: j,
+						time,
+						// left: (measure.stepList[j] ? measure.stepList[j] + 'px' : (j + 1) * widthStep + '%'), 
+						left: left?.indexOf('%') > -1 ? `calc(${left})` : left, 
+						measureNumberXML: measure.measureNumberXML,
+					});
+				}
+			}
+		} catch (error) {
+			console.log(error);
+		}
+		// console.log(metroList, metroMeasure);
+		// 5.得到所有的节拍时间
+		metronomeData.metroList = metroList;
+		metronomeData.metroMeasure = metroMeasure;
+	}
+}
+
+// 计算拍子的时值
+function calculateMetroStep(arr: any[], m: any): number[] {
+	const step = m.time / m.numerator;
+	if (arr.length === 1) {
+		if (arr[0].svgElelent && !arr[0].svgElelent.isRest()) {
+			let bbox = arr[0]?.svgElelent?.getBoundingBox() || { x: 0 };
+			return [(bbox.x - m.stave_x) * 0.7]
+		}
+		return []
+	};
+	// console.log("🚀 ~ arr", arr, step, m.measureNumberXML);
+	let total = 0;
+	let notes: any[] = [];
+	let stepList: number[] = [];
+	for (let i = 0; i < arr.length; i++) {
+		const item = arr[i];
+		let noteTime = item.endtime - item.time;
+		total += noteTime;
+		// 大于一拍
+		let bbox = item?.svgElelent?.getBoundingBox() || { x: 0 };
+		// console.log(item?.svgElelent?.attrs?.el)
+		if (noteTime > step) {
+			const exceedStep = Math.floor(noteTime / step)
+			for(let j = 0; j < exceedStep; j++){
+				// console.log('超过一拍了:' + Math.floor(noteTime / step), notes, m.measureNumberXML)
+				total -= step;
+				if (j === 0){
+					let x = (bbox.x - m.stave_x) * 0.7;
+					if (notes.length > 0) {
+						bbox = notes[0]?.svgElelent?.getBoundingBox() || { x: 0 };
+						x = (bbox.x - m.stave_x) * 0.7;
+					}
+					stepList.push(x);
+					notes = []
+				} else {
+					let x: any = undefined
+					stepList.push(x);
+				}
+				
+			}
+		} else {
+			notes.push(item);
+		}
+		// console.log(notes)
+		if (Math.abs(total - step) < 0.001) {
+			let x = (bbox.x - m.stave_x) * 0.7;
+			if (notes.length > 0) {
+				bbox = notes[0]?.svgElelent?.getBoundingBox() || { x: 0 };
+				x = (bbox.x - m.stave_x) * 0.7;
+			}
+			// console.log("一拍", notes, m.measureNumberXML);
+			stepList.push(x);
+			total = 0;
+			notes = [];
+		}
+	}
+	stepList = stepList.reduce((list: any[], n: number) => {
+		if (list.includes(n)){
+			list.push(undefined as any)
+		} else {
+			list.push(n)
+		}
+		return list
+	}, [])
+	// console.log('stepList', [...stepList], m.measureNumberXML)
+	return stepList;
+}
+// 计算单声部多声轨的拍子的时值
+function calculateMutilpleMetroStep(arr: any[], m: any): number[] {
+	// console.log("🚀 ~ m:", [...m.svgs])
+	const step = m.time / m.numerator;
+	const measure_bbox = arr[0]?.svgElelent?.attrs?.el?.parentElement?.parentElement?.getBoundingClientRect?.() || {x: 0}
+	if (arr.length === 1) {
+		const staveNote = m.svgs[0];
+		// 大于一拍
+		let bbox = staveNote?.attrs?.el?.getBoundingClientRect?.() || { x: 0 };
+		if (staveNote && !staveNote.isRest()) {
+			return [bbox.x - measure_bbox.x]
+		}
+		return []
+	};
+	// console.log("🚀 ~ arr", arr, step, m.measureNumberXML);
+	let total = 0;
+	let notes: any[] = [];
+	let stepList: number[] = [];
+	for (let i = 0; i < arr.length; i++) {
+		const item = arr[i];
+		item._index = i
+		const noteTime = item.endtime - item.time;
+		total += noteTime;
+		let svgEle = m.svgs[i]?.attrs?.el;
+		// 大于一拍
+		let bbox = svgEle?.getBoundingClientRect?.() || { x: 0 };
+		// console.log(m.measureNumberXML, svgEle, i)
+		if (noteTime > step) {
+			total -= step;
+			// console.log('超过一拍了', notes, m.measureNumberXML)
+			let x = bbox.x - measure_bbox.x;
+			if (notes.length > 0) {
+				svgEle = m.svgs[notes[0]._index]?.attrs?.el
+				bbox = svgEle?.getBoundingClientRect?.() || { x: 0 };
+				x = bbox.x - measure_bbox.x;
+			}
+			stepList.push(x);
+			notes = []
+		} else {
+			notes.push(item);
+		}
+		// console.log(notes)
+		if (Math.abs(total - step) < 0.001) {
+			let x = bbox.x - measure_bbox.x;
+			if (notes.length > 0) {
+				svgEle = m.svgs[notes[0]._index]?.attrs?.el
+				bbox = svgEle?.getBoundingClientRect?.() || { x: 0 };
+				x = bbox.x - measure_bbox.x;
+			}
+			// console.log("一拍",svgEle,notes,m.svgs, m.measureNumberXML);
+			stepList.push(x);
+			total = 0;
+			notes = [];
+		}
+	}
+	stepList = stepList.reduce((list: any[], n: number) => {
+		if (list.includes(n)){
+			list.push(undefined as any)
+		} else {
+			list.push(n)
+		}
+		return list
+	}, [])  //Array.from(new Set(stepList))
+	// console.log('stepList', stepList, m.measureNumberXML)
+	return stepList;
+}
+
+// 延迟兼容处理
+function setCurrentTime(time: number){
+	if (browserInfo.huawei || browserInfo.xiaomi){
+		time += 0.125
+	} else if (browserInfo.android) {
+		time += 0.11
+	} else if (browserInfo.ios){
+		time += 0.01
+	}
+	return time
+}
+
+export default Metronome;

+ 382 - 0
src/helpers/multiple-audio.ts

@@ -0,0 +1,382 @@
+/** 播放多个音频 */
+export default class MultipleAudio {
+
+  audios: any = {}
+
+  audioList: string[] = []
+
+  length: number = 0
+
+  status: 'init' | 'play' | 'pause' = 'init'
+
+  speed: number = 90
+
+  muted: boolean = false
+
+  audio: null | HTMLAudioElement = null
+
+
+  // group = new Pizzicato.Group([])
+
+  currentTime: number = 0
+
+  duration: number = 0
+
+  timer: any = null
+
+  accelerateRefreshPlayer = () => {
+    if (this.timer) {
+      return
+    }
+    const prevTime = this.currentTime
+    let now = new Date().getTime()
+    this.timer = setInterval(() => {
+      this.currentTime = (new Date().getTime() - now) / 1000 + prevTime
+      // console.log(this.currentTime)
+      this.event.emit('timeupdate', this)
+    }, 10)
+  }
+
+  clearAccelerateRefreshPlayer = () => {
+    clearInterval(this.timer)
+    this.timer = null
+  }
+
+  constructor(list: string[]) {
+    this.setSongs(list)
+    // this.event.on('timeupdate', () => {
+    //   console.log(this.currentTime)
+    // })
+  }
+
+  async setSongs(list: string[]) {
+    this.audioList = list.filter(item => !!item)
+    this.audio = null
+    this.audios = {}
+    const filterReqs = list.filter(item => !!item).map(async url => ({
+      // bolb: await request.get(url, {responseType: 'blob'}),
+      url
+    }))
+    const res = await Promise.all(filterReqs)
+    for (const item of res) {
+      const audio = new Audio(item.url)
+      // audio.controls = true
+      // document.body.append(audio)
+      audio.load()
+      this.audios[item.url] = audio
+      if (!this.audio) {
+        this.audio = audio
+      }
+    }
+    // console.log(filterReqs.length, this.audio)
+    this.length = filterReqs.length
+    if (this.audio) {
+      this.audio.addEventListener('loadedmetadata', evt => {
+        // this.duration = this.audio?.duration || 0
+        // this.event.emit('loadedmetadata', evt, this.audio?.duration)
+        this.setDuration()
+      })
+      this.audio.addEventListener('timeupdate', evt => {
+        this.currentTime = this.audio?.currentTime || 0
+        this.event.emit('timeupdate', evt)
+        let used = false
+        if (this.currentTime === this.duration && !used) {
+          used = true
+          // this.event.emit('ended')
+          // this.audio?.dispatchEvent(endedEvent)
+          // for (const key in this.audios) {
+          //   if (Object.prototype.hasOwnProperty.call(this.audios, key)) {
+          //     const audio = this.audios[key]
+          //     const endedEvent = new Event('ended')
+          //     audio.dispatchEvent(endedEvent)
+          //   }
+          // }
+          // console.log('ended')
+        }
+      })
+      // this.audio.addEventListener('ended', () => {
+      //   this.setCurrentTime(0)
+      //   // this.play()
+      // })
+    }
+    if (list.length) {
+      this.status = this.getStatus()
+    }
+    this.event.on('allWaiting', () => {
+      if (this.hasWaitng()) {
+        this.event.emit('waiting')
+      }
+    })
+    this.event.on('allPlaying', () => {
+      if (!this.hasWaitng()) {
+        this.event.emit('playing')
+      }
+    })
+    this.syncEvent()
+  }
+
+  setDuration(aus?: any) {
+    const audios: HTMLAudioElement[] = Object.values(aus || this.audios || {})
+    if (audios.length) {
+      const times: number[] = []
+      for (const item of audios) {
+        const duration = (item as HTMLAudioElement).duration
+        if (duration > 0) {
+          times.push(duration)
+        }
+      }
+      const num = Math.floor(Math.max(...times) -  Math.min(...times))
+      if (num >= 1){
+        console.log('该教程原音与伴奏时长超过' + num + '秒,请修改后使用')
+        // Dialog.alert({
+        //   message: '该教程原音与伴奏时长超过' + num + '秒,请修改后使用',
+        // })
+      }
+      this.duration = Math.min(...times)
+
+      if (this.duration > 0) {
+        this.event?.emit('loadedmetadata', null, this.duration)
+      }
+    }
+  }
+
+  destroyed() {
+    this.pause()
+    this.event.removeAllListeners()
+    this.audio = null
+    // Object.values(this.audios).map(item => item.remove())
+    this.audios = {}
+  }
+
+  hasWaitng() {
+    let status = false
+    for (const audio of Object.values(this.audios)) {
+      if ((audio as any).dataset.status === 'waiting') {
+        status = true
+        break
+      }
+    }
+    return status
+  }
+
+  syncEvent() {
+    let isEnded = false
+    const play = (evt: Event) => {
+      isEnded = false
+      // console.log('实际延迟', new Date().getTime() - starttime)
+      // console.log('开始触发play事件', new Date().getTime())
+      this.event.emit('play', evt)
+      // console.log(this.audioList[0])
+      if (compareURL((evt.target as HTMLAudioElement)?.src, this.audioList[0])) {
+        playStartTime = new Date().getTime()
+      }
+      // this.play()
+    }
+    const pause = async (evt: Event) => {
+      await this.pause()
+      this.event.emit('pause', evt)
+      if (compareURL((evt.target as HTMLAudioElement)?.src, this.audioList[0])) {
+        const playTime = new Date().getTime() - playStartTime
+        playStartTime = new Date().getTime()
+        this.event.emit('updatePlayTime', playTime / 1000)
+      }
+    }
+    const waiting = (evt: any) => {
+      if (this.status === 'play') {
+        evt.target.dataset.status = 'waiting'
+      }
+      this.event.emit('allWaiting')
+    }
+    const playing = (evt: any) => {
+      evt.target.dataset.status = ''
+      this.event.emit('allPlaying')
+    }
+    const ended = async (evt: Event) => {
+      if (!isEnded) {
+        isEnded = true
+        await this.pause()
+      }
+      for (const key in this.audios) {
+        if (Object.prototype.hasOwnProperty.call(this.audios, key)) {
+          this.event.emit('ended', {
+            target: this.audios[key]
+          })
+          // const audio = this.audios[key]
+          // const endedEvent = new Event('ended')
+          // audio.dispatchEvent(endedEvent)
+        }
+      }
+      // this.event.emit('ended', evt)
+    }
+    // for (const audio of Object.values(this.audios)) {
+    //   audio.removeEventListener('play', play)
+    //   audio.removeEventListener('pause', pause)
+    //   audio.removeEventListener('waiting', waiting)
+    //   audio.removeEventListener('playing', playing)
+    //   audio.removeEventListener('ended', ended)
+    // }
+    for (const audio of Object.values(this.audios)) {
+      (audio as HTMLAudioElement).addEventListener('loadedmetadata', () => this.setDuration(this.audios))
+      ;(audio as HTMLAudioElement).addEventListener('play', play)
+      ;(audio as HTMLAudioElement).addEventListener('pause', pause)
+      ;(audio as HTMLAudioElement).addEventListener('waiting', waiting)
+      ;(audio as HTMLAudioElement).addEventListener('playing', playing)
+      ;(audio as HTMLAudioElement).addEventListener('ended', ended)
+    }
+      // this.audio?.addEventListener('play', play)
+      // this.audio?.addEventListener('pause', pause)
+      // this.audio?.addEventListener('waiting', waiting)
+      // this.audio?.addEventListener('playing', playing)
+      // this.audio?.addEventListener('ended', ended)
+  }
+
+  getStatus() {
+    return !this.audio ? 'init' : this.audio?.paused ? 'pause' : 'play'
+  }
+
+  play(delay?: number) {
+    let plused = false
+    if (this.getStatus() !== 'play') {
+      return new Promise((resolve) => {
+        setTimeout(() => {
+          starttime = new Date().getTime()
+          Object.values(this.audios).map(async (item: any, inedx: number) => {
+            // console.log('play duration', item.duration)
+            await item.play()
+            if (!plused) {
+              plused = true
+              // console.log('starttime', starttime)
+              // console.log('延迟时间', new Date().getTime() - starttime)
+              // runtime.evaluatingFixTime += new Date().getTime() - starttime
+            }
+          })
+          resolve(this.audios)
+          // Promise.all(Object.values(this.audios).map(async (item: any) => await item.play()))
+          //   .then(res => {
+          //     this.status = this.getStatus()
+          //     resolve(res)
+          //     return res
+          //   })
+        }, (delay || 100))
+      })
+    }
+    this.status = this.getStatus()
+    return Promise.resolve()
+  }
+
+  pause() {
+    this.status = this.getStatus()
+    return Promise.all(Object.values(this.audios).map(async (item: any) => await item.pause()))
+    .then(res => {
+      this.status = this.getStatus()
+      return res
+    })
+  }
+
+  setVolume = (none: boolean, cb: () => void) => {
+    let timer = setInterval(() => {
+      Object.values(this.audios).map((item: any) => {
+        if (none) {
+          item.volume -= 0.01
+          if (item.volume <= 0.01) {
+            item.volume = 0
+            clearInterval(timer)
+            cb && cb()
+          }
+        } else {
+          item.volume += 0.01
+          if (item.volume >= 1) {
+            item.volume = 1
+            clearInterval(timer)
+            cb && cb()
+          }
+        }
+        console.log(item.volume)
+      })
+    }, 16.7)
+  }
+
+  setMute(muted: boolean, url?: string) {
+    if (url) {
+      if (this.audios[url]) {
+        // this.setVolume(muted, () => this.audios[url].muted = muted)
+        this.audios[url].muted = muted
+        // this.audios[url].volume = (muted ? 0 : 1)
+      }
+    } else {
+      this.muted = muted
+      // this.setVolume(muted, () => {
+        Object.values(this.audios).map((item: any) => item.muted = muted)
+      // })
+    }
+  }
+
+  setSpeed(speed: number, url?: string) {
+    if (url) {
+      if (this.audios[url]) {
+        this.audios[url].playbackRate = speed
+        // this.audios[url].speed = speed
+      }
+    } else {
+      this.speed = speed
+      // this.group.speed = speed
+      Object.values(this.audios).map((item: any) => {
+        item.playbackRate = speed
+        // console.log(item.getSourceNode().playbackRate.value = speed)
+        return item
+      })
+    }
+  }
+
+
+  setCurrentTime(time: number) {
+    this.currentTime = time
+    // if (this.status === 'play') {
+    //   this.pause()
+    //   this.play()
+    // }
+    Object.values(this.audios).map((item: any) => item.currentTime = time)
+  }
+
+  toggleMute(url?: string) {
+    if (url) {
+      if (this.audios[url]) {
+        this.audios[url].muted = !this.audios[url].muted
+      }
+    } else {
+      Object.values(this.audios).map((item: any) => item.muted = !this.muted)
+      this.muted = !this.muted
+    }
+  }
+
+  togglePlay(delay?: number) {
+    if (this.getStatus() === 'pause') {
+      this.play(delay)
+    } else if (this.getStatus() === 'play') {
+      this.setMute(true)
+      this.pause()
+    }
+  }
+}
+
+export interface IAudios {
+  [key: string]: HTMLAudioElement
+}
+
+export interface IMultipleAudio {
+  audios: IAudios
+  audio: null | HTMLAudioElement
+  status: string
+  muted: boolean
+  speed: number
+  length: number
+  event: EventEmitterType
+  play(): void
+  pause(): void
+  setMute(muted: boolean, url?: string): void
+  setSpeed(speed: number, url?: string): void
+  togglePlay(): void
+  toggleMute(): void
+  setCurrentTime(time: number): void
+  destroyed: () => void
+}

+ 0 - 948
src/helpers/runtime.ts

@@ -1,948 +0,0 @@
-/**
- * 播放过程中必要的context,效果与state一致仅用于操作,但是包含方法,好处是可以在任意地方调用,操作播放状态
- */
-
-import { Toast } from 'vant'
-import { reactive, watchEffect } from 'vue'
-import store from 'store'
-import { constant, throttle } from 'lodash'
-import { getActtiveNoteByTimes, getDuration, getIndex, getNoteBySlursStart, setStepIndex, getSlursNote, getVoicePartInfo, getTone, formatBeatUnit } from './helpers'
-import SectionHint from '/src/helpers/section-hint'
-import { browser, findNearestNumber, formatTime, getPlatform, JavaScriptAnimate } from '/src/helpers/utils'
-import request from '/src/helpers/request'
-// import * as Tone from 'tone'
-import router from '/src/router'
-import detailState from './state'
-import SettingState from '/src/pages/detail/setting-state'
-// import fixtimeRela from '/src/constant/fixtime'
-import TickPlayer, { getTickTime } from '/src/components/tick/player'
-// import { tickByNumerator } from '/src/components/tick/constant'
-import { postMessage, IPostMessage, listenerMessage, promisefiyPostMessage } from '/src/helpers/native-message'
-import { OpenSheetMusicDisplay } from '/osmd-extended/src'
-
-import EventEmitter from 'eventemitter3'
-import { metronomeData } from '/src/helpers/metronome'
-
-export const event = new EventEmitter()
-
-const browserInfo = browser()
-
-const getLinkId = (): string => {
-  return location.hash.split('?')[0].split('/').pop() || ''
-}
-
-export const getFixtimeRelaVal = () => {
-  const route: any = router.currentRoute.value
-  const linkId = location.hash.split('?')[0].split('/').pop()
-  return 0//(fixtimeRela as any)[(route.params.id || linkId)] || 0
-}
-
-export const getFixTime = (speed: number) => {
-  const duration: any = getDuration(state.osmd as OpenSheetMusicDisplay)
-  // console.log(duration, 'duration', state.osmd)
-  let numerator = duration.numerator || 0
-  let denominator = duration.denominator || 4
-  const beatUnit = duration.beatUnit || 'quarter'
-  if (detailState.repeatedBeats) {
-    // 音频制作问题仅2拍不重复
-    numerator = numerator === 2 ? 4 : numerator
-  }
-  // console.log('diff', speed, duration, formatBeatUnit(beatUnit), denominator, numerator, (numerator / denominator))
-  return !detailState.needTick && !detailState.skipTick ? (60 / speed * formatBeatUnit(beatUnit)) * (numerator / denominator) : 0
-}
-
-let prevIndex: number = 0
-
-export type IPlayState = 'init' | 'play' | 'pause' | 'suspend'
-
-export type IMode = 'background' | 'music'
-
-export type ISonges = {
-  background?: string,
-  music?: string,
-}
-
- const state = reactive({
-  /** 曲目信息 */
-  songs: {} as ISonges,
-  /** 播放状态 */
-  playState: 'init' as IPlayState,
-  /** 当前播放背景 */
-  sectionHint: new SectionHint(),
-  /** 音频播放器实例 */
-  audiosInstance: null as any,
-  /** 原声伴奏 */
-  mode: 'music' as IMode,
-  /** 是否是第一次播放 */
-  isFirstPlay: true,
-  metro: null as any,
-  metroing: false,
-  /** 时长 */
-  duration: '0:00',
-  durationNum: 0,
-  /** 当前时间 */
-  currentTime: '0:00',
-  currentTimeNum: 0,
-  loading: false,
-  /** 速度 */
-  speed: 90,
-  browser: browser(),
-  /** 调速是否可见 */
-  speedShow: false,
-  /** 播放进度进度是否可见 */
-  progressShow: false,
-  touched: false,
-  /** opendisplaymusicdisplay 实例 */
-  osmd: undefined as unknown as OpenSheetMusicDisplay,
-  /** 节拍器实例 */
-  tickPlayer: null as any,
-  /** 评测状态 */
-  evaluatingStatus: false,
-  /** 评测提示 */
-  evaluatingTips: false,
-  clickTime: 0,
-  evaluatingFixTime: 0,
-  /** 摄像头状态 */
-  cameraStatus: false,
-  /** 录制状态 */
-  captureStatus: false,
-  ticking: false,
-  /** 第几分轨 */
-  partIndex: 0,
-  /** 当前光标索引位置,隐藏时也有值 */
-  activeIndex: 0,
- })
-
- const syncStepIndex = (i: number) => {
-   if (state.osmd!.cursor.Hidden !== false) {
-    state.osmd!.cursor.show()
-    // state.osmd.cursor.reset()
-   }
-  //  console.log(state.osmd.cursor)
-   prevIndex = i
-   setStepIndex(state.osmd, i)
-   refreshIndex(detailState.times[i]?.time)
-  // refreshIndexBase(i)
- }
-
- watchEffect(() => {
-  detailState.maskStatus = state.playState === 'play'
-})
-
-const syncPlayState = async () => {
-  if (detailState.activeDetail.isAppPlay) {
-    const cloudGetMediaStatus = await promisefiyPostMessage({
-      api: 'cloudGetMediaStatus'
-    })
-    const status = cloudGetMediaStatus?.content.status
-    state.playState = status
-  } else {
-    state.playState = state.audiosInstance.getStatus()
-  }
-}
-
- export default state
-
- export const setCurrentTime = (time: number) => {
-   console.log('setCurrentTime', time)
-  const fixt = time// - 5
-  // console.log('set', fixt)
-  detailState.fixedKey = 0
-  state.currentTimeNum = fixt
-  state.currentTime = formatTime(fixt)
-  state.audiosInstance.setCurrentTime(fixt)
-  promisefiyPostMessage({
-    api: 'cloudSetCurrentTime',
-    content: {
-      currentTime: time * 1000,
-      songID: detailState.activeDetail.examSongId,
-    }
-  })
-  refreshView()
-  syncPlayState()
-  const index = getIndex(detailState.times, state.currentTimeNum)
-  // console.log(index)
-  syncStepIndex(index)
-
-  // changeMode(state.mode)
-}
-
- export const onTimeChanged = (num: number) => {
-  const time = Math.min(num, state.durationNum)
-  const index = getIndex(detailState.times, state.currentTimeNum)
-  setCurrentTime(time)
-  syncStepIndex(index)
-  // console.log('onTimeChanged', time)
-  // changeMode(state.mode)
-}
-
-export const changeMode = (val: IMode) => {
-  state.mode = val
-  const cm: IMode = val === 'background' ? 'music' : 'background'
-  state.audiosInstance?.setMute(true, state.songs[cm])
-  state.audiosInstance?.setMute(false, state.songs[val])
-  if (detailState.activeDetail.isAppPlay) {
-    const data = new Map()
-    for (const name of detailState.partListNames) {
-      data.set(name, 60)
-    }
-    for (const name of getVoicePartInfo().partListNames) {
-      data.set(name, (cm === 'background' ? 100 : 0))
-    }
-    promisefiyPostMessage({
-      api: 'cloudVolume',
-      content: {
-        parts: Array.from(data.keys()).map(item => ({
-          name: item,
-          volume: data.get(item),
-        })),
-      }
-    })
-    // promisefiyPostMessage({
-    //   api: 'cloudSwitch',
-    //   content: {
-    //     songID: detailState.activeDetail.examSongId,
-    //     parts: cm === 'background' ? [] : getVoicePartInfo().partListNames,
-    //   }
-    // })
-  }
-}
-
-export const changeAllMode = () => {
-  state.mode = 'background'
-  state.audiosInstance?.setMute(true)
-}
-
-export const changeSpeed = (speed: number) => {
-  // const route: any = router.currentRoute.value
-  const speeds = store.get('speeds') || {}
-  speeds[getLinkId()] = speed
-  store.set('speeds', speeds)
-  state.speed = speed
-  state.audiosInstance?.setSpeed(speed/ detailState.baseSpeed)
-  console.log('速度倍率', speed/ detailState.baseSpeed)
-  promisefiyPostMessage({
-    api: 'cloudChangeSpeed',
-    content: {
-      speed: speed,
-      originalSpeed: detailState.activeDetail.originalSpeed,
-      songID: detailState.activeDetail.examSongId,
-    }
-  })
-  // changeMode(state.mode)
-  if (state.playState === 'play') {
-    syncStepIndex(getIndex(detailState.times, state.currentTimeNum))
-  }
-}
-
-let nextLook: boolean = false
-
-let syncTimed: boolean = false
-
-export const resetCursor = () => {
-  if (state.osmd) {
-    state.osmd.cursor.reset()
-    state.osmd.cursor.hide()
-    detailState.fixedKey = 0
-  }
-}
-
-export const refreshIndexBase = (index: number) => {
-  if (index < 0) return
-  const { osmd }: any = state
-  if (osmd) {
-    if (detailState.times[index]) {
-      if (!detailState.sectionStatus) {
-        state.sectionHint.show()
-      }
-      if (detailState.times[index] && detailState.times[index].noteElement) {
-        if (!detailState.sectionStatus) {
-          state.sectionHint.showForElement(detailState.times[index])
-        }
-        // resetElementColor()
-      }
-      if (osmd.cursor.hidden !== false) {
-        osmd.cursor.reset()
-        osmd.cursor.show()
-        detailState.fixedKey = 0
-      }
-      if (prevIndex !== index) {
-        setStepIndex(state.osmd, detailState.times[index].i, prevIndex)
-        prevIndex = index
-      }
-      detailState.fixedKey = detailState.times[index].realKey
-    }
-  }
-}
-
-export const refreshIndex = (ctime?: number) => {
-  const { osmd }: any = state
-  if (osmd && (ctime || state.audiosInstance.audio)) {
-    const currentTimeNum = ctime || (state.audiosInstance.audio as HTMLAudioElement).currentTime;
-    // console.log("🚀 ~ ctime", currentTimeNum)
-    
-    const index = getIndex(detailState.times, currentTimeNum)
-
-    state.activeIndex = index
-    removeRepateBackground(index)
-    // console.log(currentTimeNum, index, detailState.times[detailState.times.length - 1]?.endtime)
-    const lastNote = detailState.times[detailState.times.length - 1]
-    const endtime = lastNote?.sourceEndTime || lastNote?.endtime
-    if (currentTimeNum > endtime) {
-      state.osmd!.cursor.hide()
-      state.sectionHint.destroy()
-    } else {
-      if (detailState.times[index]) {
-        refreshIndexBase(index)
-      }
-    }
-  }
-}
-
-/** 重复中移除重复的背景 */
-export const removeRepateBackground = (index: number) => {
-  if (state.evaluatingStatus && index) {
-    const activeNote = detailState.times[index]
-    const note = detailState.times[index + 1] || activeNote
-    const number = note?.noteElement?.sourceMeasure?.measureListIndex
-    // 0 比较特殊重置等操作会导致误操作所以跳过第0个
-    if (note && detailState.evaluatings[number] && index > 0) {
-      detailState.evaluatings = {
-        ...detailState.evaluatings,
-        [number]: undefined
-      }
-    }
-  }
-}
-
-export const refreshPlayer = async (ctime?: number) => {
-  // if (!state.durationNum) {
-  //   loadedmetadata()
-  // }
-  // const status: IPlayState = state.audiosInstance.getStatus()
-  const { osmd }: any = state
-  if (osmd && (ctime || state.audiosInstance.audio)) {
-    // state.playState = status
-    const currentTimeNum = ctime || (state.audiosInstance.audio as HTMLAudioElement).currentTime
-    try {
-      metronomeData?.metro?.sound(currentTimeNum);
-    } catch (error) {}
-    // console.log('refreshPlayer', currentTimeNum)
-    const mintime = 0//detailState.times[0].time
-    if (currentTimeNum + 1 < mintime) {
-      setCurrentTime(mintime)
-      return
-    } else {
-      syncTimed = false
-    }
-    const nextTime = () => {
-      if (detailState.sectionStatus && detailState.section.length === 2) {
-        // console.log("state.currentTimeNum >= detailState.section[0].time", state.currentTimeNum, detailState.section[0].time)
-        if (currentTimeNum >= detailState.section[0].time) {
-          detailState.sectionFlash = false
-        }
-        const nextNote = detailState.times[detailState.section[1].i + 1]
-        const time = nextNote ? nextNote.halfTone === 0 ? detailState.section[1].endtime : nextNote.time : state.durationNum
-        // console.table({
-        //   currentTimeNum,
-        //   isNext: (browserInfo.xiaomi ? 0.2 : 0.08) >= time,
-        //   time,
-        //   nextNote
-        // })
-        return currentTimeNum + (browserInfo.xiaomi ? 0.2 : 0.08) >= time
-      }
-      return false
-    }
-    const isNext = nextTime()
-    if (isNext) {
-      // console.log("isNext", detailState.section[1], detailState.section[1].endtime, currentTimeNum)
-      state.audiosInstance.setMute(true)
-      state.osmd!.cursor.hide()
-      // resetPlayStatus()
-      if (detailState.activeDetail?.isAppPlay) {
-        pause()
-      } else {
-        await state.audiosInstance.pause()
-      }
-      // setCurrentTime(detailState.section[0].sourceStartTime || detailState.section[0].time)
-      setSectionModeCurrentTime()
-      clearAccelerateRefreshPlayer()
-      setTimeout(() => setPlayState(), 1000)
-      state.loading = false
-      return
-    }
-    // if (!state.touched) {
-    //   state.currentTimeNum = currentTimeNum
-    //   state.currentTime = formatTime((state.audiosInstance.audio as HTMLAudioElement).currentTime)
-    // }
-  }
-}
-
-export const resetPlayStatus = async (notStop?: boolean) => {
-  try {
-    prevIndex = 0
-    state.osmd!.cursor.reset()
-    state.osmd!.cursor.hide()
-    detailState.fixedKey = 0
-    detailState.sectionFlash = false
-    if (state.sectionHint) {
-      state.sectionHint.destroy()
-    }
-    if (!notStop) {
-      if (detailState.activeDetail.isAppPlay) {
-        await promisefiyPostMessage({
-          api: 'cloudSuspend',
-          content: {
-            songID: detailState.activeDetail.examSongId,
-          }
-        })
-      } else {
-        state.audiosInstance.pause()
-      }
-    }
-    syncPlayState()
-  } catch (error) {}
-}
-
-export const play = async () => {
-  if (state.isFirstPlay) {
-    resetPlayStatus()
-    detailState.fixedKey = 0
-  }
-  if (detailState.activeDetail.isAppPlay) {
-    await syncPlayState()
-    promisefiyPostMessage({
-      api: 'cloudSuspend',
-      content: {
-        songID: detailState.activeDetail.examSongId,
-      }
-    })
-  } else {
-    state.playState = state.audiosInstance.getStatus()
-    clearAccelerateRefreshPlayer()
-    accelerateRefreshPlayer()
-    startCapture()
-  }
-}
-
-export const pause = async () => {
-  if (detailState.sectionStatus) {
-    state.osmd!.cursor.hide()
-  }
-  if (detailState.activeDetail.isAppPlay) {
-    await syncPlayState()
-    promisefiyPostMessage({
-      api: 'cloudSuspend'
-    })
-  } else {
-    state.playState = state.audiosInstance.getStatus()
-    clearAccelerateRefreshPlayer()
-    setTimeout(() => {
-      // 确保播放完毕,延迟200ms
-      endCapture()
-    }, 200)
-  }
-}
-
-export const waiting = () => {
-  state.loading = true
-}
-
-export const playing = () => {
-  state.loading = false
-}
-
-export const ended = async (evt: Event) => {
-  resetPlayStatus()
-  detailState.fixedKey = 0
-  if (!state.evaluatingStatus) {
-    refreshPlayer(0)
-    if (SettingState.sett.loop) {
-      await setPlayState()
-    }
-  }
-  setCurrentTime(0)
-  event.emit('ended', evt)
-}
-
-let timer: any = null
-
-let now = 0
-
-let nowTime = 0
-
-let prevTime: number = 0
-
-const accelerateRefreshPlayer = () => {
-  if (timer || !state.audiosInstance) {
-    return
-  }
-  timer = setInterval(() => {
-    requestAnimationFrame(() => {
-      refreshPlayer()
-      if (state.audiosInstance.getStatus() === 'play') {
-        refreshIndex()
-      }
-    })
-  }, 16.7)
-}
-
-const clearAccelerateRefreshPlayer = () => {
-  clearInterval(timer)
-  timer = null
-  now = 0
-}
-
-export const sectionChange = () => {
-  detailState.sectionStatus = !detailState.sectionStatus
-  clearAccelerateRefreshPlayer()
-  resetPlayStatus()
-  if (!detailState.sectionStatus) {
-    setCurrentTime(0)
-    detailState.fixedKey = 0
-  }
-  if (detailState.sectionStatus && detailState.section.length != 2) {
-    resetCursor()
-  }
-}
-
-export const clearSectionStatus = () => {
-  detailState.section = []
-  detailState.sectionBoundingBoxs = []
-  detailState.sectionStatus = false
-}
-
-export const getFirsrNoteByMeasureOpenIndex = (index: number, tie = true) => {
-  for (const note of detailState.times) {
-    if (note?.measureOpenIndex === index) {
-      let noteTies: any = null
-      // console.log(note.measures)
-      for (const item of note.measures) {
-        const tone = getTone(item)
-        // console.log('tone', tone)
-        if (tone) {
-          noteTies = tone
-        }
-      }
-      if (noteTies) {
-        if (noteTies?.measureOpenIndex !== index) {
-          for (const n of detailState.times) {
-            if (n.noteElement === noteTies) {
-              return n
-            }
-          }
-        }
-      }
-      return note
-    }
-  }
-  return null
-}
-
-export const getFirsrNoteByMeasureListIndex = (index: number, tie = true) => {
-  for (const note of detailState.times) {
-    if (note?.noteElement?.sourceMeasure?.measureListIndex === index) {
-      let noteTies: any = null
-      // console.log(note.measures)
-      for (const item of note.measures) {
-        const tone = getTone(item)
-        // console.log('tone', tone)
-        if (tone) {
-          noteTies = tone
-        }
-      }
-      if (noteTies) {
-        if (noteTies.sourceMeasure?.measureListIndex !== index) {
-          for (const n of detailState.times) {
-            if (n.noteElement === noteTies) {
-              return n
-            }
-          }
-        }
-      }
-      return note
-    }
-  }
-  return null
-}
-
-export const setSectionModeCurrentTime = () => {
-  // console.log(detailState.needTick, 'setSectionModeCurrentTime', detailState.section[0])
-  if (detailState.needTick) {
-    setCurrentTime(detailState.section[0].sourceStartTime || detailState.section[0].time)
-  } else {
-    const measureListIndex = detailState.section[0].noteElement?.sourceMeasure?.measureListIndex
-    if (measureListIndex > 0) {
-      setCurrentTime(getFirsrNoteByMeasureOpenIndex(detailState.section[0].measureOpenIndex -1).time)
-      detailState.sectionFlash = true
-    } else {
-      setCurrentTime(0)
-    }
-  }
-}
-
-export const setPlayerView = () => {
-  // console.log(detailState.sectionStatus, 'detailState.sectionStatus')
-  // console.log(detailState.needTick)
-  if (detailState.sectionStatus) {
-    syncStepIndex(getIndex(detailState.times, state.currentTimeNum))
-    if (detailState.section.length === 2) {
-      // setCurrentTime(detailState.section[0].sourceStartTime || detailState.section[0].time)
-      setSectionModeCurrentTime()
-    } else {
-      detailState.section = []
-      detailState.sectionBoundingBoxs = []
-      detailState.sectionStatus = false
-      Toast.clear()
-    }
-  }
-}
-
-const cloudToggleState = async () => {
-  const cloudGetMediaStatus = await promisefiyPostMessage({
-    api: 'cloudGetMediaStatus'
-  })
-  const status = cloudGetMediaStatus?.content.status
-  if (status === 'init') {
-    return
-  }
-  if (status === 'suspend') {
-    await promisefiyPostMessage({
-      api: 'cloudPlay',
-      content: {
-        songID: detailState.activeDetail.examSongId,
-        startTime: state.currentTimeNum * 1000,
-        originalSpeed: detailState.activeDetail.originalSpeed,
-        speed: state.speed,
-        hertz: SettingState.sett.hertz,
-      }
-    })
-  } else {
-    await promisefiyPostMessage({
-      api: 'cloudSuspend'
-    })
-  }
-  const cloudGetMediaStatused = await promisefiyPostMessage({
-    api: 'cloudGetMediaStatus'
-  })
-  state.playState = cloudGetMediaStatused?.content.status
-  console.log(cloudGetMediaStatused, 'cloudGetMediaStatused')
-}
-
-export const toggleState = async (delay?: number) => {
-  if (detailState.activeDetail.isAppPlay) {
-    await cloudToggleState()
-
-  } else {
-    // console.log(detailState.activeDetail)
-    // console.log('delay', delay)
-    state.isFirstPlay = false
-    setPlayerView()
-    state.audiosInstance.togglePlay(delay)
-    if (!state.evaluatingStatus) {
-      changeMode(state.mode)
-    }
-    state.playState = state.audiosInstance.getStatus()
-  }
-}
-
-const setActiveKey = (index: number) => {
-  detailState.activeTick = index
-}
-
-const setTickStop = () => {
-  // console.log('节拍器结束', new Date().getTime() - state.clickTime)
-  detailState.activeTick = -1
-  detailState.activeTickRepeat = 1
-  toggleState(getTickTime(state.speed / detailState.baseSpeed))
-}
-
-let timeliner: unknown
-
-export const startIntervalTimeline = (maxTime: number, end: () => void) => {
-  const nowTimeline = new Date().getTime()
-  let currenttTime = 0
-  const throttleIndex = throttle(() => {
-    if (currenttTime) {
-      refreshView()
-    }
-  }, 1200)
-  const start = () => {
-    requestAnimationFrame(() => {
-      currenttTime = (new Date().getTime() - nowTimeline) / 1000
-      refreshIndex(currenttTime)
-      if (maxTime && currenttTime >= maxTime) {
-        clearIntervalTimeline()
-        end()
-      }
-      throttleIndex()
-    })
-  }
-  start()
-  timeliner = setInterval(() => {
-    start()
-  }, 16.7)
-}
-
-export const clearIntervalTimeline = () => {
-  if (timeliner) {
-    clearInterval(timeliner as any)
-  }
-}
-
-export const setTick = (stop: () => void, speed?: number) => {
-  // 节拍时间是固定的无需调整
-  // console.log('ticking')
-  const mixStop = () => {
-    stop()
-    event.emit('tickEnd')
-  }
-  if(detailState.needTick) {
-    if (detailState.activeDetail.isAppPlay) {
-      const { numerator, denominator } = getDuration(state.osmd as OpenSheetMusicDisplay)
-      state.ticking = true
-      postMessage({
-        api: 'cloudMetronome',
-        content: {
-          // 少量情况下需要重复
-          repeat: (numerator === 2 ? 2 : 1),
-          denominator,
-          numerator,
-        }
-      }, res => {
-        state.ticking = false
-        if (res?.content.status === 'finish') {
-          mixStop()
-        }
-      })
-    } else {
-      const { numerator } = getDuration(state.osmd as OpenSheetMusicDisplay)
-      const activeTickRepeat = (numerator === 2 ? 2 : 1)
-      detailState.activeTickRepeat = activeTickRepeat
-      console.log('ticking')
-      state.tickPlayer = new TickPlayer(numerator, (speed || state.speed) / 90)
-      state.tickPlayer?.start(numerator, (speed || state.speed) / 90, activeTickRepeat)
-      state.tickPlayer?.event.off('tick', setActiveKey)
-      state.tickPlayer?.event.off('stop', mixStop)
-      state.tickPlayer?.event.on('tick', setActiveKey)
-      state.tickPlayer?.event.on('stop', mixStop)
-    }
-  } else {
-    mixStop()
-  }
-}
-
-export const setPlayState = async () => {
-  if (detailState.activeTick > -1 || state.ticking) {
-    return
-  }
-  await syncPlayState()
-
-  if (state.playState !== 'pause' && state.playState !== 'suspend') {
-    await toggleState()
-    return
-  }
-
-  setPlayerView()
-  setTick(setTickStop)
-}
-
-export const stopTick = () => {
-  if (state.tickPlayer) {
-    state.tickPlayer.destroy()
-  }
-  event.emit('stopTick')
-  detailState.activeTickRepeat = 1
-  detailState.activeTick = -1
-}
-
-export const windowResize = () => {
-  const index = getIndex(detailState.times, state.currentTimeNum)
-  setTimeout(() => {
-    state.sectionHint?.showForElement(detailState.times[index]?.noteElement)
-  }, 200)
-}
-
-export const loadedmetadata = () => {
-  state.duration = formatTime(state.audiosInstance.duration)
-  state.durationNum = state.audiosInstance.duration
-}
-
-let prevDiff: number = 0
-let viewing: boolean = false
-
-export const refreshView = () => {
-  const top = Math.max(parseFloat(state.osmd!.cursor.cursorElement.style.top) - 50, 0)
-  // const diff: number = state.osmd.cursor.cursorElement.getBoundingClientRect().top;
-  // console.log('diff', Math.abs(diff - prevDiff))
-  if (Math.abs(prevDiff - top) > 10 && !viewing) {
-    viewing = true
-    // const oldTop = state.osmd.container.parentElement.scrollTop
-    setTimeout(() => {
-      viewing = false
-      // console.log(oldTop, top, 'top')
-      // JavaScriptAnimate({
-      //   duration: 500,
-      //   timing: num => num,
-      //   draw: progress => {
-      //     console.log((top - oldTop) * progress)
-      //     state.osmd.container.parentElement.scrollTop += (top - oldTop) * progress
-      //   }
-      // })
-      // state.osmd.container.parentElement.scrollTop = top
-      // @ts-ignore
-      state.osmd!.container.parentElement.scrollTo({
-        top: top,
-        left: 0,
-        behavior: 'smooth',
-      })
-      prevDiff = top
-    }, 100)
-  }
-  // if (prevDiff !== diff && Math.abs(diff - prevDiff) > 2) {
-  //   prevDiff = top
-    // state.osmd.container.parentElement.scrollTop  = diff
-    // state.osmd.container.parentElement.scrollTo({
-    //   top: top,
-    //   left: 0,
-    //   behavior: 'smooth'
-    // })
-    // console.log(diff)
-    // state.osmd.cursor.cursorElement.scrollIntoView({behavior: diff < 1000 ? "smooth" : "auto", block: "center"});
-  // }
-}
-
-export const updatePlayTime = async (time: number) => {
-  if (!state.evaluatingStatus) {
-    const behaviorId = localStorage.getItem('behaviorId') || undefined
-    try {
-      const res = await request.post('/sysMusicRecord/add', {
-        data: {
-          sysMusicScoreId: getLinkId(),
-          feature: 'CLOUD_STUDY_TRAIN',
-          playTime: time,
-          deviceType: getPlatform(),
-          behaviorId,
-          campId: sessionStorage.getItem('campId') || ''
-        }
-      })
-      event.emit('updatePlayTimeSuccess', res.data)
-    } catch (error) {}
-  }
-}
-
-export const setAudioInit = () => {
-  state.audiosInstance.event.on('loadedmetadata', loadedmetadata)
-  state.audiosInstance.event.on('waiting', waiting)
-  state.audiosInstance.event.on('playing', playing)
-  state.audiosInstance.event.on('play', play, false)
-  state.audiosInstance.event.on('pause', pause, false)
-  state.audiosInstance.event.on('ended', ended, false)
-  state.audiosInstance.event.on('updatePlayTime', updatePlayTime, false)
-  listenerMessage('cloudplayed', async () => {
-    await syncPlayState()
-    state.currentTimeNum = 0
-    state.currentTime = '00:00'
-  })
-  listenerMessage('cloudTimeUpdae', res => {
-    const time = res?.content.currentTime / 1000
-    requestAnimationFrame(async () => {
-      // const cloudGetMediaStatus = await promisefiyPostMessage({
-      //   api: 'cloudGetMediaStatus',
-      //   content: {
-      //     songID: detailState.activeDetail.examSongId,
-      //   }
-      // })
-      // const status = cloudGetMediaStatus?.content.status
-      if (state.playState === 'play') {
-        state.currentTimeNum = time
-        refreshPlayer(time)
-        refreshIndex(time)
-      }
-      refreshView()
-    })
-  })
-  state.audiosInstance.event.on('timeupdate', () => {
-    requestAnimationFrame(() => {
-      if (state.audiosInstance.getStatus() === 'play') {
-        state.currentTimeNum = state.audiosInstance?.currentTime
-        refreshPlayer()
-      }
-      refreshView()
-    })
-  })
-
-  window.addEventListener('resize', windowResize)
-}
-
-export const back = () => {
-  if (state.browser.isApp) {
-    postMessage({
-      api: 'back'
-    })
-  } else {
-    (this as any).$router.replace({
-      path: '/'
-    })
-  }
-}
-
-export const setStepView = (activeNote: any, time?: number) => {
-  prevIndex = Math.max(activeNote.i, 0)
-  syncStepIndex(activeNote.i)
-  if (time) {
-    // console.log(time)
-    refreshPlayer(time)
-  }  
-  refreshView()
-}
-
-export const  noteClick = (evt: MouseEvent) => {
-  if (state.isFirstPlay) {
-    Toast('开始播放后才能调整进度')
-    return
-  }
-  // const fixtime = getFixTime()
-  let activeNote = getNoteBySlursStart(getActtiveNoteByTimes(evt))
-  if (activeNote) {
-    // console.log(activeNote)
-    const time = activeNote.sourceStartTime || activeNote.time
-    // prevIndex = Math.max(activeNote.i, 0)
-    // syncStepIndex(activeNote.i)
-    // refreshIndex(time)
-    setCurrentTime(time)
-    setStepView(activeNote.i, time)
-    detailState.fixedKey = activeNote.realKey
-  }
-}
-
-export const startCapture = async () => {
-  // console.log('startCapture:',SettingState.sett.camera, browserInfo.isApp, !state.captureStatus, state.evaluatingStatus, SettingState.eva.save)
-  if (SettingState.sett.camera && browserInfo.isApp && !state.captureStatus && state.evaluatingStatus && SettingState.eva.save) {
-    state.captureStatus = true
-    postMessage({
-      api: 'startCapture'
-    })
-  }
-}
-
-export const endCapture = async () => {
-  if (browserInfo.isApp && state.evaluatingStatus && SettingState.sett.camera) {
-    postMessage({
-      api: 'endCapture'
-    }, () => {
-      state.captureStatus = false
-    })
-  }
-}
-
-export const setCaptureMode = async () => {
-  if (browserInfo.isApp && SettingState.sett.camera) {
-    postMessage({
-      api: 'setCaptureMode',
-      content: {
-        mode: state.evaluatingStatus ? 'evaluating' : 'practice'
-      }
-    })
-  }
-}

+ 36 - 0
src/page-gym/detail/index.module.less

@@ -0,0 +1,36 @@
+
+.skeleton {
+    position: fixed;
+    left: 0;
+    top: 0;
+    width: 100vw;
+    height: 100vh;
+    padding: 20px 30px;
+    background-color: #fff;
+    --van-skeleton-paragraph-height: .8rem;
+}
+.detail{
+    display: flex;
+    flex-direction: column;
+    width: 100vw;
+    height: 100vh;
+    overflow: hidden;
+    background-color: var(--van-primary-color);
+    .headHeight{
+        height: 62px;
+    }
+    .container{
+        margin: 0 18px 18px 18px;
+        flex: 1;
+        overflow-y: auto;
+        border-radius: 10px;
+        background: #fff;
+        &::-webkit-scrollbar{
+            width: 0;
+            display: none;
+        }
+    }
+    .musicContainer{
+        position: relative;
+    }
+}

+ 48 - 3
src/page-gym/detail/index.tsx

@@ -1,11 +1,16 @@
 import { Skeleton } from "vant";
-import { defineComponent, onMounted, reactive } from "vue";
+import { defineComponent, onMounted, reactive, Transition } from "vue";
 import { useRoute } from "vue-router";
+import { formateTimes } from "../../helpers/formateMusic";
+import Metronome, { metronomeData } from "../../helpers/metronome";
 import state from "../../state";
 import { storeData } from "../../store";
+import { setGlobalData } from "../../utils";
 import { getQuery } from "../../utils/queryString";
+import AudioList from "../../view/audio-list";
 import MusicScore from "../../view/music-score";
 import { sysMusicScoreAccompanimentQueryPage, sysMusicScoreCategoriesQueryTree } from "../api";
+import HeaderTop from "../header-top";
 import styles from "./index.module.less";
 
 //特殊教材分类id
@@ -18,6 +23,7 @@ export default defineComponent({
 		const paramsId = route.params.id as string;
 		const detailData = reactive({
 			isLoading: true,
+			isRenderLoading: true, //曲谱渲染
 		});
 		// console.log(route.params, query)
 		/** 获取曲谱数据 */
@@ -27,7 +33,9 @@ export default defineComponent({
 			const musicInfo = res.data[index];
 			// console.log("🚀 ~ musicInfo:", musicInfo);
 			setState(musicInfo, index);
+			setCustom();
 			detailData.isLoading = false;
+			// initAudioEvent()
 		};
 		const getCategorId = (arr: any[], key = "sysMusicScoreCategoriesList"): any[] => {
 			const list = [];
@@ -59,6 +67,11 @@ export default defineComponent({
 			console.log(getIds(res.data).toString());
 		};
 
+		const setCustom = () => {
+			if (state.extConfigJson.multitrack) {
+				setGlobalData("multitrack", state.extConfigJson.multitrack);
+			}
+		};
 		const setState = (data: any, index: number) => {
 			state.xmlUrl = data.xmlUrl;
 			state.partIndex = index;
@@ -66,7 +79,7 @@ export default defineComponent({
 			state.categoriesId = data.categoriesId;
 			state.categoriesName = data.categoriesName;
 			state.enableEvaluation = data.enableEvaluation;
-			state.examSongId = data.examSongId;
+			state.examSongId = data.examSongId + "";
 			state.examSongName = data.examSongName;
 			// 解析扩展字段
 			if (data.extConfigJson) {
@@ -88,9 +101,41 @@ export default defineComponent({
 		};
 
 		onMounted(async () => {
+			;(window as any).appName = 'gym';
 			await getCategory();
 			await getMusicInfo();
 		});
-		return () => <div class={styles.detail}>{!detailData.isLoading && <MusicScore />}</div>;
+
+		/** 渲染完成 */
+		const handleRendered = (osmd: any) => {
+			detailData.isRenderLoading = false;
+			state.osmd = osmd;
+			state.times = formateTimes(osmd);
+			console.log("🚀 ~ state.times:", state.times);
+			try {
+				metronomeData.metro = new Metronome({ speed: state.activeSpeed });
+				metronomeData.metro.init(state.times);
+			} catch (error) {}
+		};
+		return () => (
+			<div class={styles.detail}>
+				{detailData.isRenderLoading && (
+					<div class={styles.skeleton}>
+						<Skeleton class={styles.skeleton} row={8} />
+					</div>
+				)}
+				<div class={styles.headHeight}>
+					<Transition name="van-slide-down">{!detailData.isRenderLoading && <HeaderTop />}</Transition>
+				</div>
+				<div class={styles.container}>
+					{!detailData.isLoading && (
+						<>
+							<MusicScore class={styles.musicContainer} onRendered={handleRendered} />
+							<AudioList />
+						</>
+					)}
+				</div>
+			</div>
+		);
 	},
 });

File diff suppressed because it is too large
+ 9 - 0
src/page-gym/header-top/image/arrow.svg


+ 11 - 0
src/page-gym/header-top/image/icon-back.svg

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <g id="云教练2版" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="画板" transform="translate(-27.000000, -123.000000)">
+            <g id="返回箭头备份" transform="translate(43.000000, 139.000000) scale(-1, 1) translate(-43.000000, -139.000000) translate(27.000000, 123.000000)">
+                <circle id="椭圆形" fill="#FFFFFF" cx="16" cy="16" r="16"></circle>
+                <path d="M17.3783645,23.2991468 L23.9125501,16.7649612 C24.157653,16.5199302 24.2953565,16.1875536 24.2953565,15.8409766 C24.2953565,15.4943995 24.157653,15.162023 23.9125501,14.916992 L17.3783645,8.38280634 C17.1333334,8.13770343 16.8009569,8 16.4543799,8 C16.1078028,8 15.7754263,8.13770343 15.5303953,8.38280634 C15.0201983,8.89302883 15.0201983,9.72023586 15.5303953,10.2304583 L19.8337592,14.5344566 L7.30683717,14.5344566 C6.96021662,14.5343725 6.62776858,14.6720297 6.38267084,14.9171274 C6.13757309,15.1622252 6,15.4946732 6,15.8412938 C6,16.1879143 6.13757308,16.5203624 6.38267083,16.7654601 C6.62776858,17.0105579 6.96021662,17.1482151 7.30683717,17.1481309 L19.8337592,17.1481309 L15.5303953,21.4514948 C15.2002874,21.7816027 15.0713656,22.2627455 15.1921934,22.7136812 C15.3130213,23.164617 15.6652423,23.516838 16.116178,23.6376658 C16.5671138,23.7584937 17.0482566,23.6295719 17.3783645,23.299464 L17.3783645,23.2991468 Z" id="路径" fill="#2DC7AA" fill-rule="nonzero"></path>
+            </g>
+        </g>
+    </g>
+</svg>

+ 14 - 0
src/page-gym/header-top/image/icon-background.svg

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="34px" height="34px" viewBox="0 0 34 34" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>编组 5备份</title>
+    <g id="云教练2版" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="画板" transform="translate(-449.000000, -55.000000)">
+            <g id="编组-5备份" transform="translate(451.000000, 57.000000)">
+                <rect id="矩形" stroke="#8EE1DC" stroke-width="2" fill="#FFFFFF" x="-1" y="-1" width="32" height="32" rx="12"></rect>
+                <g id="编组" transform="translate(5.000000, 7.000000)" fill="#01C1B5" fill-rule="nonzero">
+                    <path d="M14.6025001,14.7575455 C15.727559,15.7058862 17.3506352,15.6867741 18.4543635,14.7121888 C19.5580918,13.7376035 19.8248793,12.0879777 19.0875,10.7973182 L14.6025001,14.7575455 Z M13.7782501,13.6664545 L18.19725,9.81440909 C17.0824899,9.02562521 15.5933198,9.10278516 14.5609925,10.0028184 C13.5286652,10.9028516 13.2084129,12.4032342 13.7790001,13.6664545 L13.7782501,13.6664545 Z M18.0000001,6.37654546 C17.4994985,6.24404164 16.9845011,6.17859277 16.4677501,6.18181818 L16.4677501,3.12413637 C16.4677501,2.27060539 15.7961772,1.57868182 14.9677501,1.57868182 L6.12750021,1.57868182 C5.49300021,1.57868182 4.97850024,2.108 4.97850022,2.76172728 L4.97850022,3.17281818 C4.97850022,3.59958367 5.31428666,3.94554546 5.72850022,3.94554546 L14.1862501,3.94554546 C14.6004636,3.94554547 14.9362501,4.29150724 14.9362501,4.71827272 L14.9362501,4.7515 C14.9362501,5.17826549 14.6004637,5.52422727 14.1862501,5.52422728 L5.72850022,5.52422728 C5.31428666,5.52422728 4.97850023,5.87018906 4.97850022,6.29695454 L4.97850022,13.2182727 C4.97850029,14.6348324 3.86414637,15.7833006 2.48925026,15.7837273 L1.91475026,15.7837273 C1.40679702,15.7837273 0.919659179,15.5757761 0.560552337,15.2056424 C0.201445496,14.8355086 -0.000198671618,14.3335276 1.46884807e-07,13.8101818 L1.46884807e-07,13.6131364 C1.46884807e-07,12.6317727 0.771750288,11.8374091 1.72350028,11.8374091 C2.67525027,11.8374091 3.44700026,11.0422727 3.44700024,10.0616818 L3.44700024,1.54545454 C3.44700024,0.691923568 4.11857311,0 4.94700022,0 L16.5000001,0 C17.3284272,0 18.0000001,0.691923568 18.0000001,1.54545454 L18.0000001,6.37654546 Z M16.5000001,17 C14.0147187,17 12.0000001,14.9242293 12.0000001,12.3636364 C12.0000001,9.80304343 14.0147187,7.72727272 16.5000001,7.72727272 C18.9852814,7.72727272 21.0000001,9.80304343 21.0000001,12.3636364 C21.0000001,14.9242293 18.9852814,17 16.5000001,17 Z" id="形状"></path>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 14 - 0
src/page-gym/header-top/image/icon-evaluating.svg

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="34px" height="34px" viewBox="0 0 34 34" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>编组 5</title>
+    <g id="云教练2版" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="长笛备份-10" transform="translate(-426.000000, -9.000000)">
+            <g id="编组-5" transform="translate(428.000000, 11.000000)">
+                <rect id="矩形" stroke="#8EE1DC" stroke-width="2" fill="#FFFFFF" x="-1" y="-1" width="32" height="32" rx="12"></rect>
+                <g id="编组" transform="translate(7.444444, 7.000000)" fill="#01C1B5" fill-rule="nonzero">
+                    <path d="M11.864492,9.57379933 L12.5217501,13.3728952 C12.5737935,13.6738302 12.5217501,13.88914 12.2732847,14.0687034 C12.0237192,14.2483919 11.6928065,14.2722378 11.4196047,14.1302204 L7.90163735,12.2514553 C7.6846214,12.1383792 7.42530062,12.1383792 7.20828467,12.2514553 L3.83805347,14.1302204 C3.56485164,14.2722378 3.23393899,14.2483919 2.98437349,14.0687034 C2.73506872,13.88914 2.53612853,13.6738301 2.58817197,13.3728952 L3.39400558,9.49482469 C3.43512937,9.25594025 3.35511011,9.01229367 3.17995602,8.84307607 L0.245378621,6.55696807 C0.0240651899,6.34386262 -0.0557141321,6.02471109 0.0396498736,5.73396478 C0.135013879,5.44321847 0.388943125,5.23142074 0.694462978,5.18779718 L4.51126031,4.5244102 C4.75346399,4.4893316 4.96278558,4.33859462 5.07114679,4.12122388 L6.82887134,0.448487437 C6.96482343,0.173977217 7.2466509,0 7.55538071,0 C7.86411053,0 8.14593799,0.173977217 8.28189008,0.448487437 L9.96994362,4.12122388 C10.0790669,4.33902762 10.2889194,4.4894951 10.5306695,4.5244102 L14.4171379,5.18779718 C14.7227539,5.23134591 14.9767089,5.44333977 15.0717762,5.7342692 C15.1668436,6.02519864 15.0864514,6.34434966 14.8645434,6.55696807 L12.0777022,8.9212194 C11.9031198,9.09061742 11.8230779,9.33464369 11.864492,9.57379933 Z" id="路径"></path>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 13 - 0
src/page-gym/header-top/image/icon-music.svg

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="34px" height="34px" viewBox="0 0 34 34" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>编组 5备份 4</title>
+    <g id="云教练2版" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="画板" transform="translate(-411.000000, -55.000000)">
+            <g id="编组-5备份-4" transform="translate(413.000000, 57.000000)">
+                <rect id="矩形" stroke="#8EE1DC" stroke-width="2" fill="#FFFFFF" x="-1" y="-1" width="32" height="32" rx="12"></rect>
+                <path d="M21.5000001,7 C22.3284272,7 23.0000001,7.69192357 23.0000001,8.54545454 L23.0000001,13.3765455 L22.978,13.371 L22.9785014,20.2187573 C22.9785014,21.635317 21.8641475,22.7837852 20.4892514,22.7842119 L19.9147514,22.7842119 C19.4067982,22.7842119 18.9196603,22.5762607 18.5605535,22.2061269 C18.2014467,21.8359931 17.9998025,21.3340121 18,20.8106664 L18,20.6136209 C18,19.6322573 18.7717515,18.8378937 19.7235014,18.8378937 C20.6752514,18.8378937 21.4470014,18.0427573 21.4470014,17.0621664 L21.4470014,10.5459391 C21.4470014,10.4809192 21.4536669,10.421707 21.4669979,10.3683026 L21.4677501,10.1241364 C21.4677501,9.27060539 20.7961772,8.57868182 19.9677501,8.57868182 L11.1275002,8.57868182 C10.4930002,8.57868182 9.97850024,9.108 9.97850022,9.76172728 L9.97850022,10.1728182 C9.97850022,10.5995837 10.3142867,10.9455455 10.7285002,10.9455455 L19.1862501,10.9455455 C19.6004636,10.9455455 19.9362501,11.2915072 19.9362501,11.7182727 L19.9362501,11.7515 C19.9362501,12.1782655 19.6004637,12.5242273 19.1862501,12.5242273 L10.7285002,12.5242273 C10.3142867,12.5242273 9.97850023,12.8701891 9.97850022,13.2969545 L9.97850022,20.2182727 C9.97850029,21.6348324 8.86414637,22.7833006 7.48925026,22.7837273 L6.91475026,22.7837273 C6.40679702,22.7837273 5.91965918,22.5757761 5.56055234,22.2056424 C5.2014455,21.8355086 4.99980133,21.3335276 5.00000015,20.8101818 L5.00000015,20.6131364 C5.00000015,19.6808409 5.69650465,18.917313 6.58216175,18.8432909 L6.72350028,18.8374091 C7.67525027,18.8374091 8.44700026,18.0422727 8.44700024,17.0616818 L8.44700024,8.54545454 C8.44700024,7.69192357 9.11857311,7 9.94700022,7 L21.5000001,7 Z" id="形状结合" fill="#01C1B5" fill-rule="nonzero"></path>
+                <g id="编组备份" transform="translate(18.000000, 10.000000)"></g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 12 - 0
src/page-gym/header-top/image/icon-pause.svg

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="34px" height="34px" viewBox="0 0 34 34" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>编组 5备份 7</title>
+    <g id="云教练2版" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="画板" transform="translate(-346.000000, -117.000000)">
+            <g id="编组-5备份-7" transform="translate(348.000000, 119.000000)">
+                <rect id="矩形" stroke="#8EE1DC" stroke-width="2" fill="#FFFFFF" x="-1" y="-1" width="32" height="32" rx="12"></rect>
+                <path d="M11.6428571,10 C12.4712843,10 13.1428571,10.6715729 13.1428571,11.5 L13.1428571,19.5 C13.1428571,20.3284271 12.4712843,21 11.6428571,21 L11.5,21 C10.6715729,21 10,20.3284271 10,19.5 L10,11.5 C10,10.6715729 10.6715729,10 11.5,10 L11.6428571,10 Z M18.5,10 C19.3284271,10 20,10.6715729 20,11.5 L20,19.5 C20,20.3284271 19.3284271,21 18.5,21 L18.3571429,21 C17.5287157,21 16.8571429,20.3284271 16.8571429,19.5 L16.8571429,11.5 C16.8571429,10.6715729 17.5287157,10 18.3571429,10 L18.5,10 Z" id="形状结合备份" fill="#2DC7AA"></path>
+            </g>
+        </g>
+    </g>
+</svg>

+ 12 - 0
src/page-gym/header-top/image/icon-play.svg

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="34px" height="34px" viewBox="0 0 34 34" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>编组 5备份 6</title>
+    <g id="云教练2版" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="画板" transform="translate(-308.000000, -117.000000)">
+            <g id="编组-5备份-6" transform="translate(310.000000, 119.000000)">
+                <rect id="矩形" stroke="#8EE1DC" stroke-width="2" fill="#FFFFFF" x="-1" y="-1" width="32" height="32" rx="12"></rect>
+                <path d="M20.6455278,14.4055419 L12.1044498,9.10418309 C11.6258549,8.80966315 11,9.17781308 11,9.73003795 L11,20.3327556 C11,20.8849805 11.6258549,21.2531304 12.1044498,20.9586104 L20.6087128,15.6572516 C21.1241227,15.3995467 21.1241227,14.6632469 20.6455278,14.4055419 Z" id="路径" fill="#01C1B5" fill-rule="nonzero"></path>
+            </g>
+        </g>
+    </g>
+</svg>

BIN
src/page-gym/header-top/image/iconStep.png


+ 5 - 0
src/page-gym/header-top/image/index.ts

@@ -0,0 +1,5 @@
+const icons = import.meta.glob("./*", { import: "default", eager: true });
+export const headImg = (name: string) => {
+    const src = icons[`./${name}`] as unknown as string || ''
+	return src;
+};

+ 14 - 0
src/page-gym/header-top/image/menu.svg

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="34px" height="34px" viewBox="0 0 34 34" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>编组 5备份 4</title>
+    <g id="云教练2版" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="画板" transform="translate(-587.000000, -55.000000)">
+            <g id="编组-5备份-4" transform="translate(589.000000, 57.000000)">
+                <rect id="矩形" stroke="#8EE1DC" stroke-width="2" fill="#FFFFFF" x="-1" y="-1" width="32" height="32" rx="12"></rect>
+                <g id="编组" transform="translate(5.000000, 6.000000)" fill="#01C1B5" fill-rule="nonzero">
+                    <path d="M13.4777127,0.0182290708 C14.4044052,0.0179861139 15.2701896,0.47987939 15.786046,1.24971788 L15.8832682,1.40711806 L19.4443793,7.57378472 C19.9055877,8.37249457 19.9402825,9.34803984 19.5369792,10.1774957 L19.4443793,10.3515625 L15.8832682,16.5182291 C15.4201933,17.3208032 14.5874276,17.8396355 13.6629123,17.9015625 L13.4777127,17.9071182 L6.35735677,17.9071182 C5.43066421,17.9073611 4.56487987,17.4454678 4.04902344,16.6756293 L3.95180122,16.5182291 L0.389756944,10.3515625 C-0.0714513871,9.55285265 -0.106146222,8.57730739 0.297157118,7.74785156 L0.389756944,7.57378472 L3.95086806,1.40711806 C4.41394723,0.604537909 5.24672247,0.085704652 6.17124566,0.0237847222 L6.35642361,0.0182290708 L13.4768012,0.0182290708 L13.4777127,0.0182290708 Z M13.4777127,1.87007376 L6.35735677,1.87007376 C6.06542361,1.87007376 5.79055442,2.00761662 5.6156901,2.24138455 L5.55549045,2.33305122 L1.99437934,8.49971788 C1.85122226,8.74833447 1.83139307,9.04926681 1.9406901,9.31451823 L1.99437934,9.42562934 L5.55549045,15.592296 C5.70147524,15.8451441 5.95816545,16.0143272 6.24809028,16.0487847 L6.35735677,16.0552735 L13.4777127,16.0552735 C13.7696458,16.0552735 14.044515,15.9177306 14.2193793,15.6839627 L14.279579,15.592296 L17.8406901,9.42562934 C17.9838472,9.17701275 18.0036764,8.87608041 17.8943793,8.61082899 L17.8406901,8.49971788 L14.279579,2.33305122 C14.1335942,2.0802031 13.876904,1.91102001 13.5869792,1.8765625 L13.4777127,1.87007376 L13.4777127,1.87007376 Z M10.1,5.66 C12.0329966,5.66 13.6,7.22700338 13.6,9.16 C13.6,11.0929966 12.0329966,12.66 10.1,12.66 C8.16700338,12.66 6.6,11.0929966 6.6,9.16 C6.6,7.22700338 8.16700338,5.66 10.1,5.66 Z" id="形状"></path>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

BIN
src/page-gym/header-top/image/minus.png


BIN
src/page-gym/header-top/image/music.png


BIN
src/page-gym/header-top/image/plus.png


+ 15 - 0
src/page-gym/header-top/image/replay.svg

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="34px" height="34px" viewBox="0 0 34 34" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>编组 5备份 4</title>
+    <g id="云教练2版" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="画板" transform="translate(-499.000000, -55.000000)">
+            <g id="编组-5备份-4" transform="translate(501.000000, 57.000000)">
+                <rect id="矩形" stroke="#8EE1DC" stroke-width="2" fill="#FFFFFF" x="-1" y="-1" width="32" height="32" rx="12"></rect>
+                <g id="编组" transform="translate(15.000000, 15.500000) scale(-1, 1) translate(-15.000000, -15.500000) translate(4.000000, 5.000000)" fill="#01C1B5" fill-rule="nonzero">
+                    <path d="M21.8359743,5.91618045 C21.4133965,7.0257019 21.0615062,8.16089287 20.7824176,9.31493233 C20.5279569,10.3734135 19.9595125,10.6413835 19.1023816,10.1813684 C19.1023816,10.1813684 16.8062829,8.88915789 15.8866529,8.47231579 C14.8132553,7.98401503 14.9714872,7.47437594 16.3613486,6.9433985 C16.7720572,6.78708271 17.1663969,6.59057144 17.5726412,6.4238346 C16.1878095,3.53041761 13.2777299,1.67769054 10.0712582,1.64801504 C5.30782736,1.72874517 1.51067257,5.65548936 1.58774614,10.4210526 C1.5098371,15.1866217 5.30633371,19.1140403 10.0697701,19.1955789 C12.4640261,19.1829371 14.7366908,18.1386081 16.3062899,16.3297895 C16.5067122,16.1698604 16.7415656,16.0587535 16.9922922,16.0052481 C17.3774369,16.0098332 17.6998373,16.2986565 17.7467459,16.6811278 C17.7563723,16.870475 17.7141837,17.0588621 17.6247238,17.226 C17.5872265,17.2811669 17.5427057,17.331212 17.4922852,17.3748722 C15.6413244,19.5566109 12.9315942,20.8226441 10.0712582,20.8421053 C4.41465343,20.7450836 -0.0930331504,16.0801605 0.0014588411,10.4210526 C0.0139187466,9.29275203 0.205800339,8.17362578 0.569903248,7.10566917 C0.580968335,7.01624856 0.606605274,6.92924621 0.645795047,6.8481203 C2.01644926,2.79485654 5.79595676,0.0492687634 10.0727463,0 C13.931717,0.0337531718 17.4260937,2.28823581 19.048811,5.79112781 C19.5205306,5.58270676 20.0041547,5.40406014 20.4714101,5.18075188 C21.8166293,4.53861654 22.2739642,4.78375939 21.8434147,5.91618045 L21.8359743,5.91618045 Z" id="路径"></path>
+                    <path d="M8.18320392,13.7071392 C7.50450737,14.1305274 6.94736842,13.8126149 6.94736842,13.0014923 L6.94736842,7.84061301 C6.94736842,7.02949037 7.50306026,6.71157784 8.18320392,7.13496603 L12.2264409,9.65152602 C12.5343515,9.77570179 12.7368421,10.0807 12.7368421,10.4203098 C12.7368421,10.7599196 12.5343515,11.0649179 12.2264409,11.1890937 L8.18320392,13.7071392 Z" id="路径" transform="translate(9.842105, 10.421053) scale(-1, 1) translate(-9.842105, -10.421053) "></path>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 15 - 0
src/page-gym/header-top/image/section0.svg

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="34px" height="34px" viewBox="0 0 34 34" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>编组 5备份</title>
+    <g id="云教练2版" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="画板" transform="translate(-176.000000, -55.000000)">
+            <g id="编组-5备份" transform="translate(178.000000, 57.000000)">
+                <rect id="矩形" stroke="#8EE1DC" stroke-width="2" fill="#FFFFFF" x="-1" y="-1" width="32" height="32" rx="12"></rect>
+                <g id="编组" transform="translate(15.500000, 15.000000) rotate(-90.000000) translate(-15.500000, -15.000000) translate(6.500000, 6.000000)" fill="#01C1B5" fill-rule="nonzero">
+                    <path d="M1.3031071,7.71434066 L5.04076678,7.71434066 L5.04076678,16.6618347 C5.04076678,17.3741595 5.58990162,17.9822416 6.31617672,17.9996154 C7.06016586,18.0169891 7.66244277,17.4436546 7.66244277,16.713956 L7.66244277,1.66826702 C7.66244277,0.747456955 6.90073961,0.00038463772 5.96189619,0.00038463772 C5.39504732,0.00038463772 4.86362652,0.278365035 4.54477403,0.747456955 L0.647688105,6.48080265 C0.27569354,7.00201589 0.647688105,7.71434066 1.3031071,7.71434066 Z" id="路径"></path>
+                    <path d="M12.9057947,10.2856593 L12.9057947,1.3381653 C12.9057947,0.625840532 12.3566599,0.0177584126 11.6303848,0.00038463772 C10.8863957,-0.0169891371 10.2841188,0.556345432 10.2841188,1.28604398 L10.2841188,16.331733 C10.2841188,17.252543 11.0458219,17.9996154 11.9846653,17.9996154 C12.5515142,17.9996154 13.082935,17.721635 13.4017875,17.252543 L17.3697295,11.4323285 C17.706296,10.9458628 17.3520155,10.2856593 16.7497386,10.2856593 L12.9057947,10.2856593 Z" id="路径"></path>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 15 - 0
src/page-gym/header-top/image/section1.svg

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="34px" height="34px" viewBox="0 0 34 34" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>编组 5备份 3</title>
+    <g id="云教练2版" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="画板" transform="translate(-217.000000, -55.000000)">
+            <g id="编组-5备份-3" transform="translate(219.000000, 57.000000)">
+                <rect id="矩形" stroke="#8EE1DC" stroke-width="2" fill="#FFFFFF" x="-1" y="-1" width="32" height="32" rx="12"></rect>
+                <g id="编组" transform="translate(15.500000, 15.000000) rotate(-90.000000) translate(-15.500000, -15.000000) translate(7.000000, 6.000000)" fill-rule="nonzero">
+                    <path d="M0.803107102,7.71434066 L4.54076678,7.71434066 L4.54076678,16.6618347 C4.54076678,17.3741595 5.08990162,17.9822416 5.81617672,17.9996154 C6.56016586,18.0169891 7.16244277,17.4436546 7.16244277,16.713956 L7.16244277,1.66826702 C7.16244277,0.747456955 6.40073961,0.00038463772 5.46189619,0.00038463772 C4.89504732,0.00038463772 4.36362652,0.278365035 4.04477403,0.747456955 L0.147688105,6.48080265 C-0.22430646,7.00201589 0.147688105,7.71434066 0.803107102,7.71434066 Z" id="路径" fill="#01C1B5"></path>
+                    <path d="M12.4057947,10.2856593 L12.4057947,1.3381653 C12.4057947,0.625840532 11.8566599,0.0177584126 11.1303848,0.00038463772 C10.3863957,-0.0169891371 9.78411876,0.556345432 9.78411876,1.28604398 L9.78411876,16.331733 C9.78411876,17.252543 10.5458219,17.9996154 11.4846653,17.9996154 C12.0515142,17.9996154 12.582935,17.721635 12.9017875,17.252543 L16.8697295,11.4323285 C17.206296,10.9458628 16.8520155,10.2856593 16.2497386,10.2856593 L12.4057947,10.2856593 Z" id="路径" fill="#FFC830"></path>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 15 - 0
src/page-gym/header-top/image/section2.svg

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="34px" height="34px" viewBox="0 0 34 34" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>编组 5备份 5</title>
+    <g id="云教练2版" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="画板" transform="translate(-258.000000, -55.000000)">
+            <g id="编组-5备份-5" transform="translate(260.000000, 57.000000)">
+                <rect id="矩形" stroke="#8EE1DC" stroke-width="2" fill="#FFFFFF" x="-1" y="-1" width="32" height="32" rx="12"></rect>
+                <g id="编组" transform="translate(15.500000, 15.000000) rotate(-90.000000) translate(-15.500000, -15.000000) translate(7.000000, 6.000000)" fill="#FFC830" fill-rule="nonzero">
+                    <path d="M0.803107102,7.71434066 L4.54076678,7.71434066 L4.54076678,16.6618347 C4.54076678,17.3741595 5.08990162,17.9822416 5.81617672,17.9996154 C6.56016586,18.0169891 7.16244277,17.4436546 7.16244277,16.713956 L7.16244277,1.66826702 C7.16244277,0.747456955 6.40073961,0.00038463772 5.46189619,0.00038463772 C4.89504732,0.00038463772 4.36362652,0.278365035 4.04477403,0.747456955 L0.147688105,6.48080265 C-0.22430646,7.00201589 0.147688105,7.71434066 0.803107102,7.71434066 Z" id="路径"></path>
+                    <path d="M12.4057947,10.2856593 L12.4057947,1.3381653 C12.4057947,0.625840532 11.8566599,0.0177584126 11.1303848,0.00038463772 C10.3863957,-0.0169891371 9.78411876,0.556345432 9.78411876,1.28604398 L9.78411876,16.331733 C9.78411876,17.252543 10.5458219,17.9996154 11.4846653,17.9996154 C12.0515142,17.9996154 12.582935,17.721635 12.9017875,17.252543 L16.8697295,11.4323285 C17.206296,10.9458628 16.8520155,10.2856593 16.2497386,10.2856593 L12.4057947,10.2856593 Z" id="路径"></path>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 21 - 0
src/page-gym/header-top/image/speed.svg

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="34px" height="34px" viewBox="0 0 34 34" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>编组 5备份 4</title>
+    <g id="云教练2版" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="画板" transform="translate(-549.000000, -55.000000)">
+            <g id="编组-5备份-4" transform="translate(551.000000, 57.000000)">
+                <rect id="矩形" stroke="#8EE1DC" stroke-width="2" fill="#FFFFFF" x="-1" y="-1" width="32" height="32" rx="12"></rect>
+                <g id="编组-2" transform="translate(4.000000, 6.000000)" fill="#01C1B5" fill-rule="nonzero">
+                    <g id="编组">
+                        <path d="M17.1788934,2.65984165 L21.2637262,2.66129281 C21.6441871,2.6595274 21.9627029,2.95663144 21.9969824,3.34525683 C22.0312619,3.73388223 21.7699052,4.08478959 21.3953125,4.15308163 L21.2637262,4.16614204 L17.1788934,4.16614204 C17.3474571,3.67892357 17.3474571,3.14706012 17.1788934,2.65984165 L17.1788934,2.65984165 Z" id="形状"></path>
+                        <path d="M12.7368421,0 C14.6553049,0 16.2105263,1.5552214 16.2105263,3.47368421 C16.2105263,5.39214703 14.6553049,6.94736842 12.7368421,6.94736842 C11.0558023,6.94736842 9.65365999,5.75326639 9.33235683,4.16700399 L0.73627375,4.16614204 C0.355812911,4.16790744 0.0372970883,3.8708034 0.00301760378,3.48217801 C-0.0312618807,3.09355262 0.230094793,2.74264526 0.604687516,2.67435322 L0.73627375,2.66129281 L9.35922942,2.65897043 C9.72584461,1.13357143 11.0989635,0 12.7368421,0 Z" id="形状结合"></path>
+                    </g>
+                    <g id="编组备份-3" transform="translate(11.000000, 15.078947) scale(-1, 1) translate(-11.000000, -15.078947) translate(0.000000, 11.578947)">
+                        <path d="M17.1788934,2.65984165 L21.2637262,2.66129281 C21.6441871,2.6595274 21.9627029,2.95663144 21.9969824,3.34525683 C22.0312619,3.73388223 21.7699052,4.08478959 21.3953125,4.15308163 L21.2637262,4.16614204 L17.1788934,4.16614204 C17.3474571,3.67892357 17.3474571,3.14706012 17.1788934,2.65984165 L17.1788934,2.65984165 Z" id="形状"></path>
+                        <path d="M12.7368421,0 C14.6553049,0 16.2105263,1.5552214 16.2105263,3.47368421 C16.2105263,5.39214703 14.6553049,6.94736842 12.7368421,6.94736842 C11.0558023,6.94736842 9.65365999,5.75326639 9.33235683,4.16700399 L0.73627375,4.16614204 C0.355812911,4.16790744 0.0372970883,3.8708034 0.00301760378,3.48217801 C-0.0312618807,3.09355262 0.230094793,2.74264526 0.604687516,2.67435322 L0.73627375,2.66129281 L9.35922942,2.65897043 C9.72584461,1.13357143 11.0989635,0 12.7368421,0 Z" id="形状结合"></path>
+                    </g>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

BIN
src/page-gym/header-top/image/tickoff.png


BIN
src/page-gym/header-top/image/tickon.png


+ 70 - 0
src/page-gym/header-top/index.module.less

@@ -0,0 +1,70 @@
+.headerTop{
+    display: flex;
+    align-items: center;
+    width: 100%;
+    height: 100%;
+    flex-shrink: 0;
+    padding: 8px 10px;
+}
+.back{
+    display: flex;
+    align-items: center;
+    height: 100%;
+    padding: 0 11px 0 6px;
+    img{
+        display: block;
+        width: 24px;
+        height: 24px;
+    }
+}
+.headRight{
+    display: flex;
+    align-items: center;
+    margin-left: auto;
+    .btn{
+        position: relative;
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        align-items: center;
+        color: #fff;
+        font-size: 11px;
+        line-height: 16px;
+        font-weight: 400;
+        padding: 0 6px;
+        .iconBtn{
+            display: block;
+            width: 25px;
+            height: 25px;
+        }
+        span{
+            white-space: nowrap;
+        }
+        .btnWrap{
+            position: relative;
+            width: 25px;
+            height: 25px;
+        }
+        .progress{
+            position: absolute;
+            left: 50%;
+            top: 50%;
+            transform: translate(-50%, -50%);
+            width: 20px;
+            height: 20px;
+        }
+        &.disabled{
+            opacity: .8;
+        }
+    }
+    .badge{
+        :global{
+            .van-badge{
+                border: none;
+                color: #005e58;
+                background-color: #d7fffc;
+                font-weight: 400;
+            }
+        }
+    }
+}

+ 134 - 0
src/page-gym/header-top/index.tsx

@@ -0,0 +1,134 @@
+import { computed, defineComponent, reactive, ref } from "vue";
+import styles from "./index.module.less";
+
+import iconBack from "./image/icon-back.svg";
+import Title from "./title";
+import state, { togglePlay } from "../../state";
+import { headImg } from "./image";
+import { useRect } from "@vant/use";
+import { Badge, Circle, Popover } from "vant";
+import { metronomeData } from "../../helpers/metronome";
+import Speed from "./speed";
+import { audioList } from "../../view/audio-list";
+
+export default defineComponent({
+	name: "header-top",
+	setup() {
+		const headRef = ref();
+		const headRect = useRect(headRef.value);
+
+		const headData = reactive({
+			speedShow: false,
+		});
+
+		return () => (
+			<div ref={headRef} class={styles.headerTop}>
+				<div class={styles.back}>
+					<img src={iconBack} />
+				</div>
+				<Title text={state.examSongName} />
+
+				<div class={styles.headRight}>
+					<div class={styles.btn} id="tips-step-2">
+						<img class={styles.iconBtn} src={headImg("icon-evaluating.svg")} />
+						{/* <Button class={styles.button} icon={runtime.evaluatingStatus ? Icons.evaluating2 : Icons.evaluating} disabled={(this.isAppPlay ? !state.activeDetail?.midiUrl || state.midiPlayIniting : !(runtime.songs.background || runtime.songs.music)) || isHomework || !state.enableEvaluation} onClick={this.authBefore("evaluating", this.evaluating)} /> */}
+						<span>评测</span>
+					</div>
+					<div class={styles.btn} id="tips-step-4">
+						<img class={styles.iconBtn} src={headImg(`section${state.section.length}.svg`)} />
+						{/* <Button class={styles.button} icon={Icons["section" + state.section.length]} color="#01C1B5" disabled={runtime.isFirstPlay || runtime.evaluatingStatus || isHomework} onClick={this.authBefore("excerpts", RuntimeUtils.sectionChange)} /> */}
+						<span>选段</span>
+					</div>
+					<div class={styles.btn} id="tips-step-5" onClick={() => togglePlay()}>
+						<div class={styles.btnWrap}>
+							<img style={{ display: state.playState === "paused" ? "block" : "none" }} class={styles.iconBtn} src={headImg("icon-play.svg")} />
+							<img style={{ display: state.playState !== "paused" ? "block" : "none" }} class={styles.iconBtn} src={headImg("icon-pause.svg")} />
+							<Circle class={styles.progress} stroke-width={80} currentRate={state.audioData.progress} rate={100} layerColor="#01C1B5" color="#FFC830" />
+						</div>
+						{/* <Button class={styles.button} icon={runtime.playState === "play" || runtime.metroing ? Icons.pause : Icons.play} disabled={this.playerButtonIsDisabled} color={runtime.loading ? "#01C1B5" : ""} />
+						<Circle class={classnames(styles.circle, styles.button)} stroke-width={80} currentRate={progress} rate={100} layerColor="#01C1B5" color="#FFC830" /> */}
+						<span>{state.playState === "play" ? "暂停" : "播放"}</span>
+					</div>
+					<div
+						class={styles.btn}
+						id="tips-step-6"
+						onClick={() => {
+							state.playSource = state.playSource === "music" ? "background" : "music";
+						}}
+					>
+						<img style={{ display: state.playSource === "music" ? "block" : "none" }} class={styles.iconBtn} src={headImg("icon-music.svg")} />
+						<img style={{ display: state.playSource !== "music" ? "block" : "none" }} class={styles.iconBtn} src={headImg("icon-background.svg")} />
+						{/* <Button
+							class={styles.button}
+							disabled={this.changeModeIsDisabled || isHomework}
+							icon={runtime.mode === "background" ? Icons.background : Icons.music}
+							onClick={this.authBefore("switch", () => {
+								RuntimeUtils.changeMode(runtime.mode === "background" ? "music" : "background");
+							})}
+						/> */}
+						<span>{state.playSource === "music" ? "原声" : "伴奏"}</span>
+					</div>
+					<div
+						class={styles.btn}
+						onClick={async () => {
+							metronomeData.lineShow = !metronomeData.lineShow;
+						}}
+					>
+						<img class={styles.iconBtn} src={headImg("iconStep.png")} />
+						<span>{metronomeData.lineShow ? "高级" : "初级"}</span>
+					</div>
+					<div
+						class={styles.btn}
+						onClick={async () => {
+							metronomeData.disable = !metronomeData.disable;
+						}}
+					>
+						<img style={{ display: metronomeData.disable ? "block" : "none" }} class={styles.iconBtn} src={headImg("tickoff.png")} />
+						<img style={{ display: !metronomeData.disable ? "block" : "none" }} class={styles.iconBtn} src={headImg("tickon.png")} />
+						<span style={{ whiteSpace: "nowrap" }}>节拍器</span>
+					</div>
+					<div class={styles.btn} id="tips-step-7">
+						<img class={styles.iconBtn} src={headImg("replay.svg")} />
+						{/* <Button
+							class={styles.button}
+							icon={Icons.replay}
+							disabled={runtime.isFirstPlay || runtime.evaluatingStatus || isHomework}
+							onClick={this.authBefore("follow", async () => {
+								if (state.activeTick > -1) {
+									return;
+								}
+								RuntimeUtils.setCurrentTime(0);
+								RuntimeUtils.ended(new Event("ended"));
+							})}
+						/> */}
+						<span>重播</span>
+					</div>
+
+					<Popover trigger="manual" v-model:show={headData.speedShow} placement="bottom" overlay={false}>
+						{{
+							reference: () => (
+								<div
+									id="tips-step-8"
+									class={[styles.btn]}
+									onClick={() => {
+										headData.speedShow = !headData.speedShow;
+									}}
+								>
+									<Badge class={styles.badge} content={120}>
+										<img class={styles.iconBtn} src={headImg("speed.svg")} />
+									</Badge>
+									<span>速度</span>
+								</div>
+							),
+							default: () => <Speed />,
+						}}
+					</Popover>
+					<div class={styles.btn}>
+						<img class={styles.iconBtn} src={headImg("menu.svg")} />
+						<span>设置</span>
+					</div>
+				</div>
+			</div>
+		);
+	},
+});

+ 35 - 0
src/page-gym/header-top/speed/index.module.less

@@ -0,0 +1,35 @@
+.speedContainer {
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  background-color: #fff;
+  padding: 4px;
+}
+
+.btn {
+  width: 30px;
+  height: 30px;
+  border: none;
+  background-color: transparent;
+  padding: 0;
+  font-size: 18px;
+}
+
+.slider {
+  height: 40vh;
+  margin: 10px 0;
+}
+
+.customButton {
+  width: 36px;
+  height: 23px;
+  font-size: 12px;
+  font-weight: bold;
+  border-radius: 14px;
+  box-shadow: 0 5px 10px rgba(0, 0, 0, .1);
+  background: #fff;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}

+ 86 - 0
src/page-gym/header-top/speed/index.tsx

@@ -0,0 +1,86 @@
+import { defineComponent, reactive } from "vue";
+import { Button, Slider } from "vant";
+import styles from "./index.module.less";
+import { headImg } from "../image";
+import state from "../../../state";
+
+export default defineComponent({
+	name: "speed",
+	props: {
+		changed: {
+			type: Function,
+			default: (speed: number) => {},
+		},
+		updateSpeed: {
+			type: Function,
+			default: (speed: number) => {},
+		},
+		mode: {
+			type: String,
+		},
+		changeMode: {
+			type: Function,
+			default: (val: string) => {},
+		},
+		lib: {
+			type: Object,
+		},
+	},
+	setup(props) {
+		state.speed = props.lib?.speed;
+        const speed = reactive({
+            value: 70
+        })
+		const changeSpeed = (speed: number) => {
+			state.speed = speed;
+			props.changed(state.speed);
+		};
+
+		const updateSpeed = (speed: number) => {
+			state.speed = speed;
+			props.updateSpeed(state.speed);
+		};
+
+		const minusSpeed = () => {
+
+			speed.value = Math.max(speed.value - 1, 45);
+			state.speed = Math.max(state.speed - 1, 45);
+			props.changed(state.speed);
+		};
+
+		const plusSpeed = () => {
+            speed.value = Math.min(speed.value + 1, 270)
+			state.speed = Math.min(state.speed + 1, 270);
+			props.changed(state.speed);
+		};
+
+		return () => (
+			<div class={styles.speedContainer}>
+				<Button class={styles.btn} icon={headImg("plus.png")} disabled={state.speed == 270} onClick={plusSpeed} />
+				<Slider
+					class={styles.slider}
+					max={270}
+					min={45}
+					vertical
+                    v-model={speed.value}
+                    reverse
+				>
+					{{
+                        button: () => <div class={styles.customButton}>{speed.value}</div>
+                    }}
+				</Slider>
+				<Button class={styles.btn} icon={headImg("minus.png")} disabled={state.speed == 45} onClick={minusSpeed} />
+			</div>
+		);
+	},
+	methods: {
+		resetSpeed() {
+			state.speed = this.lib?.speed;
+			this.changed(this.lib?.speed);
+		},
+		refUpdateSpeed(speed: number) {
+			state.speed = speed;
+			this.updateSpeed(state.speed);
+		},
+	},
+});

+ 25 - 0
src/page-gym/header-top/title/index.module.less

@@ -0,0 +1,25 @@
+.container {
+  width: 170px;
+  height: 31px;
+  background: #fff;
+  display: flex;
+  align-items: center;
+  border-radius: 18px;
+  padding: 6px;
+
+  .noticeBar {
+    flex: 1;
+    padding: 0 6px;
+  }
+}
+
+.icon {
+  width: 26px;
+  height: 26px;
+  flex-shrink: 0;
+}
+
+.status {
+  margin-left: auto;
+  flex-shrink: 0;
+}

+ 39 - 0
src/page-gym/header-top/title/index.tsx

@@ -0,0 +1,39 @@
+import { defineComponent } from 'vue'
+import { NoticeBar } from 'vant'
+import styles from './index.module.less'
+
+import MusicIcon from '../image/music.png'
+import ArrowIcon from '../image/arrow.svg'
+
+export default defineComponent({
+  name: 'detail-title',
+  props: {
+    text: {
+      type: String,
+      default: ''
+    },
+    rightView: {
+      type: Boolean,
+      default: true,
+    },
+    onClick: {
+      type: Function,
+    } as any
+  },
+  render() {
+    return (
+      <div class={styles.container}>
+        <img class={styles.icon} src={MusicIcon}/>
+        <NoticeBar
+          text={this.text}
+          color="#000"
+          class={styles.noticeBar}
+          background="none"
+        />
+        {this.rightView ? (
+          <img class={styles.status} src={ArrowIcon}/>
+        ) : null}
+      </div>
+    )
+  }
+})

+ 74 - 61
src/state.ts

@@ -1,69 +1,69 @@
 import { Toast } from "vant";
 import { reactive, watchEffect } from "vue";
-
-export type GradualNote = GradualItem[];
-export type GradualItem = {
-	start: number;
-	measureIndex: number;
-	noteInMeasureIndex: number;
-	allDuration: number;
-	leftDuration: number;
-	type: string;
-	closedMeasureIndex: number;
-};
-
-type IMode = "homework" | "contact" | "evaluating";
-
-export enum GradualVersion {
-	BASE,
-	ENSEMBLE,
-}
-
-export type GradualTimes = null | {
-	[key: string]: string;
-};
+import { OpenSheetMusicDisplay } from "../osmd-extended/src";
+import { GradualNote, GradualTimes, GradualVersion, IMode } from "./type";
+import { audioList } from "./view/audio-list";
 
 const state = reactive({
 	/** 曲谱资源URL */
 	xmlUrl: "",
-    /** 声部ID */
+	/** 声部ID */
 	subjectId: 0 as number,
-    /** 分类ID */
-    categoriesId: 0,
-    /** 分类名称 */
-    categoriesName: '',
-    /** 是否支持评测 */
-    enableEvaluation: true,
-    /** 曲谱ID */
-    examSongId: 0,
-    /** 曲谱名称 */
-    examSongName: '',
-    /** 扩展字段 */
-    extConfigJson: {},
-    /** 是否开启节拍器 */
-    isOpenMetronome: false,
-    /** 是否显示指法 */
-    isShowFingering: false,
-    /** 原音 */
-    music: '',
-    /** 伴奏 */
-    accompany: '',
-    /** midiURL */
-    midiUrl: '',
-    /** 父分ID */
-    parentCategoriesId: 0,
-    /** 资源类型: mp3 | midi */
-    playMode: 'MP3',
-    /** 后台设置速度 */
-    speed: 0,
-    /** 分轨名称 */
-    track: '',
+	/** 分类ID */
+	categoriesId: 0,
+	/** 分类名称 */
+	categoriesName: "",
+	/** 是否支持评测 */
+	enableEvaluation: true,
+	/** 曲谱ID */
+	examSongId: "",
+	/** 曲谱名称 */
+	examSongName: "",
+	/** 扩展字段 */
+	extConfigJson: {} as any,
+	/** 是否开启节拍器 */
+	isOpenMetronome: false,
+	/** 是否显示指法 */
+	isShowFingering: false,
+	/** 原音 */
+	music: "",
+	/** 伴奏 */
+	accompany: "",
+	/** midiURL */
+	midiUrl: "",
+	/** 父分ID */
+	parentCategoriesId: 0,
+	/** 资源类型: mp3 | midi */
+	playMode: "MP3",
+	/** 后台设置速度 */
+	speed: 0,
+	/** 分轨名称 */
+	track: "",
 	/** 当前显示声部索引 */
 	partIndex: 0,
-    
+	/** 是否需要节拍器 */
+	needTick: false,
+	/** 曲谱实例 */
+	osmd: null as unknown as OpenSheetMusicDisplay,
+	/**是否是特殊乐谱类型, 主要针对管乐迷  */
+	isSpecialBookCategory: false,
+	/** 选段数据 */
+	section: [] as any[],
+	/** 播放状态 */
+	playState: "paused" as "play" | "paused",
+	/** 原音,伴奏 */
+	playSource: "music" as "music" | "background",
+	/** 播放器实例 */
+	audioData: {
+		/** 播放进度 */
+		progress: 0
+
+	},
+	
+	repeatedBeats: 0,
+
 	sectionStatus: false,
 	maskStatus: false,
-	section: [] as any[],
 	times: [] as any[],
 	timesById: {} as any,
 	sectionBoundingBoxs: [] as any[],
@@ -84,18 +84,16 @@ const state = reactive({
 	activeDetail: null as any,
 	// 是否跳过节拍器时间
 	skipTick: false,
-	needTick: false, // 是否需要节拍器
-	// 是否需要节拍器
-	repeatedBeats: 0,
+
 	sectionFlash: false,
 	befireSection: null as any,
 	/** 是否是打击乐 */
 	isPercussion: false,
 	isAppPlay: false, // 是否是app播放
 	partListNames: [] as string[], // 当前曲谱中所有声部名字
-	
+
 	midiPlayIniting: false, // midi播放器是否初始化中
-	isSpecialBookCategory: false, // 是否是特殊乐谱类型
+
 	/** 渐变速度信息 */
 	gradual: [] as GradualNote[],
 	/** 渐变速度版本 */
@@ -105,7 +103,22 @@ const state = reactive({
 	/** 单声部多声轨 */
 	multitrack: 0,
 	/** 缩放 */
-	zoom: 0.7,
+	zoom: 0.8,
 });
+/** 音频加载完成 */
+export const onLoadedmetadata = (evt: Event) => {
+	// console.log(evt)
+}
+/** 播放中事件 */
+export const onTimeupdate = (evt: Event) => {
+	console.log(evt.timeStamp)
+};
+/** 播放完成事件 */
+export const onEnded = (evt: Event) => {};
+
+export const togglePlay = () => {
+	state.playState = state.playState === "paused" ? "play" : "paused";
+	audioList.audioPlay(state.playState);
+};
 
 export default state;

+ 21 - 0
src/type.ts

@@ -0,0 +1,21 @@
+export type GradualNote = GradualItem[];
+export type GradualItem = {
+	start: number;
+	measureIndex: number;
+	noteInMeasureIndex: number;
+	allDuration: number;
+	leftDuration: number;
+	type: string;
+	closedMeasureIndex: number;
+};
+
+export type IMode = "homework" | "contact" | "evaluating";
+
+export enum GradualVersion {
+	BASE,
+	ENSEMBLE,
+}
+
+export type GradualTimes = null | {
+	[key: string]: string;
+};

+ 8 - 0
src/utils/index.ts

@@ -38,3 +38,11 @@ export const setToken = (value: any) => {
 export const getToken = () => {
 	return sessionStorage.getItem(AuthorizationKey) || "";
 };
+
+/** 设置全局通信 */
+export const setGlobalData = (_key: string, _value: any) => {
+	if (!_key || !_value) return;
+	const GYM = (window as any).GYM || {};
+	GYM[_key] = _value;
+	(window as any).GYM = GYM;
+};

+ 7 - 0
src/view/audio-list/index.module.less

@@ -0,0 +1,7 @@
+.audioList{
+    position: fixed;
+    left: 0;
+    bottom: 0;
+    width: 100%;
+    z-index: -1000;
+}

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

@@ -0,0 +1,32 @@
+import { computed, defineComponent, reactive } from "vue";
+import state, { onEnded, onLoadedmetadata, onTimeupdate } from "../../state";
+import styles from './index.module.less'
+
+export const audioList = reactive({
+	song: [] as HTMLAudioElement[],
+	audioPlay: async (playState: "play" | "paused") => {
+		for (let _audio of audioList.song) {
+			if (playState == "play") {
+				_audio.play();
+			} else {
+				_audio.pause();
+			}
+		}
+	},
+});
+
+export default defineComponent({
+	name: "audio-list",
+	setup() {
+		/** 原音是否静音 */
+		const isMusicMuted = computed(() => {
+			return state.playSource === "music";
+		});
+		return () => (
+			<div class={styles.audioList}>
+				<audio muted={!isMusicMuted.value} preload="auto" ref={(el) => audioList.song.push(el as HTMLAudioElement)} controls src={state.music} onLoadedmetadata={onLoadedmetadata} onTimeupdate={onTimeupdate} onEnded={onEnded} />
+				<audio muted={isMusicMuted.value} preload="auto" ref={(el) => audioList.song.push(el as HTMLAudioElement)} controls src={state.accompany} />
+			</div>
+		);
+	},
+});

+ 0 - 14
src/view/music-score/index.module.less

@@ -1,14 +0,0 @@
-.skeleton {
-    padding: 60px 30px;
-    --van-skeleton-paragraph-height: 1rem;
-
-    :global {
-        .van-skeleton-title {
-            margin: 0 auto;
-        }
-
-        .van-skeleton-paragraph:last-child {
-            width: 100% !important;
-        }
-    }
-}

+ 63 - 24
src/view/music-score/index.tsx

@@ -1,28 +1,67 @@
 import { Skeleton } from "vant";
-import { defineComponent, onBeforeMount, reactive } from "vue";
-import { onlyVisible } from "../../helpers/formateMusic";
+import { defineComponent, nextTick, onBeforeMount, reactive } from "vue";
+import { formatXML, onlyVisible } from "../../helpers/formateMusic";
 import state from "../../state";
-import styles from './index.module.less'
+import styles from "./index.module.less";
+// // @ts-ignore
+import { OpenSheetMusicDisplay } from "/osmd-extended/src";
 
 export default defineComponent({
-    name: 'music-score',
-    emits:['rendered'],
-    setup(){
-        const musicData = reactive({
-            isRenderLoading: true
-        })
-        const {xmlUrl, partIndex} = state
-        console.log("🚀 ~ xmlUrl, partIndex:", xmlUrl, partIndex)
-        const getXML = async () => {
-            const res = await fetch(xmlUrl).then(response => response.text())
-            const score = onlyVisible(res, state.partIndex)
-            // console.log("🚀 ~ res:", res)
-        }
-        onBeforeMount(async () => {
-            await getXML()
-        })
-        return () => <div>
-            <Skeleton class={styles.skeleton} loading={musicData.isRenderLoading} row={8} title></Skeleton>
-        </div>
-    }
-})
+	name: "music-score",
+	emits: ["rendered"],
+	setup(prop, {emit}) {
+		const musicData = reactive({
+			isRenderLoading: true,
+            score: ''
+		});
+		const getXML = async () => {
+			const res = await fetch(state.xmlUrl).then((response) => response.text());
+            const xml = formatXML(res)
+			musicData.score = onlyVisible(xml, state.partIndex);
+		};
+		const init = async () => {
+			const container: HTMLElement = document.querySelector("#musicContainer")!;
+			// console.log("🚀 ~ container:", container);
+			if (!container || !musicData.score) return;
+			const osmd = new OpenSheetMusicDisplay(container, {
+				drawTitle: false,
+				drawSubtitle: false,
+				drawMeasureNumbers: false,
+				autoResize: false,
+				followCursor: false,
+				drawPartNames: false, // 是否渲染声部
+
+				// autoBeam: true,
+				// drawMetronomeMarks: false,
+				// drawComposer: false,
+				// drawLyricist: false,
+				// ...this.opotions,
+			}, );
+            
+            // osmd.EngravingRules.CompactMode = true // 紧凑模式
+            osmd.EngravingRules.PageRightMargin = 2
+            osmd.EngravingRules.PageTopMargin = 2
+            osmd.EngravingRules.PageLeftMargin = 2
+            osmd.EngravingRules.PageBottomMargin = 2
+            osmd.EngravingRules.PageBottomMargin = 2
+            await osmd.load(musicData.score)
+            osmd.zoom = state.zoom
+            osmd.render()
+            // console.log("🚀 ~ osmd:", osmd)
+            emit('rendered', osmd)
+		};
+		onBeforeMount(async () => {
+			await getXML();
+            await init();
+            musicData.isRenderLoading = false;
+		});
+		return () => (
+			<div>
+                {/* <button onClick={() => {
+                    state.osmd.cursor.next()
+                }}>next</button> */}
+				<div id="musicContainer"></div>
+			</div>
+		);
+	},
+});

+ 10 - 1
tsconfig.json

@@ -11,7 +11,16 @@
     "esModuleInterop": true,
     "lib": ["ESNext", "DOM"],
     "skipLibCheck": true,
-    "noEmit": true
+    "noEmit": true,
+    "baseUrl": "./",
+    "paths": {
+      "/src/*": [
+        "src/*"
+      ],
+      "/osmd-extended/*": [
+        "osmd-extended/*"
+      ]
+    },
   },
   "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
   "references": [{ "path": "./tsconfig.node.json" }]

Some files were not shown because too many files changed in this diff