runtime.ts 28 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036
  1. /**
  2. * 播放过程中必要的context,效果与state一致仅用于操作,但是包含方法,好处是可以在任意地方调用,操作播放状态
  3. */
  4. import { Toast } from 'vant'
  5. import { reactive, watchEffect } from 'vue'
  6. import store from 'store'
  7. import { constant, throttle, debounce } from 'lodash'
  8. import {
  9. getActtiveNoteByTimes,
  10. getDuration,
  11. getIndex,
  12. getNoteBySlursStart,
  13. setStepIndex,
  14. getSlursNote,
  15. getVoicePartInfo,
  16. formatBeatUnit,
  17. } from './helpers'
  18. import SectionHint from '/src/helpers/section-hint'
  19. import { browser, formatTime, getPlatform, getRequestHostname } from '/src/helpers/utils'
  20. import request from '/src/helpers/request'
  21. // import * as Tone from 'tone'
  22. import router from '/src/router'
  23. import appState from '/src/state'
  24. import detailState from './state'
  25. import SettingState from '/src/pages/detail/setting-state'
  26. // import fixtimeRela from '/src/constant/fixtime'
  27. import TickPlayer, { getTickTime } from '/src/components/tick/player'
  28. // import { tickByNumerator } from '/src/components/tick/constant'
  29. import { postMessage, listenerMessage, promisefiyPostMessage } from '/src/helpers/native-message'
  30. import EventEmitter from 'eventemitter3'
  31. import { useClientType, useOriginSearch } from '/src/subpages/colexiu/uses'
  32. import { evaluatPlayerStop } from '/src/subpages/colexiu/buttons/evaluating'
  33. import { unitTestData } from '/src/subpages/colexiu/unitTest'
  34. export const event = new EventEmitter()
  35. const browserInfo = browser()
  36. const initBehaviorId = '' + new Date().valueOf()
  37. const getLinkId = (): string => {
  38. const seearchid = useOriginSearch().id as string
  39. return location.hash.split('?')[0].split('/').pop() || seearchid || ''
  40. }
  41. export const getFixtimeRelaVal = () => {
  42. const route: any = router.currentRoute.value
  43. const linkId = location.hash.split('?')[0].split('/').pop()
  44. return 0 //(fixtimeRela as any)[(route.params.id || linkId)] || 0
  45. }
  46. export const getFixTime = (speed: number) => {
  47. const duration: any = getDuration(state.osmd)
  48. let numerator = duration.numerator || 0
  49. let denominator = duration.denominator || 4
  50. const beatUnit = duration.beatUnit || 'quarter'
  51. if (detailState.repeatedBeats) {
  52. // 音频制作问题仅2拍不重复
  53. numerator = numerator === 2 ? 4 : numerator
  54. }
  55. //酷乐秀计算方法
  56. // const time = !detailState.needTick && !detailState.skipTick ? ((denominator * 60) / speed / denominator) * numerator : 0
  57. // 管乐迷计算方法
  58. // console.log('diff', speed, duration, formatBeatUnit(beatUnit), denominator, numerator, (numerator / denominator))
  59. const time = !detailState.needTick && !detailState.skipTick ? (60 / speed * formatBeatUnit(beatUnit)) * (numerator / denominator) : 0
  60. // console.log({duration, t:(60 / speed * formatBeatUnit(beatUnit)) * (numerator / denominator), time, numerator,denominator, "duration.numerator": duration.numerator})
  61. return time
  62. }
  63. let prevIndex: number = 0
  64. export type IPlayState = 'init' | 'play' | 'pause' | 'suspend'
  65. export type IMode = 'background' | 'music'
  66. export type ISonges = {
  67. background?: string
  68. music?: string
  69. }
  70. const state = reactive({
  71. /** 曲目信息 */
  72. songs: {} as ISonges,
  73. /** 播放状态 */
  74. playState: 'init' as IPlayState,
  75. /** 当前播放背景 */
  76. sectionHint: new SectionHint(),
  77. /** 音频播放器实例 */
  78. audiosInstance: null as any,
  79. /** 原声伴奏 */
  80. mode: 'music' as IMode,
  81. /** 是否是第一次播放 */
  82. isFirstPlay: true,
  83. metro: null as any,
  84. metroing: false,
  85. /** 时长 */
  86. duration: '0:00',
  87. durationNum: 0,
  88. /** 当前时间 */
  89. currentTime: '0:00',
  90. currentTimeNum: 0,
  91. loading: false,
  92. /** 速度 */
  93. speed: 90,
  94. browser: browser(),
  95. /** 调速是否可见 */
  96. speedShow: false,
  97. /** 播放进度进度是否可见 */
  98. progressShow: false,
  99. touched: false,
  100. /** opendisplaymusicdisplay 实例 */
  101. osmd: null as any,
  102. /** 节拍器实例 */
  103. tickPlayer: null as any,
  104. /** 评测状态 */
  105. evaluatingStatus: false,
  106. /** 评测提示 */
  107. evaluatingTips: false,
  108. clickTime: 0,
  109. evaluatingFixTime: 0,
  110. /** 摄像头状态 */
  111. cameraStatus: false,
  112. /** 录制状态 */
  113. captureStatus: false,
  114. ticking: false,
  115. /** 第几分轨 */
  116. partIndex: 0,
  117. /** 当前音符index */
  118. activeIndex: 0
  119. })
  120. const syncStepIndex = (i: number) => {
  121. // console.log("🚀 ~ i", i)
  122. if (state.osmd.hidden !== false) {
  123. state.osmd.cursor.show()
  124. }
  125. prevIndex = i
  126. setStepIndex(state.osmd, i)
  127. refreshIndex(detailState.times[i]?.time)
  128. }
  129. /**播放状态改变时,向父页面发送事件 */
  130. export const sendParentMessage = (playState: string) => {
  131. window.parent.postMessage(
  132. {
  133. api: 'headerTogge',
  134. playState: playState
  135. },
  136. '*'
  137. )
  138. }
  139. watchEffect(() => {
  140. detailState.maskStatus = state.playState === 'play'
  141. if(['play', 'pause'].includes(state.playState)) {
  142. sendParentMessage(state.playState)
  143. }
  144. })
  145. const syncPlayState = async () => {
  146. if (detailState.activeDetail.isAppPlay) {
  147. const cloudGetMediaStatus = await promisefiyPostMessage({
  148. api: 'cloudGetMediaStatus',
  149. })
  150. const status = cloudGetMediaStatus?.content.status
  151. state.playState = status
  152. } else {
  153. state.playState = state.audiosInstance.getStatus()
  154. }
  155. }
  156. export default state
  157. export const setCurrentTime = (time: number) => {
  158. console.log('setCurrentTime', time)
  159. const fixt = time // - 5
  160. // console.log('set', fixt)
  161. detailState.fixedKey = 0
  162. state.currentTimeNum = fixt
  163. state.currentTime = formatTime(fixt)
  164. if (detailState.activeDetail.isAppPlay) {
  165. promisefiyPostMessage({
  166. api: 'cloudSetCurrentTime',
  167. content: {
  168. currentTime: time * 1000,
  169. songID: detailState.activeDetail.examSongId,
  170. },
  171. })
  172. } else {
  173. state.audiosInstance.setCurrentTime(fixt)
  174. }
  175. refreshView()
  176. syncPlayState()
  177. const index = getIndex(detailState.times, state.currentTimeNum)
  178. syncStepIndex(index)
  179. // changeMode(state.mode)
  180. }
  181. export const onTimeChanged = (num: number) => {
  182. const time = Math.min(num, state.durationNum)
  183. const index = getIndex(detailState.times, state.currentTimeNum)
  184. setCurrentTime(time)
  185. syncStepIndex(index)
  186. // console.log('onTimeChanged', time)
  187. // changeMode(state.mode)
  188. }
  189. /** 获取当前MidiId */
  190. export const getActiveMidiId = () => {
  191. return state.osmd?.sheet?.instruments?.[0]?.subInstruments?.[0]?.midiInstrumentID ?? 0
  192. }
  193. /**
  194. * 修改原音或伴奏
  195. * @param val IMode
  196. */
  197. export const changeMode = async (val: IMode) => {
  198. const cm: IMode = val === 'background' ? 'music' : 'background'
  199. // console.log(!state.songs[val], val, cm)
  200. if (detailState.activeDetail.isAppPlay) {
  201. const data = new Map()
  202. for (const name of detailState.partListNames) {
  203. data.set(name, 60)
  204. }
  205. for (const name of getVoicePartInfo().partListNames) {
  206. data.set(name, cm === 'background' ? 100 : 0)
  207. }
  208. promisefiyPostMessage({
  209. api: 'cloudVolume',
  210. content: {
  211. activeMidiId: getActiveMidiId(),
  212. activeMidiVolume: cm === 'background' ? 100 : 0,
  213. parts: Array.from(data.keys()).map((item) => ({
  214. name: item,
  215. volume: data.get(item),
  216. })),
  217. },
  218. })
  219. // state.mode = val
  220. // promisefiyPostMessage({
  221. // api: 'cloudSwitch',
  222. // content: {
  223. // songID: detailState.activeDetail.examSongId,
  224. // parts: cm === 'background' ? [] : getVoicePartInfo().partListNames,
  225. // }
  226. // })
  227. }
  228. // if (!state.songs[val]) {
  229. // return
  230. // }
  231. state.mode = val
  232. state.audiosInstance?.setMute(true, state.songs[cm])
  233. state.audiosInstance?.setMute(false, state.songs[val])
  234. }
  235. export const changeAllMode = () => {
  236. if (detailState.activeDetail?.isAppPlay) {
  237. const data = new Map()
  238. for (const name of detailState.partListNames) {
  239. data.set(name, 1)
  240. }
  241. promisefiyPostMessage({
  242. api: 'cloudVolume',
  243. content: {
  244. activeMidiId: getActiveMidiId(),
  245. activeMidiVolume: 100,
  246. parts: Array.from(data.keys()).map((item) => ({
  247. name: item,
  248. volume: data.get(item),
  249. })),
  250. },
  251. })
  252. } else {
  253. state.mode = 'background'
  254. state.audiosInstance?.setMute(true)
  255. }
  256. }
  257. export const changeSpeed = (speed: number, isSave: boolean = true) => {
  258. // console.log(speed)
  259. // const route: any = router.currentRoute.value
  260. const speeds = store.get('speeds') || {}
  261. if (isSave){
  262. speeds[getLinkId()] = speed
  263. store.set('speeds', speeds)
  264. }
  265. state.speed = speed
  266. if (!detailState.activeDetail) return
  267. state.audiosInstance?.setSpeed(speed / detailState.baseSpeed)
  268. promisefiyPostMessage({
  269. api: 'cloudChangeSpeed',
  270. content: {
  271. speed: speed,
  272. originalSpeed: detailState.activeDetail.originalSpeed,
  273. songID: detailState.activeDetail.examSongId,
  274. },
  275. })
  276. // changeMode(state.mode)
  277. if (state.playState === 'play') {
  278. syncStepIndex(getIndex(detailState.times, state.currentTimeNum))
  279. }
  280. }
  281. let nextLook: boolean = false
  282. let syncTimed: boolean = false
  283. export const resetCursor = () => {
  284. if (state.osmd) {
  285. if(state.osmd.product){
  286. state.osmd.cursor.setPosition({...detailState.times[0].cursorBox})
  287. } else {
  288. state.osmd.cursor.reset()
  289. }
  290. state.osmd.cursor.hide()
  291. detailState.fixedKey = 0
  292. }
  293. }
  294. export const refreshIndexBase = (index: number) => {
  295. if (index < 0) return
  296. const { osmd }: any = state
  297. if (osmd) {
  298. if (detailState.times[index]) {
  299. if (!detailState.sectionStatus) {
  300. state.sectionHint.show()
  301. }
  302. if (detailState.times[index] && detailState.times[index].noteElement) {
  303. state.sectionHint.showForElement(detailState.times[index])
  304. }
  305. if (!osmd.product){
  306. if (osmd.cursor.hidden !== false) {
  307. osmd.cursor.reset()
  308. osmd.cursor.show()
  309. detailState.fixedKey = 0
  310. }
  311. }
  312. if (prevIndex !== index) {
  313. setStepIndex(state.osmd, detailState.times[index].i, prevIndex)
  314. prevIndex = index
  315. }
  316. detailState.fixedKey = detailState.times[index].realKey
  317. detailState.activeNote = detailState.times[index]
  318. }
  319. }
  320. }
  321. export const refreshIndex = (ctime?: number) => {
  322. const { osmd }: any = state
  323. if (osmd && (ctime || state.audiosInstance.audio)) {
  324. const currentTimeNum = ctime || (state.audiosInstance.audio as HTMLAudioElement).currentTime
  325. const index = getIndex(detailState.times, currentTimeNum)
  326. state.activeIndex = index
  327. removeRepateBackground(index)
  328. // console.log(currentTimeNum, index, detailState.times[detailState.times.length - 1]?.endtime)
  329. const lastNote = detailState.times[detailState.times.length - 1]
  330. const endtime = lastNote?.sourceEndTime || lastNote?.endtime
  331. if (currentTimeNum > endtime) {
  332. state.osmd.cursor.hide()
  333. state.sectionHint.destroy()
  334. } else {
  335. if (detailState.times[index]) {
  336. refreshIndexBase(index)
  337. }
  338. }
  339. }
  340. }
  341. /** 重复中移除重复的背景 */
  342. export const removeRepateBackground = (index: number) => {
  343. if (state.evaluatingStatus && index) {
  344. const activeNote = detailState.times[index]
  345. const note = detailState.times[index + 1] || activeNote
  346. const number = note?.noteElement?.sourceMeasure?.measureListIndex
  347. // 0 比较特殊重置等操作会导致误操作所以跳过第0个
  348. if (note && detailState.evaluatings[number] && index > 0) {
  349. detailState.evaluatings = {
  350. ...detailState.evaluatings,
  351. [number]: undefined,
  352. }
  353. }
  354. }
  355. }
  356. /**
  357. * 刷新播放的状态
  358. * @param ctime 当前时间
  359. * @returns
  360. */
  361. export const refreshPlayer = async (ctime?: number) => {
  362. // if (!state.durationNum) {
  363. // loadedmetadata()
  364. // }
  365. // const status: IPlayState = state.audiosInstance.getStatus()
  366. const { osmd }: any = state
  367. if (osmd && (ctime || state.audiosInstance.audio)) {
  368. // state.playState = status
  369. const currentTimeNum = ctime || (state.audiosInstance.audio as HTMLAudioElement).currentTime
  370. // console.log('refreshPlayer', currentTimeNum)
  371. const mintime = 0 //detailState.times[0].time
  372. if (currentTimeNum + 1 < mintime) {
  373. setCurrentTime(mintime)
  374. return
  375. } else {
  376. syncTimed = false
  377. }
  378. const nextTime = () => {
  379. if (detailState.sectionStatus && detailState.section.length === 2) {
  380. if (currentTimeNum >= detailState.section[0].time) {
  381. detailState.sectionFlash = false
  382. }
  383. const nextNote = detailState.times[detailState.section[1].i + 1]
  384. const time = nextNote
  385. ? nextNote.halfTone === 0
  386. ? detailState.section[1].endtime
  387. : nextNote.time
  388. : state.durationNum
  389. return currentTimeNum + (browserInfo.xiaomi ? 0.2 : 0.08) >= time
  390. }
  391. return false
  392. }
  393. const isNext = nextTime()
  394. if (isNext) {
  395. // console.log("isNext", detailState.section[1], detailState.section[1].endtime, currentTimeNum)
  396. state.audiosInstance.setMute(true)
  397. state.osmd.cursor.hide()
  398. if (detailState.activeDetail?.isAppPlay) {
  399. pause()
  400. } else {
  401. await state.audiosInstance.pause()
  402. }
  403. // 如果是单元测验 和课后训练直接结束
  404. if (unitTestData.isSelectMeasureMode && state.evaluatingStatus){
  405. event.emit('ended')
  406. return
  407. }
  408. setSectionModeCurrentTime()
  409. clearAccelerateRefreshPlayer()
  410. setTimeout(() => setPlayState(), 1000)
  411. state.loading = false
  412. return
  413. }
  414. // 当MIDI进度超过最后一个音符时间触发结束事件
  415. if(detailState.activeDetail?.isAppPlay && state.durationNum + 3 < currentTimeNum) {
  416. if (state.evaluatingStatus) {
  417. pause()
  418. event.emit('ended', new Event('ended'))
  419. } else {
  420. if (SettingState.sett.loop) {
  421. await pause()
  422. await setCurrentTime(0)
  423. await play()
  424. } else {
  425. pause()
  426. }
  427. }
  428. }
  429. }
  430. }
  431. /**
  432. * 重置播放状态
  433. * @param notStop 是否停止播放
  434. */
  435. export const resetPlayStatus = async (notStop?: boolean) => {
  436. try {
  437. prevIndex = 0
  438. state.osmd.cursor.reset()
  439. state.osmd.cursor.hide()
  440. detailState.fixedKey = 0
  441. detailState.sectionFlash = false
  442. if (state.sectionHint) {
  443. state.sectionHint.destroy()
  444. }
  445. if (!notStop) {
  446. if (detailState.activeDetail.isAppPlay) {
  447. await promisefiyPostMessage({
  448. api: 'cloudSuspend',
  449. content: {
  450. songID: detailState.activeDetail.examSongId,
  451. },
  452. })
  453. endCapture()
  454. } else {
  455. console.log('resetPlayStatus调用暂停')
  456. state.audiosInstance.pause()
  457. }
  458. }
  459. syncPlayState()
  460. } catch (error) {
  461. console.log('resetPlayStatus错误', error)
  462. }
  463. }
  464. export const play = async () => {
  465. if (state.isFirstPlay) {
  466. resetPlayStatus()
  467. detailState.fixedKey = 0
  468. }
  469. if (detailState.activeDetail.isAppPlay) {
  470. await syncPlayState()
  471. promisefiyPostMessage({
  472. api: 'cloudSuspend',
  473. content: {
  474. songID: detailState.activeDetail.examSongId,
  475. },
  476. })
  477. } else {
  478. state.playState = state.audiosInstance.getStatus()
  479. clearAccelerateRefreshPlayer()
  480. accelerateRefreshPlayer()
  481. }
  482. }
  483. const setDelayTime = async (time: number) => {
  484. return new Promise((resolve) => {
  485. setTimeout(() => {
  486. resolve(time)
  487. }, time)
  488. })
  489. }
  490. /**
  491. * 暂停播放
  492. */
  493. export const pause = async () => {
  494. if (detailState.sectionStatus) {
  495. state.osmd.cursor.hide()
  496. }
  497. if (detailState.activeDetail.isAppPlay) {
  498. await syncPlayState()
  499. await promisefiyPostMessage({
  500. api: 'cloudSuspend',
  501. })
  502. await setDelayTime(200)
  503. endCapture()
  504. } else {
  505. state.playState = state.audiosInstance.getStatus()
  506. clearAccelerateRefreshPlayer()
  507. state.audiosInstance.pause()
  508. setTimeout(() => {
  509. // 确保播放完毕,延迟200ms
  510. endCapture()
  511. }, 200)
  512. }
  513. }
  514. export const waiting = () => {
  515. state.loading = true
  516. }
  517. export const playing = () => {
  518. state.loading = false
  519. }
  520. export const ended = debounce(async (evt: Event) => {
  521. resetPlayStatus()
  522. detailState.fixedKey = 0
  523. if (!state.evaluatingStatus) {
  524. refreshPlayer(0)
  525. if (SettingState.sett.loop) {
  526. await setPlayState()
  527. }
  528. }
  529. setCurrentTime(0)
  530. event.emit('ended', evt)
  531. }, 300, {
  532. 'leading': true,
  533. 'trailing': false
  534. })
  535. let timer: any = null
  536. let now = 0
  537. let nowTime = 0
  538. let prevTime: number = 0
  539. const accelerateRefreshPlayer = () => {
  540. if (timer || !state.audiosInstance) {
  541. return
  542. }
  543. timer = setInterval(() => {
  544. requestAnimationFrame(() => {
  545. refreshPlayer()
  546. if (state.audiosInstance.getStatus() === 'play') {
  547. refreshIndex()
  548. }
  549. })
  550. }, 16.7)
  551. }
  552. const clearAccelerateRefreshPlayer = () => {
  553. clearInterval(timer)
  554. timer = null
  555. now = 0
  556. }
  557. /**
  558. * 选择段落
  559. */
  560. export const sectionChange = () => {
  561. detailState.sectionStatus = !detailState.sectionStatus
  562. clearAccelerateRefreshPlayer()
  563. resetPlayStatus()
  564. if (!detailState.sectionStatus) {
  565. setCurrentTime(0)
  566. detailState.fixedKey = 0
  567. }
  568. if (detailState.sectionStatus && detailState.section.length != 2) {
  569. resetCursor()
  570. }
  571. }
  572. export const clearSectionStatus = () => {
  573. detailState.section = []
  574. detailState.sectionBoundingBoxs = []
  575. detailState.sectionStatus = false
  576. }
  577. export const getFirsrNoteByMeasureListIndex = (index: number, tie = true) => {
  578. for (const note of detailState.times) {
  579. if (note?.noteElement?.sourceMeasure?.measureListIndex === index) {
  580. let noteTies: any = null
  581. for (const item of note.measures) {
  582. if (getSlursNote(item)) {
  583. noteTies = getSlursNote(item)
  584. }
  585. }
  586. if (noteTies) {
  587. if (noteTies.sourceMeasure?.measureListIndex !== index) {
  588. for (const n of detailState.times) {
  589. if (n.noteElement.NoteToGraphicalNoteObjectId === noteTies.NoteToGraphicalNoteObjectId) {
  590. return n
  591. }
  592. }
  593. }
  594. }
  595. return note
  596. }
  597. }
  598. return null
  599. }
  600. export const setSectionModeCurrentTime = () => {
  601. if (detailState.needTick) {
  602. setCurrentTime(detailState.section[0].sourceStartTime || detailState.section[0].time)
  603. } else {
  604. const measureListIndex = detailState.section[0].noteElement?.sourceMeasure?.measureListIndex
  605. if (measureListIndex > 0) {
  606. // setCurrentTime(getFirsrNoteByMeasureListIndex(measureListIndex - 1).time)
  607. // 如果没有节拍器,默认提前一个小节
  608. setCurrentTime(getFirsrNoteByMeasureListIndex(measureListIndex).time)
  609. detailState.sectionFlash = true
  610. } else {
  611. setCurrentTime(0)
  612. }
  613. }
  614. }
  615. export const setPlayerView = () => {
  616. if (detailState.sectionStatus) {
  617. syncStepIndex(getIndex(detailState.times, state.currentTimeNum))
  618. if (detailState.section.length === 2) {
  619. setSectionModeCurrentTime()
  620. } else {
  621. detailState.section = []
  622. detailState.sectionBoundingBoxs = []
  623. detailState.sectionStatus = false
  624. Toast.clear()
  625. }
  626. }
  627. }
  628. const cloudToggleState = async () => {
  629. const cloudGetMediaStatus = await promisefiyPostMessage({
  630. api: 'cloudGetMediaStatus',
  631. })
  632. const status = cloudGetMediaStatus?.content.status
  633. if (status === 'init') {
  634. return
  635. }
  636. if (status === 'suspend') {
  637. await promisefiyPostMessage({
  638. api: 'cloudPlay',
  639. content: {
  640. songID: detailState.activeDetail.examSongId,
  641. startTime: state.currentTimeNum * 1000,
  642. originalSpeed: detailState.activeDetail.originalSpeed,
  643. speed: state.speed,
  644. hertz: SettingState.sett.hertz,
  645. },
  646. })
  647. } else {
  648. await promisefiyPostMessage({
  649. api: 'cloudSuspend',
  650. })
  651. }
  652. const cloudGetMediaStatused = await promisefiyPostMessage({
  653. api: 'cloudGetMediaStatus',
  654. })
  655. state.playState = cloudGetMediaStatused?.content.status
  656. console.log(cloudGetMediaStatused, 'cloudGetMediaStatused')
  657. }
  658. export const toggleState = async (delay?: number) => {
  659. if (detailState.activeDetail.isAppPlay) {
  660. await cloudToggleState()
  661. } else {
  662. // console.log(detailState.activeDetail)
  663. // console.log('delay', delay)
  664. state.isFirstPlay = false
  665. setPlayerView()
  666. await state.audiosInstance.togglePlay(delay)
  667. if (!state.evaluatingStatus) {
  668. changeMode(state.mode)
  669. }
  670. state.playState = state.audiosInstance.getStatus()
  671. }
  672. }
  673. const setActiveKey = (index: number) => {
  674. detailState.activeTick = index
  675. }
  676. const setTickStop = () => {
  677. // console.log('节拍器结束', new Date().getTime() - state.clickTime)
  678. detailState.activeTick = -1
  679. detailState.activeTickRepeat = 1
  680. toggleState(getTickTime(state.speed / detailState.baseSpeed))
  681. }
  682. let timeliner: any = -1
  683. export const startIntervalTimeline = (maxTime: number, end: () => void) => {
  684. const nowTimeline = new Date().getTime()
  685. let currenttTime = 0
  686. const throttleIndex = throttle(() => {
  687. if (currenttTime) {
  688. refreshView()
  689. }
  690. }, 1200)
  691. const start = () => {
  692. requestAnimationFrame(() => {
  693. currenttTime = (new Date().getTime() - nowTimeline) / 1000
  694. refreshIndex(currenttTime)
  695. if (maxTime && currenttTime >= maxTime) {
  696. clearIntervalTimeline()
  697. end()
  698. }
  699. throttleIndex()
  700. })
  701. }
  702. start()
  703. timeliner = setInterval(() => {
  704. start()
  705. }, 16.7)
  706. }
  707. export const clearIntervalTimeline = () => {
  708. clearInterval(timeliner)
  709. }
  710. const onTickDestroy = () => {
  711. event.emit('tickDestroy')
  712. }
  713. export const setTick = (stop: () => void, speed?: number) => {
  714. // 节拍时间是固定的无需调整
  715. const mixStop = () => {
  716. stop()
  717. event.emit('tickEnd')
  718. }
  719. if (detailState.needTick) {
  720. let { numerator, denominator } = getDuration(state.osmd)
  721. if (state.osmd.numerator && state.osmd.denominator){
  722. numerator = state.osmd.numerator
  723. denominator = state.osmd.denominator
  724. }
  725. if (detailState.activeDetail.isAppPlay) {
  726. state.ticking = true
  727. postMessage(
  728. {
  729. api: 'cloudMetronome',
  730. content: {
  731. // 少量情况下需要重复
  732. repeat: numerator === 2 ? 2 : 1,
  733. denominator,
  734. numerator,
  735. },
  736. },
  737. (res) => {
  738. state.ticking = false
  739. if (res?.content.status === 'finish') {
  740. mixStop()
  741. } else if (res?.content.status === 'cancel') {
  742. event.emit('tickDestroy')
  743. }
  744. }
  745. )
  746. } else {
  747. const activeTickRepeat = numerator === 2 ? 2 : 1
  748. detailState.activeTickRepeat = activeTickRepeat
  749. console.log('ticking')
  750. state.tickPlayer = new TickPlayer(numerator, (speed || state.speed) / 90)
  751. state.tickPlayer?.start(numerator, (speed || state.speed) / 90, activeTickRepeat)
  752. state.tickPlayer?.event.off('tick', setActiveKey)
  753. state.tickPlayer?.event.off('stop', mixStop)
  754. state.tickPlayer?.event.off('destroy', onTickDestroy)
  755. state.tickPlayer?.event.on('tick', setActiveKey)
  756. state.tickPlayer?.event.on('stop', mixStop)
  757. state.tickPlayer?.event.on('destroy', onTickDestroy)
  758. }
  759. } else {
  760. mixStop()
  761. }
  762. }
  763. /**设置播放状态 */
  764. export const setPlayState = async () => {
  765. if (detailState.activeTick > -1 || state.ticking) {
  766. return
  767. }
  768. await syncPlayState()
  769. if (state.playState !== 'pause' && state.playState !== 'suspend') {
  770. await toggleState()
  771. return
  772. }
  773. setPlayerView()
  774. setTick(setTickStop)
  775. }
  776. export const stopTick = () => {
  777. if (state.tickPlayer) {
  778. state.tickPlayer.destroy()
  779. }
  780. event.emit('stopTick')
  781. detailState.activeTickRepeat = 1
  782. detailState.activeTick = -1
  783. }
  784. export const windowResize = () => {
  785. const index = getIndex(detailState.times, state.currentTimeNum)
  786. setTimeout(() => {
  787. state.sectionHint?.showForElement(detailState.times[index]?.noteElement)
  788. }, 200)
  789. }
  790. export const loadedmetadata = () => {
  791. state.duration = formatTime(state.audiosInstance.duration)
  792. state.durationNum = state.audiosInstance.duration
  793. }
  794. let prevDiff: number = 0
  795. let viewing: boolean = false
  796. export const refreshView = () => {
  797. let cursorEl : any = undefined
  798. let containerEl : any = undefined
  799. if (state?.osmd?.product){
  800. cursorEl = state.osmd.cursor.img
  801. containerEl = document.querySelector('#colexiu-detail-music-sheet')
  802. }
  803. const top = Math.max(parseFloat((cursorEl || state.osmd.cursor.cursorElement).style.top), 0)
  804. if (Math.abs(prevDiff - top) > 10 && !viewing) {
  805. viewing = true
  806. setTimeout(() => {
  807. viewing = false
  808. const scrollElement = containerEl ? containerEl :
  809. appState.clintNmae === 'colexiu'
  810. ? state.osmd.container.parentElement.parentElement
  811. : state.osmd.container.parentElement
  812. scrollElement.scrollTo({
  813. top: top,
  814. left: 0,
  815. behavior: 'smooth',
  816. })
  817. prevDiff = top
  818. }, 100)
  819. }
  820. }
  821. const updatePlayTime = async (time: number) => {
  822. const search = useOriginSearch()
  823. const behaviorId = sessionStorage.getItem('behaviorId') || search.behaviorId || initBehaviorId
  824. const prefix = getRequestHostname()
  825. const clientType = useClientType()
  826. // 已有全局记录时长, 单独记录不需要了
  827. // if (!state.evaluatingStatus) {
  828. // // 如果是后台不需要统计时长
  829. // if (clientType === 'web') return
  830. // try {
  831. // const res = await request.post('/musicPracticeRecord/save', {
  832. // prefix: prefix,
  833. // requestType: 'json',
  834. // data: {
  835. // musicSheetId: getLinkId(),
  836. // sysMusicScoreId: getLinkId(),
  837. // feature: search.feature,
  838. // playTime: time,
  839. // deviceType: getPlatform(),
  840. // behaviorId,
  841. // },
  842. // })
  843. // event.emit('updatePlayTimeSuccess', res.data)
  844. // } catch (error) {}
  845. // }
  846. }
  847. export const setAudioInit = () => {
  848. state.audiosInstance.event.on('loadedmetadata', loadedmetadata)
  849. state.audiosInstance.event.on('waiting', waiting)
  850. state.audiosInstance.event.on('playing', playing)
  851. state.audiosInstance.event.on('play', play, false)
  852. state.audiosInstance.event.on('pause', pause, false)
  853. state.audiosInstance.event.on('ended', ended, false)
  854. state.audiosInstance.event.on('updatePlayTime', updatePlayTime, false)
  855. listenerMessage('cloudplayed', async () => {
  856. await syncPlayState()
  857. state.currentTimeNum = 0
  858. state.currentTime = '00:00'
  859. state.audiosInstance.event.emit('ended', new Event('ended'))
  860. })
  861. listenerMessage('cloudTimeUpdae', (res) => {
  862. // console.log('cloudTimeUpdae', res?.content.currentTime)
  863. const time = res?.content.currentTime / 1000
  864. // requestAnimationFrame(async () => {
  865. // const cloudGetMediaStatus = await promisefiyPostMessage({
  866. // api: 'cloudGetMediaStatus',
  867. // content: {
  868. // songID: detailState.activeDetail.examSongId,
  869. // }
  870. // })
  871. // const status = cloudGetMediaStatus?.content.status
  872. if (state.playState === 'play') {
  873. state.currentTimeNum = time
  874. state.currentTime = formatTime(time)
  875. refreshPlayer(time)
  876. refreshIndex(time)
  877. }
  878. refreshView()
  879. // })
  880. })
  881. state.audiosInstance.event.on('timeupdate', () => {
  882. state.currentTimeNum = state.audiosInstance.currentTime
  883. state.currentTime = formatTime(state.audiosInstance.currentTime)
  884. requestAnimationFrame(() => {
  885. if (state.audiosInstance.getStatus() === 'play') {
  886. refreshPlayer()
  887. }
  888. refreshView()
  889. })
  890. })
  891. window.addEventListener('resize', windowResize)
  892. }
  893. export const back = () => {
  894. if (state.browser.isApp) {
  895. postMessage({
  896. api: 'back',
  897. })
  898. } else {
  899. // (this as any).$router.replace({
  900. // path: '/',
  901. // })
  902. }
  903. }
  904. export const setStepView = (activeNote: any, time?: number) => {
  905. prevIndex = Math.max(activeNote.i, 0)
  906. syncStepIndex(activeNote.i)
  907. if (time) {
  908. // console.log(time)
  909. refreshPlayer(time)
  910. }
  911. refreshView()
  912. }
  913. export const noteClick = (evt: MouseEvent) => {
  914. if (state.isFirstPlay) {
  915. Toast('开始播放后才能调整进度')
  916. return
  917. }
  918. let activeNote = getNoteBySlursStart(getActtiveNoteByTimes(evt))
  919. if (activeNote) {
  920. const time = activeNote.sourceStartTime || activeNote.time
  921. setCurrentTime(time)
  922. setStepView(activeNote.i, time)
  923. detailState.fixedKey = activeNote.realKey
  924. detailState.activeNote = activeNote
  925. }
  926. }
  927. let playStartTime: number = 0
  928. export const startCapture = async () => {
  929. console.log('SettingState.sett.camera:', SettingState.sett.camera, " SettingState.eva.save:", SettingState.eva.save)
  930. if (
  931. SettingState.sett.camera &&
  932. SettingState.eva.save
  933. ) {
  934. postMessage({
  935. api: 'startCapture',
  936. })
  937. }
  938. }
  939. export const endCapture = async () => {
  940. if (SettingState.eva.save && SettingState.sett.camera ) {
  941. postMessage(
  942. {
  943. api: 'endCapture',
  944. })
  945. }
  946. }
  947. export const setCaptureMode = async () => {
  948. if (browserInfo.isApp && SettingState.sett.camera) {
  949. postMessage({
  950. api: 'setCaptureMode',
  951. content: {
  952. mode: state.evaluatingStatus ? 'evaluating' : 'practice',
  953. },
  954. })
  955. }
  956. }