metronome.ts 16 KB

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