Procházet zdrojové kódy

feat: 评测报告修改

TIANYONG před 2 měsíci
rodič
revize
9477bb7b29

+ 1 - 1
osmd-extended

@@ -1 +1 @@
-Subproject commit 2932a178d64983901e533ffd0d6abb1a8235a046
+Subproject commit 9cd9602f73d1392390f938a1d99db257ef062bb1

+ 25 - 2
src/page-instrument/report-new/index.module.less

@@ -153,9 +153,16 @@ body {
                 background: rgba(120, 211, 205, 0.25);
               }
               .measure-list {
+                // position: fixed;
+                // left: 0;
+                // z-index: 9;
+                position: absolute;
+                background: #EFF7FF;
                 display: flex;
                 align-items: center;
-                
+              }
+              .line-list {
+                position: relative;
               }
               .time-marker {
                 position: absolute;
@@ -174,7 +181,18 @@ body {
                 color: #000;
                 font-size: 12px;
                 font-weight: normal;
-              }          
+                > span {
+                    position: inherit;
+                    transform: scale(0.7);
+                }
+              }       
+              .realNote {
+                position: absolute;
+                height: 30PX;
+                line-height: 30PX;
+                background: rgba(16,216,203,0.6);
+                font-weight: normal;
+              }                  
         }        
     }
     .stMeasure {
@@ -235,6 +253,11 @@ body {
             }
         }
     }
+    .stLine {
+        position: absolute;
+        width: 1PX;
+        background: rgba(120, 211, 205, 0.58);
+    }
 }
 
 

+ 75 - 48
src/page-instrument/report-new/index.tsx

@@ -7,7 +7,7 @@ import { getQuery } from "/src/utils/queryString";
 import backIcon from "./image/back_icon.png";
 import videoIcon from "./image/video_icon.png";
 import { api_musicPracticeRecordDetail } from "../api";
