metronome.ts 21 KB

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