|
@@ -0,0 +1,343 @@
|
|
|
|
+import "./index.less"
|
|
|
|
+import state from "/src/state"
|
|
|
|
+import { getAudioCurrentTime } from "/src/view/audio-list"
|
|
|
|
+
|
|
|
|
+type rectNotesPosType = { x: number; y: number; width: number; MeasureNumberXML: number; frequency: number; noteId: number }[]
|
|
|
|
+type intonationLineType = {
|
|
|
|
+ canvasDom: null | HTMLCanvasElement
|
|
|
|
+ canvasCtx: null | undefined | CanvasRenderingContext2D
|
|
|
|
+ canvasDomWith: number
|
|
|
|
+ canvasDomHeight: number
|
|
|
|
+ intonationLineBoxDom: null | HTMLElement
|
|
|
|
+ osmdCanvasPageDom: null | HTMLElement
|
|
|
|
+ intonationLineBotDom: null | HTMLElement
|
|
|
|
+ musicLineDom: null | HTMLCanvasElement
|
|
|
|
+ rectNotesPos: rectNotesPosType
|
|
|
|
+ frequencyAv: number
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+export const intonationLineState = {
|
|
|
|
+ canvasDom: null,
|
|
|
|
+ canvasCtx: null,
|
|
|
|
+ canvasDomWith: 0,
|
|
|
|
+ canvasDomHeight: 80,
|
|
|
|
+ intonationLineBoxDom: null,
|
|
|
|
+ osmdCanvasPageDom: null,
|
|
|
|
+ intonationLineBotDom: null,
|
|
|
|
+ musicLineDom: null,
|
|
|
|
+ rectNotesPos: [], // 音符 音准线
|
|
|
|
+ frequencyAv: -1
|
|
|
|
+} as intonationLineType
|
|
|
|
+
|
|
|
|
+const progressMusicData = {
|
|
|
|
+ frequency: -1,
|
|
|
|
+ progressMusic: []
|
|
|
|
+} as {
|
|
|
|
+ frequency: number
|
|
|
|
+ progressMusic: { noteId: number; x: number; y: number; width: number; status: "start" | "ing" | "end" }[]
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * 初始化 音准器
|
|
|
|
+ */
|
|
|
|
+export function initIntonationLine() {
|
|
|
|
+ // 创建dom
|
|
|
|
+ createIntonationLine()
|
|
|
|
+ // 根据音符获取坐标
|
|
|
|
+ intonationLineState.rectNotesPos = getRectNotesPosByBatePos()
|
|
|
|
+ console.log("rectNotesPos:", intonationLineState.rectNotesPos)
|
|
|
|
+ const { musicLineDom } = drawMusicLine()
|
|
|
|
+ intonationLineState.musicLineDom = musicLineDom
|
|
|
|
+ // 画初始进度
|
|
|
|
+ drawProgressectMusic()
|
|
|
|
+ // 鸟的初始位置
|
|
|
|
+ intonationLineState.intonationLineBotDom && (intonationLineState.intonationLineBotDom.style.left = `${intonationLineState.rectNotesPos[0].x}px`)
|
|
|
|
+ console.log(intonationLineState, "评测小鸟数据")
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// 模拟修改值
|
|
|
|
+setInterval(() => {
|
|
|
|
+ progressMusicData.frequency = getRandomNumberBetween(600, 800)
|
|
|
|
+}, 10)
|
|
|
|
+function getRandomNumberBetween(min: number, max: number) {
|
|
|
|
+ return Math.floor(Math.random() * (max - min + 1)) + min
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * 根据播放时间进度移动处理
|
|
|
|
+ */
|
|
|
|
+export function moveInitIntonationLineByPlayTime() {
|
|
|
|
+ const currentTime = getAudioCurrentTime()
|
|
|
|
+ if (currentTime <= state.fixtime) return
|
|
|
|
+ if (currentTime > state.times.last()?.endtime) return
|
|
|
|
+ // 当休止小节,可能当前音符在谱面上没有实际的音符(没有bbox),所以往后找谱面上有的音符
|
|
|
|
+ let nextIndex = state.activeNoteIndex + 1
|
|
|
|
+ let nextBBox = state.times[nextIndex]?.bbox
|
|
|
|
+ while (!nextBBox && nextIndex < state.times.length) {
|
|
|
|
+ nextIndex += 1
|
|
|
|
+ nextBBox = state.times[nextIndex]?.bbox
|
|
|
|
+ }
|
|
|
|
+ // 当前的音符和下一个音符之间的时值 (当是最后一个音符的时候,下一个音符的时间取当前音符的endtime)
|
|
|
|
+ const noteDuration =
|
|
|
|
+ (nextIndex > state.times.length - 1 ? state.times[state.activeNoteIndex]?.endtime : state.times[nextIndex].time) -
|
|
|
|
+ state.times[state.activeNoteIndex]?.time
|
|
|
|
+ // 当前时值在该区间的占比
|
|
|
|
+ const playProgress = (currentTime - state.times[state.activeNoteIndex]?.time) / noteDuration
|
|
|
|
+ // 当前的进度的位置信息
|
|
|
|
+ const noteDistance =
|
|
|
|
+ nextIndex > state.times.length - 1
|
|
|
|
+ ? intonationLineState.rectNotesPos[intonationLineState.rectNotesPos.length - 1].width
|
|
|
|
+ : state.times[nextIndex].bbox.x - state.times[state.activeNoteIndex].bbox.x
|
|
|
|
+ const affectDistance = noteDistance * playProgress
|
|
|
|
+ const translateXNum = affectDistance + state.times[state.activeNoteIndex].bbox.x - state.times[0].bbox.x
|
|
|
|
+ intonationLineState.osmdCanvasPageDom && (intonationLineState.osmdCanvasPageDom.style.transform = `translateX(-${translateXNum}px)`)
|
|
|
|
+ // 更新音准器小鸟的位置信息
|
|
|
|
+ updateBotByFrequency()
|
|
|
|
+ // 更新音准器数据
|
|
|
|
+ updateProgressMusicData(affectDistance)
|
|
|
|
+}
|
|
|
|
+/**
|
|
|
|
+ * 根据频率更新小鸟的位置信息
|
|
|
|
+ */
|
|
|
|
+function updateBotByFrequency() {
|
|
|
|
+ const frequency = progressMusicData.frequency
|
|
|
|
+ let translateYNum = 0
|
|
|
|
+ if (frequency !== -1) {
|
|
|
|
+ const y =
|
|
|
|
+ intonationLineState.canvasDomHeight / 2 -
|
|
|
|
+ (((frequency - intonationLineState.frequencyAv) / intonationLineState.frequencyAv) * intonationLineState.canvasDomHeight) / 2
|
|
|
|
+ if (y >= 0 && y <= intonationLineState.canvasDomHeight) {
|
|
|
|
+ translateYNum = intonationLineState.canvasDomHeight - y
|
|
|
|
+ } else if (y > intonationLineState.canvasDomHeight) {
|
|
|
|
+ translateYNum = 0
|
|
|
|
+ } else if (y < 0) {
|
|
|
|
+ translateYNum = intonationLineState.canvasDomHeight
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ intonationLineState.intonationLineBotDom && (intonationLineState.intonationLineBotDom.style.transform = `translateY(-${translateYNum}px)`)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * 根据 频率更新音准器数据
|
|
|
|
+ * @affectDistance 偏移的距离
|
|
|
|
+ */
|
|
|
|
+const affectNum = 50
|
|
|
|
+function updateProgressMusicData(affectDistance: number) {
|
|
|
|
+ const { frequency, progressMusic } = progressMusicData
|
|
|
|
+ const { activeNoteIndex, times } = state
|
|
|
|
+ const activeNote = times[activeNoteIndex]
|
|
|
|
+ // 最后
|
|
|
|
+ const lastProgressmusic = progressMusic.length ? progressMusic[progressMusic.length - 1] : null
|
|
|
|
+ if (frequency + affectNum > activeNote.frequency && frequency - affectNum < activeNote.frequency) {
|
|
|
|
+ const y =
|
|
|
|
+ intonationLineState.canvasDomHeight / 2 -
|
|
|
|
+ (((activeNote.frequency - intonationLineState.frequencyAv) / intonationLineState.frequencyAv) * intonationLineState.canvasDomHeight) / 2
|
|
|
|
+ if (lastProgressmusic) {
|
|
|
|
+ // 有值的时候先看是不是当前小节
|
|
|
|
+ if (lastProgressmusic.noteId === activeNote.noteId) {
|
|
|
|
+ if (lastProgressmusic.status === "end") {
|
|
|
|
+ // 当前小节 新增
|
|
|
|
+ progressMusicData.progressMusic.push({
|
|
|
|
+ noteId: activeNote.noteId,
|
|
|
|
+ x: activeNote.bbox.x + affectDistance,
|
|
|
|
+ y,
|
|
|
|
+ width: 0,
|
|
|
|
+ status: "start"
|
|
|
|
+ })
|
|
|
|
+ } else {
|
|
|
|
+ // 更新处理
|
|
|
|
+ lastProgressmusic.status = "ing"
|
|
|
|
+ lastProgressmusic.width = activeNote.bbox.x + affectDistance - lastProgressmusic.x
|
|
|
|
+ }
|
|
|
|
+ } else if (lastProgressmusic.status !== "end") {
|
|
|
|
+ // 不是当前小节 并且不为end时候
|
|
|
|
+ lastProgressmusic.status = "end"
|
|
|
|
+ // 下一个 小节 新增
|
|
|
|
+ progressMusicData.progressMusic.push({
|
|
|
|
+ noteId: activeNote.noteId,
|
|
|
|
+ x: activeNote.bbox.x + affectDistance,
|
|
|
|
+ y,
|
|
|
|
+ width: 0,
|
|
|
|
+ status: "start"
|
|
|
|
+ })
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ // 增加新值
|
|
|
|
+ progressMusicData.progressMusic.push({
|
|
|
|
+ noteId: activeNote.noteId,
|
|
|
|
+ x: activeNote.bbox.x + affectDistance,
|
|
|
|
+ y,
|
|
|
|
+ width: 0,
|
|
|
|
+ status: "start"
|
|
|
|
+ })
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ // 没有匹配上的时候 关闭最后一个
|
|
|
|
+ if (lastProgressmusic && lastProgressmusic.status !== "end") {
|
|
|
|
+ lastProgressmusic.status = "end"
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ // 更新音准器
|
|
|
|
+ drawProgressectMusic()
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * 画音准线进度
|
|
|
|
+ */
|
|
|
|
+function drawProgressectMusic() {
|
|
|
|
+ const canvasCtx = intonationLineState.canvasCtx!
|
|
|
|
+ const canvasDom = intonationLineState.canvasDom!
|
|
|
|
+ canvasCtx.clearRect(0, 0, canvasDom.width, canvasDom.height)
|
|
|
|
+ canvasCtx.drawImage(intonationLineState.musicLineDom!, 0, 0)
|
|
|
|
+ console.log(progressMusicData.progressMusic, 888)
|
|
|
|
+ // 过滤掉宽度为0的数据
|
|
|
|
+ const filterProgressMusic = progressMusicData.progressMusic.filter(item => {
|
|
|
|
+ return item.width !== 0
|
|
|
|
+ })
|
|
|
|
+ drawPathRectByArr(canvasCtx!, filterProgressMusic, "#FFC121")
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * 画音准线
|
|
|
|
+ */
|
|
|
|
+function drawMusicLine() {
|
|
|
|
+ const musicLineDom = document.createElement("canvas")
|
|
|
|
+ musicLineDom.width = intonationLineState.canvasDomWith
|
|
|
|
+ musicLineDom.height = intonationLineState.canvasDomHeight
|
|
|
|
+ const musicLineCtx = musicLineDom.getContext("2d")!
|
|
|
|
+ musicLineCtx.clearRect(0, 0, musicLineDom.width, musicLineDom.height)
|
|
|
|
+ // 画矩形
|
|
|
|
+ drawPathRectByArr(musicLineCtx, intonationLineState.rectNotesPos, "rgba(255, 255, 255, 0.3)")
|
|
|
|
+ return {
|
|
|
|
+ musicLineDom,
|
|
|
|
+ musicLineCtx
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * 创建dom
|
|
|
|
+ */
|
|
|
|
+function createIntonationLine() {
|
|
|
|
+ // osmdCanvasPage
|
|
|
|
+ const osmdCanvasPageDom = document.querySelector("#osmdCanvasPage1") as HTMLElement
|
|
|
|
+ intonationLineState.osmdCanvasPageDom = osmdCanvasPageDom
|
|
|
|
+ // box
|
|
|
|
+ const intonationLineBoxDom = document.createElement("div")
|
|
|
|
+ intonationLineBoxDom.className = "intonationLineBox"
|
|
|
|
+ intonationLineState.intonationLineBoxDom = intonationLineBoxDom
|
|
|
|
+ // con
|
|
|
|
+ const intonationLineConDom = document.createElement("div")
|
|
|
|
+ intonationLineConDom.className = "intonationLineCon"
|
|
|
|
+ //canvas
|
|
|
|
+ const intonationLineCanvasDom = document.createElement("canvas")
|
|
|
|
+ intonationLineCanvasDom.className = "intonationLineCanvas"
|
|
|
|
+ intonationLineState.canvasDom = intonationLineCanvasDom
|
|
|
|
+ intonationLineState.canvasDomWith = osmdCanvasPageDom?.offsetWidth | 0
|
|
|
|
+ intonationLineCanvasDom.width = intonationLineState.canvasDomWith
|
|
|
|
+ intonationLineCanvasDom.height = intonationLineState.canvasDomHeight
|
|
|
|
+ intonationLineState.canvasCtx = intonationLineCanvasDom.getContext("2d")
|
|
|
|
+ // bot
|
|
|
|
+ const intonationLineBotDom = document.createElement("div")
|
|
|
|
+ intonationLineBotDom.className = "intonationLineBot"
|
|
|
|
+ intonationLineState.intonationLineBotDom = intonationLineBotDom
|
|
|
|
+ document.querySelector("#musicAndSelection")?.appendChild(intonationLineBotDom)
|
|
|
|
+ intonationLineConDom.appendChild(intonationLineCanvasDom)
|
|
|
|
+ intonationLineBoxDom.appendChild(intonationLineConDom)
|
|
|
|
+ // 添加到 osmdCanvasPage1
|
|
|
|
+ osmdCanvasPageDom?.insertBefore(intonationLineBoxDom, osmdCanvasPageDom.firstChild)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * 根据音符获取坐标
|
|
|
|
+ */
|
|
|
|
+function getRectNotesPosByBatePos(): rectNotesPosType {
|
|
|
|
+ let totalAvInde = 0
|
|
|
|
+ // 取平均值
|
|
|
|
+ const totalAv =
|
|
|
|
+ state.times.reduce((total, item) => {
|
|
|
|
+ if (item.frequency !== -1) {
|
|
|
|
+ // -1 为休止符
|
|
|
|
+ total += item.frequency
|
|
|
|
+ totalAvInde++
|
|
|
|
+ }
|
|
|
|
+ return total
|
|
|
|
+ }, 0) / totalAvInde
|
|
|
|
+ intonationLineState.frequencyAv = totalAv
|
|
|
|
+ const notesPos = state.times.reduce((notesArr: any[], item) => {
|
|
|
|
+ // 当休止小节,可能当前音符在谱面上没有实际的音符(没有bbox),所以往后找谱面上有的音符
|
|
|
|
+ if (item.bbox) {
|
|
|
|
+ notesArr.push({
|
|
|
|
+ noteId: item.noteId,
|
|
|
|
+ frequency: item.frequency,
|
|
|
|
+ MeasureNumberXML: item.MeasureNumberXML,
|
|
|
|
+ x: item.bbox.x,
|
|
|
|
+ // 当为休止符的时候 取最下面的位置*0.9,确保能显示完整
|
|
|
|
+ y:
|
|
|
|
+ intonationLineState.canvasDomHeight / 2 -
|
|
|
|
+ ((((item.frequency === -1 ? 2 * totalAv * 0.1 : item.frequency) - totalAv) / totalAv) * intonationLineState.canvasDomHeight) / 2
|
|
|
|
+ // cavans 高度为160 所以基准为80
|
|
|
|
+ })
|
|
|
|
+ }
|
|
|
|
+ return notesArr
|
|
|
|
+ }, [])
|
|
|
|
+ // 最后一个音符延长(这里建立一个虚拟的音符延长)
|
|
|
|
+ const extendPoint = {
|
|
|
|
+ ...notesPos[notesPos.length - 1]
|
|
|
|
+ }
|
|
|
|
+ extendPoint.MeasureNumberXML++
|
|
|
|
+ extendPoint.frequency = -1
|
|
|
|
+ // 当总长度减30小于最后一个音符时候,取最后一个音符加15
|
|
|
|
+ extendPoint.x = intonationLineState.canvasDomWith - 30 > extendPoint.x ? intonationLineState.canvasDomWith - 30 : extendPoint.x + 15
|
|
|
|
+ notesPos.push(extendPoint)
|
|
|
|
+ /* 计算音准线 */
|
|
|
|
+ return notesPos.reduce(
|
|
|
|
+ (
|
|
|
|
+ arr: { MeasureNumberXML: number; frequency: number; x: number; y: number; width: number; noteId: number }[],
|
|
|
|
+ { frequency, x, y, MeasureNumberXML, noteId },
|
|
|
|
+ index
|
|
|
|
+ ) => {
|
|
|
|
+ const canvasDataLen = notesPos.length
|
|
|
|
+ if (index < canvasDataLen - 1) {
|
|
|
|
+ const nextCanvasObj = notesPos[index + 1]
|
|
|
|
+ arr.push({
|
|
|
|
+ noteId,
|
|
|
|
+ MeasureNumberXML,
|
|
|
|
+ frequency,
|
|
|
|
+ x,
|
|
|
|
+ y,
|
|
|
|
+ width: nextCanvasObj.x - x
|
|
|
|
+ })
|
|
|
|
+ }
|
|
|
|
+ return arr
|
|
|
|
+ },
|
|
|
|
+ []
|
|
|
|
+ )
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * 根据坐标信息 画矩形路径
|
|
|
|
+ */
|
|
|
|
+function drawPathRectByArr(ctx: CanvasRenderingContext2D, arr: { x: number; y: number; width: number }[], color: string) {
|
|
|
|
+ arr.map(({ x, y, width }) => {
|
|
|
|
+ drawRect(ctx, x, y, width * 0.92, 7, 4, color)
|
|
|
|
+ })
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * 画矩形
|
|
|
|
+ */
|
|
|
|
+function drawRect(ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, radius: number, color: string) {
|
|
|
|
+ ctx.fillStyle = color
|
|
|
|
+ ctx.beginPath()
|
|
|
|
+ ctx.moveTo(x + radius, y)
|
|
|
|
+ ctx.lineTo(x + width - radius, y)
|
|
|
|
+ ctx.arcTo(x + width, y, x + width, y + radius, radius)
|
|
|
|
+ ctx.lineTo(x + width, y + height - radius)
|
|
|
|
+ ctx.arcTo(x + width, y + height, x + width - radius, y + height, radius)
|
|
|
|
+ ctx.lineTo(x + radius, y + height)
|
|
|
|
+ ctx.arcTo(x, y + height, x, y + height - radius, radius)
|
|
|
|
+ ctx.lineTo(x, y + radius)
|
|
|
|
+ ctx.arcTo(x, y, x + radius, y, radius)
|
|
|
|
+ ctx.closePath()
|
|
|
|
+ ctx.fill()
|
|
|
|
+}
|