evaluating.tsx 31 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022
  1. import { Button, Toast, Popup } from 'vant'
  2. import { defineComponent, onBeforeUnmount, onMounted, Ref, ref, Teleport, Transition, reactive } from 'vue'
  3. import '@dotlottie/player-component'
  4. import detailState, { isRhythmicExercises } 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. CallBack
  13. } from '/src/helpers/native-message'
  14. import { browser } from '/src/helpers/utils'
  15. import runtime, * as RuntimeUtils from '/src/pages/detail/runtime'
  16. import { getBoundingBoxByverticalNote, getNoteByMeasuresSlursStart, getParentNote } from '/src/pages/detail/helpers'
  17. import { handleCheckEvaluatStatus, useClientType, useOriginSearch, setWiredStatus } from '../uses'
  18. import { startButtonShow } from './index'
  19. import { getLeveByScoreMeasure } from '/src/pages/detail/evaluating/helper'
  20. import Evaluating, { evaluatingShow } from '../popups/evaluating'
  21. // @ts-ignore
  22. import styles from './index.module.less'
  23. import { Vue3Lottie } from 'vue3-lottie'
  24. import startData from './data/start_new.json'
  25. import startingData from './data/starting_new.json'
  26. import { unitTestData } from '../unitTest'
  27. import iconEvaluatingStart from './icons/icon-evaluatingStart.png'
  28. import qs from 'query-string'
  29. import CheckDelayPopup from "/src/pages/detail/CheckDelayPopup";
  30. import Headphone, { HeadphoneData } from "/src/pages/detail/Headphone";
  31. import { modelType, followRef } from './index'
  32. /** 初始化评测音频 */
  33. export const evaluatCreateMusicPlayer = () => {
  34. return new Promise((resolve) => {
  35. // 初始化曲谱音频 和效音音频
  36. postMessage(
  37. {
  38. api: "createMusicPlayer",
  39. content: {
  40. musicSrc: runtime.songs.background || runtime.songs.music, // 曲谱音频url
  41. tuneSrc: "https://oss.dayaedu.com/cloud-coach/1686725501654check_music1_(1).mp3", //效音音频url
  42. },
  43. },
  44. () => {
  45. if (browserInfo.ios) {
  46. resolve(true);
  47. }
  48. }
  49. );
  50. // 安卓不需要
  51. if(!browserInfo.ios){
  52. resolve(true)
  53. }
  54. })
  55. }
  56. const searchParams: any = qs.parse(location.search)
  57. /**
  58. * 节拍器时长
  59. * 评测模式时,应该传节拍器时长
  60. * 阶段评测时,判断是否从第一小节开始,并且曲子本身含有节拍器,需要传节拍器时长,否则传0
  61. */
  62. let actualBeatLength = 0
  63. let backtime = 0
  64. const initBehaviorId = '' + new Date().valueOf()
  65. const evaluating = ref(false)
  66. const playStatus: Ref<'connecting' | 'play' | 'stop'> = ref('stop')
  67. const endloading = ref(false)
  68. const connentLoading = ref(false)
  69. const playUrl: Ref<string> = ref('')
  70. const endResult = ref(null)
  71. const browserInfo = browser()
  72. const scoreList: any[] = []
  73. let calculateInfo: any = {}
  74. /** 延迟数据 */
  75. export const delayData = reactive({
  76. /** 是否强制检测 */
  77. isForce: true,
  78. /** 弹窗 */
  79. open: false,
  80. /** 延迟次数 */
  81. count: 0,
  82. /** 延迟时间 */
  83. time: 0,
  84. /** 耳机状态 */
  85. erji: false,
  86. /** 检测状态 */
  87. checkStatus: 'init' as 'init' | 'ing' | 'error',
  88. step: 1,
  89. earPhoneType: "" as "" | "有线耳机" | "蓝牙耳机",
  90. })
  91. let startTuneTimer: any = null
  92. /** 获取耳机状态 */
  93. const getWiredStatus = (): Promise<boolean> => {
  94. return new Promise((resolve) => {
  95. const timer = setTimeout(() => {
  96. resolve(false);
  97. }, 1000)
  98. postMessage({
  99. api: "isWiredHeadsetOn",
  100. }, (res) => {
  101. delayData.earPhoneType = res?.content?.type || "";
  102. const checkIsWired = res?.content?.checkIsWired ? true : false;
  103. if (checkIsWired) {
  104. if (delayData.step <= 5) {
  105. delayData.step = 3
  106. }
  107. } else {
  108. if (delayData.step === 2) {
  109. delayData.step = 4
  110. }
  111. }
  112. clearTimeout(timer)
  113. resolve(checkIsWired);
  114. });
  115. });
  116. }
  117. const closeErji = () => {
  118. //
  119. }
  120. /** 获取设备延迟 */
  121. const getDeviceDelay = (): Promise<number> => {
  122. return new Promise((resolve) => {
  123. const timer = setTimeout(() => {
  124. resolve(0);
  125. }, 1000)
  126. postMessage({
  127. api: "getDeviceDelay",
  128. }, (res) => {
  129. const delay = res?.content?.value > 0 ? res?.content?.value : 0;
  130. clearTimeout(timer)
  131. resolve(delay);
  132. });
  133. });
  134. }
  135. /** 设备延迟检测结束 */
  136. const handleCheckDelayEnd = () => {
  137. if (delayData.erji) {
  138. closeErji();
  139. } else {
  140. // this.erjiShow = true;
  141. HeadphoneData.toggle();
  142. }
  143. }
  144. /** 评测效验 */
  145. const checkEvaluating = async () => {
  146. // delayData.erji = await getWiredStatus();
  147. delayData.time = await getDeviceDelay();
  148. // 没有设备延迟数据,显示检测组件,并持续检测耳机状态
  149. if (!delayData.time || delayData.isForce) {
  150. delayData.count = 0;
  151. checkWiredStatus();
  152. if (runtime.delayCheckFirst && searchParams.evaluatingRecord) {
  153. // closeErji()
  154. }
  155. return;
  156. }
  157. handleCheckDelayEnd()
  158. }
  159. /** 持续检测耳机状态 */
  160. const checkWiredStatus = () => {
  161. console.log('耳机状态',delayData.checkStatus, delayData.step)
  162. // 设备检测结束,停止获取耳机状态
  163. // if (delayData.checkStatus !== 'ing' || delayData.open === false) {
  164. // return
  165. // }
  166. if (delayData.open === false) {
  167. return
  168. }
  169. setTimeout(async () => {
  170. delayData.erji = await getWiredStatus();
  171. if (delayData.erji) {
  172. delayData.count = 0;
  173. delayData.time = 0;
  174. // delayData.checkStatus = 'error'
  175. } else {
  176. if (delayData.step === 3) {
  177. delayData.step = 1
  178. delayData.checkStatus = 'init'
  179. }
  180. }
  181. checkWiredStatus();
  182. }, 1000)
  183. }
  184. /** 切换效音 */
  185. const handleToggleTune = (state: 'start' | 'stop' | 'finishTune') => {
  186. if (state === 'start') {
  187. delayData.step = 5
  188. // 开始效音
  189. postMessage({
  190. api: "startTune",
  191. content: {
  192. count: delayData.count + '',
  193. }
  194. }, (res) => {
  195. // 用户没有授权,需要重置状态
  196. if (res?.content?.reson) {
  197. delayData.step = 1
  198. delayData.checkStatus = 'init'
  199. } else {
  200. setTimeout(() => {
  201. handleToggleTune('stop')
  202. }, 1500)
  203. }
  204. })
  205. } else if (state === 'stop') {
  206. // 结束效音,触发时机: 1.监听后台效音返回 2.点击跳过效音或关闭效音
  207. postMessage({
  208. api: "endTune"
  209. })
  210. // 提前关闭或者返回,需要重置step状态
  211. if (delayData.open === false) {
  212. setTimeout(() => {
  213. delayData.step = 1
  214. }, 500);
  215. }
  216. } else if (state === 'finishTune') {
  217. delayData.step = 6
  218. // 效音完成
  219. postMessage({
  220. api: "finishTune",
  221. }, (res) => {
  222. const result = res?.content?.result //1成功 0失败
  223. // Toast('检测延迟完成')
  224. // setTimeout(() => {
  225. // delayData.open = false
  226. // }, 500)
  227. })
  228. }
  229. }
  230. /** 开始效音 */
  231. const startTune = () => {
  232. // 带了耳机,停止播放效音
  233. if (delayData.erji) return;
  234. handleToggleTune('start')
  235. }
  236. /** 停止设备延迟检测 */
  237. const handleStopCheckDelay = () => {
  238. runtime.delayCheckFirst = true
  239. delayData.open = false
  240. startButtonShow.value = true
  241. setTimeout(() => {
  242. delayData.checkStatus = 'init'
  243. delayData.step = 1
  244. postMessage(
  245. {
  246. api: 'isWiredHeadsetOn',
  247. },
  248. setWiredStatus
  249. )
  250. }, 500);
  251. handleToggleTune('stop')
  252. // this.close();
  253. }
  254. const handleDelayBack = () => {
  255. modelType.value = 'init'
  256. delayData.open = false
  257. delayData.checkStatus = 'init'
  258. delayData.step = 1
  259. handleToggleTune('stop')
  260. clearTimeout(startTuneTimer)
  261. }
  262. /** 开始检测设备延迟 */
  263. const handleStartCheckDelay = async () => {
  264. if (delayData.checkStatus === 'ing') return;
  265. delayData.step = 2
  266. delayData.erji = await getWiredStatus();
  267. if (delayData.erji) {
  268. delayData.checkStatus = 'error'
  269. return;
  270. }
  271. delayData.checkStatus = 'ing';
  272. startTuneTimer = setTimeout(() => {
  273. if (delayData.open === true) {
  274. startTune()
  275. }
  276. }, 2000)
  277. }
  278. // frequency 频率, amplitude 振幅, decibels 分贝
  279. type TCriteria = "frequency" | "amplitude" | "decibels";
  280. /** 获取评测标准 */
  281. const getEvaluationCriteria = () => {
  282. let criteria: TCriteria = "frequency";
  283. // 声部打击乐
  284. if ([23, 113, 121].includes(detailState.subjectId)) {
  285. criteria = "amplitude";
  286. } else if (isRhythmicExercises()) {
  287. // 分类为节奏练习
  288. criteria = "decibels";
  289. }
  290. return criteria;
  291. };
  292. /**
  293. * 默认按照442计算的音符频率,此处转化为按照设置进行调整
  294. * @param num 频率
  295. * @returns 转化后频率
  296. */
  297. const formatPitch = (num?: number): number => {
  298. if (!num) {
  299. return -1
  300. }
  301. if (SettingState.sett.hertz && SettingState.sett.hertz !== 442) {
  302. return (num / 442) * SettingState.sett.hertz
  303. }
  304. return num
  305. }
  306. let starTime = 0
  307. const formatTimes = () => {
  308. const rate = runtime.speed / detailState.baseSpeed //1
  309. actualBeatLength = Math.round(detailState.times[0].fixtime * 1000 / rate)
  310. const difftime = detailState.times?.[0]?.difftime || 0
  311. let ListenMode = false
  312. let dontEvaluatingMode = false
  313. let skip = false
  314. const datas = []
  315. let times = detailState.times
  316. // 阶段评测前一个节拍的标示
  317. let preLyricsContent = ''
  318. let preTimes = []
  319. let unitTestIdx = 0
  320. let preTime = 0
  321. if (unitTestData.isSelectMeasureMode) {
  322. const startIndex = detailState.times.findIndex(
  323. (n: any) => n.NoteToGraphicalNoteObjectId == detailState.section[0].NoteToGraphicalNoteObjectId
  324. )
  325. const endIndex = detailState.times.findIndex(
  326. (n: any) => n.NoteToGraphicalNoteObjectId == detailState.section[1].NoteToGraphicalNoteObjectId
  327. )
  328. if (startIndex > 1) {
  329. preTime = detailState.times[startIndex-1].time * 1000
  330. }
  331. times = detailState.times.filter((n: any, index: number) => {
  332. return index >= startIndex && index <= endIndex
  333. })
  334. preTimes = detailState.times.filter((n: any, index: number) => {
  335. return index < startIndex
  336. })
  337. starTime = times[0].sourceRelativeTime || times[0].relativeTime
  338. actualBeatLength = startIndex == 0 && !detailState.needTick ? actualBeatLength : 0
  339. // console.log("🚀 ~ times", times, '开始小节', startIndex, actualBeatLength)
  340. unitTestIdx = startIndex
  341. }
  342. // 找到阶段评测,开始小节前面最近的是play或者listen的小节
  343. if (preTimes.length) {
  344. for (let index = preTimes.length-1; index >= 0; index--) {
  345. const item = preTimes[index]
  346. const note = getNoteByMeasuresSlursStart(item)
  347. if (note.formatLyricsEntries.contains('Play') || note.formatLyricsEntries.contains('Play...')) {
  348. preLyricsContent = 'Play'
  349. break
  350. }
  351. if (note.formatLyricsEntries.contains('Listen')) {
  352. preLyricsContent = 'Listen'
  353. break
  354. }
  355. }
  356. preLyricsContent = preLyricsContent ? preLyricsContent : 'Play'
  357. }
  358. // 阶段评测beatLength需要加上预备小节的持续时长
  359. actualBeatLength = preTimes.length ? actualBeatLength + preTimes[preTimes.length - 1].duration * 1000 : actualBeatLength
  360. let measureIndex = -1
  361. let recordMeasure = -1
  362. let firstNoteTime = unitTestIdx > 1 ? preTime : 0
  363. for (let index = 0; index < times.length; index++) {
  364. const item = times[index]
  365. const note = getNoteByMeasuresSlursStart(item)
  366. const rate = runtime.speed / detailState.baseSpeed //1
  367. const start = difftime + (item.sourceRelativeTime || item.relativeTime) - starTime
  368. const end = difftime + (item.sourceRelaEndtime || item.relaEndtime) - starTime
  369. // console.log(start, end, starTime)
  370. const isStaccato =
  371. typeof note.voiceEntry.isStaccato === 'function' ? note.voiceEntry.isStaccato() : note.voiceEntry.isStaccato
  372. const noteRate = isStaccato ? 0.5 : 1
  373. // console.log('注脚', note.formatLyricsEntries)
  374. // 如果阶段评测,开始小节没有注脚,则取前面最近的小节的注脚
  375. if (index == 0 && !note.formatLyricsEntries.length) {
  376. ListenMode = preLyricsContent === 'Play' ? false : preLyricsContent === 'Listen' ? true : false
  377. }
  378. if (note.formatLyricsEntries.contains('Play') || note.formatLyricsEntries.contains('Play...')) {
  379. ListenMode = false
  380. }
  381. if (note.formatLyricsEntries.contains('Listen')) {
  382. ListenMode = true
  383. }
  384. if (note.formatLyricsEntries.contains('纯律结束')) {
  385. dontEvaluatingMode = false
  386. }
  387. if (note.formatLyricsEntries.contains('纯律')) {
  388. dontEvaluatingMode = true
  389. }
  390. const nextNote = detailState.times[index + 1]
  391. if (skip && (note.stave || !note.noteElement.isRestFlag || (nextNote && !nextNote.noteElement.isRestFlag))) {
  392. skip = false
  393. }
  394. if (note.noteElement.isRestFlag && !!note.stave && !!nextNote && nextNote.noteElement.isRestFlag) {
  395. skip = true
  396. }
  397. if (note.measureOpenIndex != recordMeasure) {
  398. measureIndex++
  399. recordMeasure = note.measureOpenIndex
  400. }
  401. // console.log(note.measureOpenIndex , measureIndex, note.noteElement.sourceMeasure.measureListIndex)
  402. const data = {
  403. timeStamp: (start * 1000) / rate,
  404. duration: ((end * 1000) / rate - (start * 1000) / rate) * noteRate,
  405. frequency: formatPitch(item.noteElement?.pitch?.frequency),
  406. nextFrequency: formatPitch(item.noteElement?.pitch?.nextFrequency),
  407. prevFrequency: formatPitch(item.noteElement?.pitch?.prevFrequency),
  408. measureIndex: measureIndex, //note.measureOpenIndex,
  409. measureRenderIndex: note.noteElement.sourceMeasure.measureListIndex,
  410. dontEvaluating: ListenMode || dontEvaluatingMode || item.skipMode,
  411. musicalNotesIndex: index, //item.i,
  412. denominator: note.noteElement?.Length.denominator,
  413. isOrnament: !!note?.voiceEntry?.ornamentContainer,
  414. }
  415. // console.log('时间1111', data)
  416. datas.push(data)
  417. }
  418. return {
  419. datas,
  420. firstNoteTime
  421. }
  422. }
  423. const connect = async () => {
  424. const search = useOriginSearch()
  425. connentLoading.value = true
  426. const behaviorId = sessionStorage.getItem('behaviorId') || search.behaviorId || initBehaviorId
  427. const rate = runtime.speed / detailState.baseSpeed //1
  428. calculateInfo = formatTimes()
  429. const content = {
  430. musicXmlInfos: calculateInfo.datas,
  431. firstNoteTime: calculateInfo.firstNoteTime,
  432. subjectId: detailState.subjectId ? detailState.subjectId : detailState.isPercussion ? 1 : detailState.subjectId,
  433. detailId: detailState.activeDetail?.id,
  434. examSongId: search.id,
  435. xmlUrl: detailState?.activeDetail?.xmlUrl,
  436. partIndex: detailState.partIndex,
  437. behaviorId,
  438. platform: 'WEB',
  439. clientId: 'STUDENT',
  440. hertz: SettingState.sett.hertz,
  441. feature: 'EVALUATION',
  442. practiceSource: (search.resourceType && search.resourceType === 'practice') ? 'UNIT_TEST_PRACTICE' : search.unitId ? 'UNIT_TEST' : 'PRACTICE',
  443. // 这里定义的是数字但是因为是通过input输入所以强制转化一次
  444. reactionTimeMs: parseFloat('' + SettingState.eva.reactionTimeMs) || 0,
  445. speed: runtime.speed,
  446. heardLevel: SettingState.eva.difficulty,
  447. // beatLength: Math.round((RuntimeUtils.getFixTime(detailState.times[0].beatSpeed) * 1000) / rate),
  448. beatLength: actualBeatLength,
  449. evaluationCriteria: getEvaluationCriteria(),
  450. }
  451. // console.log("🚀 ~ content:", content, rate)
  452. const clientType = useClientType()
  453. if (clientType === 'student') {
  454. content.clientId = 'STUDENT'
  455. } else if (clientType === 'teacher') {
  456. content.clientId = 'TEACHER'
  457. } else {
  458. content.clientId = 'BACKEND'
  459. }
  460. if (browserInfo.android) {
  461. content.platform = 'ANDROID'
  462. }
  463. if (browserInfo.ios) {
  464. content.platform = 'IOS'
  465. }
  466. const evt = await promisefiyPostMessage({
  467. api: 'startEvaluating',
  468. content: content,
  469. })
  470. if (evt?.content?.reson) {
  471. Toast.fail({
  472. message: evt?.content?.reson,
  473. })
  474. connentLoading.value = false
  475. throw evt
  476. }
  477. connentLoading.value = false
  478. }
  479. const sendOffsetTime = (offsetTime: number) => {
  480. postMessage(
  481. {
  482. api: 'proxyServiceMessage',
  483. content: {
  484. header: {
  485. commond: 'audioPlayStart',
  486. type: 'SOUND_COMPARE',
  487. },
  488. body: {
  489. offsetTime,
  490. },
  491. },
  492. },
  493. () => {
  494. backtime = 0
  495. }
  496. )
  497. }
  498. const cancelTheEvaluation = () => {
  499. const search = useOriginSearch()
  500. setTimeout(() => {
  501. postMessage({ api: 'endEvaluating', content: { musicScoreId: search.id } })
  502. playStatus.value = 'stop'
  503. RuntimeUtils.pause()
  504. RuntimeUtils.resetPlayStatus()
  505. RuntimeUtils.clearIntervalTimeline()
  506. RuntimeUtils.setCurrentTime(0)
  507. Toast.clear()
  508. }, 500);
  509. }
  510. const stopPlay = () => {
  511. console.log('调用stopPlay')
  512. if (!connentLoading.value && evaluating.value) {
  513. cancelTheEvaluation()
  514. }
  515. startButtonShow.value = true
  516. connentLoading.value = false
  517. evaluating.value = false
  518. modelType.value = 'init'
  519. }
  520. export const evaluatStopPlay = stopPlay
  521. const startPlay = () => {
  522. console.log('连接服务成功,开始播放', new Date().getTime() - runtime.clickTime)
  523. // if (!SettingState.eva.mute) {
  524. // RuntimeUtils.changeAllMode()
  525. // } else {
  526. // RuntimeUtils.changeMode('background')
  527. // }
  528. startButtonShow.value = false
  529. RuntimeUtils.setPlayState()
  530. }
  531. const setPlayer = async () => {
  532. // 连接中,禁止重复连接
  533. if (connentLoading.value) return
  534. runtime.clickTime = new Date().getTime()
  535. RuntimeUtils.resetPlayStatus()
  536. if (detailState.isPauseRecording) {
  537. evaluating.value = false
  538. startPlay()
  539. return
  540. }
  541. detailState.evaluatings = {}
  542. const hint = Toast({
  543. duration: 0,
  544. message: '服务连接中...',
  545. type: 'loading',
  546. })
  547. try {
  548. await connect()
  549. //startPlay()
  550. setTimeout(() => {
  551. console.log('关闭弹窗')
  552. startButtonShow.value = false
  553. Toast.clear()
  554. hint.close()
  555. }, 100)
  556. } catch (error) {
  557. runtime.evaluatingStatus = false
  558. Toast.clear()
  559. }
  560. evaluatStart()
  561. }
  562. const togglePlay = () => {
  563. if (detailState.isPauseRecording) {
  564. evaluating.value = false
  565. startPlay()
  566. return
  567. }
  568. if (evaluating.value) {
  569. stopPlay()
  570. } else {
  571. setPlayer()
  572. }
  573. }
  574. const timeupdate = () => {
  575. console.log('播放事件被触发', playUrl.value, evaluating.value)
  576. if (playUrl.value) {
  577. const nowTime = new Date().getTime()
  578. // console.log('播放开始的时间', nowTime)
  579. // synced = true
  580. let time = runtime.audiosInstance?.audios[playUrl.value].currentTime
  581. // 只有选段模式,并且开始小节非第一小节时,才执行以下计算
  582. const sectionIdx = detailState.section.length ? detailState.section[0].i : 0
  583. if (unitTestData.isSelectMeasureMode && sectionIdx > 0 ) {
  584. time = time - detailState.section[0].time
  585. }
  586. console.log('已播放时长: ', time * 1000)
  587. console.log('不减掉已播放时间: ', nowTime - backtime)
  588. const delayTime = nowTime - backtime - time * 1000
  589. console.log('真正播放延迟', delayTime, time, unitTestData.isSelectMeasureMode, sectionIdx)
  590. // 蓝牙耳机延迟一点发送消息确保在录音后面
  591. setTimeout(() => {
  592. sendOffsetTime(delayTime)
  593. }, 220)
  594. }
  595. }
  596. /**
  597. * 播放器停止事件
  598. */
  599. const playerStop = () => {
  600. // alert('stop' + this.endloading)
  601. console.log('playerStop播放器停止事件', endloading.value)
  602. if (endloading.value) {
  603. return
  604. }
  605. playStatus.value = 'stop'
  606. endloading.value = true
  607. startButtonShow.value = true
  608. RuntimeUtils.resetPlayStatus()
  609. RuntimeUtils.clearIntervalTimeline()
  610. RuntimeUtils.setCurrentTime(0)
  611. Toast({
  612. duration: 0,
  613. message: '评分中...',
  614. type: 'loading',
  615. })
  616. setTimeout(() => {
  617. postMessage(
  618. {
  619. api: 'endEvaluating',
  620. content: {
  621. musicScoreId: useOriginSearch().id,
  622. },
  623. },
  624. (evt) => {
  625. console.log('调用endEvaluating结束', evt)
  626. endloading.value = false
  627. evaluating.value = false
  628. // RuntimeUtils.setCaptureMode()
  629. }
  630. )
  631. RuntimeUtils.endCapture()
  632. }, 500);
  633. }
  634. export const evaluatPlayerStop = playerStop
  635. const endevent = (evt: Event) => {
  636. // 如果是单元测验和课后训练 播放结束
  637. if (unitTestData.isSelectMeasureMode && playStatus.value === 'play') {
  638. playerStop()
  639. canSubmit.value = true
  640. return
  641. }
  642. if ((evt.target as HTMLAudioElement)?.src === playUrl.value && playStatus.value === 'play') {
  643. playerStop()
  644. canSubmit.value = true
  645. }
  646. if (detailState.isAppPlay) {
  647. playerStop()
  648. canSubmit.value = true
  649. }
  650. }
  651. /**正式开始评测 */
  652. const evaluatStart = () => {
  653. playStatus.value = 'play'
  654. if (detailState.isPauseRecording) {
  655. postMessage(
  656. {
  657. api: 'resumeRecording',
  658. },
  659. () => {
  660. evaluating.value = true
  661. detailState.isPauseRecording = false
  662. RuntimeUtils.setCaptureMode()
  663. }
  664. )
  665. return
  666. } else {
  667. RuntimeUtils.setCaptureMode()
  668. }
  669. console.log('开始录音', new Date().getTime())
  670. postMessage(
  671. {
  672. api: 'startRecording',
  673. content: {
  674. accompanimentState: SettingState.eva.mute ? 1 : 0,
  675. firstNoteTime: calculateInfo.firstNoteTime || 0,
  676. }
  677. },
  678. () => {
  679. // console.log('开始录音app回调时间', Date.now())
  680. backtime = Date.now()
  681. evaluating.value = true
  682. runtime.playState = "play";
  683. if (detailState.activeDetail?.midiUrl) {
  684. console.log('midiUrl', detailState.activeDetail?.midiUrl)
  685. setTimeout(() => {
  686. sendOffsetTime(0)
  687. }, 220)
  688. }
  689. }
  690. )
  691. RuntimeUtils.startCapture()
  692. }
  693. /** 音频播放完结束评测 */
  694. const playEnd_endEvalute = () => {
  695. playerStop()
  696. }
  697. /**
  698. * 酷乐秀活动接口,Url中有设置并且仅在学生端提评分交数据
  699. * 管乐团单元测验, url中有单元测验ID仅在学生端提评分交数据
  700. */
  701. const submitEvaluationScore = async (data: any) => {
  702. const search = useOriginSearch()
  703. if (search.unitId) {
  704. if (!canSubmit.value) {
  705. Toast('完整演奏结束才算测验分数!')
  706. return
  707. }
  708. (endResult.value as any)?.score && scoreList.push((endResult.value as any)?.score)
  709. /** 有单元测验时,存储分数缓存 */
  710. postMessage({
  711. api: 'setCache',
  712. content: {
  713. key: 'h5-orchestra-unit',
  714. value: JSON.stringify({
  715. musicId: search.id || '',
  716. unitId: search.unitId || '',
  717. questionId: search.questionId || '',
  718. score: canSubmit.value ? (endResult.value as any)?.score || 0 : 0,
  719. }),
  720. },
  721. })
  722. canSubmit.value = false
  723. }
  724. }
  725. /** 活动使用,只有在全部评测完成后才能提交,避免只是一小节就高分的情况 */
  726. const canSubmit = ref(false)
  727. /**接受websocket返回的信息 */
  728. const sendResult = (evt?: IPostMessage) => {
  729. const { body, header } = evt?.content || {}
  730. console.log('评测返回', body)
  731. if (body && header) {
  732. const data = evt?.content?.body
  733. if (evt?.content.header.commond === 'overall') {
  734. // console.log(evt)
  735. detailState.isHideEvaluatReportSaveBtn = false;
  736. Toast.clear()
  737. endResult.value = data
  738. evaluatingShow.value = true
  739. submitEvaluationScore(data)
  740. } else if (evt?.content.header.commond === 'checkDone') {
  741. // 此处已经在校音中单独监听不做处理
  742. } else if (evt?.content.header.commond === 'checking') {
  743. // 此处已经在校音中单独监听不做处理
  744. } else if (evt?.content.header.commond === "recordEnd") {
  745. if (delayData.checkStatus !== 'ing') return
  746. delayData.count++;
  747. if (delayData.count >= 2) {
  748. handleToggleTune('finishTune');
  749. return;
  750. }
  751. setTimeout(() => {
  752. startTune()
  753. }, 100)
  754. } else {
  755. const getBeforeNote = (index: number) => {
  756. while (index >= 0) {
  757. const item = detailState.times[index]
  758. if (item.stave) {
  759. return item
  760. }
  761. index--
  762. }
  763. }
  764. const setEvaluatings = (note: any, data: any, dontTransition = false) => {
  765. const startNote = getBoundingBoxByverticalNote(note)
  766. // console.log(detailState.evaluatings, startNote)
  767. detailState.evaluatings = {
  768. ...detailState.evaluatings,
  769. [startNote.measureIndex]: {
  770. ...startNote,
  771. ...getLeveByScoreMeasure(data.score),
  772. score: data.score,
  773. dontTransition,
  774. },
  775. }
  776. }
  777. for (let index = 0; index < detailState.times.length; index++) {
  778. let time = detailState.times[index]
  779. if (data.measureRenderIndex == time.noteElement.sourceMeasure.measureListIndex) {
  780. if (!time.stave) {
  781. const ntime = getBeforeNote(index)
  782. // console.log('ntime', ntime)
  783. if (ntime) {
  784. time = ntime
  785. }
  786. }
  787. if (!time.noteElement.tie) {
  788. setEvaluatings(time, data)
  789. } else {
  790. for (const item of time.noteElement.tie.notes) {
  791. const note = getParentNote(item)
  792. if (!note) continue
  793. setEvaluatings(
  794. note,
  795. data,
  796. item.NoteToGraphicalNoteObjectId !== time.noteElement.tie.StartNote?.NoteToGraphicalNoteObjectId
  797. )
  798. }
  799. }
  800. break
  801. }
  802. }
  803. }
  804. }
  805. }
  806. const onProgress = () => {
  807. // console.log(runtime.currentTimeNum, detailState.times[detailState.times.length - 1]?.time - 2, detailState.times)
  808. if (runtime.currentTimeNum >= detailState.times[detailState.times.length - 1]?.time - 2) {
  809. canSubmit.value = true
  810. }
  811. }
  812. const cloudMetronome = (evt: any) => {
  813. startButtonShow.value = true
  814. }
  815. /** 监听评测弹窗是否隐藏保存演奏按钮 */
  816. const hideComplexButton = (callback: CallBack, listen?: boolean) => {
  817. if (listen) {
  818. listenerMessage("hideComplexButton", callback);
  819. } else {
  820. removeListenerMessage("hideComplexButton", callback);
  821. }
  822. };
  823. // 隐藏存演奏按钮
  824. const handleComplexButton = (res?: IPostMessage) => {
  825. console.log('监听是否隐藏保存按钮', res)
  826. if (res?.content) {
  827. const { header, body } = res.content;
  828. detailState.isHideEvaluatReportSaveBtn = true
  829. }
  830. };
  831. // 离开之前再提交一次最高分数
  832. export const submitMaxScore = () => {
  833. const search = useOriginSearch()
  834. if (search.unitId && scoreList.length) {
  835. console.log('最高分',scoreList,Math.max(...scoreList))
  836. postMessage({
  837. api: 'setCache',
  838. content: {
  839. key: 'h5-orchestra-unit',
  840. value: JSON.stringify({
  841. musicId: search.id || '',
  842. unitId: search.unitId || '',
  843. questionId: search.questionId || '',
  844. score: Math.max(...scoreList),
  845. }),
  846. },
  847. })
  848. }
  849. }
  850. /**
  851. * 木管(长笛 萨克斯 单簧管)乐器一级的2、3、6测评要放原音音频
  852. * 铜管乐器一级的1a,1b,5,6测评要放原音音频
  853. */
  854. const getMusicMode = (): RuntimeUtils.IMode => {
  855. const muguan = [2, 4, 5, 6];
  856. const tongguan = [12, 13, 14, 15, 17];
  857. if (muguan.includes(detailState.subjectId) && (detailState.activeDetail?.examSongName || "").search(/[^\u0000-\u00FF](1-2|1-3|1-6)/gi) > -1) {
  858. return "music";
  859. }
  860. if (tongguan.includes(detailState.subjectId) && (detailState.activeDetail?.examSongName || "").search(/[^\u0000-\u00FF](1-1-1|1-1-2|1-5|1-6)/gi) > -1) {
  861. return "music";
  862. }
  863. if ([23, 113, 121].includes(detailState.subjectId)) {
  864. return "music";
  865. }
  866. return "background";
  867. };
  868. export default defineComponent({
  869. name: 'ColexiuButtonEvaluating',
  870. setup(props, { expose }) {
  871. onMounted(async () => {
  872. console.log('进入评测模块')
  873. delayData.open = (runtime.delayCheckFirst && searchParams.evaluatingRecord || !SettingState.sett.tuning) ? false : true
  874. if (!SettingState.eva.mute) {
  875. RuntimeUtils.changeAllMode();
  876. } else {
  877. RuntimeUtils.changeMode('background', 'all')
  878. }
  879. handleCheckEvaluatStatus()
  880. // 如果为单元测验和课后训练,不清楚选段数据
  881. if (!unitTestData.isSelectMeasureMode) {
  882. detailState.section = []
  883. detailState.sectionStatus = false
  884. }
  885. playUrl.value = runtime.songs.background || (runtime.songs.music as string)
  886. // runtime.audiosInstance?.audios[playUrl.value]?.addEventListener('play', timeupdate)
  887. // runtime.audiosInstance?.audios[playUrl.value]?.addEventListener('timeupdate', onProgress)
  888. // RuntimeUtils.event.on('next-click', playerStop)
  889. RuntimeUtils.event.on('ended', endevent)
  890. listenerMessage('sendResult', sendResult)
  891. // listenerMessage('cancelEvaluating', cancelTheEvaluation)
  892. listenerMessage('cloudTimeUpdae', onProgress)
  893. RuntimeUtils.event.on('tickDestroy', cloudMetronome)
  894. RuntimeUtils.event.on('tickEnd', evaluatStart)
  895. runtime.playEndCallback.endEvaluat = playEnd_endEvalute
  896. hideComplexButton(handleComplexButton, true);
  897. // 开始效验
  898. checkEvaluating()
  899. })
  900. onBeforeUnmount(() => {
  901. // runtime.audiosInstance?.audios[playUrl.value]?.removeEventListener('play', timeupdate)
  902. // runtime.audiosInstance?.audios[playUrl.value]?.removeEventListener('timeupdate', onProgress)
  903. // RuntimeUtils.event.off('next-click', playerStop)
  904. RuntimeUtils.event.off('ended', endevent)
  905. RuntimeUtils.event.off('tickDestroy', cloudMetronome)
  906. removeListenerMessage('sendResult', sendResult)
  907. // removeListenerMessage('cancelEvaluating', cancelTheEvaluation)
  908. removeListenerMessage('cloudTimeUpdae', onProgress)
  909. hideComplexButton(() => {}, false);
  910. RuntimeUtils.event.off('tickEnd', evaluatStart)
  911. submitMaxScore()
  912. })
  913. expose({
  914. setPlayer,
  915. startPlay,
  916. stopPlay,
  917. togglePlay,
  918. playerStop,
  919. evaluating,
  920. connentLoading,
  921. playStatus,
  922. cancelTheEvaluation,
  923. })
  924. return () => {
  925. return (
  926. <Teleport to="body" key="StartEvaluating">
  927. {/* 评测完成结果显示 */}
  928. <Evaluating data={endResult.value} />
  929. <Transition name="finish">
  930. {startButtonShow.value && !delayData.open && modelType.value !== 'init' && (
  931. <div
  932. style={{
  933. backgroundImage: `url(${iconEvaluatingStart})`,
  934. 'transform': detailState.isSpecialShapedScreen ? `translateX(${detailState.notchHeight / 4}px)` : '',
  935. }}
  936. class={[styles.evaluatStartBtn]}
  937. onClick={() => {
  938. setPlayer()
  939. }}
  940. ></div>
  941. )}
  942. </Transition>
  943. {!evaluating.value ? (
  944. <div class={styles.dialogueBox} key="start">
  945. <div class={styles.dialogue}>
  946. <div>
  947. 演奏前请调整好乐器,保证最佳演奏状态。<span class={styles.triangle}></span>
  948. </div>
  949. </div>
  950. <Vue3Lottie class={styles.dialogueIcon} animationData={startData}></Vue3Lottie>
  951. </div>
  952. ) : (
  953. <div class={styles.dialogueBox} key="starting">
  954. <div class={styles.inRadio}>收音中...</div>
  955. <Vue3Lottie class={styles.inRadioIcon} animationData={startingData}></Vue3Lottie>
  956. </div>
  957. )}
  958. {/* 延迟检测窗口 */}
  959. <Transition>
  960. <Popup
  961. teleport="body"
  962. class="popup-scale"
  963. transition="van-scale"
  964. overlay={false}
  965. show={delayData.open}
  966. onClose={() => handleCheckDelayEnd()}
  967. >
  968. <CheckDelayPopup
  969. delayData={delayData}
  970. onStartCheckDelay={() => handleStartCheckDelay()}
  971. onClose={() => handleStopCheckDelay()}
  972. onBack={() => handleDelayBack()}
  973. />
  974. </Popup>
  975. </Transition>
  976. </Teleport>
  977. )
  978. }
  979. },
  980. })