index.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  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. const tickData = reactive({
  11. len: 0,
  12. denominator: undefined as undefined | number,
  13. reduceLen: 0,
  14. tickEnd: false,
  15. /** 节拍器时间 */
  16. beatLengthInMilliseconds: 0,
  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. }, tickData.beatLengthInMilliseconds);
  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. * @param beat 节拍数
  93. * @param denominator 节拍器分母
  94. */
  95. export const handleInitTick = (beat: number, denominator?: number) => {
  96. tickData.len = beat;
  97. tickData.denominator = denominator;
  98. // 节拍器的个数除以2 直到小于等于4为止
  99. while (beat > 4 && beat % 2 === 0) {
  100. beat = beat / 2;
  101. }
  102. tickData.reduceLen = beat
  103. };
  104. /** 开始节拍器 */
  105. // 评测和练习模式,根据是否播放系统节拍器和mp3节拍器来控制是否发声,跟练模式百分之播
  106. export const handleStartTick = async () => {
  107. tickData.show = true;
  108. tickData.tickEnd = false;
  109. tickData.index = 0;
  110. tickData.beatLengthInMilliseconds = tickData.denominator ? 4 / tickData.denominator * (60 / state.speed) * 1000 : (60 / state.speed) * 1000;
  111. for(let i = 0; i <= useLen.value; i++){
  112. // 提前结束, 直接放回false
  113. if (tickData.tickEnd) return false;
  114. // Audio 标签播放音频
  115. const source = i === 0 ? audioData.tick : i === useLen.value ? null : audioData.tock;
  116. await handlePlay(i, source)
  117. }
  118. tickData.show = false;
  119. return true
  120. };
  121. export default defineComponent({
  122. name: "metronome",
  123. setup() {
  124. const posObj = {
  125. top: "0px",
  126. left: "0px"
  127. }
  128. function initPos() {
  129. const musicAndSelectionDom = document.querySelector("#musicAndSelection")
  130. const osmdSvgPage1Dom = musicAndSelectionDom?.querySelector("#osmdSvgPage1")
  131. const stafflineDom = osmdSvgPage1Dom?.querySelector(".staffline")
  132. const musicAndSelectionDomPos = musicAndSelectionDom?.getBoundingClientRect()
  133. const osmdSvgPage1DomPos = osmdSvgPage1Dom?.getBoundingClientRect()
  134. const stafflineDomPos = stafflineDom?.getBoundingClientRect()
  135. Object.assign(posObj,{
  136. top: (osmdSvgPage1DomPos?.top||0)-(musicAndSelectionDomPos?.top||0)-18+"px",
  137. left: (stafflineDomPos?.left||0)-(osmdSvgPage1DomPos?.left||0)+"px"
  138. })
  139. }
  140. onMounted(() => {
  141. initPos()
  142. Promise.all([createAudio(tickWav), createAudio(tockWav)]).then(
  143. ([tick, tock]) => {
  144. if (tick) {
  145. audioData.tick = tick;
  146. }
  147. if (tock) {
  148. audioData.tock = tock;
  149. }
  150. }
  151. );
  152. });
  153. return () => (
  154. tickData.show &&
  155. <div class={styles.dots} style={posObj}>
  156. {
  157. Array.from({ length: useLen.value }).map((item,index)=>{
  158. return <div class={[styles.dot,((useLen.value - tickData.index)<=index)&&styles.hide]}></div>
  159. })
  160. }
  161. </div>
  162. );
  163. },
  164. });