metronome.ts 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710
  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. firstBeatTypeArr:[] as number[] // 第一小节的节拍
  42. });
  43. watch(
  44. () => metronomeData.cursorMode,
  45. () => {
  46. const img: HTMLElement = document.querySelector("#cursorImg-0")!;
  47. if (img) {
  48. switch (metronomeData.cursorMode) {
  49. case 1:
  50. img.classList.remove("lineHide");
  51. img.style.opacity = 'inherit'
  52. metronomeData.cursorTips = '您已切换到指针跟随音符播放';
  53. img.style.opacity = 'inherit'
  54. break;
  55. case 2:
  56. img.classList.add("lineHide");
  57. img.style.opacity = 'inherit'
  58. metronomeData.cursorTips = '您已切换到指针跟随节拍播放';
  59. // console.log('光标',img)
  60. break;
  61. case 3:
  62. img.style.opacity = '0'
  63. metronomeData.cursorTips = '您已关闭指针显示';
  64. // console.log('隐藏光标')
  65. break;
  66. default:
  67. break;
  68. }
  69. hideCursorTip()
  70. }
  71. }
  72. );
  73. // 切换隐藏光标
  74. const toggleLine = () => {
  75. if (!metronomeData.lineShow) return;
  76. const img: HTMLElement = document.querySelector("#cursorImg-0")!;
  77. if (img) {
  78. if (state.times[state.activeNoteIndex].multipleRestMeasures) {
  79. img.classList.remove("lineHide");
  80. } else {
  81. img.classList.add("lineHide");
  82. }
  83. }
  84. };
  85. watch(
  86. () => metronomeData.lineShow,
  87. () => {
  88. const img: HTMLElement = document.querySelector("#cursorImg-0")!;
  89. if (img) {
  90. if (metronomeData.lineShow) {
  91. img.classList.add("lineHide");
  92. } else {
  93. img.classList.remove("lineHide");
  94. }
  95. }
  96. }
  97. );
  98. class Metronome {
  99. playType = "tick";
  100. source = null as any; // 创建音频源头
  101. source1 = null as any;
  102. source2 = null as any;
  103. constructor(option?: IOptions) {}
  104. init(times: any[]) {
  105. this.calculation(times);
  106. metronomeData.activeList = [];
  107. this.initPlayer()
  108. }
  109. initPlayer() {
  110. // if (!this.source1) {
  111. // this.source1 = this.loadAudio1();
  112. // }
  113. // if (!this.source2) {
  114. // this.source2 = this.loadAudio2();
  115. // }
  116. // metronomeData.initPlayerState = true;
  117. if(metronomeData.initPlayerState) return
  118. Promise.all([this.createAudio(tickWav), this.createAudio(tockWav)]).then(
  119. ([tick, tock]) => {
  120. if (tick) {
  121. audioData.tick = tick;
  122. }
  123. if (tock) {
  124. audioData.tock = tock;
  125. }
  126. metronomeData.initPlayerState = true;
  127. }
  128. );
  129. }
  130. createAudio = (src: string): Promise<HTMLAudioElement | null> => {
  131. return new Promise((resolve) => {
  132. // const a = new Audio(src + '?v=' + Date.now());
  133. const a = new Audio(src);
  134. a.load();
  135. a.onloadedmetadata = () => {
  136. resolve(a);
  137. };
  138. a.onerror = () => {
  139. resolve(null);
  140. };
  141. });
  142. };
  143. // 播放
  144. sound = (currentTime: number) => {
  145. if (!state.sectionStatus){
  146. currentTime = setCurrentTime(currentTime);
  147. }
  148. let index = -1;
  149. let activeMetro = -1;
  150. for (let i = 0; i < metronomeData.metroList.length; i++) {
  151. const item = metronomeData.metroList[i];
  152. if (currentTime >= item) {
  153. // console.log(currentTime , item)
  154. index = i;
  155. activeMetro = item;
  156. } else {
  157. break;
  158. }
  159. }
  160. if (index > -1 && metronomeData.activeIndex !== index) {
  161. metronomeData.activeIndex = index;
  162. // console.log("播放", metronomeData.activeIndex);
  163. metronomeData.activeMetro = this.getStep(activeMetro);
  164. // console.log("🚀 ~ metronomeData.activeMetro",metronomeData.activeMetro.measureNumberIndex, metronomeData.activeMetro.index)
  165. tickTockPlayTime = currentTime
  166. this.playAudio();
  167. metronomeData.isClick = false;
  168. return;
  169. }
  170. // toggleLine()
  171. metronomeData.isClick = false;
  172. };
  173. // 暂停的时候,点击音符,需要找到对应的节拍器的位置
  174. findMetronomePosition = (currentTime: number) => {
  175. // console.log('取消选段',currentTime)
  176. const originTime = currentTime;
  177. // if (!state.sectionStatus){
  178. // currentTime = setCurrentTime(currentTime);
  179. // }
  180. let index = -1;
  181. let activeMetro = -1;
  182. for (let i = 0; i < metronomeData.metroList.length; i++) {
  183. const item = metronomeData.metroList[i];
  184. if (currentTime >= item) {
  185. // console.log(currentTime , item)
  186. index = i;
  187. activeMetro = item;
  188. } else {
  189. break;
  190. }
  191. }
  192. if (index > -1 && metronomeData.activeIndex !== index) {
  193. metronomeData.activeIndex = index;
  194. // console.log("节拍器播放", '节拍器索引:',metronomeData.activeIndex, '节拍器时间:',currentTime, '实际时间:',originTime,'xml计算的时间:',metronomeData.metroList[metronomeData.activeIndex]);
  195. metronomeData.activeMetro = this.getStep(activeMetro);
  196. console.log("🚀 ~ metronomeData.activeMetro",metronomeData.activeMetro.measureNumberIndex, metronomeData.activeMetro.index)
  197. tickTockPlayTime = currentTime
  198. // this.playAudio();
  199. metronomeData.isClick = false;
  200. return;
  201. }
  202. metronomeData.isClick = false;
  203. if (currentTime === 0) {
  204. metronomeData.activeMetro = {}
  205. }
  206. };
  207. // 播放
  208. playAudio = () => {
  209. // 关闭定时器节拍器
  210. return
  211. /* 如果是 评测模式且不为MIDI并且节拍器资源加载成功的时候 不运行节拍器播放*/
  212. if (state.modeType === "practise" && state.playMode !== "MIDI") {
  213. if(state.playType === "play" && state.playSource === "music" && audioDataState.songCollection.beatSongEle){
  214. return
  215. }
  216. if(state.playType === "play" && state.playSource === "background" && audioDataState.songCollection.beatBackgroundEle){
  217. return
  218. }
  219. if(state.playType === "sing" && state.playSource === "music" && audioDataState.songCollection.beatFanSongEle){
  220. return
  221. }
  222. if(state.playType === "sing" && state.playSource === "background" && audioDataState.songCollection.beatBanSongEle){
  223. return
  224. }
  225. if(state.playType === "sing" && state.playSource === "mingSong" && audioDataState.songCollection.beatMingSongEle){
  226. return
  227. }
  228. }
  229. // console.log("播放自带的节拍器 233333")
  230. if (!metronomeData.initPlayerState || state.playState === 'paused') return;
  231. const beatVolume = state.setting.beatVolume / 100
  232. // this.source = metronomeData.activeMetro?.index === 0 ? this.source1 : this.source2;
  233. // this.source.volume(metronomeData.disable || state.playState === 'paused' ? 0 : beatVolume);
  234. // Audio 播放音频
  235. this.source = metronomeData.activeMetro?.index === 0 ? audioData.tick : audioData.tock;
  236. this.source.volume = metronomeData.disable ? 0 : beatVolume;
  237. if (this.source.volume <= 0) {
  238. this.source.muted = true
  239. } else {
  240. this.source.muted = false
  241. }
  242. // console.log('节拍器播放的时间',tickTockPlayTime)
  243. this.source.play();
  244. };
  245. /**
  246. * 跟练模式播放,跟练模式没有曲子音频播放器
  247. */
  248. simulatePlayAudio = () => {
  249. // console.log(333, metronomeData.followAudioIndex)
  250. if (!metronomeData.initPlayerState) return;
  251. const beatVolume = state.setting.beatVolume / 100
  252. // this.source = metronomeData.followAudioIndex === 1 ? this.source1 : this.source2;
  253. // Audio 播放音频
  254. this.source = metronomeData.followAudioIndex === 1 ? audioData.tick : audioData.tock;
  255. // this.source.volume(metronomeData.disable ? 0 : beatVolume);
  256. this.source.volume = metronomeData.disable ? 0 : beatVolume
  257. /**
  258. * https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLMediaElement/volume
  259. * volume属性在部分ios手机的Safari浏览器不被支持
  260. */
  261. if (this.source.volume <= 0) {
  262. this.source.muted = true
  263. } else {
  264. this.source.muted = false
  265. }
  266. // console.log('音量',this.source,this.source.volume)
  267. this.source.play();
  268. metronomeData.followAudioIndex += 1;
  269. metronomeData.followAudioIndex = metronomeData.followAudioIndex > metronomeData.totalNumerator ? 1 : metronomeData.followAudioIndex;
  270. };
  271. // 切换
  272. selectPlay() {}
  273. loadAudio1 = () => {
  274. return new Howl({
  275. src: tockAndTick.tick,
  276. // 如果是ios手机,需要强制使用audio,不然部分系统版本第一次播放没有声音
  277. // html5: browserInfo.ios,
  278. });
  279. };
  280. loadAudio2 = () => {
  281. return new Howl({
  282. src: tockAndTick.tock,
  283. // html5: browserInfo.ios,
  284. });
  285. };
  286. getStep(time: number) {
  287. for (let i = 0; i < metronomeData.metroMeasure.length; i++) {
  288. const list = metronomeData.metroMeasure[i];
  289. const item = list.find((n: any) => n.time === time);
  290. if (item) {
  291. // console.log('index',item)
  292. return item;
  293. }
  294. }
  295. return {};
  296. }
  297. // 计算 所有的拍子的时间
  298. calculation(times: any[]) {
  299. // console.log("🚀 ~ times", times);
  300. // 1.统计有多少小节
  301. const measures: any[] = [];
  302. let xmlNumber = -1;
  303. let isDiff = false //弱起时间不够补的时候
  304. for (let i = 0; i < times.length; i++) {
  305. const note = times[i];
  306. const measureNumberXML = note.MeasureNumberXML;
  307. // console.log("🚀 ~ note?.noteElement?.sourceMeasure", note?.noteElement?.sourceMeasure)
  308. // console.log("🚀 ~ measureNumberXML", measureNumberXML, note)
  309. // console.log("🚀 ~ measureNumberXML", note)
  310. const measureListIndex = measureNumberXML - 1;
  311. if (measureNumberXML > -1) {
  312. if (measureNumberXML != xmlNumber) {
  313. // 弱起的时候 根据音符结尾时间减去音符开头时间,得到的不是正常小节的时间,然后平均分配节拍之后,当前节拍间隔会非常短 这里弱起取整个小节的时间
  314. // 当小节时间 减去音符时间,前奏没有预留时间时候,从歌词开始唱的那里开始响节拍器
  315. let startTime = note.measures[0].time
  316. if(i === 0 && note.measures[0].difftime>0){
  317. startTime = note.measures[note.measures.length - 1].endtime - note.measures[0].measureLength
  318. if(startTime < 0){
  319. isDiff = true
  320. }
  321. }
  322. if(isDiff) {
  323. // 当前小节有歌词,开放弱起节拍器
  324. let isLyric = false
  325. let noteIndex = 0
  326. while(!isLyric && noteIndex<note.measures.length){
  327. isLyric = !!note.measures[noteIndex]?.formatLyricsEntries?.length
  328. noteIndex ++
  329. }
  330. isDiff = !isLyric
  331. }
  332. if(isDiff){
  333. xmlNumber = measureNumberXML;
  334. continue
  335. }
  336. // 最后一小节的时间 有可能和下一小节开始时间接不上
  337. const { time, endtime, noteLengthTime } = note.measures[note.measures.length - 1]
  338. let nextNoteStartTime = times[note.measures[note.measures.length - 1].i + 1]?.time
  339. let noteEndTime = 0
  340. if(!nextNoteStartTime){
  341. // 当不够的时候补上时值
  342. noteEndTime = time + noteLengthTime > endtime ? time + noteLengthTime : endtime
  343. }else{
  344. if(Math.abs(nextNoteStartTime - endtime)*1000< 10){
  345. // 当首位本来就是相连的
  346. noteEndTime = endtime
  347. }else{
  348. // 当首位不相连,差值大于这个音符的持续时间的时候取这个音符的时间(防止有间奏的连起来时间太长),否则直接取后一个音符的开始时间
  349. noteEndTime = nextNoteStartTime - time > noteLengthTime ? time + noteLengthTime : nextNoteStartTime
  350. }
  351. }
  352. const m = {
  353. measureNumberXML: measureNumberXML,
  354. measureNumberIndex: measureListIndex,
  355. CompoundTempo: note?.noteElement?.sourceMeasure?.CompoundTempo || "",
  356. numerator: note?.noteElement?.sourceMeasure?.ActiveTimeSignature?.numerator || 0,
  357. start: startTime,
  358. end: noteEndTime,
  359. time: noteEndTime - startTime,
  360. stave_x: note?.noteElement?.sourceMeasure?.verticalMeasureList?.[0]?.stave?.x || 0,
  361. end_x: note?.stave?.end_x || 0 || 0,
  362. stepList: [] as number[],
  363. svgs: [] as any[],
  364. isRestFlag: note.isRestFlag,
  365. };
  366. /**
  367. * bug:#9877
  368. * 多分轨合并显示,不同分轨的音符数量可能不同
  369. */
  370. let measureArr = note.measures;
  371. if (state.isCombineRender) {
  372. measureArr = measureArr.filter((item: any) => item.MeasureNumberXML === m.measureNumberXML)
  373. }
  374. m.stepList = calculateMetroStep(measureArr, m);
  375. measures.push(m);
  376. xmlNumber = measureNumberXML;
  377. }
  378. }
  379. }
  380. //console.log(measures, measures.length,'小节汇总');
  381. let metroList: number[] = [];
  382. const metroMeasure: any[] = [];
  383. console.log("节拍器 每一小节时间:", measures)
  384. console.log("节拍器 间隔:", measures.map(item => {
  385. return {
  386. time: item.time,
  387. measureNumberXML: item.measureNumberXML
  388. }
  389. }))
  390. try {
  391. for (let i = 0; i < measures.length; i++) {
  392. const measure = measures[i];
  393. // 87拍和45拍要根据小节返回的CompoundTempo特殊处理
  394. const beatTypeArr = getBeatTypeArr(measure.CompoundTempo)
  395. if(i===0){
  396. metronomeData.firstBeatTypeArr = beatTypeArr
  397. }
  398. metroMeasure[i] = [] as number[];
  399. // 根据有几个拍子,划分成几份
  400. const widthStep = 100 / beatTypeArr.length;
  401. // 当前拍子的组合数(2+3+2,3+2)中的数字
  402. let beatNum = 0;
  403. for (let j = 0; j < beatTypeArr.length; j++) {
  404. // 累加
  405. const beatMuit = Array(j).fill("").reduce((num:number,v:any,i:number) => {
  406. return num += Math.abs(beatTypeArr[i])
  407. }, 0) || 0
  408. const time = measure.time * beatMuit + measure.start;
  409. metroList.push(time);
  410. let left = "";
  411. // 当前拍子数对应的节拍位置索引
  412. let currentIdx = 0;
  413. if (j == 0) {
  414. currentIdx = 0
  415. } else {
  416. beatNum += Number(measure.CompoundTempo.split('+')[j]);
  417. currentIdx = beatNum
  418. }
  419. if (measure.stepList[currentIdx]) {
  420. left = measure.stepList[currentIdx] + "px";
  421. } else {
  422. const preLeft = measure.stepList[j - 1];
  423. left = !preLeft ? `${widthStep*(j+1)}%` : preLeft.toString().indexOf("%") > -1 ? `${preLeft} + ${widthStep*(j+1)}%` : `${preLeft}px + ${widthStep*(j+1)}%`;
  424. measure.stepList[j] = left;
  425. }
  426. metroMeasure[i].push({
  427. isTick: beatTypeArr[j] < 0,
  428. index: j,
  429. time,
  430. left: left?.indexOf("%") > -1 ? `calc(${left})` : left,
  431. measureNumberXML: measure.measureNumberXML,
  432. isRestFlag: measure.isRestFlag,
  433. stepList: measure.stepList
  434. });
  435. }
  436. }
  437. // for (let i = 0; i < measures.length; i++) {
  438. // const measure = measures[i];
  439. // const noteStep = measure.time / measure.numerator;
  440. // // console.log("🚀 ~ measure.measureNumberXML",measure.measureNumberXML, noteStep)
  441. // const WIDTH = 100;
  442. // const widthStep = WIDTH / (measure.numerator + 1);
  443. // metroMeasure[i] = [] as number[];
  444. // // console.log('stepList', [...measure.stepList], measure.measureNumberXML)
  445. // for (let j = 0; j < measure.numerator; j++) {
  446. // const time = noteStep * j + measure.start;
  447. // metroList.push(time);
  448. // let left = "";
  449. // if (measure.stepList[j]) {
  450. // left = measure.stepList[j] + "px";
  451. // } else {
  452. // const preLeft = measure.stepList[j - 1];
  453. // left = !preLeft ? `${widthStep}%` : preLeft.toString().indexOf("%") > -1 ? `${preLeft} + ${widthStep}%` : `${preLeft}px + ${widthStep}%`;
  454. // measure.stepList[j] = left;
  455. // }
  456. // metroMeasure[i].push({
  457. // index: j,
  458. // time,
  459. // // left: (measure.stepList[j] ? measure.stepList[j] + 'px' : (j + 1) * widthStep + '%'),
  460. // left: left?.indexOf("%") > -1 ? `calc(${left})` : left,
  461. // measureNumberXML: measure.measureNumberXML,
  462. // isRestFlag: measure.isRestFlag,
  463. // });
  464. // }
  465. // }
  466. } catch (error) {
  467. console.log(error);
  468. }
  469. console.log('节拍器',metroList, metroMeasure);
  470. // 5.得到所有的节拍时间
  471. metronomeData.metroList = metroList;
  472. metronomeData.metroMeasure = metroMeasure;
  473. // console.log(9999,metroList,7777,metroMeasure)
  474. metronomeData.activeMetro = metroMeasure[0]?.[0] || {};
  475. }
  476. }
  477. /** 获取节拍类型数组 */
  478. export function getBeatTypeArr(CompoundTempo?:string){
  479. const speedBeatUnit = state.speedBeatUnit
  480. const Numerator = state.osmd?.Sheet?.SheetPlaybackSetting?.Rhythm?.Numerator || 4
  481. const denominator = state.osmd?.Sheet?.SheetPlaybackSetting?.Rhythm?.Denominator || 4
  482. let loopArr = []
  483. // 规则 负数代表重声,正数代表轻声
  484. switch (`${Numerator}/${denominator}`) {
  485. case "2/2":
  486. loopArr = [-1/2, 1/2]
  487. break;
  488. case "3/2":
  489. loopArr = [-1/3, 1/3, 1/3]
  490. break;
  491. case "5/4":
  492. //5/4拍根据谱面的CompoundTempo来特殊处理
  493. if(CompoundTempo==="2+3"){
  494. loopArr = [-1/5, 1/5, -1/5, 1/5, 1/5]
  495. }else{
  496. loopArr = [-1/5, 1/5, 1/5, -1/5, 1/5]
  497. }
  498. break;
  499. case "3/8":
  500. // 3/8拍 速度为浮点4分音符时候特殊处理
  501. if(speedBeatUnit==="1/4."){
  502. loopArr = [-1/1]
  503. }else{
  504. loopArr = [-1/3, 1/3, 1/3]
  505. }
  506. break;
  507. case "6/8":
  508. loopArr = [-1/2, 1/2]
  509. break;
  510. case "7/8":
  511. //7/8拍根据谱面的CompoundTempo来特殊处理
  512. if(CompoundTempo==="2+2+3"){
  513. loopArr = [-2/7, 2/7, 3/7]
  514. }if(CompoundTempo==="2+3+2"){
  515. loopArr = [-2/7, 3/7, 2/7]
  516. }else{
  517. loopArr = [-3/7, 2/7, 3/7]
  518. }
  519. break;
  520. case "9/8":
  521. loopArr = [-3/9, 3/9, 3/9]
  522. break;
  523. default:
  524. loopArr.push(-1/Numerator);
  525. for (let i = 1; i < Numerator; i++) {
  526. loopArr.push(1/Numerator);
  527. }
  528. break;
  529. }
  530. console.log(loopArr, "loopArr")
  531. return loopArr
  532. }
  533. // 计算拍子的时值
  534. function calculateMetroStep(arr: any[], m: any): number[] {
  535. const measureLength = arr.reduce((total: number, item: any) => {
  536. total += item._noteLength;
  537. return total;
  538. }, 0);
  539. const clap = measureLength / m.numerator;
  540. if (arr.length === 1) {
  541. const wholeNote = arr[0].svgElement;
  542. if (wholeNote && !wholeNote.isRest()) {
  543. const measure_bbox = wholeNote?.attrs?.el?.parentElement?.parentElement?.getBoundingClientRect?.() || { x: 0, right: 0 };
  544. let bbox = wholeNote?.attrs?.el?.getBoundingClientRect?.() || { x: 0 };
  545. let stepWidth = Math.abs(measure_bbox.right - bbox.x) / m.numerator;
  546. let stepList: number[] = [];
  547. for (let i = 0; i < m.numerator; i++) {
  548. stepList.push(bbox.x - measure_bbox.x + i * stepWidth);
  549. }
  550. // console.log("🚀 ~ stepList:", stepList, m.measureNumberXML)
  551. return stepList;
  552. }
  553. try {
  554. // 开头是休止符
  555. if (m.measureNumberXML === 1 && wholeNote && wholeNote.isRest()) {
  556. const measure_bbox = wholeNote?.attrs?.el?.parentElement?.parentElement?.getBoundingClientRect?.() || { x: 0, right: 0 };
  557. let bbox = wholeNote?.attrs?.el?.getBoundingClientRect?.() || { x: 0 };
  558. let stepWidth = Math.abs(measure_bbox.right - bbox.x) / m.numerator;
  559. let stepList: any[] = [];
  560. // 第一小节是休止符,节拍指针应该等分宽度
  561. const widthStep = 100 / (m.numerator + 1);
  562. // for (let i = -1; i < m.numerator - 1; i++) {
  563. // stepList.push(bbox.x - measure_bbox.x + i * stepWidth);
  564. // }
  565. // for (let i = 1; i <= m.numerator; i++) {
  566. // stepList.push(widthStep * i + '%');
  567. // }
  568. // console.log(wholeNote?.attrs?.el, m.measureNumberXML)
  569. // console.log("🚀 ~ stepList:", stepList, m.measureNumberXML)
  570. return stepList;
  571. }
  572. } catch (error) {
  573. console.log("🚀 ~ error:", error);
  574. }
  575. return [];
  576. }
  577. // console.log("🚀 ~ arr", [...arr],`小节总时值: ${measureLength}`, clap, m.measureNumberXML);
  578. let totalLength = 0;
  579. let notes: any[] = [];
  580. let stepList: number[] = [];
  581. for (let i = 0; i < arr.length; i++) {
  582. const item = arr[i];
  583. item.index = i;
  584. const noteLength = item._noteLength;
  585. totalLength += noteLength;
  586. // 大于一拍
  587. const exceedStep = Math.floor(totalLength / clap);
  588. // console.log(`note`, item?.svgElement?.attrs?.el,notes.length,{noteLength, exceedStep,clap}, m.measureNumberXML)
  589. if (exceedStep >= 1) {
  590. totalLength -= clap;
  591. // 一拍
  592. let measure_bbox = item?.svgElement?.attrs?.el?.parentElement?.parentElement?.getBoundingClientRect?.() || { x: 0 };
  593. /**
  594. * bug: #9875
  595. * 简谱,渲染有点问题,先使用vf-stave的位置
  596. */
  597. if (state.musicRenderType !== "staff") {
  598. measure_bbox = item?.svgElement?.attrs?.el?.parentElement?.parentElement?.querySelector('.vf-stave')?.getBoundingClientRect?.() || { x: 0 };
  599. }
  600. /**
  601. * 如果measure_bbox不存在(多分轨合并显示可能会出现),则用note再获取一次
  602. */
  603. if (!measure_bbox.width && notes.length > 0) {
  604. measure_bbox = state.musicRenderType !== "staff" ? notes[0]?.svgElement?.attrs?.el?.parentElement?.parentElement?.querySelector('.vf-stave')?.getBoundingClientRect?.() || { x: 0 } :
  605. notes[0]?.svgElement?.attrs?.el?.parentElement?.parentElement?.getBoundingClientRect?.() || { x: 0 }
  606. }
  607. if (notes.length > 0) {
  608. let bbox = notes[0]?.svgElement?.attrs?.el?.getBoundingClientRect?.() || { x: 0 };
  609. let x: any = bbox.x - measure_bbox.x;
  610. if (notes[0]._noteLength / clap >= 1) {
  611. const nextNote = arr[notes[0].index + 1]?.svgElement?.attrs?.el?.getBoundingClientRect?.() || { x: measure_bbox.right } || { x: 0 };
  612. const stepWidth = Math.abs(bbox.x - nextNote.x) / 2;
  613. x = bbox.x - measure_bbox.x + stepWidth;
  614. // console.log(`音符超一拍`, notes[0]?.svgElement?.attrs?.el, arr[notes[0].index + 1]?.svgElement?.attrs?.el, bbox.x - nextNote.x, stepWidth, m.measureNumberXML);
  615. }
  616. // console.log(`一拍`, notes[0]?.svgElement?.attrs?.el, m.measureNumberXML, notes[0]._noteLength , clap, 'aa')
  617. stepList.push(x);
  618. } else {
  619. let bbox = item?.svgElement?.attrs?.el?.getBoundingClientRect?.() || { x: 0 };
  620. let x: any = bbox.x - measure_bbox.x;
  621. // console.log(`一拍`, item?.svgElement?.attrs?.el, m.measureNumberXML)
  622. stepList.push(x);
  623. }
  624. notes = [];
  625. let bbox = item?.svgElement?.attrs?.el?.getBoundingClientRect?.() || { x: 0 };
  626. let x: any = bbox.x - measure_bbox.x;
  627. let stepWidth = 0;
  628. if (exceedStep > 1) {
  629. // 二拍以上
  630. const nextNote = arr[i + 1]?.svgElement?.attrs?.el?.getBoundingClientRect?.() || { x: measure_bbox.right } || { x: 0 };
  631. stepWidth = Math.abs(bbox.x - nextNote.x) / exceedStep;
  632. // console.log("二拍以上 ~ nextNote:",bbox.x , nextNote.x,stepWidth, item?.svgElement?.attrs?.el,arr[i + 1]?.svgElement?.attrs?.el, exceedStep);
  633. }
  634. for (let j = 1; j < exceedStep; j++) {
  635. totalLength -= clap;
  636. // console.log(`超一拍`,item?.svgElement?.attrs?.el, m.measureNumberXML)
  637. stepList.push(x + stepWidth * j);
  638. }
  639. }
  640. //有时值就将音符加入
  641. if (totalLength > Number.EPSILON && totalLength > 0) {
  642. notes.push(item);
  643. }
  644. }
  645. stepList = stepList.reduce((list: any[], n: number) => {
  646. if (list.includes(n)) {
  647. list.push(undefined as any);
  648. } else {
  649. list.push(n);
  650. }
  651. return list;
  652. }, []);
  653. // console.log("stepList", [...stepList], m.measureNumberXML);
  654. return stepList;
  655. }
  656. // 延迟兼容处理
  657. function setCurrentTime(time: number) {
  658. if (browserInfo.huawei || browserInfo.xiaomi) {
  659. time += 0.125;
  660. } else if (browserInfo.android) {
  661. time += 0.11;
  662. } else if (browserInfo.ios) {
  663. time += 0.01;
  664. }
  665. return time;
  666. }
  667. // 自动隐藏光标提示
  668. function hideCursorTip() {
  669. if (!tipsTimer) {
  670. tipsTimer = setTimeout(() => {
  671. metronomeData.cursorTips = ''
  672. clearTimeout(tipsTimer)
  673. tipsTimer = null
  674. }, 2000);
  675. } else {
  676. clearTimeout(tipsTimer)
  677. tipsTimer = setTimeout(() => {
  678. metronomeData.cursorTips = ''
  679. clearTimeout(tipsTimer)
  680. tipsTimer = null
  681. }, 2000);
  682. }
  683. }
  684. export default Metronome;