瀏覽代碼

Merge branch 'iteration-beat-one' into online

lex 1 年之前
父節點
當前提交
f9e56ba963
共有 90 個文件被更改,包括 1936 次插入0 次删除
  1. 8 0
      src/router/routes-common.ts
  2. 595 0
      src/views/tempo-practice/beat-desc.ts
  3. 159 0
      src/views/tempo-practice/beat-tick.ts
  4. 二進制
      src/views/tempo-practice/images/bg.png
  5. 二進制
      src/views/tempo-practice/images/btn-1.png
  6. 二進制
      src/views/tempo-practice/images/btn-2.png
  7. 二進制
      src/views/tempo-practice/images/btn-3.png
  8. 二進制
      src/views/tempo-practice/images/btn-4.png
  9. 二進制
      src/views/tempo-practice/images/btn-5.png
  10. 二進制
      src/views/tempo-practice/images/icon-add.png
  11. 二進制
      src/views/tempo-practice/images/icon-arrow.png
  12. 二進制
      src/views/tempo-practice/images/icon-arrow2.png
  13. 二進制
      src/views/tempo-practice/images/icon-back.png
  14. 二進制
      src/views/tempo-practice/images/icon-close.png
  15. 二進制
      src/views/tempo-practice/images/icon-pause.png
  16. 二進制
      src/views/tempo-practice/images/icon-play.png
  17. 二進制
      src/views/tempo-practice/images/icon-plus.png
  18. 二進制
      src/views/tempo-practice/images/icon-set-title.png
  19. 二進制
      src/views/tempo-practice/images/icon-setting.png
  20. 二進制
      src/views/tempo-practice/images/icon-title.png
  21. 二進制
      src/views/tempo-practice/images/music/f-1.png
  22. 二進制
      src/views/tempo-practice/images/music/f-10.png
  23. 二進制
      src/views/tempo-practice/images/music/f-11.png
  24. 二進制
      src/views/tempo-practice/images/music/f-12.png
  25. 二進制
      src/views/tempo-practice/images/music/f-13.png
  26. 二進制
      src/views/tempo-practice/images/music/f-14.png
  27. 二進制
      src/views/tempo-practice/images/music/f-15.png
  28. 二進制
      src/views/tempo-practice/images/music/f-16.png
  29. 二進制
      src/views/tempo-practice/images/music/f-17.png
  30. 二進制
      src/views/tempo-practice/images/music/f-18.png
  31. 二進制
      src/views/tempo-practice/images/music/f-19.png
  32. 二進制
      src/views/tempo-practice/images/music/f-2.png
  33. 二進制
      src/views/tempo-practice/images/music/f-20.png
  34. 二進制
      src/views/tempo-practice/images/music/f-21.png
  35. 二進制
      src/views/tempo-practice/images/music/f-22.png
  36. 二進制
      src/views/tempo-practice/images/music/f-23.png
  37. 二進制
      src/views/tempo-practice/images/music/f-24.png
  38. 二進制
      src/views/tempo-practice/images/music/f-25.png
  39. 二進制
      src/views/tempo-practice/images/music/f-26.png
  40. 二進制
      src/views/tempo-practice/images/music/f-27.png
  41. 二進制
      src/views/tempo-practice/images/music/f-28.png
  42. 二進制
      src/views/tempo-practice/images/music/f-29.png
  43. 二進制
      src/views/tempo-practice/images/music/f-3.png
  44. 二進制
      src/views/tempo-practice/images/music/f-30.png
  45. 二進制
      src/views/tempo-practice/images/music/f-31.png
  46. 二進制
      src/views/tempo-practice/images/music/f-4.png
  47. 二進制
      src/views/tempo-practice/images/music/f-5.png
  48. 二進制
      src/views/tempo-practice/images/music/f-6.png
  49. 二進制
      src/views/tempo-practice/images/music/f-7.png
  50. 二進制
      src/views/tempo-practice/images/music/f-8.png
  51. 二進制
      src/views/tempo-practice/images/music/f-9.png
  52. 5 0
      src/views/tempo-practice/images/music/index.ts
  53. 二進制
      src/views/tempo-practice/images/music/j-1.png
  54. 二進制
      src/views/tempo-practice/images/music/j-10.png
  55. 二進制
      src/views/tempo-practice/images/music/j-11.png
  56. 二進制
      src/views/tempo-practice/images/music/j-12.png
  57. 二進制
      src/views/tempo-practice/images/music/j-13.png
  58. 二進制
      src/views/tempo-practice/images/music/j-14.png
  59. 二進制
      src/views/tempo-practice/images/music/j-15.png
  60. 二進制
      src/views/tempo-practice/images/music/j-16.png
  61. 二進制
      src/views/tempo-practice/images/music/j-17.png
  62. 二進制
      src/views/tempo-practice/images/music/j-18.png
  63. 二進制
      src/views/tempo-practice/images/music/j-19.png
  64. 二進制
      src/views/tempo-practice/images/music/j-2.png
  65. 二進制
      src/views/tempo-practice/images/music/j-20.png
  66. 二進制
      src/views/tempo-practice/images/music/j-21.png
  67. 二進制
      src/views/tempo-practice/images/music/j-22.png
  68. 二進制
      src/views/tempo-practice/images/music/j-23.png
  69. 二進制
      src/views/tempo-practice/images/music/j-24.png
  70. 二進制
      src/views/tempo-practice/images/music/j-25.png
  71. 二進制
      src/views/tempo-practice/images/music/j-26.png
  72. 二進制
      src/views/tempo-practice/images/music/j-27.png
  73. 二進制
      src/views/tempo-practice/images/music/j-28.png
  74. 二進制
      src/views/tempo-practice/images/music/j-29.png
  75. 二進制
      src/views/tempo-practice/images/music/j-3.png
  76. 二進制
      src/views/tempo-practice/images/music/j-30.png
  77. 二進制
      src/views/tempo-practice/images/music/j-31.png
  78. 二進制
      src/views/tempo-practice/images/music/j-4.png
  79. 二進制
      src/views/tempo-practice/images/music/j-5.png
  80. 二進制
      src/views/tempo-practice/images/music/j-6.png
  81. 二進制
      src/views/tempo-practice/images/music/j-7.png
  82. 二進制
      src/views/tempo-practice/images/music/j-8.png
  83. 二進制
      src/views/tempo-practice/images/music/j-9.png
  84. 317 0
      src/views/tempo-practice/index.module.less
  85. 258 0
      src/views/tempo-practice/index.tsx
  86. 128 0
      src/views/tempo-practice/setting-modal/index.module.less
  87. 168 0
      src/views/tempo-practice/setting-modal/index.tsx
  88. 157 0
      src/views/tempo-practice/setting.ts
  89. 140 0
      src/views/tempo-practice/tick.ts
  90. 1 0
      src/views/tempo-practice/tockAndTick.json

