🦄 refactor: 重构为毫秒单位

This commit is contained in:
imsyy
2025-11-18 15:08:15 +08:00
parent 772f6552e7
commit 39c35e8a31
16 changed files with 147 additions and 1125 deletions

View File

@@ -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;
});
// 计算每个点的透明度

View File

@@ -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);

View File

@@ -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();
};

View File

@@ -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();
};

View File

@@ -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 {

View File

@@ -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";

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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";
};

View File

@@ -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]; // 保持上一句(重叠时不跳)
};

View File

@@ -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();

View File

@@ -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 };
// };

View File

@@ -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";

View File

@@ -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);
}
/**
* 设置播放速率

View File

@@ -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;
};
/**
* 获取当前时间段的问候语
*/

View File

@@ -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