Selaa lähdekoodia

Merge branch 'feature-tianyong' into gym-test

TIANYONG 9 kuukautta sitten
vanhempi
commit
b07d08ff4b

BIN
src/assets/tick.wav


BIN
src/assets/tock.wav


+ 2 - 1
src/helpers/formateMusic.ts

@@ -1292,7 +1292,8 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 				// 找出这个音符前面音符的结束时间
 				let preNoteTImes = allNotes[allNotes.length - 1]?.endtime*1000
 				if(!preNoteTImes){
-					preNoteTImes = Math.max(fixtime - noteLength, 0)*1000 //如果前一个音符没有结束时间,证明这个音符是第一个音符没有打时间,所以往前奏里面找补
+					//如果前一个音符没有结束时间,证明这个音符是第一个音符没有打时间,当有timegap以fixtime当开始时间(1795013294269087745),当第一个小节有times这个往前奏里面找补(1795013306436763649)
+					preNoteTImes = (state.evXmlBeginArr.length>0 ? fixtime : Math.max(fixtime - noteLength, 0))*1000
 				}
 				// 找出这个音符后面音符的开始时间
 				let nextI = i

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

@@ -81,7 +81,7 @@ export default defineComponent({
 				if (state.playState === "play") {
 					training.starTime = Date.now();
 				} else {
-					addHomeworkRecored();
+					// addHomeworkRecored();
 				}
 			}
 		);

+ 3 - 3
src/page-instrument/evaluat-model/earphone/index.module.less

@@ -20,9 +20,9 @@
     .earphoneBtn {
         position: absolute;
         left: 50%;
-        bottom: 13px;
-        width: 133px;
-        height: 39px;
+        bottom: 25px;
+        width: 105px;
+        height: 30px;
         transform: translateX(-43%);
     }
 }

BIN
src/page-instrument/header-top/image/gl.png


BIN
src/page-instrument/header-top/image/glImg.png


BIN
src/page-instrument/header-top/image/lx.png


BIN
src/page-instrument/header-top/image/lxImg.png


BIN
src/page-instrument/header-top/image/pc.png


BIN
src/page-instrument/header-top/image/pcImg.png


BIN
src/page-instrument/header-top/image/sj.png


+ 3 - 1
src/page-instrument/header-top/index.module.less

@@ -156,7 +156,9 @@
         align-items: center;
         cursor: pointer;
         margin-right: 24px;
-
+        &.modeType{
+            margin-right: 14px;
+        }
         &:last-child {
             margin-right: 0;
         }

+ 16 - 5
src/page-instrument/header-top/index.tsx

