useMetronome.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import { ref, Ref, watch, onUnmounted, computed, onMounted } from 'vue';
  2. import tickWav from './audio/tick.wav';
  3. import tockWav from './audio/tock.wav';
  4. /* 播放相关 */
  5. export default function useMetronome(
  6. beatVal: Ref<string>,
  7. beatSymbol: Ref<string>
  8. ) {
  9. /* 音量 */
  10. const volumeNum = ref(100);
  11. watch(volumeNum, () => {
  12. changeVolume(volumeNum.value / 100);
  13. });
  14. /* 播放状态 */
  15. const playState = ref<'play' | 'pause'>('pause');
  16. /* 速度 */
  17. const speedNum = ref(90);
  18. /* 音频hooks */
  19. const { start, stop, changeVolume } = useHandleAudio([tickWav, tockWav]);
  20. onUnmounted(() => {
  21. pausePlay();
  22. });
  23. // 开始播放
  24. async function startPlay() {
  25. (await start(computeTimeArr.value, {
  26. volume: volumeNum.value / 100,
  27. playbackRate: speedNum.value / 60
  28. })) && (playState.value = 'play');
  29. }
  30. //暂停播放
  31. function pausePlay() {
  32. stop();
  33. playState.value = 'pause';
  34. }
  35. const computeTimeArr = computed(() => {
  36. if (beatSymbol.value === '1') {
  37. return beatVal.value.split('-');
  38. }
  39. return beatSymbol.value.split('-');
  40. });
  41. return {
  42. volumeNum,
  43. playState,
  44. speedNum,
  45. startPlay,
  46. pausePlay
  47. };
  48. }
  49. import Crunker from 'crunker';
  50. function useHandleAudio(files: [File | Blob | string, File | Blob | string]) {
  51. const crunker = new Crunker();
  52. async function handleBatetimeToAudio(
  53. files: [File | Blob | string, File | Blob | string],
  54. timeArr: string[],
  55. playbackRate: number
  56. ) {
  57. try {
  58. const buffersArr = await crunker.fetchAudio(...files);
  59. const tickAudioBuff = buffersArr[0];
  60. const tockAudioBuff = buffersArr[1];
  61. let mergeAudio: AudioBuffer | undefined;
  62. /* 处理音频合并 */
  63. timeArr.map((time, index) => {
  64. const timeNum = Number(time);
  65. let nowBuff =
  66. index === 0 && timeNum !== 0 ? tickAudioBuff : tockAudioBuff;
  67. /* 当速度过快时候 响的时候大于整个拍子时候 对响进行裁剪 当间隔小于响的时候也进行裁剪 */
  68. if (
  69. 1 / playbackRate - nowBuff.duration * timeArr.length <= 0 ||
  70. (timeNum || 1) / playbackRate - nowBuff.duration <= 0
  71. ) {
  72. nowBuff = crunker.sliceAudio(
  73. nowBuff,
  74. 0,
  75. nowBuff.duration / playbackRate,
  76. 0,
  77. 0.12
  78. );
  79. }
  80. mergeAudio
  81. ? (mergeAudio = crunker.concatAudio([mergeAudio, nowBuff]))
  82. : (mergeAudio = nowBuff);
  83. mergeAudio = crunker.padAudio(
  84. mergeAudio,
  85. mergeAudio.duration - 0.01, // 预留0.01的安全距离 他这里有bug
  86. (timeNum || 1) / playbackRate - nowBuff.duration
  87. );
  88. });
  89. return mergeAudio;
  90. } catch (err) {
  91. console.log(err);
  92. return undefined;
  93. }
  94. }
  95. const audioCtx = crunker.context;
  96. let audioSourceNode: AudioBufferSourceNode | null;
  97. let audioGainNode: GainNode | null;
  98. async function start(
  99. timeArr: string[],
  100. opt: { volume: number; playbackRate: number }
  101. ) {
  102. const buffer = await handleBatetimeToAudio(
  103. files,
  104. timeArr,
  105. opt.playbackRate
  106. );
  107. if (buffer) {
  108. audioSourceNode = audioCtx.createBufferSource();
  109. audioSourceNode.buffer = buffer;
  110. audioGainNode = audioCtx.createGain();
  111. audioSourceNode.connect(audioGainNode);
  112. audioGainNode.connect(audioCtx.destination);
  113. audioGainNode.gain.value = opt.volume;
  114. audioSourceNode.loop = true;
  115. audioSourceNode.start();
  116. return true;
  117. } else {
  118. return false;
  119. }
  120. }
  121. function stop() {
  122. audioSourceNode?.stop();
  123. audioSourceNode = null;
  124. audioGainNode = null;
  125. }
  126. function changeVolume(volume: number) {
  127. audioGainNode && (audioGainNode.gain.value = volume);
  128. }
  129. return {
  130. start,
  131. stop,
  132. changeVolume
  133. };
  134. }
  135. // 缓存
  136. const localStorageName = 'metronomePos';
  137. export function getCachePos(
  138. useId: string
  139. ): null | undefined | Record<string, any> {
  140. const localCachePos = localStorage.getItem(localStorageName);
  141. if (localCachePos) {
  142. try {
  143. return JSON.parse(localCachePos)[useId + localStorageName];
  144. } catch {
  145. return null;
  146. }
  147. }
  148. return null;
  149. }
  150. export function setCachePos(useId: string, pos: Record<string, any>) {
  151. const localCachePos = localStorage.getItem(localStorageName);
  152. let cachePosObj: Record<string, any> = {};
  153. if (localCachePos) {
  154. try {
  155. cachePosObj = JSON.parse(localCachePos);
  156. } catch {
  157. //
  158. }
  159. }
  160. cachePosObj[useId + localStorageName] = pos;
  161. localStorage.setItem(localStorageName, JSON.stringify(cachePosObj));
  162. }