index.tsx 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. import { defineComponent, reactive, onMounted } 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 browserInfo = browser();
  11. export const tickData = reactive({
  12. list: [] as number[],
  13. len: 0,
  14. tickEnd: false,
  15. /** 节拍器时间 */
  16. beatLengthInMilliseconds: 0,
  17. state: "",
  18. source1: "" as unknown as Howl,
  19. source2: "" as unknown as Howl,
  20. index: 0,
  21. show: false,
  22. });
  23. const handlePlay = (i: number, source: any | null) => {
  24. return new Promise((resolve) => {
  25. setTimeout(() => {
  26. if (tickData.tickEnd) {
  27. resolve(i)
  28. return
  29. };
  30. tickData.index++;
  31. if (source) {
  32. const beatVolume = state.setting.beatVolume / 100
  33. source.volume = beatVolume;
  34. if (source.volume <= 0) {
  35. source.muted = true
  36. } else {
  37. source.muted = false
  38. }
  39. source.play();
  40. }
  41. resolve(i);
  42. }, tickData.beatLengthInMilliseconds);
  43. });
  44. };
  45. // HTMLAudioElement 音频
  46. const audioData = reactive({
  47. tick: null as unknown as HTMLAudioElement,
  48. tock: null as unknown as HTMLAudioElement,
  49. });
  50. const createAudio = (src: string): Promise<HTMLAudioElement | null> => {
  51. return new Promise((resolve) => {
  52. const a = new Audio(src + '?v=' + Date.now());
  53. a.load();
  54. a.onloadedmetadata = () => {
  55. resolve(a);
  56. };
  57. a.onerror = () => {
  58. resolve(null);
  59. };
  60. });
  61. };
  62. /** 设置节拍器
  63. * @param beatLengthInMilliseconds 节拍间隔时间
  64. * @param beat 节拍数
  65. */
  66. export const handleInitTick = (beatLengthInMilliseconds: number, beat: number) => {
  67. tickData.state = "";
  68. tickData.beatLengthInMilliseconds = beatLengthInMilliseconds
  69. tickData.len = beat;
  70. };
  71. /** 开始节拍器 */
  72. export const handleStartTick = async () => {
  73. tickData.show = true;
  74. tickData.tickEnd = false;
  75. if (tickData.state !== "ok") {
  76. tickData.source1 = new Howl({
  77. src: tockAndTick.tick,
  78. // 如果是ios手机,需要强制使用audio,不然部分系统版本第一次播放没有声音
  79. html5: browserInfo.ios,
  80. });
  81. tickData.source2 = new Howl({
  82. src: tockAndTick.tock,
  83. });
  84. tickData.state = "ok";
  85. }
  86. tickData.index = 0;
  87. tickData.beatLengthInMilliseconds = (60 / state.speed) * 1000;
  88. for(let i = 0; i <= tickData.len; i++){
  89. // 提前结束, 直接放回false
  90. if (tickData.tickEnd) return false;
  91. // Howl 插件播放音频
  92. // const source = i === 0 ? tickData.source1 : i === tickData.len ? null : tickData.source2;
  93. // Audio 标签播放音频
  94. const source = i === 0 ? audioData.tick : i === tickData.len ? null : audioData.tock;
  95. await handlePlay(i, source)
  96. }
  97. tickData.show = false;
  98. return true
  99. };
  100. export default defineComponent({
  101. name: "metronome",
  102. setup() {
  103. /** 手动关闭 */
  104. const handleClose = () => {
  105. tickData.tickEnd = true
  106. };
  107. onMounted(() => {
  108. Promise.all([createAudio(tickWav), createAudio(tockWav)]).then(
  109. ([tick, tock]) => {
  110. if (tick) {
  111. audioData.tick = tick;
  112. }
  113. if (tock) {
  114. audioData.tock = tock;
  115. }
  116. }
  117. );
  118. });
  119. return () => (
  120. <Popup class={[styles.popup, 'normal-close']} v-model:show={tickData.show} closeable onClickCloseIcon={handleClose}>
  121. <div class={styles.dots}>
  122. {Array(tickData.len)
  123. .fill(0)
  124. .map((n, i) => (
  125. <div class={[styles.dot, tickData.index > i && styles.active, tickData.index > i && i === 0 && styles.one]}></div>
  126. ))}
  127. </div>
  128. </Popup>
  129. );
  130. },
  131. });