+ 8 - 0
src/router/routes-common.ts

@@ -19,6 +19,14 @@ export default [
         } as metaType
       },
       {
+        path: '/tempo-practice',
+        name: 'tempo-practice',
+        component: () => import('@/views/tempo-practice'),
+        meta: {
+          title: '节奏练习'
+        }
+      },
+      {
         path: '/order-detail',
         name: 'order-detail',
         component: () => import('@/views/student-register/order-detail'),

+ 595 - 0
src/views/tempo-practice/beat-desc.ts

@@ -0,0 +1,595 @@
+export const beatDesc = {
+  1: {
+    beatNum: 1, // 元素数量
+    beatNo: '4-1', // 几分音符
+    attribute: [
+      {
+        number: 4, // 对应几分音符
+        type: 'sound' // 音符 | 休止符
+      }
+    ]
+  },
+  2: {
+    beatNum: 1,
+    beatNo: '4-1',
+    attribute: [
+      {
+        number: 4,
+        type: 'rest'
+      }
+    ]
+  },
+  3: {
+    beatNum: 2,
+    beatNo: '4-1',
+    attribute: [
+      {
+        number: 8,
+        type: 'sound'
+      },
+      {
+        number: 8,
+        type: 'sound'
+      }
+    ]
+  },
+  4: {
+    beatNum: 2,
+    beatNo: '4-1',
+    attribute: [
+      {
+        number: 8,
+        type: 'reset'
+      },
+      {
+        number: 8,
+        type: 'sound'
+      }
+    ]
+  },
+  5: {
+    beatNum: 4,
+    beatNo: '4-1',
+    attribute: [
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      }
+    ]
+  },
+  6: {
+    beatNum: 3,
+    beatNo: '4-1',
+    attribute: [
+      {
+        number: 8,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      }
+    ]
+  },
+  7: {
+    beatNum: 3,
+    beatNo: '4-1',
+    attribute: [
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 8,
+        type: 'sound'
+      }
+    ]
+  },
+  8: {
+    beatNum: 2,
+    beatNo: '4-1',
+    attribute: [
+      {
+        number: 8,
+        point: true, // 点
+        type: 'sound'
+      },
+      {
+        number: 8,
+        type: 'sound'
+      }
+    ]
+  },
+  9: {
+    beatNum: 2,
+    beatNo: '4-1',
+    attribute: [
+      {
+        number: 8,
+        type: 'sound'
+      },
+      {
+        number: 8,
+        point: true,
+        type: 'sound'
+      }
+    ]
+  },
+  10: {
+    beatNum: 3,
+    beatNo: '4-1',
+    liaison: true, // 是否连音
+    attribute: [
+      {
+        number: 8,
+        type: 'sound'
+      },
+      {
+        number: 8,
+        type: 'sound'
+      },
+      {
+        number: 8,
+        type: 'sound'
+      }
+    ]
+  },
+  11: {
+    beatNum: 6,
+    beatNo: '4-1',
+    liaison: true,
+    attribute: [
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      }
+    ]
+  },
+  12: {
+    beatNum: 3,
+    beatNo: '4-1',
+    attribute: [
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 8,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      }
+    ]
+  },
+  13: {
+    beatNum: 3,
+    beatNo: '4-1',
+    attribute: [
+      {
+        number: 16,
+        type: 'reset'
+      },
+      {
+        number: 8,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      }
+    ]
+  },
+  14: {
+    beatNum: 4,
+    beatNo: '4-1',
+    attribute: [
+      {
+        number: 16,
+        type: 'reset'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      }
+    ]
+  },
+
+  15: {
+    beatNum: 1,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 4,
+        point: true,
+        type: 'sound'
+      }
+    ]
+  },
+  16: {
+    beatNum: 1,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 4,
+        point: true,
+        type: 'reset'
+      }
+    ]
+  },
+  17: {
+    beatNum: 2,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 4,
+        type: 'sound'
+      },
+      {
+        number: 8,
+        type: 'sound'
+      }
+    ]
+  },
+  18: {
+    beatNum: 3,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 4,
+        type: 'sound'
+      }
+    ]
+  },
+  19: {
+    beatNum: 3,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 8,
+        type: 'sound'
+      },
+      {
+        number: 8,
+        type: 'sound'
+      },
+      {
+        number: 8,
+        type: 'sound'
+      }
+    ]
+  },
+  20: {
+    beatNum: 3,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 8,
+        type: 'reset'
+      },
+      {
+        number: 8,
+        type: 'sound'
+      },
+      {
+        number: 8,
+        type: 'sound'
+      }
+    ]
+  },
+  21: {
+    beatNum: 4,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 8,
+        type: 'sound'
+      },
+      {
+        number: 8,
+        type: 'sound'
+      }
+    ]
+  },
+
+  22: {
+    beatNum: 4,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 8,
+        type: 'sound'
+      },
+      {
+        number: 8,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      }
+    ]
+  },
+  23: {
+    beatNum: 4,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 8,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 8,
+        type: 'sound'
+      }
+    ]
+  },
+  24: {
+    beatNum: 4,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 8,
+        type: 'sound'
+      },
+      {
+        number: 8,
+        type: 'reset'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      }
+    ]
+  },
+  25: {
+    beatNum: 5,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 8,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      }
+    ]
+  },
+  26: {
+    beatNum: 5,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 8,
+        type: 'sound'
+      }
+    ]
+  },
+  27: {
+    beatNum: 5,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 16,
+        type: 'reset'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 8,
+        type: 'sound'
+      }
+    ]
+  },
+  28: {
+    beatNum: 6,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      }
+    ]
+  },
+
+  29: {
+    beatNum: 3,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 8,
+        point: true,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 8,
+        type: 'sound'
+      }
+    ]
+  },
+  30: {
+    beatNum: 3,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 8,
+        type: 'sound'
+      },
+      {
+        number: 8,
+        point: true,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      }
+    ]
+  },
+  31: {
+    beatNum: 3,
+    beatNo: '8-3',
+    attribute: [
+      {
+        number: 8,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      },
+      {
+        number: 8,
+        type: 'sound'
+      },
+      {
+        number: 16,
+        type: 'sound'
+      }
+    ]
+  }
+} as any;

+ 159 - 0
src/views/tempo-practice/beat-tick.ts

