import state from './state' import appState from '/src/state' import { browser } from '/src/helpers/utils' import runtime, { getFixTime } from './runtime' // @ts-ignore import { isSpecialMark, isSpeedKeyword, Fraction, SourceMeasure, isGradientWords, GRADIENT_SPEED_RESET_TAG, StringUtil, } from '/osmd-extended/src' import dayjs from 'dayjs' const browserInfo = browser() const getLinkId = (): string => { return location.hash.split('?')[0].split('/').pop() || '' } export const retain = (time: number) => { return Math.ceil(time * 1000000) / 1000000 } export function formatBeatUnit(beatUnit: string) { let multiple = 4 switch (beatUnit) { case '1024th': // bpm = bpm; multiple = 1024 break case '512th': // divisionsFromNote = (noteDuration / 4) * 512; multiple = 512 break case '256th': // divisionsFromNote = (noteDuration / 4) * 256; multiple = 256 break case '128th': // divisionsFromNote = (noteDuration / 4) * 128; multiple = 128 break case '64th': // divisionsFromNote = (noteDuration / 4) * 64; multiple = 64 break case '32nd': // divisionsFromNote = (noteDuration / 4) * 32; multiple = 32 break case '16th': // divisionsFromNote = (noteDuration / 4) * 16; multiple = 16 break case 'eighth': // divisionsFromNote = (noteDuration / 4) * 8; multiple = 8 break case 'quarter': multiple = 4 break case 'half': // divisionsFromNote = (noteDuration / 4) * 2; multiple = 2 break case 'whole': // divisionsFromNote = (noteDuration / 4); multiple = 1 break case 'breve': // divisionsFromNote = (noteDuration / 4) / 2; multiple = 0.5 break case 'long': // divisionsFromNote = (noteDuration / 4) / 4; multiple = 0.25 break case 'maxima': // divisionsFromNote = (noteDuration / 4) / 8; multiple = 0.125 break default: break } return multiple } export const formatLyricsEntries = (note: any) => { const voiceEntries = note.parentStaffEntry?.voiceEntries || [] const lyricsEntries: string[] = [] for (const voic of voiceEntries) { if (voic.lyricsEntries?.table) { const values: any[] = Object.values(voic.lyricsEntries.table) for (const lyric of values) { lyricsEntries.push(lyric?.value.text) } } } return lyricsEntries } export const getMeasureDurationDiff = (measure: any) => { const { realValue: SRealValue } = measure.activeTimeSignature const { realValue: RRealValue } = measure.duration return SRealValue - RRealValue } export type GradualChange = { resetXmlNoteIndex: number startXmlNoteIndex: number endXmlNoteIndex: number startWord: string } const speedInfo: { [key in string]: number } = { 'rall.': 1.333333333, 'poco rit.': 1.333333333, 'rit.': 1.333333333, 'molto rit.': 1.333333333, 'molto rall': 1.333333333, lentando: 1.333333333, allargando: 1.333333333, morendo: 1.333333333, 'accel.': 0.8, calando: 2, 'poco accel.': 0.8, } /** 按照dorico的渐快渐慢生成对应的速度 */ export const createSpeedInfo = (gradualChange: GradualChange | undefined, speed: number) => { if (gradualChange && speedInfo[gradualChange.startWord?.toLocaleLowerCase()]) { const notenum = Math.max(gradualChange.endXmlNoteIndex, 3) const speeds: number[] = [] const startSpeed = speed const endSpeed = speed / speedInfo[gradualChange.startWord?.toLocaleLowerCase()] for (let index = 0; index < notenum; index++) { const speed = startSpeed + ((endSpeed - startSpeed) / notenum) * (index + 1) speeds.push(speed) } return speeds } return } const tranTime = (str: string = '') => { let result = str const splits = str.split(':') if (splits.length === 1) { result = `00:${splits[0]}:00` } else if (splits.length === 2) { result = `00:${splits[0]}:${splits[1]}` } // console.log(`1970-01-01 00:${result}0`) return `1970-01-01 00:${result}0` } export const getAllNodes = (osmd: any) => { console.log(9999, osmd) const detailId = getLinkId() let fixtime = browserInfo.huawei ? 0.08 : 0 //getFixTime() const allNotes: any[] = [] const allNoteId: string[] = [] const allMeasures: any[] = [] const { baseSpeed = 100 } = state const formatRealKey = (realKey: number, detail: any) => { // 长笛的LEVEL 2-5-1条练习是泛音练习,以每小节第一个音的指法为准,高音不变变指法。 const olnyOneIds = ['906'] if (olnyOneIds.includes(detailId)) { return detail.measures[0]?.realKey || realKey } // 圆号的LEVEL 2-5条练习是泛音练习,最后四小节指法以连音线第一个小节为准 const olnyOneIds2 = ['782', '784'] if (olnyOneIds2.includes(detailId)) { const measureNumbers = [14, 16, 30, 32] if (measureNumbers.includes(detail.firstVerticalMeasure?.measureNumber)) { return allNotes[allNotes.length - 1]?.realKey || realKey } } // 2-6 第三小节指法按照第一个音符显示 const filterIds = ['900', '901', '640', '641', '739', '740', '800', '801', '773', '774', '869', '872', '714', '715'] if (filterIds.includes(detailId)) { if (detail.firstVerticalMeasure?.measureNumber === 3 || detail.firstVerticalMeasure?.measureNumber === 9) { return detail.measures[0]?.realKey || realKey } } return realKey } if (state.gradualTimes) { console.log('合奏速度', state.gradual, state.gradualTimes) } if (osmd?.cursor) { try { osmd.cursor.reset() } catch (error) {} const iterator = osmd.cursor.iterator let i = 0 let si = 0 let measures: any[] = [] let stepSpeeds: number[] = [] let usetime = 0 let relaMeasureLength = 0 /** 弱起时间 */ let difftime = 0 let beatUnit = 'quarter' let useGradualTime = 0 let gradualSpeed let gradualChange: GradualChange | undefined let gradualChangeIndex = 0 let measureNumberPrinted = 1 let indexOf = 0 let staveIndex = 0 let staveNoteIndex = 0 let currentRealValueTotal = 0 let skipNextNote = false let multipleRestMeasures = 0 // const useedmeasures: Set = new Set() while (!iterator.endReached) { // 为获取所有节点有修改源码currentVoiceEntries仅返回可见第一行,请搜索“修改为 仅根据当前可见声部第一行跳转”可找到位置 // 多声部仅循环一次,以第一声部为准 // console.log('Entries', {...osmd.cursor}, osmd.cursor.cursorElement) const cursorImg: HTMLElement = osmd.cursor.cursorElement const cursorBox: any = { move: false } if (cursorImg) { cursorBox.move = true cursorBox.x = cursorImg.offsetLeft cursorBox.y = cursorImg.offsetTop cursorBox.w = cursorImg.offsetWidth cursorBox.h = cursorImg.offsetHeight } const voiceEntries = iterator.currentVoiceEntries?.[0] ? [iterator.currentVoiceEntries?.[0]] : [] const voiceEntries2 = iterator.currentVoiceEntries?.[1] let skipMode = false for (const v of voiceEntries) { // 始终只取第一个声部中第一个音符的时间 let note: any = v.notes[0] if (['Piano'].includes(state.activeDetail?.code) || state.activeDetail?.musicSheetType == 'CONCERT') { let _notes = [] try { _notes = iterator.currentVoiceEntries ?.map((_n: any) => _n.notes) .flat() .sort((a: any, b: any) => a.Length.realValue - b.Length.realValue) note = _notes[0] } catch (error) {} } if (note) { if (si === 0) { allMeasures.push(note.sourceMeasure) } if (si === 0 && state.isSpecialBookCategory) { for (const expression of note.sourceMeasure?.TempoExpressions) { if (expression?.InstantaneousTempo?.beatUnit) { // 取最后一个有效的tempo beatUnit = expression.InstantaneousTempo.beatUnit } } } measureNumberPrinted = note.sourceMeasure?.MeasureNumberXML // 判断是否是同一小节 if (staveIndex == note.sourceMeasure?.MeasureNumberXML) { staveNoteIndex++ } else { // staveIndex不同,重新赋值 staveIndex = note.sourceMeasure?.MeasureNumberXML staveNoteIndex = 0 } let measureSpeed = note.sourceMeasure.tempoInBPM const { metronomeNoteIndex } = iterator.currentMeasure if (metronomeNoteIndex !== 0 && metronomeNoteIndex > si) { measureSpeed = allNotes[allNotes.length - 1]?.speed || 100 } const activeVerticalMeasureList = [note.sourceMeasure.verticalMeasureList?.[0]] || [] const { realValue } = iterator.currentTimeStamp const { RealValue: vRealValue, Denominator: vDenominator } = formatDuration( iterator.currentMeasure.activeTimeSignature, iterator.currentMeasure.duration ) let { wholeValue, numerator, denominator, realValue: noteRealValue } = note.length let relativeTime = usetime //realValue * 4 * (60 / measureSpeed) /** * 速度不能为0, 此处的速度应该是按照设置的速度而不是校准后的速度,否则mp3速度不对 */ let beatSpeed = (state.isSpecialBookCategory ? getTimeByBeatUnit(beatUnit, measureSpeed, iterator.currentMeasure.activeTimeSignature.Denominator) : baseSpeed) || 1 let speed = (state.isSpecialBookCategory ? measureSpeed : baseSpeed) || 1 // console.log('曲谱设置的速度', { base: getTimeByBeatUnit(beatUnit, measureSpeed, iterator.currentMeasure.activeTimeSignature.Denominator), beatSpeed, speed}) gradualChange = iterator.currentMeasure.speedInfo || gradualChange gradualSpeed = osmd.sheet.soundTempos?.get(note.sourceMeasure.measureListIndex) || gradualSpeed if (!gradualSpeed || gradualSpeed.length < 2) { gradualSpeed = createSpeedInfo(gradualChange, speed) } /** * 渐变速度逻辑如下: * 1. 获取渐变关键词,记录开始区间、结束区间、渐变整体结束; * 2. 计算区间内每个小节最小音符时长单位,整除计算比例; * 3. 按照比例计算下一个音符的速度; * 4. 合奏曲目这里需要处理一下,因为音符位置可能不太相同,理想的情况是每个分谱指定结束时间. */ let gradualLength = 0 const measureListIndex = iterator.currentMeasure.measureListIndex if (state.gradualTimes && Object.keys(state.gradualTimes).length > 0) { const withInRangeNote = state.gradual.find((item, index) => { const nextItem: any = state.gradual[index + 1] return ( item[0].measureIndex <= measureListIndex && item[1]?.measureIndex! >= measureListIndex && (!nextItem || nextItem?.[0].measureIndex !== measureListIndex) ) }) if (!withInRangeNote) { useGradualTime = 0 } const [first, last] = withInRangeNote || [] if (first && last) { // 小节数量 const continuous = last.measureIndex - first.measureIndex // 开始小节内 const inTheFirstMeasure = first.closedMeasureIndex == measureListIndex && si >= first.noteInMeasureIndex // 结束小节内 const inTheLastMeasure = last.closedMeasureIndex === measureListIndex && si < last.noteInMeasureIndex // 范围内小节 const inFiestOrLastMeasure = first.closedMeasureIndex !== measureListIndex && last.closedMeasureIndex !== measureListIndex if (inTheFirstMeasure || inTheLastMeasure || inFiestOrLastMeasure) { const startTime = state.gradualTimes[first.measureIndex] const endTime = state.gradualTimes[last.measureIndex] if (startTime && endTime) { const times = continuous - first.leftDuration / first.allDuration + last.leftDuration / last.allDuration const diff = dayjs(tranTime(endTime)).diff(dayjs(tranTime(startTime)), 'millisecond') gradualLength = ((noteRealValue / vRealValue / times) * diff) / 1000 useGradualTime += gradualLength } } } } else if ( gradualChange && gradualSpeed && (gradualChange.startXmlNoteIndex === si || gradualChangeIndex > 0) ) { const startSpeed = gradualSpeed[0] - (gradualSpeed[1] - gradualSpeed[0]) const { resetXmlNoteIndex, endXmlNoteIndex } = gradualChange const noteDiff = endXmlNoteIndex let stepSpeed = (gradualSpeed[gradualSpeed.length - 1] - startSpeed) / noteDiff stepSpeed = note.DotsXml ? stepSpeed / 1.5 : stepSpeed if (gradualChangeIndex < noteDiff) { const tempSpeed = Math.ceil(speed + stepSpeed * gradualChangeIndex) let tmpSpeed = getTimeByBeatUnit( beatUnit, tempSpeed, iterator.currentMeasure.activeTimeSignature.Denominator ) const maxLength = (wholeValue + numerator / denominator) * vDenominator * (60 / tmpSpeed) speed += Math.ceil(stepSpeed * (gradualChangeIndex + 1)) tmpSpeed = getTimeByBeatUnit(beatUnit, speed, iterator.currentMeasure.activeTimeSignature.Denominator) const minLength = (wholeValue + numerator / denominator) * vDenominator * (60 / tmpSpeed) gradualLength = (maxLength + minLength) / 2 } else if (resetXmlNoteIndex > gradualChangeIndex) { speed = allNotes[i - 1]?.speed } beatSpeed = (state.isSpecialBookCategory ? getTimeByBeatUnit(beatUnit, speed, iterator.currentMeasure.activeTimeSignature.Denominator) : baseSpeed) || 1 const isEnd = !(gradualChangeIndex < noteDiff) && !(resetXmlNoteIndex > gradualChangeIndex) gradualChangeIndex++ // console.log(gradualChangeIndex) if (isEnd) { gradualChangeIndex = 0 gradualChange = undefined gradualSpeed = undefined stepSpeeds = [] } } if (i === 0) { fixtime += getFixTime(beatSpeed) } // console.log({fixtime, relativeTime}, 99999999) // 酷乐秀计算音符时值方法 // let noteLength = // (numerator === 0 && note.isRestFlag ? vRealValue : (wholeValue + numerator) / denominator) * // vDenominator * // (60 / beatSpeed) //管乐迷计算时值方法 let noteLength = gradualLength ? gradualLength : Math.min(vRealValue, noteRealValue) * formatBeatUnit(beatUnit) * (60 / beatSpeed) const measureLength = vRealValue * vDenominator * (60 / beatSpeed) // 单独处理个别的声部 if (['Piano'].includes(state.activeDetail?.code)) { const currentRealValue = iterator.currentTimeStamp.realValue - currentRealValueTotal noteLength = (currentRealValue || (numerator === 0 ? vRealValue : (wholeValue + numerator) / denominator)) * vDenominator * (60 / beatSpeed) } // 如果是休止符并且整个小节休止,休止符的时值小于小节时值,取小节的时值 if (note.isRestFlag && note?.sourceMeasure?.allRests) { multipleRestMeasures = note?.sourceMeasure?.multipleRestMeasures } if (multipleRestMeasures > 0) { multipleRestMeasures -= 1 noteLength = measureLength } // 如果休止符的时值大于小节的时值 if (note.isRestFlag && noteLength > measureLength) { noteLength = measureLength } // 处理附点时长不正确问题 if (note.DotsXml && note.tuplet) { console.log('处理附点时长不正确问题') noteLength = noteLength * 1.5 } // 后倚音通过跳过的方式实现 if (skipNextNote) { noteLength = 0.0000001 skipNextNote = false skipMode = true } const Expressions = note.sourceMeasure.staffLinkedExpressions?.[0] for (const Expression of Expressions || []) { if (Expression) { const needSkip = Expression.expressions?.find((item: any) => item.label === '跳过下一个') if (needSkip && Fraction.Equal(note.voiceEntry?.Timestamp, Expression.Timestamp)) { skipNextNote = true break } } } currentRealValueTotal = iterator.currentTimeStamp.realValue usetime += noteLength relaMeasureLength += noteLength let relaEndtime = noteLength + relativeTime // console.log({noteLength,relativeTime ,relaEndtime, endtime: relaEndtime + fixtime}) const fixedKey = note.ParentVoiceEntry.ParentVoice.Parent.SubInstruments[0].fixedKey || 0 // const svgElelent = activeVerticalMeasureList[0]?.vfVoices['1']?.tickables[si] const svgElelent = activeVerticalMeasureList[0]?.vfVoices['1']?.tickables[staveNoteIndex] // console.log(relativeTime) if (allNotes.length && allNotes[allNotes.length - 1].relativeTime === relativeTime) { continue } // console.log(iterator.currentMeasure) // 如果是弱起就补齐缺省的时长 if (i === 0) { let _firstMeasureRealValue = 0 const staffEntries = note.sourceMeasure.verticalMeasureList?.[0]?.staffEntries || [] //计算第一个小节里面的音符时值是否等于整个小节的时值 staffEntries.forEach((_a: any) => { if(_a?.sourceStaffEntry?.voiceEntries?.[0]?.notes?.[0]?.length?.realValue){ _firstMeasureRealValue += _a.sourceStaffEntry.voiceEntries[0].notes[0].length.realValue } }) if (_firstMeasureRealValue < vRealValue){ // console.log(_firstMeasureRealValue, vRealValue) // 如果是弱起,将整个小节的时值减去音符的时值,就是缺省的时值 difftime = measureLength - noteLength } /** 如果是酷乐秀MIDI类型文件就不处理弱起 */ if (state.activeDetail?.audioType === 'MIDI') { difftime = 0 } fixtime += difftime } const nodeDetail = { fixtime, skipMode, NoteToGraphicalNoteObjectId: note.NoteToGraphicalNoteObjectId, cursorBox, skipNextNote, measureNumberPrinted, difftime, octaveOffset: activeVerticalMeasureList[0]?.octaveOffset, frequency: note.pitch?.frequency, speed, beatSpeed, i, si, stepSpeeds, indexOfMeasures: indexOf, measureOpenIndex: allMeasures.length - 1, measures, //: groupMeasures[indexOf], tempoInBPM: note.sourceMeasure.tempoInBPM, measureLength, relaMeasureLength, id: svgElelent?.attrs.id, note: note.halfTone + 12, // see issue #224 relativeTime: retain(relativeTime), time: retain(relativeTime + fixtime), endtime: retain(relaEndtime + fixtime), relaEndtime: retain(relaEndtime), realValue, halfTone: note.halfTone, voiceEntry: { isStaccato: note.voiceEntry.isStaccato(), Timestamp: { ...note.voiceEntry.Timestamp }, ornamentContainer: note.voiceEntry.ornamentContainer ? { ...note.voiceEntry.ornamentContainer } : '', }, noteElement: { NoteToGraphicalNoteObjectId: note.NoteToGraphicalNoteObjectId, notehead: note.notehead ? { filled: note.notehead.filled, shape: note.notehead.shape, sourceNote: note.notehead.sourceNote?.NoteToGraphicalNoteObjectId, } : '', noteheadColor: note.noteheadColor, isRestFlag: note.isRestFlag, sourceMeasure: { measureListIndex: note?.SourceMeasure?.measureListIndex, MeasureNumberXML: note?.SourceMeasure?.MeasureNumberXML, allRests: note?.SourceMeasure?.allRests, isRestFlag: note?.SourceMeasure?.isRestFlag, multipleRestMeasures: note?.SourceMeasure?.multipleRestMeasures, verticalMeasureList: Array.isArray(note?.SourceMeasure?.verticalMeasureList) ? note.SourceMeasure.verticalMeasureList.map((v: any) => { const { x, y, width, height, start_x, end_x } = v?.stave || {} return v ? { stave: { x, y, width, height, start_x, end_x, }, boundingBox: v && v.boundingBox ? { absolutePosition: { ...v.boundingBox.absolutePosition }, size: { ...v.boundingBox.size }, } : '', } : undefined }) : [], activeTimeSignature: { ...note.activeTimeSignature }, }, tie: note.tie ? { StartNote: { NoteToGraphicalNoteObjectId: note.tie.StartNote.NoteToGraphicalNoteObjectId, }, notes: (Array.isArray(note.tie.notes) && note.tie.notes?.map((_tie: any) => { return { NoteToGraphicalNoteObjectId: _tie.NoteToGraphicalNoteObjectId, } })) || [], } : '', slurs: Array.isArray(note.slurs) ? note.slurs.map((slur: any) => { return { startNote: { NoteToGraphicalNoteObjectId: slur.startNote.NoteToGraphicalNoteObjectId, }, endNote: { NoteToGraphicalNoteObjectId: slur.endNote.NoteToGraphicalNoteObjectId, }, } }) : [], pitch: { prevFrequency: note.pitch?.prevFrequency, nextFrequency: note.pitch?.nextFrequency, frequency: note.pitch?.frequency, }, Length: { ...note.Length, }, }, svgElelent: svgElelent ? { bbox: svgElelent.getBoundingBox?.() || '', top_y: svgElelent.top_y, note_height: svgElelent.note_height, } : '', fixedKey, realKey: 0, duration: 0, formatLyricsEntries: formatLyricsEntries(note), stave: activeVerticalMeasureList[0] && activeVerticalMeasureList[0].stave ? { attrs: activeVerticalMeasureList[0].stave ? { ...activeVerticalMeasureList[0].stave.attrs } : {}, } : '', firstVerticalMeasure: { measureNumber: activeVerticalMeasureList?.[0]?.measureNumber }, noteLength: 1, halfTone1: Array.isArray(v.notes) ? v.notes.map((n: any) => n.halfTone + 12).filter(Boolean) : [], halfTone2: voiceEntries2 && Array.isArray(voiceEntries2.notes) ? voiceEntries2.notes.map((n: any) => n.halfTone + 12).filter(Boolean) : [], } nodeDetail.realKey = formatRealKey(note.halfTone - fixedKey * 12, nodeDetail) nodeDetail.duration = nodeDetail.endtime - nodeDetail.time const tickables = activeVerticalMeasureList[0]?.vfVoices['1']?.tickables || [] const sublength = note.sourceMeasure.verticalMeasureList?.[0]?.staffEntries?.length || tickables.length nodeDetail.noteLength = sublength || 1 allNotes.push(nodeDetail) allNoteId.push(nodeDetail.id) measures.push({ realKey: nodeDetail.realKey, NoteToGraphicalNoteObjectId: note.NoteToGraphicalNoteObjectId, }) if (si < sublength - 1) { si++ } else { si = 0 relaMeasureLength = 0 measures = [] } } } // iterator.moveToNextVisibleVoiceEntry(false) osmd.cursor.next() i++ } try { osmd.cursor.reset() } catch (error) {} } // 按照时间轴排序 // console.log('看看👀', allNotes) const sortArray = allNotes .sort((a, b) => a.relativeTime - b.relativeTime) .map((item, index) => ({ ...item, i: index })) // for (let i = 0; i < sortArray.length; i++) { // const note = { ...sortArray[i] } // const prevNote = sortArray[i - 1] // const isNotNeedStop = note.noteElement.tie && prevNote?.noteElement.tie && note.halfTone === prevNote?.halfTone // const isOvertone = false // if (prevNote) { // if (isNotNeedStop || isOvertone) { // note.sourceStartTime = note.time // note.sourceRelativeTime = note.relativeTime // note.sourceRealValue = note.realValue // note.sourceEndTime = note.endtime // note.sourceRelaEndtime = note.relaEndtime // note.relativeTime = prevNote.relativeTime // note.realValue = prevNote.realValue // note.time = prevNote.time // note.endtime = prevNote.endtime // note.relaEndtime = prevNote.relaEndtime // } // // 此处会导致休止符继续上一个音的指法 // if (note.halfTone === 0) { // note.realKey = prevNote.realKey // } // } // sortArray[i] = note // } // console.log(sortArray) return sortArray } export const getAllNoteElements = (osmd: any) => { const list: any[] = [] const listById: { [key: string]: any } = {} for (const measure of osmd.drawer.graphicalMusicSheet.measureList) { const activeMeasure = measure[0] for (const tickable of activeMeasure.vfVoices['1'].tickables) { list.push(tickable) listById[tickable.attrs.id] = tickable } } return { list, listById, } } export const setStepIndex = (osmd: any, num: number, prev?: number) => { if (osmd.product) { if (num || num === 0) { // console.log(prev, num) if (prev && num - prev === 1) { osmd.cursor.setPosition({ ...state.times[num].cursorBox }) } else if (prev && num - prev > 0) { while (num - prev > 0) { prev++ osmd.cursor.setPosition({ ...state.times[prev].cursorBox }) } } else { osmd.cursor.setPosition({ ...state.times[num].cursorBox }) } } } else { if (num || num === 0) { // console.log(prev, num) if (prev && num - prev === 1) { osmd.cursor.next() } else if (prev && num - prev > 0) { while (num - prev > 0) { prev++ num - prev > 0 osmd.cursor.next() } } else { let i = 0 osmd.cursor.reset() while (i < num) { i++ if (osmd.cursor.hidden !== false) { osmd.cursor.show() } else { // console.log(i, num) osmd.cursor.next() } } } } } } export const getIndex = (times: any[], currentTime: Number) => { // console.log(currentTime) if (currentTime > state.times[state.times.length - 1].endtime) { return -1 } let index = 0 const ftime = times.filter((n, fi) => { const p: any = times[fi - 1] return p?.skipNextNote === false }) for (let i = 0; i < ftime.length; i++) { const item = ftime[i] const prevItem = ftime[i - 1] if (currentTime >= item.time) { if (!prevItem || item.time != prevItem.time) { index = item.i } } else { break } } if (state.sectionStatus && state.section.length === 2) { // 限制不超过此范围 const startSection = state.befireSection || state.section[0] index = Math.min(Math.max(index, startSection.i), state.section[1].i) // console.log('endIndex', index) } return index } export const getSlursNote = (_note: any, pos?: 'start' | 'end') => { const note: any = state.times.find((n: any) => n.NoteToGraphicalNoteObjectId == _note.NoteToGraphicalNoteObjectId) || {} let itemNote = pos === 'end' ? note.noteElement.slurs[0]?.endNote : note.noteElement.slurs[0]?.startNote // console.log("🚀 ~ itemNote", itemNote, note) if (!itemNote) return undefined return state.times.find((n: any) => n.NoteToGraphicalNoteObjectId == itemNote.NoteToGraphicalNoteObjectId) } export const getNoteBySlursStart = (note: any, anyNoteHasSlurs?: boolean, pos?: 'start' | 'end') => { let activeNote = note let slursNote = getSlursNote(activeNote, pos) if (!slursNote && anyNoteHasSlurs) { for (const _item of activeNote.measures) { const item = state.times.find((n: any) => n.NoteToGraphicalNoteObjectId == _item.NoteToGraphicalNoteObjectId) // console.log("🚀 ~ item", item) if (item.noteElement.slurs.length) { slursNote = getSlursNote(item, pos) activeNote = item } } } if (activeNote && slursNote !== activeNote.noteElement) { // time = activeNote.time for (const note of state.times) { if (slursNote === note.noteElement) { return note } } } return activeNote } /** 根据 noteElement 获取note */ export const getParentNote = (note: any) => { let parentNote if (note) { // time = activeNote.time for (const n of state.times) { if (note.NoteToGraphicalNoteObjectId === n.noteElement.NoteToGraphicalNoteObjectId) { // console.log(note) return n } } } return parentNote } /** 获取小节之间的连音线,仅同音高*/ export const getNoteByMeasuresSlursStart = (note: any) => { let activeNote = note let tieNote // console.log(note.noteElement) if (note.noteElement.tie && note.noteElement.tie.StartNote) { tieNote = note.noteElement.tie.StartNote } if (activeNote && tieNote && tieNote !== activeNote.noteElement) { // time = activeNote.time for (const note of state.times) { if (tieNote.NoteToGraphicalNoteObjectId === note.noteElement.NoteToGraphicalNoteObjectId) { // console.log(note) return note } } } return activeNote } export const getActtiveNoteByTimes = (evt: MouseEvent) => { const el = (evt.target as HTMLDivElement)?.dataset // console.log(state) const data: any = {} for (const time of state.times) { if (time.id) { data[time.id] = time } } const activeNote = data[el.id || ''] return activeNote } const getPrevHasSourceNote = (note: any) => { const indexOf = Math.max(state.times.indexOf(note) - 1, 0) for (let index = indexOf; index >= 0; index--) { const item = state.times[index] if (item?.stave) { return item } } } export const getBoundingBoxByverticalNote = (note: any) => { let measures = note?.noteElement?.sourceMeasure?.verticalMeasureList measures = !measures || !measures[0] ? note?.noteElement?.isRestFlag && getPrevHasSourceNote(note)?.noteElement?.sourceMeasure?.verticalMeasureList : measures let height = 0 if (measures) { const firstMeasure = measures[runtime.partIndex] for (let index = 0; index < measures.length; index++) { const measure = measures[index] if (measure?.stave) { const { height: measureHeight } = measure?.stave if (index > 0) { height += measures[index - 1]?.stave.height } height += measureHeight const { x, y, width, context, start_x, end_x } = firstMeasure?.stave return { measureIndex: note?.noteElement?.sourceMeasure.measureListIndex || 0, MeasureNumberXML: note?.noteElement?.sourceMeasure.MeasureNumberXML || 1, start_x, end_x, height, x, y, width, context, } } } } return { measureIndex: 0, height, start_x: 0, end_x: 0, x: 0, y: 0, width: 0, context: { element: null, }, } } export const getDuration = (osmd: any): any => { if (osmd) { const firstMeasure = osmd?.graphic?.measureList[0][0] // console.log(osmd?.graphic?.measureList[0][0]?.parentSourceMeasure) if (firstMeasure) { const { duration, tempoInBPM, activeTimeSignature, TempoExpressions } = firstMeasure?.parentSourceMeasure if (duration) { let beatUnit = 'quarter' for (const item of TempoExpressions) { beatUnit = item.InstantaneousTempo.beatUnit || 'quarter' } return { ...formatDuration(activeTimeSignature, duration), tempoInBPM, beatUnit, } } } } return {} } export const formatDuration = (activeTimeSignature: Fraction, duration: Fraction): Fraction => { // 弱起第一小节duration不对 return activeTimeSignature } /** 根据音符单位,速度,几几拍计算正确的时间 */ export const getTimeByBeatUnit = (beatUnit: string, bpm: number, denominator: number) => { let multiple = 4 switch (beatUnit) { case '1024th': // bpm = bpm; multiple = 1024 break case '512th': // divisionsFromNote = (noteDuration / 4) * 512; multiple = 512 break case '256th': // divisionsFromNote = (noteDuration / 4) * 256; multiple = 256 break case '128th': // divisionsFromNote = (noteDuration / 4) * 128; multiple = 128 break case '64th': // divisionsFromNote = (noteDuration / 4) * 64; multiple = 64 break case '32nd': // divisionsFromNote = (noteDuration / 4) * 32; multiple = 32 break case '16th': // divisionsFromNote = (noteDuration / 4) * 16; multiple = 16 break case 'eighth': // divisionsFromNote = (noteDuration / 4) * 8; multiple = 8 break case 'quarter': multiple = 4 break case 'half': // divisionsFromNote = (noteDuration / 4) * 2; multiple = 2 break case 'whole': // divisionsFromNote = (noteDuration / 4); multiple = 1 break case 'breve': // divisionsFromNote = (noteDuration / 4) / 2; multiple = 0.5 break case 'long': // divisionsFromNote = (noteDuration / 4) / 4; multiple = 0.25 break case 'maxima': // divisionsFromNote = (noteDuration / 4) / 8; multiple = 0.125 break default: break } return (denominator / multiple) * bpm } /** 获取第一个小节的节拍速度,替换初始的速度 */ export const getFirstBpmByMeasures = (measures: SourceMeasure[]) => { for (const item of measures) { if (item.TempoInBPM || (item as any).tempoInBPM) { return getMeasureRealBpm(item) } } return 90 } /** * 根据小节获取当前节拍下真实的速度 * 如: 6/8拍 4分音符bpm速度为120 则计算出8分音符的速度为120 * 2 */ export const getMeasureRealBpm = (measure: SourceMeasure) => { let bpm = measure.TempoInBPM let tempoExpression = null let activeTimeSignature: Fraction | null = null for (const expression of measure?.TempoExpressions) { if (expression?.InstantaneousTempo.beatUnit) { // 取最后一个有效的tempo tempoExpression = expression activeTimeSignature = measure.ActiveTimeSignature } } if (tempoExpression && activeTimeSignature) { bpm = tempoExpression?.InstantaneousTempo.TempoInBpm // let multiple = 1 // console.log(tempoExpression.InstantaneousTempo.beatUnit) bpm = getTimeByBeatUnit(tempoExpression.InstantaneousTempo.beatUnit, bpm, activeTimeSignature.Denominator) } return bpm } export const getEnvHostname = () => { if (location.origin.indexOf('online') > -1) { return 'https://mstuonline.dayaedu.com' } else if (location.origin.indexOf('dev') > -1) { return 'http://mstudev.dayaedu.com' } return 'https://mstutest.dayaedu.com' } export const getTvIconUrl = () => { if (location.origin.indexOf('online') > -1) { return 'https://mteaonline.dayaedu.com/#/guide' } else if (location.origin.indexOf('dev') > -1) { return 'http://mteadev.dayaedu.com/#/guide' } return 'https://mteatest.dayaedu.com/#/guide' } export const setPrefix = (url: string): string => { if (url) { return '?' + url } return '' } export type InitXmlInfo = { title?: string } export const formatXML = (xml: string, initInfo?: InitXmlInfo): string => { if (!xml) return '' const xmlParse = new DOMParser().parseFromString(xml, 'text/xml') const measures = xmlParse.getElementsByTagName('measure') let beats = -1 let beatType = -1 /** 创建默认速度标记,避免无速度导致问题 */ // const defaultSpeedTag = document.createElement('direction') // const defaultSpeedTagString = ` // // quarter // 100 // // // 1 // 1` // defaultSpeedTag.innerHTML = defaultSpeedTagString // if (xmlParse.getElementsByTagName('per-minute').length === 0) { // measures[0]?.insertAdjacentElement('afterbegin', defaultSpeedTag) // } // 小节中如果没有节点默认为休止符 for (const measure of measures) { if (beats === -1 && measure.getElementsByTagName('beats').length) { beats = parseInt(measure.getElementsByTagName('beats')[0].textContent || '4') } if (beatType === -1 && measure.getElementsByTagName('beat-type').length) { beatType = parseInt(measure.getElementsByTagName('beat-type')[0].textContent || '4') } // if (speed === -1 && measure.getElementsByTagName('per-minute').length) { // speed = parseInt(measure.getElementsByTagName('per-minute')[0].textContent || this.firstLib?.speed) // } const divisions = parseInt(measure.getElementsByTagName('divisions')[0]?.textContent || '256') if (measure.getElementsByTagName('note').length === 0) { const forwardTimeElement = measure.getElementsByTagName('forward')[0]?.getElementsByTagName('duration')[0] if (forwardTimeElement) { forwardTimeElement.textContent = '0' } measure.innerHTML = measure.innerHTML + ` ${divisions * beats} 1 whole ` } } if (initInfo) { const workTitle = xmlParse.querySelector('work-title') if (workTitle && initInfo.title) { workTitle.textContent = initInfo.title } } return new XMLSerializer().serializeToString(xmlParse) } export type CustomInfo = { showSpeed: boolean parsedXML: string code: string } /** 从xml中获取自定义信息,并删除多余的字符串 */ export const getCustomInfo = (xml: string): CustomInfo => { const data = { showSpeed: true, parsedXML: xml, code: '', } const xmlParse = new DOMParser().parseFromString(xml, 'text/xml') const words = xmlParse.getElementsByTagName('words') for (const word of words) { if (word && word.textContent?.trim() === '隐藏速度') { data.showSpeed = false word.textContent = '' } if (word && word.textContent?.trim() === '@') { word.textContent = 'segno' } } data.parsedXML = new XMLSerializer().serializeToString(xmlParse) data.code = xmlParse.querySelector('part-name')?.innerHTML || '' return data } /** * 替换文本标签中的内容 */ const replaceTextConent = (beforeText: string, afterText: string, ele: Element): Element => { const words = ele?.getElementsByTagName('words') for (const word of words) { if (word && word.textContent?.trim() === beforeText) { word.textContent = afterText } } return ele } /** * 添加第一分谱信息至当前分谱 * @param ele 需要插入的元素 * @param fitstParent 合奏谱第一个分谱 * @param parent 需要添加的分谱 */ const setElementNoteBefore = (ele: Element, fitstParent: Element, parent?: Element | null) => { let noteIndex: number = 0 if (!fitstParent) { return } for (let index = 0; index < fitstParent.childNodes.length; index++) { const element = fitstParent.childNodes[index] if (element.nodeName === 'note') { noteIndex++ } if (element === ele) { break } } if (noteIndex === 0 && parent) { parent.insertBefore(ele, parent.childNodes[0]) return } if (parent && parent.childNodes.length > 0) { let noteIndex2: number = 0 for (let index = 0; index < parent.childNodes.length; index++) { const element = parent.childNodes[index] if (element.nodeName === 'note') { noteIndex2 = noteIndex2 + 1 if (noteIndex2 === noteIndex) { parent.insertBefore(ele, element) break } } } } // console.log(noteIndex, parent) } /** * 检查传入文字是否为重复关键词 * @param text 总谱xml * @returns 是否是重复关键词 */ export const isRepeatWord = (text: string): boolean => { if (text) { const innerText = text.toLocaleLowerCase() const dsRegEx: string = 'd\\s?\\.s\\.' const dcRegEx: string = 'd\\.\\s?c\\.' return ( innerText === '@' || StringUtil.StringContainsSeparatedWord(innerText, dsRegEx + ' al fine', true) || StringUtil.StringContainsSeparatedWord(innerText, dsRegEx + ' al coda', true) || StringUtil.StringContainsSeparatedWord(innerText, dcRegEx + ' al fine', true) || StringUtil.StringContainsSeparatedWord(innerText, dcRegEx + ' al coda', true) || StringUtil.StringContainsSeparatedWord(innerText, dcRegEx) || StringUtil.StringContainsSeparatedWord(innerText, 'da\\s?capo', true) || StringUtil.StringContainsSeparatedWord(innerText, dsRegEx, true) || StringUtil.StringContainsSeparatedWord(innerText, 'dal\\s?segno', true) || StringUtil.StringContainsSeparatedWord(innerText, 'al\\s?coda', true) || StringUtil.StringContainsSeparatedWord(innerText, 'to\\s?coda', true) || StringUtil.StringContainsSeparatedWord(innerText, 'a (la )?coda', true) || StringUtil.StringContainsSeparatedWord(innerText, 'fine', true) || StringUtil.StringContainsSeparatedWord(innerText, 'coda', true) || StringUtil.StringContainsSeparatedWord(innerText, 'segno', true) ) } return false } export const onlyVisible = (xml: string, partIndex: number): string => { if (!xml) return '' const xmlParse = new DOMParser().parseFromString(xml, 'text/xml') const partList = xmlParse.getElementsByTagName('part-list')?.[0]?.getElementsByTagName('score-part') || [] const partListNames = Array.from(partList).map( (item) => item.getElementsByTagName('part-name')?.[0].textContent || '' ) const parts = xmlParse.getElementsByTagName('part') || [] if (!parts.length) { // throw new Error('') return '' } // const firstTimeInfo = parts[0]?.getElementsByTagName('metronome')[0]?.parentElement?.parentElement?.cloneNode(true) const firstMeasures = [...parts[0]?.getElementsByTagName('measure')] const metronomes = [...parts[0]?.getElementsByTagName('metronome')] const words = [...parts[0]?.getElementsByTagName('words')] const rehearsals = [...parts[0]?.getElementsByTagName('rehearsal')] /** 第一分谱如果是约定的配置分谱则跳过 */ if (partListNames[0]?.toLocaleUpperCase?.() === "COMMON") { partIndex++; partListNames.shift(); } const visiblePartInfo = partList[partIndex] state.partListNames = partListNames if (visiblePartInfo) { const id = visiblePartInfo.getAttribute('id') Array.from(parts).forEach((part) => { if (part && part.getAttribute('id') !== id) { part.parentNode?.removeChild(part) // 不等于第一行才添加避免重复添加 } else if (part && part.getAttribute('id') !== 'P1') { // 速度标记仅保留最后一个 const metronomeData: { [key in string]: Element } = {} for (let i = 0; i < metronomes.length; i++) { const metronome = metronomes[i] const metronomeContainer = metronome.parentElement?.parentElement?.parentElement if (metronomeContainer) { const index = firstMeasures.indexOf(metronomeContainer) metronomeData[index] = metronome } } Object.values(metronomeData).forEach((metronome) => { const metronomeContainer = metronome.parentElement?.parentElement const parentMeasure = metronomeContainer?.parentElement const measureMetronomes = [...(parentMeasure?.childNodes || [])] const metronomesIndex = metronomeContainer ? measureMetronomes.indexOf(metronomeContainer) : -1 // console.log(parentMeasure) if (parentMeasure && metronomesIndex > -1) { const index = firstMeasures.indexOf(parentMeasure) const activeMeasure = part.getElementsByTagName('measure')[index] setElementNoteBefore(metronomeContainer, parentMeasure, activeMeasure) // console.log(measureMetronomes, metronomesIndex, activeMeasure?.childNodes, activeMeasure?.childNodes[metronomesIndex]) // activeMeasure?.insertBefore(metronomeContainer.cloneNode(true), activeMeasure?.childNodes[metronomesIndex]) // // part.getElementsByTagName('measure')[index]?.appendChild(metronomeContainer.cloneNode(true)) // // console.log(index, parentMeasure, firstMeasures.indexOf(parentMeasure)) } }) /** word比较特殊需要精确到note位置 */ words.forEach((word) => { const text = word.textContent || '' if ( (isSpecialMark(text) || isSpeedKeyword(text) || isGradientWords(text) || isRepeatWord(text) || GRADIENT_SPEED_RESET_TAG) && text ) { const wordContainer = word.parentElement?.parentElement const parentMeasure = wordContainer?.parentElement const measureWords = [...(parentMeasure?.childNodes || [])] const wordIndex = wordContainer ? measureWords.indexOf(wordContainer) : -1 if (wordContainer && parentMeasure && wordIndex > -1) { const index = firstMeasures.indexOf(parentMeasure) const activeMeasure = part.getElementsByTagName('measure')[index] setElementNoteBefore(wordContainer, parentMeasure, activeMeasure) // activeMeasure?.insertBefore(wordContainer.cloneNode(true), activeMeasure?.childNodes[wordIndex]) } } }) rehearsals.forEach((rehearsal) => { const container = rehearsal.parentElement?.parentElement const parentMeasure = container?.parentElement // console.log(rehearsal) if (parentMeasure) { const index = firstMeasures.indexOf(parentMeasure) part.getElementsByTagName('measure')[index]?.appendChild(container.cloneNode(true)) // console.log(index, parentMeasure, firstMeasures.indexOf(parentMeasure)) } }) } // 最后一个小节的结束线元素不在最后 调整 if (part && part.getAttribute('id') === id) { const barlines = part.getElementsByTagName('barline') const lastParent = barlines[barlines.length - 1]?.parentElement if (lastParent?.lastElementChild?.tagName !== 'barline') { const children = lastParent?.children || [] for (let el of children) { if (el.tagName === 'barline') { // 将结束线元素放到最后 lastParent?.appendChild(el) break } } } } }) Array.from(partList).forEach((part) => { if (part && part.getAttribute('id') !== id) { part.parentNode?.removeChild(part) } }) // 处理装饰音问题 const notes = xmlParse.getElementsByTagName('note') const getNextvNoteDuration = (i: number) => { let nextNote = notes[i + 1] // 可能存在多个装饰音问题,取下一个非装饰音时值 for (let index = i; index < notes.length; index++) { const note = notes[index] if (!note.getElementsByTagName('grace')?.length) { nextNote = note break } } const nextNoteDuration = nextNote?.getElementsByTagName('duration')[0] return nextNoteDuration } Array.from(notes).forEach((note, i) => { const graces = note.getElementsByTagName('grace') if (graces && graces.length) { // if (i !== 0) { note.appendChild(getNextvNoteDuration(i)?.cloneNode(true)) // } } }) } // console.log(new XMLSerializer().serializeToString(xmlParse)) return new XMLSerializer().serializeToString(xmlParse) } // 倚音后连音线 export const appoggianceFormate = (xml: string): string => { if (!xml) return xml const xmlParse = new DOMParser().parseFromString(xml, 'text/xml') const graces = xmlParse.querySelectorAll('grace') if (!graces.length) return xml const getNextElement = (el: HTMLElement): HTMLElement => { if (el.querySelector('grace')) { return getNextElement(el?.nextElementSibling as HTMLElement) } return el } for (let grace of graces) { const notations = grace.parentElement?.querySelector('notations') if (notations && notations.querySelectorAll('slur').length > 1) { let nextEle: Element = getNextElement(grace.parentElement?.nextElementSibling as HTMLElement) if (nextEle && nextEle.querySelectorAll('slur').length > 0) { const slurNumber = Array.from(nextEle.querySelector('notations')?.children || []).map((el: Element) => { return el.getAttribute('number') }) const slurs = notations.querySelectorAll('slur') for (let nota of slurs) { if (!slurNumber.includes(nota.getAttribute('number'))) { nextEle.querySelector('notations')?.appendChild(nota) } } } } } return new XMLSerializer().serializeToString(xmlParse) } export const getVoicePartInfo = () => { const { MusicalInstrumentClassification, chinesePartName } = appState let subjectId = -1 const { partListNames, partIndex } = state const filterPartNames = partListNames.filter((item) => (item || '').trim() !== '') if (filterPartNames.length) { for (const Classification of Object.entries(MusicalInstrumentClassification)) { const [key, value] = Classification as [string, string[]] const activePart = partListNames[partIndex] // console.log({activePart, value, partListNames}) const filterValue = value.filter((item) => item && activePart.indexOf(item || '') > -1) if (activePart && (filterValue.length || value.includes(activePart))) { if (!isNaN(+key)) { subjectId = +key } return { realPartListNames: partListNames, subjectId: subjectId, partListNames: value, partName: activePart, chinesePartName: chinesePartName[activePart] || activePart, } } } } return { subjectId: subjectId, partListNames: [], } } /** 根据ID获取最顶级ID */ export const isWithinScope = (tree: any[], id: number): number => { if (!tree) return 0 let result = 0 for (const item of tree) { if (item.id === id) { result = item.id break } if (item.sysMusicScoreCategoriesList) { result = isWithinScope(item.sysMusicScoreCategoriesList, id) if (result > 0) { result = item.id } if (result) break } } return result } /** * 特殊教材分类id */ export const classids = [1, 30] /** * 指定id是否在给定的类别中 * @param tree * @param id * @param parentInTree default: false */ export const idIsInClassIds = (tree: any[], id: number, parentInTree = false): boolean => { if (!tree) return false let result = false for (const item of tree) { // console.log(id, item.id, (parentInTree || classids.includes(item.id))) if (item.id === id && (parentInTree || classids.includes(item.id))) { result = true break } if (item.sysMusicScoreCategoriesList) { result = idIsInClassIds(item.sysMusicScoreCategoriesList, id, parentInTree || classids.includes(item.id)) if (result) break } } // console.log('result', result) return result } /** * 获取图片地址 * @param src 图片地址 * @returns */ const getImageSize = (src: string): Promise => { return new Promise((resolve, reject) => { const img = new Image() img.src = src img.onload = () => { resolve(img) } img.onerror = (err) => reject(err) }) }