import { reactive, watch } from "vue"; import { tickUrl as tick, tockUrl as tock } from "/src/constant/audios"; import { browser } from "./utils"; import state from "../pages/detail/state"; type IOptions = { speed: number; }; const ac = window.AudioContext || (window as any).webkitAudioContext || (window as any).mozAudioContext || (window as any).msAudioContext; const browserInfo = browser(); let tipsTimer: any = null; // 光标提示定时器 export const metronomeData = reactive({ disable: true, lineShow: false, isClick: false, metro: null as unknown as Metronome, metroList: [] as number[], activeList: [] as number[], metroMeasure: [] as any[], activeIndex: null as unknown as number, activeMetro: {} as any, cursorMode: 2 as number, // 光标模式:1:音符指针;2:节拍指针;3:关闭指针 cursorTips: '' as string, // 光标模式提示文字 }); 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() } } ); class Metronome { ctx = new ac(); 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 = []; return new Promise(async (resolve) => { if (this.source1 && this.source2) return resolve(true); this.source1 = await this.loadAudio1(); this.source2 = await this.loadAudio2(); resolve(true); }); } // 播放 sound = (currentTime: number) => { // console.log("🚀 ~ currentTime", currentTime) // 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); metronomeData.activeMetro = this.getStep(activeMetro); // console.log("🚀 ~ 节拍metronomeData.activeMetro",metronomeData.activeMetro.measureNumberIndex, metronomeData.activeMetro.index, metronomeData.activeMetro) this.playAudio(); metronomeData.isClick = false; return; } metronomeData.isClick = false; }; // 播放 playAudio = () => { this.source = this.ctx.createBufferSource(); this.source.buffer = metronomeData.activeMetro?.index === 0 ? this.source1 : this.source2; const gainNode = this.ctx.createGain(); gainNode.gain.value = metronomeData.disable ? 0 : 0.4; this.source.connect(gainNode); gainNode.connect(this.ctx.destination); this.source.start(0); //立即播放 }; // 切换 selectPlay() {} loadAudio1 = async () => { const audioUrl = tick; // "/tick.wav"; const res = await fetch(audioUrl); const arrayBuffer = await res.arrayBuffer(); // byte array字节数组 // console.log("🚀 ~ arrayBuffer", arrayBuffer) const audioBuffer = await this.ctx.decodeAudioData(arrayBuffer, function (decodeData) { return decodeData; }); return audioBuffer; }; loadAudio2 = async () => { const audioUrl = tock; //"/tock.wav"; const res = await fetch(audioUrl); const arrayBuffer = await res.arrayBuffer(); // byte array字节数组 const audioBuffer = await this.ctx.decodeAudioData(arrayBuffer, function (decodeData) { return decodeData; }); return audioBuffer; }; 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; for (let i = 0; i < times.length; i++) { const note = times[i]; const measureNumberXML = note?.noteElement?.sourceMeasure?.measureNumber + 1 || -1; // console.log("🚀 ~ note?.noteElement?.sourceMeasure", note?.noteElement?.sourceMeasure) // console.log("🚀 ~ measureNumberXML", measureNumberXML, note) // console.log("🚀 ~ measureNumberXML", note) const measureListIndex = note?.noteElement?.sourceMeasure?.measureListIndex; if (measureNumberXML > -1) { if (measureNumberXML != xmlNumber) { const m = { measureNumberXML: measureNumberXML, measureNumberIndex: measureListIndex, numerator: note?.noteElement?.sourceMeasure?.ActiveTimeSignature?.numerator || 0, start: note.measures[0].time, end: note.measures[note.measures.length - 1].endtime, time: note.measures[note.measures.length - 1].endtime - note.measures[0].time, stave_x: note?.noteElement?.sourceMeasure?.verticalMeasureList?.[0]?.stave?.x || 0, end_x: (note?.stave?.end_x || 0) || 0, stepList: [] as number[], svgs: [] as any[], }; // 2.统计小节的拍数 // 3.统计小节的时长, 开始时间,结束时间 // console.log(measureNumberXML,note.measures, times.filter((n: any) => n?.noteElement?.sourceMeasure?.measureListIndex == measureListIndex)) if ([121].includes(state.subjectId)) { const _measures = times.filter((n: any) => n?.noteElement?.sourceMeasure?.measureListIndex == measureListIndex); note.measures = _measures; m.start = note.measures[0].time; m.end = note.measures[note.measures.length - 1].endtime; m.time = note.measures[note.measures.length - 1].endtime - note.measures[0].time; try { const tickables = note.noteElement.sourceMeasure.verticalMeasureList.reduce((arr: any[], value: any) => { arr.push(...value.vfVoices["1"].tickables); return arr; }, []); const xList: any[] = []; m.svgs = tickables .map((n: any) => { const x = n.getBoundingBox().x; if (!xList.includes(x) && n.duration !== "w") { xList.push(x); n._start_x = x; return n; } }) .filter(Boolean) .sort((a: any, b: any) => a._start_x - b._start_x); // console.log(measureNumberXML, m.svgs) } catch (error) { console.log(error); } m.stepList = calculateMutilpleMetroStep(note.measures, m); } else { m.stepList = calculateMetroStep(note.measures, m); } measures.push(m); xmlNumber = measureNumberXML; } } } console.log(measures, measures.length,6667); let metroList: number[] = []; const metroMeasure: any[] = []; // 4.按照拍数将时长平均分配 try { for (let i = 0; i < measures.length; i++) { const measure = measures[i]; const noteStep = measure.time / measure.numerator; // console.log("🚀 ~ measure.measureNumberXML",measure.measureNumberXML, noteStep) const WIDTH = [121].includes(state.subjectId) ? 95 : 100; const widthStep = WIDTH / (measure.numerator + 1); metroMeasure[i] = [] as number[]; // console.log('stepList', [...measure.stepList], measure.measureNumberXML) for (let j = 0; j < measure.numerator; j++) { const time = noteStep * j + measure.start; metroList.push(time); let left = ""; if (measure.stepList[j] === -1 || (measure.measureNumberXML === 1 && !measure.stepList[j])) { continue } if (measure.stepList[j]) { left = measure.stepList[j] + "px"; } else { const preLeft = measure.stepList[j - 1]; left = !preLeft ? `${widthStep}%` : preLeft.toString().indexOf("%") > -1 ? `${preLeft} + ${widthStep}%` : `${preLeft}px + ${widthStep}%`; measure.stepList[j] = left; } metroMeasure[i].push({ index: j, time, // left: (measure.stepList[j] ? measure.stepList[j] + 'px' : (j + 1) * widthStep + '%'), left: left?.indexOf("%") > -1 ? `calc(${left})` : left, measureNumberXML: measure.measureNumberXML, }); } } } catch (error) { console.log(error); } // console.log(metroList, metroMeasure); // 5.得到所有的节拍时间 metronomeData.metroList = metroList; metronomeData.metroMeasure = metroMeasure; } } // 计算拍子的时值 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].svgElelent 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++){ // 是第一个小节,并且不是全音符,是弱起 if (m.measureNumberXML === 1 && wholeNote.duration !== "w") { stepList.push(i === 0 ? bbox.x - measure_bbox.x : -1) } else { 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: number[] = []; for(let i = -1; i < m.numerator - 1; i++){ stepList.push(bbox.x - measure_bbox.x + i * stepWidth) } // 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 = totalLength / clap; // console.log(`note`, item?.svgElelent?.attrs?.el,notes.length,{noteLength, exceedStep,clap}, m.measureNumberXML) if (exceedStep >= 1) { totalLength -= clap; // 一拍 const measure_bbox = item?.svgElelent?.attrs?.el?.parentElement?.parentElement?.getBoundingClientRect?.() || { x: 0 }; if (notes.length > 0) { let bbox = notes[0]?.svgElelent?.attrs?.el?.querySelector('.vf-note')?.getBoundingClientRect?.() || { x: 0 }; let x: any = bbox.x - measure_bbox.x; if ((notes[0]._noteLength / clap) >= 1) { const nextNote = arr[notes[0].index + 1]?.svgElelent?.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]?.svgElelent?.attrs?.el, arr[notes[0].index + 1]?.svgElelent?.attrs?.el, bbox.x - nextNote.x, stepWidth, m.measureNumberXML); } // console.log(`一拍`, notes[0]?.svgElelent?.attrs?.el, m.measureNumberXML, notes[0]._noteLength , clap, 'aa') stepList.push(x); } else { let bbox = item?.svgElelent?.attrs?.el?.querySelector('.vf-note')?.getBoundingClientRect?.() || { x: 0 }; let x: any = bbox.x - measure_bbox.x // console.log(`一拍`, item?.svgElelent?.attrs?.el, m.measureNumberXML) stepList.push(x); } notes = []; let bbox = item?.svgElelent?.attrs?.el?.querySelector('.vf-note')?.getBoundingClientRect?.() || { x: 0 }; let x: any = bbox.x - measure_bbox.x; let stepWidth = 0; if (exceedStep > 1) { // 二拍以上 const nextNote = arr[i + 1]?.svgElelent?.attrs?.el?.querySelector('.vf-note')?.getBoundingClientRect?.() || { x: measure_bbox.right } || { x: 0 }; stepWidth = Math.abs(bbox.x - nextNote.x) / Math.ceil(exceedStep); // console.log("二拍以上 ~ nextNote:",bbox.x , nextNote.x,stepWidth, item?.svgElelent?.attrs?.el,arr[i + 1]?.svgElelent?.attrs?.el, exceedStep); } for (let j = 1; j < exceedStep; j++) { totalLength -= clap; // console.log(`超一拍`,item?.svgElelent?.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); // for (let i in stepList) { // stepList[i] = stepList[i] / state.musicZoom // } // console.log('🚀 ~ stepList:',stepList) return stepList; } // 计算单声部多声轨的拍子的时值 function calculateMutilpleMetroStep(arr: any[], m: any): number[] { // console.log("🚀 ~ m:", [...m.svgs]) const step = m.time / m.numerator; const measure_bbox = arr[0]?.svgElelent?.attrs?.el?.parentElement?.parentElement?.getBoundingClientRect?.() || { x: 0 }; if (arr.length === 1) { const staveNote = m.svgs[0]; // 大于一拍 let bbox = staveNote?.attrs?.el?.getBoundingClientRect?.() || { x: 0 }; if (staveNote && !staveNote.isRest()) { return [bbox.x - measure_bbox.x]; } return []; } // console.log("🚀 ~ arr", arr, step, m.measureNumberXML); let total = 0; let notes: any[] = []; let stepList: number[] = []; for (let i = 0; i < arr.length; i++) { const item = arr[i]; item._index = i; const noteTime = item.endtime - item.time; total += noteTime; let svgEle = m.svgs[i]?.attrs?.el; // 大于一拍 let bbox = svgEle?.getBoundingClientRect?.() || { x: 0 }; // console.log(m.measureNumberXML, svgEle, i) if (noteTime > step) { total -= step; // console.log('超过一拍了', notes, m.measureNumberXML) let x = bbox.x - measure_bbox.x; if (notes.length > 0) { svgEle = m.svgs[notes[0]._index]?.attrs?.el; bbox = svgEle?.getBoundingClientRect?.() || { x: 0 }; x = bbox.x - measure_bbox.x; } stepList.push(x); notes = []; } else { notes.push(item); } // console.log(notes) if (Math.abs(total - step) < 0.001) { let x = bbox.x - measure_bbox.x; if (notes.length > 0) { svgEle = m.svgs[notes[0]._index]?.attrs?.el; bbox = svgEle?.getBoundingClientRect?.() || { x: 0 }; x = bbox.x - measure_bbox.x; } // console.log("一拍",svgEle,notes,m.svgs, m.measureNumberXML); stepList.push(x); total = 0; notes = []; } } stepList = stepList.reduce((list: any[], n: number) => { if (list.includes(n)) { list.push(undefined as any); } else { list.push(n); } return list; }, []); //Array.from(new Set(stepList)) // console.log('stepList', stepList, m.measureNumberXML) // for (let i in stepList) { // stepList[i] = stepList[i] / state.musicZoom // } // console.log('🚀 ~ stepList:',stepList) 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); } } export default Metronome;