弃用依赖替代,逐字样式升级,调用AMLL歌词库默认使用加速镜像源(可在设置内关闭),语音提示添加延迟时间避免洪水

This commit is contained in:
NanoRocky 2024-12-14 22:51:23 +08:00
parent 3aa28f6a9c
commit f18146c54d
8 changed files with 1141 additions and 968 deletions

View File

@ -16,16 +16,15 @@
"dependencies": {
"@worstone/vue-aplayer": "^1.0.7",
"aplayer": "^1.10.1",
"axios": "^1.7.8",
"axios": "^1.7.9",
"dayjs": "^1.11.13",
"element-plus": "^2.8.8",
"element-plus": "^2.9.1",
"fetch-jsonp": "^1.3.0",
"lodash-es": "^4.17.21",
"pinia": "^2.2.7",
"pinia-plugin-persistedstate": "^4.1.3",
"pinia": "^2.3.0",
"pinia-plugin-persistedstate-2": "^2.0.27",
"swiper": "^11.1.15",
"three": "^0.170.0",
"three": "^0.171.0",
"vue": "^3.5.13"
},
"devDependencies": {
@ -37,15 +36,23 @@
"@vicons/tabler": "^0.12.0",
"@vicons/utils": "^0.1.4",
"@vitejs/plugin-vue": "^5.2.1",
"eslint": "^9.15.0",
"eslint-plugin-vue": "^9.31.0",
"prettier": "^3.4.1",
"sass": "^1.81.0",
"terser": "^5.36.0",
"unplugin-auto-import": "^0.18.6",
"eslint": "^9.17.0",
"eslint-plugin-vue": "^9.32.0",
"prettier": "^3.4.2",
"sass": "^1.83.0",
"terser": "^5.37.0",
"unplugin-auto-import": "^0.19.0",
"unplugin-vue-components": "^0.27.5",
"vite": "^5.4.11",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-pwa": "^0.20.5"
"vite": "^6.0.3",
"vite-plugin-compression2": "^1.3.3",
"vite-plugin-pwa": "^0.21.1"
},
"pnpm": {
"overrides": {
"glob": "^9.0.1",
"@jridgewell/sourcemap-codec":"^1.5.0",
"magic-string":"^0.30.15",
"workbox-build":"^7.3.0"
}
}
}

