Przeglądaj źródła

Merge branch 'new-feature-tianyong' into ktyq-online

TIANYONG 1 rok temu
rodzic
commit
b3fb7453c2

+ 8 - 0
src/components/the-audio/index.module.less

@@ -0,0 +1,8 @@
+.wrap {
+    background: goldenrod;
+    position: fixed;
+    left: 0;
+    bottom: 0;
+    width: 100%;
+    z-index: 999;
+}

+ 6 - 1
src/components/the-audio/index.tsx

@@ -1,4 +1,4 @@
-import { defineComponent, reactive, ref } from "vue";
+import { defineComponent, reactive, ref, onMounted } from "vue";
 import styles from "./index.module.less";
 import { Icon } from "vant";
 export default defineComponent({
@@ -11,6 +11,11 @@ export default defineComponent({
     },
 	setup(props) {
         const videoRef = ref()
+		onMounted(() => {
+			var audio: any = document.querySelector('audio');
+			audio.volume = 0.1;
+			
+		})
 		return () => (
 			<div class={styles.wrap}>
 				<audio ref={videoRef} preload="auto" controls src={props.src}></audio>

+ 111 - 0
src/helpers/communication.ts

@@ -69,6 +69,18 @@ export const api_startRecording = (content: any): Promise<IPostMessage | undefin
 	if (!storeData.isApp) return Promise.resolve({} as any);
 	return promisefiyPostMessage({ api: "startRecording", content: content });
 };
+/** 评测开始录音 */
+export const api_startRecordingCb = (content: any, callback: CallBack) => {
+	postMessage(
+		{
+			api: "startRecording",
+			content,
+		},
+		callback
+	);
+};
+
+
 /** 评测结束录音 */
 export const api_stopRecording = (): Promise<IPostMessage | undefined> => {
 	if (!storeData.isApp) return Promise.resolve({} as any);
@@ -362,4 +374,103 @@ export const removeSocketStatus = (callback: CallBack) => {
 /** 检查APP端websocket状态 */
 export const api_disconnectSocket = () => {
 	return promisefiyPostMessage({ api: "disconnectSocket" });
+};
+
+
+
+// MIDI播放&评测相关的api
+
+/** 发送midi音频等信息 */
+export const api_cloudDetail = (content: any, callback: CallBack) => {
+	postMessage(
+		{
+			api: "cloudDetail",
+			content,
+		},
+		callback
+	);
+};
+
+/** 检查midi播放器状态,status: 'init' | 'play' | 'suspend' */
+export const api_cloudGetMediaStatus = () => {
+	return promisefiyPostMessage({ api: "cloudGetMediaStatus" });
+};
+
+/** midi开始播放 */
+export const api_cloudPlay = (content: any) => {
+	promisefiyPostMessage({
+		api: "cloudPlay",
+		content,
+	});
+};
+
+/** midi暂停播放 */
+export const api_cloudSuspend = (content: any) => {
+	promisefiyPostMessage({
+		api: "cloudSuspend",
+		content,
+	});
+};
+
+/** midi跳转到指定位置播放 */
+export const api_cloudSetCurrentTime = (content: any) => {
+	promisefiyPostMessage({
+		api: "cloudSetCurrentTime",
+		content,
+	});
+};
+
+/** midi调整播放速度 */
+export const api_cloudChangeSpeed = (content: any) => {
+	promisefiyPostMessage({
+		api: "cloudChangeSpeed",
+		content,
+	});
+};
+
+/** midi设置声轨音量 */
+export const api_cloudVolume = (content: any) => {
+	promisefiyPostMessage({
+		api: "cloudVolume",
+		content,
+	});
+};
+
+/** midi,播放系统节拍器 */
+export const api_cloudMetronome = (content: any, callback: CallBack) => {
+	postMessage(
+		{
+			api: "cloudMetronome",
+			content,
+		},
+		callback
+	);
+};
+
+/** midi练习播放&评测播放回调 */
+export const api_cloudTimeUpdae = (callback: any) => {
+	listenerMessage("cloudTimeUpdae", callback);
+};
+
+/** 卸载监听midi播放回调 */
+export const api_remove_cloudTimeUpdae = (callback: any) => {
+	removeListenerMessage("cloudTimeUpdae", callback);
+};
+
+/** midi播放结束回调 */
+export const api_cloudplayed = (callback: any) => {
+	listenerMessage("cloudplayed", callback);
+};
+
+/** 卸载midi播放结束回调 */
+export const api_remove_cloudplayed = (callback: any) => {
+	removeListenerMessage("cloudplayed", callback);
+};
+
+/** midi评测传offsetTime和micDelay */
+export const api_midiMicDelay = (content: any) => {
+	postMessage({
+		api: "proxyServiceMessage",
+		content,
+	});
 };

+ 2 - 2
src/helpers/formateMusic.ts

@@ -935,8 +935,8 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 				continue;
 			}
 			// console.log(iterator.currentMeasure)
-			// 如果是弱起就补齐缺省的时长
-			if (i === 0) {
+			// 如果是弱起就补齐缺省的时长,midi音频不需要考虑弱起
+			if (i === 0 && !state.isAppPlay) {
 				let _firstMeasureRealValue = 0;
 				const staffEntries = note.sourceMeasure.verticalMeasureList?.[0]?.staffEntries || [];
 				//计算第一个小节里面的音符时值是否等于整个小节的时值

+ 67 - 12
src/helpers/metronome.ts

@@ -9,12 +9,21 @@ import { browser } from "/src/utils/index";
 import state from "/src/state";
 import { Howl } from "howler";
 import tockAndTick from "/src/constant/tockAndTick.json";
+import tickWav from "/src/assets/tick.wav";
+import tockWav from "/src/assets/tock.wav";
+
 type IOptions = {
 	speed: number;
 };
 const browserInfo = browser();
 let tipsTimer: any = null; // 光标提示定时器
 
+// HTMLAudioElement 音频
+const audioData = reactive({
+	tick: null as unknown as HTMLAudioElement,
+	tock: null as unknown as HTMLAudioElement,
+});
+
 export const metronomeData = reactive({
 	disable: true,
 	initPlayerState: false,
@@ -101,14 +110,39 @@ class Metronome {
 		metronomeData.activeList = [];
 	}
 	initPlayer() {
-		if (!this.source1) {
-			this.source1 = this.loadAudio1();
-		}
-		if (!this.source2) {
-			this.source2 = this.loadAudio2();
-		}
-		metronomeData.initPlayerState = true;
+		// if (!this.source1) {
+		// 	this.source1 = this.loadAudio1();
+		// }
+		// if (!this.source2) {
+		// 	this.source2 = this.loadAudio2();
+		// }
+		// metronomeData.initPlayerState = true;
+
+		Promise.all([this.createAudio(tickWav), this.createAudio(tockWav)]).then(
+			([tick, tock]) => {
+				if (tick) {
+					audioData.tick = tick;
+				}
+				if (tock) {
+					audioData.tock = tock;
+				}
+				metronomeData.initPlayerState = true;
+			}
+		);		
 	}
+	createAudio = (src: string): Promise<HTMLAudioElement | null> => {
+		return new Promise((resolve) => {
+			// const a = new Audio(src + '?v=' + Date.now());
+			const a = new Audio(src);
+			a.load();
+			a.onloadedmetadata = () => {
+				resolve(a);
+			};
+			a.onerror = () => {
+				resolve(null);
+			};
+		});
+	};
 
 	// 播放
 	sound = (currentTime: number) => {
@@ -142,10 +176,18 @@ class Metronome {
 	};
 	// 播放
 	playAudio = () => {
-		if (!metronomeData.initPlayerState) return;
+		if (!metronomeData.initPlayerState || state.playState === 'paused') return;
 		const beatVolume = state.setting.beatVolume / 100
-		this.source = metronomeData.activeMetro?.index === 0 ? this.source1 : this.source2;
-		this.source.volume(metronomeData.disable || state.playState === 'paused' ? 0 : beatVolume);
+		// this.source = metronomeData.activeMetro?.index === 0 ? this.source1 : this.source2;
+		// this.source.volume(metronomeData.disable || state.playState === 'paused' ? 0 : beatVolume);
+		// Audio 播放音频
+		this.source = metronomeData.activeMetro?.index === 0 ? audioData.tick : audioData.tock;
+		this.source.volume = metronomeData.disable ? 0 : beatVolume;
+		if (this.source.volume <= 0) {
+			this.source.muted = true
+		} else {
+			this.source.muted = false
+		}
 		this.source.play();
 	};
 
@@ -156,8 +198,21 @@ class Metronome {
 		// console.log(333, metronomeData.followAudioIndex)
 		if (!metronomeData.initPlayerState) return;
 		const beatVolume = state.setting.beatVolume / 100
-		this.source = metronomeData.followAudioIndex === 1 ? this.source1 : this.source2;
-		this.source.volume(metronomeData.disable ? 0 : beatVolume);
+		// this.source = metronomeData.followAudioIndex === 1 ? this.source1 : this.source2;
+		// Audio 播放音频
+		this.source = metronomeData.followAudioIndex === 1 ? audioData.tick : audioData.tock;
+		// this.source.volume(metronomeData.disable ? 0 : beatVolume);
+		this.source.volume = metronomeData.disable ? 0 : beatVolume
+		/**
+		 * https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLMediaElement/volume
+		 * volume属性在部分ios手机的Safari浏览器不被支持
+		 */
+		if (this.source.volume <= 0) {
+			this.source.muted = true
+		} else {
+			this.source.muted = false
+		}
+		console.log('音量',this.source,this.source.volume)
 		this.source.play();
 		metronomeData.followAudioIndex += 1;
 		metronomeData.followAudioIndex = metronomeData.followAudioIndex > metronomeData.totalNumerator ? 1 : metronomeData.followAudioIndex;

+ 122 - 0
src/helpers/midiPlay.tsx

@@ -0,0 +1,122 @@
+/**
+ * app播放midi
+ */
+
+import { ref } from 'vue'
+import { getDuration } from "/src/helpers/formateMusic";
+import state, { onPlay } from "/src/state";
+import { OpenSheetMusicDisplay } from "/osmd-extended/src";
+import { api_cloudDestroy, api_cloudDetail, api_cloudVolume, api_cloudGetMediaStatus, 
+  api_cloudPlay, api_cloudSuspend,  } from "/src/helpers/communication";
+import { audioData } from "/src/view/audio-list"  
+
+export type IMode = 'background' | 'music'
+
+export const initMidi = (durationNum: number, midiUrl?: string) => {
+  const initial = ref(false)
+  if (midiUrl) {
+    console.log('曲谱为midi,使用app播放')
+    initial.value = true
+    state.midiPlayIniting = true
+    const duration: any = getDuration(state.osmd as unknown as OpenSheetMusicDisplay);
+    // 销毁播放器
+    api_cloudDestroy();
+    // 发送初始化信息
+    api_cloudDetail({
+      midi: midiUrl,
+      denominator: duration.denominator,
+      numerator: duration.numerator,
+      originalSpeed: state.originSpeed,
+      interval: 50,
+      duration: durationNum * 1000,
+    }, () => {
+      state.midiPlayIniting = false
+      initial.value = false
+      if (midiUrl) {
+        changeMode('music')
+      }
+    })
+    state.durationNum = durationNum
+  }
+  return {
+    initial,
+  }
+}
+
+/** 获取当前MidiId */
+export const getActiveMidiId = () => {
+  return state.osmd?.sheet?.instruments?.[0]?.subInstruments?.[0]?.midiInstrumentID ?? 0
+}
+
+/**
+ * 修改原音或伴奏
+ * @param val IMode
+ */
+export const changeMode = async (val: IMode, type?: string | undefined) => {
+  const cm: IMode = val === 'background' ? 'music' : 'background'
+  console.log(!state.songs[val], val, cm)
+  if (state.isAppPlay) {
+    const data = new Map()
+    for (const name of state.partListNames) {
+      data.set(name, 60)
+    }
+    // for (const name of getVoicePartInfo().partListNames) {
+    //   data.set(name, cm === 'background' ? 100 : 0)
+    // }
+    api_cloudVolume({
+      activeMidiId: getActiveMidiId(),
+      activeMidiVolume: cm === 'background' ? 100 : 0,
+      parts: Array.from(data.keys()).map((item) => ({
+        name: item,
+        volume: data.get(item),
+      })),
+    })
+  }
+  state.playSource = val
+  if (type === 'all') {
+    state.audiosInstance?.setMute(true, state.songs[cm])
+    state.audiosInstance?.setMute(true, state.songs[val])
+  } else {
+    state.audiosInstance?.setMute(true, state.songs[cm])
+    state.audiosInstance?.setMute(false, state.songs[val])
+  }
+}
+
+/**
+ * 切换midi播放状态
+ */
+export const cloudToggleState = async (type: "play" | "paused") => {
+  const cloudGetMediaStatus = await api_cloudGetMediaStatus();
+  const status = cloudGetMediaStatus?.content.status
+  // console.log('api','midi状态',status)
+  if (status === 'init') {
+    return
+  }
+  if (status === 'suspend' && type === 'paused') return
+  if (status === 'suspend') {
+    if (state.isSelectMeasureMode) {
+      audioData.progress = state.midiSectionStart
+    }
+    await api_cloudPlay({
+      songID: state.examSongId,
+      startTime: audioData.progress * 1000,
+      originalSpeed: state.originSpeed, // midi初始速度
+      speed: state.modeType === "evaluating" ? state.originSpeed : state.speed,
+      hertz: 440, //SettingState.sett.hertz,
+    })
+    // startCapture()
+    onPlay()
+  } else {
+    await api_cloudSuspend({
+      songID: state.examSongId,
+    })
+    // 如果是评测终止状态,需要重置进度s
+    if (state.modeType === "evaluating") {
+      audioData.progress = 0
+    }
+    // endCapture()
+  }
+  const cloudGetMediaStatused = await api_cloudGetMediaStatus()
+  state.playState = cloudGetMediaStatused?.content.status === "suspend" ? "paused" : "play"
+  console.log(cloudGetMediaStatused, 'cloudGetMediaStatused')
+}

+ 10 - 1
src/page-instrument/component/the-music-list/list.tsx

@@ -1,4 +1,4 @@
-import { defineComponent, onMounted, reactive } from "vue";
+import { defineComponent, onMounted, reactive, watch } from "vue";
 import styles from "./index.module.less";
 import { api_musicSheetPage } from "../../api";
 import state, { togglePlay } from "/src/state";
@@ -26,8 +26,10 @@ export default defineComponent({
 			list: [] as any[],
 			finished: false,
 			loading: false,
+			hasNext: true,
 		});
 		const getList = async () => {
+			if (!data.hasNext) return
 			data.loading = true;
 			try {
 				const res = await api_musicSheetPage({
@@ -37,12 +39,19 @@ export default defineComponent({
 					data.list = [...data.list, ...res.data.rows];
 				}
 				data.finished = res.data?.rows?.length < forms.rows;
+				data.hasNext = res.data?.total > data.list.length
 			} catch (error) {
 				console.log(error);
 			}
 
 			data.loading = false;
 		};
+		watch(
+			() => props.recentFlag,
+			() => {
+				data.hasNext = true
+			}
+		);
 		onMounted(() => {
 			getList();
 		});

+ 2 - 2
src/page-instrument/evaluat-model/earphone/index.tsx

@@ -10,8 +10,8 @@ export default defineComponent({
 			<div class={styles.fraction}>
 				<img class={styles.erji} src={icons.erji} />
 				<div class={styles.content}>
-					<div class={styles.title}>请佩戴耳机</div>
-					<div class={styles.tip}>佩戴耳机可以保证测评准确率哦~</div>
+					<div class={styles.title}>请佩戴有线耳机</div>
+					<div class={styles.tip}>佩戴有线耳机能保证评测准确率哦!</div>
 					<img src={icons.erjibtn} class={styles.btn} onClick={() => emit("close")} />
 				</div>
 			</div>

+ 26 - 3
src/page-instrument/evaluat-model/index.tsx

@@ -7,7 +7,7 @@ import state, { handleRessetState, resetPlaybackToStart, musicalInstrumentCodeIn
 import { storeData } from "/src/store";
 import { browser } from "/src/utils";
 import { getNoteByMeasuresSlursStart } from "/src/helpers/formateMusic";
-import { Icon, Popup, showToast } from "vant";
+import { Icon, Popup, showToast, closeToast, showLoadingToast } from "vant";
 import EvaluatResult from "./evaluat-result";
 import EvaluatAudio from "./evaluat-audio";
 import { api_getDeviceDelay, api_openAdjustRecording, api_proxyServiceMessage, api_videoUpdate, getEarphone, api_back } from "/src/helpers/communication";
@@ -249,7 +249,11 @@ export default defineComponent({
     /** 评测结果按钮处理 */
     const handleEvaluatResult = (type: "practise" | "tryagain" | "look" | "share" | "update") => {
       if (type === "update") {
-        if (evaluatingData.resultData?.recordIdStr || evaluatingData.resultData?.recordId) {
+        if (state.isAppPlay) {
+          evaluatModel.evaluatUpdateAudio = true;
+          resetPlaybackToStart()
+          return;
+        } else if (evaluatingData.resultData?.recordIdStr || evaluatingData.resultData?.recordId) {
           // 上传云端
           // evaluatModel.evaluatUpdateAudio = true;
           api_openAdjustRecording({
@@ -311,12 +315,31 @@ export default defineComponent({
     };
 
     const startBtnHandle = async () => {
+      // 如果是异常状态,先等待500ms再执行后续流程
+      if (evaluatingData.isErrorState && !state.setting.soundEffect) {
+        // console.log('异常流程1')
+        showLoadingToast({
+          message: "处理中",
+          duration: 1000,
+          overlay: true,
+          overlayClass: styles.scoreMode,
+        });
+        await new Promise<void>((resolve) => {
+          setTimeout(() => {
+            closeToast();
+            evaluatingData.isErrorState =false
+            // console.log('异常流程2')
+            resolve()
+          }, 1000);
+        })
+      }
+      // console.log('异常流程3')
       // 检测APP端socket状态
       const res: any = await startCheckDelay();
       if (res?.checked) {
         handleConnect();
         handleStartBegin(calculateInfo.firstNoteTime);
-        if (evaluatingData.isErrorState = true) {
+        if (evaluatingData.isErrorState) {
           evaluatingData.isErrorState = false;
           evaluatingData.resulstMode = false;
         }

+ 8 - 4
src/page-instrument/header-top/index.tsx

@@ -164,9 +164,10 @@ export default defineComponent({
       if (headTopData.modeType !== "show" || state.modeType === "follow") return { display: false, disabled: false };
       // 评测开始 禁用
       if (state.modeType === "evaluating") return { display: false, disabled: true };
-      // 原声, 伴奏 少一个,就不能切换
-      if (!state.music || !state.accompany) return { display: true, disabled: true };
-
+      if (!state.isAppPlay) {
+        // 原声, 伴奏 少一个,就不能切换
+        if (!state.music || !state.accompany) return { display: true, disabled: true };
+      }
       return {
         disabled: false,
         display: true,
@@ -192,7 +193,8 @@ export default defineComponent({
       if (headTopData.modeType !== "show") return { display: false, disabled: false };
       // 评测模式 不显示,跟练模式 不显示
       if (["evaluating", "follow"].includes(state.modeType)) return { display: false, disabled: true };
-
+      // midi音频未初始化完成不可点击
+      if (state.isAppPlay && state.midiPlayIniting) return { display: true, disabled: true };
       return {
         display: true,
         disabled: false,
@@ -209,6 +211,8 @@ export default defineComponent({
       if (state.playState === "play") return { display: false, disabled: true };
       // 播放进度为0 不显示
       const currentTime = getAudioCurrentTime();
+      // midi音频未初始化完成不可点击
+      if (state.isAppPlay && state.midiPlayIniting) return { display: false, disabled: true };
       if (!currentTime) return { display: false, disabled: true };
 
       return {

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

@@ -29,6 +29,9 @@ import { recalculateNoteData } from "/src/view/selection";
 import ToggleMusicSheet from "/src/view/plugins/toggleMusicSheet";
 import { setCustomGradual, setCustomNoteRealValue } from "/src/helpers/customMusicScore"
 import { usePageVisibility } from "@vant/use";
+import { initMidi } from "/src/helpers/midiPlay"
+import TheAudio from "/src/components/the-audio"
+import tickWav from "/src/assets/tick.wav";
 
 /**
  * 特殊教材分类id
@@ -43,7 +46,8 @@ const calcCeilFrequency = (frequency: number) => {
 /** 需要处理频率的乐器
  */
 const resetFrequency = (list: any[]) => {
-  const instrumentNames = ["ocarina", "pan-flute", "piccolo", "hulusi-flute"];
+  // const instrumentNames = ["ocarina", "pan-flute", "piccolo", "hulusi-flute"];
+  const instrumentNames = ["ocarina", "pan-flute", "hulusi-flute"];
   if (!state.fingeringInfo?.name || !instrumentNames.includes(state.fingeringInfo.name)) return list;
   console.log(state.subjectId, state.fingeringInfo.name, instrumentNames);
   for (let i = 0; i < list.length; i++) {
@@ -154,6 +158,12 @@ export default defineComponent({
       state.times = resetFrequency(state.times);
       state.times = setNoteHalfTone(state.times);
       console.log("🚀 ~ state.times:", state.times, state.subjectId, state);
+      // 初始化midi音频信息
+      const songEndTime = state.times[state.times.length - 1 || 0]?.endtime || 0
+      if (state.isAppPlay) {
+        const durationNum = songEndTime
+        initMidi(durationNum, state.midiUrl)
+      }
       state.measureTime = state.times[0]?.measureLength || 0
       try {
         metronomeData.metro = new Metronome();
@@ -366,6 +376,8 @@ export default defineComponent({
         {/* 播放 */}
         {!detailData.isLoading && <AudioList />}
 
+        {/* {!detailData.isLoading && <TheAudio src={tickWav} />} */}
+
         {/* 评测 */}
         {state.modeType === "evaluating" && (
           <>

+ 71 - 10
src/state.ts

@@ -6,10 +6,10 @@ import { GradualNote, GradualTimes, GradualVersion } from "./type";
 import { handleEndEvaluat, handleStartEvaluat } from "./view/evaluating";
 import { IFingering, mappingVoicePart, subjectFingering, matchVoicePart } from "/src/view/fingering/fingering-config";
 import { handleStartTick } from "./view/tick";
-import { audioListStart, getAudioCurrentTime, getAudioDuration, setAudioCurrentTime, setAudioPlaybackRate } from "./view/audio-list";
+import { audioListStart, getAudioCurrentTime, getAudioDuration, setAudioCurrentTime, setAudioPlaybackRate, audioData } from "./view/audio-list";
 import { toggleFollow } from "./view/follow-practice";
 import { browser, setStorageSpeed, setGlobalData } from "./utils";
-import { api_createMusicPlayer } from "./helpers/communication";
+import { api_cloudGetMediaStatus, api_createMusicPlayer, api_cloudChangeSpeed, api_cloudSuspend, api_cloudSetCurrentTime, api_cloudDestroy } from "./helpers/communication";
 import { verifyCanRepeat, getDuration } from "./helpers/formateMusic";
 import { getMusicSheetDetail } from "./utils/baseApi"
 import { getQuery } from "/src/utils/queryString";
@@ -40,6 +40,11 @@ export enum IPlatform {
   PC = "PC",
 }
 
+export type ISonges = {
+  background?: string
+  music?: string
+}
+
 /**
  * 特殊教材分类id
  */
@@ -364,7 +369,7 @@ const state = reactive({
   },
   /** 后台设置的基准评测频率 */
   baseFrequency: 440,
-  /** 节拍器的时间 */
+  /** mp3节拍器的时间,统计拍数、速度计算得出 */
   fixtime: 0,
   /** 指法信息 */
   fingeringInfo: {} as IFingering,
@@ -419,7 +424,17 @@ const state = reactive({
   /** 是否为详情预览模式 */
   isPreView: false,
   /** 是否为评测报告模式 */
-  isEvaluatReport: false,  
+  isEvaluatReport: false,
+  /** midi播放器是否初始化中 */  
+  midiPlayIniting: false,
+  /** 曲目信息 */
+  songs: {} as ISonges,  
+  isAppPlay: false, // 是否midi音频,midi是app播放
+  /** 音频播放器实例 */
+  audiosInstance: null as any,
+  /** midi音频的时长 */
+  durationNum: 0,
+  midiSectionStart: 0,
 });
 const browserInfo = browser();
 let offset_duration = 0;
@@ -432,6 +447,7 @@ export const customData = reactive({
 });
 /** 在渲染前后计算光标应该走到的音符 */
 const setStep = () => {
+  // console.log('播放状态',state.playState)
   if (state.playState !== "play") {
     console.log("暂停播放");
     return;
@@ -475,6 +491,10 @@ const autoResetPlay = () => {
 /** 播放完成事件 */
 export const onEnded = () => {
   console.log("音频播放结束");
+  // if (state.isAppPlay) {
+  //     // 销毁播放器
+  //     api_cloudDestroy();
+  // }
   // 修改状态为结束
   state.playEnd = true;
   state.playState = "paused";
@@ -535,14 +555,14 @@ const handlePlaying = () => {
 
       // if (Math.abs(selectEndItem.endtime - currentTime) < offset_duration) {
         if (currentTime - selectEndItem.endtime > offset_duration) {
-        console.log("选段播放结束");
+        console.log("选段播放结束",state.setting.repeatAutoPlay);
         // 如果为选段评测模式
         if (state.modeType === "evaluating" && state.isSelectMeasureMode) {
           onEnded();
           return;
         }
         // #8698 bug修复
-        if (state.modeType === "practise" && state.sectionStatus && !state.setting.repeatAutoPlay) {
+        if (state.modeType === "practise" && state.sectionStatus) {
           onEnded();
           resetPlaybackToStart();
           return;
@@ -561,7 +581,7 @@ const handlePlaying = () => {
   metronomeData.metro?.sound(currentTime);
 };
 /** 跳转到指定音符开始播放 */
-export const skipNotePlay = (itemIndex: number, isStart = false) => {
+export const skipNotePlay = async (itemIndex: number, isStart = false) => {
   const item = state.times[itemIndex];
   let itemTime = item.time;
   if (isStart) {
@@ -571,6 +591,14 @@ export const skipNotePlay = (itemIndex: number, isStart = false) => {
     setAudioCurrentTime(itemTime, itemIndex);
     gotoNext(item);
     metronomeData.metro?.sound(itemTime);
+    if (state.isAppPlay) {
+      await api_cloudSetCurrentTime({
+        currentTime: itemTime * 1000,
+        songID: state.examSongId,
+      })
+      audioData.progress = itemTime
+      state.midiSectionStart = itemTime
+    }
   }
 };
 
@@ -579,7 +607,26 @@ export const skipNotePlay = (itemIndex: number, isStart = false) => {
  * @param playState 可选: 默认 undefined, 需要切换的状态 play:播放, paused: 暂停
  */
 export const togglePlay = async (playState?: "play" | "paused") => {
-  state.playState = playState ? playState : state.playState === "paused" ? "play" : "paused";
+  // midi播放
+  if (state.isAppPlay) {
+    if( playState === "paused" ) {
+      await api_cloudSuspend({
+        songID: state.examSongId,
+      })
+      state.playState = 'paused'
+      return
+    }
+    await api_cloudChangeSpeed({
+      speed: state.modeType === "evaluating" ? state.originSpeed : state.speed,
+      originalSpeed: state.originSpeed,
+      songID: state.examSongId,
+    });
+    const cloudGetMediaStatus = await api_cloudGetMediaStatus();
+    const status = cloudGetMediaStatus?.content.status === "suspend" ? "play" : "paused"
+    state.playState = status
+  } else {
+    state.playState = playState ? playState : state.playState === "paused" ? "play" : "paused";
+  }
   if (state.playState === "play" && state.sectionStatus && state.section.length == 2 && state.playProgress === 0) {
     resetPlaybackToStart();
   }
@@ -701,7 +748,7 @@ export const getNote = (currentTime: number) => {
   const times = state.times;
   const len = state.times.length;
   /** 播放超过了最后一个音符的时间,直接结束, 2秒误差 */
-  if (currentTime > times[len - 1].endtime + 2) {
+  if (currentTime > times[len - 1].endtime + 2 && !state.isAppPlay) {
     onEnded();
     return;
   }
@@ -723,6 +770,10 @@ export const getNote = (currentTime: number) => {
 
 /** 重播 */
 export const handleResetPlay = () => {
+  // 如果是midi需要重置播放进度
+  if (state.isAppPlay) {
+    audioData.progress = 0
+  }
   resetPlaybackToStart();
   // 如果是暂停, 直接播放
   togglePlay("play");
@@ -896,6 +947,10 @@ export const handleRessetState = () => {
   // 切换模式,清除选段
   skipNotePlay(0, true);
   clearSelection();
+  // midi 重置播放进度
+  if (state.isAppPlay) {
+    audioData.progress = 0;
+  }
   if (state.modeType === "evaluating") {
     handleStartEvaluat();
   } else if (state.modeType === "practise") {
@@ -981,6 +1036,8 @@ const setState = (data: any, index: number) => {
   // state.isOpenMetronome = data.isUseSystemBeat ? false : true;
   state.isOpenMetronome = data.isPlayBeat && !data.isUseSystemBeat ? true : false
   state.isShowFingering = data.isShowFingering ? true : false;
+  // 设置曲谱的播放模式, APP播放(midi音频是app播放) | h5播放
+  state.isAppPlay = data.playMode === 'MIDI';
   state.music = data.music;
   state.accompany = data.accompany;
   state.midiUrl = data.midiFileUrl;
@@ -993,7 +1050,11 @@ const setState = (data: any, index: number) => {
   const track = data.code || data.track;
   state.track = track ? track.replace(/ /g, "").toLocaleLowerCase() : "";
   // 能否评测,根据当前声轨有无伴奏判断
-  state.enableEvaluation = state.accompany ? true : false
+  if (state.isAppPlay) {
+    state.enableEvaluation = state.midiUrl ? true : false
+  } else {
+    state.enableEvaluation = state.accompany ? true : false
+  }
   state.isConcert = data.musicSheetType === "CONCERT" ? true : false;
   // multiTracksSelection 返回为空,默认代表全部分轨
   state.canSelectTracks = data.multiTracksSelection === "null" || data.multiTracksSelection === "" || data.multiTracksSelection === null ? [] : data.multiTracksSelection?.split(',');

+ 65 - 17
src/view/audio-list/index.tsx

@@ -1,4 +1,4 @@
-import { computed, defineComponent, onMounted, reactive, ref, watch } from "vue";
+import { computed, defineComponent, onMounted, onUnmounted, reactive, ref, watch } from "vue";
 import styles from "./index.module.less";
 import {
 	getMidiCurrentTime,
@@ -9,15 +9,16 @@ import {
 	setMidiCurrentTime,
 } from "./midiPlayer";
 import state, { IPlayState, onEnded, onPlay } from "/src/state";
-import { api_playProgress } from "/src/helpers/communication";
+import { api_playProgress, api_cloudTimeUpdae, api_cloudplayed, api_remove_cloudplayed, api_remove_cloudTimeUpdae } from "/src/helpers/communication";
 import { evaluatingData } from "/src/view/evaluating";
+import { cloudToggleState } from "/src/helpers/midiPlay"
 
-const audioData = reactive({
+export const audioData = reactive({
 	songEle: null as unknown as HTMLAudioElement,
 	backgroundEle: null as unknown as HTMLAudioElement,
 	midiRender: false,
-	progress: 0,
-	duration: 0
+	progress: 0, // midi播放进度(单位:秒)
+	duration: 0 // 音频总时长(单位:秒)
 });
 const midiRef = ref();
 /** 播放或暂停 */
@@ -26,9 +27,11 @@ export const audioListStart = (type: "play" | "paused") => {
 	if (type === "play" && state.originSpeed !== 0) {
 		setAudioPlaybackRate(state.speed / state.originSpeed);
 	}
+	// console.log('api','midi状态1',type,state.isAppPlay)
 	// 如果是midi播放
-	if (audioData.midiRender) {
-		handleTogglePlayMidi(type);
+	if (state.isAppPlay) {
+		// handleTogglePlayMidi(type);
+		cloudToggleState(type);
 		return;
 	}
 	if (type === "play") {
@@ -42,7 +45,8 @@ export const audioListStart = (type: "play" | "paused") => {
 /** 设置倍数播放 */
 export const setAudioPlaybackRate = (rate: number) => {
 	// 如果是midi播放
-	if (audioData.midiRender) {
+	if (state.isAppPlay) {
+		if (state.modeType === "evaluating") return
 		hanldeSetMidiPlaybackRate(rate);
 		return;
 	}
@@ -53,11 +57,11 @@ export const setAudioPlaybackRate = (rate: number) => {
 /** 获取当前播放的时间 */
 export const getAudioCurrentTime = () => {
 	// 如果是midi播放
-	if (audioData.midiRender) {
-		const c = getMidiCurrentTime();
-		return c;
+	if (state.isAppPlay) {
+		// const c = getMidiCurrentTime();
+		return audioData.progress;
 	}
-	// console.log('返回的时间',audioData.songEle?.currentTime,audioData.progress)
+	// console.log('返回的时间',state.playSource, audioData.songEle?.currentTime,audioData.progress)
 	if (state.playSource === "music") return audioData.songEle?.currentTime || audioData.progress;
 	if (state.playSource === "background") return audioData.backgroundEle?.currentTime || audioData.progress;
 	
@@ -66,9 +70,10 @@ export const getAudioCurrentTime = () => {
 /** 获取曲谱的总时间 */
 export const getAudioDuration = () => {
 	// 如果是midi播放
-	if (audioData.midiRender) {
-		const d = getMidiDuration();
-		return d;
+	if (state.isAppPlay) {
+		// const d = getMidiDuration();
+		const songEndTime = state.times[state.times.length - 1 || 0]?.endtime || 0
+		return audioData.duration || songEndTime;
 	}
 	return audioData.songEle?.duration || audioData.backgroundEle?.duration || audioData.duration;
 };
@@ -77,7 +82,7 @@ export const getAudioDuration = () => {
 export const setAudioCurrentTime = (time: number, index = 0) => {
 	// console.log('开始时间12345',time)
 	// 如果是midi播放
-	if (audioData.midiRender) {
+	if (state.isAppPlay) {
 		setMidiCurrentTime(index);
 		return;
 	}
@@ -113,7 +118,7 @@ export default defineComponent({
 		/** iframe 加载完成后, 加载midiURL */
 		const handleLoad = () => {
 			midiRef.value.contentWindow.handleRendered = () => {
-				audioData.midiRender = true;
+				audioData.midiRender = true
 			};
 			hanldeInitMidiData(midiRef.value);
 		};
@@ -166,6 +171,38 @@ export default defineComponent({
 			}
 		};
 
+		// midi播放进度回调
+		const midiProgress = (res: any) => {
+			// console.log('api',res)
+			if (audioData.duration == 0) {
+				const songEndTime = state.times[state.times.length - 1 || 0]?.endtime || 0
+				audioData.duration = songEndTime
+			}
+			const currentTime = res?.currentTime || res?.content?.currentTime;
+			const total = res?.totalDuration || res?.content?.totalDuration;
+			const time = currentTime / 1000;
+			audioData.progress = time;
+			// if (
+			// 	audioData.duration > 1000 &&
+			// 	currentTime >= audioData.duration * 1000
+			// ) {
+			// 	console.log('midi结束')
+			// 	onEnded();
+			// }
+			// 选段模式,播放结束
+			if (state.sectionStatus && state.section.length == 2 && currentTime >= state.section) {
+				//
+			}
+		}
+		// midi播放结束回调
+		const midiPlayEnd = (res: any) => {
+			if (res) {
+				console.log('midi结束')
+				audioData.progress = 0
+				onEnded();
+			}
+		}
+
 		onMounted(() => {
 			if (state.playMode !== "MIDI") {
 				Promise.all([createAudio(state.music), createAudio(state.accompany)]).then(
@@ -189,8 +226,19 @@ export default defineComponent({
 				);
 
 				api_playProgress(progress);
+			} else {
+				const songEndTime = state.times[state.times.length - 1 || 0]?.endtime || 0
+				audioData.duration = songEndTime
+				// 监听midi播放进度
+				api_cloudTimeUpdae(midiProgress);
+				// 监听midi播放结束
+				api_cloudplayed(midiPlayEnd);
 			}
 		});
+		onUnmounted(() => {
+			api_remove_cloudplayed(midiPlayEnd);
+			api_remove_cloudTimeUpdae(midiProgress);
+		});
 
 		// console.log(state.playMode, state.midiUrl);
 		return () => (

+ 5 - 1
src/view/audio-list/midiPlayer.tsx

@@ -1,3 +1,7 @@
+/**
+ * h5播放midi
+ */
+
 import { defineComponent, reactive } from "vue";
 import state, { gotoNext, onEnded, onPlay } from "/src/state";
 
@@ -15,7 +19,7 @@ const playNote = () => {
     const item = state.times[midiData.index]
     // 播放到最有一个音符,结束
     if (!item){
-        onEnded()
+        // onEnded()
         return
     }
     midiData.index++

+ 65 - 9
src/view/evaluating/index.tsx

@@ -15,6 +15,7 @@ import {
 	startSoundCheck,
 	api_openWebView,
 	api_startRecording,
+	api_startRecordingCb,
 	api_stopRecording,
 	api_recordStartTime,
 	api_remove_recordStartTime,
@@ -28,6 +29,9 @@ import {
 	addSocketStatus,
 	removeSocketStatus,
 	api_disconnectSocket,
+	api_midiMicDelay,
+	api_cloudSetCurrentTime,
+	api_cloudChangeSpeed,
 } from "/src/helpers/communication";
 import state, {
 	IPlayState,
@@ -40,7 +44,7 @@ import state, {
 import { IPostMessage } from "/src/utils/native-message";
 import { usePageVisibility } from "@vant/use";
 import { browser } from "/src/utils";
-import { getAudioCurrentTime, toggleMutePlayAudio } from "../audio-list";
+import { getAudioCurrentTime, toggleMutePlayAudio, audioListStart, audioData } from "../audio-list";
 import { handleStartTick, tickData } from "../tick";
 import AbnormalPop from "../abnormal-pop";
 import { storeData } from "../../store";
@@ -102,6 +106,20 @@ export const evaluatingData = reactive({
 	isAudioPlayEnd: false,
 });
 
+const sendOffsetTime = async (offsetTime: number) => {
+	const delayData = await api_getDeviceDelay();
+	api_midiMicDelay({
+		header: {
+			commond: 'audioPlayStart',
+			type: 'SOUND_COMPARE',
+		  },
+		  body: {
+			offsetTime,
+			micDelay: delayData?.content?.value
+		  },
+	})
+  }
+
 /** 点击开始评测按钮 */
 export const handleStartEvaluat = async () => {
 	if (state.modeType === "evaluating") {
@@ -309,6 +327,12 @@ const handleScoreResult = (res?: IPostMessage) => {
 
 /** 开始评测 */
 export const handleStartBegin = async (preTimes?: number) => {
+	if (state.isAppPlay) {
+		await api_cloudSetCurrentTime({
+			currentTime: 0,
+			songID: state.examSongId,
+		})
+	}
 	evaluatingData.isComplete = false;
 	evaluatingData.evaluatings = {};
 	evaluatingData.resultData = {};
@@ -344,15 +368,34 @@ export const handleStartBegin = async (preTimes?: number) => {
 	}
 	if (evaluatingData.isErrorState) return
 	//开始录音
-	await api_startRecording({
+	// await api_startRecording({
+	// 	accompanimentState: state.setting.enableAccompaniment ? 1 : 0,
+	// 	firstNoteTime: preTimes || 0,
+	// });
+	await api_startRecordingCb({
 		accompanimentState: state.setting.enableAccompaniment ? 1 : 0,
 		firstNoteTime: preTimes || 0,
-	});
-
+	}, () => {
+		if (state.isAppPlay) {
+			setTimeout(() => {
+				sendOffsetTime(0)
+			}, 300);
+		}
+	})
 	// 如果开启了摄像头, 开启录制视频
 	if (state.setting.camera) {
 		console.log("开始录制视频");
-		api_startCapture();
+		await api_startCapture();
+	}
+	// 如果是midi音频评测,需要调用cloudPlay
+	if (state.isAppPlay) {
+		await api_cloudChangeSpeed({
+			speed: state.originSpeed,
+			originalSpeed: state.originSpeed,
+			songID: state.examSongId,
+		});
+		audioData.progress = 0
+		audioListStart(state.playState);
 	}
 };
 
@@ -414,6 +457,7 @@ export const handleEndEvaluat = (isComplete = false) => {
 	// 结束录音
 	// api_stopRecording();
 	// 结束评测
+	console.log('评测结束1')
 	endEvaluating({
 		musicScoreId: state.examSongId,
 	});
@@ -435,7 +479,7 @@ export const handleEndEvaluat = (isComplete = false) => {
 };
 
 /**
- * 结束评测
+ * 结束评测(手动结束评测)
  */
 export const handleEndBegin = () => {
 	handleEndEvaluat();
@@ -445,7 +489,7 @@ export const handleEndBegin = () => {
 /**
  * 取消评测
  */
-export const handleCancelEvaluat = () => {
+export const handleCancelEvaluat = (cancelType?: string) => {
 	evaluatingData.evaluatings = {};
 	evaluatingData.startBegin = false;
 	// 关闭提示
@@ -458,10 +502,22 @@ export const handleCancelEvaluat = () => {
 			status: 200,
 		},
 	});
-	// 取消评测
+	/**
+	 * 异常状态是取消评测(cancelEvaluating),正常结束时结束评测(endEvaluating)
+	 */
+	// if (cancelType === "cancel") {
+	// 	// 取消评测
+	// 	cancelEvaluating();
+	// } else {
+	// 	endEvaluating({
+	// 		musicScoreId: state.examSongId,
+	// 	});
+	// }
+	
 	cancelEvaluating();
 	// 停止播放
 	handleStopPlay();
+	console.log('评测结束2')
 	endEvaluating({
 		musicScoreId: state.examSongId,
 	});
@@ -535,7 +591,7 @@ const handleAccompanyError = (res?: IPostMessage) => {
 				}
 				// 评测中
 				if (state.modeType === "evaluating" && evaluatingData.startBegin) {
-					handleCancelEvaluat();
+					handleCancelEvaluat('cancel');
 				}
 				if (tickData.show) {
 					tickData.tickEnd = true

+ 25 - 0
src/view/follow-practice/index.tsx

@@ -8,6 +8,11 @@ import { handleStartTick } from "/src/view/tick";
 import { metronomeData } from "/src/helpers/metronome";
 import { getDuration } from "/src/helpers/formateMusic";
 import { OpenSheetMusicDisplay } from "/osmd-extended/src";
+import { browser, getBehaviorId } from "/src/utils";
+import { api_musicPracticeRecordSave } from "../../page-instrument/api";
+import { getQuery } from "/src/utils/queryString";
+
+const query: any = getQuery();
 
 export const followData = reactive({
 	list: [] as any, // 频率列表
@@ -18,6 +23,25 @@ export const followData = reactive({
 	earphone: false,
 });
 
+// 记录跟练时长
+const handleRecord = (total: number) => {
+	if (query.isCbs) return
+	if (total < 0) total = 0;
+	const totalTime = total / 1000;
+
+	const body = {
+		clientType: storeData.user.clientType,
+		musicSheetId: state.examSongId,
+		sysMusicScoreId: state.examSongId,
+		feature: "FOLLOW_UP_TRAINING",
+		practiceSource: "FOLLOW_UP_TRAINING",
+		playTime: totalTime,
+		deviceType: browser().android ? "ANDROID" : "IOS",
+		behaviorId: getBehaviorId(),
+	};
+	api_musicPracticeRecordSave(body);
+};
+
 /** 点击跟练模式 */
 export const toggleFollow = (notCancel = true) => {
 	state.modeType = state.modeType === "follow" ? "practise" : "follow";
@@ -40,6 +64,7 @@ const openToggleRecord = async (open: boolean = true) => {
 	} else {
 		const playTime = Date.now() - followTime.value;
 		if (followTime.value !== 0 && playTime > 0) {
+			handleRecord(playTime);
 			followTime.value = 0;
 		}
 	}

+ 14 - 3
src/view/tick/index.tsx

@@ -30,7 +30,16 @@ const handlePlay = (i: number, source: any | null) => {
 				return
 			};
 			tickData.index++;
-			if (source) source.play();
+			if (source) {
+				const beatVolume = state.setting.beatVolume / 100
+				source.volume = beatVolume;
+				if (source.volume <= 0) {
+					source.muted = true
+				} else {
+					source.muted = false
+				}
+				source.play();
+			}
 			resolve(i);
 		}, tickData.beatLengthInMilliseconds);
 	});
@@ -86,8 +95,10 @@ export const handleStartTick = async () => {
 	for(let i = 0; i <= tickData.len; i++){
 		// 提前结束, 直接放回false
 		if (tickData.tickEnd) return false;
-		const source = i === 0 ? tickData.source1 : i === tickData.len ? null : tickData.source2;
-		// const source = i === 0 ? audioData.tick : i === tickData.len ? null : audioData.tock;
+		// Howl 插件播放音频
+		// const source = i === 0 ? tickData.source1 : i === tickData.len ? null : tickData.source2;
+		// Audio 标签播放音频
+		const source = i === 0 ? audioData.tick : i === tickData.len ? null : audioData.tock;
 		await handlePlay(i, source)
 	}
 	tickData.show = false;

+ 2 - 2
vite.config.ts

@@ -68,8 +68,8 @@ export default defineConfig({
 				// target: "https://test.lexiaoya.cn",
 				// target: "https://dev.kt.colexiu.com",
 				// target: "https://test.resource.colexiu.com", // 内容平台开发环境,内容平台开发,需在url链接上加上isCbs=true
-				target: "https://test.resource.colexiu.com",
-				// target: "https://test.kt.colexiu.com",
+				// target: "https://test.resource.colexiu.com",
+				target: "https://test.kt.colexiu.com",
 				changeOrigin: true,
 				rewrite: (path) => path.replace(/^\/instrument/, ""),
 			},