@@ -0,0 +1,159 @@
+import { reactive } from 'vue';
+import tockAndTick from './tockAndTick.json';
+import { Howl } from 'howler';
+import { initSelectScorePart, setting } from './setting';
+import { beatDesc } from './beat-desc';
+
+const beatData = reactive({
+  list: [] as number[],
+  len: 0,
+  tickEnd: false,
+  /** 节拍器时间 */
+  beatLengthInMilliseconds: 0,
+  loopTime: 0, // 循环时长
+  state: '',
+  source1: '' as any,
+  source2: new Howl({
+    src: tockAndTick.tock
+  }) as any,
+  index: 0,
+  show: false
+});
+
+const handlePlay = (i: number, source: any, timer: any) => {
+  let payBeatTime = new Date().getTime();
+  return new Promise(resolve => {
+    if (beatData.tickEnd) {
+      resolve(i);
+      return;
+    }
+
+    let timeSppedEnum = 16.7;
+    const proofTime = () => {
+      if (setting.playState !== 'play') {
+        return;
+      }
+      setTimeout(() => {
+        const currentTime = new Date().getTime();
+        // 两次定时任务时间间隔
+        const diffTime = currentTime - payBeatTime;
+        if (diffTime >= beatData.loopTime) {
+          beatData.index++;
+          if (timer.type === 'sound') {
+            if (source) source.play();
+          }
+
+          resolve(i);
+          payBeatTime = currentTime;
+        } else {
+          if (Math.abs(diffTime - beatData.loopTime) <= timeSppedEnum) {
+            // 为了处理最后循环时间,用循环耗时
+            for (let index = 0; index < 500000; index++) {
+              let forTime = new Date().getTime();
+              if (Math.abs(forTime - payBeatTime) >= beatData.loopTime) {
+                beatData.index++;
+                if (timer.type === 'sound') {
+                  if (source) source.play();
+                }
+                resolve(i);
+                payBeatTime = forTime;
+                break;
+              }
+            }
+          } else {
+            proofTime();
+          }
+        }
+      }, timeSppedEnum);
+    };
+    proofTime();
+  });
+};
+
+/** 开始节拍器 */
+export const handleStartBeat = async () => {
+  beatData.show = true;
+  beatData.tickEnd = false;
+  beatData.index = 0;
+  beatData.beatLengthInMilliseconds = (60 / setting.speed) * 1000;
+  // let startTime = +new Date();
+  for (let i = 0; i < setting.scorePart.length; i++) {
+    if (beatData.tickEnd) return false;
+    for (let j = 0; j < setting.scorePart[i].length; j++) {
+      if (beatData.tickEnd) return false;
+      // 提前结束, 直接放回false
+      const part = setting.scorePart[i][j];
+      const params = {
+        ...part,
+        ...beatDesc[part.index]
+      };
+      // console.log(params);
+      const single16th = beatData.beatLengthInMilliseconds;
+      const source = beatData.source2;
+      for (let g = 0; g < params.attribute.length; g++) {
+        let time = 0;
+        const attr = params.attribute[g];
+        // 计算每一拍需要的时长
+        // 四分音符 有延音
+        switch (attr.number) {
+          case 4:
+            if (attr.point) {
+              time = single16th * 0.5 * 3;
+            } else {
+              time = single16th;
+            }
+            break;
+          case 8:
+            // 连音
+            if (params.liaison) {
+              time = single16th * (1 / params.beatNum);
+            } else if (attr.point) {
+              time = single16th * 0.25 * 3;
+            } else {
+              time = single16th * 0.5;
+            }
+            break;
+          case 16:
+            if (params.liaison) {
+              time = single16th * (1 / params.beatNum);
+            } else {
+              time = single16th * 0.25;
+            }
+            break;
+        }
+
+        await handlePlay(i, source, {
+          time,
+          type: attr.type
+        });
+        beatData.loopTime = time;
+        initSelectScorePart(i, j);
+      }
+    }
+  }
+
+  // console.log(+new Date() - startTime);
+  beatData.show = false;
+  handleStartBeat();
+  return true;
+};
+
+/** 设置节拍器
+ * @param beatLengthInMilliseconds 节拍间隔时间
+ * @param beat 节拍数
+ */
+export const handleInitBeat = (
+  beatLengthInMilliseconds: number,
+  beat: number
+) => {
+  beatData.state = '';
+  beatData.beatLengthInMilliseconds = beatLengthInMilliseconds;
+  beatData.loopTime = beatLengthInMilliseconds;
+  beatData.len = beat;
+};
+
+/** 节拍器暂停 */
+export const hendleEndBeat = () => {
+  beatData.tickEnd = true;
+  initSelectScorePart();
+};

二進制
src/views/tempo-practice/images/bg.png


二進制
src/views/tempo-practice/images/btn-1.png


二進制
src/views/tempo-practice/images/btn-2.png


二進制
src/views/tempo-practice/images/btn-3.png


二進制
src/views/tempo-practice/images/btn-4.png


二進制
src/views/tempo-practice/images/btn-5.png


二進制
src/views/tempo-practice/images/icon-add.png


二進制
src/views/tempo-practice/images/icon-arrow.png


二進制
src/views/tempo-practice/images/icon-arrow2.png


二進制
src/views/tempo-practice/images/icon-back.png


二進制
src/views/tempo-practice/images/icon-close.png


二進制
src/views/tempo-practice/images/icon-pause.png


二進制
src/views/tempo-practice/images/icon-play.png


二進制
src/views/tempo-practice/images/icon-plus.png


二進制
src/views/tempo-practice/images/icon-set-title.png


二進制
src/views/tempo-practice/images/icon-setting.png


二進制
src/views/tempo-practice/images/icon-title.png


二進制
src/views/tempo-practice/images/music/f-1.png


二進制
src/views/tempo-practice/images/music/f-10.png


二進制
src/views/tempo-practice/images/music/f-11.png


二進制
src/views/tempo-practice/images/music/f-12.png


二進制
src/views/tempo-practice/images/music/f-13.png


二進制
src/views/tempo-practice/images/music/f-14.png


二進制
src/views/tempo-practice/images/music/f-15.png


二進制
src/views/tempo-practice/images/music/f-16.png


二進制
src/views/tempo-practice/images/music/f-17.png


二進制
src/views/tempo-practice/images/music/f-18.png


二進制
src/views/tempo-practice/images/music/f-19.png


二進制
src/views/tempo-practice/images/music/f-2.png


二進制
src/views/tempo-practice/images/music/f-20.png


二進制
src/views/tempo-practice/images/music/f-21.png


二進制
src/views/tempo-practice/images/music/f-22.png


二進制
src/views/tempo-practice/images/music/f-23.png


二進制
src/views/tempo-practice/images/music/f-24.png


二進制
src/views/tempo-practice/images/music/f-25.png


二進制
src/views/tempo-practice/images/music/f-26.png


二進制
src/views/tempo-practice/images/music/f-27.png


二進制
src/views/tempo-practice/images/music/f-28.png


二進制
src/views/tempo-practice/images/music/f-29.png


二進制
src/views/tempo-practice/images/music/f-3.png


二進制
src/views/tempo-practice/images/music/f-30.png


二進制
src/views/tempo-practice/images/music/f-31.png


二進制
src/views/tempo-practice/images/music/f-4.png


