소스 검색

Merge branch 'feature-tianyong' into klx-test

TIANYONG 5 달 전
부모
커밋
7bf64d4e0c

+ 2 - 2
src/helpers/formateMusic.ts

@@ -883,7 +883,7 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 		if (state.isCombineRender) {
 			iterator.currentVoiceEntries = iterator.currentVoiceEntries.filter((item: any) => {
 				const trackName = state.isEvxml && state.evxmlAddPartName ? item.parentVoice.parent.IdString || '' : item.parentVoice.parent.Name || '';
-				return trackName === firstTrackName
+				return trackName?.trim() === firstTrackName
 			});
 		}
 		let minIndex = 0, elRealValue = 0
@@ -1060,7 +1060,7 @@ export const formateTimes = (osmd: OpenSheetMusicDisplay) => {
 			
 			// 合并展示某些分轨,需要把展示的分轨筛选出来
 			if (state.isCombineRender && note.sourceMeasure.verticalMeasureList.length) {
-				note.sourceMeasure.verticalMeasureList = note.sourceMeasure?.verticalMeasureList.filter((item: any) => state.canSelectTracks.includes(item?.parentStaff?.parentInstrument.Name))
+				note.sourceMeasure.verticalMeasureList = note.sourceMeasure?.verticalMeasureList.filter((item: any) => state.canSelectTracks.includes(item?.parentStaff?.parentInstrument.Name?.trim()))
 			}
 
 			activeVerticalMeasureList = [note.sourceMeasure?.verticalMeasureList?.[currentTrackIndex]] || [];

+ 24 - 24
src/helpers/svgToPng.ts

@@ -1,29 +1,29 @@
 // 将svg转成png
 export const getSvgPngToSize = (osmd: any) => {
-    if (osmd) {
-      if (osmd.Drawer.Backends.length > 0) {
-        var imgList = []
-        
-        for (var idx = 0, len = osmd.Drawer.Backends.length; idx < len; idx++) {
-          var backend = osmd.Drawer.Backends[idx]
-          var state = backend.ctx.state;
-          var width = backend.ctx.width / state.scale.x;
-          var height = backend.ctx.height / state.scale.y;
-          const textX = width - 120,textY = height - 50;
-          const textDom = `<g><text x="${textX}" y="${textY}" stroke-width="3" fill="#000000" stroke="none" stroke-dasharray="none" font-family="Times New Roman" font-size="36px" font-weight="bold" font-style="none">第${idx+1}页</text></g>`
-          backend.ctx.svg.innerHTML = backend.ctx.svg.innerHTML + textDom;
-          var cont = new XMLSerializer().serializeToString(
-            backend.ctx.svg
-          )
-          imgList.push({
-            img: cont,
-            width: width,
-            height: height,
-          })
-        }
-        return imgList
+  if (osmd) {
+    if (osmd.Drawer.Backends.length > 0) {
+      var imgList = []
+      
+      for (var idx = 0, len = osmd.Drawer.Backends.length; idx < len; idx++) {
+        var backend = osmd.Drawer.Backends[idx]
+        var state = backend.ctx.state;
+        var width = backend.ctx.width / state.scale.x;
+        var height = backend.ctx.height / state.scale.y;
+        const textX = width - 90,textY = height - 90;
+        const textDom = `<g><text x="${textX}" y="${textY}" stroke-width="3" fill="#000000" stroke="none" stroke-dasharray="none" font-family="Times New Roman" font-size="36px" font-weight="bold" font-style="none">${idx+1}/${len}</text></g>`
+        backend.ctx.svg.innerHTML = backend.ctx.svg.innerHTML + textDom;
+        var cont = new XMLSerializer().serializeToString(
+          backend.ctx.svg
+        )
+        imgList.push({
+          img: cont,
+          width: width,
+          height: height,
+        })
       }
-    } else {
-      console.log('没有OSMD')
+      return imgList
     }
+  } else {
+    console.log('没有OSMD')
+  }
 }

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

@@ -10,7 +10,7 @@ import Speed from "./speed";
 import { evaluatingData, handleStartEvaluat } from "/src/view/evaluating";
 import Settting from "./settting";
 import state, { IPlatform, handleChangeSection, handleResetPlay, handleRessetState, togglePlay, IPlayState, refreshMusicSvg, EnumMusicRenderType, resetSettings, handleGuide, resetCursorPosition } from "/src/state";
-import { getAudioCurrentTime } from "/src/view/audio-list";
+import { audioData, getAudioCurrentTime } from "/src/view/audio-list";
 import { followData, toggleFollow } from "/src/view/follow-practice";
 import { api_back } from "/src/helpers/communication";
 import MusicType from "./music-type";
@@ -402,6 +402,8 @@ export default defineComponent({
       if (state.modeType === "follow") return { display: false, disabled: false };
       // 评测开始 禁用
       if (state.modeType === "evaluating") return { display: false, disabled: true };
+      // 总谱渲染在播放过程中 不能切换 
+      if(state.isCombineRender && state.playState === "play") return { display: true, disabled: true } 
       if (!state.isAppPlay) {
         if (state.playType === "play") {
           // 原声, 伴奏 少一个,就不能切换
@@ -850,6 +852,11 @@ export default defineComponent({
                   }
                 }
                 handlerModeChange(oldPlayType, oldPlaySource);
+                // 总谱 并且开启了单个声轨音频时候
+                if(state.isCombineRender && state.playSource === "background") {
+                  audioData.combineIndex = -1
+                  state.music = ""
+                }                
                 showToast({
                   message: state.playType === "play" ? (state.playSource === "music" ? "已切换为原声" : "已切换为伴奏") : state.playSource === "music" ? "已切换为范唱" : state.playSource === "background" ? "已切换为伴唱" : "已切换为唱名",
                   position: "top",

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

@@ -66,6 +66,7 @@
                     font-size: 15px;
                     color: #000000;
                     line-height: 21px;
+                    word-break: keep-all;
                 }
                 .titbtn {
                     width: 78px;
@@ -178,11 +179,16 @@
                         }
                     }
                 }
-                .speBox {
+                .qhBox {
                     >div {
-                        width: 54px;
+                        width: 56px;
                     }
                 }                
+                // .speBox {
+                //     >div {
+                //         width: 54px;
+                //     }
+                // }                
                 .frequency{
                     display: flex;
                     align-items: center;

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

@@ -84,7 +84,23 @@ export default defineComponent({
                                     <div class={styles.tit}>循环播放</div>
                                     <Switch v-model={state.setting.repeatAutoPlay}></Switch>
                                 </div>
-                        }                        
+                        }    
+                        {   !state.isCombineRender &&                    
+                            <div class={styles.cellBox}>
+                                <div class={styles.tit}>合并休止小节</div>
+                                <Switch 
+                                    v-model={state.setting.combineMultipleRest}
+                                    onChange={ async (value) => {
+                                        await checkMoveNoSave();
+                                        headTopData.settingMode = false
+                                        const _time = setTimeout(() => {
+                                            clearTimeout(_time)
+                                            refreshMusicSvg();
+                                        }, 100);
+                                    }}
+                                ></Switch>
+                            </div>
+                        }                                            
                         {/* {
                             state.isSingleLine && state.modeType === "practise" && !state.isCombineRender && !state.isPercussion && 
                                 <div class={styles.cellBox}>
@@ -230,7 +246,7 @@ export default defineComponent({
                             ["practise", "evaluating"].includes(state.modeType) ? 
                             <div class={styles.cellBox}>
                                 <div class={styles.tit}>切换谱面</div>
-                                <div class={styles.radioBox}>
+                                <div class={[styles.radioBox, styles.qhBox]}>
                                     {
                                         [{name:'单行谱',value:true},{name:'多行谱',value:false}].map(item=>{
                                             return <div class={ state.isSingleLine===item.value && styles.active } onClick={ async ()=>{ 

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

@@ -1,7 +1,10 @@
 .speedContainer{
     width: 334px;
     &.isHideBeat .content{
-        height: 172px;
+        height: 206px;
+        .speedSel{
+            padding-bottom: 0px !important;
+        }
     }
     .head{
         height: 42px;
@@ -25,7 +28,7 @@
     }
     .content{
         margin-top: -26px;
-        height: 230px;
+        height: 264px;
         background: #FFFFFF;
         border-radius: 16px;
         padding: 36px 16px 16px 16px;
@@ -122,8 +125,8 @@
                 }
             }
             .speedSel{
-                margin-top: 20px;
-                padding-bottom: 8px;
+                margin-top: 8px;
+                padding-bottom: 18px;
                 display: flex;
                 justify-content: space-between;
                 flex-wrap: wrap;
@@ -138,7 +141,7 @@
                     font-size: 13px;
                     color: rgba(0,0,0,0.6);
                     cursor: pointer;
-                    margin-bottom: 10px;
+                    margin-top: 10px;
                     margin-right: 3px;
                     &:active{
                         background: #B9F2DF;

+ 2 - 1
src/page-instrument/header-top/speed/index.tsx

@@ -39,7 +39,8 @@ export default defineComponent({
 				state.speed = speed.value
 				// handleSetSpeed(speed.value);
 				if (state.playState === 'paused') {
-					const currentItem: any = (state.sectionStatus && state.section.length === 2) ? state.sectionFirst || state.section[0] : state.times[state.activeNoteIndex];
+					// const currentItem: any = (state.sectionStatus && state.section.length === 2) ? state.sectionFirst || state.section[0] : state.times[state.activeNoteIndex];
+					const currentItem: any = state.times[state.activeNoteIndex];
 					state.basePlayRate = currentItem?.measureSpeed ? state.speed / currentItem.measureSpeed : state.speed / state.originSpeed;
 				}
 			}

+ 11 - 9
src/page-instrument/view-detail/index.tsx

@@ -316,15 +316,17 @@ export default defineComponent({
       // }
 
       // 管乐迷曲谱详情页,需要下载A4尺寸的图片
-      if (query.downPng === 'A4') {
-        const imgList = getSvgPngToSize(state.osmd)
-        console.log('A4', imgList)
-        window.parent.postMessage({
-          api: 'musicStaffRender',
-          loading: false,
-          osmdImg: imgList
-        }, '*');
-      }
+      setTimeout(() => {
+        if (query.downPng === 'A4' && state.partIndex != 999) {
+          const imgList = getSvgPngToSize(state.osmd)
+          console.log('A4', imgList)
+          window.parent.postMessage({
+            api: 'musicStaffRender',
+            loading: false,
+            osmdImg: imgList
+          }, '*');
+        }
+      }, 100);
 
       state.musicScoreBtnDirection = state.playBtnDirection;
       state.musicRendered = true;

+ 35 - 4
src/state.ts

@@ -441,6 +441,8 @@ const state = reactive({
     reactionTimeMs: 0,
     /** 节拍器音量 */
     beatVolume: 50,
+    /** 合并休止小节 */
+    combineMultipleRest: true,
   },
   /** 后台设置的基准评测频率 */
   baseFrequency: 440,
@@ -834,7 +836,7 @@ export const skipNotePlay = async (itemIndex: number, isStart = false, handType?
     // 非选段模式,点击音符,动态设置右下角的速度
     if (item.measureSpeed && state.section.length < 2) {
       // console.log('速度3')
-      state.speed = state.basePlayRate * item.measureSpeed
+      state.speed = state.basePlayRate * 10000 * item.measureSpeed / 10000
     }
     setAudioCurrentTime(itemTime, itemIndex);
     // 一行谱,点击音符,或者播放完成,需要跳转音符位置
@@ -1191,7 +1193,8 @@ export const handleSetSpeed = (speed: number) => {
   // setStorageSpeed(state.examSongId, speed);
   state.speed = speed;
   // 当前的音符
-  const currentItem: any = (state.sectionStatus && state.section.length === 2) ? state.section[0] : state.times[state.activeNoteIndex];
+  // const currentItem: any = (state.sectionStatus && state.section.length === 2) ? state.section[0] : state.times[state.activeNoteIndex];
+  const currentItem: any = state.times[state.activeNoteIndex];
   state.basePlayRate = currentItem?.measureSpeed ? state.speed / currentItem.measureSpeed : state.speed / state.originSpeed;
   const actualRate = state.originAudioPlayRate * state.basePlayRate;
   console.log('速度设置',speed,'小节计算的倍率',state.basePlayRate,'实际播放倍率',actualRate)
@@ -1213,6 +1216,14 @@ export const handleChangeSection = () => {
     resetBaseRate(state.activeNoteIndex);
     //skipNotePlay(0, true);  取消选段的时候 不跳回开头
     state.sectionFirst = null;
+    // IOS18.1.1浏览器渲染更新有问题,需要手动更新一下
+    const selectionDom = document.getElementById('selectionBox')
+    if (selectionDom) {
+      selectionDom.style.display = 'none';
+      requestAnimationFrame(() => {
+        selectionDom.style.display = 'block';
+      })
+    }    
     return;
   }
   state.sectionStatus = true;
@@ -1398,13 +1409,13 @@ export const scrollViewNote = (resetTop?: boolean) => {
     musicScrollTop = (offsetTop - state.headTopHeight - 30) * state.musicZoom
     musicAndSelection.scrollTo({
       top: (offsetTop - state.headTopHeight - 30) * state.musicZoom,
-      behavior: "smooth",
+      behavior: "auto",
     });
   } else {
     musicScrollTop = 0
     musicAndSelection.scrollTo({
       top: 0,
-      behavior: "smooth",
+      behavior: "auto",
     });
   }
 };
@@ -1519,6 +1530,9 @@ function xmlToTracks(xmlString: string) {
   const partNames = Array.from(xmlParse.getElementsByTagName('part-name'));
   return partNames.reduce((arr: string[], item) => {
     const textContent = item?.textContent?.trim()
+    if (textContent?.toLocaleLowerCase() === "common") {
+      (window as any).HasCommonTrack = true;
+    }
     if (textContent != "COMMON" && textContent != "common" && textContent) {
       arr.push(textContent)
     }
@@ -1588,6 +1602,23 @@ function initMusicSource(data: any, tracks: string[], partIndex: number, workRec
             audioBeatMixUrl: banSongObj.scoreAudioBeatMixUrl
           }
         }
+        // 总谱 需要播放各个声部的音频
+        if(state.combinePartIndexs.length) {
+          // 当选择多个分轨时候
+          state.combinePartIndexs.map( partI => {
+            const musicSheetSound = musicSheetSoundList.find((item:any)=>{
+              return item.track?.toLowerCase().trim() === tracks[partI]?.toLowerCase().trim()
+            })
+            musicSheetSound?.audioFileUrl && (audioData.combineMusics[partI] = musicSheetSound.audioFileUrl)
+          })
+        }else{
+          tracks.map((itemTrack:any, partI:number) => {
+            const musicSheetSound = musicSheetSoundList.find((item:any)=>{
+              return item.track?.toLowerCase().trim() === itemTrack?.toLowerCase().trim()
+            })
+            musicSheetSound?.audioFileUrl && (audioData.combineMusics[partI] = musicSheetSound.audioFileUrl)
+          })
+        }        
         // 总谱演奏模式是 伴奏
         accompanyObj = musicSheetAccompanimentList.find((item: any) => {
           return item.audioPlayType === "PLAY"

+ 1 - 1
src/utils/crunker.ts

@@ -16,7 +16,7 @@ export default class Crunker {
       this._sampleRate = sampleRate
       this._concurrentNetworkRequests = concurrentNetworkRequests
    }
-   private _createContext(sampleRate = 44_100): AudioContext {
+   private _createContext(sampleRate = 22050): AudioContext {
       window.AudioContext = window.AudioContext || (window as any).webkitAudioContext || (window as any).mozAudioContext
       return new AudioContext({ sampleRate })
    }

+ 141 - 69
src/view/audio-list/index.tsx

@@ -18,6 +18,7 @@ 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";
+import { showToast } from "vant"
 
 export const audioData = reactive({
 	songEle: null as HTMLAudioElement | null, // 原生
@@ -44,7 +45,10 @@ export const audioData = reactive({
 		mingSongGirlEle: null as HTMLAudioElement | null,
 		beatMingSongEle: null as HTMLAudioElement | null,
 		beatMingSongGirlEle: null as HTMLAudioElement | null
-	}
+	},
+	combineIndex: -1, // 当前 播放的总谱音频索引
+	combineMusics: {} as Record<string, any>, // 音频 url
+	combineMusicEles:[] as {key:number, value:HTMLAudioElement, beatValue:HTMLAudioElement|null}[] // 存储的音频el 当大于4个时候删除
 });
 const midiRef = ref();
 /** 播放或暂停 */
@@ -204,6 +208,141 @@ export const changeMingSongType = () =>{
 		audioData.songCollection.beatMingSongEle = mingSongType === 1 ? beatMingSongEle : beatMingSongGirlEle
 	}
 }
+
+const createAudio = (src?: string): Promise<HTMLAudioElement | null> => {
+	if(!src){
+		return Promise.resolve(null)
+	}
+	return new Promise((resolve) => {
+		const a = new Audio(src + '?v=' + Date.now());
+		a.onloadedmetadata = () => {
+			resolve(a);
+		};
+		a.onerror = () => {
+			resolve(null);
+		};
+		// 当未加载 资源之前 切换到其他浏览器标签,浏览器可能会禁止资源加载所以无法触发onloadedmetadata事件,导致一直在加载中,这里做个兼容
+		if (document.visibilityState === 'visible') {
+			a.load();
+		} else {
+			const onVisibilityChange = () => {
+				if (document.visibilityState === 'visible') {
+					document.removeEventListener('visibilitychange', onVisibilityChange);
+					a.load();
+				}
+			};
+			document.addEventListener('visibilitychange', onVisibilityChange);
+		}
+	});
+};
+
+// 合成节拍器资源
+const crunker = new Crunker()
+async function mergeBeatAudio(music?:string, accompany?:string){
+	let beatMusic, beatAccompany
+	if(!state.isMixBeat) {
+		return [beatMusic, beatAccompany]
+	}
+	console.time("音频合成时间")
+	try{
+		console.time("音频加载时间")
+		const [musicBuff, accompanyBuff, tickBuff, tockBuff] = await crunker.fetchAudio(music?`${music}?v=${Date.now()}`:undefined, accompany?`${accompany}?v=${Date.now()}`:undefined, 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.isTick?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]
+}
+// 切换对应的声轨,并且配置当前的audio
+export async function changeCombineAudio (combineIndex: number){
+	// 重复点击的时候取消选中 原音
+	if(combineIndex === audioData.combineIndex){
+		audioData.combineIndex = -1
+		state.playSource = "background"
+		state.music = ""
+		// 当没有背景音文件的时候
+		if(!state.accompany) {
+			state.noMusicSource = true
+		}
+		return
+	}
+	state.loadingText = "音频资源加载中,请稍后…";
+	state.isLoading = true;
+	const musicUrl = audioData.combineMusics[combineIndex]
+	// 有就拿缓存,没有就加载
+	const cacheMusicIndex = audioData.combineMusicEles.findIndex(ele => {
+		return ele.key === combineIndex
+	})
+	const cacheMusic = audioData.combineMusicEles[cacheMusicIndex]
+	if(cacheMusic?.value){
+		audioData.songCollection.songEle = cacheMusic.value
+		audioData.songCollection.beatSongEle = cacheMusic.beatValue
+		// 使用缓存之后 当前数据位置向后偏移,删除缓存的时候以使用顺序位置
+		const itemMusic = audioData.combineMusicEles.splice(cacheMusicIndex, 1)
+		audioData.combineMusicEles.push(...itemMusic)
+	}else{
+		const music = await createAudio(musicUrl)
+		const [beatMusic] = await mergeBeatAudio(musicUrl)
+		// 当没有背景音的时候 需要绑定事件
+		if(!state.accompany){
+			if(music){
+				music.addEventListener("play", onPlay);
+				music.addEventListener("ended", onEnded);
+			}			
+			if(beatMusic){
+				beatMusic.addEventListener("play", onPlay);
+				beatMusic.addEventListener("ended", onEnded);
+			}
+		}
+		audioData.combineMusicEles.push({
+			key: combineIndex,
+			value: music!,
+			beatValue: beatMusic!
+		})
+		// 当大于4个数据的时候 删除掉最前面的数据
+		if(audioData.combineMusicEles.length > 4){
+			audioData.combineMusicEles.splice(0,1)
+		}
+		audioData.songCollection.songEle = music
+		audioData.songCollection.beatSongEle = beatMusic!
+	}
+	audioData.combineIndex = combineIndex
+	state.music = musicUrl
+	state.playSource = "music"
+	// 当没有背景音文件的时候
+	if(!state.accompany) {
+		state.noMusicSource = false
+	}
+	showToast({
+		message:  "已开启原声",
+		position: "top",
+		className: "selectionToast",
+	});
+	state.isLoading = false;
+}
 export default defineComponent({
 	name: "audio-list",
 	setup() {
@@ -238,33 +377,6 @@ export default defineComponent({
 			}
 		);
 
-		const createAudio = (src?: string): Promise<HTMLAudioElement | null> => {
-			if(!src){
-				return Promise.resolve(null)
-			}
-			return new Promise((resolve) => {
-				const a = new Audio(src + '?v=' + Date.now());
-				a.onloadedmetadata = () => {
-					resolve(a);
-				};
-				a.onerror = () => {
-					resolve(null);
-				};
-				// 当未加载 资源之前 切换到其他浏览器标签,浏览器可能会禁止资源加载所以无法触发onloadedmetadata事件,导致一直在加载中,这里做个兼容
-				if (document.visibilityState === 'visible') {
-					a.load();
-				} else {
-					const onVisibilityChange = () => {
-						if (document.visibilityState === 'visible') {
-							document.removeEventListener('visibilitychange', onVisibilityChange);
-							a.load();
-						}
-					};
-					document.addEventListener('visibilitychange', onVisibilityChange);
-				}
-			});
-		};
-
 		/**
 		 * #11046
 		 * 声音与圆点消失的节点不一致,可能原因是部分安卓手机没有立即播放,所以需要等待有音频进度返回时再播放节拍器
@@ -338,46 +450,6 @@ 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
-			if(!state.isMixBeat) {
-				return [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()}`:undefined, state.accompany?`${state.accompany}?v=${Date.now()}`:undefined, 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.isTick?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){
@@ -430,7 +502,7 @@ export default defineComponent({
 				// 处理带节拍器的音源
 				//const [beatMusic, beatAccompany, beatFanSong, beatBanSong, beatMingSong, beatMingSongGirl] = await loadBeatAudio()
 				// 客户端合成节拍器
-				const [beatMusic, beatAccompany, beatFanSong, beatBanSong, beatMingSong, beatMingSongGirl] = await mergeBeatAudio()
+				const [beatMusic, beatAccompany, beatFanSong, beatBanSong, beatMingSong, beatMingSongGirl] = await mergeBeatAudio(state.music, state.accompany)
 				Object.assign(audioData.songCollection, {
 					beatSongEle:beatMusic,
 					beatBackgroundEle:beatAccompany,

BIN
src/view/music-score/combineAudio/imgs/lock.png


BIN
src/view/music-score/combineAudio/imgs/open.png


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

@@ -0,0 +1,18 @@
+.combineAudio {
+   position: absolute;
+   left: 0;
+   top: 0;
+   z-index: 1;
+   .combineAudioImg {
+      position: absolute;
+      z-index: 119;
+      width: 22PX;
+      height: 22PX;
+      padding: 2PX;
+      transform: scale(var(--combineZoom));
+   }
+   &.play .combineAudioImg {
+      pointer-events: none;
+      opacity: 0.4;
+   }
+}

+ 68 - 0
src/view/music-score/combineAudio/index.tsx

@@ -0,0 +1,68 @@
+import { defineComponent, onMounted, ref, computed } from "vue"
+import styles from "./index.module.less"
+import { audioData, changeCombineAudio } from "/src/view/audio-list"
+import openImg from "./imgs/open.png"
+import lockImg from "./imgs/lock.png"
+import state from "/src/state"
+
+export default defineComponent({
+   name: "combineAudio",
+   setup(props, { emit }) {
+      const elementsData = ref<{ index: number; top: number; left: number }[]>([])
+      onMounted(() => {
+         const parent = document.querySelector("#osmdCanvasPage1")
+         const elements = document.querySelectorAll("g[data-trackIdx]")
+         const musicContainerPos = document.getElementById("musicAndSelection")?.getBoundingClientRect() || {
+            top: 0,
+            left: 0
+         }
+         const combineMusicsIndexs = Object.keys(audioData.combineMusics)
+         elements.forEach(element => {
+            const dataTrackIdx = element.getAttribute("data-trackIdx")
+            // 当有 dataTrackIdx 并且有原音的时候 显示
+            if (dataTrackIdx && combineMusicsIndexs.includes(dataTrackIdx)) {
+               const elementRect = element.getBoundingClientRect()
+               const height = elementRect.height
+               let top = elementRect.top + height / 2 - 11 - musicContainerPos.top
+               let left = elementRect.left - 22 - 10 - musicContainerPos.left
+               elementsData.value.push({
+                  index: parseInt(dataTrackIdx),
+                  top: top,
+                  left: left
+               })
+            }
+         })
+      })
+      const combineZoom = computed(() => {
+         let zoom = state.zoom
+         if (zoom < 1) {
+            zoom = 1
+         } else if (zoom > 1.5) {
+            zoom = 1.5
+         }
+         return zoom
+      })
+      return () => (
+         <>
+            <div class={[styles.combineAudio, state.playState === "play" && styles.play]}>
+               {elementsData.value.map(item => {
+                  return (
+                     <img
+                        class={styles.combineAudioImg}
+                        onClick={() => {
+                           changeCombineAudio(item.index)
+                        }}
+                        style={{
+                           top: item.top + "px",
+                           left: item.left - (combineZoom.value - 1) * 22 + "px",
+                           "--combineZoom": combineZoom.value
+                        }}
+                        src={audioData.combineIndex === item.index ? openImg : lockImg}
+                     />
+                  )
+               })}
+            </div>
+         </>
+      )
+   }
+})

+ 7 - 4
src/view/music-score/index.tsx

@@ -11,6 +11,7 @@ import { resetFormate, resetGivenFormate, setGlobalMusicSheet, limitSingleSvgPag
 import { setGlobalData } from "/src/utils";
 import { storeData } from "/src/store";
 import HorizontalDragScroll from './HorizontalDragScroll';
+import CombineAudio from './combineAudio';
 import { getQuery } from "/src/utils/queryString";
 
 export const musicRenderTypeKey = "musicRenderType";
@@ -97,7 +98,6 @@ export default defineComponent({
 					drawComposer: false, // 渲染作词家
 					// pageBackgroundColor: '#609FCF',
 					// autoGenerateMultipleRestMeasuresFromRestMeasures: state.isSingleLine ? false : true, // 连续休止小节是否合并显示
-					autoGenerateMultipleRestMeasuresFromRestMeasures: true,
 					// darkMode: true, // 暗黑模式
 					// pageFormat: 'A4_P',
 					// autoBeam: true,
@@ -115,7 +115,8 @@ export default defineComponent({
 				drawLyrics: (((!state.accompany && !state.music ) || state.playType === 'sing' || !state.isEvxml) && !state.isSimplePage) ? true : false, // 演唱模式才渲染歌词,simple页面不显示歌词
 				drawPartNames: props.showPartNames, // 是否渲染声轨名称
 				defaultColorMusic: props.musicColor, // 颜色
-				renderSingleHorizontalStaffline: state.isSingleLine ? true : false
+				renderSingleHorizontalStaffline: state.isSingleLine ? true : false,
+				autoGenerateMultipleRestMeasuresFromRestMeasures: state.setting.combineMultipleRest, // 是否自动合并休止小节
 			})
 			// osmd.EngravingRules.CompactMode = true // 紧凑模式
 			// osmd.EngravingRules.PageRightMargin = state.isSingleLine ? (window.innerWidth+200)/10 : 2;
@@ -139,7 +140,7 @@ export default defineComponent({
 				// osmd.EngravingRules.PageTopMargin = state.isPreView ? 1 : 3;
 				osmd.EngravingRules.PageTopMargin = (state.isPreView && state.musicRenderType === EnumMusicRenderType.staff) ? 1 : state.isPreView ? 2 : 3;
 				osmd.EngravingRules.PageTopMarginNarrow = 3;
-				osmd.EngravingRules.PageLeftMargin = 3.6;
+				osmd.EngravingRules.PageLeftMargin = state.isCombineRender ? 8 : 3.6;
 				osmd.EngravingRules.PageRightMargin = 3;
 				osmd.EngravingRules.BreathMarkDistance = 0.1; // 呼吸标记距离音符的位置,百分比
 				osmd.EngravingRules.PageBottomMargin = state.isSingleLine ? 2 : 18;
@@ -169,10 +170,11 @@ export default defineComponent({
 				const canSelectTracks = state.combinePartIndexs.length > 1 ? state.combinePartIndexs.map(partIndex => { return state.partListNames[partIndex] }) : state.canSelectTracks
 				for (let i = 0; i < osmd.Sheet.Instruments.length; i++) {
 					const trackName = state.isEvxml && state.evxmlAddPartName ? osmd.Sheet.Instruments[i].idString || '' : osmd.Sheet.Instruments[i].Name || '';
-					osmd.Sheet.Instruments[i].Visible = canSelectTracks.includes(trackName)
+					osmd.Sheet.Instruments[i].Visible = canSelectTracks.includes(trackName.trim())
 				}
 			}
 			if (query.downPng === 'A4') {
+				osmd.EngravingRules.PageTopMargin = 5
 				osmd.setPageFormat('794x1100')
 				osmd.zoom = 0.3;
 			} else {
@@ -260,6 +262,7 @@ export default defineComponent({
 			>
 				{slots.default?.()}
 				{props.showSelection && musicData.showSelection && !state.isEvaluatReport &&!state.isSimplePage && !state.isPreView && state.musicRendered && <Selection />}
+				{props.showSelection && musicData.showSelection && state.isCombineRender &&!state.isSimplePage && !state.isPreView && state.musicRendered && <CombineAudio></CombineAudio> }
 			</div>
 		);
 	},

BIN
src/view/plugins/toggleMusicSheet/choosePartName/imgs/resetBtn.png


+ 11 - 7
src/view/plugins/toggleMusicSheet/choosePartName/index.module.less

@@ -1,5 +1,6 @@
 .container {
-  width: 334px;
+  width: 510px;
+  height: 320px;
   .head{
       height: 42px;
       position: relative;
@@ -22,7 +23,7 @@
   }
   .pickerCon{
     margin-top: -26px;
-    height: 290px;
+    height: 306px;
     background: #FFFFFF;
     border-radius: 16px;
     padding: 36px 20px 12px 20px;
@@ -40,7 +41,7 @@
       .titCon{
         display: flex;
         align-items: center;
-        padding: 10px 0;
+        padding-top: 10px;
         &.stickyTit{
           position: sticky;
           top: -1px;
@@ -61,8 +62,12 @@
         }
       }
       .content{
+        display: flex;
+        flex-wrap: wrap;
+        .specialBtn {
+          width: 96px;
+        }
         .selBtn{
-          width: 100%;
           height: 34px;
           line-height: 34px;
           background: #F6F6F6;
@@ -74,9 +79,8 @@
           cursor: pointer;
           border:1px solid transparent;
           margin-top: 10px;
-          &:first-child{
-            margin-top: 0;
-          }
+          margin-right: 10px;
+          padding: 0 8px;
           &.active{
             background: #F2FFFC;
             border-color: #01C1B5;

+ 19 - 7
src/view/plugins/toggleMusicSheet/choosePartName/index.tsx

@@ -6,7 +6,8 @@ import changeName from "./imgs/changeName.png"
 import { headImg } from "/src/page-instrument/header-top/image";
 import { toggleMusicSheet } from "../index"
 import okBtn from "./imgs/okBtn.png"
-import cancelBtn from "./imgs/cancelBtn.png"
+import resetBtn from "./imgs/resetBtn.png"
+import { getQuery } from "/src/utils/queryString";
 
 export default defineComponent({
   name: 'choosePartName',
@@ -22,6 +23,7 @@ export default defineComponent({
   },
   emits: ['close'],
   setup(props, { emit }) {
+    const query: any = getQuery();
     const selValues = ref([...props.partIndexs]);
     watch(
       () => toggleMusicSheet.show,
@@ -76,11 +78,11 @@ export default defineComponent({
             {
               state.isScoreRender &&
                 <>
-                  <div class={styles.titCon}>
+                  {/* <div class={styles.titCon}>
                     <div class={styles.tit}>选择总谱</div>
-                  </div>
+                  </div> */}
                   <div class={styles.content}>
-                    <div class={[styles.selBtn, selValues.value.includes(999) && styles.active]} onClick={()=>{ hanldeSelSheet(999, true) }}>总谱</div>
+                  <div class={[styles.selBtn, styles.specialBtn, selValues.value.includes(999) && styles.active]} onClick={()=>{ hanldeSelSheet(999, true) }}>总谱</div>
                   </div>
                 </>
             }
@@ -100,12 +102,22 @@ export default defineComponent({
             </div>
           </div>
           <div class={styles.btnCon}>
-              <img src={ cancelBtn } class={styles.btn} onClick={async () => {
-                  emit('close')
+              <img src={ resetBtn } class={styles.btn} onClick={async () => {
+                  selValues.value = []
                 }
               }></img>
               <img src={ okBtn } class={styles.btn} onClick={async () => {
-                  await checkMoveNoSave();
+                  if (!selValues.value.length) {
+                    showToast({
+                      position: "top",
+                      message: "最少需要选择一个声部"
+                    });
+                    return
+                  }
+                  if (query.isMove) {
+                    await checkMoveNoSave();
+                  }
+                  toggleMusicSheet.show = false
                   nextTick(()=>{
                     emit('close', selValues.value)
                   })

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

@@ -417,6 +417,14 @@ export default defineComponent({
 												className: "selectionToast",
 											});
 										}
+										// IOS18.1.1浏览器渲染更新有问题,需要手动更新一下
+										const selectionDom = document.getElementById('selectionBox')
+										if (selectionDom) {
+											selectionDom.style.display = 'none';
+											requestAnimationFrame(() => {
+												selectionDom.style.display = 'block';
+											})
+										}										
 									}}></div>
 								</div>
 							)