index.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. import { defineComponent, reactive, onMounted, computed } from "vue";
  2. import tockAndTick from "/src/constant/tockAndTick.json";
  3. import { Howl } from "howler";
  4. import { Popup } from "vant";
  5. import styles from "./index.module.less";
  6. import state from "/src/state";
  7. import { browser } from "/src/utils/index";
  8. import tickWav from "/src/assets/tick.mp3";
  9. import tockWav from "/src/assets/tock.mp3";
  10. import { metronomeData } from "/src/helpers/metronome"
  11. const tickData = reactive({
  12. len: 0,
  13. reduceLen: 0,
  14. tickEnd: false,
  15. /** 节拍器时间 */
  16. beatLengthInMilliseconds: [] as number[],
  17. index: 0,
  18. show: false
  19. });
  20. // 是否使用系统节拍器
  21. const isUseSystemBeat = computed(()=>{
  22. return (state.playType === "play"&& !state.isOpenMetronome)||(state.playType === "sing" && !state.isSingOpenMetronome)
  23. })
  24. // 使用哪个节拍器个数
  25. const useLen = computed(()=>{
  26. return isUseSystemBeat.value ? tickData.reduceLen : tickData.len
  27. })
  28. let _time: NodeJS.Timeout
  29. // 关闭节拍器
  30. export function closeTick(){
  31. if (tickData.show) {
  32. _time && clearTimeout(_time)
  33. tickData.tickEnd = true
  34. tickData.show = false
  35. }
  36. }
  37. const tickPlayCb = (i: any, resolve: any, source: any) => {
  38. if (tickData.tickEnd) {
  39. resolve(i)
  40. return
  41. };
  42. // 第一个点,延迟100ms再消失
  43. if (i === 0) {
  44. setTimeout(() => {
  45. tickData.index++;
  46. }, 100);
  47. } else {
  48. tickData.index++;
  49. }
  50. // 当系统节拍器才播放声音,跟练模式需要播放系统节拍器的声音,评测模式,如果没有伴奏,也需要播放系统节拍器的声音
  51. if (source && (isUseSystemBeat.value || state.modeType === 'follow' || (state.modeType === 'evaluating' && !state.accompany)) ) {
  52. const beatVolume = state.setting.beatVolume / 100
  53. source.volume = beatVolume;
  54. if (source.volume <= 0) {
  55. source.muted = true
  56. } else {
  57. source.muted = false
  58. }
  59. source.play();
  60. }
  61. resolve(i);
  62. }
  63. const handlePlay = (i: number, source: any | null) => {
  64. return new Promise((resolve) => {
  65. if (i === 0 ) {
  66. tickPlayCb(i, resolve, source);
  67. } else {
  68. _time=setTimeout(() => {
  69. tickPlayCb(i, resolve, source);
  70. }, Math.abs(tickData.beatLengthInMilliseconds[i-1])*1000/state.basePlayRate);
  71. }
  72. });
  73. };
  74. // HTMLAudioElement 音频
  75. const audioData = reactive({
  76. tick: null as unknown as HTMLAudioElement,
  77. tock: null as unknown as HTMLAudioElement,
  78. });
  79. const createAudio = (src: string): Promise<HTMLAudioElement | null> => {
  80. return new Promise((resolve) => {
  81. const a = new Audio(src);
  82. a.load();
  83. a.onloadedmetadata = () => {
  84. resolve(a);
  85. };
  86. a.onerror = () => {
  87. resolve(null);
  88. };
  89. });
  90. };
  91. /** 设置节拍器
  92. */
  93. export const handleInitTick = () => {
  94. const beatLen = metronomeData.firstBeatTypeArr.length * (state.repeatedBeats ? 2 : 1)
  95. const beatLengthInMilliseconds = metronomeData.firstBeatTypeArr.map(item=>{
  96. return item*state.times[0].measureLength
  97. })
  98. tickData.beatLengthInMilliseconds = [...beatLengthInMilliseconds,...(state.repeatedBeats ? beatLengthInMilliseconds : [])]
  99. tickData.len = beatLen;
  100. // // 节拍器的个数除以2 直到小于等于4为止
  101. // while (beat > 4 && beat % 2 === 0) {
  102. // beat = beat / 2;
  103. // }
  104. tickData.reduceLen = beatLen
  105. };
  106. /** 开始节拍器 */
  107. // 评测和练习模式,根据是否播放系统节拍器和mp3节拍器来控制是否发声,跟练模式百分之播
  108. export const handleStartTick = async () => {
  109. tickData.show = true;
  110. tickData.tickEnd = false;
  111. tickData.index = 0;
  112. for(let i = 0; i <= useLen.value; i++){
  113. // 提前结束, 直接放回false
  114. if (tickData.tickEnd) return false;
  115. // Audio 标签播放音频
  116. const source = tickData.beatLengthInMilliseconds[i] < 0 ? audioData.tick : i === useLen.value ? null : audioData.tock;
  117. await handlePlay(i, source)
  118. }
  119. tickData.show = false;
  120. return true
  121. };
  122. export default defineComponent({
  123. name: "metronome",
  124. setup() {
  125. const posObj = {
  126. top: "0px",
  127. left: "0px"
  128. }
  129. function initPos() {
  130. const musicAndSelectionDom = document.querySelector("#musicAndSelection")
  131. const osmdSvgPage1Dom = musicAndSelectionDom?.querySelector("#osmdSvgPage1")
  132. const stafflineDom = osmdSvgPage1Dom?.querySelector(".staffline")
  133. const musicAndSelectionDomPos = musicAndSelectionDom?.getBoundingClientRect()
  134. const osmdSvgPage1DomPos = osmdSvgPage1Dom?.getBoundingClientRect()
  135. const stafflineDomPos = stafflineDom?.getBoundingClientRect()
  136. Object.assign(posObj,{
  137. top: (osmdSvgPage1DomPos?.top||0)-(musicAndSelectionDomPos?.top||0)-18+"px",
  138. left: (stafflineDomPos?.left||0)-(osmdSvgPage1DomPos?.left||0)+"px"
  139. })
  140. }
  141. onMounted(() => {
  142. initPos()
  143. Promise.all([createAudio(tickWav), createAudio(tockWav)]).then(
  144. ([tick, tock]) => {
  145. if (tick) {
  146. audioData.tick = tick;
  147. }
  148. if (tock) {
  149. audioData.tock = tock;
  150. }
  151. }
  152. );
  153. });
  154. return () => (
  155. tickData.show &&
  156. <div class={styles.dots} style={posObj}>
  157. {
  158. Array.from({ length: useLen.value }).map((item,index)=>{
  159. return <div class={[styles.dot,((useLen.value - tickData.index)<=index)&&styles.hide]}></div>
  160. })
  161. }
  162. </div>
  163. );
  164. },
  165. });