@@ -495,7 +495,18 @@ export default defineComponent({
     const browInfo = browser();
     /** 返回 */
     const handleBack = () => {
-      HANDLE_WORK_ADD();
+      // 如果是乐教通,点击返回按钮,需要关闭当前窗口
+      if (query["isYjt"] == "1") {
+        window.parent.postMessage(
+          {
+            api: "api_YjtClose"
+          },
+          "*"
+        );
+        return
+      }
+      // 练习作业,完整练习才记录练习次数
+      // HANDLE_WORK_ADD();
       // 不在APP中,
       if (!storeData.isApp) {
         window.parent.postMessage(
@@ -657,7 +668,7 @@ export default defineComponent({
           }}
         >
           {/* 返回和标题 */}
-          {!(state.playState == "play" || followData.start || evaluatingData.startBegin) && (
+          {!(state.playState == "play" || followData.start || evaluatingData.startBegin) && !state.isWeb &&  (
             <div id="noticeBarRollDom" class={styles.headTopLeftBox}>
               {
                 !query.isMove && <img src={iconBack} class={["headTopBackBtn", styles.img, !headTopData.showBack && styles.hidenBack]} onClick={handleBack} />
@@ -680,7 +691,7 @@ export default defineComponent({
                   <NoticeBar text={state.examSongName} background="none" />
                 </div>
               ) : (
-                isMusicList.value && (
+                isMusicList.value && !state.isHomeWork && (
                   <img
                     src={listImg}
                     class={[styles.img, styles.listImg, "driver-8"]}
@@ -711,8 +722,8 @@ export default defineComponent({
             {
               <div
                 id={state.platform === IPlatform.PC ? "teacherTop-0" : "studnetT-0"}
-                style={{ display: toggleBtn.value.display ? "" : "none" }}
-                class={["driver-9", styles.btn, toggleBtn.value.disabled && styles.disabled]}
+                style={{ display: toggleBtn.value.display ? "" : "none"}}
+                class={["driver-9", styles.btn, toggleBtn.value.disabled && styles.disabled, styles.modeType]}
                 onClick={() => {
                   headTopData.oldModeType = state.modeType;
                   handleRessetState();

+ 23 - 23
src/page-instrument/header-top/settting/index.module.less

@@ -89,33 +89,33 @@
                         flex-grow: 1;
                         :global{
                             .van-slider{
-                                height: 10px;
+                                height: 5px;
                                 background: #EAEAEA;
                                 .van-slider__bar{
                                     background: #01C1B5;
-                                    &::after{
-                                        position: absolute;
-                                        content: "";
-                                        left: 4px;
-                                        top: 2px;
-                                        width: 100%;
-                                        height: 1px;
-                                        background: #FFFFFF;
-                                        border-radius: 1px;
-                                        filter: blur(1px);
-                                    }
-                                    &::before{
-                                        position: absolute;
-                                        content: "";
-                                        left: 2px;
-                                        top: 1px;
-                                        width: 4px;
-                                        height: 4px;
-                                        background: url("../image/gg.png") no-repeat;
-                                        background-size: 100% 100%;
-                                    }
+                                    // &::after{
+                                    //     position: absolute;
+                                    //     content: "";
+                                    //     left: 4px;
+                                    //     top: 2px;
+                                    //     width: 100%;
+                                    //     height: 1px;
+                                    //     background: #FFFFFF;
+                                    //     border-radius: 1px;
+                                    //     filter: blur(1px);
+                                    // }
+                                    // &::before{
+                                    //     position: absolute;
+                                    //     content: "";
+                                    //     left: 2px;
+                                    //     top: 1px;
+                                    //     width: 4px;
+                                    //     height: 4px;
+                                    //     background: url("../image/gg.png") no-repeat;
+                                    //     background-size: 100% 100%;
+                                    // }
                                     .van-slider__button-wrapper{
-                                        bottom: -4px;
+                                        bottom: -6px;
                                         top: initial;
                                         transform: translateX(50%);
                                     }

+ 10 - 2
src/page-instrument/header-top/settting/index.tsx

@@ -57,6 +57,12 @@ export default defineComponent({
             return list;
         });
 
+        const metronomeList = computed(() => {
+            const list =  state.modeType === 'follow' ? [{name:'音符',value:1},{name:'关闭',value:3}] : [{name:'音符',value:1},{name:'节拍',value:2},{name:'关闭',value:3}];
+            return list;
+        });
+       
+
 		return () => (
 			<div class={[styles.settting]}>
                 <div class={[styles.head, "top_draging"]}>
@@ -95,7 +101,7 @@ export default defineComponent({
                                 <div class={styles.tit}>指针模式</div>
                                 <div class={styles.radioBox}>
                                     {
-                                        [{name:'音符',value:1},{name:'节拍',value:2},{name:'关闭',value:3}].map(item=>{
+                                        metronomeList.value.map(item=>{
                                             return <div class={ metronomeData.cursorMode===item.value && styles.active } onClick={ ()=>{
                                                  // 切换光标模式
                                                 metronomeData.cursorMode = item.value
@@ -107,7 +113,9 @@ export default defineComponent({
                         <div class={styles.pointerCon}>
                             <div class={styles.pointerBox}>
                                 <div>音符:指针跟随音符播放</div>
-                                <div>节拍:指针跟随节拍播放</div>
+                                {
+                                    state.modeType !== 'follow' && <div>节拍:指针跟随节拍播放</div>
+                                }
                                 <div>关闭:不显示指针</div>
                             </div>
                         </div>

+ 24 - 24
src/page-instrument/header-top/speed/index.module.less

@@ -56,35 +56,35 @@
                     flex-grow: 1;
                     :global{
                         .van-slider{
-                            height: 10px;
+                            height: 5px;
                             background: #EAEAEA;
                             .van-slider__bar{
                                 max-width: 100%;
                                 background: #01C1B5;
-                                &::after{
-                                    position: absolute;
-                                    content: "";
-                                    left: 4px;
-                                    top: 2px;
-                                    width: 100%;
-                                    height: 1px;
-                                    background: #FFFFFF;
-                                    border-radius: 1px;
-                                    filter: blur(1px);
-                                }
-                                &::before{
-                                    position: absolute;
-                                    content: "";
-                                    left: 2px;
-                                    top: 1px;
-                                    width: 4px;
-                                    height: 4px;
-                                    background: url("../image/gg.png") no-repeat;
-                                    background-size: 100% 100%;
-                                    transform: translate(-20%,-20%);
-                                }
+                                // &::after{
+                                //     position: absolute;
+                                //     content: "";
+                                //     left: 4px;
+                                //     top: 2px;
+                                //     width: 100%;
+                                //     height: 1px;
+                                //     background: #FFFFFF;
+                                //     border-radius: 1px;
+                                //     filter: blur(1px);
+                                // }
+                                // &::before{
+                                //     position: absolute;
+                                //     content: "";
+                                //     left: 2px;
+                                //     top: 1px;
+                                //     width: 4px;
+                                //     height: 4px;
+                                //     background: url("../image/gg.png") no-repeat;
+                                //     background-size: 100% 100%;
+                                //     transform: translate(-20%,-20%);
+                                // }
                                 .van-slider__button-wrapper{
-                                    bottom: -4px;
+                                    bottom: -6px;
                                     top: initial;
                                     transform: translateX(50%);
                                 }

+ 2 - 2
src/page-instrument/view-detail/index.module.less

@@ -31,7 +31,7 @@
     overflow: hidden;
     --header-height: 60px;
     --pc-header-height: 72px;
-
+    background: #fff;
     // &.practise{
     //     background: url("./images/bg1.png") no-repeat;
     //     background-size: 100% 100%;
@@ -176,7 +176,7 @@
         }
 
         .headTopBackBtn {
-            display: none;
+            // display: none;
         }
 
         .pcTitle {

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

@@ -157,6 +157,7 @@ export default defineComponent({
       state.isHomeWork = query.workRecord || query.evaluatingRecord;
       // 如果是纯预览模式,0.65倍缩放谱面
       state.isPreView = query.isPreView;
+      state.isWeb = query.isWeb;
       if (state.isPreView) {
         state.zoom = query.zoom  || 0.65
       }

+ 9 - 1
src/state.ts

@@ -22,6 +22,7 @@ import { musicScoreRef, headerColumnHide } from "/src/page-instrument/view-detai
 import { headTopData } from "/src/page-instrument/header-top/index";
 import { api_lessonTrainingTrainingStudentDetail } from "/src/page-instrument/api"
 import { undoData, moveData } from "/src/view/plugins/move-music-score"
+import { HANDLE_WORK_ADD } from "/src/page-instrument/custom-plugins/work-index";
 
 const query: any = getQuery();
 
@@ -515,6 +516,8 @@ const state = reactive({
   isPreView: false,
   /** 是否为内容平台预览模式 */
   isCbsView: false,  
+  /** 是否是管乐迷后台预览模式 */
+  isWeb: false,
   /** 是否为评测报告模式 */
   isEvaluatReport: false,
   /** midi播放器是否初始化中 */
@@ -749,6 +752,10 @@ const handlePlaying = () => {
         }
         // #8698 bug修复
         if (state.modeType === "practise" && state.sectionStatus) {
+          // 练习作业,练习完一次需要增加练习次数
+          if (query.workRecord) {
+            HANDLE_WORK_ADD()
+          }
           onEnded();
           // state.activeNoteIndex = state.sectionFirst ? state.sectionFirst.i : state.section[0].i
           // dynamicShowPlaySpeed(state.activeNoteIndex)
@@ -1568,8 +1575,9 @@ function initMusicSource(data: any, tracks: string[], partIndex: number, workRec
     state.mingSong = fanSongObj?.solmizationFileUrl
     state.mingSongGirl = fanSongObj?.femaleSolmizationFileUrl
   }
+   /*  目前 管乐迷没有用到 后台生成的节拍器 */
   // 当使用节拍器的时候才加载节拍器音频
-  if(state.isMixBeat) {
+  if(state.isMixBeat && false) {
     Object.assign(state.beatSong, {
       music: musicObj?.audioBeatMixUrl,
       accompany: accompanyObj?.audioBeatMixUrl,

+ 11 - 15
src/utils/crunker.ts

@@ -3,7 +3,7 @@ interface CrunkerConstructorOptions {
    concurrentNetworkRequests: number
 }
 
-type CrunkerInputTypes = string | File | Blob
+type CrunkerInputTypes = string | File | Blob | undefined
 
 export default class Crunker {
    private readonly _sampleRate: number
@@ -23,8 +23,8 @@ export default class Crunker {
    /**
     *转换url等类型为buffer
     */
-   async fetchAudio(...filepaths: CrunkerInputTypes[]): Promise<AudioBuffer[]> {
-      const buffers: AudioBuffer[] = []
+   async fetchAudio(...filepaths: CrunkerInputTypes[]): Promise<(AudioBuffer | undefined)[]> {
+      const buffers: (AudioBuffer | undefined)[] = []
       const groups = Math.ceil(filepaths.length / this._concurrentNetworkRequests)
       for (let i = 0; i < groups; i++) {
          const group = filepaths.slice(i * this._concurrentNetworkRequests, (i + 1) * this._concurrentNetworkRequests)
@@ -32,9 +32,12 @@ export default class Crunker {
       }
       return buffers
    }
-   private async _fetchAudio(...filepaths: CrunkerInputTypes[]): Promise<AudioBuffer[]> {
+   private async _fetchAudio(...filepaths: CrunkerInputTypes[]): Promise<(AudioBuffer | undefined)[]> {
       return await Promise.all(
          filepaths.map(async filepath => {
+            if (!filepath) {
+               return Promise.resolve(undefined)
+            }
             let buffer: ArrayBuffer
             if (filepath instanceof File || filepath instanceof Blob) {
                buffer = await filepath.arrayBuffer()
@@ -74,24 +77,17 @@ export default class Crunker {
       }
       const output = this._context.createBuffer(this._maxNumberOfChannels(buffers), this._sampleRate * this._maxDuration(buffers), this._sampleRate)
       buffers.forEach((buffer, index) => {
+         const offsetNum = Math.round(times[index] * this._sampleRate) //时间偏差
          for (let channelNumber = 0; channelNumber < buffer.numberOfChannels; channelNumber++) {
             const outputData = output.getChannelData(channelNumber)
             const bufferData = buffer.getChannelData(channelNumber)
-            const offsetNum = Math.round(times[index] * this._sampleRate) //时间偏差
-            for (let i = buffer.getChannelData(channelNumber).length - 1; i >= 0; i--) {
-               outputData[i + offsetNum] += bufferData[i]
+            for (let i = bufferData.length - 1; i >= 0; i--) {
                // 当合并大于1或者小于-1的时候可能会爆音  所以这里取最大值和最小值
-               if (outputData[i + offsetNum] > 1) {
-                  outputData[i + offsetNum] = 1
-               }
-               if (outputData[i + offsetNum] < -1) {
-                  outputData[i + offsetNum] = -1
-               }
+               const combinedValue = outputData[i + offsetNum] + bufferData[i]
+               outputData[i + offsetNum] = Math.max(-1, Math.min(1, combinedValue))
             }
-            output.getChannelData(channelNumber).set(outputData)
          }
       })
-
       return output
    }
    /**

+ 44 - 1
src/view/audio-list/index.tsx

@@ -14,6 +14,10 @@ import { evaluatingData } from "/src/view/evaluating";
 import { cloudToggleState } from "/src/helpers/midiPlay"
 import { storeData } from "/src/store";
 import { handleStartTick } from "../tick";
+import Crunker from "/src/utils/crunker"
+import tickMp3 from "/src/assets/tick.wav"
+import tockMp3 from "/src/assets/tock.wav"
+import { metronomeData } from "/src/helpers/metronome";
 
 export const audioData = reactive({
 	songEle: null as HTMLAudioElement | null, // 原生
@@ -334,6 +338,43 @@ export default defineComponent({
 		function loadBeatAudio(){
 			return Promise.all([createAudio(state.beatSong.music), createAudio(state.beatSong.accompany), createAudio(state.beatSong.fanSong), createAudio(state.beatSong.banSong), createAudio(state.beatSong.mingSong), createAudio(state.beatSong.mingSongGirl)])
 		}
+		// 合成节拍器资源
+		async function mergeBeatAudio(){
+			let beatMusic, beatAccompany
+			console.time("音频合成时间")
+			try{
+				const crunker = new Crunker()
+				console.time("音频加载时间")
+				const [musicBuff, accompanyBuff, tickBuff, tockBuff] = await crunker.fetchAudio(state.music?`${state.music}?v=${Date.now()}`:null, state.accompany?`${state.accompany}?v=${Date.now()}`:null, tickMp3, tockMp3)
+				console.timeEnd("音频加载时间")
+				// 计算音频空白时间
+				const silenceDuration = musicBuff&&!state.isEvxml ? crunker.calculateSilenceDuration(musicBuff) : 0
+				const silenceBgDuration = accompanyBuff&&!state.isEvxml ? crunker.calculateSilenceDuration(accompanyBuff) : 0
+				console.log(`音频空白时间:${silenceDuration};${silenceBgDuration}`)
+				const beats:AudioBuffer[] = []
+				const beatsTime:number[] = []
+				const beatsBgTime:number[] = []
+				metronomeData.metroMeasure.map(measures=>{
+					measures.map((item:any)=>{
+						beats.push(item.index===0?tickBuff!:tockBuff!)
+						beatsTime.push(item.time + silenceDuration) // xml 计算的时候 加上空白的时间
+						beatsBgTime.push(item.time + silenceBgDuration) // xml 计算的时候 加上空白的时间 没有背景不赋值
+					})
+				})
+				console.time("音频合并时间")
+				const musicBuffMeg = musicBuff && crunker.mergeAudioBuffers([musicBuff,...beats],[0,...beatsTime])
+				const accompanyBuffMeg = accompanyBuff && crunker.mergeAudioBuffers([accompanyBuff,...beats],[0,...beatsBgTime])
+				console.timeEnd("音频合并时间")
+				console.time("音频audioDom生成时间")
+				beatMusic = musicBuffMeg && crunker.exportAudioElement(musicBuffMeg)
+				beatAccompany = accompanyBuffMeg && crunker.exportAudioElement(accompanyBuffMeg)
+				console.timeEnd("音频audioDom生成时间")
+			}catch(err){
+				console.log(err)
+			}
+			console.timeEnd("音频合成时间")
+			return [beatMusic, beatAccompany]
+		}
 		onMounted(async () => {
 			// 预览的时候不走音频加载逻辑
 			if(state.isPreView){
@@ -384,7 +425,9 @@ export default defineComponent({
 					mingSongGirl.addEventListener("ended", onEnded);
 				}
 				// 处理带节拍器的音源
-				const [beatMusic, beatAccompany, beatFanSong, beatBanSong, beatMingSong, beatMingSongGirl] = await loadBeatAudio()
+				//const [beatMusic, beatAccompany, beatFanSong, beatBanSong, beatMingSong, beatMingSongGirl] = await loadBeatAudio()
+				// 客户端合成节拍器
+				const [beatMusic, beatAccompany, beatFanSong, beatBanSong, beatMingSong, beatMingSongGirl] = await mergeBeatAudio()
 				Object.assign(audioData.songCollection, {
 					beatSongEle:beatMusic,
 					beatBackgroundEle:beatAccompany,

+ 4 - 4
src/view/plugins/useDrag/dragbom.tsx

@@ -13,7 +13,7 @@ export default defineComponent({
 	},
   setup(props, { emit }) {
     const data = reactive({
-      guidePos: "bottom" as "bottom" | "left" | "right",
+      guidePos: "bottom" as "bottom" | "left" | "right" | "top",
     });
 
     const initGuidePos = () => {
@@ -25,8 +25,8 @@ export default defineComponent({
       const dragTop = dragBBox?.top || 0;
       const dragLeft = dragBBox?.left || 0;
       // 引导页出现在下边
-      if (pageHeight - dragTop > guideHeight) {
-        data.guidePos = "bottom"
+      if (pageHeight - dragTop < guideHeight + 20) {
+        data.guidePos = "top"
       } else {
         // 引导页出现在左边or右边
         data.guidePos = dragLeft > guideWidth ? "left" : "right"
@@ -48,7 +48,7 @@ export default defineComponent({
         </div>
         {
           props.showGuide && 
-          <div class={[styles.guide, data.guidePos === "left" && styles.guideLeft, data.guidePos === "right" && styles.guideRight, 'bom_guide']} onClick={() => emit("guideDone")}>
+          <div class={[styles.guide, data.guidePos === "top" && styles.guideTop, data.guidePos === "left" && styles.guideLeft, data.guidePos === "right" && styles.guideRight, 'bom_guide']} onClick={() => emit("guideDone")}>
             <div class={styles.guideBg}></div>
             <div class={styles.guideDone}></div>
           </div>          

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

@@ -375,6 +375,10 @@ export default defineComponent({
 								item && <div class={styles.selectBox} style={item}>
 									<div class={[styles.selectHandle,index>0&&styles.selectHandleRight,(state.playState==="play" || state.isHomeWork)&&styles.playIng]} onClick={()=>{
 										// 如果选择了2个 删除左边的时候
+										if (state.section.length===1&&index === 0) {
+											// #bug:11552
+											resetBaseRate(state.activeNoteIndex);
+										}
 										if(state.section.length===2&&index === 0){
 											state.section = []
 											// 重置速度和播放倍率