runtime.ts 29 KB

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