二進制
src/views/tempo-practice/images/music/f-5.png


二進制
src/views/tempo-practice/images/music/f-6.png


二進制
src/views/tempo-practice/images/music/f-7.png


二進制
src/views/tempo-practice/images/music/f-8.png


二進制
src/views/tempo-practice/images/music/f-9.png


+ 5 - 0
src/views/tempo-practice/images/music/index.ts

@@ -0,0 +1,5 @@
+const modules: any = import.meta.glob('./*', { eager: true });
+export const getImage = (name: string) => {
+  // console.log(modules[`./${name}`]);
+  return modules[`./${name}`]?.default || '';
+};

二進制
src/views/tempo-practice/images/music/j-1.png


二進制
src/views/tempo-practice/images/music/j-10.png


二進制
src/views/tempo-practice/images/music/j-11.png


二進制
src/views/tempo-practice/images/music/j-12.png


二進制
src/views/tempo-practice/images/music/j-13.png


二進制
src/views/tempo-practice/images/music/j-14.png


二進制
src/views/tempo-practice/images/music/j-15.png


二進制
src/views/tempo-practice/images/music/j-16.png


二進制
src/views/tempo-practice/images/music/j-17.png


二進制
src/views/tempo-practice/images/music/j-18.png


二進制
src/views/tempo-practice/images/music/j-19.png


二進制
src/views/tempo-practice/images/music/j-2.png


二進制
src/views/tempo-practice/images/music/j-20.png


二進制
src/views/tempo-practice/images/music/j-21.png


二進制
src/views/tempo-practice/images/music/j-22.png


二進制
src/views/tempo-practice/images/music/j-23.png


二進制
src/views/tempo-practice/images/music/j-24.png


二進制
src/views/tempo-practice/images/music/j-25.png


二進制
src/views/tempo-practice/images/music/j-26.png


二進制
src/views/tempo-practice/images/music/j-27.png


二進制
src/views/tempo-practice/images/music/j-28.png


二進制
src/views/tempo-practice/images/music/j-29.png


二進制
src/views/tempo-practice/images/music/j-3.png


二進制
src/views/tempo-practice/images/music/j-30.png


二進制
src/views/tempo-practice/images/music/j-31.png


二進制
src/views/tempo-practice/images/music/j-4.png


二進制
src/views/tempo-practice/images/music/j-5.png


二進制
src/views/tempo-practice/images/music/j-6.png


二進制
src/views/tempo-practice/images/music/j-7.png


二進制
src/views/tempo-practice/images/music/j-8.png


二進制
src/views/tempo-practice/images/music/j-9.png


+ 317 - 0
src/views/tempo-practice/index.module.less

@@ -0,0 +1,317 @@
+.tempoPractice {
+  position: fixed;
+  left: 0;
+  top: 0;
+  width: 100vw;
+  height: 100vh;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  background: url("./images/bg.png") no-repeat center center / cover;
+
+  display: flex;
+  flex-direction: column;
+}
+
+.conCon {
+  flex: 1 auto;
+  display: flex;
+  align-items: center;
+}
+
+.pc {
+
+  .container {
+    max-width: 1200px;
+    gap: 30px 0;
+  }
+
+  .beatSection {
+    .beat {
+      width: 206px;
+      height: 284px;
+
+      img {
+        width: 140px;
+      }
+    }
+
+    &.small {
+      .beat {
+        width: 139px !important;
+        height: 191px !important;
+
+        // width: 65px;
+        //   height: 86px;
+
+        img {
+          width: 108px;
+        }
+      }
+    }
+  }
+}
+
+.head {
+  position: relative;
+  display: flex;
+  align-items: flex-start;
+  justify-content: space-between;
+  padding: 0 23px 8px 41px;
+  transition: opacity 0.3s ease-in-out;
+
+  .back {
+    padding-top: 17px;
+
+    img {
+      width: 46px;
+      height: 46px;
+      display: block;
+    }
+  }
+
+  .title {
+    img {
+      width: 173px;
+      height: 75px;
+      display: block;
+    }
+  }
+}
+
+.container {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex: 1 auto;
+  flex-wrap: wrap;
+  gap: 15px 0;
+  max-width: 900px;
+  margin: 0 auto;
+}
+
+.beatSection {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  &.small {
+    width: 50%;
+    // margin: 0 16px;
+
+    &:nth-child(2n + 1) {
+      justify-content: flex-end;
+      padding-right: 12px;
+    }
+
+    &:nth-child(2n + 2) {
+      justify-content: flex-start;
+      padding-left: 12px;
+    }
+
+    .beat {
+      border: 2px solid #fff;
+      width: 65px;
+      height: 86px;
+      cursor: pointer;
+      margin: 0 7px;
+
+
+
+      &::before,
+      &::after {
+        width: 19px;
+        height: 5px;
+      }
+
+      img {
+        width: 48px;
+      }
+    }
+  }
+
+  .beat {
+    display: flex;
+    align-items: center;
+    flex-direction: column;
+    margin: 0 13px;
+    width: 118px;
+    height: 156px;
+    box-shadow: 0px 2px 16px 0px #76C3D2;
+    border-radius: 14px;
+    border: 3px solid #fff;
+    background: #FFFFFF;
+    position: relative;
+
+    .direction {
+      position: absolute;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      top: 0;
+      display: flex;
+      align-items: center;
+      flex-direction: column;
+      z-index: 9;
+
+      &>div {
+        width: 100%;
+        flex: 1;
+      }
+    }
+
+    &.active {
+      border: 3px solid rgba(255, 167, 0, 1);
+    }
+
+    .imgSection {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      flex: 1;
+    }
+
+    img {
+      width: 96px;
+    }
+
+    &::before,
+    &::after {
+      content: '';
+      display: block;
+      width: 30px;
+      height: 7px;
+      background: url('./images/icon-arrow.png') no-repeat center center / contain;
+      margin: 0 auto;
+    }
+
+    &::before {
+      margin-top: 3px;
+    }
+
+    &::after {
+      margin-bottom: 3px;
+      transform: rotate(180deg);
+    }
+  }
+}
+
+.footer {
+  padding: 12px 0 20px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+
+  &>div {
+    margin: 0 9px;
+    cursor: pointer;
+
+    img {
+      width: inherit;
+      height: inherit;
+      display: block;
+    }
+  }
+
+  .play {
+    width: 54px;
+    height: 55px;
+  }
+
+  .playType {
+    width: 175px;
+    height: 39px;
+  }
+
+  .randomTempo {
+    width: 90px;
+    height: 39px;
+  }
+
+  .speedChange {
+    width: 110px;
+    height: 39px;
+    background: url('./images/btn-4.png') no-repeat center center / contain;
+    display: flex;
+    align-items: center;
+    padding: 9px;
+    display: flex;
+    align-items: center;
+
+
+    :global {
+      .van-popover__wrapper {
+        flex: 1;
+      }
+    }
+
+    .speedNum {
+      flex: 1;
+      font-size: 16px;
+      font-weight: 600;
+      color: #6B3B19;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+
+      &::after {
+        content: '';
+        display: inline-block;
+        width: 8px;
+        height: 5px;
+        margin-left: 4px;
+        background: url('./images/icon-arrow2.png') no-repeat center center / contain;
+      }
+    }
+  }
+
+  .speedPlus,
+  .speedAdd {
+    width: 21px;
+    height: 21px;
+  }
+}
+
+.settingPopup {
+  background: transparent;
+  overflow: visible;
+}
+
+.popupContainer {
+  margin-top: -10px !important;
+  --van-popover-action-height: 32px;
+  --van-popover-action-font-size: 14px;
+  --van-popover-radius: 12px;
+  --van-popover-action-width: 85px;
+  padding: 4Px 0;
+  border-radius: 12px;
+  background-color: #fff;
+  box-shadow: 0 0.05333rem 0.32rem rgba(50, 50, 51, .12);
+
+
+  :global {
+    .van-popover__content {
+      max-height: 200px;
+      box-shadow: none;
+      overflow-y: auto;
+
+      &::-webkit-scrollbar {
+        width: 4px;
+      }
+
+      &::-webkit-scrollbar-thumb {
+        border-radius: 10px;
+        background: rgba(0, 0, 0, 0.2);
+      }
+
+      &::-webkit-scrollbar-track {
+        border-radius: 0;
+        background: rgba(0, 0, 0, 0.1);
+      }
+    }
+
+    .van-popover__action {
+      padding: 0 9px;
+    }
+  }
+}

