/** * 曲谱节拍器 * auth: lsq * time: 2022.11.14 */ import { reactive, watch } from "vue"; import { tickUrl as tick, tockUrl as tock } from "/src/constant/audios"; import { browser } from "/src/utils/index"; import state from "/src/state"; import { Howl } from "howler"; import tockAndTick from "/src/constant/tockAndTick.json"; import tickWav from "/src/assets/tick.mp3"; import tockWav from "/src/assets/tock.mp3"; import { audioData as audioDataState } from "../view/audio-list" type IOptions = { speed: number; }; const browserInfo = browser(); let tipsTimer: any = null; // 光标提示定时器 // HTMLAudioElement 音频 const audioData = reactive({ tick: null as unknown as HTMLAudioElement, tock: null as unknown as HTMLAudioElement, }); let tickTockPlayTime = 0; export const metronomeData = reactive({ disable: true, initPlayerState: false, lineShow: false, isClick: false, metro: null as unknown as Metronome, metroList: [] as number[], activeList: [] as number[], metroMeasure: [] as any[], activeIndex: null as any, activeMetro: {} as any, cursorMode: 2 as number, // 光标模式:1:音符指针;2:节拍指针;3:关闭指针 cursorTips: '' as string, // 光标模式提示文字 followAudioIndex: 1, // 当前的拍数 totalNumerator: 2, // 总拍数 firstBeatTypeArr:[] as number[] // 第一小节的节拍 }); watch( () => metronomeData.cursorMode, () => { const img: HTMLElement = document.querySelector("#cursorImg-0")!; if (img) { switch (metronomeData.cursorMode) { case 1: img.classList.remove("lineHide"); img.style.opacity = 'inherit' metronomeData.cursorTips = '您已切换到指针跟随音符播放'; img.style.opacity = 'inherit' break; case 2: img.classList.add("lineHide"); img.style.opacity = 'inherit' metronomeData.cursorTips = '您已切换到指针跟随节拍播放'; // console.log('光标',img) break; case 3: img.style.opacity = '0' metronomeData.cursorTips = '您已关闭指针显示'; // console.log('隐藏光标') break; default: break; } hideCursorTip() } } ); // 切换隐藏光标 const toggleLine = () => { if (!metronomeData.lineShow) return; const img: HTMLElement = document.querySelector("#cursorImg-0")!; if (img) { if (state.times[state.activeNoteIndex].multipleRestMeasures) { img.classList.remove("lineHide"); } else { img.classList.add("lineHide"); } } }; watch( () => metronomeData.lineShow, () => { const img: HTMLElement = document.querySelector("#cursorImg-0")!; if (img) { if (metronomeData.lineShow) { img.classList.add("lineHide"); } else { img.classList.remove("lineHide"); } } } ); class Metronome { playType = "tick"; source = null as any; // 创建音频源头 source1 = null as any; source2 = null as any; constructor(option?: IOptions) {} init(times: any[]) { this.calculation(times); metronomeData.activeList = []; this.initPlayer() } initPlayer() { // if (!this.source1) { // this.source1 = this.loadAudio1(); // } // if (!this.source2) { // this.source2 = this.loadAudio2(); // } // metronomeData.initPlayerState = true; if(metronomeData.initPlayerState) return Promise.all([this.createAudio(tickWav), this.createAudio(tockWav)]).then( ([tick, tock]) => { if (tick) { audioData.tick = tick; } if (tock) { audioData.tock = tock; } metronomeData.initPlayerState = true; } ); } createAudio = (src: string): Promise => { return new Promise((resolve) => { // const a = new Audio(src + '?v=' + Date.now()); const a = new Audio(src); a.load(); a.onloadedmetadata = () => { resolve(a); }; a.onerror = () => { resolve(null); }; }); }; // 播放 sound = (currentTime: number) => { let index = -1; let activeMetro = -1; for (let i = 0; i < metronomeData.metroList.length; i++) { const item = metronomeData.metroList[i]; if (currentTime >= item) { // console.log(currentTime , item) index = i; activeMetro = item; } else { break; } } if (index > -1 && metronomeData.activeIndex !== index) { metronomeData.activeIndex = index; // console.log("播放", metronomeData.activeIndex); metronomeData.activeMetro = this.getStep(activeMetro); // console.log("🚀 ~ metronomeData.activeMetro",metronomeData.activeMetro.measureNumberIndex, metronomeData.activeMetro.index) tickTockPlayTime = currentTime this.playAudio(); metronomeData.isClick = false; return; } // toggleLine() metronomeData.isClick = false; }; // 暂停的时候,点击音符,需要找到对应的节拍器的位置 findMetronomePosition = (currentTime: number) => { console.log('取消选段1111',currentTime) const originTime = currentTime; // if (!state.sectionStatus){ // currentTime = setCurrentTime(currentTime); // } let index = -1; let activeMetro = -1; for (let i = 0; i < metronomeData.metroList.length; i++) { const item = metronomeData.metroList[i]; if (currentTime >= item) { // console.log(currentTime , item) index = i; activeMetro = item; } else { break; } } if (index > -1 && metronomeData.activeIndex !== index) { metronomeData.activeIndex = index; // console.log("节拍器播放", '节拍器索引:',metronomeData.activeIndex, '节拍器时间:',currentTime, '实际时间:',originTime,'xml计算的时间:',metronomeData.metroList[metronomeData.activeIndex]); metronomeData.activeMetro = this.getStep(activeMetro); console.log("🚀 ~ metronomeData.activeMetro",metronomeData.activeMetro.measureNumberIndex, metronomeData.activeMetro.index) tickTockPlayTime = currentTime // this.playAudio(); metronomeData.isClick = false; return; } metronomeData.isClick = false; if (currentTime === 0) { metronomeData.activeMetro = {} } }; // 播放 playAudio = () => { // 关闭定时器节拍器 return /* 如果是 评测模式且不为MIDI并且节拍器资源加载成功的时候 不运行节拍器播放*/ if (state.modeType === "practise" && state.playMode !== "MIDI") { if(state.playType === "play" && state.playSource === "music" && audioDataState.songCollection.beatSongEle){ return } if(state.playType === "play" && state.playSource === "background" && audioDataState.songCollection.beatBackgroundEle){ return } if(state.playType === "sing" && state.playSource === "music" && audioDataState.songCollection.beatFanSongEle){ return } if(state.playType === "sing" && state.playSource === "background" && audioDataState.songCollection.beatBanSongEle){ return } if(state.playType === "sing" && state.playSource === "mingSong" && audioDataState.songCollection.beatMingSongEle){ return } } // console.log("播放自带的节拍器 233333") if (!metronomeData.initPlayerState || state.playState === 'paused') return; const beatVolume = state.setting.beatVolume / 100 // this.source = metronomeData.activeMetro?.index === 0 ? this.source1 : this.source2; // this.source.volume(metronomeData.disable || state.playState === 'paused' ? 0 : beatVolume); // Audio 播放音频 this.source = metronomeData.activeMetro?.index === 0 ? audioData.tick : audioData.tock; this.source.volume = metronomeData.disable ? 0 : beatVolume; if (this.source.volume <= 0) { this.source.muted = true } else { this.source.muted = false } // console.log('节拍器播放的时间',tickTockPlayTime) this.source.play(); }; /** * 跟练模式播放,跟练模式没有曲子音频播放器 */ simulatePlayAudio = () => { // console.log(333, metronomeData.followAudioIndex) if (!metronomeData.initPlayerState) return; const beatVolume = state.setting.beatVolume / 100 // this.source = metronomeData.followAudioIndex === 1 ? this.source1 : this.source2; // Audio 播放音频 this.source = metronomeData.followAudioIndex === 1 ? audioData.tick : audioData.tock; // this.source.volume(metronomeData.disable ? 0 : beatVolume); this.source.volume = metronomeData.disable ? 0 : beatVolume /** * https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLMediaElement/volume * volume属性在部分ios手机的Safari浏览器不被支持 */ if (this.source.volume <= 0) { this.source.muted = true } else { this.source.muted = false } // console.log('音量',this.source,this.source.volume) this.source.play(); metronomeData.followAudioIndex += 1; metronomeData.followAudioIndex = metronomeData.followAudioIndex > metronomeData.totalNumerator ? 1 : metronomeData.followAudioIndex; }; // 切换 selectPlay() {} loadAudio1 = () => { return new Howl({ src: tockAndTick.tick, // 如果是ios手机,需要强制使用audio,不然部分系统版本第一次播放没有声音 // html5: browserInfo.ios, }); }; loadAudio2 = () => { return new Howl({ src: tockAndTick.tock, // html5: browserInfo.ios, }); }; getStep(time: number) { for (let i = 0; i < metronomeData.metroMeasure.length; i++) { const list = metronomeData.metroMeasure[i]; const item = list.find((n: any) => n.time === time); if (item) { // console.log('index',item) return item; } } return {}; } // 计算 所有的拍子的时间 calculation(times: any[]) { // console.log("🚀 ~ times", times); // 1.统计有多少小节 const measures: any[] = []; let xmlNumber = -1; let isDiff = false //弱起时间不够补的时候 for (let i = 0; i < times.length; i++) { const note = times[i]; const measureNumberXML = note.MeasureNumberXML; // console.log("🚀 ~ note?.noteElement?.sourceMeasure", note?.noteElement?.sourceMeasure) // console.log("🚀 ~ measureNumberXML", measureNumberXML, note) // console.log("🚀 ~ measureNumberXML", note) const measureListIndex = measureNumberXML - 1; // 当渐快渐慢的时候 不播节拍器 if(isWithinRange(state.gradual, measureListIndex)){ xmlNumber = measureNumberXML; continue } if (measureNumberXML > -1) { if (measureNumberXML != xmlNumber) { // 弱起的时候 根据音符结尾时间减去音符开头时间,得到的不是正常小节的时间,然后平均分配节拍之后,当前节拍间隔会非常短 这里弱起取整个小节的时间 // 当小节时间 减去音符时间,前奏没有预留时间时候,从歌词开始唱的那里开始响节拍器 let startTime = note.measures[0].time if(i === 0 && note.measures[0].difftime>0){ startTime = note.measures[note.measures.length - 1].endtime - note.measures[0].measureLength if(startTime < 0){ isDiff = true } } if(isDiff) { // 当前小节有歌词,开放弱起节拍器 let isLyric = false let noteIndex = 0 while(!isLyric && noteIndex endtime ? time + noteLengthTime : endtime }else{ if(Math.abs(nextNoteStartTime - endtime)*1000< 10){ // 当首位本来就是相连的 noteEndTime = endtime }else{ // 当首位不相连,差值大于这个音符的持续时间的时候取这个音符的时间(防止有间奏的连起来时间太长),否则直接取后一个音符的开始时间 noteEndTime = nextNoteStartTime - time > noteLengthTime ? time + noteLengthTime : nextNoteStartTime } } const m = { measureNumberXML: measureNumberXML, measureNumberIndex: measureListIndex, CompoundTempo: note?.noteElement?.sourceMeasure?.CompoundTempo || "", numerator: note?.noteElement?.sourceMeasure?.ActiveTimeSignature?.numerator || 0, denominator: note?.noteElement?.sourceMeasure?.ActiveTimeSignature?.denominator || 0, start: startTime, end: noteEndTime, time: noteEndTime - startTime, stave_x: note?.noteElement?.sourceMeasure?.verticalMeasureList?.[0]?.stave?.x || 0, end_x: note?.stave?.end_x || 0 || 0, stepList: [] as number[], svgs: [] as any[], isRestFlag: note.isRestFlag, }; /** * bug:#9877 * 多分轨合并显示,不同分轨的音符数量可能不同 */ let measureArr = note.measures; if (state.isCombineRender) { measureArr = measureArr.filter((item: any) => item.MeasureNumberXML === m.measureNumberXML) } m.stepList = calculateMetroStep(measureArr, m); measures.push(m); xmlNumber = measureNumberXML; } } } //console.log(measures, measures.length,'小节汇总'); let metroList: number[] = []; const metroMeasure: any[] = []; console.log("节拍器 每一小节时间:", measures) console.log("节拍器 间隔:", measures.map(item => { return { time: item.time, measureNumberXML: item.measureNumberXML } })) try { for (let i = 0; i < measures.length; i++) { const measure = measures[i]; // 87拍和45拍要根据小节返回的CompoundTempo特殊处理 const beatTypeArr = getBeatTypeArr(measure.numerator, measure.denominator, measure.CompoundTempo) const CompoundTempoArr = beatTypeArr.map((beatType:number) => { return Math.abs(beatType*measure.numerator) }) if(i===0){ metronomeData.firstBeatTypeArr = beatTypeArr } metroMeasure[i] = [] as number[]; // 根据有几个拍子,划分成几份 const widthStep = 100 / (beatTypeArr.length+1); // 当前拍子的组合数(2+3+2,3+2)中的数字 let beatNum = 0; // if (measure.measureNumberXML == 98) { // debugger // } for (let j = 0; j < beatTypeArr.length; j++) { // 累加 const beatMuit = Array(j).fill("").reduce((num:number,v:any,i:number) => { return num += Math.abs(beatTypeArr[i]) }, 0) || 0 const time = measure.time * beatMuit + measure.start; metroList.push(time); let left = ""; // 当前拍子数对应的节拍位置索引 let currentIdx = 0; if (j == 0) { currentIdx = 0 } else { beatNum += CompoundTempoArr[j-1]; currentIdx = beatNum } // 如果是87拍,并且是3+2+2的组合,第二拍的节拍指针需要定位到第四个音符的位置 // if (measure.numerator === 7 && measure.denominator === 8 && measure.CompoundTempo === '3+2+2' && j === 1) { // currentIdx += 1; // } if (measure.stepList[currentIdx]) { left = measure.stepList[currentIdx] + "px"; } else { const preLeft = measure.stepList[j - 1]; left = !preLeft || preLeft.toString().indexOf("%") > -1 ? `${widthStep*(j+1)}%` : `${preLeft}px + ${widthStep}%`; measure.stepList[j] = left; } metroMeasure[i].push({ isTick: beatTypeArr[j] < 0, index: j, time, left: left?.indexOf("%") > -1 ? `calc(${left})` : left, measureNumberXML: measure.measureNumberXML, isRestFlag: measure.isRestFlag, stepList: measure.stepList }); } } } catch (error) { console.log(error); } console.log('节拍器',metroList, metroMeasure); // 5.得到所有的节拍时间 metronomeData.metroList = metroList; metronomeData.metroMeasure = metroMeasure; // console.log(9999,metroList,7777,metroMeasure) metronomeData.activeMetro = metroMeasure[0]?.[0] || {}; } } /** 获取节拍类型数组 */ export function getBeatTypeArr(numerator?:number, denominator?:number, CompoundTempo?:string){ const speedBeatUnit = state.speedBeatUnit const Numerator = numerator || state.osmd?.Sheet?.SheetPlaybackSetting?.Rhythm?.Numerator || 4 const Denominator = denominator || state.osmd?.Sheet?.SheetPlaybackSetting?.Rhythm?.Denominator || 4 let loopArr = [] // 规则 负数代表重声,正数代表轻声 switch (`${Numerator}/${Denominator}`) { case "2/2": loopArr = [-1/2, 1/2] break; case "3/2": loopArr = [-1/3, 1/3, 1/3] break; case "5/4": //5/4拍根据谱面的CompoundTempo来特殊处理 if(CompoundTempo==="2+3"){ loopArr = [-1/5, 1/5, -1/5, 1/5, 1/5] }else{ loopArr = [-1/5, 1/5, 1/5, -1/5, 1/5] } break; case "3/8": // 3/8拍 速度为浮点4分音符时候特殊处理 if(speedBeatUnit==="1/4."){ loopArr = [-1/1] }else{ loopArr = [-1/3, 1/3, 1/3] } break; case "6/8": loopArr = [-1/2, 1/2] break; case "7/8": //7/8拍根据谱面的CompoundTempo来特殊处理 if(CompoundTempo==="2+2+3"){ loopArr = [-2/7, 2/7, 3/7] }else if(CompoundTempo==="2+3+2"){ loopArr = [-2/7, 3/7, 2/7] }else{ loopArr = [-3/7, 2/7, 2/7] } break; case "9/8": loopArr = [-3/9, 3/9, 3/9] break; default: loopArr.push(-1/Numerator); for (let i = 1; i < Numerator; i++) { loopArr.push(1/Numerator); } break; } // console.log(loopArr, "loopArr") return loopArr } // 计算拍子的时值 function calculateMetroStep(arr: any[], m: any): number[] { const measureLength = arr.reduce((total: number, item: any) => { total += item._noteLength; return total; }, 0); const clap = measureLength / m.numerator; if (arr.length === 1) { const wholeNote = arr[0].svgElement; if (wholeNote && !wholeNote.isRest()) { const measure_bbox = wholeNote?.attrs?.el?.parentElement?.parentElement?.getBoundingClientRect?.() || { x: 0, right: 0 }; let bbox = wholeNote?.attrs?.el?.getBoundingClientRect?.() || { x: 0 }; let stepWidth = Math.abs(measure_bbox.right - bbox.x) / m.numerator; let stepList: number[] = []; for (let i = 0; i < m.numerator; i++) { stepList.push(bbox.x - measure_bbox.x + i * stepWidth); } // console.log("🚀 ~ stepList:", stepList, m.measureNumberXML) return stepList; } try { // 开头是休止符 if (m.measureNumberXML === 1 && wholeNote && wholeNote.isRest()) { const measure_bbox = wholeNote?.attrs?.el?.parentElement?.parentElement?.getBoundingClientRect?.() || { x: 0, right: 0 }; let bbox = wholeNote?.attrs?.el?.getBoundingClientRect?.() || { x: 0 }; let stepWidth = Math.abs(measure_bbox.right - bbox.x) / m.numerator; let stepList: any[] = []; // 第一小节是休止符,节拍指针应该等分宽度 const widthStep = 100 / (m.numerator + 1); // for (let i = -1; i < m.numerator - 1; i++) { // stepList.push(bbox.x - measure_bbox.x + i * stepWidth); // } // for (let i = 1; i <= m.numerator; i++) { // stepList.push(widthStep * i + '%'); // } // console.log(wholeNote?.attrs?.el, m.measureNumberXML) // console.log("🚀 ~ stepList:", stepList, m.measureNumberXML) return stepList; } } catch (error) { console.log("🚀 ~ error:", error); } return []; } // console.log("🚀 ~ arr", [...arr],`小节总时值: ${measureLength}`, clap, m.measureNumberXML); let totalLength = 0; let notes: any[] = []; let stepList: number[] = []; for (let i = 0; i < arr.length; i++) { const item = arr[i]; item.index = i; const noteLength = item._noteLength; totalLength += noteLength; // 大于一拍 const exceedStep = Math.floor(totalLength / clap); // console.log(`note`, item?.svgElement?.attrs?.el,notes.length,{noteLength, exceedStep,clap}, m.measureNumberXML) if (exceedStep >= 1) { totalLength -= clap; // 一拍 let measure_bbox = item?.svgElement?.attrs?.el?.parentElement?.parentElement?.getBoundingClientRect?.() || { x: 0 }; /** * bug: #9875 * 简谱,渲染有点问题,先使用vf-stave的位置 */ if (state.musicRenderType !== "staff") { measure_bbox = item?.svgElement?.attrs?.el?.parentElement?.parentElement?.querySelector('.vf-stave')?.getBoundingClientRect?.() || { x: 0 }; } /** * 如果measure_bbox不存在(多分轨合并显示可能会出现),则用note再获取一次 */ if (!measure_bbox.width && notes.length > 0) { measure_bbox = state.musicRenderType !== "staff" ? notes[0]?.svgElement?.attrs?.el?.parentElement?.parentElement?.querySelector('.vf-stave')?.getBoundingClientRect?.() || { x: 0 } : notes[0]?.svgElement?.attrs?.el?.parentElement?.parentElement?.getBoundingClientRect?.() || { x: 0 } } if (notes.length > 0) { let bbox = notes[0]?.svgElement?.attrs?.el?.getBoundingClientRect?.() || { x: 0 }; let x: any = bbox.x - measure_bbox.x; if (notes[0]._noteLength / clap >= 1) { const nextNote = arr[notes[0].index + 1]?.svgElement?.attrs?.el?.getBoundingClientRect?.() || { x: measure_bbox.right } || { x: 0 }; const stepWidth = Math.abs(bbox.x - nextNote.x) / 2; x = bbox.x - measure_bbox.x + stepWidth; // console.log(`音符超一拍`, notes[0]?.svgElement?.attrs?.el, arr[notes[0].index + 1]?.svgElement?.attrs?.el, bbox.x - nextNote.x, stepWidth, m.measureNumberXML); } // console.log(`一拍`, notes[0]?.svgElement?.attrs?.el, m.measureNumberXML, notes[0]._noteLength , clap, 'aa') stepList.push(x); } else { let bbox = item?.svgElement?.attrs?.el?.getBoundingClientRect?.() || { x: 0 }; let x: any = bbox.x - measure_bbox.x; // console.log(`一拍`, item?.svgElement?.attrs?.el, m.measureNumberXML) stepList.push(x); } notes = []; let bbox = item?.svgElement?.attrs?.el?.getBoundingClientRect?.() || { x: 0 }; let x: any = bbox.x - measure_bbox.x; let stepWidth = 0; if (exceedStep > 1) { // 二拍以上 const nextNote = arr[i + 1]?.svgElement?.attrs?.el?.getBoundingClientRect?.() || { x: measure_bbox.right } || { x: 0 }; stepWidth = Math.abs(bbox.x - nextNote.x) / exceedStep; // console.log("二拍以上 ~ nextNote:",bbox.x , nextNote.x,stepWidth, item?.svgElement?.attrs?.el,arr[i + 1]?.svgElement?.attrs?.el, exceedStep); } for (let j = 1; j < exceedStep; j++) { totalLength -= clap; // console.log(`超一拍`,item?.svgElement?.attrs?.el, m.measureNumberXML) stepList.push(x + stepWidth * j); } } //有时值就将音符加入 if (totalLength > Number.EPSILON && totalLength > 0) { notes.push(item); } } stepList = stepList.reduce((list: any[], n: number) => { if (list.includes(n)) { list.push(undefined as any); } else { list.push(n); } return list; }, []); // console.log("stepList", [...stepList], m.measureNumberXML); return stepList; } // 延迟兼容处理 function setCurrentTime(time: number) { if (browserInfo.huawei || browserInfo.xiaomi) { time += 0.125; } else if (browserInfo.android) { time += 0.11; } else if (browserInfo.ios) { time += 0.01; } return time; } // 自动隐藏光标提示 function hideCursorTip() { if (!tipsTimer) { tipsTimer = setTimeout(() => { metronomeData.cursorTips = '' clearTimeout(tipsTimer) tipsTimer = null }, 2000); } else { clearTimeout(tipsTimer) tipsTimer = setTimeout(() => { metronomeData.cursorTips = '' clearTimeout(tipsTimer) tipsTimer = null }, 2000); } } function isWithinRange(ranges:any[], index:number) { for (const range of ranges) { const start = range[0].measureIndex; const end = range[1].measureIndex; if (index >= start && index < end) { return true; } } return false; } export default Metronome;