|
@@ -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));
|
|
|
+}
|