+ 258 - 0
src/views/tempo-practice/index.tsx

@@ -0,0 +1,258 @@
+import { defineComponent, onMounted, reactive } from 'vue';
+import styles from './index.module.less';
+import { postMessage } from '@/helpers/native-message';
+import icon_title from './images/icon-title.png';
+import icon_back from './images/icon-back.png';
+import icon_setting from './images/icon-setting.png';
+import iconPlay from './images/icon-play.png';
+import iconPause from './images/icon-pause.png';
+import beat from './images/btn-2.png';
+import tempo from './images/btn-3.png';
+import randDom from './images/btn-1.png';
+import iconPlus from './images/icon-plus.png';
+import iconAdd from './images/icon-add.png';
+import { getImage } from './images/music';
+import j1 from './images/music/j-1.png';
+// import j2 from './images/music/j-2.png';
+import { Popover, Popup, showToast } from 'vant';
+import SettingModal from './setting-modal';
+import {
+  randomScoreElement,
+  renderScore,
+  setting,
+  elementDirection
+} from './setting';
+import { handleStartTick, hendleEndTick } from './tick';
+import { handleStartBeat, hendleEndBeat } from './beat-tick';
+import { browser } from '@/helpers/utils';
+import { useRoute } from 'vue-router';
+
+export default defineComponent({
+  name: 'tempo-practice',
+  setup() {
+    const route = useRoute();
+    const state = reactive({
+      platform: route.query.platform, // microapp 老师端应用里面打开单独处理返回逻辑
+      win: route.query.win,
+      settingStatus: false,
+      speedList: [
+        { text: '40', value: 40, color: '#060606' },
+        { text: '50', value: 50, color: '#060606' },
+        { text: '60', value: 60, color: '#060606' },
+        { text: '70', value: 70, color: '#060606' },
+        { text: '80', value: 80, color: '#060606' },
+        { text: '90', value: 90, color: '#060606' },
+        { text: '100', value: 100, color: '#060606' },
+        { text: '110', value: 110, color: '#060606' },
+        { text: '120', value: 120, color: '#060606' },
+        { text: '130', value: 130, color: '#060606' },
+        { text: '140', value: 140, color: '#060606' },
+        { text: '150', value: 150, color: '#060606' },
+        { text: '160', value: 160, color: '#060606' },
+        { text: '170', value: 170, color: '#060606' },
+        { text: '180', value: 180, color: '#060606' },
+        { text: '190', value: 190, color: '#060606' },
+        { text: '200', value: 200, color: '#060606' }
+      ]
+    });
+    // 返回
+    const goback = () => {
+      if (state.platform === 'microapp') {
+        window.parent.postMessage(
+          {
+            api: 'iframe_exit'
+          },
+          '*'
+        );
+
+        return;
+      }
+      if (!browser().isApp) {
+        window.close();
+        return;
+      }
+      postMessage({ api: 'goBack' });
+    };
+
+    /** 播放切换 */
+    const handlePlay = async () => {
+      if (setting.playState === 'pause') {
+        setting.playState = 'play';
+        if (setting.playType === 'beat') {
+          await handleStartTick();
+        } else {
+          await handleStartBeat();
+        }
+      } else {
+        handleStop();
+      }
+    };
+    /** 播放类型 */
+    const handlePlayType = () => {
+      handleStop();
+      if (setting.playType === 'beat') {
+        setting.playType = 'tempo';
+      } else {
+        setting.playType = 'beat';
+      }
+    };
+
+    const handleStop = () => {
+      setting.playState = 'pause';
+      if (setting.playType === 'beat') {
+        hendleEndTick();
+      } else {
+        hendleEndBeat();
+      }
+    };
+
+    onMounted(() => {
+      state.speedList.forEach((item: any) => {
+        if (item.value === setting.speed) item.color = '#1CACF1';
+      });
+      renderScore();
+    });
+    return () => (
+      <div class={[styles.tempoPractice, state.win === 'pc' ? styles.pc : '']}>
+        <div class={styles.head}>
+          <div class={styles.back} onClick={goback}>
+            <img src={icon_back} />
+          </div>
+          <div class={styles.title}>
+            <img src={icon_title} />
+          </div>
+          <div class={styles.back} onClick={() => (state.settingStatus = true)}>
+            <img src={icon_setting} />
+          </div>
+        </div>
+
+        <div class={styles.conCon}>
+          <div class={styles.container}>
+            {setting.scorePart.map((item: any, i: number) => (
+              <div
+                class={[
+                  styles.beatSection,
+                  setting.scorePart.length >= 2 &&
+                    item.length !== 1 &&
+                    styles.small
+                ]}>
+                {item.map((child: any, jIndex: number) => (
+                  <div
+                    class={[styles.beat, child.selected ? styles.active : '']}>
+                    <div class={styles.direction}>
+                      <div
+                        class={styles.up}
+                        onClick={() => {
+                          if (setting.playState === 'play') return;
+                          if (setting.tempo.length <= 1) {
+                            showToast('无法切换,请选择至少2种节奏型');
+                            return;
+                          }
+                          // const obj = randomScoreElement(child.index);
+                          const obj = elementDirection('up', child.index);
+                          child.index = obj.index;
+                          child.url = obj.url;
+                        }}></div>
+                      <div
+                        class={styles.down}
+                        onClick={() => {
+                          if (setting.playState === 'play') return;
+                          if (setting.tempo.length <= 1) {
+                            showToast('无法切换,请选择至少2种节奏型');
+                            return;
+                          }
+                          // const obj = randomScoreElement(child.index);
+                          const obj = elementDirection('down', child.index);
+                          child.index = obj.index;
+                          child.url = obj.url;
+                        }}></div>
+                    </div>
+                    <div class={styles.imgSection}>
+                      <img src={getImage(child.url)} />
+                    </div>
+                  </div>
+                ))}
+              </div>
+            ))}
+          </div>
+        </div>
+
+        <div class={styles.footer}>
+          {/* 播放 */}
+          <div class={styles.play} onClick={handlePlay}>
+            {setting.playState === 'pause' ? (
+              <img src={iconPause} />
+            ) : (
+              <img src={iconPlay} />
+            )}
+          </div>
+          {/* 播放类型 */}
+          <div class={styles.playType} onClick={handlePlayType}>
+            {setting.playType === 'beat' ? (
+              <img src={beat} />
+            ) : (
+              <img src={tempo} />
+            )}
+          </div>
+          {/* 随机生成 */}
+          <div
+            class={styles.randomTempo}
+            onClick={() => {
+              renderScore();
+              handleStop();
+            }}>
+            <img src={randDom} />
+          </div>
+          {/* 速度 */}
+          <div class={styles.speedChange}>
+            <img
+              src={iconPlus}
+              class={styles.speedPlus}
+              onClick={() => {
+                if (setting.speed <= 40) return;
+                setting.speed -= 1;
+                handleStop();
+              }}
+            />
+            <Popover
+              placement="top"
+              class={styles.popupContainer}
+              actions={state.speedList}
+              onSelect={(val: any) => {
+                if (val.value === setting.speed) return;
+                state.speedList.forEach((item: any) => {
+                  if (item.value === val.value) {
+                    item.color = '#1CACF1';
+                    setting.speed = val.value;
+                  } else {
+                    item.color = '#060606';
+                  }
+                });
+                handleStop();
+              }}>
+              {{
+                reference: () => (
+                  <div class={styles.speedNum}>{setting.speed}</div>
+                )
+              }}
+            </Popover>
+
+            <img
+              src={iconAdd}
+              class={styles.speedAdd}
+              onClick={() => {
+                if (setting.speed >= 200) return;
+                setting.speed += 1;
+                handleStop();
+              }}
+            />
+          </div>
+        </div>
+
+        <Popup v-model:show={state.settingStatus} class={styles.settingPopup}>
+          <SettingModal onClose={() => (state.settingStatus = false)} />
+        </Popup>
+      </div>
+    );
+  }
+});

