index.tsx 6.9 KB

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