metronome.ts 14 KB

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