1735
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -134,7 +134,7 @@ const siteUrl = computed(() => {
&.fade-in-start {
text-shadow: 0px 0px 2px rgba(255, 240, 245, 1);
opacity: 0.6;
opacity: 0.6; //
transform: translateY(1px);
transition:
color 0.5s linear,
@ -238,7 +238,7 @@ const siteUrl = computed(() => {
}
to {
color: rgba(255, 255, 255, 0.8);
color: rgba(255, 240, 245, 1);
opacity: 1;
text-shadow: 3px 3px 7px rgba(255, 240, 245, 1),
0px 0px 12px rgba(255, 182, 193, 1),
@ -259,7 +259,8 @@ const siteUrl = computed(() => {
position: absolute;
width: auto;
opacity: 0.6;
text-shadow: 0 0 6px rgba(255, 240, 245, 1),
color: rgba(255, 240, 245, 1);
text-shadow: 0 0 6px rgba(0, 191, 255, 1),
0px 0px 2px rgba(176, 224, 230, 1),
0px 0px 2px rgba(230, 230, 250, 1);
font-family: MiSans-Regular;
@ -278,7 +279,7 @@ const siteUrl = computed(() => {
opacity: 1;
-webkit-background-clip: text;
background-clip: text;
text-shadow: 0 0 6px rgba(255, 255, 255, 0.8),
text-shadow: 0 0 6px rgba(255, 240, 245, 1),
0 0 2px rgba(255, 165, 0, 1),
0 0 2px rgba(255, 179, 71, 1);
font-family: MiSans-Regular;
@ -290,6 +291,7 @@ const siteUrl = computed(() => {
// End
#footer {
width: 100%;
position: absolute;

View File

@ -2,11 +2,11 @@
<APlayer v-if="playList[0]" ref="player" :audio="playList" :autoplay="store.playerAutoplay" :theme="theme"
:autoSwitch="false" :loop="store.playerLoop" :order="store.playerOrder" :volume="volume" :showLrc="true"
:listFolded="listFolded" :listMaxHeight="listMaxHeight" :noticeSwitch="false" @play="onPlay" @pause="onPause"
@error="loadMusicError" />
@timeupdate="onTimeUp" @error="loadMusicError" />
</template>
<script setup>
import { MusicOne, PlayWrong } from "@icon-park/vue-next";
import { Float, MusicOne, PlayWrong } from "@icon-park/vue-next";
import { getPlayerList } from "@/api";
import { mainStore } from "@/store";
import APlayer from "@worstone/vue-aplayer";
@ -14,7 +14,9 @@ import { Speech, stopSpeech, SpeechLocal } from "@/utils/speech";
import { decodeYrc } from "../utils/decodeYrc";
const store = mainStore();
let showYrcRunning = 0;
let lastTimestamp = Date.now();
let nowLineStart = -1;
// DOM
const player = ref(null);
@ -173,10 +175,10 @@ const onPause = () => {
store.setPlayerState(player.value.audioRef.paused);
};
let nowLineStart = -1
//
function showYrc() {
// YRC ...bushi... qrc yrc
showYrcRunning = 1;
try {
// try 西[20720,-4200]西 try
if (player.value == null) {
@ -221,10 +223,15 @@ function showYrc() {
if (!songId) {
return;
};
const songUrlInfUrl = {
'netease': `https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/main/ncm-lyrics/${songId}.yrc`,
'tencent': `https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/main/qq-lyrics/${songId}.qrc`
};
const songUrlInfUrl = store.playerYrcATDBF
? {
'netease': `https://ghp.ci/https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/main/ncm-lyrics/${songId}.yrc`,
'tencent': `https://ghp.ci/https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/main/qq-lyrics/${songId}.qrc`
}
: {
'netease': `https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/main/ncm-lyrics/${songId}.yrc`,
'tencent': `https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/main/qq-lyrics/${songId}.qrc`
};
if (!['netease', 'tencent'].includes(songServer)) {
return;
};
@ -263,10 +270,15 @@ function showYrc() {
const songIdlrc = songUrlInfw.get('id')
const songServerlrc = songUrlInfw.get("server");
if (songIdlrc) {
const songUrlInfwurl = {
'netease': `https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/main/ncm-lyrics/${songIdlrc}.lrc`,
'tencent': `https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/main/qq-lyrics/${songIdlrc}.lrc`
};
const songUrlInfwurl = store.playerYrcATDBF
? {
'netease': `https://ghp.ci/https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/main/ncm-lyrics/${songId}.lrc`,
'tencent': `https://ghp.ci/https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/main/qq-lyrics/${songId}.lrc`
}
: {
'netease': `https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/main/ncm-lyrics/${songId}.lrc`,
'tencent': `https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/main/qq-lyrics/${songId}.lrc`
};
if (!['netease', 'tencent'].includes(songServerlrc)) {
return;
};
@ -365,13 +377,18 @@ function showYrc() {
};
nowLineStart = yrcFiltered.slice(-1)[0][0];
};
requestAnimationFrame(showYrc);
return requestAnimationFrame(showYrc);
} catch (error) {
console.error(error);
return requestAnimationFrame(showYrc);
};
};
const onTimeUp = () => {
if (showYrcRunning == 0) {
requestAnimationFrame(showYrc);
};
};
requestAnimationFrame(showYrc);
//
const playToggle = () => {
@ -479,6 +496,7 @@ defineExpose({ playToggle, changeVolume, changeSong, toggleList });
#fff 85%,
hsla(0deg, 0%, 100%, 0.6) 90%,
hsla(0deg, 0%, 100%, 0));
&::before,
&::after {
display: none;

View File

@ -54,6 +54,10 @@
不稳定的网络中可能导致歌词载入速度变慢</span>
<el-switch v-model="playerYrcATDB" inline-prompt :active-icon="CheckSmall" :inactive-icon="CloseSmall" />
</div>
<div v-if="playerLrcShow && playerYrcATDB" class="item">
<span class="text" white-space="pre">调用 AMLL TTML Database 时使用镜像加速</span>
<el-switch v-model="playerYrcATDBF" inline-prompt :active-icon="CheckSmall" :inactive-icon="CloseSmall" />
</div>
<div v-if="playerLrcShow" class="item">
<span class="text">逐字歌词解析总开关</span>
<el-switch v-model="playerYrcShow" inline-prompt :active-icon="CheckSmall" :inactive-icon="CloseSmall" />
@ -101,6 +105,7 @@ const {
playerYrcShow,
playerYrcShowPro,
playerYrcATDB,
playerYrcATDBF,
} = storeToRefs(store);
//

View File

@ -29,6 +29,7 @@ export const mainStore = defineStore("main", {
playerYrcShow: true, // 逐字歌词解析总开关
playerYrcShowPro: false, // 逐字效果增强开关
playerYrcATDB: true, // 允许接入 AMLL TTML Database
playerYrcATDBF: true, // 接入 AMLL TTML Database 时使用镜像加速
yrcIndex: -1, // 逐字歌词进度存储
yrcTemp: [], // 逐字歌词缓存
yrcEnable: true,
@ -100,6 +101,7 @@ export const mainStore = defineStore("main", {
"playerYrcShow",
"playerYrcShowPro",
"playerYrcATDB",
"playerYrcATDBF",
],
},
});

View File

@ -1,13 +1,14 @@
let currentAudio = null;
let audioQueue = [];
let isPlaying = false;
let controller = null; // 用于取消请求
let controller = null;
let timeoutId = null;
/**
* Speech
* Made by NanoRocky
* 使用指定参数生成语音并播放音频
* 该功能原为 Azure 设计理应兼容大部分使用 post 传参的 api 请自行根据要求修改如果也使用 Azure 您可直接使用 https://github.com/NanoRocky/AzureSpeechAPI-by-PHP 完成 API 部署
* 该功能原为 Azure 设计理应兼容大部分使用 post 传参的 api 请自行根据要求修改
* https://learn.microsoft.com/zh-cn/azure/ai-services/speech-service/speech-synthesis-markup-voice
*
* @param {string} text - 朗读的文本
@ -16,6 +17,7 @@ let controller = null; // 用于取消请求
* @param {string} [role="Boy"] - 讲话角色扮演默认为Boy
* @param {string} [rate="1"] - 语速默认为1
* @param {string} [volume="100"] - 音量默认为100
* @param {number} [delay=300] - 等待时间毫秒后发出请求防止频繁点击产生请求洪水默认为等待300毫秒
* @returns {Promise<void>} - 一个 Promise在语音播放完成时解析或出现错误时拒绝
*/
export function Speech(
@ -25,15 +27,24 @@ export function Speech(
role = "Boy",
rate = "1",
volume = "100",
delay = 300,
) {
return new Promise(async (resolve, reject) => {
// 如果有现有的等待,取消之前的 timeout
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
};
if (currentAudio) {
currentAudio.pause();
currentAudio = null;
};
// 创建新的 AbortController 实例,并中断旧请求
if (controller) {
controller.abort();
}
};
controller = new AbortController();
const { signal } = controller;
const formData = new FormData();
formData.append("text", text);
formData.append("voice", voice);
@ -41,26 +52,136 @@ export function Speech(
formData.append("role", role);
formData.append("rate", rate);
formData.append("volume", volume);
// 在指定的 delay 后开始请求
timeoutId = setTimeout(async () => {
try {
const speechapi = import.meta.env.VITE_TTS_API;
const response = await fetch(speechapi, {
method: "POST",
body: formData,
signal, // 传递 AbortSignal
});
try {
const speechapi = import.meta.env.VITE_TTS_API;
const response = await fetch(speechapi, {
method: "POST",
body: formData,
signal, // 传递 AbortSignal
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error);
};
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error);
const blob = await response.blob();
const audioUrl = URL.createObjectURL(blob);
// 将新的音频对象添加到队列
audioQueue.push(audioUrl);
if (!isPlaying) {
playNext();
};
function playNext() {
if (audioQueue.length === 0) {
isPlaying = false;
return;
};
isPlaying = true;
const nextAudioUrl = audioQueue.shift();
if (currentAudio) {
currentAudio.pause();
currentAudio = null;
};
const audio = new Audio();
audio.src = nextAudioUrl;
audio.play();
// 在音频播放结束时解析 Promise
audio.onended = () => {
resolve();
playNext();
};
// 如果发生错误,拒绝 Promise
audio.onerror = (error) => {
reject(error);
playNext();
};
// 将当前播放的语音赋值给全局变量
currentAudio = audio;
};
} catch (error) {
if (error.name === "AbortError") {
console.log("Request canceled");
} else {
console.error("Error:", error.message);
reject(error);
};
};
}, delay);
});
}
/**
* 停止当前播放的语音并清空播放队列
*/
export function stopSpeech() {
if (currentAudio) {
currentAudio.pause();
currentAudio = null;
};
audioQueue = [];
isPlaying = false;
if (controller) {
controller.abort();
controller = null;
};
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
};
};
/**
* SpeechLocal
* Made by NanoRocky
* 播放本地预生成的语音音频
* 考虑到生成延迟所以加了这个仅必要模块调用 api 实时生成其它模块使用预先生成好的音频记得根据需求更换自己的音频文件哇
*
* @param {string} fileName - 音频文件名 + 文件拓展名请将文件放在指定路径
* @param {number} [delay=0] - 等待时间毫秒后发出请求防止频繁点击产生请求洪水默认提前生成的不等待
* @returns {Promise<void>} - 一个 Promise在语音播放完成时解析或出现错误时拒绝
*/
export function SpeechLocal(fileName, delay = 0) {
return new Promise((resolve, reject) => {
if (!fileName) {
reject(new Error("No file name provided"));
return;
}
const audioUrl = `https://file.nanorocky.top/home/speechlocal/${fileName}`;
// 如果有现有的等待,取消之前的 timeout
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
};
// 清除之前的音频
if (currentAudio) {
currentAudio.pause();
currentAudio = null;
}
timeoutId = setTimeout(async () => {
// 停止当前正在播放的语音
audioQueue = [];
isPlaying = false;
if (controller) {
controller.abort();
controller = null;
}
const blob = await response.blob();
const audioUrl = URL.createObjectURL(blob);
// 将新的音频对象添加到队列
// 添加新音频到队列并播放
audioQueue.push(audioUrl);
if (!isPlaying) {
playNext();
}
@ -74,14 +195,14 @@ export function Speech(
isPlaying = true;
const nextAudioUrl = audioQueue.shift();
if (currentAudio) {
currentAudio.pause();
currentAudio = null;
}
const audio = new Audio();
audio.src = nextAudioUrl;
audio.play();
// 确保新的音频对象没有被中途替换
audio.oncanplaythrough = () => {
currentAudio = audio;
currentAudio.play();
};
// 在音频播放结束时解析 Promise
audio.onended = () => {
@ -94,104 +215,7 @@ export function Speech(
reject(error);
playNext();
};
// 将当前播放的语音赋值给全局变量
currentAudio = audio;
}
} catch (error) {
if (error.name === "AbortError") {
console.log("Request canceled");
} else {
console.error("Error:", error.message);
reject(error);
}
}
};
}, delay);
});
}
/**
* 停止当前播放的语音并清空播放队列
*/
export function stopSpeech() {
if (currentAudio) {
currentAudio.pause();
currentAudio = null;
}
audioQueue = [];
isPlaying = false;
if (controller) {
controller.abort();
controller = null;
}
}
/**
* SpeechLocal
* Made by NanoRocky
* 播放本地预生成的语音音频
* 考虑到生成延迟所以加了这个仅必要模块调用 api 实时生成其它模块使用预先生成好的音频记得根据需求更换自己的音频文件哇
*
* @param {string} fileName - 音频文件名 + 文件拓展名请将文件放在指定路径
* @returns {Promise<void>} - 一个 Promise在语音播放完成时解析或出现错误时拒绝
*/
export function SpeechLocal(fileName) {
return new Promise((resolve, reject) => {
if (!fileName) {
reject(new Error("No file name provided"));
return;
}
const audioUrl = `/speechlocal/${fileName}`;
// 清除之前的音频
if (currentAudio) {
currentAudio.pause();
currentAudio = null;
}
// 停止当前正在播放的语音
audioQueue = [];
isPlaying = false;
if (controller) {
controller.abort();
controller = null;
}
// 添加新音频到队列并播放
audioQueue.push(audioUrl);
if (!isPlaying) {
playNext();
}
function playNext() {
if (audioQueue.length === 0) {
isPlaying = false;
return;
}
isPlaying = true;
const nextAudioUrl = audioQueue.shift();
const audio = new Audio();
audio.src = nextAudioUrl;
// 确保新的音频对象没有被中途替换
audio.oncanplaythrough = () => {
currentAudio = audio;
currentAudio.play();
};
// 在音频播放结束时解析 Promise
audio.onended = () => {
resolve();
playNext();
};
// 如果发生错误,拒绝 Promise
audio.onerror = (error) => {
reject(error);
playNext();
};
}
});
}
};

View File

@ -6,7 +6,7 @@ import { VitePWA } from "vite-plugin-pwa";
import vue from "@vitejs/plugin-vue";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import viteCompression from "vite-plugin-compression";
import viteCompression from "vite-plugin-compression2";
// https://vitejs.dev/config/
export default ({ mode }) =>
@ -107,7 +107,7 @@ export default ({ mode }) =>
preprocessorOptions: {
scss: {
charset: false,
additionalData: `@use "./src/style/global.scss" as global;`,
additionalData: `@use "@/style/global.scss" as global;`,
},
},
},