import { closeToast, showToast, showConfirmDialog } from "vant"; import { nextTick, reactive, watch } from "vue"; import { OpenSheetMusicDisplay } from "../osmd-extended/src"; import { metronomeData } from "./helpers/metronome"; import { GradualNote, GradualTimes, GradualVersion } from "./type"; import { handleEndEvaluat, handleStartEvaluat } from "./view/evaluating"; import { IFingering, mappingVoicePart, subjectFingering, matchVoicePart } from "/src/view/fingering/fingering-config"; import { handleStartTick, closeTick } from "./view/tick"; import { audioListStart, getAudioCurrentTime, getAudioDuration, setAudioCurrentTime, setAudioPlaybackRate, audioData } from "./view/audio-list"; import { toggleFollow } from "./view/follow-practice"; import { browser, setStorageSpeed, setGlobalData } from "./utils"; import { api_cloudGetMediaStatus, api_createMusicPlayer, api_cloudChangeSpeed, api_cloudSuspend, api_cloudSetCurrentTime, api_cloudDestroy } from "./helpers/communication"; import { verifyCanRepeat, getDuration, xmlAddPartName } from "./helpers/formateMusic"; import { getMusicSheetDetail } from "./utils/baseApi" import { getQuery } from "/src/utils/queryString"; import { followData, skipNotePractice } from "/src/view/follow-practice/index" import { changeSongSourceByBeat } from "/src/view/audio-list" import { moveSmoothAnimation, smoothAnimationState, moveSmoothAnimationByPlayTime, moveTranslateXNum, destroySmoothAnimation, calcClientWidth } from "/src/page-instrument/view-detail/smoothAnimation" import { storeData } from "/src/store"; import { downloadXmlStr } from "./view/music-score" import { musicScoreRef, headerColumnHide } from "/src/page-instrument/view-detail/index" 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 { speedBeatTo, unitImgs } from "/src/helpers/beatConfig" const query: any = getQuery(); /** 入门 | 进阶 | 大师 */ export type IDifficulty = "BEGINNER" | "ADVANCED" | "PERFORMER"; /** 渲染类型: 五线谱,简谱 */ export enum EnumMusicRenderType { /** 五线谱 */ staff = "staff", /** 简谱(首调) */ firstTone = "firstTone", /** 固定音高 */ fixedTone = "fixedTone", } export const musicscoresettingKey = "musicscoresetting"; /** 有声音的是那个音源 */ export type IPlayState = "music" | "background" | "mingSong"; /** 播放状态 */ export type IAudioState = "play" | "paused"; /** 来源 */ export enum IPlatform { APP = "APP", PC = "PC", } export type ISonges = { background?: string music?: string } /** * 特殊教材分类id */ const classids = [1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 30, 31, 35, 36, 38, 108, 150, 151, 152, 153, 154, 155, 156, 157, 158, 178, 179, 180, 181, 182]; // 大雅金唐, 竖笛教程, 声部训练展开的分类ID // 乐器code码 export const musicalInstrumentCodeInfo = [ { name: '长笛', code: 'Flute', id: 1 }, { name: '短笛', code: 'Piccolo', id: 2 }, { name: '单簧管', code: 'Clarinet', id: 3 }, { name: '低音单簧管', code: 'Bass Clarinet', id: 4 }, { name: '中音萨克斯', code: 'Alto Saxophone', id: 5 }, { name: '次中音萨克斯', code: 'Tenor Saxophone', id: 6 }, { name: '高音萨克斯', code: 'Soprano Saxophone', id: 7 }, { name: '上低音萨克斯', code: 'Baritone Saxophone', id: 8 }, { name: '双簧管', code: 'Oboe', id: 9 }, { name: '大管', code: 'Bassoon', id: 10 }, { name: '小号', code: 'Trumpet', id: 11 }, { name: '圆号', code: 'Horn', id: 12 }, { name: '长号', code: 'Trombone', id: 13 }, { name: '上低音号', code: 'Baritone', id: 14 }, { name: '次中音号', code: 'Euphonium', id: 15 }, { name: '大号', code: 'Tuba', id: 16 }, { name: '钢琴', code: 'Piano', id: 17 }, { name: '电钢琴', code: 'Electronical Piano', id: 18 }, { name: '钢片琴', code: 'Glockenspiel', id: 19 }, { name: '小提琴', code: 'Violin', id: 20 }, { name: '中提琴', code: 'Viola', id: 21 }, { name: '大提琴', code: 'Violoncello', id: 22 }, { name: '低音提琴', code: 'Contrabass', id: 23 }, { name: '架子鼓', code: 'Drum Set', id: 24 }, { name: '小鼓', code: 'Snare Drum', id: 25 }, { name: '马林巴', code: 'Marimba', id: 26 }, { name: '颤音琴', code: 'Vibraphone', id: 27 }, { name: '钟琴', code: 'Chimes', id: 28 }, { name: '木琴', code: 'Xylophone', id: 29 }, { name: '管钟', code: 'Tubular Bells', id: 30 }, { name: '定音鼓', code: 'Timpani', id: 31 }, { name: '键盘', code: 'Mallets', id: 32 }, { name: '排箫', code: 'Panpipes', id: 33 }, { name: '陶笛', code: 'Ocarina', id: 34 }, { name: '陶笛', code: 'Alto Ocarina', id: 34 }, { name: '葫芦丝', code: 'Woodwind', id: 35 }, { name: '葫芦丝', code: 'Hulusi', id: 35 }, { name: '口风琴', code: 'Nai', id: 36 }, { name: '口风琴', code: 'Melodica', id: 36 }, { name: '德式竖笛', code: 'Tenor Recorder', id: 37 }, { name: '德式竖笛', code: 'German Recorder', id: 37 }, { name: '英式竖笛', code: 'Baroque Recorder', id: 38 }, { name: '高音陶笛', code: 'Whistling', id: 39 }, { name: '高音陶笛', code: 'Soprano Ocarina', id: 39 }, ] const state = reactive({ /** 来源 : PC , app */ platform: "" as IPlatform, appName: "" as "GYM" | "COLEXIU", musicRenderType: EnumMusicRenderType.staff as EnumMusicRenderType, /**曲谱是否渲染完成 */ musicRendered: false, /** 当前曲谱数据ID, 和曲谱ID不一致 */ detailId: "", /** 曲谱资源URL */ xmlUrl: "", /** 声部ID */ subjectId: 0 as number, trackId: 0 as string | number, isVip: false, // 是否会员 /** 分类ID */ categoriesId: 0, /** 分类名称 */ categoriesName: "", /** 是否支持评测 */ enableEvaluation: true, /** 是否支持转谱 */ enableNotation: false, /** 后台设置不能转谱,但是默认谱面不是五线谱时,需要显示转谱按钮,此时只能转首调和固定调 */ specialShowNotation: false, /** 曲谱ID */ examSongId: "", /** 内容平台的曲谱ID,可能会和业务端的id不一样 */ cbsExamSongId: "", /** 曲谱名称 */ examSongName: "", /** 曲谱封面 */ coverImg: "", /** 扩展字段 */ extConfigJson: {} as any, /** 扩展样式字段 */ extStyleConfigJson: {} as any, /** 简谱扩展样式字段 */ extJianStyleConfigJson: {} as any, /** 是否开启节拍器(mp3节拍器) */ isOpenMetronome: false, /** 演唱模式是否开启节拍器(mp3节拍器) */ isSingOpenMetronome: false, /** 是否显示指法 */ isShowFingering: false, /** 原音 */ music: "", /** 伴奏 */ accompany: "", /** 范唱 */ fanSong: "", /** 伴唱 */ banSong: "", /** 唱名 */ mingSong: "", /** 唱名女 如果有男唱名的情况下*/ mingSongGirl:"", /** 节拍器音乐资源 */ beatSong: { music: "", accompany: "", fanSong: "", banSong: "", mingSong: "", mingSongGirl: "" }, /** midiURL */ midiUrl: "", /** 父分ID */ parentCategoriesId: 0, /** 分类ID */ musicSheetCategoriesId: 0, /** 各产品端的分类ID,(管乐迷、管乐团、酷乐秀、课堂乐器) */ bizMusicCategoryId: 0, /** 资源类型: mp3 | midi */ playMode: "MP3" as "MP3" | "MIDI", /** 谱面的速度节拍 */ speedBeatUnit: "1/4", /** 设置的速度 */ speed: 0, /** 曲谱音频正常的速度 */ originSpeed: 0, /** 播放过程中显示的速度 */ playIngSpeed: 0, /** 分轨名称 */ track: "", /** 当前显示声部索引 */ partIndex: 0, /** 演奏是否需要节拍器 */ needTick: false, /** 演唱模式是否需要节拍器 */ needSingTick: false, /** 是否能使用节拍器 */ isMixBeat: true, /** 曲谱实例 */ osmd: null as unknown as OpenSheetMusicDisplay, /**是否是特殊乐谱类型, 主要针对管乐迷 */ isSpecialBookCategory: false, /** 播放状态 */ playState: "paused" as IAudioState, /** 播放结束状态 */ playEnd: false, /** 播放类型 演奏 演唱 */ playType: "play" as "play" | "sing", /** 播放那个: 原音,伴奏 */ playSource: "music" as IPlayState, /** 播放进度 */ playProgress: 0, /** 激活的note index */ activeNoteIndex: 0, /** 激活的小节 */ activeMeasureIndex: -1, /** 选段状态 */ sectionStatus: false, /** 选段数据 */ section: [] as any[], /** 选段背景 */ sectionBoundingBoxs: [] as any[], /** 开启选段预备 */ isOpenPrepare: false, /** 选段预备 */ sectionFirst: null as any, /** 音符数据 */ times: [] as any[], /** 播放模式 */ modeType: "practise" as "practise" | "follow" | "evaluating", /** 设置 */ setting: { /** 效音提醒 */ soundEffect: true, /** 护眼模式 */ eyeProtection: false, /** 摄像头 */ camera: false, /** 摄像头透明度 */ cameraOpacity: 70, /** 循环播放 */ repeatAutoPlay: true, /** 显示指法 */ displayFingering: true, /** 显示光标 */ displayCursor: true, /** 频率 */ frequency: 0, /** 评测难度 */ evaluationDifficulty: "BEGINNER" as IDifficulty, /** 保存到相册 */ saveToAlbum: true, /** 开启伴奏 */ enableAccompaniment: true, /** 反应时间 */ reactionTimeMs: 0, /** 节拍器音量 */ beatVolume: 50, }, /** 后台设置的基准评测频率 */ baseFrequency: 440, /** mp3节拍器的时间,统计拍数、速度计算得出,evxml通过读取xml元素获取 */ fixtime: 0, /** evxml等待播放的时间 */ evXmlBeginTime: 0, /** 第二遍循环evxml等待播放的时间 */ secondEvXmlBeginTime: 0, /** evxml等待播放的时间集合,多遍反复播放,会有多个timegap(前奏)时间 */ evXmlBeginArr: [] as any, /** evxml的曲子是否有times */ xmlHasTimes: false, /** evxml的曲子是否有timeGap */ xmlHasTimeGap: false, /** 有timeGap的曲子,是从哪个小节开始循环的,默认从第一小节开始循环 */ timegapRepeatMeasureIndex: 1, /** 指法信息 */ fingeringInfo: {} as IFingering, /** 滚动容器的ID */ scrollContainer: "musicAndSelection", /** 是否是打击乐 */ isPercussion: false, /** 评测标准 */ evaluationStandard: '', /** 是否重复节拍器的时间 */ repeatedBeats: 0, /**当前曲谱中所有声部名字 */ partListNames: [] as any, /** 渐变速度信息 */ gradual: [] as GradualNote[], /** 渐变速度版本 */ gradualVersion: GradualVersion.BASE as GradualVersion, /** 渐变时间信息 */ gradualTimes: null as GradualTimes, /** 单声部多声轨 */ multitrack: 0, /** 缩放 */ zoom: 0.8, /** 渲染曲谱比例 */ musicZoom: 1, /** 练习,评测是否是选段模式 */ isSelectMeasureMode: false, /** 是否是评分显示 */ isReport: false, /** 是否隐藏评测报告弹窗,保存演奏按钮,默认不隐藏 */ isHideEvaluatReportSaveBtn: false, /** 是否是合奏 */ isConcert: false, /** 用户选择的结束小节数 */ userChooseEndIndex: 0, /** 重播小节集合信息 */ repeatInfo: [] as any, /** 多分轨的曲子,可支持筛选的分轨 */ canSelectTracks: [] as any, /** 声部codeId */ subjectCodeId: 0 as number, /** 乐器codeId,用于匹配乐器指法、声部转调、特殊声部处理等 */ musicalCodeId: 0 as number, /** 乐器code,用于评测传参 */ musicalCode: '' as any, /** 合奏曲目是否合并展示 */ isCombineRender: false, /** 是否支持总谱 */ isScoreRender: false, /** 是否默认显示总谱 */ defaultScoreRender: false, /** 没有音源字段 */ noMusicSource: false, /** 小节的持续时长,以后台设置的播放速度计算 */ measureTime: 0, /** 跟练模式,节拍器播放的时间 */ beatStartTime: 0, /** 是否为详情预览模式 */ isPreView: false, /** 是否为内容平台预览模式 */ isCbsView: false, /** 是否为评测报告模式 */ isEvaluatReport: false, /** midi播放器是否初始化中 */ midiPlayIniting: false, /** 曲目信息 */ songs: {} as ISonges, isAppPlay: false, // 是否midi音频,midi是app播放 /** 音频播放器实例 */ audiosInstance: null as any, /** midi音频的时长 */ durationNum: 0, midiSectionStart: 0, /** 音频文件是否加载完成 */ audioDone: false, /** 是否为单行谱渲染模式 */ isSingleLine: false, /** 是否是evxml */ isEvxml: false, noTimes: [] as any, /** 老师端:功能按钮布局方向 */ playBtnDirection: "left" as "left" | "right", /** 云练习按钮方向,如果有指法并且是竖向的指法,为了防止播放按钮把指法挡住,此时云练习播放按钮方向应该取反 */ musicScoreBtnDirection: "right" as "left" | "right", /** 是否在老师端上课页面 */ isAttendClass: false, /** 引导页信息 */ guideInfo: null as any, noteCoords: [] as any, specialPosInit: false, /** 资源类型 */ paymentType: null, /** 播放模式,默认练习模式 */ defaultModeType: 1, /** 音符最多歌词次数 */ maxLyricNum: 0, /** 小节dom集合 */ vfmeasures: [] as SVGAElement[], /** 作曲家 */ musicComposer: '', /** 作词家 */ musicLyricist: '', // 加载条 isLoading: true, /** 加载中的文案 */ loadingText: '音频资源加载中,请稍后…', /** 是否是简单的单行谱模式页面 */ isSimplePage: false, /** xml的速度和后台设置的速度,计算出的基础音频播放倍率 */ originAudioPlayRate: 1, /** 开始播放时,记录的mp3播放倍率,用户当前设置的速度/当前小节的速度 */ basePlayRate: 1, /** 引导页显示状态 */ hasDriverPop: false, /** 播放倍率不等于1,或者是选段评测,APP暂时不支持保存演奏,需要给出提示 */ noSavePopShow: true, /** xml里面是否有歌词 */ xmlHasLyric: false, /** 生成图片的模式 */ isCreateImg: false, /** 切换谱面后,作业选段是否需要刷新 */ workSectionNeedReset: false, /** 旋律线开关 */ melodyLine: true, /** 是否是C调,切换到唱名时,只有C调所有的谱面类型都可以播放唱名文件;其它调的只有首调可以播放唱名,因为唱名是按照C调制作的,没有其它调的唱名文件 */ isCTone: false, evxmlAddPartName: false, // 妙极客的部分曲子没有part-name,需要自行添加的part-name /** 乐器id */ instrumentId: null, /** 作业是否达标 */ isWorkDone: false, /** xml的第一个measure标签的number */ firstMeasureNumber: 1, totalMeasureNumber: 0, // xml一轨总共的小节数 isFullEvaluatWork: false, // 是否是完整评测作业,完整评测作业可以保存作品 /** 是否是自动重播,练习模式开启自动重播时,播放前不需要再次计算播放倍率了,还是按照上次的播放倍率播放音频 */ isAutoRePlay: false, /** 右上角速度图标,根据当前小节的速度是几分音符的动态变化 */ speedIcon: 'speed3', // 默认取1/4拍的图片 isSingleMutliTrack: false, // 是否是单声轨多声部的声轨 }); const browserInfo = browser(); let offset_duration = 0; /** 自定义数据 */ export const customData = reactive({ /** 自定义音符时值 */ customNoteRealValue: [] as any, /** 自定义音符按读取到的时值 */ customNoteCurrentTime: false, }); /** 在渲染前后计算光标应该走到的音符 */ export const setStep = () => { // 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) { handlePlaying(); setStep(); } else { setTimeout(() => { handlePlaying(); setStep(); }, 16.7); } }); }; /** 开始播放 */ export const onPlay = () => { console.log("开始播放", '音频总时长:', getAudioDuration()); state.playEnd = false; // offset_duration = browserInfo.xiaomi ? 0.2 : 0.08; offset_duration = 0.2; setStep(); }; /** 播放模式结束自动重播 */ const autoResetPlay = () => { if (state.modeType !== "practise") return; skipNotePlay(0, true); // 没有开启自动重播, 不是练习模式 if (!state.setting.repeatAutoPlay) return; offsetTop = 0; scrollViewNote(); setTimeout(() => { // 自动播放,不需要再次计算播放倍率 state.isAutoRePlay = true; togglePlay("play"); }, 1000); }; /** 播放完成事件 */ export const onEnded = () => { console.log("音频播放结束"); // if (state.isAppPlay) { // // 销毁播放器 // api_cloudDestroy(); // } if (state.playEnd) { console.log('音频播放结束,无需再次执行') return } // 修改状态为结束 state.playEnd = true; state.playState = "paused"; // 结束播放 audioListStart(state.playState); // 调用结束评测 handleEndEvaluat(true); // 调用自动重复播放 autoResetPlay(); }; // 根据当前小节动态设置,右上角展示的速度 const dynamicShowPlaySpeed = (index: number) => { //if (!headerColumnHide.value) { const item: any = state.times[index]; if (item && item.measureSpeed ) { // console.log('速度1',item.measureSpeed) const newSpeed = state.basePlayRate * item.measureSpeed if (state.speed !== newSpeed) { state.speed = newSpeed; } } //} } // 开始播放时,计算mp3的播放倍率 export const initSetPlayRate = () => { // 自动播放,不需要再次计算播放倍率 if (state.isAutoRePlay) { state.isAutoRePlay = false return } // const item: any = (state.sectionStatus && state.section.length === 2) ? state.sectionFirst || state.section[0] : state.times[state.activeNoteIndex]; let item: any = state.times[state.activeNoteIndex]; console.log('播放状态',state.playState) if (item && item.measureSpeed) { const ratio = state.speed / item.measureSpeed // state.audiosInstance?.setSpeed(ratio) state.basePlayRate = ratio || 1; console.log('播放倍率',state.basePlayRate) } } // 重置播放倍率 export const resetBaseRate = (idx?: number) => { const index = idx ? idx : 0; const currentItem: any = state.times[index]; const currentSpeed = currentItem?.measureSpeed ? currentItem.measureSpeed : state.originSpeed; console.log('速度2',currentSpeed) state.speed = currentSpeed //state.activeNoteIndex = 0 state.basePlayRate = 1; } /** * 播放一直触发的事件 */ const handlePlaying = () => { const currentTime = getAudioCurrentTime(); const duration = getAudioDuration(); state.playProgress = (currentTime / duration) * 100; let item = getNote(currentTime); if (item) { // 选段状态下 if (state.sectionStatus && state.section.length === 2) { // 如果开启了预备拍 const selectStartItem = state.sectionFirst ? state.sectionFirst : state.section[0]; const selectEndItem = state.section[1]; /** * #9374,反复小节的曲目播放错误, bug修复 * 曲目:噢!苏珊娜-排箫-人音 * 现象:重播小节为2-9,选段为4-12,当播完第9小节后会回到第2小节重播2-8,再播放10-12 * 4-12,不符合重播规则,所以播完第9小节后,音频需要跳转到第10小节播放 */ if (state.repeatInfo.length) { const canRepeatInfo = verifyCanRepeat(state.section[0].MeasureNumberXML, state.section[1].MeasureNumberXML) const repeatIdx = canRepeatInfo.repeatIdx == -1 ? 0 : canRepeatInfo.repeatIdx if (state.modeType === "practise" && !canRepeatInfo.canRepeat && state.section[1].MeasureNumberXML > state.repeatInfo[repeatIdx].end) { const preItem = state.times[item.i - 1] if (preItem && preItem.MeasureNumberXML > item.MeasureNumberXML) { const skipItem = state.times.find((item: any) => item.MeasureNumberXML === preItem.MeasureNumberXML + 1) if (skipItem) { // 跳转到指定的音频位置 setAudioCurrentTime(skipItem.time, skipItem.i); gotoNext(skipItem); return } } } } // if (Math.abs(selectEndItem.endtime - currentTime) < offset_duration) { // if (currentTime - selectEndItem.endtime > offset_duration) { //console.log(currentTime,selectEndItem.endtime) if (currentTime - selectEndItem.endtime >= 0) { console.log("选段播放结束", state.setting.repeatAutoPlay); // 如果为选段评测模式 if (state.modeType === "evaluating" && state.isSelectMeasureMode) { onEnded(); return; } // #8698 bug修复 if (state.modeType === "practise" && state.sectionStatus) { onEnded(); // state.activeNoteIndex = state.sectionFirst ? state.sectionFirst.i : state.section[0].i // dynamicShowPlaySpeed(state.activeNoteIndex) resetPlaybackToStart(); return; } item = selectStartItem; setAudioCurrentTime(selectStartItem.time, selectStartItem.i); } } gotoNext(item); dynamicShowPlaySpeed(item.i); } // 评测不播放叮咚节拍器 // if (state.modeType !== "evaluating") { // metronomeData.metro?.sound(currentTime); // } // 不需要播放节拍器的声音,因为音频带有节拍器的声音 // metronomeData.metro?.sound(currentTime); // 一行谱,需要滚动小节 if (state.isSingleLine) { moveSmoothAnimationByPlayTime() } }; /** 跳转到指定音符开始播放 */ export const skipNotePlay = async (itemIndex: number, isStart = false) => { if (state.isPreView) return; console.log('点击音符') // 点击或者重播的时候清除一行谱的时间信息 state.isSingleLine && (smoothAnimationState.oldCurrentTime = 0) const item = state.times[itemIndex]; let itemTime = item.time; if (isStart) { itemTime = 0; } if (item) { // 非选段模式,点击音符,动态设置右下角的速度 if (item.measureSpeed && state.section.length < 2) { state.speed = state.basePlayRate * 10000 * item.measureSpeed / 10000 } setAudioCurrentTime(itemTime, itemIndex); // 一行谱,点击音符,或者播放完成,需要跳转音符位置 gotoNext(item, true); // 不需要播放节拍器的声音,因为音频带有节拍器的声音 // metronomeData.metro?.sound(itemTime); if (state.isAppPlay) { await api_cloudSetCurrentTime({ currentTime: itemTime * 1000, songID: state.examSongId, }) audioData.progress = itemTime state.midiSectionStart = itemTime } // 如果是跟练模式 if (followData.start) { skipNotePractice() } } }; /** * 切换曲谱播放状态 * @param playState 需要切换的状态 play:播放, paused: 暂停 */ export const togglePlay = async (playState: "play" | "paused", isForceCLoseToast?:boolean) => { // 如果mp3资源还在加载中,给出提示 if (!state.isAppPlay && !state.audioDone) { if (!isForceCLoseToast) showToast('音频资源加载中,请稍后') return } // 播放之前 当为评测模式和不为MIDI时候按 是否禁用节拍器 切换音源 if (playState === 'play' && state.modeType === "practise" && state.playMode !== "MIDI") { console.log("设置音源") changeSongSourceByBeat(metronomeData.disable) } if (playState === 'play') { offsetTop = 0; scrollViewNote(); } // midi播放 if (state.isAppPlay) { if (playState === "paused") { await api_cloudSuspend({ songID: state.examSongId, }) state.playState = 'paused' // 当在节拍器播放期间暂停的话 就暂停节拍器 closeTick() return } skipNotePlay(state.activeNoteIndex, false); await api_cloudChangeSpeed({ speed: state.modeType === "evaluating" ? state.originSpeed : state.speed, originalSpeed: state.originSpeed, songID: state.examSongId, }); const cloudGetMediaStatus = await api_cloudGetMediaStatus(); const status = cloudGetMediaStatus?.content.status === "suspend" ? "play" : "paused" state.playState = status } else { state.playState = playState; } if (state.playState === "play" && state.sectionStatus && state.section.length == 2 && state.playProgress === 0) { resetPlaybackToStart(); } // 当在节拍器播放期间暂停的话 就暂停节拍器 if (state.playState === "paused") { closeTick() } // 设置为开始播放时, 如果需要节拍,先播放节拍器 只有在当前播放时间不为0的时候开启节拍器 const isOneMeasureNumberXML = state.section.length === 2 && state.section[0].MeasureNumberXML === 2 //当是选段模式 并且开始小节是第二小节 就不播节拍器(这种情况有预选小节,currentTime是0) if (state.playState === "play" && getAudioCurrentTime() === 0 && !isOneMeasureNumberXML && ((state.playType === "play" && state.needTick) || (state.playType === "sing" && state.needSingTick))) { // 如果是系统节拍器 等系统节拍器播完了再播,如果是mp3节拍器 直接播 if ((state.playType === "play" && !state.isOpenMetronome) || (state.playType === "sing" && !state.isSingOpenMetronome)) { const tickend = await handleStartTick(); // console.log("🚀 ~ tickend:", tickend) // 节拍器返回false, 取消播放 if (!tickend) { state.playState = "paused"; return false; } } else { handleStartTick() } } // 如果选段没有结束, 直接开始播放,清空选段状态 if (state.playState == "play") { if (state.sectionStatus && state.section.length < 2) { clearSelection(); } } initSetPlayRate(); audioListStart(state.playState); return true; }; /** 结束播放 */ export const handleStopPlay = () => { state.playState = "paused"; audioListStart(state.playState); }; /** 重置播放为开始 */ export const resetPlaybackToStart = () => { // 如果为选段状态 if (state.sectionStatus && state.section.length === 2) { state.section = formateSelectMearure(state.section); return; } else { // 非选段状态,重播需要重置当前选中的小节为第一个小节 metronomeData.activeMetro = metronomeData.metroMeasure[0]?.[0] || {}; } skipNotePlay(0, true); }; /** 跳转到指定音符 */ export const gotoCustomNote = (index: number) => { try { state.osmd.cursor.reset(); } catch (error) { } for (let i = 0; i < index; i++) { state.osmd.cursor.next(); } }; // 找出离目标元素最近的音符 const computedDistance = (x: number, y: number) => { let minDistance = -1, minidx = 0; let a, b, c; state.noteCoords.forEach((note: any, idx: any) => { //a,b为直角三角形的两个直角边 a = Math.abs(note.x - x) b = Math.abs(note.y - y) //c为直角三角形的斜边 c = Math.sqrt(a * a + b * b) as 0 c = Number(c.toFixed(0)) as 0 if (c !== 0 && (minDistance === - 1 || c < minDistance)) { //min为元素中离目标元素最近元素的距离 minDistance = c minidx = idx } }) return minidx }; const customNotePosition = (note: any, cursor: any) => { const specialIds = ['1788850864767643649', '1788502467554750466', '1788501975122489346']; if (specialIds.includes(state.cbsExamSongId) && note.multipleRestMeasures === 0) { const pageLeft = document.getElementById('scrollContainer')?.getBoundingClientRect()?.x || 0; // 元素的位置 const element = document.getElementById('cursorImg-0')?.getBoundingClientRect?.() || { x: 0, y: 0 }; // 找出距离元素最近的音符 if (element.x && element.y) { const noteIdx = computedDistance(element.x, element.y); const targetX = state.noteCoords[noteIdx]?.x - pageLeft; console.log('音符索引', noteIdx) cursor.cursorElement.style.left = targetX + "px"; cursor.cursorElement.style.transform = `translateX(0px)`; } } } const setCursorPosition = (note: any, cursor: any, flag?: string) => { // console.log('音符',note?.i,state.osmd.Cursor.noteGraphicalId,note.svgElement?.attrs?.id) if (state.musicRenderType === EnumMusicRenderType.firstTone || state.musicRenderType === EnumMusicRenderType.fixedTone) { /** * bug:#9920、#9940 * 简谱选段模式,预备小节为休止小节时,选段播放结束,指针会重置到第一小节位置的初始位置 */ if (state.sectionStatus && state.playState === 'paused' && state.sectionFirst && (note.multipleRestMeasures || note.MeasureNumberXML !== state.sectionFirst?.MeasureNumberXML)) { return } const specialIds = ['1788850864767643649', '1788502467554750466', '1788501975122489346']; if (specialIds.includes(state.cbsExamSongId) && note.multipleRestMeasures === 0) { // console.log('音符idx',note?.i,cursor.cursorElement.style.left) const cursorLeft = cursor?.cursorElement?.style?.left ? parseFloat(cursor.cursorElement.style.left) : 0; let patchX = 0; if (state.cbsExamSongId == '1788502467554750466') { if (state.musicRenderType === EnumMusicRenderType.firstTone) { patchX = (note.i == 0 || note.i == 60) ? 21 : (note.i == 1 || note.i == 7 || note.i == 23 || note.i == 38 || note.i == 44 || note.i == 52 || note.i == 58) ? -6 : (note.i >= 2 || note.i <= 6) || (note.i >= 8 || note.i <= 22) || (note.i >= 24 || note.i <= 37) || (note.i >= 39 || note.i <= 43) || (note.i >= 45 || note.i <= 51) || (note.i >= 53 || note.i <= 57) || (note.i == 59) ? 6 : 0; } if (state.musicRenderType === EnumMusicRenderType.fixedTone) { patchX = note.i == 0 ? 31 : (note.i == 8 || note.i == 14 || note.i == 30 || note.i == 45 || note.i == 51 || note.i == 59 || note.i == 65) ? -10 : note.i == 67 ? 31 : 0; } } else if (state.cbsExamSongId == '1788501975122489346') { if (state.musicRenderType === EnumMusicRenderType.firstTone) { patchX = (note.i == 0) ? 21 : (note.i == 1 || note.i == 7 || note.i == 23 || note.i == 38 || note.i == 44 || note.i == 52 || note.i == 58) ? -6 : (note.i == 9 || note.i == 10 || note.i == 12 || note.i == 13) ? 3 : (note.i == 14 || note.i == 30 || note.i == 45 || note.i == 51 || note.i == 59) ? 6 : (note.i == 45) ? -8 : (note.i >= 15 || note.i <= 29) || (note.i >= 31 || note.i <= 36) || (note.i >= 38 || note.i <= 44) || (note.i >= 46 || note.i <= 50) || (note.i >= 52 || note.i <= 58) || (note.i >= 60 || note.i <= 64) || (note.i == 66) ? 4 : 0; } if (state.musicRenderType === EnumMusicRenderType.fixedTone) { patchX = note.i == 0 ? 31 : (note.i == 8 || note.i == 14 || note.i == 30 || note.i == 45 || note.i == 51 || note.i == 59 || note.i == 65) ? -10 : note.i == 67 ? 31 : 0; } } if (flag === 'refresh' || (flag === 'init' && !state.specialPosInit)) { // console.log('音符idx',note?.i,cursor.cursorElement.style.left) cursor.cursorElement.style.left = cursorLeft + patchX + "px"; state.specialPosInit = true; } } else { nextTick(() => { let bbox = note.bbox; if (!bbox) { const musicContainer = document.getElementById("musicAndSelection")?.getBoundingClientRect() || { x: 0, y: 0, }; const parentLeft = musicContainer.x || 0; const noteEle = document.querySelector(`#vf-${note.svgElement?.attrs?.id}`); if (noteEle) { const noteHead = noteEle.querySelector(".vf-numbered-note-head"); const noteHeadBbox = noteHead?.getBoundingClientRect?.(); if (noteHeadBbox) { note.bbox = { left: noteHeadBbox.x - parentLeft - noteHeadBbox.width / 4, width: noteHeadBbox.width * 1.5, }; bbox = note.bbox; } } } if (!bbox) return; const baseW = state.platform === IPlatform.PC ? 29 : 18; const width = (bbox.width - baseW) / 3; // console.log(555555,bbox.left,width) cursor.cursorElement.style.left = bbox.left + "px"; cursor.cursorElement.style.transform = `translateX(${width}px)`; }); } } }; /** * 跳转到下一个音符 * 一行谱,点击音符,或者播放完成,需要跳转音符位置,增加参数skipNote **/ export const gotoNext = (note: any, skipNote?: boolean) => { // console.log(33333333333,state.activeNoteIndex,note.i) const num = note.i; if (state.activeNoteIndex === note.i) { /* 没有光标了 这里就算加上光标也要性能优化 */ // try { // setCursorPosition(note, state.osmd.cursor, 'init'); // } catch (error) { // console.log(error); // } // 重置 或者切换演奏演唱的时候 可能出现 state.activeNoteIndex === note.i的情况 执行 if(state.playState === "paused"){ fillWordColor(); } if (state.isSingleLine && state.playState === "paused") { moveSvgDom(skipNote); } return; } const osmd = state.osmd; let prev = state.activeNoteIndex; state.activeNoteIndex = num; state.activeMeasureIndex = note.MeasureNumberXML; dynamicShowPlaySpeed(state.activeNoteIndex); if (prev && num - prev === 1) { // console.log('跳转音符',11111,osmd.cursor) // if (!note.id && note.multipleRestMeasures === 0) { // } else { // osmd.cursor.next(); // } osmd.cursor.next(); } else if (prev && num - prev > 0) { while (num - prev > 0) { prev++; // console.log('跳转音符',22222) osmd.cursor.next(); } } else { gotoCustomNote(num); } /* 取消光标了 */ // try { // setCursorPosition(note, state.osmd.cursor, 'refresh'); // } catch (error) { // console.log(error); // } fillWordColor(); // 一行谱,需要滚动小节 if (state.isSingleLine && state.playState === "paused") { moveSvgDom(skipNote); } scrollViewNote(); }; /** 获取指定音符 */ export const getNote = (currentTime: number) => { const times = state.times; const len = state.times.length; /** 播放超过了最后一个音符的时间,直接结束, 2秒误差 */ if (currentTime > times[len - 1].endtime + 2 && !state.isAppPlay && !state.isSimplePage) { // onEnded(); return; } let _item = null as any; for (let i = state.activeNoteIndex; i < len; i++) { let item = times[i]; const prevItem = times[i - 1]; // if (state.isEvxml) { // let diffArr: any[] = []; // times.forEach((note: any, noteIdx: number) => { // if (currentTime >= note.time && currentTime <= times[noteIdx+1].time) { // let diffTime = times[noteIdx+1].time - currentTime; // diffArr.push({ // diffTime, // idx: noteIdx // }) // } // }) // diffArr.sort((a, b) => a.diffTime - b.diffTime); // item = diffArr.length ? times[diffArr[0].idx] : item; // } if (currentTime >= item.time) { if (!prevItem || item.time != prevItem.time) { _item = item; } } else { break; } } // console.log("activeNoteIndex", currentTime, state.activeNoteIndex, _item.i); return _item; }; /** 重播 */ export const handleResetPlay = () => { // 如果是midi需要重置播放进度 if (state.isAppPlay) { audioData.progress = 0 } // 如果是作业模式,不还原速度 /** * #TODO:2024.09.14,业务需求变更,重播不还原用户设置的速度 */ // if (!query.workRecord) { // resetBaseRate(); // } resetPlaybackToStart(); // 如果是暂停, 直接播放 togglePlay("play"); }; /** 设置速度 */ export const handleSetSpeed = (speed: number) => { // setStorageSpeed(state.examSongId, speed); state.speed = speed; // 当前的音符 // 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; const actualRate = state.originAudioPlayRate * state.basePlayRate; console.log('速度设置',speed,'小节计算的倍率',state.basePlayRate,'实际播放倍率',actualRate) }; /** 清除选段状态 */ export const clearSelection = () => { state.sectionStatus = false; state.section = []; closeToast(); }; /** 开启选段 */ export const handleChangeSection = () => { // 如果开启了选段,再次点击取消选段 if (state.sectionStatus) { togglePlay("paused"); clearSelection(); // 重置速度和播放倍率 resetBaseRate(state.activeNoteIndex); //skipNotePlay(0, true); 取消选段的时候 不跳回开头 state.sectionFirst = null; return; } state.sectionStatus = true; // 开启 if (state.sectionStatus) { togglePlay("paused"); } showToast({ message: "请选择开始小节", duration: 0, position: "top", className: "selectionToast", }); }; /** 效验并格式化选段小节 */ const formateSelectMearure = (_list: any[]): any[] => { if (!_list.length) return []; const list = _list.sort((a, b) => a.time - b.time); const startXml = list[0]?.measureOpenIndex; const endXml = list.last()?.measureOpenIndex; const selectStartMeasure = state.times.filter((n: any) => startXml === n.measureOpenIndex) || []; const selectEndMeasure = state.times.filter((n: any) => endXml === n.measureOpenIndex) || []; // 没有找到选段小节 if (!selectStartMeasure.length || !selectEndMeasure.length) { clearSelection(); return []; } list[0] = selectStartMeasure[0]; list[1] = selectEndMeasure.last(); let startItemINdex = list[0].i; // 开启预备拍 if (state.isOpenPrepare) { const startXmlIndex = list[0].MeasureNumberXML; state.sectionFirst = state.times.find((n: any) => startXmlIndex - n.MeasureNumberXML === 1); startItemINdex = state.sectionFirst ? state.sectionFirst.i : startItemINdex; } skipNotePlay(startItemINdex, startItemINdex === 0); return list; }; /** 选择选段 */ export const handleSelection = (item: any) => { if (!state.sectionStatus || state.section.length > 1) return; if (state.section.length !== 2 && item) { state.section.push(item); if (state.section.length === 2) { setSection(state.section[0].MeasureNumberXML, state.section[1].MeasureNumberXML) //state.section = formateSelectMearure(state.section); closeToast(); } } if (state.section.length === 1) { showToast({ message: "请选择结束小节", duration: 0, position: "top", className: "selectionToast", }); } }; /** 阶段练习、阶段评测设置选段小节 */ export const setSection = (start: number, end: number, userSpeed?: number) => { const startNotes = state.times.filter( (n: any) => n.noteElement.sourceMeasure.MeasureNumberXML == start ) const endNotes = state.times.filter( (n: any) => n.noteElement.sourceMeasure.MeasureNumberXML == end ) state.userChooseEndIndex = end const lastEndId = endNotes[endNotes.length - 1].noteId let lastEndNotes = endNotes.filter((n: any) => n.noteId === lastEndId) // 是否符合重播规则 const canRepeatInfo = verifyCanRepeat(start, end) console.log('能否重播', canRepeatInfo) const isCanRepeat = canRepeatInfo.canRepeat // 如果符合重播规则,但是lastEndNotes长度为1,则需要向前找,直到找到lastEndNotes长度为2 /** * 如果符合重播规则,但是lastEndNotes长度为1,则需要向前找,直到找到lastEndNotes长度为2 * 2024.03.28 新增逻辑,如果当前选中的结束小节是跳房子,则不向前找,因为这种场景不符合重播规则(bug:#9921) * TODO:通过vf-volta判断是否是跳房子 */ let isSkipVolta = false; // 结束小节是否是跳房子小节 if (lastEndNotes.length === 1) { isSkipVolta = lastEndNotes[0]?.stave?.modifiers?.some((item: any) => item.getAttribute('type') === 'Volta') } let currentEndNum: number = end const lastEndIndex: any = state.repeatInfo[canRepeatInfo.repeatIdx]?.end || 0 while (isCanRepeat && lastEndNotes.length === 1 && lastEndNotes[0].MeasureNumberXML <= lastEndIndex && !isSkipVolta) { currentEndNum = currentEndNum - 1 const newEndNotes = state.times.filter( (n: any) => n.noteElement.sourceMeasure.MeasureNumberXML == currentEndNum ) const newLastEndId = newEndNotes[newEndNotes.length - 1].noteId lastEndNotes = newEndNotes.filter((n: any) => n.noteId === newLastEndId) } const endIdx: any = (isCanRepeat && canRepeatInfo.repeatIdx == state.repeatInfo.length - 1) ? lastEndNotes.length - 1 : 0 const startNote = startNotes[0] // const endNote = endNotes[endNotes.length - 1] const endNote = lastEndNotes[endIdx] if (startNote && endNote) { state.isSelectMeasureMode = true; // 设置小节 hanldeDirectSelection([startNote, endNote]); // 评测作业,练习作业的场景,需要用老师布置的速度,设置播放速度 if (userSpeed) { handleSetSpeed(userSpeed); } } } /** 直接设置选段 */ export const hanldeDirectSelection = (list: any[]) => { if (!Array.isArray(list) || list.length !== 2) return; state.sectionStatus = true; setTimeout(() => { state.section = formateSelectMearure(list); // 选段完成后,需要根据预报小节的速度,设置右下角显示的速度 const currentItem: any = (state.sectionStatus && state.section.length === 2) ? state.sectionFirst || state.section[0] : state.times[state.activeNoteIndex]; if (currentItem.measureSpeed && query.workRecord === undefined) { handleSetSpeed(currentItem.measureSpeed); } console.log('选段小节', state.section) }, 0); }; let offsetTop = 0; /** * 窗口内滚动到音符的区域 * @param isScroll 可选: 强制滚动到顶部, 默认: false * @returns void */ export const scrollViewNote = (resetTop?: boolean) => { // const cursorElement = document.getElementById("cursorImg-0")!; const noteId = state.times[state.activeNoteIndex].id; if (state.isSingleLine) { return; } if (state.activeNoteIndex <= 1 || resetTop) { offsetTop = 0; } const domId = "vf" + noteId; const cursorElement: any = noteId ? document.querySelector(`[data-vf=${domId}]`)?.parentElement : document.getElementById('restDot')?.parentElement; const musicAndSelection = document.getElementById(state.scrollContainer)!; // offsetTop = musicAndSelection.scrollTop || offsetTop; const noteCenterOffsetTop = cursorElement ? cursorElement?.offsetTop + (cursorElement?.offsetHeight/2) : 0; // console.log('滑动',offsetTop, noteCenterOffsetTop) if (!cursorElement || !noteCenterOffsetTop || !musicAndSelection || offsetTop === noteCenterOffsetTop || Math.abs(offsetTop - noteCenterOffsetTop) < 30) return; offsetTop = noteCenterOffsetTop; if (offsetTop > 100) { musicAndSelection.scrollTo({ top: (offsetTop - 100) * state.musicZoom, behavior: "smooth", }); } else { musicAndSelection.scrollTo({ top: 0, behavior: "smooth", }); } }; /** 检测是否是节奏练习 */ export const isRhythmicExercises = () => { return state.examSongName.indexOf("节奏练习") > -1; }; /** 重置状态 */ export const handleRessetState = () => { // 切换模式,清除选段 state.noSavePopShow = true; clearSelection(); skipNotePlay(0, true); resetBaseRate(); // midi 重置播放进度 if (state.isAppPlay) { audioData.progress = 0; } if (state.modeType === "evaluating") { handleStartEvaluat(); } else if (state.modeType === "practise") { togglePlay("paused", true); } else if (state.modeType === "follow") { toggleFollow(false); } }; export default state; /** 初始化评测音频 */ export const evaluatCreateMusicPlayer = () => { return api_createMusicPlayer({ musicSrc: state.accompany || state.music, // 曲谱音频url // tuneSrc: "https://oss.dayaedu.com/cloud-coach/1686725501654check_music1_(1).mp3", //效音音频url tuneSrc: "https://oss.dayaedu.com/MECMP/1722593665681.mp3", //效音音频url checkFrequence: 496, }); }; /** 获取内容平台的接口详情并初始化state信息 */ export const getMusicDetail = async (id: string, type?: string) => { const res = await getMusicSheetDetail(id, type); if (res?.code === 200) { await getMusicInfo(res) } }; const getMusicInfo = async (res: any) => { // 是否支持总谱 state.isScoreRender = res.data?.isScoreRender // 是否默认显示总谱 state.defaultScoreRender = res.data?.defaultScoreRender // 是否显示节拍器 state.isMixBeat = res.data?.isMixBeat let partIndex = query["part-index"] ? parseInt(query["part-index"]) : -1 // -1为partIndex没有值的时候 // 如果是评测报告,会有默认的分轨index if (state.isEvaluatReport) { partIndex = state.partIndex; } // 布置作业 取作业的乐器id const workRecord = query.workRecord let workRecordInstrumentId:undefined | string if(workRecord){ const res = await api_lessonTrainingTrainingStudentDetail(workRecord); if (res?.code === 200) { workRecordInstrumentId = res.data?.instrumentId } } // multiTracksSelection 返回为空,默认代表全部分轨 state.canSelectTracks = res.data.multiTracksSelection === "null" || res.data.multiTracksSelection === "" || res.data.multiTracksSelection === null ? [] : res.data.multiTracksSelection?.split(','); state.canSelectTracks = state.canSelectTracks.map((item: any)=>item.trim()) /* 获取声轨列表 */ let xmlString = await fetch(res.data.xmlFileUrl).then((response) => response.text()); xmlString = xmlAddPartName(xmlString); downloadXmlStr.value = xmlString //给musice-score 赋值xmlString 以免加载2次 const tracks = xmlToTracks(xmlString) //获取声轨列表 // 设置音源 track 为当前的声轨 index为当前的 const { track, index, musicalInstrumentId } = state.isSimplePage ? { track:tracks[0], index: state.partIndex, musicalInstrumentId: '' } : initMusicSource(res.data, tracks, partIndex, workRecordInstrumentId) // 这里返回的track可能和实际的对不上,所以重新筛选一下 const realTrack = musicalInstrumentId && res.data?.musicalInstruments?.length ? res.data?.musicalInstruments.find((item: any) => item?.id == musicalInstrumentId)?.code?.split(',')?.[0] : ''; const musicInfo = { ...res.data, track: res.data.musicSheetType === 'CONCERT' ? track : realTrack }; console.log("🚀 ~ musicInfo:", musicInfo); setState(musicInfo, index); }; //获取xml中的音轨数据 function xmlToTracks(xmlString: string) { const xmlParse = new DOMParser().parseFromString(xmlString, "text/xml"); const partNames = Array.from(xmlParse.getElementsByTagName('part-name')); return partNames.reduce((arr: string[], item) => { const textContent = item?.textContent?.trim() if (textContent?.trim()?.toLocaleLowerCase() !== "common" && textContent) { arr.push(textContent) } return arr }, []); } // 设置音源 function initMusicSource(data: any, tracks: string[], partIndex: number, workRecordInstrumentId?: string) { let track:string,index:number, musicalInstrumentId: string const instrumentId = workRecordInstrumentId || query.instrumentId || storeData.user?.instrumentId state.instrumentId = instrumentId; let { musicSheetType, isAllSubject, musicSheetSoundList, musicSheetAccompanimentList } = data musicSheetSoundList || (musicSheetSoundList = []) musicSheetAccompanimentList || (musicSheetAccompanimentList = []) let musicObj, accompanyObj, fanSongObj, banSongObj /* 独奏 */ if (musicSheetType === "SINGLE") { accompanyObj = musicSheetAccompanimentList.find((item: any) => { return item.audioPlayType === "PLAY" }) // 是否全声部(isAllSubject)为true 时候没有乐器只有一个原音(比如节奏练习,这个曲子全部乐器都支持);当前用户有乐器就匹配 不然取第一个原音 musicObj = musicSheetSoundList.find((item: any) => { return isAllSubject ? item.audioPlayType === "PLAY" : (item.audioPlayType === "PLAY" && item.musicalInstrumentId == instrumentId) }) // 当没有找到原音的时候,并且instrumentId没有值的时候,取默认第一个乐器 if(!musicObj && !instrumentId){ musicObj = musicSheetSoundList.find((item: any) => { return item.audioPlayType === "PLAY" }) } fanSongObj = musicSheetSoundList.find((item: any) => { return item.audioPlayType === "SING" }) banSongObj = musicSheetAccompanimentList.find((item: any) => { return item.audioPlayType === "SING" }) track = musicObj?.track //没有原音的时候track为空 不显示指法 index = tracks.findIndex(item => { return item === track }) musicalInstrumentId = musicObj?.musicalInstrumentId } else { /* 合奏 */ // 支持总谱 并且当前是总谱。partIndex是999时候,或者默认是总谱并且partIndex为-1时候 -1就是partIndex没有值 if(state.isScoreRender && (partIndex===999 || (state.defaultScoreRender && partIndex===-1))){ // 总谱渲染 state.isCombineRender = true banSongObj = musicSheetAccompanimentList.find((item: any) => { return item.audioPlayType === "SING" }) // 总谱演唱模式是 范唱,取banSongObj 里面的scoreAudioFileUrl字段 // 先取scoreAudioFileUrl的值 如果 没有就是空 if(banSongObj){ fanSongObj = { audioFileUrl: banSongObj.scoreAudioFileUrl, audioBeatMixUrl: banSongObj.scoreAudioBeatMixUrl } } // 总谱演奏模式是 伴奏 accompanyObj = musicSheetAccompanimentList.find((item: any) => { return item.audioPlayType === "PLAY" }) track = "总谱" index = 999 musicalInstrumentId = '' }else{ // 合奏只显示一个声轨 当为-1时候,取tracks中 后端勾选了的第一个值 track = partIndex === -1 ? tracks.find(value => state.canSelectTracks.includes(value))! : tracks[partIndex] // 根据当前的声轨 取数据 musicObj = musicSheetSoundList.find((item: any) => { return item.audioPlayType === "PLAY" && item.track === track }) fanSongObj = musicSheetSoundList.find((item: any) => { return item.audioPlayType === "SING" && item.track === track }) banSongObj = musicSheetAccompanimentList.find((item: any) => { return item.audioPlayType === "SING" }) accompanyObj = musicSheetAccompanimentList.find((item: any) => { return item.audioPlayType === "PLAY" }) index = tracks.findIndex(item => { return item === track }) musicalInstrumentId = musicObj?.musicalInstrumentId } state.partListNames = tracks } // 当没有任何曲目的时候报错 if (!musicObj?.audioFileUrl && !accompanyObj?.audioFileUrl && !fanSongObj?.audioFileUrl && !banSongObj?.audioFileUrl && !fanSongObj?.solmizationFileUrl && !fanSongObj?.femaleSolmizationFileUrl) { state.noMusicSource = true // 没有音源文件 // 独奏的时候 if(musicSheetType === "SINGLE"){ // 并且是midi没有midi文件的时候 if(data.playMode === "MIDI" && !data.midiFileUrl) { // 是预览的时候 不报错 if(!query.isPreView){ throw new Error("该曲目无任何音源"); } } } } Object.assign(state, { music: musicObj?.audioFileUrl, accompany: accompanyObj?.audioFileUrl, fanSong: fanSongObj?.audioFileUrl, banSong: banSongObj?.audioFileUrl, }) // 如果没有男唱名 if(!fanSongObj?.solmizationFileUrl){ state.mingSong = fanSongObj?.femaleSolmizationFileUrl }else{ state.mingSong = fanSongObj?.solmizationFileUrl state.mingSongGirl = fanSongObj?.femaleSolmizationFileUrl } // 当使用节拍器的时候才加载节拍器音频 if(state.isMixBeat) { Object.assign(state.beatSong, { music: musicObj?.audioBeatMixUrl, accompany: accompanyObj?.audioBeatMixUrl, fanSong: fanSongObj?.audioBeatMixUrl, banSong: banSongObj?.audioBeatMixUrl }) // 如果没有男唱名 if(!fanSongObj?.solmizationBeatUrl){ state.beatSong.mingSong = fanSongObj?.femaleSolmizationBeatUrl }else{ state.beatSong.mingSong = fanSongObj?.solmizationBeatUrl state.beatSong.mingSongGirl = fanSongObj?.femaleSolmizationBeatUrl } } return { index, track, musicalInstrumentId } } const setState = (data: any, index: number) => { // 获取当前模式 声部切换用 const localStoragePlayType = localStorage.getItem("musicScorePlayType") if(localStoragePlayType) { localStorage.removeItem("musicScorePlayType") const fields = localStoragePlayType.split(',') state.playType = fields[0] as any state.playSource = fields[1] as any } // 根据当前文件有没有 设置当前的播放模式 const playObj = { "play_music":"music", "play_background":"accompany", "play_mingSong":"mingSong", "sing_music":"fanSong", "sing_background":"banSong", "sing_mingSong":"mingSong", } as any // 当前缓存 有值的时候 用这个,没有的时候 走筛选 // @ts-ignore if(!state[playObj[`${state.playType}_${state.playSource}`]]){ if(state.playType === "play"){ if(state.music){ state.playSource = "music" }else if(state.accompany){ state.playSource = "background" }else if(state.mingSong){ state.playSource = "mingSong" }else{ if(state.fanSong){ state.playType = "sing" state.playSource = "music" }else if(state.banSong){ state.playType = "sing" state.playSource = "background" } } }else{ if(state.fanSong){ state.playSource = "music" }else if(state.banSong){ state.playSource = "background" }else if(state.mingSong){ state.playSource = "mingSong" }else{ if(state.music){ state.playType = "play" state.playSource = "music" }else if(state.accompany){ state.playType = "play" state.playSource = "background" } } } } state.appName = "COLEXIU"; state.detailId = data.bizId; state.xmlUrl = data.xmlFileUrl; state.paymentType = data.paymentType state.partIndex = index >= 0 ? index : 0; state.trackId = data.track; state.subjectId = data.subjectIds ? data.subjectIds.split(',')?.[0] : 0; // 声部code const subjectCode = data.subjectCodes ? data.subjectCodes.split(',')?.[0] : ''; // 乐器code // let musicalCode = data.musicalInstrumentIdCodes ? data.musicalInstrumentIdCodes.split(',')?.[0] : ''; /** * 单曲,指法根据用户当前的乐器来显示,如果没有则取musicSheetSoundList第一个track */ // const currentInstrumentId = query.instrumentId || storeData.user?.instrumentId; // let musicalCode = !currentInstrumentId ? data.musicSheetSoundList?.find((item:any)=>{ return item.audioPlayType === "PLAY" })?.track || '' : data.musicSheetSoundList?.find((item: any) => item?.musicalInstrumentId == currentInstrumentId && item.audioPlayType === "PLAY")?.track || ''; // const pitchSubject = musicalInstrumentCodeInfo.find((n) => n.code.toLocaleLowerCase() === subjectCode.toLocaleLowerCase()) // const pitchMusical = musicalInstrumentCodeInfo.find((n) => n.code.toLocaleLowerCase() === musicalCode.toLocaleLowerCase()) // state.subjectCodeId = pitchSubject ? pitchSubject.id : 0 // state.musicalCodeId = pitchMusical ? pitchMusical.id : 0 state.categoriesId = data.musicCategoryId; state.categoriesName = data.musicTagNames; // state.enableEvaluation = data.isEvaluated ? true : false; state.examSongId = data.bizId + ""; state.cbsExamSongId = data.id + ""; // 曲子:1795013331841662977,1.3.5.7跳房子带repeat,2.4.6.8跳房子带DC,该曲子需要特殊处理,需要传入自定义的跳转规则 if (state.examSongId == '1795013331841662977') { ; (window as any).DYCustomHopscotch = [1,2,1,2,1,2,1,2] } ; (window as any).DYExamSongId = state.examSongId state.examSongName = data.name; state.coverImg = data.musicCover ?? ""; // 如果是simple页面,只显示单轨 if (state.isSimplePage) { state.isCombineRender = false; } setCustom(state.isCombineRender ? data.musicSheetSoundList?.length : 0); // 解析扩展字段 if (data.extConfigJson) { try { state.extConfigJson = JSON.parse(data.extConfigJson as string); } catch (error) { console.error("解析扩展字段错误:", error); } } state.gradualTimes = state.extConfigJson.gradualTimes; state.repeatedBeats = state.extConfigJson.repeatedBeats || 0; state.isEvxml = state.extConfigJson.isEvxml == 1 ? true : false; // 是否开启节拍器 state.needTick = !!data.isPlayBeat state.needSingTick = !!data.isPlaySingBeat // state.isOpenMetronome = data.isUseSystemBeat ? false : true; // 演奏模式是否播mp3节拍器 state.isOpenMetronome = data.isPlayBeat && !data.isUseSystemBeat ? true : false // 演唱模式是否播mp3节拍器 state.isSingOpenMetronome = data.isPlaySingBeat && !data.isUseSingSystemBeat ? true : false state.isShowFingering = data.isShowFingering ? true : false; // 设置曲谱的播放模式, APP播放(midi音频是app播放) | h5播放 state.isAppPlay = data.playMode === 'MIDI'; state.midiUrl = data.midiFileUrl; state.parentCategoriesId = data.musicTag; state.musicSheetCategoriesId = data.musicCategoryId; state.bizMusicCategoryId = data.bizMusicCategoryId state.playMode = data.playMode === "MP3" ? "MP3" : "MIDI"; // 设置速度节拍 state.speedBeatUnit = data.speedBeatUnit || "1/4" // 这里把后台设置的速度 转换为1/4拍的速度 state.originSpeed = state.speed = speedBeatTo({unit: data.speedBeatUnit || "1/4",speed: parseFloat(data.playSpeed) || 0}, `1/4`); // state.originSpeed = state.speed = data.playSpeed; // state.playIngSpeed = data.playSpeed; const track = data.code || data.track; state.track = track ? track.replace(/ /g, "").toLocaleLowerCase() : ""; // 能否评测,根据当前声轨有无伴奏判断 if (state.isAppPlay) { state.enableEvaluation = state.midiUrl ? true : false } else { state.enableEvaluation = state.accompany || state.music ? true : false } state.isConcert = data.musicSheetType === "CONCERT" ? true : false; // 开启预备小节 state.isOpenPrepare = true; state.extStyleConfigJson = data.extStyleConfigJson || {} state.extJianStyleConfigJson = data.extJianStyleConfigJson || {} // console.log("🚀 ~ state.subjectId:", state.subjectId, state.track as any , state.subjectId) // 是否打击乐 /** * 是否打击乐:AMPLITUDE & 节奏练习:DECIBELS * evaluationStandard:("评测标准 节奏 AMPLITUDE 音准 FREQUENCY 分贝 DECIBELS") * 打击乐&节奏练习,没有跟练模式 */ // state.isPercussion = isRhythmicExercises(); state.isPercussion = data.evaluationStandard === "AMPLITUDE" || data.evaluationStandard === "DECIBELS"; state.evaluationStandard = data.evaluationStandard?.toLocaleLowerCase() || '' // 设置是否特殊曲谱, 是特殊曲谱取反(不理解之前的思考逻辑), 使用后台设置的速度 state.isSpecialBookCategory = !classids.includes(Number(data.musicCategoryId)); // 设置指法 // const code = state.isConcert ? mappingVoicePart(state.trackId, "ENSEMBLE") : mappingVoicePart(state.subjectId, "INSTRUMENT"); /** * 各平台的乐器声部id不统一,为了兼容处理老的数据,加上乐器code码,此码唯一 * 获取指法code */ // const code = state.isConcert ? matchVoicePart(state.trackId, "CONCERT") : matchVoicePart(state.musicalCodeId, "SINGLE"); const code = matchVoicePart(state.trackId, "CONCERT") state.fingeringInfo = subjectFingering(code); console.log("🚀 ~ state.fingeringInfo:", code, state.fingeringInfo, state.trackId, state.track); state.musicalCodeId = state.fingeringInfo?.id || 0 state.musicalCode = musicalInstrumentCodeInfo.find(item => item.id === state.musicalCodeId)?.code || state.trackId ; (window as any).DYSubjectId = state.musicalCodeId // 开启自定义每行显示的小节数 ; (window as any).customSectionAmount = true // 标识是课堂乐器,用于移调时进行区分处理 ; (window as any).DYProjectName = 'musicScore'; // 如果切换的声轨没有指法,择指法开关置灰并且不可点击 if (!state.fingeringInfo.name && state.setting.displayFingering) { state.setting.displayFingering = false } // 如果是PC端,放大曲谱 state.platform = query.platform?.toLocaleUpperCase() || ""; if (state.platform === IPlatform.PC) { // pc端,谱面默认初始大小设置为1.5 state.zoom = 1.5; if (query.zoom <= 1) { state.zoom = query.zoom ? Number(query.zoom) : state.zoom; } else { state.zoom = localStorage.getItem('scoreZoom') ? Number(localStorage.getItem('scoreZoom')) : state.zoom } state.enableEvaluation = false; } if (storeData.isApp) { state.zoom = localStorage.getItem('scoreZoom') ? Number(localStorage.getItem('scoreZoom')) : state.zoom } /** * 默认渲染什么谱面类型 & 能否转谱逻辑 * 渲染类型:首先取url参数musicRenderType,没有该参数则取musicalInstruments字段匹配的当前分轨的defaultScore,没有匹配到则取默认值('firstTone') * 能否转谱:先取isConvertibleScore字段,如果isConvertibleScore为true,则取musicalInstruments字段匹配的当前分轨的transferFlag,都为true则可以转谱 * */ // let pitchTrack = null // if (state.isConcert) { // musicalCode = musicalInstrumentCodeInfo.find((item: any) => item.id === state.musicalCodeId)?.code // pitchTrack = data.musicalInstruments?.find((item: any) => item.code?.split(',')[0] === musicalCode) // } else { // pitchTrack = data.musicalInstruments?.find((item: any) => item.code?.split(',')[0] === musicalCode) // } let musicalRenderType = '' // if (pitchTrack?.defaultScore) { // musicalRenderType = pitchTrack?.defaultScore === 'STAVE' ? 'staff' : pitchTrack?.defaultScore === 'JIAN' ? 'fixedTone' : pitchTrack?.defaultScore === 'FIRST' ? 'firstTone' : '' // } // state.musicRenderType = query.musicRenderType || musicalRenderType || EnumMusicRenderType.firstTone; /** * 2024.7.30,使用新的字段 * 谱面类型,scoreType * STAVE("五线谱"),JIAN("固定调"),FIRST("首调"), */ musicalRenderType = data.scoreType === 'STAVE' ? 'staff' : data.scoreType === 'JIAN' ? 'fixedTone' : data.scoreType === 'FIRST' ? '' : 'firstTone'; if (!state.isEvaluatReport) { state.musicRenderType = query.musicRenderType || musicalRenderType || EnumMusicRenderType.firstTone; } state.musicRenderType = query.musicRenderType || musicalRenderType || EnumMusicRenderType.firstTone; /** * TODO:摇篮曲特殊处理 */ if (['1788501975122489346', '1788502467554750466', '1789839575249596417'].includes(state.cbsExamSongId)) { if (state.musicRenderType === 'fixedTone') { state.musicRenderType = EnumMusicRenderType.firstTone; } } /** * 2024.7.30,使用新的字段 * 能否转谱,isConvertibleScore * true:能转谱,false:不能转谱 * 额外的逻辑:后台设置不能转谱时,如果默认谱面不是五线谱,需要显示转谱按钮,但是只能转首调和固定调 */ // state.enableNotation = pitchTrack ? data.isConvertibleScore && pitchTrack.transferFlag : data.isConvertibleScore state.enableNotation = data.isConvertibleScore state.specialShowNotation = !data.isConvertibleScore && data.scoreType !== 'STAVE'; console.log("state对象", state); // 评测基准频率 state.baseFrequency = data.evaluationFrequency ? data.evaluationFrequency.split(",")[0] : 440 state.baseFrequency = Number(state.baseFrequency) // 用户上次的频率和基准频率误差超过10,则重置 if (Math.abs(state.setting.frequency - state.baseFrequency) > 10) { state.setting.frequency = state.baseFrequency >= 0 ? state.baseFrequency : 440 } else { state.setting.frequency = state.setting.frequency || state.baseFrequency } state.playBtnDirection = query.imagePos === 'left' ? 'left' : 'right'; state.isAttendClass = (query.imagePos === 'left' || query.imagePos === 'right') ? true : false; }; // 多分轨合并显示标示 const setCustom = (trackNum?: number) => { if (trackNum || state.extConfigJson.multitrack) { state.multitrack = trackNum || 0 setGlobalData("multitrack", trackNum || state.extConfigJson.multitrack); } }; /** 跟练模式播放节拍器(叮咚) */ export const followBeatPaly = () => { let metroTimer: any = null; if (!followData.start) { clearTimeout(metroTimer) metroTimer = null return; } const time = state.measureTime * 1000 / metronomeData.totalNumerator / state.basePlayRate; requestAnimationFrame(() => { const endTime = Date.now(); if (endTime - state.beatStartTime < time) { followBeatPaly(); } else { // metroTimer = setTimeout(() => { // metronomeData.metro?.simulatePlayAudio() // startTime = Date.now(); // followBeatPaly(); // }, time); metronomeData.metro?.simulatePlayAudio() state.beatStartTime = Date.now(); followBeatPaly(); } }); }; // 音符添加bbox export const addNoteBBox = (list: any[]) => { const musicContainer = document.getElementById("musicAndSelection")?.getBoundingClientRect() || { x: 0, y: 0, }; const parentLeft = musicContainer.x || 0; 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(); const noteBbox = document.getElementById(`vf-${svgElement?.attrs?.id}`)?.getBoundingClientRect?.() || { x: 0, width: 0 }; bbox = { left: noteBbox.x - parentLeft - noteBbox.width / 4, // 用于简谱模式,跳动音符时,设置光标的位置(五线谱:osmd自动设置光标位置,简谱:需要手动设置光标位置) 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; } } // 给歌词和音符添加动态颜色 let prevActiveNoteIndex = -1 // 上一个激活的 export const fillWordColor = () => { // console.log('当前音符',state.activeNoteIndex,prevActiveNoteIndex) if(prevActiveNoteIndex !== -1) { const prevActiveNoteId = state.times[prevActiveNoteIndex]?.svgElement?.attrs?.id const svgEl = document.getElementById(`vf-${prevActiveNoteId}`) const stemEl = document.getElementById(`vf-${prevActiveNoteId}-stem`) const stemLine = document.getElementById(`vf-${prevActiveNoteId}-lines`) svgEl?.classList.remove('noteActive') stemEl?.classList.remove('noteActive') // stemLine?.classList.remove('noteActive') svgEl?.parentElement?.classList.remove('voiceActive') const si = state.times[prevActiveNoteIndex].si || 0; svgEl?.parentElement?.querySelectorAll('rect')?.forEach((item: any) => { item?.classList.remove('rectActive'); }) // svgEl?.parentElement?.querySelectorAll('rect')?.[si]?.classList.remove('rectActive'); } const activeNoteId = state.times[state.activeNoteIndex]?.svgElement?.attrs?.id const svgEl = document.getElementById(`vf-${activeNoteId}`) const stemEl = document.getElementById(`vf-${activeNoteId}-stem`) const stemLine = document.getElementById(`vf-${activeNoteId}-lines`) svgEl?.classList.add('noteActive') stemEl?.classList.add('noteActive') // stemLine?.classList.add('noteActive') // 如果是简谱、固定调,需要把音符后面跟着的"-"也添加颜色 if (state.musicRenderType === EnumMusicRenderType.firstTone || state.musicRenderType === EnumMusicRenderType.fixedTone) { // const parentVoice = svgEl?.parentElement; // 简谱模式下,二分音符和全音符才显示音符右侧的"-" if (state.times[state.activeNoteIndex].noteElement?.length?.realValue >= 0.5) { // 如果是二分音符,只亮该音符后面那个"-",本小节其它的"-"不亮 if (state.times[state.activeNoteIndex].noteElement?.length?.realValue === 0.5) { const si = state.times[state.activeNoteIndex].si || 0; const halfNotes = state.times[state.activeNoteIndex].measures.filter((item: any) => item?.noteElement?.length?.realValue === 0.5) || []; const sIdx = halfNotes?.findIndex((item: any) => item?.noteElement === state.times[state.activeNoteIndex]?.noteElement); const filterRects = svgEl?.parentElement?.querySelectorAll('rect')?.length ? Array.from(svgEl?.parentElement?.querySelectorAll('rect')).filter(item => item.parentElement === svgEl?.parentElement) : []; filterRects?.[sIdx]?.classList.add('rectActive'); } else { svgEl?.parentElement?.classList.add('voiceActive'); } } } prevActiveNoteIndex = state.activeNoteIndex // 给当前匹配到的歌词添加颜色 const currentNote = state.times[state.activeNoteIndex]; const lyrics: SVGAElement[] = Array.from(document.querySelectorAll(".vf-lyric")); lyrics.forEach((lyric) => { lyric?.classList.remove('lyricActive') }) const currentLyrics: SVGAElement[] = Array.from(document.querySelectorAll(`.lyric${currentNote?.noteId}`)); currentLyrics.forEach((lyric, index) => { const lyricIndex = lyric.getAttribute('lyricIndex'); // bug:#10942,如果需要反复唱的小节,只有一遍歌词,反复唱的时候,歌词都需要高亮 const onlyOneLyric = currentNote.measures?.every((item: any) => item?.formatLyricsEntries?.length <= 1); if ((index === currentNote.repeatIdx && currentNote.repeatIdx + 1 == lyricIndex) || (currentNote.repeatIdx != index && !onlyOneLyric && currentNote.repeatIdx + 1 == lyricIndex) || (currentNote.repeatIdx > 0 && currentNote.formatLyricsEntries?.length === 1 && onlyOneLyric)) { lyric?.classList.add('lyricActive') } // bug: #11189,兼容处理需要唱4遍,但是只打了2遍歌词的情况,1、3唱一样的歌词,2、4唱一样的歌词 if ( currentNote.formatLyricsEntries.length == 2 && currentNote.repeatIdx >= 2 && index === (currentNote.repeatIdx - 2) ) { lyric?.classList.add('lyricActive') } // if ((index === currentNote.repeatIdx && currentNote.repeatIdx + 1 == lyricIndex)) { // lyric?.classList.add('lyricActive') // } }) } /** 跳动svgdom */ export const moveSvgDom = (skipNote?: boolean) => { /** * 计算需要移动的距离 * 当前选中的音符和第一个音符之间的间距 */ if (skipNote) { // 点击 清空translateXNum smoothAnimationState.translateXNum = 0 moveTranslateXNum(0) // 移动小鸟的位置 moveSmoothAnimation(0, state.activeNoteIndex, false) // 移动谱面当当前音符的位置 const distance = state.times[state.activeNoteIndex].bbox?.x - state.times[0].bbox?.x smoothAnimationState.osdmScrollDom!.scrollTo({ left: distance, behavior: "smooth", }); } } // 暂停的时候 恢复一行谱偏移位置信息 watch( () => state.playState, () => { if (state.isSingleLine) { // 当在播放中暂停 执行这个方法 if (!state.playEnd && state.playState === "paused") { moveTranslateXNum(0) // 因为safari浏览器scrollWidth的值一直变化,scrollLeft + smoothAnimationState.translateXNum 为最大宽度的时候,实际上scrollLeft滚不到最大宽度,所以在下一帧处理滚动,能滚动到最大滚动位置 requestAnimationFrame(() => { const scrollLeft = smoothAnimationState.osdmScrollDom!.scrollLeft smoothAnimationState.osdmScrollDom!.scrollLeft = scrollLeft + smoothAnimationState.translateXNum smoothAnimationState.translateXNum = 0 }); } } } ) /* 根据当前的小节获取前面有多少需要去除的合并小节 */ function getNeedReduceMultipleRestNum(currMeasureIndex: number) { let needReduceMultipleRestNum = 0; for (let noteIndex = 0; noteIndex < state.times.length; noteIndex++) { const note = state.times[noteIndex]; if (note.MeasureNumberXML > currMeasureIndex) { break; } if (note.multipleRestMeasures && note.multipleRestMeasures > 1) { needReduceMultipleRestNum += 1; } } return needReduceMultipleRestNum } watch( () => state.activeMeasureIndex, () => { // 监听音符小节的变化,取对应的小节速度图片 const currentNote = state.times[state.activeNoteIndex] state.speedIcon = unitImgs[currentNote.speedBeatUnit] // 需要减去的合并小节数 // const needReduceMultipleRestNum = getNeedReduceMultipleRestNum(state.activeMeasureIndex) // const matchMeasureNum = state.activeMeasureIndex - needReduceMultipleRestNum - 1 // console.log('选中的小节',matchMeasureNum,'需要减去的小节',needReduceMultipleRestNum,'当前的小节',state.activeMeasureIndex) state.vfmeasures.forEach((item: any, idx: number) => { const dataNum = item.getAttribute('data-num') // 值可能为字符串类型的undefined let measureNum = (dataNum && dataNum !== "undefined") ? Number(dataNum) : -1; let nextDataNum = state.vfmeasures[idx+1]?.getAttribute('data-num') // 当有换行小节,下个小节的nextDataNum是undefined,所以这里需要往后找一个 if(!(nextDataNum && nextDataNum !== "undefined")){ nextDataNum = state.vfmeasures[idx + 2]?.getAttribute('data-num') } const nextMeasureNum = Number(nextDataNum) // 当measureNum 为undefined 则是下一个小节的换行小节,所以这里等于下一个小节 if(measureNum === -1) { measureNum = nextMeasureNum } if (measureNum >= 0 && (measureNum === state.activeMeasureIndex || (measureNum < state.activeMeasureIndex && nextMeasureNum > state.activeMeasureIndex))) { item.querySelector('.vf-custom-bg')?.setAttribute("fill", "rgba(9,159,255,0.15)") // 预备小节 if(state.sectionFirst && measureNum === state.sectionFirst.MeasureNumberXML && state.section.length === 2){ item?.querySelector('.vf-custom-bg')?.setAttribute("fill", "rgba(255,193,48,0.15)") } } else { // 有选段只清除选段处的 if (state.section.length === 2) { // 把中间置灰 let leftMeasureNumberXML = state.section[0].MeasureNumberXML let rightMeasureNumberXML = state.section[1].MeasureNumberXML if (leftMeasureNumberXML > rightMeasureNumberXML) { leftMeasureNumberXML = state.section[1].MeasureNumberXML rightMeasureNumberXML = state.section[0].MeasureNumberXML } if(measureNum >= leftMeasureNumberXML && measureNum <= rightMeasureNumberXML){ item.querySelector('.vf-custom-bg')?.setAttribute("fill", 'transparent') } if (measureNum >= leftMeasureNumberXML && measureNum <= rightMeasureNumberXML) { item.querySelector('.vf-custom-bg')?.setAttribute("fill", "rgba(9,159,255,0.15)") } // 预备小节 if(state.sectionFirst && measureNum === state.sectionFirst.MeasureNumberXML){ item?.querySelector('.vf-custom-bg')?.setAttribute("fill", "rgba(255,193,48,0.15)") } } else { item.querySelector('.vf-custom-bg')?.setAttribute("fill", 'transparent') } } }) } ); watch( () => state.section, () => { if (state.section.length === 2) { // 把前面和 后面置灰 let leftMeasureNumberXML = state.section[0].MeasureNumberXML let rightMeasureNumberXML = state.section[1].MeasureNumberXML if (leftMeasureNumberXML > rightMeasureNumberXML) { leftMeasureNumberXML = state.section[1].MeasureNumberXML rightMeasureNumberXML = state.section[0].MeasureNumberXML } state.vfmeasures.forEach((item: any, idx: number) => { const dataNum = item.getAttribute('data-num') // 值可能为字符串类型的undefined let measureNum = (dataNum && dataNum !== "undefined") ? Number(dataNum) : -1; let nextDataNum = state.vfmeasures[idx+1]?.getAttribute('data-num') // 当有换行小节,下个小节的nextDataNum是undefined,所以这里需要往后找一个 if(!(nextDataNum && nextDataNum !== "undefined")){ nextDataNum = state.vfmeasures[idx + 2]?.getAttribute('data-num') } const nextMeasureNum = Number(nextDataNum) // 当measureNum 为undefined 则是下一个小节的换行小节,所以这里等于下一个小节 if(measureNum === -1) { measureNum = nextMeasureNum } // 小于选中置灰 if (measureNum < leftMeasureNumberXML) { item.querySelector('.vf-custom-bg')?.setAttribute("fill", 'transparent') } // 大于选中置灰 if(measureNum > rightMeasureNumberXML){ item.querySelector('.vf-custom-bg')?.setAttribute("fill", 'transparent') } if (measureNum >= leftMeasureNumberXML && measureNum <= rightMeasureNumberXML) { item.querySelector('.vf-custom-bg')?.setAttribute("fill", "rgba(9,159,255,0.15)") } // 预备小节 if(state.sectionFirst && measureNum === state.sectionFirst.MeasureNumberXML){ item?.querySelector('.vf-custom-bg')?.setAttribute("fill", "rgba(255,193,48,0.15)") } }) }else{ // 恢复选段前 state.vfmeasures.forEach((item: any, idx: number) => { const dataNum = item.getAttribute('data-num') // 值可能为字符串类型的undefined let measureNum = (dataNum && dataNum !== "undefined") ? Number(dataNum) : -1; let nextDataNum = state.vfmeasures[idx+1]?.getAttribute('data-num') // 当有换行小节,下个小节的nextDataNum是undefined,所以这里需要往后找一个 if(!(nextDataNum && nextDataNum !== "undefined")){ nextDataNum = state.vfmeasures[idx + 2]?.getAttribute('data-num') } const nextMeasureNum = Number(nextDataNum) // 当measureNum 为undefined 则是下一个小节的换行小节,所以这里等于下一个小节 if(measureNum === -1) { measureNum = nextMeasureNum } if (measureNum >= 0 && (measureNum === state.activeMeasureIndex || (measureNum < state.activeMeasureIndex && nextMeasureNum > state.activeMeasureIndex)) ) { item.querySelector('.vf-custom-bg')?.setAttribute("fill", "rgba(9,159,255,0.15)") } else { item.querySelector('.vf-custom-bg')?.setAttribute("fill", "transparent") } }) } } ) // 后台编辑谱面模式,切换谱面时,如果有操作没有保存,需要给出提示 export const checkMoveNoSave = async () => { return new Promise((resolve, reject) => { if (query.isMove) { if (moveData.open && undoData.undoList.length) { showConfirmDialog({ className: "noSaveModal", title: "温馨提示", message: "您有新的修改还未保存,切换谱面后本次编辑的内容将不会保存", }).then(() => { moveData.open = false resolve(true) }).catch(() => { return; }); } else { moveData.open = false undoData.undoList = [] resolve(true) } } else { resolve(true) } }); } /** 刷新谱面 */ export const refreshMusicSvg = () => { moveData.noteCoords = [] moveData.modelList = [] clearSelection(); resetBaseRate(); state.activeMeasureIndex = -1; if (query.workRecord) { state.workSectionNeedReset = true; } // 销毁旋律线 destroySmoothAnimation() musicScoreRef.value?.refreshMusicScore() } // 指法改变显示的时候 osdmScrollDomWith 宽度会变化 所以指法改变的时候这个宽度重新计算 watch( () => state.setting.displayFingering, () => { // 有字符 并且是竖向指法 并且是一行谱 if(state.fingeringInfo?.name && state.fingeringInfo.direction === "vertical" && state.isSingleLine){ nextTick(() => { calcClientWidth() }) } // 如果有指法,并且是竖向指法时,切换指法时,谱面宽度变化,需要重新渲染谱面 if (state.fingeringInfo?.name && state.fingeringInfo.direction === "vertical" && !state.isSingleLine) { headTopData.settingMode = false; refreshMusicSvg(); } } ) // 节奏律动改变显示的时候 osdmScrollDomWith 宽度会变化 所以指法改变的时候这个宽度重新计算 watch( () => headTopData.rhythmMode, () => { // 竖向 并且是一行谱 if(headTopData.rhythmModeDirection === "vertical" && state.isSingleLine){ nextTick(() => { calcClientWidth() }) } // 竖向时,谱面宽度变化,需要重新渲染谱面 if (headTopData.rhythmModeDirection === "vertical" && !state.isSingleLine) { refreshMusicSvg(); } } )