123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349 |
- 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 () => <div></div>;
- },
- });
|