+ 128 - 0
src/views/tempo-practice/setting-modal/index.module.less

@@ -0,0 +1,128 @@
+.settingContainer {
+  position: relative;
+  width: 430px;
+  height: min(86vh, 340px);
+  background: #fff;
+  border-radius: 26px;
+  padding: 20px 0;
+
+  .title {
+    position: absolute;
+    left: 50%;
+    top: -6px;
+    margin-left: -70px;
+    z-index: 9;
+    width: 140px;
+    height: 34px;
+    background: url('../images/icon-set-title.png') no-repeat center center / contain;
+  }
+
+  .iconClose {
+    position: absolute;
+    right: 13px;
+    top: 13px;
+    z-index: 9;
+    display: inline-block;
+    width: 31px;
+    height: 32px;
+    background: url('../images/icon-close.png') no-repeat center center / contain;
+  }
+}
+
+.settingContent {
+  padding: 0 26px;
+  overflow-y: auto;
+  height: 100%;
+
+  &::-webkit-scrollbar {
+    width: 4px;
+  }
+
+  &::-webkit-scrollbar-thumb {
+    border-radius: 10px;
+    background: rgba(0, 0, 0, 0.2);
+  }
+
+  &::-webkit-scrollbar-track {
+    border-radius: 0;
+    background: rgba(0, 0, 0, 0.1);
+  }
+}
+
+.settingParams {
+  // padding: 20px 26px;
+  padding-bottom: 53px;
+}
+
+.parmaTitle {
+  font-size: 14px;
+  font-weight: 600;
+  color: #131415;
+  line-height: 20px;
+  padding-bottom: 8px;
+}
+
+.paramContent {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 0 12px;
+  padding-bottom: 18px;
+
+
+  .btn {
+    width: 64px;
+    height: 26px;
+    font-size: 12px;
+    font-weight: 600;
+    color: rgba(0, 0, 0, 0.7);
+    line-height: 17px;
+    background: #F5F6F7;
+    border: none;
+    padding: 0;
+
+    &.active {
+      background: #19AEFF;
+      color: #FFFFFF;
+    }
+  }
+
+  &.tempo {
+    gap: 8px 8px;
+    padding-bottom: 0;
+  }
+
+  .active {
+    background: #D0EBFF;
+  }
+
+  img {
+    width: 46px;
+    height: 46px;
+    background: #F5F6F7;
+    border-radius: 4px;
+  }
+}
+
+.btnGroup {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  z-index: 9;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 14px 0;
+  background: linear-gradient(to bottom, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1));
+  border-radius: 0 0 26px 26px;
+
+  .btnSubmit {
+    width: 143px;
+    height: 45px;
+    line-height: 45px;
+    border-radius: 20px;
+    background: url('../images/btn-5.png') no-repeat center center / contain;
+    border: none;
+  }
+}

+ 168 - 0
src/views/tempo-practice/setting-modal/index.tsx

