evaluating.tsx 18 KB

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