evaluating.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629
  1. import { Button, Toast } from 'vant'
  2. import { defineComponent, Directive, onBeforeUnmount, onMounted, Ref, ref, Teleport } from 'vue'
  3. import '@dotlottie/player-component'
  4. import ButtonIcon from './icon'
  5. import detailState from '/src/pages/detail/state'
  6. import SettingState from '/src/pages/detail/setting-state'
  7. import appState from '/src/state'
  8. import { Fraction } from '/osmd-extended/src'
  9. import {
  10. IPostMessage,
  11. listenerMessage,
  12. postMessage,
  13. promisefiyPostMessage,
  14. removeListenerMessage,
  15. } from '/src/helpers/native-message'
  16. import { browser, getRequestHostname } from '/src/helpers/utils'
  17. import runtime, * as RuntimeUtils from '/src/pages/detail/runtime'
  18. import { getBoundingBoxByverticalNote, getNoteByMeasuresSlursStart, getParentNote } from '/src/pages/detail/helpers'
  19. import { useClientType, useOriginSearch } from '../uses'
  20. import { startButtonShow } from './index'
  21. import { getLeveByScoreMeasure } from '/src/pages/detail/evaluating/helper'
  22. import Evaluating, { evaluatingShow } from '../popups/evaluating'
  23. // @ts-ignore
  24. import StartEvaluating from './dotlotties/start-evaluating.lottie?url'
  25. import iconStartEvaluating from './dotlotties/StartEvaluating.png'
  26. import iconRecord from './dotlotties/iconRecord.png'
  27. // @ts-ignore
  28. import Recording from './dotlotties/recording2.lottie?url'
  29. import request from '/src/helpers/request'
  30. import styles from './index.module.less'
  31. let backtime = 0
  32. const initBehaviorId = '' + new Date().valueOf()
  33. const evaluating = ref(false)
  34. const playStatus: Ref<'connecting' | 'play' | 'stop'> = ref('stop')
  35. const endloading = ref(false)
  36. const connentLoading = ref(false)
  37. const playUrl: Ref<string> = ref('')
  38. const endResult = ref(null)
  39. export const animate: Directive = {
  40. mounted: (el: HTMLElement) => {
  41. el.addEventListener('click', (evt: Event) => {
  42. let element = evt.target as HTMLElement
  43. element.classList.add(...['animate__animated', 'animate__tada'])
  44. })
  45. el.addEventListener('animationend', (evt: Event) => {
  46. let element = evt.target as HTMLElement
  47. element.classList.remove(...['animate__animated', 'animate__tada'])
  48. })
  49. },
  50. }
  51. const browserInfo = browser()
  52. /**
  53. * 默认按照442计算的音符频率,此处转化为按照设置进行调整
  54. * @param num 频率
  55. * @returns 转化后频率
  56. */
  57. const formatPitch = (num?: number): number => {
  58. if (!num) {
  59. return -1
  60. }
  61. if (SettingState.sett.hertz && SettingState.sett.hertz !== 442) {
  62. return (num / 442) * SettingState.sett.hertz
  63. }
  64. return num
  65. }
  66. const formatTimes = () => {
  67. const difftime = detailState.times?.[0]?.difftime || 0
  68. let ListenMode = false
  69. let dontEvaluatingMode = false
  70. let skip = false
  71. const datas = []
  72. for (let index = 0; index < detailState.times.length; index++) {
  73. const item = detailState.times[index]
  74. const note = getNoteByMeasuresSlursStart(item)
  75. // console.log(item.nodeElement)
  76. const rate = runtime.speed / detailState.baseSpeed //1
  77. // const fixtime = 0
  78. const start = difftime + (item.sourceRelativeTime || item.relativeTime)
  79. const end = difftime + (item.sourceRelaEndtime || item.relaEndtime)
  80. const isStaccato =
  81. typeof note.voiceEntry.isStaccato === 'function' ? note.voiceEntry.isStaccato() : note.voiceEntry.isStaccato
  82. const noteRate = isStaccato ? 0.5 : 1
  83. if (note.formatLyricsEntries.contains('Play') || note.formatLyricsEntries.contains('Play...')) {
  84. ListenMode = false
  85. }
  86. if (note.formatLyricsEntries.contains('Listen')) {
  87. ListenMode = true
  88. }
  89. if (note.formatLyricsEntries.contains('纯律结束')) {
  90. dontEvaluatingMode = false
  91. }
  92. if (note.formatLyricsEntries.contains('纯律')) {
  93. dontEvaluatingMode = true
  94. }
  95. const nextNote = detailState.times[index + 1]
  96. // console.log("noteinfo", note.noteElement.isRestFlag && !!note.stave && !!nextNote)
  97. if (skip && (note.stave || !note.noteElement.isRestFlag || (nextNote && !nextNote.noteElement.isRestFlag))) {
  98. skip = false
  99. }
  100. if (note.noteElement.isRestFlag && !!note.stave && !!nextNote && nextNote.noteElement.isRestFlag) {
  101. skip = true
  102. }
  103. // console.log(note.measureOpenIndex, item.measureOpenIndex, note)
  104. // console.log("skip", skip)
  105. const data = {
  106. timeStamp: (start * 1000) / rate,
  107. duration: ((end * 1000) / rate - (start * 1000) / rate) * noteRate,
  108. frequency: formatPitch(item.noteElement?.pitch?.frequency),
  109. nextFrequency: formatPitch(item.noteElement?.pitch?.nextFrequency),
  110. prevFrequency: formatPitch(item.noteElement?.pitch?.prevFrequency),
  111. // 重复的情况index会自然累加,render的index是谱面渲染的index
  112. measureIndex: note.measureOpenIndex,
  113. measureRenderIndex: note.noteElement.sourceMeasure.measureListIndex,
  114. // dontEvaluating: ListenMode,
  115. dontEvaluating: ListenMode || dontEvaluatingMode || item.skipMode,
  116. musicalNotesIndex: item.i,
  117. denominator: note.noteElement?.Length.denominator,
  118. isOrnament: !!note?.voiceEntry?.ornamentContainer,
  119. }
  120. datas.push(data)
  121. }
  122. // console.log("🚀 ~ datas", datas)
  123. return datas
  124. }
  125. const connect = async () => {
  126. const search = useOriginSearch()
  127. connentLoading.value = true
  128. const behaviorId = sessionStorage.getItem('behaviorId') || search.behaviorId || initBehaviorId
  129. const rate = runtime.speed / detailState.baseSpeed //1
  130. const content = {
  131. musicXmlInfos: formatTimes(),
  132. // id: search.id,
  133. subjectId: detailState.subjectId,
  134. detailId: detailState.activeDetail?.id,
  135. examSongId: search.id,
  136. xmlUrl: detailState?.activeDetail?.xmlUrl,
  137. partIndex: detailState.partIndex,
  138. behaviorId,
  139. platform: 'WEB',
  140. clientId: 'STUDENT',
  141. hertz: SettingState.sett.hertz,
  142. feature: 'EVALUATION',
  143. practiceSource: search.unitId ? 'UNIT_TEST' : 'PRACTICE',
  144. // 这里定义的是数字但是因为是通过input输入所以强制转化一次
  145. reactionTimeMs: parseFloat('' + SettingState.eva.reactionTimeMs) || 0,
  146. speed: runtime.speed,
  147. heardLevel: SettingState.eva.difficulty,
  148. beatLength: Math.round((RuntimeUtils.getFixTime(detailState.times[0].beatSpeed) * 1000) / rate),
  149. }
  150. const clientType = useClientType()
  151. if (clientType === 'student') {
  152. content.clientId = 'STUDENT'
  153. } else if (clientType === 'teacher') {
  154. content.clientId = 'TEACHER'
  155. } else {
  156. content.clientId = 'BACKEND'
  157. }
  158. if (browserInfo.android) {
  159. content.platform = 'ANDROID'
  160. }
  161. if (browserInfo.ios) {
  162. content.platform = 'IOS'
  163. }
  164. // console.log("评测数据", content)
  165. const evt = await promisefiyPostMessage({
  166. api: 'startEvaluating',
  167. content: content,
  168. })
  169. if (evt?.content?.reson) {
  170. Toast.fail({
  171. message: evt?.content?.reson,
  172. })
  173. connentLoading.value = false
  174. throw evt
  175. }
  176. connentLoading.value = false
  177. }
  178. const sendOffsetTime = (offsetTime: number) => {
  179. postMessage(
  180. {
  181. api: 'proxyServiceMessage',
  182. content: {
  183. header: {
  184. commond: 'audioPlayStart',
  185. type: 'SOUND_COMPARE',
  186. },
  187. body: {
  188. offsetTime,
  189. },
  190. },
  191. },
  192. () => {
  193. backtime = 0
  194. }
  195. )
  196. }
  197. const cancelTheEvaluation = () => {
  198. const search = useOriginSearch()
  199. RuntimeUtils.resetPlayStatus()
  200. RuntimeUtils.clearIntervalTimeline()
  201. RuntimeUtils.setCurrentTime(0)
  202. playStatus.value = 'stop'
  203. postMessage(
  204. {
  205. api: 'endEvaluating',
  206. content: {
  207. musicScoreId: search.id,
  208. },
  209. },
  210. (evt) => {
  211. evaluating.value = false
  212. // RuntimeUtils.setCaptureMode()
  213. Toast.clear()
  214. }
  215. )
  216. }
  217. const stopPlay = (show: boolean = true) => {
  218. console.log('调用stopPlay')
  219. if (show) {
  220. // Toast({
  221. // duration: 0,
  222. // message: '评分中...',
  223. // type: 'loading',
  224. // })
  225. }
  226. startButtonShow.value = true
  227. cancelTheEvaluation()
  228. }
  229. export const evaluatStopPlay = stopPlay
  230. const startPlay = () => {
  231. console.log('连接服务成功,开始播放', new Date().getTime() - runtime.clickTime)
  232. // synced = false
  233. if (!SettingState.eva.mute) {
  234. RuntimeUtils.changeAllMode()
  235. } else {
  236. RuntimeUtils.changeMode('background')
  237. }
  238. startButtonShow.value = false
  239. // RuntimeUtils.changeSpeed(90)
  240. RuntimeUtils.setPlayState()
  241. }
  242. const setPlayer = async () => {
  243. console.log('调用setPlayer')
  244. // this.startloading = true
  245. runtime.clickTime = new Date().getTime()
  246. RuntimeUtils.resetPlayStatus()
  247. runtime.evaluatingTips = false
  248. if (detailState.isPauseRecording) {
  249. evaluating.value = false
  250. // this.startloading = false
  251. startPlay()
  252. return
  253. }
  254. detailState.evaluatings = {}
  255. RuntimeUtils.setCurrentTime(0)
  256. const hint = Toast({
  257. duration: 0,
  258. message: '服务连接中...',
  259. type: 'loading',
  260. })
  261. try {
  262. await connect()
  263. startPlay()
  264. setTimeout(() => {
  265. Toast.clear()
  266. hint.close()
  267. }, 100)
  268. } catch (error) {
  269. runtime.evaluatingStatus = false
  270. Toast.clear()
  271. }
  272. }
  273. const togglePlay = () => {
  274. if (detailState.isPauseRecording) {
  275. evaluating.value = false
  276. startPlay()
  277. return
  278. }
  279. if (evaluating.value) {
  280. stopPlay()
  281. } else {
  282. setPlayer()
  283. }
  284. }
  285. const cancelEvaluating = (data?: IPostMessage) => {
  286. // this.starting = false
  287. if (data?.content.reson) {
  288. // Toast.fail({
  289. // message: data?.content?.reson,
  290. // })
  291. stopPlay()
  292. }
  293. }
  294. const timeupdate = () => {
  295. console.log('播放事件被触发', playUrl.value, evaluating.value)
  296. if (playUrl.value) {
  297. const nowTime = new Date().getTime()
  298. console.log('第一次播放时间', nowTime)
  299. // synced = true
  300. const time = runtime.audiosInstance?.audios[playUrl.value].currentTime
  301. console.log('已播放时长: ', time * 1000)
  302. console.log('不减掉已播放时间: ', nowTime - backtime)
  303. const delayTime = nowTime - backtime - time * 1000
  304. console.log('真正播放延迟', delayTime)
  305. // 蓝牙耳机延迟一点发送消息确保在录音后面
  306. setTimeout(() => {
  307. sendOffsetTime(delayTime)
  308. }, 220)
  309. }
  310. }
  311. /**
  312. * 播放器停止事件
  313. */
  314. const playerStop = () => {
  315. // alert('stop' + this.endloading)
  316. console.log('playerStop播放器停止事件', endloading.value)
  317. if (endloading.value) {
  318. return
  319. }
  320. playStatus.value = 'stop'
  321. endloading.value = true
  322. startButtonShow.value = true
  323. RuntimeUtils.resetPlayStatus()
  324. RuntimeUtils.clearIntervalTimeline()
  325. RuntimeUtils.setCurrentTime(0)
  326. Toast({
  327. duration: 0,
  328. message: '评分中...',
  329. type: 'loading',
  330. })
  331. // const route = (this as any).$route
  332. postMessage(
  333. {
  334. api: 'endEvaluating',
  335. content: {
  336. musicScoreId: useOriginSearch().id,
  337. },
  338. },
  339. (evt) => {
  340. console.log('调用endEvaluating结束', evt)
  341. endloading.value = false
  342. evaluating.value = false
  343. // RuntimeUtils.setCaptureMode()
  344. }
  345. )
  346. }
  347. export const evaluatPlayerStop = playerStop
  348. const endevent = (evt: Event) => {
  349. if ((evt.target as HTMLAudioElement)?.src === playUrl.value && playStatus.value === 'play') {
  350. playerStop()
  351. canSubmit.value = true
  352. }
  353. if (detailState.isAppPlay) {
  354. playerStop()
  355. canSubmit.value = true
  356. }
  357. }
  358. const start = () => {
  359. playStatus.value = 'play'
  360. if (detailState.isPauseRecording) {
  361. postMessage(
  362. {
  363. api: 'resumeRecording',
  364. },
  365. () => {
  366. evaluating.value = true
  367. detailState.isPauseRecording = false
  368. RuntimeUtils.setCaptureMode()
  369. }
  370. )
  371. return
  372. }
  373. console.log('开始录音', new Date().getTime())
  374. postMessage(
  375. {
  376. api: 'startRecording',
  377. },
  378. () => {
  379. console.log('开始录音回调时间', new Date().getTime())
  380. backtime = new Date().getTime()
  381. evaluating.value = true
  382. console.log('midiUrl', detailState.activeDetail?.midiUrl)
  383. if (detailState.activeDetail?.midiUrl) {
  384. setTimeout(() => {
  385. sendOffsetTime(0)
  386. }, 220)
  387. }
  388. }
  389. )
  390. }
  391. /**
  392. * 活动接口,Url中有设置并且仅在学生端提评分交数据
  393. */
  394. const submitEvaluationScore = async (data: any) => {
  395. if (detailState.setting && detailState.setting.mode === 'EVALUATING') {
  396. if (!canSubmit.value) {
  397. Toast('请完成整首曲目评测!')
  398. return
  399. }
  400. try {
  401. await request.post('/activity/evaluationScore', {
  402. requestType: 'json',
  403. data: {
  404. userId: appState.user.userId,
  405. score: data.score,
  406. ...detailState.setting.submitData,
  407. },
  408. })
  409. } catch (error) {}
  410. canSubmit.value = false
  411. }
  412. }
  413. /** 活动使用,只有在全部评测完成后才能提交,避免只是一小节就高分的情况 */
  414. const canSubmit = ref(false)
  415. /** 有单元测验时,存储分数缓存 */
  416. const setUnitReuslt = () => {
  417. // 评测结束,如果是单元测验
  418. const search = useOriginSearch()
  419. if (search.unitId) {
  420. postMessage({
  421. api: 'setCache',
  422. content: {
  423. key: 'h5-orchestra-unit',
  424. value: JSON.stringify({
  425. musicId: search.id || '',
  426. unitId: search.unitId || '',
  427. score: (endResult.value as any)?.score || 0,
  428. }),
  429. },
  430. })
  431. }
  432. }
  433. const sendResult = (evt?: IPostMessage) => {
  434. console.log('评测返回', evt?.content)
  435. if (evt?.content) {
  436. const data = evt?.content?.body
  437. if (evt?.content.header.commond === 'overall') {
  438. // console.log(evt)
  439. Toast.clear()
  440. endResult.value = data
  441. evaluatingShow.value = true
  442. submitEvaluationScore(data)
  443. setUnitReuslt()
  444. } else if (evt?.content.header.commond === 'checkDone') {
  445. // 此处已经在校音中单独监听不做处理
  446. } else if (evt?.content.header.commond === 'checking') {
  447. // 此处已经在校音中单独监听不做处理
  448. } else {
  449. const getBeforeNote = (index: number) => {
  450. while (index >= 0) {
  451. const item = detailState.times[index]
  452. if (item.stave) {
  453. return item
  454. }
  455. index--
  456. }
  457. }
  458. const setEvaluatings = (note: any, data: any, dontTransition = false) => {
  459. const startNote = getBoundingBoxByverticalNote(note)
  460. console.log(detailState.evaluatings, startNote)
  461. detailState.evaluatings = {
  462. ...detailState.evaluatings,
  463. [startNote.measureIndex]: {
  464. ...startNote,
  465. ...getLeveByScoreMeasure(data.score),
  466. score: data.score,
  467. dontTransition,
  468. },
  469. }
  470. }
  471. for (let index = 0; index < detailState.times.length; index++) {
  472. let time = detailState.times[index]
  473. if (data.measureRenderIndex == time.noteElement.sourceMeasure.measureListIndex) {
  474. if (!time.stave) {
  475. const ntime = getBeforeNote(index)
  476. // console.log('ntime', ntime)
  477. if (ntime) {
  478. time = ntime
  479. }
  480. }
  481. if (!time.noteElement.tie) {
  482. setEvaluatings(time, data)
  483. } else {
  484. for (const item of time.noteElement.tie.notes) {
  485. const note = getParentNote(item)
  486. if (!note) continue
  487. setEvaluatings(
  488. note,
  489. data,
  490. item.NoteToGraphicalNoteObjectId !== time.noteElement.tie.StartNote?.NoteToGraphicalNoteObjectId
  491. )
  492. }
  493. }
  494. break
  495. }
  496. }
  497. }
  498. }
  499. }
  500. const onProgress = () => {
  501. // console.log(runtime.currentTimeNum, detailState.times[detailState.times.length - 1]?.time - 2, detailState.times)
  502. if (runtime.currentTimeNum >= detailState.times[detailState.times.length - 1]?.time - 2) {
  503. canSubmit.value = true
  504. }
  505. }
  506. const cloudMetronome = (evt: any) => {
  507. startButtonShow.value = true
  508. }
  509. export default defineComponent({
  510. name: 'ColexiuButtonEvaluating',
  511. directives: { animate },
  512. setup(props, { expose }) {
  513. onMounted(async () => {
  514. runtime.evaluatingTips = true
  515. detailState.section = []
  516. detailState.sectionStatus = false
  517. RuntimeUtils.changeAllMode()
  518. playUrl.value = runtime.songs.background || (runtime.songs.music as string)
  519. runtime.audiosInstance?.audios[playUrl.value]?.addEventListener('play', timeupdate)
  520. runtime.audiosInstance?.audios[playUrl.value]?.addEventListener('timeupdate', onProgress)
  521. RuntimeUtils.event.on('next-click', playerStop)
  522. RuntimeUtils.event.on('ended', endevent)
  523. listenerMessage('sendResult', sendResult)
  524. listenerMessage('cancelEvaluating', cancelEvaluating)
  525. listenerMessage('cloudTimeUpdae', onProgress)
  526. RuntimeUtils.event.on('tickDestroy', cloudMetronome)
  527. RuntimeUtils.event.on('tickEnd', start)
  528. await RuntimeUtils.pause()
  529. RuntimeUtils.setCurrentTime(0)
  530. })
  531. onBeforeUnmount(() => {
  532. runtime.audiosInstance?.audios[playUrl.value]?.removeEventListener('play', timeupdate)
  533. runtime.audiosInstance?.audios[playUrl.value]?.removeEventListener('timeupdate', onProgress)
  534. RuntimeUtils.event.off('next-click', playerStop)
  535. RuntimeUtils.event.off('ended', endevent)
  536. RuntimeUtils.event.off('tickDestroy', cloudMetronome)
  537. removeListenerMessage('sendResult', sendResult)
  538. removeListenerMessage('cancelEvaluating', cancelEvaluating)
  539. removeListenerMessage('cloudTimeUpdae', onProgress)
  540. RuntimeUtils.event.off('tickEnd', start)
  541. })
  542. expose({
  543. setPlayer,
  544. startPlay,
  545. stopPlay,
  546. togglePlay,
  547. playerStop,
  548. evaluating,
  549. connentLoading,
  550. playStatus,
  551. cancelTheEvaluation,
  552. })
  553. return () => {
  554. return (
  555. <>
  556. {/* <Button
  557. v-animate
  558. class={[styles.button, styles.hasText]}
  559. style={{ display: detailState.frozenMode ? 'none' : '' }}
  560. onClick={() => {
  561. runtime.evaluatingStatus = false
  562. if (playStatus.value === 'play' || playStatus.value === 'connecting') {
  563. cancelTheEvaluation()
  564. }
  565. }}
  566. >
  567. <ButtonIcon name="practise" />
  568. <span>练习</span>
  569. </Button> */}
  570. {/* 评测 */}
  571. <Evaluating data={endResult.value} />
  572. {!evaluating.value ? (
  573. <Teleport to="body" key="StartEvaluating">
  574. <div class={styles.dialogueBox}>
  575. <div class={styles.dialogue}>
  576. <div>
  577. 演奏前请调整好乐器,保证最佳演奏状态。<span class={styles.triangle}></span>
  578. </div>
  579. </div>
  580. <img class={styles.dialogueIcon} src={iconStartEvaluating} />
  581. {/* <dotlottie-player src={StartEvaluating} autoplay loop class={styles.animation} /> */}
  582. </div>
  583. </Teleport>
  584. ) : (
  585. <Teleport to="body" key="Recording">
  586. <div class={styles.dialogueBox}>
  587. <div class={styles.inRadio}>收音中...</div>
  588. <img class={styles.inRadioIcon} src={iconRecord} />
  589. {/* <dotlottie-player src={Recording} autoplay loop class={styles.animation} /> */}
  590. </div>
  591. </Teleport>
  592. )}
  593. </>
  594. )
  595. }
  596. },
  597. })