metronome.ts 16 KB

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