@@ -0,0 +1,168 @@
+import { computed, defineComponent, onMounted, reactive } from 'vue';
+import styles from './index.module.less';
+import { Button, showToast } from 'vant';
+import {
+  barLineList,
+  beatList,
+  elementList,
+  renderScore,
+  setting,
+  tempo4,
+  tempo8
+} from '../setting';
+import { getImage } from '../images/music';
+import { hendleEndTick } from '../tick';
+import { hendleEndBeat } from '../beat-tick';
+import deepClone from '@/helpers/deep-clone';
+
+export default defineComponent({
+  emits: ['close'],
+  name: 'setting-modal',
+  setup(props, { emit }) {
+    const state = reactive({
+      element: 'jianpu' as 'jianpu' | 'staff', // 元素
+      beat: '4-4' as '4-2' | '4-3' | '4-4' | '8-3' | '8-6', // 拍号
+      barLine: '1' as '1' | '2' | '4', // 小节数
+      tempo: ['1', '2', '3'] as any[] // 节奏形筛选
+    });
+
+    const tempoList = computed(() => {
+      if (['4-2', '4-3', '4-4'].includes(state.beat)) {
+        return tempo4;
+      } else if (['8-3', '8-6'].includes(state.beat)) {
+        return tempo8;
+      }
+      return tempo4;
+    });
+
+    const onChangeTempo = (item: any) => {
+      const index = state.tempo.indexOf(item);
+      if (index !== -1) {
+        state.tempo.splice(index, 1);
+      } else {
+        state.tempo.push(item);
+      }
+    };
+
+    const handleStop = () => {
+      setting.playState = 'pause';
+      if (setting.playType === 'beat') {
+        hendleEndTick();
+      } else {
+        hendleEndBeat();
+      }
+    };
+
+    const onSubmit = () => {
+      if (state.tempo.length <= 0) {
+        showToast('节奏型不能为空');
+        return;
+      }
+      let status = false; // 是否有更改
+      if (
+        setting.element !== state.element ||
+        setting.beat !== state.beat ||
+        setting.barLine !== state.barLine ||
+        setting.tempo.join(',') !== state.tempo.join(',')
+      ) {
+        status = true;
+      }
+
+      // 判断是否有数据变化
+      handleStop();
+      if (status) {
+        setting.element = state.element;
+        setting.beat = state.beat;
+        setting.barLine = state.barLine;
+        setting.tempo = state.tempo;
+        renderScore();
+      }
+
+      emit('close');
+    };
+
+    return () => (
+      <div class={styles.settingContainer}>
+        <div class={styles.title}></div>
+        <i
+          class={styles.iconClose}
+          onClick={() => {
+            emit('close');
+            setTimeout(() => {
+              state.element = setting.element;
+              state.beat = setting.beat;
+              state.barLine = setting.barLine;
+              state.tempo = setting.tempo;
+            }, 100);
+          }}></i>
+
+        <div class={styles.settingContent}>
+          <div class={styles.settingParams}>
+            <div class={styles.parmaTitle}>元素</div>
+            <div class={styles.paramContent}>
+              {Object.keys(elementList).map((item: any) => (
+                <Button
+                  round
+                  class={[styles.btn, state.element === item && styles.active]}
+                  onClick={() => {
+                    state.element = item;
+                  }}>
+                  {elementList[item]}
+                </Button>
+              ))}
+            </div>
+            <div class={styles.parmaTitle}>拍号</div>
+            <div class={styles.paramContent}>
+              {Object.keys(beatList).map((item: any) => (
+                <Button
+                  round
+                  class={[styles.btn, state.beat === item && styles.active]}
+                  onClick={() => {
+                    state.beat = item;
+                    if (['4-2', '4-3', '4-4'].includes(state.beat)) {
+                      state.tempo = ['1', '2', '3'];
+                    } else if (['8-3', '8-6'].includes(state.beat)) {
+                      state.tempo = ['15', '16', '17'];
+                    }
+                  }}>
+                  {beatList[item]}
+                </Button>
+              ))}
+            </div>
+            <div class={styles.parmaTitle}>每页显示小节数量</div>
+            <div class={styles.paramContent}>
+              {Object.keys(barLineList).map((item: any) => (
+                <Button
+                  round
+                  class={[styles.btn, state.barLine === item && styles.active]}
+                  onClick={() => {
+                    state.barLine = item;
+                  }}>
+                  {barLineList[item]}
+                </Button>
+              ))}
+            </div>
+            <div class={styles.parmaTitle}>节奏型筛选</div>
+            <div class={[styles.paramContent, styles.tempo]}>
+              {Object.keys(tempoList.value).map((item: any) => (
+                <>
+                  <img
+                    onClick={() => onChangeTempo(item)}
+                    class={state.tempo.includes(item) && styles.active}
+                    src={getImage(
+                      (state.element === 'jianpu' ? 'j-' : 'f-') +
+                        tempoList.value[item]
+                    )}
+                  />
+                </>
+              ))}
+            </div>
+          </div>
+        </div>
+        <div class={styles.btnGroup}>
+          <Button class={styles.btnSubmit} onClick={onSubmit}></Button>
+        </div>
+      </div>
+    );
+  }
+});

+ 157 - 0
src/views/tempo-practice/setting.ts

@@ -0,0 +1,157 @@
+import { reactive } from 'vue';
+import { handleInitTick } from './tick';
+import { handleInitBeat } from './beat-tick';
+
+export const setting = reactive({
+  element: 'jianpu' as 'jianpu' | 'staff', // 元素
+  beat: '4-4' as '4-2' | '4-3' | '4-4' | '8-3' | '8-6', // 拍号
+  barLine: '1' as '1' | '2' | '4', // 小节数
+  tempo: ['1', '2', '3'] as any[], // 节奏形筛选
+  scorePart: [] as any, // 生成谱面
+  playState: 'pause' as 'pause' | 'play',
+  playType: 'tempo' as 'beat' | 'tempo',
+  speed: 60 // 默认速度
+});
+
+/** 元素 */
+export const elementList = {
+  jianpu: '简谱',
+  staff: '五线谱'
+} as any;
+
+/** 拍号 */
+export const beatList = {
+  '4-2': '2/4',
+  '4-3': '3/4',
+  '4-4': '4/4',
+  '8-3': '3/8',
+  '8-6': '6/8'
+} as any;
+
+/** 每页小节数量 */
+export const barLineList = {
+  1: 1,
+  2: 2,
+  4: 4
+} as any;
+
+/** 节奏型筛选 */
+// 简谱
+const temp: any = {};
+for (let i = 1; i <= 14; i++) {
+  temp[i] = i + '.png';
+}
+export const tempo4 = temp;
+
+// 五线谱
+const temp2: any = {};
+for (let i = 15; i <= 31; i++) {
+  temp2[i] = i + '.png';
+}
+export const tempo8 = temp2;
+
+/** 随机生成元素 */
+export const randomScoreElement = (element?: string) => {
+  const tempoList = setting.tempo;
+  const prefix = setting.element === 'jianpu' ? 'j-' : 'f-';
+  if (element) {
+    const newArr = tempoList.filter((item: any) => item !== element);
+    // 生成一个0到newArr长度之间的随机索引
+    const randomIndex = Math.floor(Math.random() * newArr.length);
+    return {
+      url: prefix + newArr[randomIndex] + '.png',
+      index: newArr[randomIndex]
+    };
+  } else {
+    // 如果只有一个就直接返回
+    if (tempoList.length === 1) {
+      return {
+        url: prefix + tempoList[0] + '.png',
+        index: tempoList[0]
+      };
+    } else {
+      const randomIndex = Math.floor(Math.random() * tempoList.length);
+      const randomItem = tempoList[randomIndex];
+      return {
+        url: prefix + randomItem + '.png',
+        index: randomItem
+      };
+    }
+  }
+};
+
+/** 设置元素方向 */
+export const elementDirection = (type: string, index: number) => {
+  const prefix = setting.element === 'jianpu' ? 'j-' : 'f-';
+  let ele = '';
+  let i = index;
+  const tempoList = setting.tempo;
+  if (type === 'up') {
+    if (index <= 0) {
+      ele = tempoList[tempoList.length - 1];
+      i = tempoList.length - 1;
+    } else {
+      ele = tempoList[index - 1];
+      i = index - 1;
+    }
+  } else if (type === 'down') {
+    if (index >= tempoList.length - 1) {
+      ele = tempoList[0];
+      i = 0;
+    } else {
+      ele = tempoList[index + 1];
+      i = index + 1;
+    }
+  }
+
+  return {
+    url: prefix + ele + '.png',
+    index: i
+  };
+};
+
+/** 生成谱面 */
+export const renderScore = () => {
+  const barLine = Number(setting.barLine);
+  const beatA = setting.beat.split('-').map(i => Number(i));
+  let beat = beatA[1];
+  if (beatA[0] === 8) {
+    beat = beat / 3;
+  }
+  console.log(beat, 'beat');
+  const tempBeat: any = [];
+  for (let i = 0; i < barLine; i++) {
+    tempBeat[i] = [];
+    for (let j = 0; j < beat; j++) {
+      tempBeat[i][j] = {
+        ...randomScoreElement()
+      };
+    }
+  }
+  setting.scorePart = tempBeat;
+
+  const beatLengthInMilliseconds = (60 / setting.speed) * 1000;
+  if (setting.playType === 'beat') {
+    handleInitTick(
+      beatLengthInMilliseconds,
+      Number(beatA[1]) || 4,
+      Number(beatA[0])
+    );
+  } else {
+    handleInitBeat(beatLengthInMilliseconds, Number(beatA[1]) || 4);
+  }
+  initSelectScorePart();
+};
+
+/** 初始化选中状态 */
+export const initSelectScorePart = (i?: number, j?: number) => {
+  setting.scorePart.forEach((part: Array<any>) => {
+    part.forEach((item: any) => {
+      item.selected = false;
+    });
+  });
+
+  if (i !== undefined && j !== undefined && setting.scorePart[i][j]) {
+    setting.scorePart[i][j].selected = true;
+  }
+};

