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, beatSymbol: Ref ) { /* 音量 */ const volumeNum = ref(100); watch(volumeNum, () => { changeVolume(volumeNum.value / 100); }); /* 播放状态 */ const playState = ref<'play' | 'pause'>('pause'); /* 速度 */ const speedNum = ref(90); /* 音频hooks */ const { start, stop, changeVolume } = useHandleAudio([tickWav, tockWav]); onUnmounted(() => { pausePlay(); }); // 开始播放 async function startPlay() { (await start(computeTimeArr.value, { volume: volumeNum.value / 100, playbackRate: speedNum.value / 60 })) && (playState.value = 'play'); } //暂停播放 function pausePlay() { stop(); playState.value = 'pause'; } const computeTimeArr = computed(() => { if (beatSymbol.value === '1') { return beatVal.value.split('-'); } return beatSymbol.value.split('-'); }); return { volumeNum, playState, speedNum, startPlay, 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 { 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) { const localCachePos = localStorage.getItem(localStorageName); let cachePosObj: Record = {}; if (localCachePos) { try { cachePosObj = JSON.parse(localCachePos); } catch { // } } cachePosObj[useId + localStorageName] = pos; localStorage.setItem(localStorageName, JSON.stringify(cachePosObj)); }