index.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. import { defineComponent, onMounted, onUnmounted, reactive, ref } from "vue";
  2. import state, { gotoNext, resetPlaybackToStart, followBeatPaly } from "/src/state";
  3. import { IPostMessage } from "/src/utils/native-message";
  4. import { api_cloudFollowTime, api_cloudToggleFollow } from "/src/helpers/communication";
  5. import { storeData } from "/src/store";
  6. import { audioRecorder } from "./audioRecorder";
  7. import { handleStartTick } from "/src/view/tick";
  8. import { metronomeData } from "/src/helpers/metronome";
  9. import { getDuration } from "/src/helpers/formateMusic";
  10. import { OpenSheetMusicDisplay } from "/osmd-extended/src";
  11. import { browser, getBehaviorId } from "/src/utils";
  12. import { api_musicPracticeRecordSave } from "../../page-instrument/api";
  13. import { getQuery } from "/src/utils/queryString";
  14. const query: any = getQuery();
  15. export const followData = reactive({
  16. list: [] as any, // 频率列表
  17. index: 0,
  18. start: false,
  19. rendered: false,
  20. /** 麦克风权限 */
  21. earphone: false,
  22. });
  23. // 记录跟练时长
  24. const handleRecord = (total: number) => {
  25. if (query.isCbs) return
  26. if (total < 0) total = 0;
  27. const totalTime = total / 1000;
  28. const body = {
  29. clientType: storeData.user.clientType,
  30. musicSheetId: state.examSongId,
  31. sysMusicScoreId: state.examSongId,
  32. feature: "FOLLOW_UP_TRAINING",
  33. practiceSource: "FOLLOW_UP_TRAINING",
  34. playTime: totalTime,
  35. deviceType: browser().android ? "ANDROID" : "IOS",
  36. behaviorId: getBehaviorId(),
  37. };
  38. api_musicPracticeRecordSave(body);
  39. };
  40. /** 点击跟练模式 */
  41. export const toggleFollow = (notCancel = true) => {
  42. state.modeType = state.modeType === "follow" ? "practise" : "follow";
  43. // 取消跟练
  44. if (!notCancel) {
  45. followData.start = false;
  46. openToggleRecord(false);
  47. }
  48. };
  49. const noteFrequency = ref(0);
  50. const audioFrequency = ref(0);
  51. const followTime = ref(0);
  52. // 切换录音
  53. const openToggleRecord = async (open: boolean = true) => {
  54. if (!open) api_cloudToggleFollow(open ? "start" : "end");
  55. // 记录跟练时长
  56. if (open) {
  57. followTime.value = Date.now();
  58. } else {
  59. const playTime = Date.now() - followTime.value;
  60. if (followTime.value !== 0 && playTime > 0) {
  61. handleRecord(playTime);
  62. followTime.value = 0;
  63. }
  64. }
  65. if (!storeData.isApp) {
  66. const openState = await audioRecorder?.toggleRecord(open);
  67. // 开启录音失败
  68. if (!openState && followData.start) {
  69. followData.earphone = true;
  70. followData.start = false;
  71. }
  72. }
  73. };
  74. // 清除音符状态
  75. const onClear = () => {
  76. state.times.forEach((item: any) => {
  77. const note: HTMLElement = document.querySelector(`div[data-vf=vf${item.id}]`)!;
  78. if (note) {
  79. note.classList.remove("follow-up", "follow-down", "follow-error", "follow-success");
  80. }
  81. const _note: HTMLElement = document.getElementById(`vf-${item.id}`)!;
  82. if (_note) {
  83. _note.classList.remove("follow-up", "follow-down");
  84. }
  85. });
  86. };
  87. /** 开始跟练 */
  88. export const handleFollowStart = async () => {
  89. const res = await api_cloudToggleFollow("start");
  90. // 用户没有授权,需要重置状态
  91. if (res?.content?.reson) {
  92. //
  93. } else {
  94. // 跟练模式开始前,增加播放系统节拍器
  95. followData.start = true;
  96. const tickend = await handleStartTick();
  97. // console.log("🚀 ~ tickend:", tickend)
  98. // 节拍器返回false, 取消播放
  99. if (!tickend) {
  100. followData.start = false;
  101. return false;
  102. }
  103. onClear();
  104. followData.start = true;
  105. followData.index = 0;
  106. followData.list = [];
  107. resetPlaybackToStart();
  108. openToggleRecord(true);
  109. getNoteIndex();
  110. const duration: any = getDuration(state.osmd as unknown as OpenSheetMusicDisplay);
  111. metronomeData.totalNumerator = duration.numerator || 2
  112. metronomeData.followAudioIndex = 1
  113. state.beatStartTime = 0
  114. followBeatPaly();
  115. }
  116. };
  117. /** 结束跟练 */
  118. export const handleFollowEnd = () => {
  119. onClear();
  120. followData.start = false;
  121. openToggleRecord(false);
  122. followData.index = 0;
  123. console.log("结束");
  124. };
  125. // 下一个
  126. const next = () => {
  127. gotoNext(state.times[followData.index]);
  128. };
  129. // 获取当前音符
  130. const getNoteIndex = (): any => {
  131. const item = state.times[followData.index];
  132. if (item.frequency <= 0) {
  133. followData.index = followData.index + 1;
  134. next();
  135. return getNoteIndex();
  136. }
  137. noteFrequency.value = item.frequency;
  138. // state.fixedKey = item.realKey;
  139. return {
  140. id: item.id,
  141. min: item.frequency - (item.frequency - item.prevFrequency) * 0.5,
  142. max: item.frequency + (item.nextFrequency - item.frequency) * 0.5,
  143. duration: item.duration,
  144. baseFrequency: item.frequency,
  145. };
  146. };
  147. let checking = false;
  148. /** 录音回调 */
  149. const onFollowTime = (evt?: IPostMessage) => {
  150. const frequency: number = evt?.content?.frequency;
  151. // 没有开始, 不处理
  152. if (!followData.start) return;
  153. // console.log('频率', frequency)
  154. if (frequency > 0) {
  155. audioFrequency.value = frequency;
  156. // data.list.push(frequency)
  157. checked();
  158. }
  159. };
  160. let startTime = 0;
  161. const checked = () => {
  162. if (checking) return;
  163. checking = true;
  164. const item = getNoteIndex();
  165. // 降噪处理, 频率低于 当前音的50% 时, 不处理
  166. if (audioFrequency.value < item.baseFrequency * 0.5) {
  167. checking = false;
  168. return;
  169. }
  170. if (audioFrequency.value >= item.min && audioFrequency.value <= item.max) {
  171. if (startTime === 0) {
  172. startTime = Date.now();
  173. } else {
  174. const playTime = (Date.now() - startTime) / 1000;
  175. // console.log('时长', playTime, item.duration / 2)
  176. if (playTime >= item.duration * 0.6) {
  177. startTime = 0;
  178. followData.index = followData.index + 1;
  179. setColor(item, "", true);
  180. next();
  181. checking = false;
  182. return;
  183. }
  184. }
  185. // console.log("频率对了", item.min, audioFrequency.value, item.max, item.duration);
  186. }
  187. // console.log("频率不对", item.min, audioFrequency.value, item.max, item.baseFrequency);
  188. setColor(item, audioFrequency.value > item.baseFrequency ? "follow-up" : "follow-down");
  189. checking = false;
  190. };
  191. const setColor = (item: any, state: "follow-up" | "follow-down" | "", isRight = false) => {
  192. const note: HTMLElement = document.querySelector(`div[data-vf=vf${item.id}]`)!;
  193. if (note) {
  194. note.classList.remove("follow-up", "follow-down", "follow-error", "follow-success");
  195. if (isRight) {
  196. note.classList.add("follow-success");
  197. } else {
  198. note.classList.add("follow-error", state);
  199. }
  200. }
  201. const _note: HTMLElement = document.getElementById(`vf-${item.id}`)!;
  202. if (_note) {
  203. _note.classList.remove("follow-up", "follow-down");
  204. state && _note.classList.add(state);
  205. }
  206. };
  207. export default defineComponent({
  208. name: "follow",
  209. setup() {
  210. onMounted(async () => {
  211. /** 如果是PC端 */
  212. if (!storeData.isApp) {
  213. const canRecorder = await audioRecorder.checkSupport();
  214. if (canRecorder) {
  215. audioRecorder.init();
  216. audioRecorder.progress = (frequency: number) => {
  217. onFollowTime({ api: "", content: { frequency } });
  218. };
  219. } else {
  220. followData.earphone = true;
  221. }
  222. } else {
  223. api_cloudFollowTime(onFollowTime);
  224. }
  225. console.log("进入跟练模式");
  226. });
  227. onUnmounted(() => {
  228. api_cloudFollowTime(onFollowTime, false);
  229. resetPlaybackToStart();
  230. onClear();
  231. openToggleRecord(false);
  232. console.log("退出跟练模式");
  233. });
  234. return () => <div></div>;
  235. },
  236. });