|
@@ -374,8 +374,10 @@ const state = reactive({
|
|
|
},
|
|
|
/** 后台设置的基准评测频率 */
|
|
|
baseFrequency: 440,
|
|
|
- /** mp3节拍器的时间,统计拍数、速度计算得出 */
|
|
|
+ /** mp3节拍器的时间,统计拍数、速度计算得出,evxml通过读取xml元素获取 */
|
|
|
fixtime: 0,
|
|
|
+ /** evxml等待播放的时间 */
|
|
|
+ evXmlBeginTime: 0,
|
|
|
/** 指法信息 */
|
|
|
fingeringInfo: {} as IFingering,
|
|
|
/** 滚动容器的ID */
|
|
@@ -442,6 +444,31 @@ const state = reactive({
|
|
|
midiSectionStart: 0,
|
|
|
/** 音频文件是否加载完成 */
|
|
|
audioDone: false,
|
|
|
+ /** 谱面svgdom节点 */
|
|
|
+ osmdSvgDom: null as any,
|
|
|
+ /** 滚动容器dom */
|
|
|
+ osdmScrollDom: null as any,
|
|
|
+ /** 光标dom */
|
|
|
+ cursorDom: null as any,
|
|
|
+ fistNoteLeft: 0,
|
|
|
+ /** 是否为单行谱渲染模式 */
|
|
|
+ isSingleLine: false,
|
|
|
+ /** 首尾音符的间距 */
|
|
|
+ noteDistance: 0,
|
|
|
+ /** 一行谱运动模式,平滑移动、匀速移动 */
|
|
|
+ moveType: "smooth" as "smooth" | "uniform",
|
|
|
+ /** 是否是evxml */
|
|
|
+ isEvxml: false,
|
|
|
+ noTimes: [] as any,
|
|
|
+ attendHideMenu: true,
|
|
|
+ /** 老师端:功能按钮布局方向 */
|
|
|
+ playBtnDirection: "left" as "left" | "right",
|
|
|
+ /** 云教练按钮方向,如果有指法并且是竖向的指法,为了防止播放按钮把指法挡住,此时云教练播放按钮方向应该取反 */
|
|
|
+ musicScoreBtnDirection: "right" as "left" | "right",
|
|
|
+ /** 是否在老师端上课页面 */
|
|
|
+ isAttendClass: false,
|
|
|
+ /** 引导页信息 */
|
|
|
+ guideInfo: null as any,
|
|
|
});
|
|
|
const browserInfo = browser();
|
|
|
let offset_duration = 0;
|
|
@@ -521,7 +548,7 @@ const handlePlaying = () => {
|
|
|
const duration = getAudioDuration();
|
|
|
state.playProgress = (currentTime / duration) * 100;
|
|
|
let item = getNote(currentTime);
|
|
|
- // console.log(11111,currentTime,duration,state.playSource, item.i)
|
|
|
+ // console.log(11111,currentTime,duration,state.playSource, item)
|
|
|
// console.log(item.i,item.noteId,item.measureSpeed)
|
|
|
// 练习模式下,实时刷新小节速度
|
|
|
if (item && state.modeType === "practise" && state.playState === "play" && item.measureSpeed && item.measureSpeed !== state.playIngSpeed) {
|
|
@@ -588,6 +615,15 @@ const handlePlaying = () => {
|
|
|
// metronomeData.metro?.sound(currentTime);
|
|
|
// }
|
|
|
metronomeData.metro?.sound(currentTime);
|
|
|
+ // 一行谱,需要滚动小节
|
|
|
+ if (state.isSingleLine) {
|
|
|
+ if (state.moveType === 'smooth') {
|
|
|
+ smoothMoveSvgDom();
|
|
|
+ } else {
|
|
|
+ uniformMoveSvgDom();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
};
|
|
|
/** 跳转到指定音符开始播放 */
|
|
|
export const skipNotePlay = async (itemIndex: number, isStart = false) => {
|
|
@@ -598,7 +634,8 @@ export const skipNotePlay = async (itemIndex: number, isStart = false) => {
|
|
|
}
|
|
|
if (item) {
|
|
|
setAudioCurrentTime(itemTime, itemIndex);
|
|
|
- gotoNext(item);
|
|
|
+ // 一行谱,点击音符,或者播放完成,需要跳转音符位置
|
|
|
+ gotoNext(item, true);
|
|
|
metronomeData.metro?.sound(itemTime);
|
|
|
if (state.isAppPlay) {
|
|
|
await api_cloudSetCurrentTime({
|
|
@@ -615,10 +652,10 @@ export const skipNotePlay = async (itemIndex: number, isStart = false) => {
|
|
|
* 切换曲谱播放状态
|
|
|
* @param playState 可选: 默认 undefined, 需要切换的状态 play:播放, paused: 暂停
|
|
|
*/
|
|
|
-export const togglePlay = async (playState?: "play" | "paused") => {
|
|
|
+export const togglePlay = async (playState?: "play" | "paused", sourceType?: string) => {
|
|
|
// 如果mp3资源还在加载中,给出提示
|
|
|
if (!state.isAppPlay && !state.audioDone) {
|
|
|
- showToast('音频资源加载中,请稍后')
|
|
|
+ if (sourceType !== 'courseware') showToast('音频资源加载中,请稍后')
|
|
|
return
|
|
|
}
|
|
|
// midi播放
|
|
@@ -731,8 +768,11 @@ const setCursorPosition = (note: any, cursor: any) => {
|
|
|
});
|
|
|
}
|
|
|
};
|
|
|
-/** 跳转到下一个音符 */
|
|
|
-export const gotoNext = (note: any) => {
|
|
|
+/**
|
|
|
+ * 跳转到下一个音符
|
|
|
+ * 一行谱,点击音符,或者播放完成,需要跳转音符位置,增加参数skipNote
|
|
|
+ **/
|
|
|
+export const gotoNext = (note: any, skipNote?: boolean) => {
|
|
|
// console.log(33333333333,state.activeNoteIndex,note.i)
|
|
|
const num = note.i;
|
|
|
|
|
@@ -744,7 +784,6 @@ export const gotoNext = (note: any) => {
|
|
|
}
|
|
|
return;
|
|
|
}
|
|
|
-
|
|
|
const osmd = state.osmd;
|
|
|
let prev = state.activeNoteIndex;
|
|
|
state.activeNoteIndex = num;
|
|
@@ -765,7 +804,10 @@ export const gotoNext = (note: any) => {
|
|
|
} catch (error) {
|
|
|
console.log(error);
|
|
|
}
|
|
|
-
|
|
|
+ // 一行谱,需要滚动小节
|
|
|
+ if (state.isSingleLine) {
|
|
|
+ moveSvgDom(skipNote);
|
|
|
+ }
|
|
|
scrollViewNote();
|
|
|
};
|
|
|
/** 获取指定音符 */
|
|
@@ -1067,6 +1109,7 @@ const setState = (data: any, index: number) => {
|
|
|
}
|
|
|
state.gradualTimes = state.extConfigJson.gradualTimes;
|
|
|
state.repeatedBeats = state.extConfigJson.repeatedBeats || 0;
|
|
|
+ state.isEvxml = state.extConfigJson.isEvxml == 1 ? true : false;
|
|
|
// 曲子包含节拍器,就不开启节拍器
|
|
|
state.needTick = data.isUseSystemBeat && data.isPlayBeat ? true : false;
|
|
|
// state.isOpenMetronome = data.isUseSystemBeat ? false : true;
|
|
@@ -1081,7 +1124,8 @@ const setState = (data: any, index: number) => {
|
|
|
state.musicSheetCategoriesId = data.musicCategoryId;
|
|
|
state.bizMusicCategoryId = data.bizMusicCategoryId
|
|
|
state.playMode = data.playMode === "MP3" ? "MP3" : "MIDI";
|
|
|
- state.originSpeed = state.speed = data.playSpeed;
|
|
|
+ state.originSpeed = state.speed = parseFloat(data.playSpeed) || 0;
|
|
|
+ // state.originSpeed = state.speed = data.playSpeed;
|
|
|
// state.playIngSpeed = data.playSpeed;
|
|
|
const track = data.code || data.track;
|
|
|
state.track = track ? track.replace(/ /g, "").toLocaleLowerCase() : "";
|
|
@@ -1140,8 +1184,8 @@ const setState = (data: any, index: number) => {
|
|
|
state.platform = query.platform?.toLocaleUpperCase() || "";
|
|
|
if (state.platform === IPlatform.PC) {
|
|
|
state.zoom = query.zoom || 1.5;
|
|
|
+ state.enableEvaluation = false;
|
|
|
}
|
|
|
-
|
|
|
/**
|
|
|
* 默认渲染什么谱面类型 & 能否转谱逻辑
|
|
|
* 渲染类型:首先取url参数musicRenderType,没有该参数则取musicalInstruments字段匹配的当前分轨的defaultScore,没有匹配到则取默认值('firstTone')
|
|
@@ -1205,4 +1249,196 @@ export const followBeatPaly = () => {
|
|
|
followBeatPaly();
|
|
|
}
|
|
|
});
|
|
|
-};
|
|
|
+};
|
|
|
+
|
|
|
+// 音符添加bbox
|
|
|
+export const addNoteBBox = (list: any[]) => {
|
|
|
+ let voicesBBox: any = null;
|
|
|
+ for (let i = 0; i < list.length; i++) {
|
|
|
+ const note = list[i];
|
|
|
+ const { svgElement, multipleRestMeasures, totalMultipleRestMeasures, stave } = note;
|
|
|
+ /**
|
|
|
+ * 兼容合并休止小节没有音符的情况,将合并的小节宽度等分,音符位置就在等分的位置
|
|
|
+ */
|
|
|
+ let bbox: any = null;
|
|
|
+ if (svgElement?.attrs.id) {
|
|
|
+ // @ts-ignore
|
|
|
+ bbox = document.getElementById(`vf-${svgElement?.attrs?.id}`)?.getBBox();
|
|
|
+ bbox = {
|
|
|
+ x: bbox?.x * state.zoom,
|
|
|
+ y: bbox?.y * state.zoom,
|
|
|
+ width: bbox?.width * state.zoom,
|
|
|
+ height: bbox?.height * state.zoom,
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // @ts-ignore
|
|
|
+ let currentVoicesBBox: any = document.getElementById(`${stave?.attrs?.id}`)?.nextSibling?.getBBox();
|
|
|
+ const svgBodyBBox: any = document.getElementById('musicAndSelection')?.getBoundingClientRect();
|
|
|
+ if (!currentVoicesBBox && multipleRestMeasures <= totalMultipleRestMeasures) {
|
|
|
+ currentVoicesBBox = voicesBBox;
|
|
|
+ }
|
|
|
+ let nextIndex = i + 1;
|
|
|
+ while (!list[nextIndex]?.id && nextIndex < list.length) {
|
|
|
+ nextIndex += 1;
|
|
|
+ }
|
|
|
+ // 休止小节的开头和下一个音符之间的间距
|
|
|
+ let multipleWidth: any = currentVoicesBBox?.width * state.zoom;
|
|
|
+ if (list[nextIndex]?.id) {
|
|
|
+ // @ts-ignore
|
|
|
+ multipleWidth = document.getElementById(`${list[nextIndex]?.stave?.attrs?.id}`)?.getBBox()?.x * state.zoom - currentVoicesBBox?.x * state.zoom;
|
|
|
+ }
|
|
|
+ const ratioWidth = multipleWidth / totalMultipleRestMeasures || 0;
|
|
|
+ bbox = currentVoicesBBox ? {
|
|
|
+ bottom: currentVoicesBBox.bottom,
|
|
|
+ height: 30,
|
|
|
+ left: currentVoicesBBox.x * state.zoom + ratioWidth * (multipleRestMeasures - 1),
|
|
|
+ right: currentVoicesBBox.y,
|
|
|
+ top: currentVoicesBBox.top,
|
|
|
+ width: 1,
|
|
|
+ x: currentVoicesBBox.x * state.zoom + ratioWidth * (multipleRestMeasures - 1),
|
|
|
+ y: currentVoicesBBox.y,
|
|
|
+ svgBodyLeft: svgBodyBBox?.x,
|
|
|
+ } : null;
|
|
|
+ voicesBBox = currentVoicesBBox;
|
|
|
+ }
|
|
|
+ note.bbox = bbox;
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+// 一行谱模式,创建固定的光标
|
|
|
+export const createFixedCursor = () => {
|
|
|
+ if (!state.isSingleLine) return;
|
|
|
+ const svg: any = document.getElementById("osmdSvgPage1");
|
|
|
+ state.osmdSvgDom = svg;
|
|
|
+ const scrollDom = document.getElementById("musicAndSelection");
|
|
|
+ const cursorDom = document.getElementById("cursorImg-0");
|
|
|
+ state.fistNoteLeft = cursorDom?.getBoundingClientRect()?.left || 0;
|
|
|
+ state.osdmScrollDom = scrollDom;
|
|
|
+ state.cursorDom = cursorDom;
|
|
|
+ let copyCursor: any = cursorDom?.cloneNode(true);
|
|
|
+ if (copyCursor) {
|
|
|
+ copyCursor.setAttribute('id','cursor-copy');
|
|
|
+ copyCursor.style.position = 'sticky';
|
|
|
+ copyCursor.style.zIndex = '2';
|
|
|
+ // if (!state.times[0]?.id) {
|
|
|
+ // copyCursor.style.left = state.times[0]?.bbox?.x + state.times[0]?.bbox?.width / 3 + 'px';
|
|
|
+ // }
|
|
|
+ copyCursor.style.left = state.times[0]?.bbox?.x + state.times[0]?.bbox?.width / 2 - 1 + 'px';
|
|
|
+ copyCursor.style.height = parseFloat(copyCursor.style.height) * 3 + 'px';
|
|
|
+ copyCursor.style.opacity = state.moveType === 'uniform' ? 0 : 1;
|
|
|
+ // copyCursor.style.background = 'red';
|
|
|
+ copyCursor && scrollDom?.appendChild(copyCursor);
|
|
|
+
|
|
|
+ // 创建左侧背景dom
|
|
|
+ // @ts-ignore
|
|
|
+ const firstMeasureBBox: any = document.querySelector('.vf-measure')?.getBBox();
|
|
|
+ const leftDom = document.createElement("div");
|
|
|
+ leftDom.style.width = state.times[0]?.bbox?.x - firstMeasureBBox?.x * state.zoom + 'px';
|
|
|
+ leftDom.style.height = firstMeasureBBox?.height * state.zoom + 'px';
|
|
|
+ leftDom.style.left = firstMeasureBBox?.x * state.zoom + 'px';
|
|
|
+ leftDom.style.transform = 'translateY(20px)';
|
|
|
+ leftDom.classList.add('leftNoteBg');
|
|
|
+ // scrollDom?.appendChild(leftDom);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 计算首尾音符的间距 */
|
|
|
+export const calculateDistance = () => {
|
|
|
+ const firstNoteBBox = state.times[0]?.bbox;
|
|
|
+ const lastNoteBBox = state.times.last()?.bbox;
|
|
|
+ if (firstNoteBBox && lastNoteBBox) {
|
|
|
+ const noteDistance = lastNoteBBox.x - firstNoteBBox.x + lastNoteBBox.width / 2 - firstNoteBBox.width / 2 - 1;
|
|
|
+ console.log('首尾间距', noteDistance)
|
|
|
+ state.noteDistance = noteDistance || 0;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 跳动svgdom */
|
|
|
+export const moveSvgDom = (skipNote?: boolean) => {
|
|
|
+ // const cursorLeft = state.cursorDom?.getBoundingClientRect()?.left || 0;
|
|
|
+ // const leftValue = parseFloat(state.cursorDom.style.left) - 47 - 20;
|
|
|
+ // console.log(cursorLeft,leftValue,'光标位置')
|
|
|
+ // state.osmdSvgDom.style.transform = `translateX(${-leftValue}px)`;
|
|
|
+ // state.osdmScrollDom.scrollLeft = leftValue
|
|
|
+ // console.log('当前音符',state.activeNoteIndex)
|
|
|
+ state.times.forEach((item: any, idx: number) => {
|
|
|
+ const svgEl = document.getElementById(`vf-${state.times[idx]?.svgElement?.attrs?.id}`)
|
|
|
+ const stemEl = document.getElementById(`vf-${state.times[idx]?.svgElement?.attrs?.id}-stem`)
|
|
|
+ if ((item.i === state.activeNoteIndex || item.id === state.times[state.activeNoteIndex].id) && item.svgElement) {
|
|
|
+ svgEl?.classList.add('noteActive')
|
|
|
+ stemEl?.classList.add('noteActive')
|
|
|
+ } else {
|
|
|
+ svgEl?.classList.remove('noteActive')
|
|
|
+ stemEl?.classList.remove('noteActive')
|
|
|
+ }
|
|
|
+ })
|
|
|
+ // document.getElementById('cursor-copy')?.classList.add('cursorAnimate');
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算需要移动的距离
|
|
|
+ * 当前选中的音符和第一个音符之间的间距
|
|
|
+ */
|
|
|
+ if (skipNote) {
|
|
|
+ const distance = state.times[state.activeNoteIndex].bbox?.x - state.times[0].bbox?.x + state.times[state.activeNoteIndex].bbox?.width / 2 - state.times[0].bbox?.width / 2;
|
|
|
+ state.osdmScrollDom.scrollTo({
|
|
|
+ left: distance,
|
|
|
+ behavior: "smooth",
|
|
|
+ });
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 平滑移动svgdom */
|
|
|
+export const smoothMoveSvgDom = () => {
|
|
|
+ const currentTime = getAudioCurrentTime();
|
|
|
+ const matchNoteIdx = state.times.findIndex((item: any) => Math.abs(item.time - currentTime) * 1000 < 100 )
|
|
|
+ // if (matchNoteIdx >= 0) {
|
|
|
+ // console.log('匹配',matchNoteIdx,currentTime)
|
|
|
+ // }
|
|
|
+
|
|
|
+ if (currentTime <= state.fixtime) return;
|
|
|
+ if (currentTime > state.times.last()?.time) return;
|
|
|
+ // console.log('跳转音符',currentTime)
|
|
|
+ const currentBBox = state.times[state.activeNoteIndex]?.bbox;
|
|
|
+ let nextIndex = state.activeNoteIndex + 1;
|
|
|
+ let nextBBox = state.times[nextIndex]?.bbox;
|
|
|
+ while (!nextBBox && nextIndex < state.times.length) {
|
|
|
+ nextIndex += 1;
|
|
|
+ nextBBox = state.times[nextIndex]?.bbox;
|
|
|
+ }
|
|
|
+ // 下一个音符和当前播放音符之间的间距
|
|
|
+ let noteDistance = nextBBox?.x - state.times[state.activeNoteIndex].bbox?.x + nextBBox?.width / 4 - state.times[state.activeNoteIndex].bbox?.width / 4 || 0
|
|
|
+ if (noteDistance) {
|
|
|
+ // 当前的音符和下一个音符之间的时值
|
|
|
+ const noteDuration = state.times[nextIndex].time - state.times[state.activeNoteIndex]?.time;
|
|
|
+ // 当前时值在该区间的占比
|
|
|
+ const playProgress = (currentTime - state.times[state.activeNoteIndex]?.time) / noteDuration;
|
|
|
+ // 如果当前播放的音符是休止小节的,实际没有音符
|
|
|
+ // if (!state.times[state.activeNoteIndex]?.id && state.times[state.activeNoteIndex]?.multipleRestMeasures && state.times[state.activeNoteIndex+1].id) {
|
|
|
+ // noteDistance = noteDistance - state.times[state.activeNoteIndex]?.bbox?.svgBodyLeft;
|
|
|
+ // }
|
|
|
+ const distance = noteDistance * playProgress;
|
|
|
+
|
|
|
+ // 上一个音符和第一个音符的间距
|
|
|
+ let preDistance = state.times[state.activeNoteIndex].bbox?.x - state.times[0].bbox?.x + state.times[state.activeNoteIndex].bbox?.width / 4;
|
|
|
+
|
|
|
+ // console.log(state.activeNoteIndex,'滑动', distance, preDistance, state.osdmScrollDom.scrollLeft, noteDistance, nextIndex, currentTime, noteDuration )
|
|
|
+ // console.log('当前音符',state.activeNoteIndex,'距离',noteDistance,'比例',playProgress,'上一个距离',preDistance,'时值',currentTime, noteDuration)
|
|
|
+ //console.log('滑动','音符:',state.activeNoteIndex,'小节:', state.activeMeasureIndex)
|
|
|
+ state.osdmScrollDom.scrollLeft = distance + preDistance;
|
|
|
+ } else {
|
|
|
+ const playProgress = (currentTime - state.times[0]?.time) / state.times.last()?.time
|
|
|
+ const distance = state.noteDistance * playProgress;
|
|
|
+ state.osdmScrollDom.scrollLeft = distance;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 匀速平移
|
|
|
+export const uniformMoveSvgDom = () => {
|
|
|
+ const currentTime = getAudioCurrentTime();
|
|
|
+ if (currentTime <= state.fixtime) return;
|
|
|
+ if (currentTime > state.times.last()?.time) return;
|
|
|
+ const playProgress = (currentTime - state.fixtime) / state.times.last()?.time;
|
|
|
+ const distance = playProgress * state.noteDistance || 0;
|
|
|
+ state.osdmScrollDom.scrollLeft = distance;
|
|
|
+}
|