import { defineComponent, onMounted, onUnmounted, reactive, ref, watch } from "vue"; import state, { gotoNext, resetPlaybackToStart, followBeatPaly, skipNotePlay, initSetPlayRate } from "/src/state"; import { IPostMessage } from "/src/utils/native-message"; import { api_cloudFollowTime, api_cloudToggleFollow } from "/src/helpers/communication"; import { storeData } from "/src/store"; import { audioRecorder } from "./audioRecorder"; import { handleStartTick } from "/src/view/tick"; import { metronomeData } from "/src/helpers/metronome"; import { getDuration } from "/src/helpers/formateMusic"; import { OpenSheetMusicDisplay } from "/osmd-extended/src"; import { browser, getBehaviorId } from "/src/utils"; import { api_musicPracticeRecordSave } from "../../page-instrument/api"; import { getQuery } from "/src/utils/queryString"; import { getAudioDuration } from "/src/view/audio-list"; const query: any = getQuery(); export const followData = reactive({ list: [] as any, // 频率列表 index: 0, start: false, rendered: false, /** 麦克风权限 */ earphone: false, isBeginMask: false, // 倒计时和系统节拍器时候的遮罩,防止用户点击 dontAccredit: true, // 没有开启麦克风权限,不需要调用结束收音的api practiceStart: false, }); // 记录跟练时长 const handleRecord = (total: number) => { if (query.isCbs) return if (total < 0) total = 0; const totalTime = total / 1000; const rate = state.basePlayRate * state.originAudioPlayRate; // 播放倍率 // 如果是选段,则选选段开头小节的速度 const currentSpeed = state.sectionStatus && state.section.length === 2 && state.section[0].measureSpeed ? state.section[0].measureSpeed * state.basePlayRate : state.speed; const body = { clientType: storeData.user.clientType, musicSheetId: state.examSongId, sysMusicScoreId: state.examSongId, feature: "FOLLOW_UP_TRAINING", practiceSource: "FOLLOW_UP_TRAINING", playTime: totalTime, deviceType: browser().android ? "ANDROID" : "IOS", behaviorId: getBehaviorId(), sourceTime: getAudioDuration(), // 音频时长 instrumentId: state.instrumentId, playRate: rate, partIndex: state.partIndex, // 音轨 speed: currentSpeed, // 速度 }; api_musicPracticeRecordSave(body); }; /** 点击跟练模式 */ export const toggleFollow = (notCancel = true) => { state.modeType = state.modeType === "follow" ? "practise" : "follow"; // 取消跟练 if (!notCancel) { followData.start = false; followData.practiceStart = false; // 开启了麦克风授权,才需要调用结束收音 if (storeData.isApp && !followData.dontAccredit) { openToggleRecord(false); } } }; const noteFrequency = ref(0); const audioFrequency = ref(0); const followTime = ref(0); // 切换录音 const openToggleRecord = async (open: boolean = true) => { if (!open) api_cloudToggleFollow(open ? "start" : "end"); // 记录跟练时长 if (open) { followTime.value = Date.now(); } else { const playTime = Date.now() - followTime.value; if (followTime.value !== 0 && playTime > 0) { handleRecord(playTime); followTime.value = 0; } } if (!storeData.isApp) { const openState = await audioRecorder?.toggleRecord(open); // 开启录音失败 if (!openState && followData.start) { followData.earphone = true; followData.start = false; followData.practiceStart = false; } } }; // 清除音符状态 const onClear = () => { state.times.forEach((item: any) => { const note: HTMLElement = document.querySelector(`div[data-vf=vf${item.id}]`)!; if (note) { note.classList.remove("follow-up", "follow-down", "follow-error", "follow-success"); } const _note: HTMLElement = document.getElementById(`vf-${item.id}`)!; const stemEl = document.getElementById(`vf-${item.id}-stem`); if (_note) { _note.classList.remove("follow-up", "follow-down", "follow-success"); stemEl?.classList.remove("follow-up", "follow-down", "follow-success"); } }); }; /** 开始跟练 */ export const handleFollowStart = async () => { followData.isBeginMask = true checking = false; const res = await api_cloudToggleFollow("start"); // 用户没有授权,需要重置状态 if (res?.content?.reson) { followData.isBeginMask = false followData.start = false; followData.practiceStart = false; } else { followData.dontAccredit = false; // 从头开始跟练,跟练模式开始前,增加播放系统节拍器 if (state.activeNoteIndex === 0) { const tickend = await handleStartTick(); // console.log("🚀 ~ tickend:", tickend) // 节拍器返回false, 取消播放 if (!tickend) { followData.isBeginMask = false followData.start = false; followData.practiceStart = false; return false; } } onClear(); followData.isBeginMask = false followData.start = true; followData.practiceStart = true; // followData.index = 0; followData.index = state.activeNoteIndex; followData.list = []; initSetPlayRate(); // resetPlaybackToStart(); openToggleRecord(true); getNoteIndex(); const duration: any = getDuration(state.osmd as unknown as OpenSheetMusicDisplay); metronomeData.totalNumerator = duration.numerator || 2 metronomeData.followAudioIndex = 1 state.beatStartTime = 0 followBeatPaly(); } }; /** 结束跟练 */ export const handleFollowEnd = () => { onClear(); followData.start = false; followData.practiceStart = false; openToggleRecord(false); followData.index = 0; console.log("结束"); }; // 清除当前音符右侧的音符的颜色状态 const clearRightNoteColor = () => { const noteId = state.times[state.activeNoteIndex]?.id; const leftVal = document.getElementById(`vf-${noteId}`)?.getBoundingClientRect()?.left || 0; state.times.forEach((item: any) => { const note: HTMLElement = document.getElementById(`vf-${item.id}`)!; if (note?.getBoundingClientRect()?.left >= leftVal) { note.classList.remove("follow-up", "follow-down", "follow-error", "follow-success"); } }); } /** * 2024.6.17 新增自动结束跟练模式功能 * 如果跟练模式,唱完了最后一个有频率的音符,需要自动结束掉跟练模式 * 需要判断当前音符的后面是否都是休止音符(休止音符的频率都是-1),或者是否是最后一个音符了 */ const autoEndFollow = () => { if (followData.index >= state.times.length) { handleFollowEnd() return } let nextIndex = followData.index + 1; const rightTimes = state.times.slice(followData.index,state.times.length) // 后面的音符是否都是休止音符 const isAllRest = !rightTimes.some((item: any) => item.frequency > 1); if (isAllRest && state.times[followData.index].frequency < 1) { handleFollowEnd() return } clearRightNoteColor(); } // 下一个 const next = () => { if (followData.index < state.times.length) { gotoNext(state.times[followData.index]); } autoEndFollow(); }; // 获取当前音符 const getNoteIndex = (): any => { const item = state.times[followData.index]; if (item.frequency <= 0) { followData.index = followData.index + 1; next(); return getNoteIndex(); } noteFrequency.value = item.frequency; // state.fixedKey = item.realKey; return { id: item.id, min: item.frequency - (item.frequency - item.prevFrequency) * 0.5, max: item.frequency + (item.nextFrequency - item.frequency) * 0.5, duration: item.duration, baseFrequency: item.frequency, }; }; let checking = false; /** 录音回调 */ const onFollowTime = (evt?: IPostMessage) => { const frequency: number = evt?.content?.frequency; // 没有开始, 不处理 if (!followData.start) return; // console.log('频率', frequency) if (frequency > 0) { audioFrequency.value = frequency; // data.list.push(frequency) checked(); } }; let startTime = 0; const checked = () => { if (checking) return; checking = true; const item = getNoteIndex(); // 降噪处理, 频率低于 当前音的50% 时, 不处理 if (audioFrequency.value < item.baseFrequency * 0.5) { checking = false; return; } if (audioFrequency.value >= item.min && audioFrequency.value <= item.max) { if (startTime === 0) { startTime = Date.now(); } else { const playTime = (Date.now() - startTime) / 1000; // console.log('时长', playTime, item.duration / 2) if (playTime >= item.duration * 0.6) { startTime = 0; followData.index = followData.index + 1; setColor(item, "", true); setTimeout(() => { next(); checking = false; }, 3000); return; } } // console.log("频率对了", item.min, audioFrequency.value, item.max, item.duration); } // console.log("频率不对", item.min, audioFrequency.value, item.max, item.baseFrequency); setColor(item, audioFrequency.value > item.baseFrequency ? "follow-up" : "follow-down"); checking = false; }; const setColor = (item: any, state: "follow-up" | "follow-down" | "", isRight = false) => { const note: HTMLElement = document.querySelector(`div[data-vf=vf${item.id}]`)!; if (note) { note.classList.remove("follow-up", "follow-down", "follow-error", "follow-success"); if (isRight) { note.classList.add("follow-success"); } else { note.classList.add("follow-error", state); } } const _note: HTMLElement = document.getElementById(`vf-${item.id}`)!; if (_note) { const stemEl = document.getElementById(`vf-${item.id}-stem`) _note.classList.remove("follow-up", "follow-down"); stemEl?.classList.remove("follow-up", "follow-down","follow-success"); if (state) { _note.classList.add(state); stemEl?.classList.add(state) } if (isRight) { _note.classList.add("follow-success"); stemEl?.classList.add("follow-success") } } }; // 进度跟练,点击某个音符开始跟练 export const skipNotePractice = () => { followData.index = state.activeNoteIndex // 清除其它音符的错误提示 const noteFollows: HTMLElement[] = Array.from(document.querySelectorAll(".follow-error")); noteFollows.forEach((noteFollow) => { noteFollow?.classList.remove("follow-up", "follow-down", "follow-error"); }) clearRightNoteColor(); } // 移动到对应音符的位置 watch( () => followData.index, () => { skipNotePlay(followData.index); } ); export default defineComponent({ name: "follow", setup() { onMounted(async () => { /** 如果是PC端 */ if (!storeData.isApp) { const canRecorder = await audioRecorder.checkSupport(); if (canRecorder) { audioRecorder.init(); audioRecorder.progress = (frequency: number) => { onFollowTime({ api: "", content: { frequency } }); }; } else { followData.earphone = true; } } else { api_cloudFollowTime(onFollowTime); } console.log("进入跟练模式"); }); onUnmounted(() => { // api_cloudFollowTime(onFollowTime, false); resetPlaybackToStart(); onClear(); // 开启了麦克风授权,才需要调用结束收音 if (storeData.isApp && !followData.dontAccredit) { openToggleRecord(false); } console.log("退出跟练模式"); }); return () =>
; }, });