Pārlūkot izejas kodu

定时器改为拼接

黄琪勇 1 gadu atpakaļ
vecāks
revīzija
a50cc322a6

+ 4 - 1
components.d.ts

@@ -9,8 +9,11 @@ export {}
 
 declare module '@vue/runtime-core' {
   export interface GlobalComponents {
-    Flipper: typeof import('./src/components/timerMeter/modals/flipper.vue')['default']
+    Flipper: typeof import('./src/components/timerMeterOld/modals/flipper.vue')['default']
+    Metronome: typeof import('./src/components/Metronome/Metronome.vue')['default']
+    MetronomeBox: typeof import('./src/components/Metronome/MetronomeBox.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
+    TimerMeter: typeof import('./src/components/timerMeter/TimerMeter.vue')['default']
   }
 }

+ 2 - 2
dev-dist/sw.js

@@ -67,7 +67,7 @@ if (!self.define) {
     });
   };
 }
-define(['./workbox-b5f7729d'], (function (workbox) { 'use strict';
+define(['./workbox-5357ef54'], (function (workbox) { 'use strict';
 
   self.skipWaiting();
   workbox.clientsClaim();
@@ -82,7 +82,7 @@ define(['./workbox-b5f7729d'], (function (workbox) { 'use strict';
     "revision": "3ca0b8505b4bec776b69afdba2768812"
   }, {
     "url": "index.html",
-    "revision": "0.q5ukm3ve3dg"
+    "revision": "0.mnmr5vubgq"
   }], {});
   workbox.cleanupOutdatedCaches();
   workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {

+ 6 - 0
package-lock.json

@@ -18,6 +18,7 @@
         "clean-deep": "^3.4.0",
         "cos-js-sdk-v5": "^1.4.20",
         "cropperjs": "^1.5.13",
+        "crunker": "^2.4.0",
         "dayjs": "^1.11.7",
         "echarts": "^5.4.2",
         "eventemitter3": "^5.0.1",
@@ -3843,6 +3844,11 @@
         "node": ">= 8"
       }
     },
+    "node_modules/crunker": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmmirror.com/crunker/-/crunker-2.4.0.tgz",
+      "integrity": "sha512-3J0aBgLaB8jpGh6o+bisjYzOEkJLYxhnisHXogjY4fxYewh1aVCptWAz7RSTYPncCyxdfA+UaWAM1BrQiZvZgQ=="
+    },
     "node_modules/crypt": {
       "version": "0.0.2",
       "license": "BSD-3-Clause",

+ 1 - 0
package.json

@@ -32,6 +32,7 @@
     "clean-deep": "^3.4.0",
     "cos-js-sdk-v5": "^1.4.20",
     "cropperjs": "^1.5.13",
+    "crunker": "^2.4.0",
     "dayjs": "^1.11.7",
     "echarts": "^5.4.2",
     "eventemitter3": "^5.0.1",

+ 1 - 1
public/version.json

@@ -1 +1 @@
-{"version":1715235715601}
+{"version":1715319137641}

+ 28 - 8
src/components/Metronome/Metronome.vue

@@ -128,11 +128,14 @@
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, onMounted, h, watch } from 'vue';
+import { ref, reactive, onMounted, onUnmounted, h, watch } from 'vue';
 import { NInputNumber, NSelect, NSlider } from 'naive-ui';
 import useMetronome from './useMetronome';
 import Dragbom from '@/hooks/useDrag/dragbom';
+import { getCachePos, setCachePos } from './useMetronome';
+import { useUserStore } from '@/store/modules/users';
 
+const users = useUserStore();
 const emits = defineEmits<{
   (e: 'windowMet'): void;
   (e: 'closeMet'): void;
@@ -248,6 +251,13 @@ const { volumeNum, playState, speedNum, startPlay, pausePlay } = useMetronome(
 );
 
 onMounted(() => {
+  const cachePos = getCachePos(users.info.id);
+  if (cachePos) {
+    beatVal.value = cachePos.beatVal;
+    beatSymbol.value = cachePos.beatSymbol;
+    volumeNum.value = cachePos.volumeNum;
+    speedNum.value = cachePos.speedNum;
+  }
   /*  n-input-number 有bug  input-props设置maxlength不上去 */
   const iptDoms = document.querySelector(
     '.Metronome .MetronomeBox .optMid .n-input__input-el'
@@ -255,6 +265,16 @@ onMounted(() => {
   iptDoms?.setAttribute('maxlength', '3');
   getCircleBar(speedToScalc(speedNum.value));
 });
+
+onUnmounted(() => {
+  setCachePos(users.info.id, {
+    beatVal: beatVal.value,
+    beatSymbol: beatSymbol.value,
+    volumeNum: volumeNum.value,
+    speedNum: speedNum.value
+  });
+});
+
 watch(playState, () => {
   emits('playStateChange', playState.value);
 });
@@ -692,8 +712,8 @@ defineExpose({
       &::before {
         background: #47a7fe !important;
         border-radius: 6Px !important;
-        left: 6Px;
-        right: 6Px;
+        left: 9Px;
+        right: 9Px;
       }
     }
     .n-base-select-option__content {
@@ -775,8 +795,8 @@ defineExpose({
       &::before {
         background: #47a7fe !important;
         border-radius: 6Px !important;
-        left: 6Px;
-        right: 6Px;
+        left: 9Px;
+        right: 9Px;
       }
     }
     .beatSymbolImg {
@@ -818,9 +838,9 @@ defineExpose({
   .n-base-select-menu.n-select-menu {
   --n-border-radius: 10Px !important;
   .n-scrollbar-rail.n-scrollbar-rail--vertical{
-    right: 1Px;
-    width: 4Px;
-    opacity: 0.6;
+    right: 3Px;
+    --n-scrollbar-width:3Px !important;
+    opacity: 0.4;
   }
 }
 </style>

+ 131 - 34
src/components/Metronome/useMetronome.ts

@@ -1,65 +1,45 @@
-import { ref, Ref, watch, onUnmounted } from 'vue';
+import { ref, Ref, watch, onUnmounted, computed, onMounted } from 'vue';
 import tickWav from './audio/tick.wav';
 import tockWav from './audio/tock.wav';
+
 /*  播放相关 */
 export default function useMetronome(
   beatVal: Ref<string>,
   beatSymbol: Ref<string>
 ) {
-  let _timeTask: NodeJS.Timer;
-  const playerTick = new Audio(tickWav);
-  const playerTock = new Audio(tockWav);
   /* 音量 */
   const volumeNum = ref(100);
   watch(volumeNum, () => {
-    playerTick.volume = volumeNum.value / 100;
-    playerTock.volume = volumeNum.value / 100;
+    changeVolume(volumeNum.value / 100);
   });
   /* 播放状态 */
   const playState = ref<'play' | 'pause'>('pause');
   /* 速度 */
   const speedNum = ref(90);
-
+  /* 音频hooks */
+  const { start, stop, changeVolume } = useHandleAudio([tickWav, tockWav]);
   onUnmounted(() => {
     pausePlay();
   });
   // 开始播放
-  function startPlay() {
-    playerTick.currentTime = 0;
-    playerTock.currentTime = 0;
-    const timeArr = computeTimeArr();
-    handleBeatPlay(timeArr, speedNum.value);
-    playState.value = 'play';
+  async function startPlay() {
+    (await start(computeTimeArr.value, {
+      volume: volumeNum.value / 100,
+      playbackRate: speedNum.value / 60
+    })) && (playState.value = 'play');
   }
   //暂停播放
   function pausePlay() {
-    playerTick.pause();
-    playerTock.pause();
-    clearTimeout(_timeTask);
+    stop();
     playState.value = 'pause';
   }
-  function handleBeatPlay(timeArr: string[], speed: number) {
-    let index = 0;
-    timeTask();
-    function timeTask() {
-      const bateNum = Number(timeArr[index]);
-      // 当为index=0的时候第一排为重(当为0拍的时候不为重)
-      const playVm = index === 0 && bateNum !== 0 ? playerTick : playerTock;
-      playVm.play();
-      _timeTask = setTimeout(() => {
-        playVm.pause();
-        playVm.currentTime = 0;
-        index === timeArr.length - 1 ? (index = 0) : index++;
-        timeTask();
-      }, ((1000 * 60) / speed) * (bateNum || 1)); // 为0拍的时候 变为1
-    }
-  }
-  function computeTimeArr() {
+  const computeTimeArr = computed(() => {
     if (beatSymbol.value === '1') {
       return beatVal.value.split('-');
     }
     return beatSymbol.value.split('-');
-  }
+  });
+
   return {
     volumeNum,
     playState,
@@ -68,3 +48,120 @@ export default function useMetronome(
     pausePlay
   };
 }
+
+import Crunker from 'crunker';
+function useHandleAudio(files: [File | Blob | string, File | Blob | string]) {
+  const crunker = new Crunker();
+  async function handleBatetimeToAudio(
+    files: [File | Blob | string, File | Blob | string],
+    timeArr: string[],
+    playbackRate: number
+  ) {
+    try {
+      const buffersArr = await crunker.fetchAudio(...files);
+      const tickAudioBuff = buffersArr[0];
+      const tockAudioBuff = buffersArr[1];
+      let mergeAudio: AudioBuffer | undefined;
+      /* 处理音频合并 */
+      timeArr.map((time, index) => {
+        const timeNum = Number(time);
+        let nowBuff =
+          index === 0 && timeNum !== 0 ? tickAudioBuff : tockAudioBuff;
+        /*  当速度过快时候 响的时候大于整个拍子时候 对响进行裁剪  当间隔小于响的时候也进行裁剪 */
+        if (
+          1 / playbackRate - nowBuff.duration * timeArr.length <= 0 ||
+          (timeNum || 1) / playbackRate - nowBuff.duration <= 0
+        ) {
+          nowBuff = crunker.sliceAudio(
+            nowBuff,
+            0,
+            nowBuff.duration / playbackRate,
+            0,
+            0.12
+          );
+        }
+        mergeAudio
+          ? (mergeAudio = crunker.concatAudio([mergeAudio, nowBuff]))
+          : (mergeAudio = nowBuff);
+        mergeAudio = crunker.padAudio(
+          mergeAudio,
+          mergeAudio.duration - 0.01, // 预留0.01的安全距离 他这里有bug
+          (timeNum || 1) / playbackRate - nowBuff.duration
+        );
+      });
+      return mergeAudio;
+    } catch (err) {
+      console.log(err);
+      return undefined;
+    }
+  }
+
+  const audioCtx = crunker.context;
+  let audioSourceNode: AudioBufferSourceNode | null;
+  let audioGainNode: GainNode | null;
+  async function start(
+    timeArr: string[],
+    opt: { volume: number; playbackRate: number }
+  ) {
+    const buffer = await handleBatetimeToAudio(
+      files,
+      timeArr,
+      opt.playbackRate
+    );
+    if (buffer) {
+      audioSourceNode = audioCtx.createBufferSource();
+      audioSourceNode.buffer = buffer;
+      audioGainNode = audioCtx.createGain();
+      audioSourceNode.connect(audioGainNode);
+      audioGainNode.connect(audioCtx.destination);
+      audioGainNode.gain.value = opt.volume;
+      audioSourceNode.loop = true;
+      audioSourceNode.start();
+      return true;
+    } else {
+      return false;
+    }
+  }
+  function stop() {
+    audioSourceNode?.stop();
+    audioSourceNode = null;
+    audioGainNode = null;
+  }
+  function changeVolume(volume: number) {
+    audioGainNode && (audioGainNode.gain.value = volume);
+  }
+  return {
+    start,
+    stop,
+    changeVolume
+  };
+}
+
+// 缓存
+const localStorageName = 'metronomePos';
+export function getCachePos(
+  useId: string
+): null | undefined | Record<string, any> {
+  const localCachePos = localStorage.getItem(localStorageName);
+  if (localCachePos) {
+    try {
+      return JSON.parse(localCachePos)[useId + localStorageName];
+    } catch {
+      return null;
+    }
+  }
+  return null;
+}
+export function setCachePos(useId: string, pos: Record<string, any>) {
+  const localCachePos = localStorage.getItem(localStorageName);
+  let cachePosObj: Record<string, any> = {};
+  if (localCachePos) {
+    try {
+      cachePosObj = JSON.parse(localCachePos);
+    } catch {
+      //
+    }
+  }
+  cachePosObj[useId + localStorageName] = pos;
+  localStorage.setItem(localStorageName, JSON.stringify(cachePosObj));
+}