+ 140 - 0
src/views/tempo-practice/tick.ts

@@ -0,0 +1,140 @@
+import { reactive } from 'vue';
+import tockAndTick from './tockAndTick.json';
+import { Howl } from 'howler';
+import { initSelectScorePart, setting } from './setting';
+import { beatDesc } from './beat-desc';
+
+const tickData = reactive({
+  list: [] as number[],
+  len: 0, // 有几拍
+  afterBeat: 0, // 一小节里面有几拍
+  tickEnd: false,
+  /** 节拍器时间 */
+  beatLengthInMilliseconds: 0,
+  state: '',
+  source1: '' as any,
+  source2: '' as any,
+  index: 0,
+  show: false
+});
+
+let diffTime = 0; // 每一次节拍时间差
+const handlePlay = (i: number, source: any) => {
+  let payBeatTime = new Date().getTime();
+  return new Promise(resolve => {
+    if (tickData.tickEnd) {
+      resolve(i);
+      return;
+    }
+    let timeSppedEnum = 16.7;
+    const proofTime = () => {
+      if (setting.playState !== 'play') {
+        return;
+      }
+      setTimeout(() => {
+        const currentTime = new Date().getTime();
+        // 两次定时任务时间间隔
+        const diffTime = currentTime - payBeatTime;
+        if (diffTime >= tickData.beatLengthInMilliseconds) {
+          tickData.index++;
+          if (source) source.play();
+          resolve(i);
+          payBeatTime = currentTime;
+        } else {
+          if (
+            Math.abs(diffTime - tickData.beatLengthInMilliseconds) <=
+            timeSppedEnum
+          ) {
+            // 为了处理最后循环时间,用循环耗时
+            for (let index = 0; index < 500000; index++) {
+              let forTime = new Date().getTime();
+              if (
+                Math.abs(forTime - payBeatTime) >=
+                tickData.beatLengthInMilliseconds
+              ) {
+                tickData.index++;
+                if (source) source.play();
+                resolve(i);
+                payBeatTime = forTime;
+                break;
+              }
+            }
+          } else {
+            proofTime();
+          }
+        }
+      }, timeSppedEnum);
+    };
+    proofTime();
+  });
+};
+
+/** 设置节拍器
+ * @param beatLengthInMilliseconds 节拍间隔时间
+ * @param beat 节拍数
+ */
+export const handleInitTick = (
+  beatLengthInMilliseconds: number,
+  beat: number,
+  afterBeat: number
+) => {
+  tickData.state = '';
+  tickData.beatLengthInMilliseconds = beatLengthInMilliseconds;
+  tickData.len = beat;
+  tickData.afterBeat = afterBeat;
+};
+
+/** 开始节拍器 */
+export const handleStartTick = async () => {
+  tickData.show = true;
+  tickData.tickEnd = false;
+  if (tickData.state !== 'ok') {
+    tickData.source1 = new Howl({
+      src: tockAndTick.tick
+    });
+    tickData.source2 = new Howl({
+      src: tockAndTick.tock
+    });
+    tickData.state = 'ok';
+  }
+  tickData.index = 0;
+  tickData.beatLengthInMilliseconds = (60 / setting.speed) * 1000;
+
+  if (tickData.afterBeat === 8) {
+    tickData.beatLengthInMilliseconds = tickData.beatLengthInMilliseconds * 0.5;
+  }
+
+  for (let i = 0; i < setting.scorePart.length; i++) {
+    if (tickData.tickEnd) return false;
+    const temp = setting.scorePart[i];
+    let len = temp.length;
+    if (tickData.afterBeat === 8) {
+      len = tickData.len;
+    }
+
+    for (let j = 0; j < len; j++) {
+      // 提前结束, 直接放回false
+      if (tickData.tickEnd) return false;
+      const source =
+        j === 0 ? tickData.source1 : j === len ? null : tickData.source2;
+
+      await handlePlay(j, source);
+      if (tickData.afterBeat === 8) {
+        initSelectScorePart(i, Math.floor((j <= 0 ? 1 : j) / 3));
+      } else {
+        initSelectScorePart(i, j);
+      }
+    }
+  }
+
+  // console.log(+new Date() - startTime);
+  tickData.show = false;
+  handleStartTick();
+  return true;
+};
+
+/** 节拍器暂停 */
+export const hendleEndTick = () => {
+  tickData.tickEnd = true;
+  initSelectScorePart();
+};

File diff suppressed because it is too large
+ 1 - 0
src/views/tempo-practice/tockAndTick.json


Some files were not shown because too many files changed in this diff