metronome.ts 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702
  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: 2 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. findMetronomePosition = (currentTime: number) => {
  174. console.log('取消选段',currentTime)
  175. const originTime = currentTime;
  176. // if (!state.sectionStatus){
  177. // currentTime = setCurrentTime(currentTime);
  178. // }
  179. let index = -1;
  180. let activeMetro = -1;
  181. for (let i = 0; i < metronomeData.metroList.length; i++) {
  182. const item = metronomeData.metroList[i];
  183. if (currentTime >= item) {
  184. // console.log(currentTime , item)
  185. index = i;
  186. activeMetro = item;
  187. } else {
  188. break;
  189. }
  190. }
  191. if (index > -1 && metronomeData.activeIndex !== index) {
  192. metronomeData.activeIndex = index;
  193. // console.log("节拍器播放", '节拍器索引:',metronomeData.activeIndex, '节拍器时间:',currentTime, '实际时间:',originTime,'xml计算的时间:',metronomeData.metroList[metronomeData.activeIndex]);
  194. metronomeData.activeMetro = this.getStep(activeMetro);
  195. console.log("🚀 ~ metronomeData.activeMetro",metronomeData.activeMetro.measureNumberIndex, metronomeData.activeMetro.index)
  196. tickTockPlayTime = currentTime
  197. // this.playAudio();
  198. metronomeData.isClick = false;
  199. return;
  200. }
  201. metronomeData.isClick = false;
  202. if (currentTime === 0) {
  203. metronomeData.activeMetro = {}
  204. }
  205. };
  206. // 播放
  207. playAudio = () => {
  208. /* 如果是 评测模式且不为MIDI并且节拍器资源加载成功的时候 不运行节拍器播放*/
  209. if (state.modeType === "practise" && state.playMode !== "MIDI") {
  210. if(state.playType === "play" && state.playSource === "music" && audioDataState.songCollection.beatSongEle){
  211. return
  212. }
  213. if(state.playType === "play" && state.playSource === "background" && audioDataState.songCollection.beatBackgroundEle){
  214. return
  215. }
  216. if(state.playType === "sing" && state.playSource === "music" && audioDataState.songCollection.beatFanSongEle){
  217. return
  218. }
  219. if(state.playType === "sing" && state.playSource === "background" && audioDataState.songCollection.beatBanSongEle){
  220. return
  221. }
  222. if(state.playType === "sing" && state.playSource === "mingSong" && audioDataState.songCollection.beatMingSongEle){
  223. return
  224. }
  225. }
  226. // console.log("播放自带的节拍器 233333")
  227. if (!metronomeData.initPlayerState || state.playState === 'paused') return;
  228. const beatVolume = state.setting.beatVolume / 100
  229. // this.source = metronomeData.activeMetro?.index === 0 ? this.source1 : this.source2;
  230. // this.source.volume(metronomeData.disable || state.playState === 'paused' ? 0 : beatVolume);
  231. // Audio 播放音频
  232. this.source = metronomeData.activeMetro?.index === 0 ? audioData.tick : audioData.tock;
  233. this.source.volume = metronomeData.disable ? 0 : beatVolume;
  234. if (this.source.volume <= 0) {
  235. this.source.muted = true
  236. } else {
  237. this.source.muted = false
  238. }
  239. // console.log('节拍器播放的时间',tickTockPlayTime)
  240. this.source.play();
  241. };
  242. /**
  243. * 跟练模式播放,跟练模式没有曲子音频播放器
  244. */
  245. simulatePlayAudio = () => {
  246. // console.log(333, metronomeData.followAudioIndex)
  247. if (!metronomeData.initPlayerState) return;
  248. const beatVolume = state.setting.beatVolume / 100
  249. // this.source = metronomeData.followAudioIndex === 1 ? this.source1 : this.source2;
  250. // Audio 播放音频
  251. this.source = metronomeData.followAudioIndex === 1 ? audioData.tick : audioData.tock;
  252. // this.source.volume(metronomeData.disable ? 0 : beatVolume);
  253. this.source.volume = metronomeData.disable ? 0 : beatVolume
  254. /**
  255. * https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLMediaElement/volume
  256. * volume属性在部分ios手机的Safari浏览器不被支持
  257. */
  258. if (this.source.volume <= 0) {
  259. this.source.muted = true
  260. } else {
  261. this.source.muted = false
  262. }
  263. // console.log('音量',this.source,this.source.volume)
  264. this.source.play();
  265. metronomeData.followAudioIndex += 1;
  266. metronomeData.followAudioIndex = metronomeData.followAudioIndex > metronomeData.totalNumerator ? 1 : metronomeData.followAudioIndex;
  267. };
  268. // 切换
  269. selectPlay() {}
  270. loadAudio1 = () => {
  271. return new Howl({
  272. src: tockAndTick.tick,
  273. // 如果是ios手机,需要强制使用audio,不然部分系统版本第一次播放没有声音
  274. // html5: browserInfo.ios,
  275. });
  276. };
  277. loadAudio2 = () => {
  278. return new Howl({
  279. src: tockAndTick.tock,
  280. // html5: browserInfo.ios,
  281. });
  282. };
  283. getStep(time: number) {
  284. for (let i = 0; i < metronomeData.metroMeasure.length; i++) {
  285. const list = metronomeData.metroMeasure[i];
  286. const item = list.find((n: any) => n.time === time);
  287. if (item) {
  288. // console.log('index',item)
  289. return item;
  290. }
  291. }
  292. return {};
  293. }
  294. // 计算 所有的拍子的时间
  295. calculation(times: any[]) {
  296. // console.log("🚀 ~ times", times);
  297. // 1.统计有多少小节
  298. const measures: any[] = [];
  299. let xmlNumber = -1;
  300. let isDiff = false //弱起时间不够补的时候
  301. for (let i = 0; i < times.length; i++) {
  302. const note = times[i];
  303. const measureNumberXML = note.MeasureNumberXML;
  304. // console.log("🚀 ~ note?.noteElement?.sourceMeasure", note?.noteElement?.sourceMeasure)
  305. // console.log("🚀 ~ measureNumberXML", measureNumberXML, note)
  306. // console.log("🚀 ~ measureNumberXML", note)
  307. const measureListIndex = measureNumberXML - 1;
  308. if (measureNumberXML > -1) {
  309. if (measureNumberXML != xmlNumber) {
  310. // 弱起的时候 根据音符结尾时间减去音符开头时间,得到的不是正常小节的时间,然后平均分配节拍之后,当前节拍间隔会非常短 这里弱起取整个小节的时间
  311. // 当小节时间 减去音符时间,前奏没有预留时间时候,从歌词开始唱的那里开始响节拍器
  312. let startTime = note.measures[0].time
  313. if(i === 0 && note.measures[0].difftime>0){
  314. startTime = note.measures[note.measures.length - 1].endtime - note.measures[0].measureLength
  315. if(startTime < 0){
  316. isDiff = true
  317. }
  318. }
  319. if(isDiff) {
  320. // 当前小节有歌词,开放弱起节拍器
  321. let isLyric = false
  322. let noteIndex = 0
  323. while(!isLyric && noteIndex<note.measures.length){
  324. isLyric = !!note.measures[noteIndex]?.formatLyricsEntries?.length
  325. noteIndex ++
  326. }
  327. isDiff = !isLyric
  328. }
  329. if(isDiff){
  330. xmlNumber = measureNumberXML;
  331. continue
  332. }
  333. // 最后一小节的时间 有可能和下一小节开始时间接不上
  334. const { time, endtime, noteLengthTime } = note.measures[note.measures.length - 1]
  335. let nextNoteStartTime = times[note.measures[note.measures.length - 1].i + 1]?.time
  336. let noteEndTime = 0
  337. if(!nextNoteStartTime){
  338. // 当不够的时候补上时值
  339. noteEndTime = time + noteLengthTime > endtime ? time + noteLengthTime : endtime
  340. }else{
  341. if(Math.abs(nextNoteStartTime - endtime)*1000< 10){
  342. // 当首位本来就是相连的
  343. noteEndTime = endtime
  344. }else{
  345. // 当首位不相连,差值大于这个音符的持续时间的时候取这个音符的时间(防止有间奏的连起来时间太长),否则直接取后一个音符的开始时间
  346. noteEndTime = nextNoteStartTime - time > noteLengthTime ? time + noteLengthTime : nextNoteStartTime
  347. }
  348. }
  349. const m = {
  350. measureNumberXML: measureNumberXML,
  351. measureNumberIndex: measureListIndex,
  352. numerator: note?.noteElement?.sourceMeasure?.ActiveTimeSignature?.numerator || 0,
  353. start: startTime,
  354. end: noteEndTime,
  355. time: noteEndTime - startTime,
  356. stave_x: note?.noteElement?.sourceMeasure?.verticalMeasureList?.[0]?.stave?.x || 0,
  357. end_x: note?.stave?.end_x || 0 || 0,
  358. stepList: [] as number[],
  359. svgs: [] as any[],
  360. isRestFlag: note.isRestFlag,
  361. };
  362. // 2.统计小节的拍数
  363. // 3.统计小节的时长, 开始时间,结束时间
  364. // console.log(measureNumberXML,note.measures, times.filter((n: any) => n?.noteElement?.sourceMeasure?.measureListIndex == measureListIndex))
  365. if ([121].includes(state.subjectId)) {
  366. const _measures = times.filter((n: any) => n?.noteElement?.sourceMeasure?.measureListIndex == measureListIndex);
  367. note.measures = _measures;
  368. m.start = note.measures[0].time;
  369. m.end = note.measures[note.measures.length - 1].endtime;
  370. m.time = note.measures[note.measures.length - 1].endtime - note.measures[0].time;
  371. try {
  372. const tickables = note.noteElement.sourceMeasure.verticalMeasureList.reduce((arr: any[], value: any) => {
  373. arr.push(...value.vfVoices["1"].tickables);
  374. return arr;
  375. }, []);
  376. const xList: any[] = [];
  377. m.svgs = tickables
  378. .map((n: any) => {
  379. const x = n.getBoundingBox().x;
  380. if (!xList.includes(x) && n.duration !== "w") {
  381. xList.push(x);
  382. n._start_x = x;
  383. return n;
  384. }
  385. })
  386. .filter(Boolean)
  387. .sort((a: any, b: any) => a._start_x - b._start_x);
  388. // console.log(measureNumberXML, m.svgs)
  389. } catch (error) {
  390. console.log(error);
  391. }
  392. m.stepList = calculateMutilpleMetroStep(note.measures, m);
  393. } else {
  394. /**
  395. * bug:#9877
  396. * 多分轨合并显示,不同分轨的音符数量可能不同
  397. */
  398. let measureArr = note.measures;
  399. if (state.isCombineRender) {
  400. measureArr = measureArr.filter((item: any) => item.MeasureNumberXML === m.measureNumberXML)
  401. }
  402. m.stepList = calculateMetroStep(measureArr, m);
  403. }
  404. measures.push(m);
  405. xmlNumber = measureNumberXML;
  406. }
  407. }
  408. }
  409. //console.log(measures, measures.length,'小节汇总');
  410. let metroList: number[] = [];
  411. const metroMeasure: any[] = [];
  412. console.log("节拍器 每一小节时间:", measures)
  413. console.log("节拍器 间隔:", measures.map(item => {
  414. return {
  415. time: item.time,
  416. measureNumberXML: item.measureNumberXML
  417. }
  418. }))
  419. // 4.按照拍数将时长平均分配
  420. try {
  421. for (let i = 0; i < measures.length; i++) {
  422. const measure = measures[i];
  423. const noteStep = measure.time / measure.numerator;
  424. // console.log("🚀 ~ measure.measureNumberXML",measure.measureNumberXML, noteStep)
  425. const WIDTH = [121].includes(state.subjectId) ? 95 : 100;
  426. const widthStep = WIDTH / (measure.numerator + 1);
  427. metroMeasure[i] = [] as number[];
  428. // console.log('stepList', [...measure.stepList], measure.measureNumberXML)
  429. for (let j = 0; j < measure.numerator; j++) {
  430. const time = noteStep * j + measure.start;
  431. metroList.push(time);
  432. let left = "";
  433. if (measure.stepList[j]) {
  434. left = measure.stepList[j] + "px";
  435. } else {
  436. const preLeft = measure.stepList[j - 1];
  437. left = !preLeft ? `${widthStep}%` : preLeft.toString().indexOf("%") > -1 ? `${preLeft} + ${widthStep}%` : `${preLeft}px + ${widthStep}%`;
  438. measure.stepList[j] = left;
  439. }
  440. metroMeasure[i].push({
  441. index: j,
  442. time,
  443. // left: (measure.stepList[j] ? measure.stepList[j] + 'px' : (j + 1) * widthStep + '%'),
  444. left: left?.indexOf("%") > -1 ? `calc(${left})` : left,
  445. measureNumberXML: measure.measureNumberXML,
  446. isRestFlag: measure.isRestFlag,
  447. });
  448. }
  449. }
  450. } catch (error) {
  451. console.log(error);
  452. }
  453. console.log('节拍器',metroList, metroMeasure);
  454. // 5.得到所有的节拍时间
  455. metronomeData.metroList = metroList;
  456. metronomeData.metroMeasure = metroMeasure;
  457. // console.log(9999,metroList,7777,metroMeasure)
  458. metronomeData.activeMetro = metroMeasure[0]?.[0] || {};
  459. }
  460. }
  461. // 计算拍子的时值
  462. function calculateMetroStep(arr: any[], m: any): number[] {
  463. const measureLength = arr.reduce((total: number, item: any) => {
  464. total += item._noteLength;
  465. return total;
  466. }, 0);
  467. const clap = measureLength / m.numerator;
  468. if (arr.length === 1) {
  469. const wholeNote = arr[0].svgElement;
  470. if (wholeNote && !wholeNote.isRest()) {
  471. const measure_bbox = wholeNote?.attrs?.el?.parentElement?.parentElement?.getBoundingClientRect?.() || { x: 0, right: 0 };
  472. let bbox = wholeNote?.attrs?.el?.getBoundingClientRect?.() || { x: 0 };
  473. let stepWidth = Math.abs(measure_bbox.right - bbox.x) / m.numerator;
  474. let stepList: number[] = [];
  475. for (let i = 0; i < m.numerator; i++) {
  476. stepList.push(bbox.x - measure_bbox.x + i * stepWidth);
  477. }
  478. // console.log("🚀 ~ stepList:", stepList, m.measureNumberXML)
  479. return stepList;
  480. }
  481. try {
  482. // 开头是休止符
  483. if (m.measureNumberXML === 1 && wholeNote && wholeNote.isRest()) {
  484. const measure_bbox = wholeNote?.attrs?.el?.parentElement?.parentElement?.getBoundingClientRect?.() || { x: 0, right: 0 };
  485. let bbox = wholeNote?.attrs?.el?.getBoundingClientRect?.() || { x: 0 };
  486. let stepWidth = Math.abs(measure_bbox.right - bbox.x) / m.numerator;
  487. let stepList: any[] = [];
  488. // 第一小节是休止符,节拍指针应该等分宽度
  489. const widthStep = 100 / (m.numerator + 1);
  490. // for (let i = -1; i < m.numerator - 1; i++) {
  491. // stepList.push(bbox.x - measure_bbox.x + i * stepWidth);
  492. // }
  493. // for (let i = 1; i <= m.numerator; i++) {
  494. // stepList.push(widthStep * i + '%');
  495. // }
  496. // console.log(wholeNote?.attrs?.el, m.measureNumberXML)
  497. // console.log("🚀 ~ stepList:", stepList, m.measureNumberXML)
  498. return stepList;
  499. }
  500. } catch (error) {
  501. console.log("🚀 ~ error:", error);
  502. }
  503. return [];
  504. }
  505. // console.log("🚀 ~ arr", [...arr],`小节总时值: ${measureLength}`, clap, m.measureNumberXML);
  506. let totalLength = 0;
  507. let notes: any[] = [];
  508. let stepList: number[] = [];
  509. for (let i = 0; i < arr.length; i++) {
  510. const item = arr[i];
  511. item.index = i;
  512. const noteLength = item._noteLength;
  513. totalLength += noteLength;
  514. // 大于一拍
  515. const exceedStep = Math.floor(totalLength / clap);
  516. // console.log(`note`, item?.svgElement?.attrs?.el,notes.length,{noteLength, exceedStep,clap}, m.measureNumberXML)
  517. if (exceedStep >= 1) {
  518. totalLength -= clap;
  519. // 一拍
  520. let measure_bbox = item?.svgElement?.attrs?.el?.parentElement?.parentElement?.getBoundingClientRect?.() || { x: 0 };
  521. /**
  522. * bug: #9875
  523. * 简谱,渲染有点问题,先使用vf-stave的位置
  524. */
  525. if (state.musicRenderType !== "staff") {
  526. measure_bbox = item?.svgElement?.attrs?.el?.parentElement?.parentElement?.querySelector('.vf-stave')?.getBoundingClientRect?.() || { x: 0 };
  527. }
  528. /**
  529. * 如果measure_bbox不存在(多分轨合并显示可能会出现),则用note再获取一次
  530. */
  531. if (!measure_bbox.width && notes.length > 0) {
  532. measure_bbox = state.musicRenderType !== "staff" ? notes[0]?.svgElement?.attrs?.el?.parentElement?.parentElement?.querySelector('.vf-stave')?.getBoundingClientRect?.() || { x: 0 } :
  533. notes[0]?.svgElement?.attrs?.el?.parentElement?.parentElement?.getBoundingClientRect?.() || { x: 0 }
  534. }
  535. if (notes.length > 0) {
  536. let bbox = notes[0]?.svgElement?.attrs?.el?.getBoundingClientRect?.() || { x: 0 };
  537. let x: any = bbox.x - measure_bbox.x;
  538. if (notes[0]._noteLength / clap >= 1) {
  539. const nextNote = arr[notes[0].index + 1]?.svgElement?.attrs?.el?.getBoundingClientRect?.() || { x: measure_bbox.right } || { x: 0 };
  540. const stepWidth = Math.abs(bbox.x - nextNote.x) / 2;
  541. x = bbox.x - measure_bbox.x + stepWidth;
  542. // console.log(`音符超一拍`, notes[0]?.svgElement?.attrs?.el, arr[notes[0].index + 1]?.svgElement?.attrs?.el, bbox.x - nextNote.x, stepWidth, m.measureNumberXML);
  543. }
  544. // console.log(`一拍`, notes[0]?.svgElement?.attrs?.el, m.measureNumberXML, notes[0]._noteLength , clap, 'aa')
  545. stepList.push(x);
  546. } else {
  547. let bbox = item?.svgElement?.attrs?.el?.getBoundingClientRect?.() || { x: 0 };
  548. let x: any = bbox.x - measure_bbox.x;
  549. // console.log(`一拍`, item?.svgElement?.attrs?.el, m.measureNumberXML)
  550. stepList.push(x);
  551. }
  552. notes = [];
  553. let bbox = item?.svgElement?.attrs?.el?.getBoundingClientRect?.() || { x: 0 };
  554. let x: any = bbox.x - measure_bbox.x;
  555. let stepWidth = 0;
  556. if (exceedStep > 1) {
  557. // 二拍以上
  558. const nextNote = arr[i + 1]?.svgElement?.attrs?.el?.getBoundingClientRect?.() || { x: measure_bbox.right } || { x: 0 };
  559. stepWidth = Math.abs(bbox.x - nextNote.x) / exceedStep;
  560. // console.log("二拍以上 ~ nextNote:",bbox.x , nextNote.x,stepWidth, item?.svgElement?.attrs?.el,arr[i + 1]?.svgElement?.attrs?.el, exceedStep);
  561. }
  562. for (let j = 1; j < exceedStep; j++) {
  563. totalLength -= clap;
  564. // console.log(`超一拍`,item?.svgElement?.attrs?.el, m.measureNumberXML)
  565. stepList.push(x + stepWidth * j);
  566. }
  567. }
  568. //有时值就将音符加入
  569. if (totalLength > Number.EPSILON && totalLength > 0) {
  570. notes.push(item);
  571. }
  572. }
  573. stepList = stepList.reduce((list: any[], n: number) => {
  574. if (list.includes(n)) {
  575. list.push(undefined as any);
  576. } else {
  577. list.push(n);
  578. }
  579. return list;
  580. }, []);
  581. // console.log("stepList", [...stepList], m.measureNumberXML);
  582. return stepList;
  583. }
  584. // 计算单声部多声轨的拍子的时值
  585. function calculateMutilpleMetroStep(arr: any[], m: any): number[] {
  586. // console.log("🚀 ~ m:", [...m.svgs])
  587. const step = m.time / m.numerator;
  588. const measure_bbox = arr[0]?.svgElement?.attrs?.el?.parentElement?.parentElement?.getBoundingClientRect?.() || { x: 0 };
  589. if (arr.length === 1) {
  590. const staveNote = m.svgs[0];
  591. // 大于一拍
  592. let bbox = staveNote?.attrs?.el?.getBoundingClientRect?.() || { x: 0 };
  593. if (staveNote && !staveNote.isRest()) {
  594. return [bbox.x - measure_bbox.x];
  595. }
  596. return [];
  597. }
  598. // console.log("🚀 ~ arr", arr, step, m.measureNumberXML);
  599. let total = 0;
  600. let notes: any[] = [];
  601. let stepList: number[] = [];
  602. for (let i = 0; i < arr.length; i++) {
  603. const item = arr[i];
  604. item._index = i;
  605. const noteTime = item.endtime - item.time;
  606. total += noteTime;
  607. let svgEle = m.svgs[i]?.attrs?.el;
  608. // 大于一拍
  609. let bbox = svgEle?.getBoundingClientRect?.() || { x: 0 };
  610. // console.log(m.measureNumberXML, svgEle, i)
  611. if (noteTime > step) {
  612. total -= step;
  613. // console.log('超过一拍了', notes, m.measureNumberXML)
  614. let x = bbox.x - measure_bbox.x;
  615. if (notes.length > 0) {
  616. svgEle = m.svgs[notes[0]._index]?.attrs?.el;
  617. bbox = svgEle?.getBoundingClientRect?.() || { x: 0 };
  618. x = bbox.x - measure_bbox.x;
  619. }
  620. stepList.push(x);
  621. notes = [];
  622. } else {
  623. notes.push(item);
  624. }
  625. // console.log(notes)
  626. if (Math.abs(total - step) < 0.001) {
  627. let x = bbox.x - measure_bbox.x;
  628. if (notes.length > 0) {
  629. svgEle = m.svgs[notes[0]._index]?.attrs?.el;
  630. bbox = svgEle?.getBoundingClientRect?.() || { x: 0 };
  631. x = bbox.x - measure_bbox.x;
  632. }
  633. // console.log("一拍",svgEle,notes,m.svgs, m.measureNumberXML);
  634. stepList.push(x);
  635. total = 0;
  636. notes = [];
  637. }
  638. }
  639. stepList = stepList.reduce((list: any[], n: number) => {
  640. if (list.includes(n)) {
  641. list.push(undefined as any);
  642. } else {
  643. list.push(n);
  644. }
  645. return list;
  646. }, []); //Array.from(new Set(stepList))
  647. // console.log('stepList', stepList, m.measureNumberXML)
  648. return stepList;
  649. }
  650. // 延迟兼容处理
  651. function setCurrentTime(time: number) {
  652. if (browserInfo.huawei || browserInfo.xiaomi) {
  653. time += 0.125;
  654. } else if (browserInfo.android) {
  655. time += 0.11;
  656. } else if (browserInfo.ios) {
  657. time += 0.01;
  658. }
  659. return time;
  660. }
  661. // 自动隐藏光标提示
  662. function hideCursorTip() {
  663. if (!tipsTimer) {
  664. tipsTimer = setTimeout(() => {
  665. metronomeData.cursorTips = ''
  666. clearTimeout(tipsTimer)
  667. tipsTimer = null
  668. }, 2000);
  669. } else {
  670. clearTimeout(tipsTimer)
  671. tipsTimer = setTimeout(() => {
  672. metronomeData.cursorTips = ''
  673. clearTimeout(tipsTimer)
  674. tipsTimer = null
  675. }, 2000);
  676. }
  677. }
  678. export default Metronome;