evaluating.tsx 18 KB

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