metronome.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576
  1. /**
  2. * 曲谱节拍器
  3. * auth: lsq
  4. * time: 2022.11.14
  5. */
  6. import { reactive, watch } from "vue";
  7. import { tickUrl as tick, tockUrl as tock } from "/src/constant/audios";
  8. import { browser } from "/src/utils/index";
  9. import state from "/src/state";
  10. import { Howl } from "howler";
  11. import tockAndTick from "/src/constant/tockAndTick.json";
  12. import tickWav from "/src/assets/tick.wav";
  13. import tockWav from "/src/assets/tock.wav";
  14. type IOptions = {
  15. speed: number;
  16. };
  17. const browserInfo = browser();
  18. let tipsTimer: any = null; // 光标提示定时器
  19. // HTMLAudioElement 音频
  20. const audioData = reactive({
  21. tick: null as unknown as HTMLAudioElement,
  22. tock: null as unknown as HTMLAudioElement,
  23. });
  24. let tickTockPlayTime = 0;
  25. export const metronomeData = reactive({
  26. disable: true,
  27. initPlayerState: false,
  28. lineShow: false,
  29. isClick: false,
  30. metro: null as unknown as Metronome,
  31. metroList: [] as number[],
  32. activeList: [] as number[],
  33. metroMeasure: [] as any[],
  34. activeIndex: null as unknown as number,
  35. activeMetro: {} as any,
  36. cursorMode: 1 as number, // 光标模式:1:音符指针;2:节拍指针;3:关闭指针
  37. cursorTips: '' as string, // 光标模式提示文字
  38. followAudioIndex: 1, // 当前的拍数
  39. totalNumerator: 2, // 总拍数
  40. });
  41. watch(
  42. () => metronomeData.cursorMode,
  43. () => {
  44. const img: HTMLElement = document.querySelector("#cursorImg-0")!;
  45. if (img) {
  46. switch (metronomeData.cursorMode) {
  47. case 1:
  48. img.classList.remove("lineHide");
  49. img.style.opacity = 'inherit'
  50. metronomeData.cursorTips = '您已切换到指针跟随音符播放';
  51. img.style.opacity = 'inherit'
  52. break;
  53. case 2:
  54. img.classList.add("lineHide");
  55. img.style.opacity = 'inherit'
  56. metronomeData.cursorTips = '您已切换到指针跟随节拍播放';
  57. // console.log('光标',img)
  58. break;
  59. case 3:
  60. img.style.opacity = '0'
  61. metronomeData.cursorTips = '您已关闭指针显示';
  62. // console.log('隐藏光标')
  63. break;
  64. default:
  65. break;
  66. }
  67. hideCursorTip()
  68. }
  69. }
  70. );
  71. // 切换隐藏光标
  72. const toggleLine = () => {
  73. if (!metronomeData.lineShow) return;
  74. const img: HTMLElement = document.querySelector("#cursorImg-0")!;
  75. if (img) {
  76. if (state.times[state.activeNoteIndex].multipleRestMeasures) {
  77. img.classList.remove("lineHide");
  78. } else {
  79. img.classList.add("lineHide");
  80. }
  81. }
  82. };
  83. watch(
  84. () => metronomeData.lineShow,
  85. () => {
  86. const img: HTMLElement = document.querySelector("#cursorImg-0")!;
  87. if (img) {
  88. if (metronomeData.lineShow) {
  89. img.classList.add("lineHide");
  90. } else {
  91. img.classList.remove("lineHide");
  92. }
  93. }
  94. }
  95. );
  96. class Metronome {
  97. playType = "tick";
  98. source = null as any; // 创建音频源头
  99. source1 = null as any;
  100. source2 = null as any;
  101. constructor(option?: IOptions) {}
  102. init(times: any[]) {
  103. this.calculation(times);
  104. metronomeData.activeList = [];
  105. }
  106. initPlayer() {
  107. // if (!this.source1) {
  108. // this.source1 = this.loadAudio1();
  109. // }
  110. // if (!this.source2) {
  111. // this.source2 = this.loadAudio2();
  112. // }
  113. // metronomeData.initPlayerState = true;
  114. Promise.all([this.createAudio(tickWav), this.createAudio(tockWav)]).then(
  115. ([tick, tock]) => {
  116. if (tick) {
  117. audioData.tick = tick;
  118. }
  119. if (tock) {
  120. audioData.tock = tock;
  121. }
  122. metronomeData.initPlayerState = true;
  123. }
  124. );
  125. }
  126. createAudio = (src: string): Promise<HTMLAudioElement | null> => {
  127. return new Promise((resolve) => {
  128. // const a = new Audio(src + '?v=' + Date.now());
  129. const a = new Audio(src);
  130. a.load();
  131. a.onloadedmetadata = () => {
  132. resolve(a);
  133. };
  134. a.onerror = () => {
  135. resolve(null);
  136. };
  137. });
  138. };
  139. // 播放
  140. sound = (currentTime: number) => {
  141. if (!state.sectionStatus){
  142. currentTime = setCurrentTime(currentTime);
  143. }
  144. let index = -1;
  145. let activeMetro = -1;
  146. for (let i = 0; i < metronomeData.metroList.length; i++) {
  147. const item = metronomeData.metroList[i];
  148. if (currentTime >= item) {
  149. // console.log(currentTime , item)
  150. index = i;
  151. activeMetro = item;
  152. } else {
  153. break;
  154. }
  155. }
  156. if (index > -1 && metronomeData.activeIndex !== index) {
  157. metronomeData.activeIndex = index;
  158. // console.log("播放", metronomeData.activeIndex);
  159. metronomeData.activeMetro = this.getStep(activeMetro);
  160. // console.log("🚀 ~ metronomeData.activeMetro",metronomeData.activeMetro.measureNumberIndex, metronomeData.activeMetro.index)
  161. tickTockPlayTime = currentTime
  162. this.playAudio();
  163. metronomeData.isClick = false;
  164. return;
  165. }
  166. toggleLine()
  167. metronomeData.isClick = false;
  168. };
  169. // 播放
  170. playAudio = () => {
  171. if (!metronomeData.initPlayerState || state.playState === 'paused') return;
  172. const beatVolume = state.setting.beatVolume / 100
  173. // this.source = metronomeData.activeMetro?.index === 0 ? this.source1 : this.source2;
  174. // this.source.volume(metronomeData.disable || state.playState === 'paused' ? 0 : beatVolume);
  175. // Audio 播放音频
  176. this.source = metronomeData.activeMetro?.index === 0 ? audioData.tick : audioData.tock;
  177. this.source.volume = metronomeData.disable ? 0 : beatVolume;
  178. if (this.source.volume <= 0) {
  179. this.source.muted = true
  180. } else {
  181. this.source.muted = false
  182. }
  183. // console.log('节拍器播放的时间',tickTockPlayTime)
  184. this.source.play();
  185. };
  186. /**
  187. * 跟练模式播放,跟练模式没有曲子音频播放器
  188. */
  189. simulatePlayAudio = () => {
  190. // console.log(333, metronomeData.followAudioIndex)
  191. if (!metronomeData.initPlayerState) return;
  192. const beatVolume = state.setting.beatVolume / 100
  193. // this.source = metronomeData.followAudioIndex === 1 ? this.source1 : this.source2;
  194. // Audio 播放音频
  195. this.source = metronomeData.followAudioIndex === 1 ? audioData.tick : audioData.tock;
  196. // this.source.volume(metronomeData.disable ? 0 : beatVolume);
  197. this.source.volume = metronomeData.disable ? 0 : beatVolume
  198. /**
  199. * https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLMediaElement/volume
  200. * volume属性在部分ios手机的Safari浏览器不被支持
  201. */
  202. if (this.source.volume <= 0) {
  203. this.source.muted = true
  204. } else {
  205. this.source.muted = false
  206. }
  207. console.log('音量',this.source,this.source.volume)
  208. this.source.play();
  209. metronomeData.followAudioIndex += 1;
  210. metronomeData.followAudioIndex = metronomeData.followAudioIndex > metronomeData.totalNumerator ? 1 : metronomeData.followAudioIndex;
  211. };
  212. // 切换
  213. selectPlay() {}
  214. loadAudio1 = () => {
  215. return new Howl({
  216. src: tockAndTick.tick,
  217. // 如果是ios手机,需要强制使用audio,不然部分系统版本第一次播放没有声音
  218. // html5: browserInfo.ios,
  219. });
  220. };
  221. loadAudio2 = () => {
  222. return new Howl({
  223. src: tockAndTick.tock,
  224. // html5: browserInfo.ios,
  225. });
  226. };
  227. getStep(time: number) {
  228. for (let i = 0; i < metronomeData.metroMeasure.length; i++) {
  229. const list = metronomeData.metroMeasure[i];
  230. const item = list.find((n: any) => n.time === time);
  231. if (item) {
  232. // console.log('index',item)
  233. return item;
  234. }
  235. }
  236. return {};
  237. }
  238. // 计算 所有的拍子的时间
  239. calculation(times: any[]) {
  240. // console.log("🚀 ~ times", times);
  241. // 1.统计有多少小节
  242. const measures: any[] = [];
  243. let xmlNumber = -1;
  244. for (let i = 0; i < times.length; i++) {
  245. const note = times[i];
  246. const measureNumberXML = note?.noteElement?.sourceMeasure?.MeasureNumberXML;
  247. // console.log("🚀 ~ note?.noteElement?.sourceMeasure", note?.noteElement?.sourceMeasure)
  248. // console.log("🚀 ~ measureNumberXML", measureNumberXML, note)
  249. // console.log("🚀 ~ measureNumberXML", note)
  250. const measureListIndex = note?.noteElement?.sourceMeasure?.measureListIndex;
  251. if (measureNumberXML > -1) {
  252. if (measureNumberXML != xmlNumber) {
  253. const m = {
  254. measureNumberXML: measureNumberXML,
  255. measureNumberIndex: measureListIndex,
  256. numerator: note?.noteElement?.sourceMeasure?.ActiveTimeSignature?.numerator || 0,
  257. start: note.measures[0].time,
  258. end: note.measures[note.measures.length - 1].endtime,
  259. time: note.measures[note.measures.length - 1].endtime - note.measures[0].time,
  260. stave_x: note?.noteElement?.sourceMeasure?.verticalMeasureList?.[0]?.stave?.x || 0,
  261. end_x: note?.stave?.end_x || 0 || 0,
  262. stepList: [] as number[],
  263. svgs: [] as any[],
  264. isRestFlag: note.isRestFlag,
  265. };
  266. // 2.统计小节的拍数
  267. // 3.统计小节的时长, 开始时间,结束时间
  268. // console.log(measureNumberXML,note.measures, times.filter((n: any) => n?.noteElement?.sourceMeasure?.measureListIndex == measureListIndex))
  269. if ([121].includes(state.subjectId)) {
  270. const _measures = times.filter((n: any) => n?.noteElement?.sourceMeasure?.measureListIndex == measureListIndex);
  271. note.measures = _measures;
  272. m.start = note.measures[0].time;
  273. m.end = note.measures[note.measures.length - 1].endtime;
  274. m.time = note.measures[note.measures.length - 1].endtime - note.measures[0].time;
  275. try {
  276. const tickables = note.noteElement.sourceMeasure.verticalMeasureList.reduce((arr: any[], value: any) => {
  277. arr.push(...value.vfVoices["1"].tickables);
  278. return arr;
  279. }, []);
  280. const xList: any[] = [];
  281. m.svgs = tickables
  282. .map((n: any) => {
  283. const x = n.getBoundingBox().x;
  284. if (!xList.includes(x) && n.duration !== "w") {
  285. xList.push(x);
  286. n._start_x = x;
  287. return n;
  288. }
  289. })
  290. .filter(Boolean)
  291. .sort((a: any, b: any) => a._start_x - b._start_x);
  292. // console.log(measureNumberXML, m.svgs)
  293. } catch (error) {
  294. console.log(error);
  295. }
  296. m.stepList = calculateMutilpleMetroStep(note.measures, m);
  297. } else {
  298. m.stepList = calculateMetroStep(note.measures, m);
  299. }
  300. measures.push(m);
  301. xmlNumber = measureNumberXML;
  302. }
  303. }
  304. }
  305. // console.log(measures, measures.length);
  306. let metroList: number[] = [];
  307. const metroMeasure: any[] = [];
  308. // 4.按照拍数将时长平均分配
  309. try {
  310. for (let i = 0; i < measures.length; i++) {
  311. const measure = measures[i];
  312. const noteStep = measure.time / measure.numerator;
  313. // console.log("🚀 ~ measure.measureNumberXML",measure.measureNumberXML, noteStep)
  314. const WIDTH = [121].includes(state.subjectId) ? 95 : 100;
  315. const widthStep = WIDTH / (measure.numerator + 1);
  316. metroMeasure[i] = [] as number[];
  317. // console.log('stepList', [...measure.stepList], measure.measureNumberXML)
  318. for (let j = 0; j < measure.numerator; j++) {
  319. const time = noteStep * j + measure.start;
  320. metroList.push(time);
  321. let left = "";
  322. if (measure.stepList[j]) {
  323. left = measure.stepList[j] + "px";
  324. } else {
  325. const preLeft = measure.stepList[j - 1];
  326. left = !preLeft ? `${widthStep}%` : preLeft.toString().indexOf("%") > -1 ? `${preLeft} + ${widthStep}%` : `${preLeft}px + ${widthStep}%`;
  327. measure.stepList[j] = left;
  328. }
  329. metroMeasure[i].push({
  330. index: j,
  331. time,
  332. // left: (measure.stepList[j] ? measure.stepList[j] + 'px' : (j + 1) * widthStep + '%'),
  333. left: left?.indexOf("%") > -1 ? `calc(${left})` : left,
  334. measureNumberXML: measure.measureNumberXML,
  335. isRestFlag: measure.isRestFlag,
  336. });
  337. }
  338. }
  339. } catch (error) {
  340. console.log(error);
  341. }
  342. console.log('节拍器',metroList, metroMeasure);
  343. // 5.得到所有的节拍时间
  344. metronomeData.metroList = metroList;
  345. metronomeData.metroMeasure = metroMeasure;
  346. // console.log(9999,metroList,7777,metroMeasure)
  347. metronomeData.activeMetro = metroMeasure[0]?.[0] || {};
  348. }
  349. }
  350. // 计算拍子的时值
  351. function calculateMetroStep(arr: any[], m: any): number[] {
  352. const measureLength = arr.reduce((total: number, item: any) => {
  353. total += item._noteLength;
  354. return total;
  355. }, 0);
  356. const clap = measureLength / m.numerator;
  357. if (arr.length === 1) {
  358. const wholeNote = arr[0].svgElement;
  359. if (wholeNote && !wholeNote.isRest()) {
  360. const measure_bbox = wholeNote?.attrs?.el?.parentElement?.parentElement?.getBoundingClientRect?.() || { x: 0, right: 0 };
  361. let bbox = wholeNote?.attrs?.el?.getBoundingClientRect?.() || { x: 0 };
  362. let stepWidth = Math.abs(measure_bbox.right - bbox.x) / m.numerator;
  363. let stepList: number[] = [];
  364. for (let i = 0; i < m.numerator; i++) {
  365. stepList.push(bbox.x - measure_bbox.x + i * stepWidth);
  366. }
  367. // console.log("🚀 ~ stepList:", stepList, m.measureNumberXML)
  368. return stepList;
  369. }
  370. try {
  371. // 开头是休止符
  372. if (m.measureNumberXML === 1 && wholeNote && wholeNote.isRest()) {
  373. const measure_bbox = wholeNote?.attrs?.el?.parentElement?.parentElement?.getBoundingClientRect?.() || { x: 0, right: 0 };
  374. let bbox = wholeNote?.attrs?.el?.getBoundingClientRect?.() || { x: 0 };
  375. let stepWidth = Math.abs(measure_bbox.right - bbox.x) / m.numerator;
  376. let stepList: any[] = [];
  377. // 第一小节是休止符,节拍指针应该等分宽度
  378. const widthStep = 100 / (m.numerator + 1);
  379. // for (let i = -1; i < m.numerator - 1; i++) {
  380. // stepList.push(bbox.x - measure_bbox.x + i * stepWidth);
  381. // }
  382. // for (let i = 1; i <= m.numerator; i++) {
  383. // stepList.push(widthStep * i + '%');
  384. // }
  385. // console.log(wholeNote?.attrs?.el, m.measureNumberXML)
  386. // console.log("🚀 ~ stepList:", stepList, m.measureNumberXML)
  387. return stepList;
  388. }
  389. } catch (error) {
  390. console.log("🚀 ~ error:", error);
  391. }
  392. return [];
  393. }
  394. // console.log("🚀 ~ arr", [...arr],`小节总时值: ${measureLength}`, clap, m.measureNumberXML);
  395. let totalLength = 0;
  396. let notes: any[] = [];
  397. let stepList: number[] = [];
  398. for (let i = 0; i < arr.length; i++) {
  399. const item = arr[i];
  400. item.index = i;
  401. const noteLength = item._noteLength;
  402. totalLength += noteLength;
  403. // 大于一拍
  404. const exceedStep = Math.floor(totalLength / clap);
  405. // console.log(`note`, item?.svgElement?.attrs?.el,notes.length,{noteLength, exceedStep,clap}, m.measureNumberXML)
  406. if (exceedStep >= 1) {
  407. totalLength -= clap;
  408. // 一拍
  409. const measure_bbox = item?.svgElement?.attrs?.el?.parentElement?.parentElement?.getBoundingClientRect?.() || { x: 0 };
  410. if (notes.length > 0) {
  411. let bbox = notes[0]?.svgElement?.attrs?.el?.getBoundingClientRect?.() || { x: 0 };
  412. let x: any = bbox.x - measure_bbox.x;
  413. if (notes[0]._noteLength / clap >= 1) {
  414. const nextNote = arr[notes[0].index + 1]?.svgElement?.attrs?.el?.getBoundingClientRect?.() || { x: measure_bbox.right } || { x: 0 };
  415. const stepWidth = Math.abs(bbox.x - nextNote.x) / 2;
  416. x = bbox.x - measure_bbox.x + stepWidth;
  417. // console.log(`音符超一拍`, notes[0]?.svgElement?.attrs?.el, arr[notes[0].index + 1]?.svgElement?.attrs?.el, bbox.x - nextNote.x, stepWidth, m.measureNumberXML);
  418. }
  419. // console.log(`一拍`, notes[0]?.svgElement?.attrs?.el, m.measureNumberXML, notes[0]._noteLength , clap, 'aa')
  420. stepList.push(x);
  421. } else {
  422. let bbox = item?.svgElement?.attrs?.el?.getBoundingClientRect?.() || { x: 0 };
  423. let x: any = bbox.x - measure_bbox.x;
  424. // console.log(`一拍`, item?.svgElement?.attrs?.el, m.measureNumberXML)
  425. stepList.push(x);
  426. }
  427. notes = [];
  428. let bbox = item?.svgElement?.attrs?.el?.getBoundingClientRect?.() || { x: 0 };
  429. let x: any = bbox.x - measure_bbox.x;
  430. let stepWidth = 0;
  431. if (exceedStep > 1) {
  432. // 二拍以上
  433. const nextNote = arr[i + 1]?.svgElement?.attrs?.el?.getBoundingClientRect?.() || { x: measure_bbox.right } || { x: 0 };
  434. stepWidth = Math.abs(bbox.x - nextNote.x) / exceedStep;
  435. // console.log("二拍以上 ~ nextNote:",bbox.x , nextNote.x,stepWidth, item?.svgElement?.attrs?.el,arr[i + 1]?.svgElement?.attrs?.el, exceedStep);
  436. }
  437. for (let j = 1; j < exceedStep; j++) {
  438. totalLength -= clap;
  439. // console.log(`超一拍`,item?.svgElement?.attrs?.el, m.measureNumberXML)
  440. stepList.push(x + stepWidth * j);
  441. }
  442. }
  443. //有时值就将音符加入
  444. if (totalLength > Number.EPSILON && totalLength > 0) {
  445. notes.push(item);
  446. }
  447. }
  448. stepList = stepList.reduce((list: any[], n: number) => {
  449. if (list.includes(n)) {
  450. list.push(undefined as any);
  451. } else {
  452. list.push(n);
  453. }
  454. return list;
  455. }, []);
  456. // console.log("stepList", [...stepList], m.measureNumberXML);
  457. return stepList;
  458. }
  459. // 计算单声部多声轨的拍子的时值
  460. function calculateMutilpleMetroStep(arr: any[], m: any): number[] {
  461. // console.log("🚀 ~ m:", [...m.svgs])
  462. const step = m.time / m.numerator;
  463. const measure_bbox = arr[0]?.svgElement?.attrs?.el?.parentElement?.parentElement?.getBoundingClientRect?.() || { x: 0 };
  464. if (arr.length === 1) {
  465. const staveNote = m.svgs[0];
  466. // 大于一拍
  467. let bbox = staveNote?.attrs?.el?.getBoundingClientRect?.() || { x: 0 };
  468. if (staveNote && !staveNote.isRest()) {
  469. return [bbox.x - measure_bbox.x];
  470. }
  471. return [];
  472. }
  473. // console.log("🚀 ~ arr", arr, step, m.measureNumberXML);
  474. let total = 0;
  475. let notes: any[] = [];
  476. let stepList: number[] = [];
  477. for (let i = 0; i < arr.length; i++) {
  478. const item = arr[i];
  479. item._index = i;
  480. const noteTime = item.endtime - item.time;
  481. total += noteTime;
  482. let svgEle = m.svgs[i]?.attrs?.el;
  483. // 大于一拍
  484. let bbox = svgEle?.getBoundingClientRect?.() || { x: 0 };
  485. // console.log(m.measureNumberXML, svgEle, i)
  486. if (noteTime > step) {
  487. total -= step;
  488. // console.log('超过一拍了', notes, m.measureNumberXML)
  489. let x = bbox.x - measure_bbox.x;
  490. if (notes.length > 0) {
  491. svgEle = m.svgs[notes[0]._index]?.attrs?.el;
  492. bbox = svgEle?.getBoundingClientRect?.() || { x: 0 };
  493. x = bbox.x - measure_bbox.x;
  494. }
  495. stepList.push(x);
  496. notes = [];
  497. } else {
  498. notes.push(item);
  499. }
  500. // console.log(notes)
  501. if (Math.abs(total - step) < 0.001) {
  502. let x = bbox.x - measure_bbox.x;
  503. if (notes.length > 0) {
  504. svgEle = m.svgs[notes[0]._index]?.attrs?.el;
  505. bbox = svgEle?.getBoundingClientRect?.() || { x: 0 };
  506. x = bbox.x - measure_bbox.x;
  507. }
  508. // console.log("一拍",svgEle,notes,m.svgs, m.measureNumberXML);
  509. stepList.push(x);
  510. total = 0;
  511. notes = [];
  512. }
  513. }
  514. stepList = stepList.reduce((list: any[], n: number) => {
  515. if (list.includes(n)) {
  516. list.push(undefined as any);
  517. } else {
  518. list.push(n);
  519. }
  520. return list;
  521. }, []); //Array.from(new Set(stepList))
  522. // console.log('stepList', stepList, m.measureNumberXML)
  523. return stepList;
  524. }
  525. // 延迟兼容处理
  526. function setCurrentTime(time: number) {
  527. if (browserInfo.huawei || browserInfo.xiaomi) {
  528. time += 0.125;
  529. } else if (browserInfo.android) {
  530. time += 0.11;
  531. } else if (browserInfo.ios) {
  532. time += 0.01;
  533. }
  534. return time;
  535. }
  536. // 自动隐藏光标提示
  537. function hideCursorTip() {
  538. if (!tipsTimer) {
  539. tipsTimer = setTimeout(() => {
  540. metronomeData.cursorTips = ''
  541. clearTimeout(tipsTimer)
  542. tipsTimer = null
  543. }, 2000);
  544. } else {
  545. clearTimeout(tipsTimer)
  546. tipsTimer = setTimeout(() => {
  547. metronomeData.cursorTips = ''
  548. clearTimeout(tipsTimer)
  549. tipsTimer = null
  550. }, 2000);
  551. }
  552. }
  553. export default Metronome;