|
@@ -7,7 +7,7 @@ import { getQuery } from "/src/utils/queryString";
|
|
import backIcon from "./image/back_icon.png";
|
|
import backIcon from "./image/back_icon.png";
|
|
import videoIcon from "./image/video_icon.png";
|
|
import videoIcon from "./image/video_icon.png";
|
|
import { api_musicPracticeRecordDetail } from "../api";
|
|
import { api_musicPracticeRecordDetail } from "../api";
|
|
-import { buildFreqTable, frequencyToNote } from "/src/utils/frequency"
|
|
|
|
|
|
+import { buildFreqTable, frequencyToNote, calculateNoteRange, getAbsoluteIndex } from "/src/utils/frequency"
|
|
|
|
|
|
|
|
|
|
export default defineComponent({
|
|
export default defineComponent({
|
|
@@ -34,10 +34,13 @@ export default defineComponent({
|
|
openRealPitch: false,
|
|
openRealPitch: false,
|
|
xmlInfo: {} as any,
|
|
xmlInfo: {} as any,
|
|
xmlNotes: [],
|
|
xmlNotes: [],
|
|
- realMaxFrequency: 0, // 实际的最高音(xml和用户演奏的取高值)
|
|
|
|
- realMinFrequency: 0, // 实际的最低音(xml和用户演奏的取低值)
|
|
|
|
|
|
+ xmlMaxFrequency: 0, // 实际的最高音(xml和用户演奏的取高值)
|
|
|
|
+ xmlMinFrequency: 0, // 实际的最低音(xml和用户演奏的取低值)
|
|
|
|
+ useMaxFrequency: 0, // 演奏最高频率
|
|
|
|
+ useMinFrequency: 0, // 演奏最高频率
|
|
measureList: [] as any, // 构造的小节数组
|
|
measureList: [] as any, // 构造的小节数组
|
|
timeChunkAnalysisList: [] as any, // 用户实际演奏的音符数据
|
|
timeChunkAnalysisList: [] as any, // 用户实际演奏的音符数据
|
|
|
|
+ baseNoteList: [] as any, // xml标准音符
|
|
});
|
|
});
|
|
|
|
|
|
// 根据音符构造小节信息
|
|
// 根据音符构造小节信息
|
|
@@ -51,16 +54,18 @@ export default defineComponent({
|
|
// 音符开始和结束时间
|
|
// 音符开始和结束时间
|
|
let startTime = 0, endTime = 0;
|
|
let startTime = 0, endTime = 0;
|
|
// xml的最高音和最低音
|
|
// xml的最高音和最低音
|
|
- let xmlMaxFrequency = 0, xmlMinFrequency = 0;
|
|
|
|
|
|
+ let xmlMaxFrequency = 0, xmlMinFrequency = notes.find(item => item.frequency > 0).frequency;
|
|
// 上一个小节的节拍数,如果相邻的小节节拍数相同,则不重复添加
|
|
// 上一个小节的节拍数,如果相邻的小节节拍数相同,则不重复添加
|
|
let preMeasureTimeSignature = null;
|
|
let preMeasureTimeSignature = null;
|
|
for (let index = 0; index < notes.length; index++) {
|
|
for (let index = 0; index < notes.length; index++) {
|
|
const note = notes[index];
|
|
const note = notes[index];
|
|
- const { measureRenderIndex, frequencyList, dontEvaluating, duration, timeSignature = {numerator: 2, denominator: 4} } = note;
|
|
|
|
|
|
+ const { measureRenderIndex, frequencyList, frequency, dontEvaluating, duration, timeSignature = {numerator: 2, denominator: 4} } = note;
|
|
startTime = endTime;
|
|
startTime = endTime;
|
|
endTime = endTime + duration / 1000
|
|
endTime = endTime + duration / 1000
|
|
xmlMaxFrequency = Math.max(...frequencyList,xmlMaxFrequency)
|
|
xmlMaxFrequency = Math.max(...frequencyList,xmlMaxFrequency)
|
|
- xmlMinFrequency = Math.min(...frequencyList,xmlMinFrequency)
|
|
|
|
|
|
+ if (frequency > 0) {
|
|
|
|
+ xmlMinFrequency = Math.min(...frequencyList,xmlMinFrequency)
|
|
|
|
+ }
|
|
if (index === 0) {
|
|
if (index === 0) {
|
|
preMeasureTimeSignature = `${timeSignature.numerator}` + '/' + `${timeSignature.denominator}`;
|
|
preMeasureTimeSignature = `${timeSignature.numerator}` + '/' + `${timeSignature.denominator}`;
|
|
preMeasureIndex = measureRenderIndex;
|
|
preMeasureIndex = measureRenderIndex;
|
|
@@ -77,7 +82,7 @@ export default defineComponent({
|
|
timeSignature,
|
|
timeSignature,
|
|
timeSignatureDiff: true,
|
|
timeSignatureDiff: true,
|
|
noteList,
|
|
noteList,
|
|
- measureDuration: 1
|
|
|
|
|
|
+ measureDuration: 2
|
|
})
|
|
})
|
|
} else {
|
|
} else {
|
|
if (preMeasureIndex === measureRenderIndex) {
|
|
if (preMeasureIndex === measureRenderIndex) {
|
|
@@ -108,14 +113,29 @@ export default defineComponent({
|
|
timeSignature: timeSignature,
|
|
timeSignature: timeSignature,
|
|
timeSignatureDiff: currentTimeSignature !== preMeasureTimeSignature ? true: false,
|
|
timeSignatureDiff: currentTimeSignature !== preMeasureTimeSignature ? true: false,
|
|
noteList,
|
|
noteList,
|
|
- measureDuration: 1
|
|
|
|
|
|
+ measureDuration: 2
|
|
})
|
|
})
|
|
preMeasureTimeSignature = currentTimeSignature
|
|
preMeasureTimeSignature = currentTimeSignature
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+ // 评测的音符才添加,不评测的音符不需要绘制基准音符
|
|
|
|
+ if (!dontEvaluating) {
|
|
|
|
+ frequencyList.forEach((item: any) => {
|
|
|
|
+ if (item !== -1) {
|
|
|
|
+ scoreData.baseNoteList.push({
|
|
|
|
+ freq: item,
|
|
|
|
+ startTime: startTime * 1000,
|
|
|
|
+ endTime: endTime * 1000
|
|
|
|
+ })
|
|
|
|
+ }
|
|
|
|
+ })
|
|
|
|
+ }
|
|
}
|
|
}
|
|
// console.log(222,measureList)
|
|
// console.log(222,measureList)
|
|
scoreData.measureList = measureList;
|
|
scoreData.measureList = measureList;
|
|
|
|
+ scoreData.xmlMaxFrequency = Math.max(xmlMaxFrequency, scoreData.useMaxFrequency);
|
|
|
|
+ scoreData.xmlMinFrequency = Math.min(xmlMinFrequency, scoreData.useMinFrequency);
|
|
}
|
|
}
|
|
|
|
|
|
const getRecordDetail = async () => {
|
|
const getRecordDetail = async () => {
|
|
@@ -127,35 +147,27 @@ export default defineComponent({
|
|
resultData = eval('(' + res?.data?.scoreData + ')');
|
|
resultData = eval('(' + res?.data?.scoreData + ')');
|
|
scoreData.xmlInfo = eval('(' + JSON.parse(res.data.scoreData)?.musicalNotesPlayStats?.musicXmlBasicInfo + ')') || {}
|
|
scoreData.xmlInfo = eval('(' + JSON.parse(res.data.scoreData)?.musicalNotesPlayStats?.musicXmlBasicInfo + ')') || {}
|
|
scoreData.xmlNotes = scoreData.xmlInfo.musicXmlInfos || []
|
|
scoreData.xmlNotes = scoreData.xmlInfo.musicXmlInfos || []
|
|
- scoreData.timeChunkAnalysisList = resultData?.musicalNotesPlayStats?.timeChunkAnalysisList || []
|
|
|
|
|
|
+ scoreData.useMinFrequency = resultData?.musicalNotesPlayStats?.timeChunkAnalysisList?.find((item: any) => item.baseFrequency > 0).baseFrequency
|
|
|
|
+ resultData?.musicalNotesPlayStats?.timeChunkAnalysisList.forEach((item: any) => {
|
|
|
|
+ const { baseFrequency, startTime, endTime } = item
|
|
|
|
+ if (baseFrequency > 0) {
|
|
|
|
+ scoreData.useMaxFrequency = Math.max(scoreData.useMaxFrequency, baseFrequency)
|
|
|
|
+ scoreData.useMinFrequency = Math.min(scoreData.useMaxFrequency, baseFrequency)
|
|
|
|
+ scoreData.timeChunkAnalysisList.push({
|
|
|
|
+ freq: baseFrequency,
|
|
|
|
+ startTime,
|
|
|
|
+ endTime
|
|
|
|
+ })
|
|
|
|
+ }
|
|
|
|
+ })
|
|
createMeasureList(scoreData.xmlNotes)
|
|
createMeasureList(scoreData.xmlNotes)
|
|
- // console.log(11111,resultData,scoreData)
|
|
|
|
|
|
+ console.log(11111,resultData,scoreData)
|
|
drawTable()
|
|
drawTable()
|
|
} catch (error) {
|
|
} catch (error) {
|
|
console.error("解析评测结果:", error);
|
|
console.error("解析评测结果:", error);
|
|
}
|
|
}
|
|
scoreData.videoFilePath = res.data?.videoFilePath || res.data?.recordFilePath;
|
|
scoreData.videoFilePath = res.data?.videoFilePath || res.data?.recordFilePath;
|
|
}
|
|
}
|
|
-
|
|
|
|
-// 将频率转换为音名和八度
|
|
|
|
-function frequencyToNote(freq: number, baseA4: number = 442): { note: string; octave: number } {
|
|
|
|
- const noteNames: string[] = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
|
|
|
|
- const A4Index = 57; // A4 在 12平均律中的绝对索引
|
|
|
|
- const semitoneOffset = 12 * Math.log2(freq / baseA4);
|
|
|
|
- const nearestSemitone = Math.round(semitoneOffset);
|
|
|
|
- const absoluteIndex = nearestSemitone + A4Index;
|
|
|
|
- const octave = Math.floor(absoluteIndex / 12);
|
|
|
|
- const noteIndex = absoluteIndex % 12;
|
|
|
|
- const note = noteNames[noteIndex];
|
|
|
|
- return { note, octave };
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- // 计算音符索引
|
|
|
|
- function getAbsoluteIndex(note: string, octave: number): number {
|
|
|
|
- const noteNames = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
|
|
|
|
- const noteIndex = noteNames.indexOf(note);
|
|
|
|
- return octave * 12 + noteIndex;
|
|
|
|
- }
|
|
|
|
|
|
|
|
// 创建网格
|
|
// 创建网格
|
|
function createGrid(container: HTMLElement, config: VisualConfig) {
|
|
function createGrid(container: HTMLElement, config: VisualConfig) {
|
|
@@ -182,15 +194,16 @@ function frequencyToNote(freq: number, baseA4: number = 442): { note: string; oc
|
|
}
|
|
}
|
|
|
|
|
|
// 绘制小节序号和拍号
|
|
// 绘制小节序号和拍号
|
|
- let preMeasureWidth: any = 0;
|
|
|
|
|
|
+ let leftMeasureWidth: any = 0; // 左侧小节宽度总和
|
|
const measureDom = document.createElement("div");
|
|
const measureDom = document.createElement("div");
|
|
measureDom.className = "measure-list";
|
|
measureDom.className = "measure-list";
|
|
|
|
+ // 将每个小节根据当前的拍号,分成几份,并绘制间隔竖线
|
|
|
|
+ const lineDom = document.createElement("div");
|
|
|
|
+ lineDom.className = "line-list";
|
|
for (let index = 0; index < scoreData.measureList.length; index++) {
|
|
for (let index = 0; index < scoreData.measureList.length; index++) {
|
|
const measure = scoreData.measureList[index];
|
|
const measure = scoreData.measureList[index];
|
|
-
|
|
|
|
- const left = `${measure.measureDuration * 1000 / msPerPixel + preMeasureWidth}px`;
|
|
|
|
const width = `${measure.measureDuration * 1000 / msPerPixel}px`;
|
|
const width = `${measure.measureDuration * 1000 / msPerPixel}px`;
|
|
- preMeasureWidth = `${measure.measureDuration * 1000 / msPerPixel}`;
|
|
|
|
|
|
+ leftMeasureWidth = index > 0 ? measure.measureDuration * 1000 / msPerPixel + leftMeasureWidth : 0;
|
|
const newDom = measure.timeSignatureDiff ? `
|
|
const newDom = measure.timeSignatureDiff ? `
|
|
<div class=${styles.stMeasure} style="width:${width}">
|
|
<div class=${styles.stMeasure} style="width:${width}">
|
|
<p class=${styles.mBeat}>
|
|
<p class=${styles.mBeat}>
|
|
@@ -199,17 +212,26 @@ function frequencyToNote(freq: number, baseA4: number = 442): { note: string; oc
|
|
</p>
|
|
</p>
|
|
<div class=${styles.mNumber}>${measure.MeasureNumberXML}</div>
|
|
<div class=${styles.mNumber}>${measure.MeasureNumberXML}</div>
|
|
</div>` : `
|
|
</div>` : `
|
|
- <div class=${styles.stMeasure}>
|
|
|
|
|
|
+ <div class=${styles.stMeasure} style="width:${width}">
|
|
<div class=${styles.mNumber}>${measure.MeasureNumberXML}</div>
|
|
<div class=${styles.mNumber}>${measure.MeasureNumberXML}</div>
|
|
</div>`
|
|
</div>`
|
|
measureDom.innerHTML = measureDom.innerHTML + newDom;
|
|
measureDom.innerHTML = measureDom.innerHTML + newDom;
|
|
-
|
|
|
|
|
|
+
|
|
|
|
+ // 小节内每个节拍的宽度
|
|
|
|
+ const meanWidth = (measure.measureDuration * 1000 / msPerPixel) / measure.timeSignature.numerator;
|
|
|
|
+ new Array(measure.timeSignature.numerator).fill(1).forEach((item, idx) => {
|
|
|
|
+ const lineLeft = leftMeasureWidth + meanWidth * idx;
|
|
|
|
+ const newDom = `<div class=${styles.stLine} style="left:${lineLeft-2}px; height: ${containerHeight}px"></div>`
|
|
|
|
+ lineDom.innerHTML = lineDom.innerHTML + newDom;
|
|
|
|
+ })
|
|
}
|
|
}
|
|
container.appendChild(measureDom);
|
|
container.appendChild(measureDom);
|
|
|
|
+ container.appendChild(lineDom);
|
|
|
|
+
|
|
}
|
|
}
|
|
|
|
|
|
// 渲染音符
|
|
// 渲染音符
|
|
- function renderNotes(container: HTMLElement, noteEvents: NoteEvent[], config: VisualConfig) {
|
|
|
|
|
|
+ function renderNotes(container: HTMLElement, noteEvents: NoteEvent[], config: VisualConfig, className: string) {
|
|
noteEvents.forEach((event) => {
|
|
noteEvents.forEach((event) => {
|
|
const { note, octave } = frequencyToNote(event.freq, config.baseA4);
|
|
const { note, octave } = frequencyToNote(event.freq, config.baseA4);
|
|
const absIndex = getAbsoluteIndex(note, octave);
|
|
const absIndex = getAbsoluteIndex(note, octave);
|
|
@@ -217,8 +239,11 @@ function frequencyToNote(freq: number, baseA4: number = 442): { note: string; oc
|
|
if (absIndex < config.bottomNoteIndex || absIndex > config.topNoteIndex) return;
|
|
if (absIndex < config.bottomNoteIndex || absIndex > config.topNoteIndex) return;
|
|
|
|
|
|
const noteDiv = document.createElement("div");
|
|
const noteDiv = document.createElement("div");
|
|
- noteDiv.className = "note";
|
|
|
|
- noteDiv.innerText = `${note}${octave}`;
|
|
|
|
|
|
+ noteDiv.className = className;
|
|
|
|
+ if (className === 'note') {
|
|
|
|
+ noteDiv.innerHTML = `<span>${note}${octave}</span>`
|
|
|
|
+ }
|
|
|
|
+ // noteDiv.innerText = `${note}${octave}`;
|
|
|
|
|
|
// 计算位置
|
|
// 计算位置
|
|
const xStart = event.startTime / config.msPerPixel;
|
|
const xStart = event.startTime / config.msPerPixel;
|
|
@@ -235,13 +260,13 @@ function frequencyToNote(freq: number, baseA4: number = 442): { note: string; oc
|
|
const drawTable = () => {
|
|
const drawTable = () => {
|
|
const container = document.getElementById("visualizer") as HTMLElement;
|
|
const container = document.getElementById("visualizer") as HTMLElement;
|
|
if (!container) return;
|
|
if (!container) return;
|
|
-
|
|
|
|
|
|
+ const realFreqRange = calculateNoteRange(scoreData.xmlMinFrequency, scoreData.xmlMaxFrequency)
|
|
const config: VisualConfig = {
|
|
const config: VisualConfig = {
|
|
- msPerPixel: 5,
|
|
|
|
|
|
+ msPerPixel: 10,
|
|
semitoneHeight: 30,
|
|
semitoneHeight: 30,
|
|
baseA4: 442,
|
|
baseA4: 442,
|
|
- topNoteIndex: 87,
|
|
|
|
- bottomNoteIndex: 0,
|
|
|
|
|
|
+ topNoteIndex: realFreqRange.topNoteIndex + 3,
|
|
|
|
+ bottomNoteIndex: realFreqRange.bottomNoteIndex - 3,
|
|
};
|
|
};
|
|
|
|
|
|
container.innerHTML = "";
|
|
container.innerHTML = "";
|
|
@@ -253,8 +278,10 @@ function frequencyToNote(freq: number, baseA4: number = 442): { note: string; oc
|
|
{ freq: 468.0, startTime: 1200, endTime: 1800 },
|
|
{ freq: 468.0, startTime: 1200, endTime: 1800 },
|
|
{ freq: 523.25, startTime: 1800, endTime: 2500 },
|
|
{ freq: 523.25, startTime: 1800, endTime: 2500 },
|
|
];
|
|
];
|
|
-
|
|
|
|
- renderNotes(container, noteEvents, config);
|
|
|
|
|
|
+ // 绘制标准音符
|
|
|
|
+ renderNotes(container, scoreData.baseNoteList, config, "note");
|
|
|
|
+ // 绘制实际用户演奏音符
|
|
|
|
+ renderNotes(container, scoreData.timeChunkAnalysisList, config, "realNote");
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -269,9 +296,9 @@ function frequencyToNote(freq: number, baseA4: number = 442): { note: string; oc
|
|
// const freqTable442 = buildFreqTable(442);
|
|
// const freqTable442 = buildFreqTable(442);
|
|
// console.log("A4 = 442Hz:", freqTable442);
|
|
// console.log("A4 = 442Hz:", freqTable442);
|
|
|
|
|
|
- // // 示例:使用 A4 = 440Hz 生成频率对照表
|
|
|
|
- // const freqTable440 = buildFreqTable(440);
|
|
|
|
- // console.log("A4 = 440Hz:", freqTable440);
|
|
|
|
|
|
+ // 示例:使用 A4 = 440Hz 生成频率对照表
|
|
|
|
+ const freqTable440 = buildFreqTable(440);
|
|
|
|
+ console.log("A4 = 440Hz:", freqTable440);
|
|
|
|
|
|
const baseA4_442 = 442;
|
|
const baseA4_442 = 442;
|
|
const testFreq1 = 373.11;
|
|
const testFreq1 = 373.11;
|