-import { buildFreqTable, frequencyToNote } from "/src/utils/frequency"
+import { buildFreqTable, frequencyToNote, calculateNoteRange, getAbsoluteIndex } from "/src/utils/frequency"
 
 
 export default defineComponent({
@@ -34,10 +34,13 @@ export default defineComponent({
         openRealPitch: false,
         xmlInfo: {} as any,
         xmlNotes: [],
-        realMaxFrequency: 0, // 实际的最高音(xml和用户演奏的取高值)
-        realMinFrequency: 0, // 实际的最低音(xml和用户演奏的取低值)
+        xmlMaxFrequency: 0, // 实际的最高音(xml和用户演奏的取高值)
+        xmlMinFrequency: 0, // 实际的最低音(xml和用户演奏的取低值)
+        useMaxFrequency: 0, // 演奏最高频率
+        useMinFrequency: 0, // 演奏最高频率
         measureList: [] as any, // 构造的小节数组
         timeChunkAnalysisList: [] as any, // 用户实际演奏的音符数据
+        baseNoteList: [] as any, // xml标准音符
     });
 
     // 根据音符构造小节信息
@@ -51,16 +54,18 @@ export default defineComponent({
         // 音符开始和结束时间
         let startTime = 0, endTime = 0;
         // xml的最高音和最低音
-        let xmlMaxFrequency = 0, xmlMinFrequency = 0;
+        let xmlMaxFrequency = 0, xmlMinFrequency = notes.find(item => item.frequency > 0).frequency;
         // 上一个小节的节拍数,如果相邻的小节节拍数相同,则不重复添加
         let preMeasureTimeSignature = null;
         for (let index = 0; index < notes.length; 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;
             endTime = endTime + duration / 1000
             xmlMaxFrequency = Math.max(...frequencyList,xmlMaxFrequency)
-            xmlMinFrequency = Math.min(...frequencyList,xmlMinFrequency)
+            if (frequency > 0) {
+                xmlMinFrequency = Math.min(...frequencyList,xmlMinFrequency)
+            }
             if (index === 0) {
                 preMeasureTimeSignature = `${timeSignature.numerator}` + '/' + `${timeSignature.denominator}`;
                 preMeasureIndex = measureRenderIndex;
@@ -77,7 +82,7 @@ export default defineComponent({
                     timeSignature,
                     timeSignatureDiff: true,
                     noteList,
-                    measureDuration: 1
+                    measureDuration: 2
                 })
             } else {
                 if (preMeasureIndex === measureRenderIndex) {
@@ -108,14 +113,29 @@ export default defineComponent({
                         timeSignature: timeSignature,
                         timeSignatureDiff: currentTimeSignature !== preMeasureTimeSignature ? true: false,
                         noteList,
-                        measureDuration: 1
+                        measureDuration: 2
                     })
                     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)
         scoreData.measureList = measureList;
+        scoreData.xmlMaxFrequency = Math.max(xmlMaxFrequency, scoreData.useMaxFrequency);
+        scoreData.xmlMinFrequency = Math.min(xmlMinFrequency, scoreData.useMinFrequency);
     }
 
     const getRecordDetail = async () => {
@@ -127,35 +147,27 @@ export default defineComponent({
           resultData = eval('(' + res?.data?.scoreData + ')');
           scoreData.xmlInfo = eval('(' + JSON.parse(res.data.scoreData)?.musicalNotesPlayStats?.musicXmlBasicInfo + ')') || {}
           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)
-          // console.log(11111,resultData,scoreData)
+          console.log(11111,resultData,scoreData)
           drawTable()
         } catch (error) {
           console.error("解析评测结果:", error);
         }
         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) {
@@ -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");
     measureDom.className = "measure-list";
+    // 将每个小节根据当前的拍号,分成几份,并绘制间隔竖线
+    const lineDom = document.createElement("div");
+    lineDom.className = "line-list";
     for (let index = 0; index < scoreData.measureList.length; index++) {
         const measure = scoreData.measureList[index];
-        
-        const left = `${measure.measureDuration * 1000 / msPerPixel + preMeasureWidth}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 ? `
         <div class=${styles.stMeasure} style="width:${width}">
             <p class=${styles.mBeat}>
@@ -199,17 +212,26 @@ function frequencyToNote(freq: number, baseA4: number = 442): { note: string; oc
             </p>  
             <div class=${styles.mNumber}>${measure.MeasureNumberXML}</div>
         </div>` : `
-        <div class=${styles.stMeasure}>
+        <div class=${styles.stMeasure} style="width:${width}">
             <div class=${styles.mNumber}>${measure.MeasureNumberXML}</div>
         </div>`
         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(lineDom);
+
   }
   
   // 渲染音符
-  function renderNotes(container: HTMLElement, noteEvents: NoteEvent[], config: VisualConfig) {
+  function renderNotes(container: HTMLElement, noteEvents: NoteEvent[], config: VisualConfig, className: string) {
     noteEvents.forEach((event) => {
       const { note, octave } = frequencyToNote(event.freq, config.baseA4);
       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;
   
       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;
@@ -235,13 +260,13 @@ function frequencyToNote(freq: number, baseA4: number = 442): { note: string; oc
   const drawTable = () => {
     const container = document.getElementById("visualizer") as HTMLElement;
     if (!container) return;
-  
+    const realFreqRange = calculateNoteRange(scoreData.xmlMinFrequency, scoreData.xmlMaxFrequency)
     const config: VisualConfig = {
-      msPerPixel: 5,
+      msPerPixel: 10,
       semitoneHeight: 30,
       baseA4: 442,
-      topNoteIndex: 87,
-      bottomNoteIndex: 0,
+      topNoteIndex: realFreqRange.topNoteIndex + 3,
+      bottomNoteIndex: realFreqRange.bottomNoteIndex - 3,
     };
   
     container.innerHTML = "";
@@ -253,8 +278,10 @@ function frequencyToNote(freq: number, baseA4: number = 442): { note: string; oc
       { freq: 468.0, startTime: 1200, endTime: 1800 },
       { 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);
         // 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 testFreq1 = 373.11;

+ 41 - 4
src/utils/frequency.ts

@@ -3,7 +3,9 @@ type FrequencyTable = {
     [octave: number]: {
       [note: string]: number;
     };
-  };
+};
+
+const noteNames = ["c", "#c", "d", "#d", "e", "f", "#f", "g", "#g", "a", "#a", "b"];  
   
 /**
  * 根据给定音名、八度和基准 A4 频率计算音符频率
@@ -13,7 +15,6 @@ type FrequencyTable = {
  * @returns 计算得到的频率(Hz)
  */
 function getFrequency(note: string, octave: number, baseA4: number = 442): number {
-    const noteNames: string[] = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
     const noteIndex = noteNames.indexOf(note);
     if (noteIndex === -1) {
         throw new Error("Invalid note: " + note);
@@ -37,7 +38,6 @@ function getFrequency(note: string, octave: number, baseA4: number = 442): numbe
  * }
  */
 export function buildFreqTable(baseA4: number = 442): FrequencyTable {
-    const noteNames: string[] = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
     const freqTable: FrequencyTable = {};
 
     // 遍历八度范围 0 ~ 9
@@ -61,7 +61,6 @@ export function buildFreqTable(baseA4: number = 442): FrequencyTable {
  * @returns 返回一个对象,包含音名(note)和八度(octave)
  */
 export 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"];
     // A4 对应的绝对半音索引:A 在 noteNames 中索引为 9,加上 4 个八度(每个八度 12 个半音),所以 A4Index = 4 * 12 + 9 = 57
     const A4Index = 57;
     // 计算待测频率相对于基准 A4 的半音偏移值(可能是小数)
@@ -78,4 +77,42 @@ export function frequencyToNote(freq: number, baseA4: number = 442): { note: str
 
     return { note, octave };
 }
+  
+
+/**
+ * 根据给定频率、基准 A4 频率计算该频率对应的绝对半音索引
+ * @param freq - 待计算的频率(Hz)
+ * @param baseA4 - A4 的基准频率(Hz),默认为 442Hz
+ * @returns 绝对半音索引(C0 对应 0, B0 对应 11, C1 对应 12,依此类推)
+ */
+function frequencyToAbsoluteIndex(freq: number, baseA4: number = 442): number {
+    const A4Index = 57; // A4 在十二平均律中的绝对索引 (4 * 12 + 9)
+    const semitoneOffset = 12 * Math.log2(freq / baseA4);
+    const nearestSemitone = Math.round(semitoneOffset);
+    return A4Index + nearestSemitone;
+}
+
+/**
+ * 根据传入的最低频率和最高频率,计算出对应的 bottomNoteIndex 和 topNoteIndex
+ * @param lowFreq - 最低频率(Hz)
+ * @param highFreq - 最高频率(Hz)
+ * @param baseA4 - A4 的基准频率(Hz),默认为 442Hz
+ * @returns 对象 { bottomNoteIndex, topNoteIndex }
+ *   - frequencyToAbsoluteIndex 会将频率转换为一个绝对半音索引。
+    例如:如果 lowFreq 对应的音符是 C3,那么绝对索引大约为 3 * 12 + 0 = 36;
+    同理,highFreq 对应的音符可能为 B4,即 4 * 12 + 11 = 59;
+    - 实际数值会因基准频率不同而略有变化。
+ */
+export function calculateNoteRange(lowFreq: number, highFreq: number, baseA4: number = 442): { bottomNoteIndex: number; topNoteIndex: number } {
+    const bottomNoteIndex = frequencyToAbsoluteIndex(lowFreq, baseA4);
+    const topNoteIndex = frequencyToAbsoluteIndex(highFreq, baseA4);
+    return { bottomNoteIndex, topNoteIndex };
+}
+
+// 计算音符索引
+export 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;
+}