mirror of
https://github.com/imsyy/SPlayer.git
synced 2025-11-25 03:14:57 +08:00
🦄 refactor: 重构为毫秒单位
This commit is contained in:
@@ -36,8 +36,8 @@ const props = defineProps<{
|
||||
// 是否显示
|
||||
const isShow = computed(() => {
|
||||
if (!settingStore.countDownShow) return false;
|
||||
// 计算实时时间 - 0.5是否小于开始 + 持续时间,小于则显示,否则不显示
|
||||
return props.seek + 0.5 < props.start + props.duration;
|
||||
// 计算实时时间 - 0.5s 是否小于开始 + 持续时间,小于则显示,否则不显示
|
||||
return props.seek + 500 < props.start + props.duration;
|
||||
});
|
||||
|
||||
// 计算每个点的透明度
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<n-flex class="menu" justify="center" vertical>
|
||||
<div class="menu-icon" @click="changeOffset(-0.5)">
|
||||
<div class="menu-icon" @click="changeOffset(-500)">
|
||||
<SvgIcon name="Replay5" />
|
||||
</div>
|
||||
<span class="time" @click="resetOffset()">
|
||||
{{ currentTimeOffsetValue }}
|
||||
</span>
|
||||
<div class="menu-icon" @click="changeOffset(0.5)">
|
||||
<div class="menu-icon" @click="changeOffset(500)">
|
||||
<SvgIcon name="Forward5" />
|
||||
</div>
|
||||
<div class="divider" />
|
||||
@@ -29,16 +29,18 @@ const statusStore = useStatusStore();
|
||||
const currentSongId = computed(() => musicStore.playSong?.id as number | undefined);
|
||||
|
||||
/**
|
||||
* 当前进度偏移值
|
||||
* 当前进度偏移值(显示为秒,保留1位小数)
|
||||
*/
|
||||
const currentTimeOffsetValue = computed(() => {
|
||||
const currentTimeOffset = statusStore.getSongOffset(currentSongId.value);
|
||||
return currentTimeOffset > 0 ? `+${currentTimeOffset}` : currentTimeOffset;
|
||||
// 将毫秒转换为秒显示(保留1位小数)
|
||||
const offsetSeconds = (currentTimeOffset / 1000).toFixed(1);
|
||||
return currentTimeOffset > 0 ? `+${offsetSeconds}` : offsetSeconds;
|
||||
});
|
||||
|
||||
/**
|
||||
* 改变进度偏移
|
||||
* @param delta 偏移量
|
||||
* @param delta 偏移量(单位:毫秒)
|
||||
*/
|
||||
const changeOffset = (delta: number) => {
|
||||
statusStore.incSongOffset(currentSongId.value, delta);
|
||||
|
||||
@@ -36,8 +36,7 @@
|
||||
import { LyricPlayer } from "@applemusic-like-lyrics/vue";
|
||||
import { LyricLine } from "@applemusic-like-lyrics/core";
|
||||
import { useMusicStore, useSettingStore, useStatusStore } from "@/stores";
|
||||
import { msToS } from "@/utils/time";
|
||||
import { getLyricLanguage } from "@/utils/lyric";
|
||||
import { getLyricLanguage } from "@/utils/format";
|
||||
import player from "@/utils/player";
|
||||
import LyricMenu from "./LyricMenu.vue";
|
||||
|
||||
@@ -48,16 +47,13 @@ const settingStore = useSettingStore();
|
||||
const lyricPlayerRef = ref<any | null>(null);
|
||||
|
||||
// 实时播放进度
|
||||
const playSeek = ref<number>(
|
||||
Math.floor((player.getSeek() + statusStore.getSongOffset(musicStore.playSong?.id)) * 1000),
|
||||
);
|
||||
const playSeek = ref<number>(player.getSeek() + statusStore.getSongOffset(musicStore.playSong?.id));
|
||||
|
||||
// 实时更新播放进度
|
||||
const { pause: pauseSeek, resume: resumeSeek } = useRafFn(() => {
|
||||
const songId = musicStore.playSong?.id;
|
||||
const offsetSeconds = statusStore.getSongOffset(songId);
|
||||
const seekInSeconds = player.getSeek() + offsetSeconds;
|
||||
playSeek.value = Math.floor(seekInSeconds * 1000);
|
||||
const offsetTime = statusStore.getSongOffset(songId);
|
||||
playSeek.value = player.getSeek() + offsetTime;
|
||||
});
|
||||
|
||||
// 歌词主色
|
||||
@@ -84,9 +80,9 @@ const amLyricsData = computed<LyricLine[]>(() => {
|
||||
// 进度跳转
|
||||
const jumpSeek = (line: any) => {
|
||||
if (!line?.line?.lyricLine?.startTime) return;
|
||||
const time = msToS(line.line.lyricLine.startTime);
|
||||
const offsetSeconds = statusStore.getSongOffset(musicStore.playSong?.id);
|
||||
player.setSeek(time - offsetSeconds);
|
||||
const time = line.line.lyricLine.startTime;
|
||||
const offsetMs = statusStore.getSongOffset(musicStore.playSong?.id);
|
||||
player.setSeek(time - offsetMs);
|
||||
player.play();
|
||||
};
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<!-- 倒计时 -->
|
||||
<CountDown
|
||||
:start="0"
|
||||
:duration="(musicStore.songLyric.yrcData[0].startTime || 0) / 1000"
|
||||
:duration="musicStore.songLyric.yrcData[0].startTime || 0"
|
||||
:seek="playSeek"
|
||||
:playing="statusStore.playStatus"
|
||||
/>
|
||||
@@ -50,7 +50,7 @@
|
||||
// on: statusStore.lyricIndex === index,
|
||||
// 当播放时间大于等于当前歌词的开始时间
|
||||
on:
|
||||
(playSeek >= item.startTime / 1000 && playSeek < item.endTime / 1000) ||
|
||||
(playSeek >= item.startTime && playSeek < item.endTime) ||
|
||||
statusStore.lyricIndex === index,
|
||||
'is-bg': item.isBG,
|
||||
'is-duet': item.isDuet,
|
||||
@@ -58,7 +58,7 @@
|
||||
]"
|
||||
:style="{
|
||||
filter: settingStore.lyricsBlur
|
||||
? (playSeek >= item.startTime / 1000 && playSeek < item.endTime / 1000) ||
|
||||
? (playSeek >= item.startTime && playSeek < item.endTime) ||
|
||||
statusStore.lyricIndex === index
|
||||
? 'blur(0)'
|
||||
: `blur(${Math.min(Math.abs(statusStore.lyricIndex - index) * 1.8, 10)}px)`
|
||||
@@ -75,8 +75,8 @@
|
||||
'content-text': true,
|
||||
'content-long':
|
||||
settingStore.showYrcLongEffect &&
|
||||
(text.endTime - item.startTime) / 1000 >= 1.5 &&
|
||||
playSeek <= text.endTime / 1000,
|
||||
text.endTime - item.startTime >= 1500 &&
|
||||
playSeek <= text.endTime,
|
||||
'end-with-space': text.word.endsWith(' ') || text.startTime === 0,
|
||||
}"
|
||||
>
|
||||
@@ -105,17 +105,14 @@
|
||||
v-if="
|
||||
settingStore.countDownShow &&
|
||||
item.startTime > 0 &&
|
||||
((musicStore.songLyric.yrcData[index + 1]?.startTime || 0) - item.endTime) /
|
||||
1000 >=
|
||||
10
|
||||
(musicStore.songLyric.yrcData[index + 1]?.startTime || 0) - item.endTime >= 10000
|
||||
"
|
||||
class="count-down-content"
|
||||
>
|
||||
<CountDown
|
||||
:start="item.endTime / 1000"
|
||||
:start="item.endTime"
|
||||
:duration="
|
||||
((musicStore.songLyric.yrcData[index + 1]?.startTime || 0) - item.endTime) /
|
||||
1000
|
||||
(musicStore.songLyric.yrcData[index + 1]?.startTime || 0) - item.endTime
|
||||
"
|
||||
:seek="playSeek"
|
||||
:playing="statusStore.playStatus"
|
||||
@@ -130,7 +127,7 @@
|
||||
<!-- 倒计时 -->
|
||||
<CountDown
|
||||
:start="0"
|
||||
:duration="(musicStore.songLyric.lrcData[0].startTime || 0) / 1000"
|
||||
:duration="musicStore.songLyric.lrcData[0].startTime || 0"
|
||||
:seek="playSeek"
|
||||
:playing="statusStore.playStatus"
|
||||
/>
|
||||
@@ -171,12 +168,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// import type { LyricContentType } from "@/types/main";
|
||||
import { LyricWord } from "@applemusic-like-lyrics/lyric";
|
||||
import { NScrollbar } from "naive-ui";
|
||||
import { useMusicStore, useSettingStore, useStatusStore } from "@/stores";
|
||||
import player from "@/utils/player";
|
||||
import { getLyricLanguage } from "@/utils/lyric";
|
||||
import { getLyricLanguage } from "@/utils/format";
|
||||
import { isElectron } from "@/utils/env";
|
||||
import LyricMenu from "./LyricMenu.vue";
|
||||
|
||||
@@ -238,14 +234,14 @@ const getYrcStyle = (wordData: LyricWord, lyricIndex: number) => {
|
||||
|
||||
// 判断当前行是否处于激活状态
|
||||
const isLineActive =
|
||||
(currentSeek >= currentLine.startTime / 1000 && currentSeek < currentLine.endTime / 1000) ||
|
||||
(currentSeek >= currentLine.startTime && currentSeek < currentLine.endTime) ||
|
||||
statusStore.lyricIndex === lyricIndex;
|
||||
|
||||
// 如果当前歌词行不是激活状态,返回固定样式,避免不必要的计算
|
||||
if (!isLineActive) {
|
||||
if (settingStore.showYrcAnimation) {
|
||||
// 判断单词是否已经唱过:已唱过保持填充状态(0%),未唱到保持未填充状态(100%)
|
||||
const hasPlayed = currentSeek >= wordData.endTime / 1000;
|
||||
const hasPlayed = currentSeek >= wordData.endTime;
|
||||
return {
|
||||
WebkitMaskPositionX: hasPlayed ? "0%" : "100%",
|
||||
};
|
||||
@@ -257,30 +253,28 @@ const getYrcStyle = (wordData: LyricWord, lyricIndex: number) => {
|
||||
// 激活状态的样式计算
|
||||
if (settingStore.showYrcAnimation) {
|
||||
// 如果播放状态不是加载中,且当前单词的时间加上持续时间减去播放进度大于 0
|
||||
if (statusStore.playLoading === false && wordData.endTime / 1000 - currentSeek > 0) {
|
||||
if (statusStore.playLoading === false && wordData.endTime - currentSeek > 0) {
|
||||
return {
|
||||
transitionDuration: `0s, 0s, 0.35s`,
|
||||
transitionDelay: `0ms`,
|
||||
WebkitMaskPositionX: `${
|
||||
100 -
|
||||
Math.max(
|
||||
((currentSeek - wordData.startTime / 1000) /
|
||||
((wordData.endTime - wordData.startTime) / 1000)) *
|
||||
100,
|
||||
((currentSeek - wordData.startTime) / (wordData.endTime - wordData.startTime)) * 100,
|
||||
0,
|
||||
)
|
||||
}%`,
|
||||
};
|
||||
}
|
||||
// 预计算时间差,避免重复计算
|
||||
const timeDiff = wordData.startTime - currentSeek * 1000;
|
||||
const timeDiff = wordData.startTime - currentSeek;
|
||||
return {
|
||||
transitionDuration: `${wordData.endTime - wordData.startTime}ms, ${(wordData.endTime - wordData.startTime) * 0.8}ms, 0.35s`,
|
||||
transitionDelay: `${timeDiff}ms, ${timeDiff + (wordData.endTime - wordData.startTime) * 0.5}ms, 0ms`,
|
||||
};
|
||||
} else {
|
||||
// 无动画模式:根据单词时间判断透明度
|
||||
return statusStore.playLoading === false && wordData.startTime / 1000 >= currentSeek
|
||||
return statusStore.playLoading === false && wordData.startTime >= currentSeek
|
||||
? { opacity: 0 }
|
||||
: { opacity: 1 };
|
||||
}
|
||||
@@ -290,8 +284,8 @@ const getYrcStyle = (wordData: LyricWord, lyricIndex: number) => {
|
||||
const jumpSeek = (time: number) => {
|
||||
if (!time) return;
|
||||
lrcMouseStatus.value = false;
|
||||
const offsetSeconds = statusStore.getSongOffset(musicStore.playSong?.id);
|
||||
player.setSeek(time / 1000 - offsetSeconds);
|
||||
const offsetMs = statusStore.getSongOffset(musicStore.playSong?.id);
|
||||
player.setSeek(time - offsetMs);
|
||||
player.play();
|
||||
};
|
||||
|
||||
|
||||
@@ -161,8 +161,8 @@
|
||||
vertical
|
||||
>
|
||||
<div class="time">
|
||||
<n-text depth="2">{{ secondsToTime(statusStore.currentTime) }}</n-text>
|
||||
<n-text depth="2">{{ secondsToTime(statusStore.duration) }}</n-text>
|
||||
<n-text depth="2">{{ msToTime(statusStore.currentTime) }}</n-text>
|
||||
<n-text depth="2">{{ msToTime(statusStore.duration) }}</n-text>
|
||||
</div>
|
||||
<!-- 定时关闭 -->
|
||||
<n-tag
|
||||
@@ -189,7 +189,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { DropdownOption } from "naive-ui";
|
||||
import { useMusicStore, useStatusStore, useDataStore, useSettingStore } from "@/stores";
|
||||
import { secondsToTime, convertSecondsToTime } from "@/utils/time";
|
||||
import { msToTime, convertSecondsToTime } from "@/utils/time";
|
||||
import { renderIcon, coverLoaded } from "@/utils/helper";
|
||||
import { toLikeSong } from "@/utils/auth";
|
||||
import {
|
||||
|
||||
@@ -76,9 +76,9 @@
|
||||
</div>
|
||||
<!-- 进度条 -->
|
||||
<div class="slider">
|
||||
<span>{{ secondsToTime(statusStore.currentTime) }}</span>
|
||||
<span>{{ msToTime(statusStore.currentTime) }}</span>
|
||||
<PlayerSlider :show-tooltip="false" />
|
||||
<span>{{ secondsToTime(statusStore.duration) }}</span>
|
||||
<span>{{ msToTime(statusStore.duration) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<n-flex class="right" align="center" justify="end">
|
||||
@@ -92,7 +92,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useMusicStore, useStatusStore, useDataStore } from "@/stores";
|
||||
import { secondsToTime } from "@/utils/time";
|
||||
import { msToTime } from "@/utils/time";
|
||||
import { openDownloadSong, openPlaylistAdd } from "@/utils/modal";
|
||||
import { toLikeSong } from "@/utils/auth";
|
||||
import player from "@/utils/player";
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useStatusStore } from "@/stores";
|
||||
import { msToTime } from "@/utils/time";
|
||||
import player from "@/utils/player";
|
||||
import { secondsToTime } from "@/utils/time";
|
||||
|
||||
withDefaults(defineProps<{ showTooltip?: boolean }>(), { showTooltip: true });
|
||||
|
||||
@@ -66,7 +66,7 @@ const endDrag = () => {
|
||||
|
||||
// 格式化提示
|
||||
const formatTooltip = (value: number) => {
|
||||
return `${secondsToTime(value)} / ${secondsToTime(statusStore.duration)}`;
|
||||
return `${msToTime(value)} / ${msToTime(statusStore.duration)}`;
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -175,16 +175,26 @@ export const useStatusStore = defineStore("status", {
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
/** 获取指定歌曲的偏移(默认 0) */
|
||||
/**
|
||||
* 获取指定歌曲的偏移
|
||||
* 单位:毫秒
|
||||
*/
|
||||
getSongOffset(songId?: number): number {
|
||||
if (!songId) return 0;
|
||||
return this.currentTimeOffsetMap?.[songId] ?? 0;
|
||||
const offsetTime = this.currentTimeOffsetMap?.[songId] ?? 0;
|
||||
return Math.floor(offsetTime * 1000);
|
||||
},
|
||||
/** 设置指定歌曲的偏移 */
|
||||
/**
|
||||
* 设置指定歌曲的偏移
|
||||
* @param songId 歌曲 id
|
||||
* @param offset 偏移量(单位:毫秒)
|
||||
*/
|
||||
setSongOffset(songId?: number, offset: number = 0) {
|
||||
if (!songId) return;
|
||||
if (!this.currentTimeOffsetMap) this.currentTimeOffsetMap = {};
|
||||
const fixed = Number(offset.toFixed(2));
|
||||
// 将毫秒转换为秒存储(保留2位小数)
|
||||
const offsetSeconds = offset / 1000;
|
||||
const fixed = Number(offsetSeconds.toFixed(2));
|
||||
if (fixed === 0) {
|
||||
// 为 0 时移除记录,避免占用空间
|
||||
delete this.currentTimeOffsetMap[songId];
|
||||
@@ -192,11 +202,15 @@ export const useStatusStore = defineStore("status", {
|
||||
this.currentTimeOffsetMap[songId] = fixed;
|
||||
}
|
||||
},
|
||||
/** 调整指定歌曲的偏移(增量) */
|
||||
incSongOffset(songId?: number, delta: number = 0.5) {
|
||||
/**
|
||||
* 调整指定歌曲的偏移(增量)
|
||||
* @param songId 歌曲 id
|
||||
* @param delta 偏移增量(单位:毫秒,默认 500ms)
|
||||
*/
|
||||
incSongOffset(songId?: number, delta: number = 500) {
|
||||
if (!songId) return;
|
||||
const current = this.getSongOffset(songId);
|
||||
const next = Number((current + delta).toFixed(2));
|
||||
const next = current + delta;
|
||||
if (next === 0) {
|
||||
delete this.currentTimeOffsetMap[songId];
|
||||
} else {
|
||||
|
||||
@@ -267,3 +267,17 @@ const getCoverSizeUrl = (url: string, size: number | null = null) => {
|
||||
return "/images/song.jpg?assest";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 检测歌词语言
|
||||
* @param lyric 歌词内容
|
||||
* @returns 语言代码("ja" | "zh-CN" | "en")
|
||||
*/
|
||||
export const getLyricLanguage = (lyric: string): string => {
|
||||
// 判断日语 根据平假名和片假名
|
||||
if (/[\u3040-\u309f\u30a0-\u30ff]/.test(lyric)) return "ja";
|
||||
// 判断简体中文 根据中日韩统一表意文字基本区
|
||||
if (/[\u4e00-\u9fa5]/.test(lyric)) return "zh-CN";
|
||||
// 默认英语
|
||||
return "en";
|
||||
};
|
||||
|
||||
@@ -1,571 +0,0 @@
|
||||
// import { LyricLine, parseLrc, parseTTML, parseYrc, TTMLLyric } from "@applemusic-like-lyrics/lyric";
|
||||
// import type { LyricType } from "@/types/main";
|
||||
import { useMusicStore, useSettingStore, useStatusStore } from "@/stores";
|
||||
// import { SettingState } from "@/stores/setting";
|
||||
import { msToS } from "./time";
|
||||
import { LyricLine } from "@applemusic-like-lyrics/lyric";
|
||||
|
||||
// /** 获取排除关键词 */
|
||||
// const getExcludeKeywords = (settings: SettingState = useSettingStore()): string[] => {
|
||||
// if (!settings.enableExcludeLyrics) return [];
|
||||
// return settings.excludeKeywords;
|
||||
// };
|
||||
|
||||
// /** 获取排除正则表达式 */
|
||||
// const getExcludeRegexes = (settings: SettingState = useSettingStore()): RegExp[] => {
|
||||
// if (!settings.enableExcludeLyrics) return [];
|
||||
// return settings.excludeRegexes.map((regex) => new RegExp(regex));
|
||||
// };
|
||||
|
||||
// /**
|
||||
// * 检测歌词是否排除
|
||||
// * @param line 歌词行
|
||||
// * @returns 是否排除
|
||||
// */
|
||||
// const isLyricExcluded = (line: string): boolean => {
|
||||
// const settingStore = useSettingStore();
|
||||
|
||||
// if (!settingStore.enableExcludeLyrics) {
|
||||
// return false;
|
||||
// }
|
||||
// const excludeKeywords = getExcludeKeywords(settingStore);
|
||||
// const excludeRegexes = getExcludeRegexes(settingStore);
|
||||
// return (
|
||||
// excludeKeywords.some((keyword) => line.includes(keyword)) ||
|
||||
// excludeRegexes.some((regex) => regex.test(line))
|
||||
// );
|
||||
// };
|
||||
|
||||
// /**
|
||||
// * 恢复默认歌词数据
|
||||
// */
|
||||
// export const resetSongLyric = () => {
|
||||
// const musicStore = useMusicStore();
|
||||
// const statusStore = useStatusStore();
|
||||
// // 重置歌词数据
|
||||
// musicStore.setSongLyric({}, true);
|
||||
// statusStore.usingTTMLLyric = false;
|
||||
// // 标记为加载中(切歌时防止显示上一首歌词)
|
||||
// statusStore.lyricLoading = true;
|
||||
// // 重置歌词索引
|
||||
// statusStore.lyricIndex = -1;
|
||||
// };
|
||||
|
||||
// /**
|
||||
// * 解析歌词数据
|
||||
// * @param lyricData 歌词数据
|
||||
// * @param skipExclude 是否跳过排除
|
||||
// * @returns 歌词数据
|
||||
// */
|
||||
// export const parsedLyricsData = (lyricData: any, skipExclude: boolean = false): void => {
|
||||
// const musicStore = useMusicStore();
|
||||
// const statusStore = useStatusStore();
|
||||
// if (lyricData.code !== 200) {
|
||||
// resetSongLyric();
|
||||
// return;
|
||||
// }
|
||||
// let lrcData: LyricType[] = [];
|
||||
// let yrcData: LyricType[] = [];
|
||||
// // 处理后歌词
|
||||
// let lrcParseData: LyricLine[] = [];
|
||||
// let tlyricParseData: LyricLine[] = [];
|
||||
// let romalrcParseData: LyricLine[] = [];
|
||||
// let yrcParseData: LyricLine[] = [];
|
||||
// let ytlrcParseData: LyricLine[] = [];
|
||||
// let yromalrcParseData: LyricLine[] = [];
|
||||
// // 普通歌词
|
||||
// if (lyricData?.lrc?.lyric) {
|
||||
// lrcParseData = parseLrc(lyricData.lrc.lyric);
|
||||
// lrcData = parseLrcData(lrcParseData, skipExclude);
|
||||
// // 其他翻译
|
||||
// if (lyricData?.tlyric?.lyric) {
|
||||
// tlyricParseData = parseLrc(lyricData.tlyric.lyric);
|
||||
// lrcData = alignLyrics(lrcData, parseLrcData(tlyricParseData), "tran");
|
||||
// }
|
||||
// if (lyricData?.romalrc?.lyric) {
|
||||
// romalrcParseData = parseLrc(lyricData.romalrc.lyric);
|
||||
// lrcData = alignLyrics(lrcData, parseLrcData(romalrcParseData), "roma");
|
||||
// }
|
||||
// }
|
||||
// // 逐字歌词
|
||||
// if (lyricData?.yrc?.lyric) {
|
||||
// yrcParseData = parseYrc(lyricData.yrc.lyric);
|
||||
// yrcData = parseYrcData(yrcParseData, skipExclude);
|
||||
// // 其他翻译
|
||||
// if (lyricData?.ytlrc?.lyric) {
|
||||
// ytlrcParseData = parseLrc(lyricData.ytlrc.lyric);
|
||||
// yrcData = alignLyrics(yrcData, parseLrcData(ytlrcParseData), "tran");
|
||||
// }
|
||||
// if (lyricData?.yromalrc?.lyric) {
|
||||
// yromalrcParseData = parseLrc(lyricData.yromalrc.lyric);
|
||||
// yrcData = alignLyrics(yrcData, parseLrcData(yromalrcParseData), "roma");
|
||||
// }
|
||||
// }
|
||||
// musicStore.setSongLyric(
|
||||
// {
|
||||
// lrcData,
|
||||
// yrcData,
|
||||
// lrcAMData: parseAMData(lrcParseData, tlyricParseData, romalrcParseData, skipExclude),
|
||||
// yrcAMData: parseAMData(yrcParseData, ytlrcParseData, yromalrcParseData, skipExclude),
|
||||
// },
|
||||
// true,
|
||||
// );
|
||||
// // 重置歌词索引
|
||||
// statusStore.lyricIndex = -1;
|
||||
// // 歌词已加载完成
|
||||
// statusStore.lyricLoading = false;
|
||||
// };
|
||||
|
||||
// /**
|
||||
// * 解析LRC歌词数据
|
||||
// * @param lrcData LRC歌词数据
|
||||
// * @param skipExclude 是否跳过排除
|
||||
// * @returns LRC歌词数据
|
||||
// */
|
||||
// export const parseLrcData = (lrcData: LyricLine[], skipExclude: boolean = false): LyricType[] => {
|
||||
// if (!lrcData) return [];
|
||||
// // 数据处理
|
||||
// const lrcList = lrcData
|
||||
// .map((line) => {
|
||||
// const words = line.words;
|
||||
// const time = msToS(words[0].startTime);
|
||||
// const content = words[0].word.trim();
|
||||
// // 排除内容
|
||||
// if (!content || (!skipExclude && isLyricExcluded(content))) {
|
||||
// return null;
|
||||
// }
|
||||
// return {
|
||||
// time,
|
||||
// content,
|
||||
// };
|
||||
// })
|
||||
// .filter((line): line is LyricType => line !== null);
|
||||
// // 筛选出非空数据并返回
|
||||
// return lrcList;
|
||||
// };
|
||||
|
||||
// /**
|
||||
// * 解析逐字歌词数据
|
||||
// * @param yrcData 逐字歌词数据
|
||||
// * @param skipExclude 是否跳过排除
|
||||
// * @returns 逐字歌词数据
|
||||
// */
|
||||
// export const parseYrcData = (yrcData: LyricLine[], skipExclude: boolean = false): LyricType[] => {
|
||||
// if (!yrcData) return [];
|
||||
// // 数据处理
|
||||
// const yrcList = yrcData
|
||||
// .map((line) => {
|
||||
// const words = line.words;
|
||||
// const time = msToS(words[0].startTime);
|
||||
// const endTime = msToS(words[words.length - 1].endTime);
|
||||
// const contents = words.map((word) => {
|
||||
// return {
|
||||
// time: msToS(word.startTime),
|
||||
// endTime: msToS(word.endTime),
|
||||
// duration: msToS(word.endTime - word.startTime),
|
||||
// content: word.word.trim(),
|
||||
// endsWithSpace: word.word.endsWith(" "),
|
||||
// };
|
||||
// });
|
||||
// // 完整歌词
|
||||
// const contentStr = contents
|
||||
// .map((word) => word.content + (word.endsWithSpace ? " " : ""))
|
||||
// .join("");
|
||||
// // 排除内容
|
||||
// if (!contentStr || (!skipExclude && isLyricExcluded(contentStr))) {
|
||||
// return null;
|
||||
// }
|
||||
// return {
|
||||
// time,
|
||||
// endTime,
|
||||
// content: contentStr,
|
||||
// contents,
|
||||
// };
|
||||
// })
|
||||
// .filter((line): line is LyricType => line !== null);
|
||||
// return yrcList;
|
||||
// };
|
||||
|
||||
// /**
|
||||
// * 歌词内容对齐
|
||||
// * @param lyrics 歌词数据
|
||||
// * @param otherLyrics 其他歌词数据
|
||||
// * @param key 对齐类型
|
||||
// * @returns 对齐后的歌词数据
|
||||
// */
|
||||
// export const alignLyrics = (
|
||||
// lyrics: LyricType[],
|
||||
// otherLyrics: LyricType[],
|
||||
// key: "tran" | "roma",
|
||||
// ): LyricType[] => {
|
||||
// const lyricsData = lyrics;
|
||||
// if (lyricsData.length && otherLyrics.length) {
|
||||
// lyricsData.forEach((v: LyricType) => {
|
||||
// otherLyrics.forEach((x: LyricType) => {
|
||||
// if (v.time === x.time || Math.abs(v.time - x.time) < 0.6) {
|
||||
// v[key] = x.content;
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
// return lyricsData;
|
||||
// };
|
||||
|
||||
// /**
|
||||
// * 对齐AM歌词
|
||||
// * @param lyrics 歌词数据
|
||||
// * @param otherLyrics 其他歌词数据
|
||||
// * @param key 对齐类型
|
||||
// * @returns 对齐后的歌词数据
|
||||
// */
|
||||
// export const alignAMLyrics = (
|
||||
// lyrics: LyricLine[],
|
||||
// otherLyrics: LyricLine[],
|
||||
// key: "translatedLyric" | "romanLyric",
|
||||
// ): LyricLine[] => {
|
||||
// const lyricsData = lyrics;
|
||||
// if (lyricsData.length && otherLyrics.length) {
|
||||
// lyricsData.forEach((v: LyricLine) => {
|
||||
// otherLyrics.forEach((x: LyricLine) => {
|
||||
// if (v.startTime === x.startTime || Math.abs(v.startTime - x.startTime) < 0.6) {
|
||||
// v[key] = x.words.map((word) => word.word).join("");
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
// return lyricsData;
|
||||
// };
|
||||
|
||||
// /**
|
||||
// * 处理本地歌词
|
||||
// * @param lyric 歌词内容
|
||||
// * @param format 歌词格式
|
||||
// */
|
||||
// export const parseLocalLyric = (lyric: string, format: "lrc" | "ttml") => {
|
||||
// const statusStore = useStatusStore();
|
||||
|
||||
// if (!lyric) {
|
||||
// resetSongLyric();
|
||||
// return;
|
||||
// }
|
||||
// switch (format) {
|
||||
// case "lrc":
|
||||
// parseLocalLyricLrc(lyric);
|
||||
// statusStore.usingTTMLLyric = false;
|
||||
// break;
|
||||
// case "ttml":
|
||||
// parseLocalLyricAM(lyric);
|
||||
// statusStore.usingTTMLLyric = true;
|
||||
// break;
|
||||
// }
|
||||
// };
|
||||
|
||||
// /**
|
||||
// * 解析本地LRC歌词
|
||||
// * @param lyric LRC格式的歌词内容
|
||||
// */
|
||||
// const parseLocalLyricLrc = (lyric: string) => {
|
||||
// const musicStore = useMusicStore();
|
||||
// const statusStore = useStatusStore();
|
||||
// const settingStore = useSettingStore();
|
||||
// // 解析
|
||||
// const lrc: LyricLine[] = parseLrc(lyric);
|
||||
// const lrcData: LyricType[] = parseLrcData(lrc, !settingStore.enableExcludeLocalLyrics);
|
||||
// // 处理结果
|
||||
// const lrcDataParsed: LyricType[] = [];
|
||||
// // 翻译提取
|
||||
// for (let i = 0; i < lrcData.length; i++) {
|
||||
// // 当前歌词
|
||||
// const lrcItem = lrcData[i];
|
||||
// // 是否具有翻译或音译
|
||||
// // 根据已解析歌词中是否有时间相同来判断,因此最先遍历的歌词行会被作为主歌词
|
||||
// const existingObj = lrcDataParsed.find((v) => v.time === lrcItem.time);
|
||||
// // 若具有翻译或音译,则判断主歌词中是否有翻译,若没有则将此句作为翻译,音译同理
|
||||
// // 如果出现时间相同的歌词行,第一行会被作为主歌词,第二行翻译,第三行音译,其余舍去
|
||||
// if (existingObj) {
|
||||
// if (!existingObj.tran) {
|
||||
// existingObj.tran = lrcItem.content;
|
||||
// } else if (!existingObj.roma) {
|
||||
// existingObj.roma = lrcItem.content;
|
||||
// }
|
||||
// } else {
|
||||
// lrcDataParsed.push(lrcItem);
|
||||
// }
|
||||
// }
|
||||
// // 更新歌词
|
||||
// musicStore.setSongLyric(
|
||||
// {
|
||||
// lrcData: lrcDataParsed,
|
||||
// lrcAMData: lrcDataParsed.map((line, index, lines) => ({
|
||||
// words: [{ startTime: line.time, endTime: 0, word: line.content }],
|
||||
// startTime: line.time * 1000,
|
||||
// endTime: lines[index + 1]?.time * 1000,
|
||||
// translatedLyric: line.tran ?? "",
|
||||
// romanLyric: line.roma ?? "",
|
||||
// isBG: false,
|
||||
// isDuet: false,
|
||||
// })),
|
||||
// yrcData: [],
|
||||
// yrcAMData: [],
|
||||
// },
|
||||
// true,
|
||||
// );
|
||||
// // 重置歌词索引
|
||||
// statusStore.lyricIndex = -1;
|
||||
// // 歌词已加载完成
|
||||
// statusStore.lyricLoading = false;
|
||||
// };
|
||||
|
||||
// /**
|
||||
// * 解析本地AM歌词
|
||||
// * @param lyric AM格式的歌词内容
|
||||
// */
|
||||
// const parseLocalLyricAM = (lyric: string) => {
|
||||
// const musicStore = useMusicStore();
|
||||
// const statusStore = useStatusStore();
|
||||
// const settingStore = useSettingStore();
|
||||
|
||||
// const skipExcludeLocal = !settingStore.enableExcludeLocalLyrics;
|
||||
// const skipExcludeTTML = !settingStore.enableTTMLLyric;
|
||||
// const skipExclude = skipExcludeLocal || skipExcludeTTML;
|
||||
|
||||
// const ttml = parseTTML(lyric);
|
||||
// const yrcAMData = parseTTMLToAMLL(ttml, skipExclude);
|
||||
// const yrcData = parseTTMLToYrc(ttml, skipExclude);
|
||||
// musicStore.setSongLyric(
|
||||
// {
|
||||
// lrcData: yrcData,
|
||||
// lrcAMData: yrcAMData,
|
||||
// yrcAMData,
|
||||
// yrcData,
|
||||
// },
|
||||
// true,
|
||||
// );
|
||||
// // 重置歌词索引
|
||||
// statusStore.lyricIndex = -1;
|
||||
// // 歌词已加载完成
|
||||
// statusStore.lyricLoading = false;
|
||||
// };
|
||||
|
||||
// /**
|
||||
// * 处理 AM 歌词
|
||||
// * @param lrcData LRC歌词数据
|
||||
// * @param tranData 翻译歌词数据
|
||||
// * @param romaData 罗马音歌词数据
|
||||
// * @param skipExclude 是否跳过排除
|
||||
// * @returns AM歌词数据
|
||||
// */
|
||||
// const parseAMData = (
|
||||
// lrcData: LyricLine[],
|
||||
// tranData?: LyricLine[],
|
||||
// romaData?: LyricLine[],
|
||||
// skipExclude: boolean = false,
|
||||
// ) => {
|
||||
// let lyricData = lrcData
|
||||
// .map((line, index, lines) => {
|
||||
// // 获取歌词文本内容
|
||||
// const content = line.words
|
||||
// .map((word) => word.word)
|
||||
// .join("")
|
||||
// .trim();
|
||||
// // 排除包含关键词的内容
|
||||
// if (!content || (!skipExclude && isLyricExcluded(content))) {
|
||||
// return null;
|
||||
// }
|
||||
// return {
|
||||
// words: line.words,
|
||||
// startTime: line.words[0]?.startTime ?? 0,
|
||||
// endTime:
|
||||
// lines[index + 1]?.words?.[0]?.startTime ??
|
||||
// line.words?.[line.words.length - 1]?.endTime ??
|
||||
// Infinity,
|
||||
// translatedLyric: "",
|
||||
// romanLyric: "",
|
||||
// isBG: line.isBG ?? false,
|
||||
// isDuet: line.isDuet ?? false,
|
||||
// };
|
||||
// })
|
||||
// .filter((line): line is NonNullable<typeof line> => line !== null);
|
||||
// if (tranData) {
|
||||
// lyricData = alignAMLyrics(lyricData, tranData, "translatedLyric");
|
||||
// }
|
||||
// if (romaData) {
|
||||
// lyricData = alignAMLyrics(lyricData, romaData, "romanLyric");
|
||||
// }
|
||||
// return lyricData;
|
||||
// };
|
||||
|
||||
// /**
|
||||
// * 从TTML格式解析歌词并转换为AMLL格式
|
||||
// * @param ttmlContent TTML格式的歌词内容
|
||||
// * @param skipExclude 是否跳过排除
|
||||
// * @returns AMLL格式的歌词行数组
|
||||
// */
|
||||
// export const parseTTMLToAMLL = (
|
||||
// ttmlContent: TTMLLyric,
|
||||
// skipExclude: boolean = false,
|
||||
// ): LyricLine[] => {
|
||||
// if (!ttmlContent) return [];
|
||||
|
||||
// try {
|
||||
// const validLines = ttmlContent.lines
|
||||
// .filter((line) => line && typeof line === "object" && Array.isArray(line.words))
|
||||
// .map((line) => {
|
||||
// const words = line.words
|
||||
// .filter((word) => word && typeof word === "object")
|
||||
// .map((word) => ({
|
||||
// word: String(word.word || " "),
|
||||
// startTime: Number(word.startTime) || 0,
|
||||
// endTime: Number(word.endTime) || 0,
|
||||
// }));
|
||||
|
||||
// if (!words.length) return null;
|
||||
|
||||
// // 获取歌词文本内容
|
||||
// const content = words
|
||||
// .map((word) => word.word)
|
||||
// .join("")
|
||||
// .trim();
|
||||
// // 排除包含关键词的内容
|
||||
// if (!content || (!skipExclude && isLyricExcluded(content))) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// const startTime = line.startTime || words[0].startTime;
|
||||
// const endTime = line.endTime || words[words.length - 1].endTime;
|
||||
|
||||
// return {
|
||||
// words,
|
||||
// startTime,
|
||||
// endTime,
|
||||
// translatedLyric: String(line.translatedLyric || ""),
|
||||
// romanLyric: String(line.romanLyric || ""),
|
||||
// isBG: Boolean(line.isBG),
|
||||
// isDuet: Boolean(line.isDuet),
|
||||
// };
|
||||
// })
|
||||
// .filter((line): line is LyricLine => line !== null);
|
||||
|
||||
// return validLines;
|
||||
// } catch (error) {
|
||||
// console.error("TTML parsing error:", error);
|
||||
// return [];
|
||||
// }
|
||||
// };
|
||||
|
||||
// /**
|
||||
// * 从TTML格式解析歌词并转换为默认Yrc格式
|
||||
// * @param ttmlContent TTML格式的歌词内容
|
||||
// * @param skipExclude 是否跳过排除
|
||||
// * @returns 默认Yrc格式的歌词行数组
|
||||
// */
|
||||
// export const parseTTMLToYrc = (
|
||||
// ttmlContent: TTMLLyric,
|
||||
// skipExclude: boolean = false,
|
||||
// ): LyricType[] => {
|
||||
// if (!ttmlContent) return [];
|
||||
|
||||
// try {
|
||||
// // 数据处理
|
||||
// const yrcList = ttmlContent.lines
|
||||
// .map((line) => {
|
||||
// const words = line.words;
|
||||
// const time = msToS(words[0].startTime);
|
||||
// const endTime = msToS(words[words.length - 1].endTime);
|
||||
// const contents = words.map((word) => {
|
||||
// return {
|
||||
// time: msToS(word.startTime),
|
||||
// endTime: msToS(word.endTime),
|
||||
// duration: msToS(word.endTime - word.startTime),
|
||||
// content: word.word.trim(),
|
||||
// endsWithSpace: word.word.endsWith(" "),
|
||||
// };
|
||||
// });
|
||||
// // 完整歌词
|
||||
// const contentStr = contents
|
||||
// .map((word) => word.content + (word.endsWithSpace ? " " : ""))
|
||||
// .join("");
|
||||
// // 排除内容
|
||||
// if (!contentStr || (!skipExclude && isLyricExcluded(contentStr))) {
|
||||
// return null;
|
||||
// }
|
||||
// return {
|
||||
// time,
|
||||
// endTime,
|
||||
// content: contentStr,
|
||||
// contents,
|
||||
// tran: line.translatedLyric || "",
|
||||
// roma: line.romanLyric || "",
|
||||
// isBG: line.isBG,
|
||||
// isDuet: line.isDuet,
|
||||
// };
|
||||
// })
|
||||
// .filter((line) => line !== null);
|
||||
// return yrcList;
|
||||
// } catch (error) {
|
||||
// console.error("TTML parsing to yrc error:", error);
|
||||
// return [];
|
||||
// }
|
||||
// };
|
||||
|
||||
// 检测语言
|
||||
export const getLyricLanguage = (lyric: string): string => {
|
||||
// 判断日语 根据平假名和片假名
|
||||
if (/[\u3040-\u309f\u30a0-\u30ff]/.test(lyric)) return "ja";
|
||||
// 判断简体中文 根据中日韩统一表意文字基本区
|
||||
if (/[\u4e00-\u9fa5]/.test(lyric)) return "zh-CN";
|
||||
// 默认英语
|
||||
return "en";
|
||||
};
|
||||
|
||||
/**
|
||||
* 计算歌词索引
|
||||
* - 普通歌词(LRC):沿用当前按开始时间定位的算法
|
||||
* - 逐字歌词(YRC):当播放时间位于某句 [time, endTime) 区间内时,索引为该句;
|
||||
* 若下一句开始时间落在上一句区间(对唱重叠),仍保持上一句索引,直到上一句结束。
|
||||
*/
|
||||
export const calculateLyricIndex = (currentTime: number): number => {
|
||||
const musicStore = useMusicStore();
|
||||
const statusStore = useStatusStore();
|
||||
const settingStore = useSettingStore();
|
||||
// 应用实时偏移(按歌曲 id 记忆) + 0.3s(解决对唱时歌词延迟问题)
|
||||
const songId = musicStore.playSong?.id;
|
||||
const offset = statusStore.getSongOffset(songId);
|
||||
const playSeek = currentTime + offset + 0.3;
|
||||
// 选择歌词类型
|
||||
const useYrc = !!(settingStore.showYrc && musicStore.songLyric.yrcData.length);
|
||||
const lyrics = useYrc ? musicStore.songLyric.yrcData : musicStore.songLyric.lrcData;
|
||||
// 无歌词时
|
||||
if (!lyrics || !lyrics.length) return -1;
|
||||
|
||||
const getStart = (v: LyricLine) => msToS(v.startTime || 0);
|
||||
const getEnd = (v: LyricLine) => msToS(v.endTime ?? Infinity);
|
||||
// 普通歌词:保持原有计算方式
|
||||
if (!useYrc) {
|
||||
const idx = lyrics.findIndex((v) => getStart(v) >= playSeek);
|
||||
return idx === -1 ? lyrics.length - 1 : idx - 1;
|
||||
}
|
||||
// TTML / YRC(支持对唱重叠)
|
||||
// 在第一句之前
|
||||
if (playSeek < getStart(lyrics[0])) return -1;
|
||||
// 计算在播放进度下处于激活区间的句子集合 activeIndices([time, endTime))
|
||||
const activeIndices: number[] = [];
|
||||
for (let i = 0; i < lyrics.length; i++) {
|
||||
const start = getStart(lyrics[i]);
|
||||
const end = getEnd(lyrics[i]);
|
||||
if (playSeek >= start && playSeek < end) {
|
||||
activeIndices.push(i);
|
||||
}
|
||||
}
|
||||
// 不在任何区间 → 找最近的上一句
|
||||
if (activeIndices.length === 0) {
|
||||
const next = lyrics.findIndex((v) => getStart(v) > playSeek);
|
||||
return next === -1 ? lyrics.length - 1 : next - 1;
|
||||
}
|
||||
// 1 句激活 → 直接返回
|
||||
if (activeIndices.length === 1) return activeIndices[0];
|
||||
// 多句激活(对唱)
|
||||
const keepCount = activeIndices.length >= 3 ? 3 : 2;
|
||||
const concurrent = activeIndices.slice(-keepCount);
|
||||
return concurrent[0]; // 保持上一句(重叠时不跳)
|
||||
};
|
||||
@@ -111,7 +111,7 @@ class LyricManager {
|
||||
// 请求序列
|
||||
const req = this.activeLyricReq;
|
||||
// 最终结果
|
||||
let result: SongLyric = { lrcData: [], yrcData: [] };
|
||||
const result: SongLyric = { lrcData: [], yrcData: [] };
|
||||
// 过期判断
|
||||
const isStale = () => this.activeLyricReq !== req || musicStore.playSong?.id !== id;
|
||||
// 处理 TTML 歌词
|
||||
@@ -343,6 +343,57 @@ class LyricManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 计算歌词索引
|
||||
* - 普通歌词(LRC):沿用当前按开始时间定位的算法
|
||||
* - 逐字歌词(YRC):当播放时间位于某句 [time, endTime) 区间内时,索引为该句;
|
||||
* 若下一句开始时间落在上一句区间(对唱重叠),仍保持上一句索引,直到上一句结束。
|
||||
*/
|
||||
public calculateLyricIndex(currentTime: number): number {
|
||||
const musicStore = useMusicStore();
|
||||
const statusStore = useStatusStore();
|
||||
const settingStore = useSettingStore();
|
||||
// 应用实时偏移(按歌曲 id 记忆) + 0.3s(解决对唱时歌词延迟问题)
|
||||
const songId = musicStore.playSong?.id;
|
||||
const offset = statusStore.getSongOffset(songId);
|
||||
const playSeek = currentTime + offset + 300;
|
||||
// 选择歌词类型
|
||||
const useYrc = !!(settingStore.showYrc && musicStore.songLyric.yrcData.length);
|
||||
const lyrics = useYrc ? musicStore.songLyric.yrcData : musicStore.songLyric.lrcData;
|
||||
// 无歌词时
|
||||
if (!lyrics || !lyrics.length) return -1;
|
||||
// 获取开始时间和结束时间
|
||||
const getStart = (v: LyricLine) => v.startTime || 0;
|
||||
const getEnd = (v: LyricLine) => v.endTime ?? Infinity;
|
||||
// 普通歌词:保持原有计算方式
|
||||
if (!useYrc) {
|
||||
const idx = lyrics.findIndex((v) => getStart(v) >= playSeek);
|
||||
return idx === -1 ? lyrics.length - 1 : idx - 1;
|
||||
}
|
||||
// TTML / YRC(支持对唱重叠)
|
||||
// 在第一句之前
|
||||
if (playSeek < getStart(lyrics[0])) return -1;
|
||||
// 计算在播放进度下处于激活区间的句子集合 activeIndices([time, endTime))
|
||||
const activeIndices: number[] = [];
|
||||
for (let i = 0; i < lyrics.length; i++) {
|
||||
const start = getStart(lyrics[i]);
|
||||
const end = getEnd(lyrics[i]);
|
||||
if (playSeek >= start && playSeek < end) {
|
||||
activeIndices.push(i);
|
||||
}
|
||||
}
|
||||
// 不在任何区间 → 找最近的上一句
|
||||
if (activeIndices.length === 0) {
|
||||
const next = lyrics.findIndex((v) => getStart(v) > playSeek);
|
||||
return next === -1 ? lyrics.length - 1 : next - 1;
|
||||
}
|
||||
// 1 句激活 → 直接返回
|
||||
if (activeIndices.length === 1) return activeIndices[0];
|
||||
// 多句激活(对唱)
|
||||
const keepCount = activeIndices.length >= 3 ? 3 : 2;
|
||||
const concurrent = activeIndices.slice(-keepCount);
|
||||
return concurrent[0]; // 保持上一句(重叠时不跳)
|
||||
}
|
||||
}
|
||||
|
||||
export default new LyricManager();
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
// import { useMusicStore, useSettingStore, useStatusStore } from "@/stores";
|
||||
// import {
|
||||
// parsedLyricsData,
|
||||
// parseLocalLyric,
|
||||
// parseTTMLToAMLL,
|
||||
// parseTTMLToYrc,
|
||||
// resetSongLyric,
|
||||
// } from "../lyric";
|
||||
// import { songLyric, songLyricTTML } from "@/api/song";
|
||||
// import { parseTTML } from "@applemusic-like-lyrics/lyric";
|
||||
// import { LyricLine } from "@applemusic-like-lyrics/core";
|
||||
// import { LyricType } from "@/types/main";
|
||||
|
||||
// /**
|
||||
// * 获取歌词
|
||||
// * @param id 歌曲id
|
||||
// */
|
||||
// export const getLyricData = async (id: number) => {
|
||||
// const musicStore = useMusicStore();
|
||||
// const settingStore = useSettingStore();
|
||||
// const statusStore = useStatusStore();
|
||||
// // 切歌或重新获取时,先标记为加载中
|
||||
// statusStore.lyricLoading = true;
|
||||
|
||||
// if (!id) {
|
||||
// statusStore.usingTTMLLyric = false;
|
||||
// resetSongLyric();
|
||||
// statusStore.lyricLoading = false;
|
||||
// return;
|
||||
// }
|
||||
|
||||
// try {
|
||||
// // 检测本地歌词覆盖
|
||||
// const getLyric = getLyricFun(settingStore.localLyricPath, id);
|
||||
|
||||
// // 并发请求:如果 TTML 先到并且有效,则直接采用 TTML,不再等待或覆盖为 LRC
|
||||
// const lrcPromise = getLyric("lrc", songLyric);
|
||||
// // 这里的第二个 getLyric 方法不传入第二个参数(在线获取函数)表明不进行在线获取,仅获取本地
|
||||
// const ttmlPromise = settingStore.enableTTMLLyric
|
||||
// ? getLyric("ttml", songLyricTTML)
|
||||
// : getLyric("ttml");
|
||||
|
||||
// let settled = false; // 是否已采用某一种歌词并结束加载状态
|
||||
// let ttmlAdopted = false; // 是否已采用 TTML
|
||||
|
||||
// const adoptTTML = async () => {
|
||||
// if (!ttmlPromise) {
|
||||
// statusStore.usingTTMLLyric = false;
|
||||
// return;
|
||||
// }
|
||||
// try {
|
||||
// const { lyric: ttmlContent, isLocal: ttmlLocal } = await ttmlPromise;
|
||||
// if (!ttmlContent) {
|
||||
// statusStore.usingTTMLLyric = false;
|
||||
// return;
|
||||
// }
|
||||
// // 本地 TTML 使用 parseLocalLyric,在线 TTML 使用原有解析方式
|
||||
// if (ttmlLocal) {
|
||||
// parseLocalLyric(ttmlContent, "ttml");
|
||||
// statusStore.usingTTMLLyric = true;
|
||||
// ttmlAdopted = true;
|
||||
// if (!settled) {
|
||||
// statusStore.lyricLoading = false;
|
||||
// settled = true;
|
||||
// }
|
||||
// console.log("✅ TTML lyrics adopted (prefer TTML)");
|
||||
// return;
|
||||
// }
|
||||
// // 在线 TTML 解析
|
||||
// const parsedResult = parseTTML(ttmlContent);
|
||||
// if (!parsedResult?.lines?.length) {
|
||||
// statusStore.usingTTMLLyric = false;
|
||||
// return;
|
||||
// }
|
||||
// const skipExcludeLocal = ttmlLocal && !settingStore.enableExcludeLocalLyrics;
|
||||
// const skipExcludeTTML = !settingStore.enableExcludeTTML;
|
||||
// const skipExclude = skipExcludeLocal || skipExcludeTTML;
|
||||
// const ttmlLyric = parseTTMLToAMLL(parsedResult, skipExclude);
|
||||
// const ttmlYrcLyric = parseTTMLToYrc(parsedResult, skipExclude);
|
||||
|
||||
// const updates: Partial<{
|
||||
// yrcAMData: LyricLine[];
|
||||
// yrcData: LyricType[];
|
||||
// lrcData: LyricType[];
|
||||
// lrcAMData: LyricLine[];
|
||||
// }> = {};
|
||||
// if (ttmlLyric?.length) {
|
||||
// updates.yrcAMData = ttmlLyric;
|
||||
// // 若当前无 LRC-AM 数据,使用 TTML-AM 作为回退
|
||||
// if (!musicStore.songLyric.lrcAMData?.length) {
|
||||
// updates.lrcAMData = ttmlLyric;
|
||||
// }
|
||||
// }
|
||||
// if (ttmlYrcLyric?.length) {
|
||||
// updates.yrcData = ttmlYrcLyric;
|
||||
// // 若当前无 LRC 数据,使用 TTML 行级数据作为回退
|
||||
// if (!musicStore.songLyric.lrcData?.length) {
|
||||
// updates.lrcData = ttmlYrcLyric;
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (Object.keys(updates).length) {
|
||||
// musicStore.setSongLyric(updates);
|
||||
// statusStore.usingTTMLLyric = true;
|
||||
// ttmlAdopted = true;
|
||||
// if (!settled) {
|
||||
// statusStore.lyricLoading = false;
|
||||
// settled = true;
|
||||
// }
|
||||
// console.log("✅ TTML lyrics adopted (prefer TTML)");
|
||||
// } else {
|
||||
// statusStore.usingTTMLLyric = false;
|
||||
// }
|
||||
// } catch (err) {
|
||||
// console.error("❌ Error loading TTML lyrics:", err);
|
||||
// statusStore.usingTTMLLyric = false;
|
||||
// }
|
||||
// };
|
||||
|
||||
// const adoptLRC = async () => {
|
||||
// try {
|
||||
// const { lyric: lyricRes, isLocal: lyricLocal } = await lrcPromise;
|
||||
// // 如果 TTML 已采用,则忽略 LRC
|
||||
// if (ttmlAdopted) return;
|
||||
// // 如果没有歌词内容,直接返回
|
||||
// if (!lyricRes) return;
|
||||
// // 本地歌词使用 parseLocalLyric,在线歌词使用 parsedLyricsData
|
||||
// if (lyricLocal) {
|
||||
// parseLocalLyric(lyricRes, "lrc");
|
||||
// } else {
|
||||
// parsedLyricsData(lyricRes, !settingStore.enableExcludeLocalLyrics);
|
||||
// }
|
||||
// statusStore.usingTTMLLyric = false;
|
||||
// if (!settled) {
|
||||
// statusStore.lyricLoading = false;
|
||||
// settled = true;
|
||||
// }
|
||||
// console.log("✅ LRC lyrics adopted");
|
||||
// } catch (err) {
|
||||
// console.error("❌ Error loading LRC lyrics:", err);
|
||||
// if (!settled) statusStore.lyricLoading = false;
|
||||
// }
|
||||
// };
|
||||
|
||||
// // 启动并发任务:TTML 与 LRC 同时进行,哪个先成功就先用
|
||||
// void adoptLRC();
|
||||
// void adoptTTML();
|
||||
// } catch (error) {
|
||||
// console.error("❌ Error loading lyrics:", error);
|
||||
// statusStore.usingTTMLLyric = false;
|
||||
// resetSongLyric();
|
||||
// statusStore.lyricLoading = false;
|
||||
// }
|
||||
// };
|
||||
|
||||
// /**
|
||||
// * 获取歌词函数生成器
|
||||
// * @param paths 本地歌词路径数组
|
||||
// * @param id 歌曲ID
|
||||
// * @returns 返回一个函数,该函数接受扩展名和在线获取函数作为参数
|
||||
// */
|
||||
// const getLyricFun =
|
||||
// (paths: string[], id: number) =>
|
||||
// async (
|
||||
// ext: string,
|
||||
// getOnline?: (id: number) => Promise<string | null>,
|
||||
// ): Promise<{ lyric: string | null; isLocal: boolean }> => {
|
||||
// for (const path of paths) {
|
||||
// const lyric = await window.electron.ipcRenderer.invoke("read-local-lyric", path, id, ext);
|
||||
// if (lyric) return { lyric, isLocal: true };
|
||||
// }
|
||||
// return { lyric: getOnline ? await getOnline(id) : null, isLocal: false };
|
||||
// };
|
||||
@@ -1,285 +0,0 @@
|
||||
/**
|
||||
* 基于 HTMLAudioElement + Web Audio 的播放器引擎
|
||||
*/
|
||||
export class PlayerNative {
|
||||
private audio: HTMLAudioElement;
|
||||
private ctx: AudioContext;
|
||||
private sourceNode: MediaElementAudioSourceNode;
|
||||
private gainNode: GainNode;
|
||||
private analyser?: AnalyserNode;
|
||||
private events: Map<string, Set<(...args: any[]) => void>> = new Map();
|
||||
|
||||
/**
|
||||
* 构造播放器引擎
|
||||
* @param ctx 可选的外部 `AudioContext`,不传则内部创建
|
||||
*/
|
||||
constructor(ctx?: AudioContext) {
|
||||
this.audio = new Audio();
|
||||
this.audio.preload = "auto";
|
||||
this.audio.crossOrigin = "anonymous";
|
||||
|
||||
this.ctx = ctx ?? new AudioContext();
|
||||
this.sourceNode = this.ctx.createMediaElementSource(this.audio);
|
||||
this.gainNode = this.ctx.createGain();
|
||||
this.sourceNode.connect(this.gainNode).connect(this.ctx.destination);
|
||||
|
||||
this.bindDomEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载指定音频地址,切换歌曲时只需调用此方法即可
|
||||
* @param src 音频 URL(需允许跨域以启用频谱/均衡等处理)
|
||||
*/
|
||||
load(src: string): void {
|
||||
this.audio.src = src;
|
||||
// 重置并触发新加载
|
||||
this.audio.load();
|
||||
this.emit("loadstart");
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始播放如果 `AudioContext` 处于挂起状态,将自动恢复
|
||||
* @returns 播放 Promise,用于捕获自动播放限制等异常
|
||||
*/
|
||||
async play(): Promise<void> {
|
||||
if (this.ctx.state === "suspended") {
|
||||
try {
|
||||
await this.ctx.resume();
|
||||
} catch (e) {
|
||||
this.emit("error", e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
await this.audio.play();
|
||||
this.emit("play");
|
||||
} catch (e) {
|
||||
this.emit("error", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停播放
|
||||
*/
|
||||
pause(): void {
|
||||
this.audio.pause();
|
||||
this.emit("pause");
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止播放并将进度归零
|
||||
*/
|
||||
stop(): void {
|
||||
this.audio.pause();
|
||||
this.audio.currentTime = 0;
|
||||
this.emit("stop");
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到指定秒数
|
||||
* @param seconds 目标时间(秒)
|
||||
*/
|
||||
seek(seconds: number): void {
|
||||
try {
|
||||
this.audio.currentTime = Math.max(0, seconds);
|
||||
this.emit("seek", seconds);
|
||||
} catch (e) {
|
||||
this.emit("error", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置音量(0.0 ~ 1.0)
|
||||
* @param volume 音量值
|
||||
*/
|
||||
setVolume(volume: number): void {
|
||||
const v = Math.min(1, Math.max(0, volume));
|
||||
this.gainNode.gain.setValueAtTime(v, this.ctx.currentTime);
|
||||
this.emit("volume", v);
|
||||
}
|
||||
|
||||
/**
|
||||
* 渐变到目标音量
|
||||
* @param target 目标音量(0.0 ~ 1.0)
|
||||
* @param durationMs 渐变时长(毫秒)
|
||||
*/
|
||||
fadeTo(target: number, durationMs: number): void {
|
||||
const now = this.ctx.currentTime;
|
||||
const t = Math.min(1, Math.max(0, target));
|
||||
this.gainNode.gain.cancelScheduledValues(now);
|
||||
this.gainNode.gain.setValueAtTime(this.gainNode.gain.value, now);
|
||||
this.gainNode.gain.linearRampToValueAtTime(t, now + durationMs / 1000);
|
||||
this.emit("fade", t, durationMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否循环播放
|
||||
* @param loop 是否循环
|
||||
*/
|
||||
setLoop(loop: boolean): void {
|
||||
this.audio.loop = !!loop;
|
||||
this.emit("loop", !!loop);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置播放速率
|
||||
* @param rate 倍速(例如 1.0 正常速)
|
||||
*/
|
||||
setRate(rate: number): void {
|
||||
const r = Math.max(0.25, Math.min(4, rate));
|
||||
this.audio.playbackRate = r;
|
||||
this.emit("rate", r);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将内部音频链路连接到外部 `AnalyserNode`,用于频谱/可视化
|
||||
* @param analyser 频谱分析节点
|
||||
*/
|
||||
connectAnalyser(analyser: AnalyserNode): void {
|
||||
try {
|
||||
// 重新布线:gain -> analyser -> destination
|
||||
this.gainNode.disconnect();
|
||||
this.gainNode.connect(analyser).connect(this.ctx.destination);
|
||||
this.analyser = analyser;
|
||||
this.emit("analyser");
|
||||
} catch (e) {
|
||||
this.emit("error", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取音频总时长(秒)
|
||||
*/
|
||||
getDuration(): number {
|
||||
return Number.isFinite(this.audio.duration) ? this.audio.duration : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前播放进度(秒)
|
||||
*/
|
||||
getCurrentTime(): number {
|
||||
return this.audio.currentTime || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓冲范围集合(TimeRanges)
|
||||
*/
|
||||
getBuffered(): TimeRanges {
|
||||
return this.audio.buffered;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定原生 `HTMLAudioElement` 的事件到引擎事件系统
|
||||
*/
|
||||
private bindDomEvents(): void {
|
||||
this.audio.addEventListener("loadstart", () => this.emit("loadstart"));
|
||||
this.audio.addEventListener("canplay", () => this.emit("canplay"));
|
||||
this.audio.addEventListener("canplaythrough", () => this.emit("loaded"));
|
||||
this.audio.addEventListener("playing", () => this.emit("playing"));
|
||||
this.audio.addEventListener("pause", () => this.emit("pause"));
|
||||
this.audio.addEventListener("ended", () => this.emit("end"));
|
||||
this.audio.addEventListener("waiting", () => this.emit("waiting"));
|
||||
this.audio.addEventListener("stalled", () => this.emit("stalled"));
|
||||
this.audio.addEventListener("error", (e) => this.emit("error", e));
|
||||
this.audio.addEventListener("timeupdate", () => this.emit("time", this.audio.currentTime));
|
||||
this.audio.addEventListener("progress", () => this.emit("progress", this.audio.buffered));
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅事件
|
||||
* @param event 事件名
|
||||
* @param handler 事件处理函数
|
||||
*/
|
||||
on(event: string, handler: (...args: any[]) => void): void {
|
||||
if (!this.events.has(event)) this.events.set(event, new Set());
|
||||
this.events.get(event)!.add(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消订阅事件;不传参数则清空所有事件
|
||||
* @param event 事件名(可选)
|
||||
* @param handler 处理函数(可选)
|
||||
*/
|
||||
off(event?: string, handler?: (...args: any[]) => void): void {
|
||||
if (!event) {
|
||||
this.events.clear();
|
||||
return;
|
||||
}
|
||||
if (!handler) {
|
||||
this.events.get(event)?.clear();
|
||||
return;
|
||||
}
|
||||
this.events.get(event)?.delete(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源与断开音频链路;不会销毁全局 `AudioContext`
|
||||
*/
|
||||
destroy(): void {
|
||||
try {
|
||||
this.off();
|
||||
this.audio.pause();
|
||||
this.audio.src = "";
|
||||
this.audio.removeAttribute("src");
|
||||
// 一般用于释放媒体资源的模式
|
||||
this.audio.load();
|
||||
} catch {}
|
||||
try {
|
||||
this.sourceNode.disconnect();
|
||||
} catch {}
|
||||
try {
|
||||
this.gainNode.disconnect();
|
||||
} catch {}
|
||||
try {
|
||||
this.analyser?.disconnect();
|
||||
} catch {}
|
||||
this.emit("destroy");
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发事件
|
||||
* @param event 事件名
|
||||
* @param args 事件参数
|
||||
*/
|
||||
private emit(event: string, ...args: any[]): void {
|
||||
const set = this.events.get(event);
|
||||
if (!set || set.size === 0) return;
|
||||
set.forEach((fn) => {
|
||||
try {
|
||||
fn(...args);
|
||||
} catch {}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个 PlayerNative 实例的便捷工厂
|
||||
* @param ctx 可选的外部 `AudioContext`
|
||||
*/
|
||||
export function createPlayerNative(ctx?: AudioContext): PlayerNative {
|
||||
return new PlayerNative(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* 引擎事件类型参考(自由扩展)
|
||||
*/
|
||||
export type PlayerNativeEvent =
|
||||
| "loadstart"
|
||||
| "canplay"
|
||||
| "loaded"
|
||||
| "playing"
|
||||
| "pause"
|
||||
| "stop"
|
||||
| "seek"
|
||||
| "volume"
|
||||
| "fade"
|
||||
| "loop"
|
||||
| "rate"
|
||||
| "analyser"
|
||||
| "time"
|
||||
| "progress"
|
||||
| "waiting"
|
||||
| "stalled"
|
||||
| "end"
|
||||
| "error"
|
||||
| "destroy";
|
||||
@@ -3,8 +3,6 @@ import type { MessageReactive } from "naive-ui";
|
||||
import { Howl, Howler } from "howler";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import { useMusicStore, useStatusStore, useDataStore, useSettingStore } from "@/stores";
|
||||
// import { resetSongLyric, parseLocalLyric, calculateLyricIndex } from "./lyric";
|
||||
import { calculateLyricIndex } from "./lyric";
|
||||
import { calculateProgress } from "./time";
|
||||
import { shuffleArray, runIdle } from "./helper";
|
||||
import { heartRateList } from "@/api/playlist";
|
||||
@@ -108,11 +106,11 @@ class Player {
|
||||
}
|
||||
if (!this.player.playing()) return;
|
||||
const currentTime = this.getSeek();
|
||||
const duration = this.player.duration();
|
||||
const duration = Math.floor(this.player.duration() * 1000);
|
||||
// 计算进度条距离
|
||||
const progress = calculateProgress(currentTime, duration);
|
||||
// 计算歌词索引(支持 LRC 与逐字 YRC,对唱重叠处理)
|
||||
const lyricIndex = calculateLyricIndex(currentTime);
|
||||
const lyricIndex = lyricManager.calculateLyricIndex(currentTime);
|
||||
// 更新状态
|
||||
statusStore.$patch({ currentTime, duration, progress, lyricIndex });
|
||||
// 客户端事件
|
||||
@@ -300,9 +298,9 @@ class Player {
|
||||
}
|
||||
// 恢复进度(仅在明确指定且大于0时才恢复,避免切换歌曲时意外恢复进度)
|
||||
if (seek && seek > 0) {
|
||||
const duration = this.player.duration();
|
||||
const duration = Math.floor(this.player.duration() * 1000);
|
||||
// 确保恢复的进度有效且距离歌曲结束大于2秒
|
||||
if (duration && seek < duration - 2) {
|
||||
if (duration && seek < duration - 2000) {
|
||||
this.setSeek(seek);
|
||||
}
|
||||
}
|
||||
@@ -907,7 +905,7 @@ class Player {
|
||||
}
|
||||
/**
|
||||
* 设置播放进度
|
||||
* @param time 播放进度
|
||||
* @param time 播放进度(单位:毫秒)
|
||||
*/
|
||||
setSeek(time: number) {
|
||||
const statusStore = useStatusStore();
|
||||
@@ -916,17 +914,17 @@ class Player {
|
||||
console.warn("⚠️ Player not ready for seek");
|
||||
return;
|
||||
}
|
||||
this.player.seek(time);
|
||||
this.player.seek(time / 1000);
|
||||
statusStore.currentTime = time;
|
||||
}
|
||||
/**
|
||||
* 获取播放进度
|
||||
* @returns 播放进度(单位:秒)
|
||||
* @returns 播放进度(单位:毫秒)
|
||||
*/
|
||||
getSeek(): number {
|
||||
// 检查播放器状态
|
||||
if (!this.player || this.player.state() !== "loaded") return 0;
|
||||
return this.player.seek();
|
||||
return Math.floor(this.player.seek() * 1000);
|
||||
}
|
||||
/**
|
||||
* 设置播放速率
|
||||
|
||||
@@ -75,25 +75,10 @@ export const formatCommentTime = (timestamp: number): string => {
|
||||
*/
|
||||
export const calculateProgress = (currentTime: number, duration: number): number => {
|
||||
if (duration === 0) return 0;
|
||||
|
||||
const progress = (currentTime / duration) * 100;
|
||||
return Math.min(Math.round(progress * 100) / 100, 100);
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据进度和总时长反推当前时间
|
||||
* @param {number} progress 进度百分比,范围通常是0到100
|
||||
* @param {number} duration 总时长,单位为秒
|
||||
* @returns {number} 当前时间,单位为秒,精确到0.01秒
|
||||
*/
|
||||
export const calculateCurrentTime = (progress: number, duration: number): number => {
|
||||
// 确保在有效范围内
|
||||
progress = Math.min(Math.max(progress, 0), 100);
|
||||
|
||||
const currentTime = (progress / 100) * duration;
|
||||
return Math.round(currentTime * 100) / 100;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前时间段的问候语
|
||||
*/
|
||||
|
||||
@@ -335,21 +335,18 @@ const renderLyricLines = computed<RenderLine[]>(() => {
|
||||
const getYrcStyle = (wordData: LyricWord, lyricIndex: number) => {
|
||||
const currentLine = lyricData.yrcData?.[lyricIndex];
|
||||
if (!currentLine) return { WebkitMaskPositionX: "100%" };
|
||||
const seekSec = playSeekMs.value / 1000;
|
||||
const startSec = (currentLine.startTime || 0) / 1000;
|
||||
const endSec = (currentLine.endTime || 0) / 1000;
|
||||
const seekSec = playSeekMs.value;
|
||||
const startSec = currentLine.startTime || 0;
|
||||
const endSec = currentLine.endTime || 0;
|
||||
const isLineActive =
|
||||
(seekSec >= startSec && seekSec < endSec) || lyricData.lyricIndex === lyricIndex;
|
||||
|
||||
if (!isLineActive) {
|
||||
const hasPlayed = seekSec >= (wordData.endTime || 0) / 1000;
|
||||
const hasPlayed = seekSec >= (wordData.endTime || 0);
|
||||
return { WebkitMaskPositionX: hasPlayed ? "0%" : "100%" };
|
||||
}
|
||||
const durationSec = Math.max(((wordData.endTime || 0) - (wordData.startTime || 0)) / 1000, 0.001);
|
||||
const progress = Math.max(
|
||||
Math.min((seekSec - (wordData.startTime || 0) / 1000) / durationSec, 1),
|
||||
0,
|
||||
);
|
||||
const durationSec = Math.max((wordData.endTime || 0) - (wordData.startTime || 0), 0.001);
|
||||
const progress = Math.max(Math.min((seekSec - (wordData.startTime || 0)) / durationSec, 1), 0);
|
||||
return {
|
||||
transitionDuration: `0s, 0s, 0.35s`,
|
||||
transitionDelay: `0ms`,
|
||||
@@ -382,11 +379,11 @@ const getScrollStyle = (line: RenderLine) => {
|
||||
const overflow = Math.max(0, content.scrollWidth - container.clientWidth);
|
||||
if (overflow <= 0) return { transform: "translateX(0px)" };
|
||||
// 计算进度:毫秒锚点插值(`playSeekMs`),并以当前行的 `time` 与有效 `endTime` 计算区间
|
||||
const seekSec = playSeekMs.value / 1000;
|
||||
const start = Number(line.line.startTime ?? 0) / 1000;
|
||||
const seekSec = playSeekMs.value;
|
||||
const start = Number(line.line.startTime ?? 0);
|
||||
// 仅在滚动计算中提前 1 秒
|
||||
const END_MARGIN_SEC = 1;
|
||||
const endRaw = Number(line.line.endTime) / 1000;
|
||||
const endRaw = Number(line.line.endTime);
|
||||
// 若 endTime 仍为 0 或不大于 start,视为无时长:不滚动
|
||||
const hasSafeEnd = Number.isFinite(endRaw) && endRaw > 0 && endRaw > start;
|
||||
if (!hasSafeEnd) return { transform: "translateX(0px)" };
|
||||
@@ -608,7 +605,7 @@ onMounted(() => {
|
||||
// 更新锚点:以传入的 currentTime + songOffset 建立毫秒级基准,并重置帧时间
|
||||
if (typeof lyricData.currentTime === "number") {
|
||||
const offset = Number(lyricData.songOffset ?? 0);
|
||||
baseMs = Math.floor((lyricData.currentTime + offset) * 1000);
|
||||
baseMs = Math.floor(lyricData.currentTime + offset);
|
||||
anchorTick = performance.now();
|
||||
}
|
||||
// 按播放状态节能:暂停时暂停 RAF,播放时恢复 RAF
|
||||
|
||||
Reference in New Issue
Block a user