Kaynağa Gözat

Merge branch 'feature-pc-choose' into online

TIANYONG 10 ay önce
ebeveyn
işleme
7c8b846f52

+ 7 - 0
index.html

@@ -39,6 +39,13 @@
       },
     })
   </script>
+  
+  <script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
+  <script>
+    // VConsole will be exported to `window.VConsole` by default.
+    // var vConsole = new window.VConsole();
+  </script>
+
   <style>
     #lottieWeb {
       position: fixed;

+ 7 - 0
orchestra.html

@@ -39,6 +39,13 @@
       },
     })
   </script>
+
+<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
+<script>
+  // VConsole will be exported to `window.VConsole` by default.
+  // var vConsole = new window.VConsole();
+</script>
+  
   <style>
     #lottieWeb {
       position: fixed;

+ 155 - 63
src/pages/detail/runtime.ts

@@ -144,6 +144,12 @@ const state = reactive({
   basePlayRate: 1,
   /** 播放中,更加当前小节速度动态计算的展示速度 */
   playIngSpeed: 90,
+  /** 上一次app返回的播放进度 */
+  preAppAudioPlayTime: 0,
+  /** app返回的音频文件总时长 */
+  appAudioTotalTime: 0,
+  /** 播放倍率不等于1,或者是选段评测,APP暂时不支持保存演奏,需要给出提示 */
+  initShow: true,
 })
 
 const syncStepIndex = (i: number) => {
@@ -235,6 +241,7 @@ export const getActiveMidiId = () => {
  * @param val IMode
  */
 export const changeMode = async (val: IMode, type?: string | undefined) => {
+  // console.log('切换音源',val,state.songs)
   const cm: IMode = val === 'background' ? 'music' : 'background'
   // console.log(!state.songs[val], val, cm)
   if (detailState.activeDetail.isAppPlay) {
@@ -270,6 +277,10 @@ export const changeMode = async (val: IMode, type?: string | undefined) => {
   //   return
   // }
   state.mode = val
+  // 如果没有伴奏,音源切换按钮文案重置为原音
+  if (!state.songs.background) {
+    state.mode = 'music'
+  }
   if (type === 'all') {
     state.audiosInstance?.setMute(true, state.songs[cm])
     state.audiosInstance?.setMute(true, state.songs[val])
@@ -324,7 +335,7 @@ export const changeSpeed = (speed: number, isSave: boolean = true) => {
   state.speed = speed
   state.playIngSpeed = speed
   // 当前的音符
-  const currentItem: any = detailState.times[state.activeIndex];
+  const currentItem: any = (detailState.sectionStatus && detailState.section.length === 2) ? detailState.section[0] : detailState.times[state.activeIndex];
   state.basePlayRate = currentItem?.measureSpeed ? state.speed / currentItem.measureSpeed : state.speed / detailState.baseSpeed;
   if (!detailState.activeDetail) return
   state.audiosInstance?.setSpeed(state.basePlayRate)
@@ -389,7 +400,7 @@ export const refreshIndexBase = (index: number) => {
 
 // 练习模式下,开始播放时,记录mp3的播放倍率
 export const initSetPlayRate = () => {
-  const item: any = detailState.times[state.activeIndex];
+  const item: any = (detailState.sectionStatus && detailState.section.length === 2) ? detailState.section[0] : detailState.times[state.activeIndex];
   if (item && modelType.value === "practice" && item.measureSpeed) {
     const ratio = state.speed / item.measureSpeed
     // state.audiosInstance?.setSpeed(ratio)
@@ -402,7 +413,10 @@ export const initSetPlayRate = () => {
 const dynamicShowPlaySpeed = (index: number) => {
   const item: any = detailState.times[index];
 
-  if (item && modelType.value === "practice" && state.playState === "play" && item.measureSpeed ) {
+  // if (item && state.playState === "play" && item.measureSpeed ) {
+  //   state.playIngSpeed = Math.floor(state.basePlayRate * item.measureSpeed)
+  // }
+  if (item && item.measureSpeed ) {
     state.playIngSpeed = Math.floor(state.basePlayRate * item.measureSpeed)
   }
 }
@@ -410,14 +424,23 @@ const dynamicShowPlaySpeed = (index: number) => {
 export const refreshIndex = (ctime?: number) => {
   const { osmd }: any = state
   if (osmd && (ctime || state.audiosInstance.audio)) {
-    const currentTimeNum = ctime || (state.audiosInstance.audio as HTMLAudioElement).currentTime
+    let currentTimeNum = ctime || (state.audiosInstance.audio as HTMLAudioElement).currentTime
+    // 如果是评测状态,当前时间取app返回的进度
+    if (state.evaluatingStatus) {
+      currentTimeNum = ctime ? ctime : 0
+    }
     try {
       metronomeData?.metro?.sound(currentTimeNum);
     } catch (error) {}
 
     const index = getIndex(detailState.times, currentTimeNum)
     // 监听app返回的ctime
-    // console.log(777777777,index,ctime)
+    // console.log(777777777,index,ctime,state.audiosInstance.audio.currentTime)
+    // if (state.evaluatingStatus) {
+    //   console.log('评测模式','app返回的播放进度',ctime)
+    // } else {
+    //   console.log('练习模式','h5播放器的播放进度',state.audiosInstance.audio.currentTime)
+    // }
     dynamicShowPlaySpeed(index);
     state.activeIndex = index
     removeRepateBackground(index)
@@ -494,6 +517,8 @@ export const refreshPlayer = async (ctime?: number) => {
       return false
     }
     const isNext = nextTime()
+    // console.log('选段123',detailState.section,detailState.sectionStatus)
+    // 选段播放结束
     if (isNext) {
       // console.log("isNext", detailState.section[1], detailState.section[1].endtime, currentTimeNum)
       state.audiosInstance.setMute(true)
@@ -503,16 +528,17 @@ export const refreshPlayer = async (ctime?: number) => {
       } else {
         await state.audiosInstance.pause()
       }
-      // 如果是单元测验 和课后训练直接结束
-      if (unitTestData.isSelectMeasureMode && state.evaluatingStatus){
-        console.log(1)
+      // 如果是单元测验 和课后训练,或者是选段评测,选段播放结束 直接结束
+      if ((unitTestData.isSelectMeasureMode || detailState.section.length === 2) && state.evaluatingStatus){
+        console.log('选段播放结束')
         event.emit('ended')
         return
       }
       setSectionModeCurrentTime()
       clearAccelerateRefreshPlayer()
       setTimeout(() => {
-        if (detailState.section.length){
+        // 开启了循环播放
+        if (detailState.section.length && SettingState.sett.loop){
           setPlayState()
         }
       }, 1000)
@@ -572,6 +598,7 @@ export const resetPlayStatus = async (notStop?: boolean) => {
 }
 
 export const play = async () => {
+  console.log('播放执行')
   // 评测是app播放,非h5播放
   if (modelType.value === 'evaluation') return
   if (state.isFirstPlay) {
@@ -593,6 +620,47 @@ export const play = async () => {
   }
 }
 
+const refreshNote = () => {
+  if (state.playState === 'play') {
+    const time = state.preAppAudioPlayTime / 1000;
+    state.currentTimeNum = time
+    refreshPlayer(time)
+    // console.log(66666663333)
+    refreshIndex(time)
+    // 播放到最后一秒,停止播放
+    if (state.appAudioTotalTime > 1000 && state.preAppAudioPlayTime >= state.appAudioTotalTime) {
+      console.log('播放结束123')
+      state.playState = 'pause'
+      state.playEndCallback.endEvaluat()
+      ended(new Event('ended'))
+    }
+  }
+  refreshView()
+}
+
+// 评测播放刷新音符进度
+export const setEvaluatingStep = () => {
+  // console.log('播放状态',state.playState)
+  if (state.playState !== "play") {
+    console.log("暂停播放");
+    return;
+  }
+  let startTime = Date.now();
+  requestAnimationFrame(() => {
+    const endTime = Date.now();
+    // 渲染时间大于16.6,就会让页面卡顿, 如果渲染时间大与16.6就下一个渲染帧去计算
+    if (endTime - startTime < 16.7) {
+      refreshNote()
+      setEvaluatingStep()
+    } else {
+      setTimeout(() => {
+        refreshNote()
+        setEvaluatingStep()
+      }, 16.7);
+    }
+  });
+};
+
 const setDelayTime = async (time: number) => {
   return new Promise((resolve) => {
     setTimeout(() => {
@@ -629,12 +697,13 @@ export const playing = () => {
   state.loading = false
 }
 
-export const ended = debounce(async (evt: Event) => {
+export const ended = debounce(async (evt: Event, flag?: string) => {
   resetPlayStatus()
   detailState.fixedKey = 0
+  // console.log('需要重播',flag)
   if (!state.evaluatingStatus) {
     refreshPlayer(0)
-    if (SettingState.sett.loop) {
+    if (SettingState.sett.loop || (flag && flag === 'isRePlay')) {
       await setPlayState()
     }
   }
@@ -684,6 +753,7 @@ export const sectionChange = () => {
   if (!detailState.sectionStatus) {
     setCurrentTime(0)
     detailState.fixedKey = 0
+   resetBaseRate();
   }
   if (detailState.sectionStatus && detailState.section.length != 2) {
     resetCursor()
@@ -857,40 +927,51 @@ export const setTick = (stop: () => void, speed?: number) => {
       numerator = state.osmd.numerator
       denominator = state.osmd.denominator
     }
-    if (detailState.activeDetail.isAppPlay) {
-      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 if (res?.content.status === 'cancel') {
-            event.emit('tickDestroy')
-          }
-        }
-      )
-    } else {
-      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.off('destroy', onTickDestroy)
-      state.tickPlayer?.event.on('tick', setActiveKey)
-      state.tickPlayer?.event.on('stop', mixStop)
-      state.tickPlayer?.event.on('destroy', onTickDestroy)
-    }
+    // if (detailState.activeDetail.isAppPlay) {
+    //   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 if (res?.content.status === 'cancel') {
+    //         event.emit('tickDestroy')
+    //       }
+    //     }
+    //   )
+    // } else {
+    //   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.off('destroy', onTickDestroy)
+    //   state.tickPlayer?.event.on('tick', setActiveKey)
+    //   state.tickPlayer?.event.on('stop', mixStop)
+    //   state.tickPlayer?.event.on('destroy', onTickDestroy)
+    // }
+    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.off('destroy', onTickDestroy)
+    state.tickPlayer?.event.on('tick', setActiveKey)
+    state.tickPlayer?.event.on('stop', mixStop)
+    state.tickPlayer?.event.on('destroy', onTickDestroy)
   } else {
     mixStop()
   }
@@ -1043,23 +1124,34 @@ export const setAudioInit = () => {
   })
   // 监听评测曲谱音频播放进度,返回
   listenerMessage("playProgress", (res) => {
-    const time = res?.content.currentTime / 1000
-    requestAnimationFrame(async () => {
-      if (state.playState === 'play') {
-        state.currentTimeNum = time
-        refreshPlayer(time)
-        // console.log(66666663333)
-        refreshIndex(time)
-        // 播放到最后一秒,停止播放
-        if (res?.content?.totalDuration > 1000 && res?.content?.currentTime >= res?.content?.totalDuration) {
-          console.log('播放结束123')
-          state.playState = 'pause'
-          state.playEndCallback.endEvaluat()
-          ended(new Event('ended'))
-        }
+    if (state.playState === 'play') {
+      const time = res?.content.currentTime / 1000
+      const diffTime = res?.content?.currentTime - state.preAppAudioPlayTime;
+      
+      // console.log('app返回的mp3进度',time)
+      if (diffTime < 0) {
+        console.log('进度返回异常','本次时间比上次慢',diffTime,'当前播放时间:',res?.content?.currentTime)
+        return
       }
-      refreshView()
-    })
+      state.preAppAudioPlayTime = res?.content?.currentTime
+      state.appAudioTotalTime = res?.content?.totalDuration
+      // requestAnimationFrame(async () => {
+      //   if (state.playState === 'play') {
+      //     state.currentTimeNum = time
+      //     refreshPlayer(time)
+      //     // console.log(66666663333)
+      //     refreshIndex(time)
+      //     // 播放到最后一秒,停止播放
+      //     if (res?.content?.totalDuration > 1000 && res?.content?.currentTime >= res?.content?.totalDuration) {
+      //       console.log('播放结束123')
+      //       state.playState = 'pause'
+      //       state.playEndCallback.endEvaluat()
+      //       ended(new Event('ended'))
+      //     }
+      //   }
+      //   refreshView()
+      // })
+    }
   });
   state.audiosInstance.event.on('timeupdate', () => {
     state.currentTimeNum = state.audiosInstance.currentTime
@@ -1107,8 +1199,8 @@ export const noteClick = (evt: MouseEvent) => {
   console.log('点击音符',activeNote)
   if (activeNote) {
     const time = activeNote.sourceStartTime || activeNote.time
-    // 点击音符,动态设置右上角的速度
-    if (activeNote.measureSpeed) {
+    // 点击音符,动态设置右上角的速度,非选段模式
+    if (activeNote.measureSpeed && detailState.section.length < 2) {
       state.speed = Math.floor(state.basePlayRate * activeNote.measureSpeed)
       state.playIngSpeed = state.speed
     }

+ 6 - 1
src/pages/detail/section-box/index.tsx

@@ -3,7 +3,7 @@ import { defineComponent, watchEffect, TransitionGroup, ref, Ref, reactive } fro
 import event from '/src/components/music-score/event'
 import SettingState from '/src/pages/detail/setting-state'
 import state from '../state'
-import runtime, { getFirsrNoteByMeasureListIndex, getBoundingBoxByNote } from '../runtime'
+import runtime, { getFirsrNoteByMeasureListIndex, getBoundingBoxByNote, changeSpeed } from '../runtime'
 import { getActtiveNoteByTimes, getBoundingBoxByverticalNote, getNoteBySlursStart, setSettionBackground } from '../helpers'
 import { formatZoom } from '/src/helpers/utils'
 import styles from './index.module.less'
@@ -99,6 +99,11 @@ export default defineComponent({
         if (sectionLength === 1) {
           const note = getNoteBySlursStart(activeNote, true, 'end')
           state.section.push(state.times[note.i - note.si + note.noteLength - 1])
+          // 选段状态需要重置播放倍率为1
+          runtime.basePlayRate = 1;
+          const currentItem: any = state.section[0];
+          const currentSpeed = currentItem?.measureSpeed ? currentItem.measureSpeed : state.activeSpeed;
+          changeSpeed(currentSpeed)
         }
       }
       if (state.section.length === 2) {

+ 2 - 0
src/pages/detail/state.ts

@@ -92,6 +92,8 @@ const state = reactive({
   isCombineRender: false,
   /** 大雅金唐类目 */
   isDaYaCategory: false,
+  /** 选段评测,开始播放的音符的时值 */
+  firstNoteTime: 0,
 })
 
 export const isRhythmicExercises = (musicName?: string) => {

+ 2 - 0
src/subpages/colexiu-report/index.tsx

@@ -205,6 +205,8 @@ export default defineComponent({
       })
       runtime.osmd = osmd
       allNote.value = getAllNodes(runtime.osmd)
+      const startMeasureNum = record.value?.musicalNotesPlayStats?.notesData?.[0].measureRenderIndex, endMeasureNum = record.value?.musicalNotesPlayStats?.notesData?.last()?.measureRenderIndex;
+      allNote.value = allNote.value.filter((item: any) => (item.measureNumberPrinted >= startMeasureNum+1 && item.measureNumberPrinted <= endMeasureNum+1))
       setViewColor()
       const setEvaluatings = (note: any, data: any, dontTransition = true) => {
         const startNote = getBoundingBoxByverticalNote(note)

+ 61 - 18
src/subpages/colexiu/buttons/evaluating.tsx

@@ -1,5 +1,5 @@
 import { Button, Toast, Popup } from 'vant'
-import { defineComponent, onBeforeUnmount, onMounted, Ref, ref, Teleport, Transition, reactive } from 'vue'
+import { defineComponent, onBeforeUnmount, onMounted, Ref, ref, Teleport, Transition, reactive, computed } from 'vue'
 import '@dotlottie/player-component'
 import detailState, { isRhythmicExercises } from '/src/pages/detail/state'
 import SettingState from '/src/pages/detail/setting-state'
@@ -25,6 +25,7 @@ import startData from './data/start_new.json'
 import startingData from './data/starting_new.json'
 import { unitTestData } from '../unitTest'
 import iconEvaluatingStart from './icons/icon-evaluatingStart.png'
+import iconEvaluatingBegin from './icons/icon-evaluatingBegin.png'
 import qs from 'query-string'
 import CheckDelayPopup from "/src/pages/detail/CheckDelayPopup";
 import Headphone, { HeadphoneData } from "/src/pages/detail/Headphone";
@@ -320,7 +321,9 @@ export const formatPitch = (num?: number): number => {
 }
 let starTime = 0
 const formatTimes = () => {
-  const rate = runtime.speed / detailState.baseSpeed //1
+  // const rate = runtime.speed / detailState.baseSpeed //1
+  const rate = runtime.basePlayRate; // 播放倍率
+  console.log('评测倍率123',rate)
   actualBeatLength = Math.round(detailState.times[0].fixtime * 1000 / rate)
   const difftime = detailState.times?.[0]?.difftime || 0
   let ListenMode = false
@@ -333,7 +336,7 @@ const formatTimes = () => {
   let preTimes = []
   let unitTestIdx = 0
   let preTime = 0
-  if (unitTestData.isSelectMeasureMode) {
+  if (unitTestData.isSelectMeasureMode || detailState.section.length == 2) {
     const startIndex = detailState.times.findIndex(
       (n: any) => n.NoteToGraphicalNoteObjectId == detailState.section[0].NoteToGraphicalNoteObjectId
     )
@@ -341,7 +344,14 @@ const formatTimes = () => {
       (n: any) => n.NoteToGraphicalNoteObjectId == detailState.section[1].NoteToGraphicalNoteObjectId
     )
     if (startIndex > 1) {
-      preTime = detailState.times[startIndex-1].time * 1000
+      // 预备小节的小节数
+      const preMeasureNum = detailState.times[startIndex-1].measureNumberPrinted;
+      // 预备小节的第一个音符的索引
+      let preNoteNum = startIndex-1;
+      while (preMeasureNum === detailState.times[preNoteNum].measureNumberPrinted && preNoteNum >= 1 && preMeasureNum === detailState.times[preNoteNum-1].measureNumberPrinted) {
+        preNoteNum -= 1;
+      }
+      preTime = detailState.times[preNoteNum].time * 1000
     }
     times = detailState.times.filter((n: any, index: number) => {
       return index >= startIndex && index <= endIndex
@@ -371,14 +381,15 @@ const formatTimes = () => {
     preLyricsContent = preLyricsContent ? preLyricsContent : 'Play'
   }
   // 阶段评测beatLength需要加上预备小节的持续时长
-  actualBeatLength = preTimes.length ? actualBeatLength + preTimes[preTimes.length - 1].duration * 1000 : actualBeatLength
+  actualBeatLength = preTimes.length ? actualBeatLength + preTimes[preTimes.length - 1].measureLength * 1000 : actualBeatLength
   let measureIndex = -1
   let recordMeasure = -1
   let firstNoteTime = unitTestIdx > 1 ? preTime : 0
   for (let index = 0; index < times.length; index++) {
     const item = times[index]
     const note = getNoteByMeasuresSlursStart(item)
-    const rate = runtime.speed / detailState.baseSpeed //1
+    // const rate = runtime.speed / detailState.baseSpeed //1
+    const rate = runtime.basePlayRate; // 播放倍率
     const start = difftime + (item.sourceRelativeTime || item.relativeTime) - starTime
     const end = difftime + (item.sourceRelaEndtime || item.relaEndtime) - starTime
     // console.log(start, end, starTime)
@@ -409,9 +420,13 @@ const formatTimes = () => {
     if (note.noteElement.isRestFlag && !!note.stave && !!nextNote && nextNote.noteElement.isRestFlag) {
       skip = true
     }
-    if (note.measureOpenIndex != recordMeasure) {
+    // if (note.measureOpenIndex != recordMeasure) {
+    //   measureIndex++
+    //   recordMeasure = note.measureOpenIndex
+    // }
+    if (item.measureOpenIndex != recordMeasure) {
       measureIndex++
-      recordMeasure = note.measureOpenIndex
+      recordMeasure = item.measureOpenIndex
     }
     // console.log(note.measureOpenIndex , measureIndex, note.noteElement.sourceMeasure.measureListIndex)
     const data = {
@@ -421,7 +436,8 @@ const formatTimes = () => {
       nextFrequency: formatPitch(item.noteElement?.pitch?.nextFrequency),
       prevFrequency: formatPitch(item.noteElement?.pitch?.prevFrequency),
       measureIndex: measureIndex, //note.measureOpenIndex,
-      measureRenderIndex: note.noteElement.sourceMeasure.measureListIndex,
+      // measureRenderIndex: note.noteElement.sourceMeasure.measureListIndex,
+      measureRenderIndex: item.measureNumberPrinted >= 1 ? item.measureNumberPrinted - 1 : note.noteElement.sourceMeasure.measureListIndex,
       dontEvaluating: ListenMode || dontEvaluatingMode || item.skipMode,
       musicalNotesIndex: index, //item.i,
       denominator: note.noteElement?.Length.denominator,
@@ -430,17 +446,22 @@ const formatTimes = () => {
     // console.log('时间1111', data)
     datas.push(data)
   }
+  starTime = 0;
+  // console.log(JSON.stringify(datas),'评测数据')
   return {
     datas,
     firstNoteTime
   }
 }
 const connect = async () => {
+  detailState.firstNoteTime = 0;
+  runtime.preAppAudioPlayTime = 0;
   const search = useOriginSearch()
   connentLoading.value = true
   const behaviorId = sessionStorage.getItem('behaviorId') || search.behaviorId || initBehaviorId
   const rate = runtime.speed / detailState.baseSpeed //1
   calculateInfo = formatTimes()
+  detailState.firstNoteTime = calculateInfo.firstNoteTime;
   const content = {
     musicXmlInfos: calculateInfo.datas,
     firstNoteTime: calculateInfo.firstNoteTime,
@@ -462,6 +483,7 @@ const connect = async () => {
     // beatLength: Math.round((RuntimeUtils.getFixTime(detailState.times[0].beatSpeed) * 1000) / rate),
     beatLength: actualBeatLength,
     evaluationCriteria: getEvaluationCriteria(),
+    speedRate: parseFloat(runtime.basePlayRate.toFixed(2)), // 播放倍率
   }
   // console.log("🚀 ~ content:", content, rate)
   const clientType = useClientType()
@@ -659,8 +681,9 @@ const playerStop = () => {
 export const evaluatPlayerStop = playerStop
 
 const endevent = (evt: Event) => {
+  console.log('选段播放结束事件')
   // 如果是单元测验和课后训练 播放结束
-  if (unitTestData.isSelectMeasureMode && playStatus.value === 'play') {
+  if ((unitTestData.isSelectMeasureMode || detailState.section.length === 2) && playStatus.value === 'play') {
     playerStop()
     canSubmit.value = true
     return
@@ -704,6 +727,7 @@ const evaluatStart = () => {
       content: {
         accompanimentState: SettingState.eva.mute ? 1 : 0,
         firstNoteTime: calculateInfo.firstNoteTime || 0,
+        speedRate: parseFloat(runtime.basePlayRate.toFixed(2)), // 播放倍率
       }
     },
     () => {
@@ -711,6 +735,7 @@ const evaluatStart = () => {
       backtime = Date.now()
       evaluating.value = true
       runtime.playState = "play";
+      RuntimeUtils.setEvaluatingStep()
       if (detailState.activeDetail?.midiUrl) {
         console.log('midiUrl', detailState.activeDetail?.midiUrl)
         setTimeout(() => {
@@ -801,6 +826,7 @@ const sendResult = (evt?: IPostMessage) => {
         }
       }
       const setEvaluatings = (note: any, data: any, dontTransition = false) => {
+        // console.log('note',note,data)
         const startNote = getBoundingBoxByverticalNote(note)
         // console.log(detailState.evaluatings, startNote)
         detailState.evaluatings = {
@@ -809,7 +835,7 @@ const sendResult = (evt?: IPostMessage) => {
             ...startNote,
             ...getLeveByScoreMeasure(data.score),
             score: data.score,
-            dontTransition,
+            dontTransition, // 是否显示得分
           },
         }
       }
@@ -828,13 +854,19 @@ const sendResult = (evt?: IPostMessage) => {
             setEvaluatings(time, data)
           } else {
             for (const item of time.noteElement.tie.notes) {
+              const currentMeasureNum = time.noteElement.measureNumber;
               const note = getParentNote(item)
               if (!note) continue
-              setEvaluatings(
-                note,
-                data,
-                item.NoteToGraphicalNoteObjectId !== time.noteElement.tie.StartNote?.NoteToGraphicalNoteObjectId
-              )
+              
+              if (note.measureNumberPrinted === currentMeasureNum) {
+                // console.log('得分',item.NoteToGraphicalNoteObjectId,time.NoteToGraphicalNoteObjectId)
+                setEvaluatings(
+                  note,
+                  data,
+                  // item.NoteToGraphicalNoteObjectId !== time.noteElement.tie.StartNote?.NoteToGraphicalNoteObjectId
+                  item.NoteToGraphicalNoteObjectId !== time.NoteToGraphicalNoteObjectId
+                )
+              }
             }
           }
           break
@@ -961,6 +993,15 @@ export default defineComponent({
       submitMaxScore()
     })
 
+    // 播放倍率不等于1,或者是选段评测,APP暂时不支持保存演奏,需要给出提示
+    const showNotSaveTip = computed(() => {
+      if ((runtime.basePlayRate != 1 || detailState.section.length === 2) && runtime.initShow) {
+        return true
+      } else {
+        return false
+      }
+    })
+
     expose({
       setPlayer,
       startPlay,
@@ -978,16 +1019,18 @@ export default defineComponent({
         <Teleport to="body" key="StartEvaluating">
           {/* 评测完成结果显示 */}
           <Evaluating data={endResult.value} />
+          {/** 调速或者选段后,评测给出提示 */}
+          {/* { showNotSaveTip.value && <div class={styles.noSaveTip}>调整速度或选段后评测暂不支持保存演奏哦~<i onClick={() => runtime.initShow = false}></i></div> } */}
 
           <Transition name="finish">
             {startButtonShow.value && !delayData.open && modelType.value !== 'init' && (
               <div
                 style={{
-                  backgroundImage: `url(${iconEvaluatingStart})`,
-                  'transform': detailState.isSpecialShapedScreen ? `translateX(${detailState.notchHeight / 4}px)`  : '',
+                  backgroundImage: `url(${iconEvaluatingBegin})`
                 }}
                 class={[styles.evaluatStartBtn]}
                 onClick={async () => {
+                  runtime.initShow = true
                   // 评测模式,如果有系统节拍器,需要先播放系统节拍器
                   if (detailState.needTick) {
                     await RuntimeUtils.setEvaluatTick()

BIN
src/subpages/colexiu/buttons/icons/close_icon.png


BIN
src/subpages/colexiu/buttons/icons/icon-evaluatingBegin.png


+ 30 - 4
src/subpages/colexiu/buttons/index.module.less

@@ -150,10 +150,11 @@
 }
 .evaluatStartBtn {
   position: fixed;
-  top: calc(50% - 37.5px);
-  left: calc(50% - 37.5px);
-  width: 75px;
-  height: 75px;
+  bottom: 17px;
+  left: calc(50% - 32px);
+  width: 64px;
+  height: 20px;
+  border-radius: 12px;
   background-size: 100%;
   background-position: center;
   z-index: 10;
@@ -492,3 +493,28 @@ body {
     }
   }
 }
+
+.noSaveTip {
+  background: rgba(0,0,0,0.7);
+  font-size: 14Px;
+  border-radius: 8Px;
+  display: flex;
+  align-items: center;
+  padding: 8Px 12Px;
+  position: fixed;
+  left: 50%;
+  top: 100Px;
+  transform: translateX(-50%);
+  z-index: 1;
+  color: #fff;
+  > i {
+    width: 12Px;
+    height: 12Px;
+    background-image: url('./icons//close_icon.png');
+    background-size: 100% 100%;
+    background-position: center center;
+    background-repeat: no-repeat;
+    margin-left: 16Px;
+    cursor: pointer;
+  }
+}

+ 60 - 50
src/subpages/colexiu/buttons/index.tsx

@@ -102,13 +102,14 @@ const back: () => void = () => {
 export type IModelType = 'practice' | 'evaluation' | 'follow' | 'init'
 export const modelType = ref<IModelType>('init')
 export const onChangeModelType = (type: IModelType) => {
+  runtime.initShow = true;
   if (type === modelType.value) return
   // 跟练模式,光标只有音符模式,无节拍模式
   if (type === 'follow' && metronomeData.cursorMode === 2) {
     metronomeData.cursorMode = 1
   }
   if (type === 'evaluation') {
-    RuntimeUtils.changeSpeed(detailState.activeDetail?.originalSpeed, false)
+    // RuntimeUtils.changeSpeed(detailState.activeDetail?.originalSpeed, false)
     // 评测模式
     runtime.evaluatingStatus = true
     modelType.value = type
@@ -316,6 +317,59 @@ export default defineComponent({
                 <span>声轨</span>
               </Button>
             )}
+
+            {['practice', 'evaluation'].includes(modelType.value) && (
+              <>
+                {
+                  modelType.value === 'practice' ? 
+                  <Button
+                    data-step="m1"
+                    class={[styles.button, styles.hasText]}
+                    onClick={() => RuntimeUtils.changeMode(runtime.mode === 'background' ? 'music' : 'background')}
+                    disabled={changeModeIsDisabled}
+                  >
+                    <ButtonIcon key="music" name={runtime.mode === 'music' ? 'music' : 'accompaniment'} />
+                    <span>{runtime.mode === 'background' ? '伴奏' : '原声'}</span>
+                  </Button> : null
+                }
+                {/* 如果为单元测试和课后训练 */}
+                {unitTestData.isSelectMeasureMode ? null : (
+                  <Button
+                    data-step="m2"
+                    class={[styles.button, styles.hasText]}
+                    onClick={RuntimeUtils.sectionChange}
+                    disabled={runtime.playState === 'play'}
+                  >
+                    <ButtonIcon
+                      key="section"
+                      name={
+                        'section' +
+                        (detailState.section.length && detailState.section.length <= 2
+                          ? detailState.section.length
+                          : '')
+                      }
+                    />
+                    <span>选段</span>
+                  </Button>
+                )}
+                {
+                  modelType.value === 'practice' ? 
+                  <Button
+                    data-step="m3"
+                    class={[styles.button, styles.hasText]}
+                    disabled={runtime.playState === 'play'}
+                    onClick={() => {
+                      SettingState.sett.fingering = !SettingState.sett.fingering
+                      RuntimeUtils.event.emit('settingFingeringChange')
+                    }}
+                  >
+                    <ButtonIcon key="music" name={SettingState.sett.fingering ? 'fingeringOn' : 'fingeringOff'} />
+                    <span>指法</span>
+                  </Button> : null               
+                }
+              </>
+            )}
+
             {modelType.value === 'evaluation' && (
               <>
                 <Popover
@@ -367,60 +421,15 @@ export default defineComponent({
 
                 <Evaluating ref={evaluatingRef} />
               </>
-            )}
-
-            {modelType.value === 'practice' && (
-              <>
-                <Button
-                  data-step="m1"
-                  class={[styles.button, styles.hasText]}
-                  onClick={() => RuntimeUtils.changeMode(runtime.mode === 'background' ? 'music' : 'background')}
-                  disabled={changeModeIsDisabled}
-                >
-                  <ButtonIcon key="music" name={runtime.mode === 'music' ? 'music' : 'accompaniment'} />
-                  <span>{runtime.mode === 'background' ? '伴奏' : '原声'}</span>
-                </Button>
-                {/* 如果为单元测试和课后训练 */}
-                {unitTestData.isSelectMeasureMode ? null : (
-                  <Button
-                    data-step="m2"
-                    class={[styles.button, styles.hasText]}
-                    onClick={RuntimeUtils.sectionChange}
-                    disabled={runtime.evaluatingStatus || runtime.playState === 'play'}
-                  >
-                    <ButtonIcon
-                      key="section"
-                      name={
-                        'section' +
-                        (detailState.section.length && detailState.section.length <= 2
-                          ? detailState.section.length
-                          : '')
-                      }
-                    />
-                    <span>选段</span>
-                  </Button>
-                )}
-                <Button
-                  data-step="m3"
-                  class={[styles.button, styles.hasText]}
-                  disabled={runtime.playState === 'play'}
-                  onClick={() => {
-                    SettingState.sett.fingering = !SettingState.sett.fingering
-                    RuntimeUtils.event.emit('settingFingeringChange')
-                  }}
-                >
-                  <ButtonIcon key="music" name={SettingState.sett.fingering ? 'fingeringOn' : 'fingeringOff'} />
-                  <span>指法</span>
-                </Button>
-              </>
-            )}
+            )}    
+                    
             {['practice', 'evaluation'].includes(modelType.value) && !search.lessonTrainingId && (
               <Popover
                 trigger="manual"
                 overlay={false}
                 placement="bottom"
                 class={styles.popover}
-                show={show.value && runtime.speedShow && !(runtime.evaluatingStatus || runtime.playState === 'play')}
+                show={show.value && runtime.speedShow && !(runtime.playState === 'play')}
                 // @ts-ignore
                 onUpdate:show={(show: boolean) => (runtime.speedShow = show)}
                 vSlots={{
@@ -428,7 +437,8 @@ export default defineComponent({
                     <Button
                       data-step="m4"
                       class={[styles.button, styles.hasText, styles.speedButton]}
-                      disabled={runtime.evaluatingStatus || runtime.playState === 'play'}
+                      // disabled={runtime.evaluatingStatus || runtime.playState === 'play'}
+                      disabled={runtime.playState === 'play'}
                       onClick={() => {
                         speedRef.value?.refUpdateSpeed(runtime.playIngSpeed || runtime.speed)
                         runtime.speedShow = !runtime.speedShow

+ 1 - 1
src/subpages/colexiu/buttons/player.tsx

@@ -20,7 +20,7 @@ export default defineComponent({
         return
       }
       RuntimeUtils.setCurrentTime(0)
-      RuntimeUtils.ended(new Event('ended'))
+      RuntimeUtils.ended(new Event('ended'), 'isRePlay')
     }
 
     // 播放进入的圆周长

+ 30 - 5
src/subpages/colexiu/popups/evaluating/content.tsx

@@ -1,5 +1,5 @@
 import { Button, Dialog, Grid, GridItem, Popup, Toast } from 'vant'
-import { defineComponent, ref, toRefs } from 'vue'
+import { defineComponent, ref, toRefs, computed } from 'vue'
 import qs from 'query-string'
 import appState from '/src/state'
 import detailState from '/src/pages/detail/state'
@@ -12,7 +12,7 @@ import TryIcon from './icons/icon-try.png'
 import IntegrityIcon from './icons/integrity.svg'
 import IntonationIcon from './icons/intonation.svg'
 import CadenceIcon from './icons/cadence.svg'
-import runtime from '/src/pages/detail/runtime'
+import runtime, * as RuntimeUtils from '/src/pages/detail/runtime'
 import { postMessage } from '/src/helpers/native-message'
 import { evaluatingShow, ResultContent } from './index'
 import { getLeveByScoreId } from '/src/pages/detail/evaluating/helper'
@@ -136,9 +136,20 @@ export default defineComponent({
           isHideTitle: true, // 此处兼容安卓,意思为隐藏全部头部
           statusBarTextColor: false,
           isOpenLight: true,
+          c_orientation: 0,
         },
       })
     }
+
+    // 播放倍率不等于1,或者是选段评测,APP暂时不支持保存演奏,需要给出提示
+    const noSaveTips = computed(() => {
+      let tipContent = '';
+      if (isUnitTest || detailState.section.length === 2 || detailState.activeDetail.isAppPlay || runtime.basePlayRate != 1 || !runtime.songs.background) {
+        tipContent = isUnitTest ? '单元测验暂不支持保存作品噢~' : (!runtime.songs.background || detailState.activeDetail.isAppPlay) ? '该曲目暂不支持保存作品噢~' : detailState.section.length === 2 ? '选段后暂不支持保存作品噢~' : runtime.basePlayRate != 1 ? '调速后暂不支持保存作品噢~' : '';
+      }
+      return tipContent
+    })
+
     return () => {
       const info = getLeveByScoreId(data.value?.score)
       return (
@@ -224,6 +235,7 @@ export default defineComponent({
                           runtime.evaluatingStatus = false
                           detailState.evaluatings = {}
                           evaluatingShow.value = false
+                          RuntimeUtils.clearSectionStatus()
                           onChangeModelType('practice')
                         }}
                       >
@@ -233,6 +245,7 @@ export default defineComponent({
                     <Button
                       style={{ margin: '0 4px' }}
                       onClick={() => {
+                        runtime.initShow = true;
                         detailState.evaluatings = {}
                         emit('restart')
                       }}
@@ -248,7 +261,7 @@ export default defineComponent({
                 </div>
               </div>
 
-              {isUnitTest ? null : !detailState.isHideEvaluatReportSaveBtn ? (
+              { !detailState.isHideEvaluatReportSaveBtn ? (
                 // <div class={styles.rigthBtns}>
                 //   <div class={styles.skepBtn} onClick={() => emit('upload')}>
                 //     <img src={iconUpload} />
@@ -260,9 +273,21 @@ export default defineComponent({
                 // </div> */}
                 // </div>
                 <div class={styles.headerButton}>
-                  <div class={[styles.headBtn]} onClick={() => emit('upload')}>
-                    保存演奏
+                  <div class={[styles.headBtn, noSaveTips.value ? styles.headBtnDisable : '']} onClick={() => {
+                    if (!noSaveTips.value) {
+                      emit('upload')
+                    }
+                  }}>
+                    保存作品
                   </div>
+                  {
+                    noSaveTips.value && runtime.initShow ? 
+                    <div class={[styles.noSaveTip]}>
+                      <span class={styles.arrowIcon}></span>
+                      {noSaveTips.value}
+                      <i onClick={() => runtime.initShow = false}></i>
+                    </div> : null                   
+                  }
                 </div>                
               ) : null}
             </div>

BIN
src/subpages/colexiu/popups/evaluating/icons/close_icon.png


+ 46 - 0
src/subpages/colexiu/popups/evaluating/index.module.less

@@ -275,4 +275,50 @@
           opacity: .8;
       }
   }
+
+  .headBtnDisable {
+    background: #C4C0BD;
+    color: #656565;
+  }
+
+  .noSaveTip {
+    background: rgba(0,0,0,0.7);
+    font-size: 13Px;
+    border-radius: 8Px;
+    display: flex;
+    align-items: center;
+    padding: 8Px 12Px;
+    position: absolute;
+    left: 50%;
+    top: 48Px;
+    transform: translateX(-60%);
+    z-index: 1;
+    color: #fff;
+    width: auto;
+    word-break: keep-all;
+    > i {
+      display: inline-block;
+      min-width: 12Px;
+      width: 12Px;
+      height: 12Px;
+      background-image: url('./icons/close_icon.png');
+      background-size: 100% 100%;
+      background-position: center center;
+      background-repeat: no-repeat;
+      margin-left: 16Px;
+      cursor: pointer;
+    }
+    .arrowIcon {
+      position: absolute;
+      left: 50%;
+      top: -8PX;
+      transform: translateX(16PX);
+      width: 0;
+      height: 0;
+      border-bottom: 8PX solid rgba(0,0,0,0.7);
+      border-right: 8PX solid transparent;
+      border-left: 8PX solid transparent;  
+      z-index: 2;      
+    }
+  }
 }

+ 6 - 2
src/subpages/colexiu/popups/evaluating/index.tsx

@@ -9,6 +9,8 @@ import SettingState from '/src/pages/detail/setting-state'
 import { IPostMessage, postMessage } from '/src/helpers/native-message'
 import detailState from '/src/pages/detail/state'
 import { browser } from '/src/helpers/utils'
+import runtime, * as RuntimeUtils from '/src/pages/detail/runtime'
+
 export const evaluatingShow = ref<boolean>(false)
 const open = ref(false)
 
@@ -92,8 +94,8 @@ export default defineComponent({
     /** 评测结果按钮处理 */
     const handleEvaluatResult = () => {
       const browserInfo = browser()
-      // 如果音频是midi格式,或者非学生端,走之前的逻辑
-      if (detailState.isAppPlay || !browserInfo.isStudent) {
+      // 如果音频是midi格式,走之前的逻辑
+      if (detailState.isAppPlay) {
         confirmShow.value = true
       } else {
         if (props.data?.recordIdStr) {
@@ -102,6 +104,8 @@ export default defineComponent({
             recordId: String(props.data?.recordIdStr),
             title: detailState.activeDetail?.musicSheetName || "曲谱演奏",
             coverImg: detailState.activeDetail?.titleImg || '',
+            speedRate: parseFloat(runtime.basePlayRate.toFixed(2)), // 播放倍率
+            // firstNoteTime: detailState.firstNoteTime,
           });
         }
       }

+ 3 - 2
src/subpages/colexiu/unitTest/index.tsx

@@ -86,8 +86,9 @@ export default defineComponent({
         (n: any) => n.noteElement.sourceMeasure.MeasureNumberXML == questionExtendsInfo.value.end
       )
       const startNote = startNotes[0]
-      const endNote = endNotes[endNotes.length - 1]
-      //   console.log('🚀 ~ activeNote', startNote, endNote, questionExtendsInfo.value.end)
+      // const endNote = endNotes[endNotes.length - 1]
+      const endNote = endNotes[0]
+      // console.log('🚀 ~ activeNote', startNote, endNote, questionExtendsInfo.value.end)
       if (startNote && endNote) {
         unitTestData.isSelectMeasureMode = true
         // 设置小节

+ 1 - 1
src/subpages/colexiu/uses/use-app.ts

@@ -83,7 +83,7 @@ export const useMp3s = async (detail: MusicSheelDetail) => {
   const backgroundSong = isEncoded(detail.metronomeUrl || '') ? detail.metronomeUrl || '' : encodeURI(detail.metronomeUrl || '')
   // 原音
   const musicSong = isEncoded(activebg?.audioFileUrl|| '') ? activebg?.audioFileUrl || '' : encodeURI(activebg?.audioFileUrl|| '')
-  
+  console.log('伴奏:',backgroundSong,'原音:',musicSong)
   // 兼容未修改之前
   runtime.songs = {
     background: backgroundSong ? backgroundSong + '?t=background' : '',