mirror of
https://github.com/timeshiftsauce/CeruMusic.git
synced 2025-11-25 11:29:42 +08:00
fix: 修复歌曲播放缓存内存泄露问题 feat:歌曲播放出错自动切歌不是暂停
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ceru-music",
|
"name": "ceru-music",
|
||||||
"version": "1.4.0",
|
"version": "1.4.1",
|
||||||
"description": "一款简洁优雅的音乐播放器",
|
"description": "一款简洁优雅的音乐播放器",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "sqj,wldss,star",
|
"author": "sqj,wldss,star",
|
||||||
|
|||||||
144
src/renderer/auto-imports.d.ts
vendored
144
src/renderer/auto-imports.d.ts
vendored
@@ -7,72 +7,90 @@
|
|||||||
export {}
|
export {}
|
||||||
declare global {
|
declare global {
|
||||||
const DialogPlugin: (typeof import('tdesign-vue-next'))['DialogPlugin']
|
const DialogPlugin: (typeof import('tdesign-vue-next'))['DialogPlugin']
|
||||||
const EffectScope: typeof import('vue')['EffectScope']
|
const EffectScope: (typeof import('vue'))['EffectScope']
|
||||||
const computed: typeof import('vue')['computed']
|
const computed: (typeof import('vue'))['computed']
|
||||||
const createApp: typeof import('vue')['createApp']
|
const createApp: (typeof import('vue'))['createApp']
|
||||||
const customRef: typeof import('vue')['customRef']
|
const customRef: (typeof import('vue'))['customRef']
|
||||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
const defineAsyncComponent: (typeof import('vue'))['defineAsyncComponent']
|
||||||
const defineComponent: typeof import('vue')['defineComponent']
|
const defineComponent: (typeof import('vue'))['defineComponent']
|
||||||
const effectScope: typeof import('vue')['effectScope']
|
const effectScope: (typeof import('vue'))['effectScope']
|
||||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
const getCurrentInstance: (typeof import('vue'))['getCurrentInstance']
|
||||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
const getCurrentScope: (typeof import('vue'))['getCurrentScope']
|
||||||
const getCurrentWatcher: typeof import('vue')['getCurrentWatcher']
|
const getCurrentWatcher: (typeof import('vue'))['getCurrentWatcher']
|
||||||
const h: typeof import('vue')['h']
|
const h: (typeof import('vue'))['h']
|
||||||
const inject: typeof import('vue')['inject']
|
const inject: (typeof import('vue'))['inject']
|
||||||
const isProxy: typeof import('vue')['isProxy']
|
const isProxy: (typeof import('vue'))['isProxy']
|
||||||
const isReactive: typeof import('vue')['isReactive']
|
const isReactive: (typeof import('vue'))['isReactive']
|
||||||
const isReadonly: typeof import('vue')['isReadonly']
|
const isReadonly: (typeof import('vue'))['isReadonly']
|
||||||
const isRef: typeof import('vue')['isRef']
|
const isRef: (typeof import('vue'))['isRef']
|
||||||
const isShallow: typeof import('vue')['isShallow']
|
const isShallow: (typeof import('vue'))['isShallow']
|
||||||
const markRaw: typeof import('vue')['markRaw']
|
const markRaw: (typeof import('vue'))['markRaw']
|
||||||
const nextTick: typeof import('vue')['nextTick']
|
const nextTick: (typeof import('vue'))['nextTick']
|
||||||
const onActivated: typeof import('vue')['onActivated']
|
const onActivated: (typeof import('vue'))['onActivated']
|
||||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
const onBeforeMount: (typeof import('vue'))['onBeforeMount']
|
||||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
const onBeforeUnmount: (typeof import('vue'))['onBeforeUnmount']
|
||||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
const onBeforeUpdate: (typeof import('vue'))['onBeforeUpdate']
|
||||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
const onDeactivated: (typeof import('vue'))['onDeactivated']
|
||||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
const onErrorCaptured: (typeof import('vue'))['onErrorCaptured']
|
||||||
const onMounted: typeof import('vue')['onMounted']
|
const onMounted: (typeof import('vue'))['onMounted']
|
||||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
const onRenderTracked: (typeof import('vue'))['onRenderTracked']
|
||||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
const onRenderTriggered: (typeof import('vue'))['onRenderTriggered']
|
||||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
const onScopeDispose: (typeof import('vue'))['onScopeDispose']
|
||||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
const onServerPrefetch: (typeof import('vue'))['onServerPrefetch']
|
||||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
const onUnmounted: (typeof import('vue'))['onUnmounted']
|
||||||
const onUpdated: typeof import('vue')['onUpdated']
|
const onUpdated: (typeof import('vue'))['onUpdated']
|
||||||
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
const onWatcherCleanup: (typeof import('vue'))['onWatcherCleanup']
|
||||||
const provide: typeof import('vue')['provide']
|
const provide: (typeof import('vue'))['provide']
|
||||||
const reactive: typeof import('vue')['reactive']
|
const reactive: (typeof import('vue'))['reactive']
|
||||||
const readonly: typeof import('vue')['readonly']
|
const readonly: (typeof import('vue'))['readonly']
|
||||||
const ref: typeof import('vue')['ref']
|
const ref: (typeof import('vue'))['ref']
|
||||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
const resolveComponent: (typeof import('vue'))['resolveComponent']
|
||||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
const shallowReactive: (typeof import('vue'))['shallowReactive']
|
||||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
const shallowReadonly: (typeof import('vue'))['shallowReadonly']
|
||||||
const shallowRef: typeof import('vue')['shallowRef']
|
const shallowRef: (typeof import('vue'))['shallowRef']
|
||||||
const toRaw: typeof import('vue')['toRaw']
|
const toRaw: (typeof import('vue'))['toRaw']
|
||||||
const toRef: typeof import('vue')['toRef']
|
const toRef: (typeof import('vue'))['toRef']
|
||||||
const toRefs: typeof import('vue')['toRefs']
|
const toRefs: (typeof import('vue'))['toRefs']
|
||||||
const toValue: typeof import('vue')['toValue']
|
const toValue: (typeof import('vue'))['toValue']
|
||||||
const triggerRef: typeof import('vue')['triggerRef']
|
const triggerRef: (typeof import('vue'))['triggerRef']
|
||||||
const unref: typeof import('vue')['unref']
|
const unref: (typeof import('vue'))['unref']
|
||||||
const useAttrs: typeof import('vue')['useAttrs']
|
const useAttrs: (typeof import('vue'))['useAttrs']
|
||||||
const useCssModule: typeof import('vue')['useCssModule']
|
const useCssModule: (typeof import('vue'))['useCssModule']
|
||||||
const useCssVars: typeof import('vue')['useCssVars']
|
const useCssVars: (typeof import('vue'))['useCssVars']
|
||||||
const useDialog: typeof import('naive-ui')['useDialog']
|
const useDialog: (typeof import('naive-ui'))['useDialog']
|
||||||
const useId: typeof import('vue')['useId']
|
const useId: (typeof import('vue'))['useId']
|
||||||
const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
|
const useLoadingBar: (typeof import('naive-ui'))['useLoadingBar']
|
||||||
const useMessage: typeof import('naive-ui')['useMessage']
|
const useMessage: (typeof import('naive-ui'))['useMessage']
|
||||||
const useModel: typeof import('vue')['useModel']
|
const useModel: (typeof import('vue'))['useModel']
|
||||||
const useNotification: typeof import('naive-ui')['useNotification']
|
const useNotification: (typeof import('naive-ui'))['useNotification']
|
||||||
const useSlots: typeof import('vue')['useSlots']
|
const useSlots: (typeof import('vue'))['useSlots']
|
||||||
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
const useTemplateRef: (typeof import('vue'))['useTemplateRef']
|
||||||
const watch: typeof import('vue')['watch']
|
const watch: (typeof import('vue'))['watch']
|
||||||
const watchEffect: typeof import('vue')['watchEffect']
|
const watchEffect: (typeof import('vue'))['watchEffect']
|
||||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
const watchPostEffect: (typeof import('vue'))['watchPostEffect']
|
||||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
const watchSyncEffect: (typeof import('vue'))['watchSyncEffect']
|
||||||
}
|
}
|
||||||
// for type re-export
|
// for type re-export
|
||||||
declare global {
|
declare global {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
export type {
|
||||||
|
Component,
|
||||||
|
Slot,
|
||||||
|
Slots,
|
||||||
|
ComponentPublicInstance,
|
||||||
|
ComputedRef,
|
||||||
|
DirectiveBinding,
|
||||||
|
ExtractDefaultPropTypes,
|
||||||
|
ExtractPropTypes,
|
||||||
|
ExtractPublicPropTypes,
|
||||||
|
InjectionKey,
|
||||||
|
PropType,
|
||||||
|
Ref,
|
||||||
|
ShallowRef,
|
||||||
|
MaybeRef,
|
||||||
|
MaybeRefOrGetter,
|
||||||
|
VNode,
|
||||||
|
WritableComputedRef
|
||||||
|
} from 'vue'
|
||||||
import('vue')
|
import('vue')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ const emit = defineEmits<{
|
|||||||
const canvasRef = ref<HTMLCanvasElement>()
|
const canvasRef = ref<HTMLCanvasElement>()
|
||||||
const animationId = ref<number>()
|
const animationId = ref<number>()
|
||||||
const analyser = ref<AnalyserNode>()
|
const analyser = ref<AnalyserNode>()
|
||||||
|
// 节流渲染,目标 ~30fps
|
||||||
|
const lastFrameTime = ref(0)
|
||||||
const dataArray = ref<Uint8Array>()
|
const dataArray = ref<Uint8Array>()
|
||||||
const resizeObserver = ref<ResizeObserver>()
|
const resizeObserver = ref<ResizeObserver>()
|
||||||
const componentId = ref<string>(`visualizer-${Date.now()}-${Math.random()}`)
|
const componentId = ref<string>(`visualizer-${Date.now()}-${Math.random()}`)
|
||||||
@@ -75,93 +77,87 @@ const initAudioAnalyser = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 绘制可视化
|
// 绘制可视化
|
||||||
const draw = () => {
|
const draw = (ts?: number) => {
|
||||||
if (!canvasRef.value || !analyser.value || !dataArray.value) return
|
if (!canvasRef.value || !analyser.value || !dataArray.value) return
|
||||||
|
|
||||||
|
// 帧率节流 ~30fps
|
||||||
|
const now = ts ?? performance.now()
|
||||||
|
if (now - lastFrameTime.value < 33) {
|
||||||
|
animationId.value = requestAnimationFrame(draw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lastFrameTime.value = now
|
||||||
|
|
||||||
const canvas = canvasRef.value
|
const canvas = canvasRef.value
|
||||||
const ctx = canvas.getContext('2d')
|
const ctx = canvas.getContext('2d')
|
||||||
if (!ctx) return
|
if (!ctx) {
|
||||||
|
animationId.value = requestAnimationFrame(draw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 获取频域数据或生成模拟数据
|
// 获取频域数据或生成模拟数据
|
||||||
if (analyser.value && dataArray.value) {
|
if (analyser.value && dataArray.value) {
|
||||||
// 有真实音频分析器,获取真实数据
|
|
||||||
analyser.value.getByteFrequencyData(dataArray.value as Uint8Array<ArrayBuffer>)
|
analyser.value.getByteFrequencyData(dataArray.value as Uint8Array<ArrayBuffer>)
|
||||||
} else {
|
} else {
|
||||||
// 没有音频分析器,生成模拟数据
|
const time = now * 0.001
|
||||||
const time = Date.now() * 0.001
|
|
||||||
for (let i = 0; i < dataArray.value.length; i++) {
|
for (let i = 0; i < dataArray.value.length; i++) {
|
||||||
// 生成基于时间的模拟频谱数据
|
|
||||||
const frequency = i / dataArray.value.length
|
const frequency = i / dataArray.value.length
|
||||||
const amplitude = Math.sin(time * 2 + frequency * 10) * 0.5 + 0.5
|
const amplitude = Math.sin(time * 2 + frequency * 10) * 0.5 + 0.5
|
||||||
const bass = Math.sin(time * 4) * 0.3 + 0.7 // 低频变化
|
const bass = Math.sin(time * 4) * 0.3 + 0.7
|
||||||
dataArray.value[i] = Math.floor(amplitude * bass * 255 * (1 - frequency * 0.7))
|
dataArray.value[i] = Math.floor(amplitude * bass * 255 * (1 - frequency * 0.7))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算低频音量 (80hz-120hz 范围)
|
// 计算低频音量(前 3 个 bin)
|
||||||
// 假设采样率为 44100Hz,fftSize 为 256,则每个频率 bin 约为 172Hz
|
|
||||||
// 80-120Hz 大约对应前 1-2 个 bin
|
|
||||||
const lowFreqStart = 0
|
|
||||||
const lowFreqEnd = Math.min(3, dataArray.value.length) // 取前几个低频 bin
|
|
||||||
let lowFreqSum = 0
|
let lowFreqSum = 0
|
||||||
for (let i = lowFreqStart; i < lowFreqEnd; i++) {
|
const lowBins = Math.min(3, dataArray.value.length)
|
||||||
lowFreqSum += dataArray.value[i]
|
for (let i = 0; i < lowBins; i++) lowFreqSum += dataArray.value[i]
|
||||||
}
|
emit('lowFreqUpdate', lowFreqSum / lowBins / 255)
|
||||||
const lowFreqVolume = lowFreqSum / (lowFreqEnd - lowFreqStart) / 255
|
|
||||||
|
|
||||||
// 发送低频音量给父组件
|
// 清屏
|
||||||
emit('lowFreqUpdate', lowFreqVolume)
|
|
||||||
|
|
||||||
// 完全清空画布
|
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||||
|
|
||||||
// 如果有背景色,再填充背景
|
// 背景
|
||||||
if (props.backgroundColor !== 'transparent') {
|
if (props.backgroundColor !== 'transparent') {
|
||||||
ctx.fillStyle = props.backgroundColor
|
ctx.fillStyle = props.backgroundColor
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用容器的实际尺寸进行计算,因为 ctx 已经缩放过了
|
// 计算尺寸
|
||||||
const container = canvas.parentElement
|
const container = canvas.parentElement
|
||||||
if (!container) return
|
if (!container) {
|
||||||
|
animationId.value = requestAnimationFrame(draw)
|
||||||
|
return
|
||||||
|
}
|
||||||
const containerRect = container.getBoundingClientRect()
|
const containerRect = container.getBoundingClientRect()
|
||||||
const canvasWidth = containerRect.width
|
const canvasWidth = containerRect.width
|
||||||
const canvasHeight = props.height
|
const canvasHeight = props.height
|
||||||
|
|
||||||
// 计算对称柱状图参数
|
// 柱状参数
|
||||||
const halfBarCount = Math.floor(props.barCount / 2)
|
const halfBarCount = Math.floor(props.barCount / 2)
|
||||||
const barWidth = canvasWidth / 2 / halfBarCount
|
const barWidth = canvasWidth / 2 / halfBarCount
|
||||||
const maxBarHeight = canvasHeight * 0.9
|
const maxBarHeight = canvasHeight * 0.9
|
||||||
const centerX = canvasWidth / 2
|
const centerX = canvasWidth / 2
|
||||||
|
|
||||||
// 绘制左右对称的频谱柱状图
|
// 每帧仅创建一次渐变(自底向上),减少对象分配
|
||||||
for (let i = 0; i < halfBarCount; i++) {
|
const gradient = ctx.createLinearGradient(0, canvasHeight, 0, 0)
|
||||||
// 增强低频响应,让可视化更敏感
|
|
||||||
let barHeight = (dataArray.value[i] / 255) * maxBarHeight
|
|
||||||
|
|
||||||
// 对数据进行增强处理,让变化更明显
|
|
||||||
barHeight = Math.pow(barHeight / maxBarHeight, 0.6) * maxBarHeight
|
|
||||||
|
|
||||||
const y = canvasHeight - barHeight
|
|
||||||
|
|
||||||
// 创建渐变色
|
|
||||||
const gradient = ctx.createLinearGradient(0, canvasHeight, 0, y)
|
|
||||||
gradient.addColorStop(0, props.color)
|
gradient.addColorStop(0, props.color)
|
||||||
gradient.addColorStop(1, props.color.replace(/[\d\.]+\)$/g, '0.3)'))
|
gradient.addColorStop(1, props.color.replace(/[\d\.]+\)$/g, '0.3)'))
|
||||||
|
|
||||||
ctx.fillStyle = gradient
|
ctx.fillStyle = gradient
|
||||||
|
|
||||||
// 绘制左侧柱状图(从中心向左)
|
// 绘制对称频谱
|
||||||
|
for (let i = 0; i < halfBarCount; i++) {
|
||||||
|
let barHeight = (dataArray.value[i] / 255) * maxBarHeight
|
||||||
|
barHeight = Math.pow(barHeight / maxBarHeight, 0.6) * maxBarHeight
|
||||||
|
const y = canvasHeight - barHeight
|
||||||
|
|
||||||
const leftX = centerX - (i + 1) * barWidth
|
const leftX = centerX - (i + 1) * barWidth
|
||||||
ctx.fillRect(leftX, y, barWidth, barHeight)
|
ctx.fillRect(leftX, y, barWidth, barHeight)
|
||||||
|
|
||||||
// 绘制右侧柱状图(从中心向右)
|
|
||||||
const rightX = centerX + i * barWidth
|
const rightX = centerX + i * barWidth
|
||||||
ctx.fillRect(rightX, y, barWidth, barHeight)
|
ctx.fillRect(rightX, y, barWidth, barHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 继续动画
|
|
||||||
if (props.show && Audio.value.isPlay) {
|
if (props.show && Audio.value.isPlay) {
|
||||||
animationId.value = requestAnimationFrame(draw)
|
animationId.value = requestAnimationFrame(draw)
|
||||||
}
|
}
|
||||||
@@ -286,6 +282,10 @@ onBeforeUnmount(() => {
|
|||||||
analyser.value.disconnect()
|
analyser.value.disconnect()
|
||||||
analyser.value = undefined
|
analyser.value = undefined
|
||||||
}
|
}
|
||||||
|
// 通知管理器移除对该分析器的引用,防止 Map 持有导致 GC 不回收
|
||||||
|
try {
|
||||||
|
audioManager.removeAnalyser(componentId.value)
|
||||||
|
} catch {}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('清理音频资源时出错:', error)
|
console.warn('清理音频资源时出错:', error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ watch(
|
|||||||
const isNetease =
|
const isNetease =
|
||||||
props.songInfo && 'source' in props.songInfo && props.songInfo.source === 'wy'
|
props.songInfo && 'source' in props.songInfo && props.songInfo.source === 'wy'
|
||||||
const songinfo: any = _.cloneDeep(toRaw(props.songInfo))
|
const songinfo: any = _.cloneDeep(toRaw(props.songInfo))
|
||||||
console.log(songinfo)
|
|
||||||
if (isNetease) {
|
if (isNetease) {
|
||||||
// 网易云音乐优先尝试ttml接口
|
// 网易云音乐优先尝试ttml接口
|
||||||
try {
|
try {
|
||||||
@@ -112,30 +112,26 @@ watch(
|
|||||||
).text()) as any
|
).text()) as any
|
||||||
if (!res || res.length < 100) throw new Error('ttml 无歌词')
|
if (!res || res.length < 100) throw new Error('ttml 无歌词')
|
||||||
parsedLyrics = parseTTML(res).lines
|
parsedLyrics = parseTTML(res).lines
|
||||||
console.log('搜索到ttml歌词', parsedLyrics)
|
|
||||||
} catch {
|
} catch {
|
||||||
// ttml失败后使用新的歌词API
|
// ttml失败后使用新的歌词API
|
||||||
const lyricData = await window.api.music.requestSdk('getLyric', {
|
const lyricData = await window.api.music.requestSdk('getLyric', {
|
||||||
source: 'wy',
|
source: 'wy',
|
||||||
songInfo: songinfo
|
songInfo: songinfo
|
||||||
})
|
})
|
||||||
console.log('网易云歌词数据:', lyricData)
|
|
||||||
|
|
||||||
if (lyricData.crlyric) {
|
if (lyricData.crlyric) {
|
||||||
// 使用逐字歌词
|
// 使用逐字歌词
|
||||||
lyricText = lyricData.crlyric
|
lyricText = lyricData.crlyric
|
||||||
console.log('网易云逐字歌词', lyricText)
|
|
||||||
parsedLyrics = parseYrc(lyricText)
|
parsedLyrics = parseYrc(lyricText)
|
||||||
console.log('使用网易云逐字歌词', parsedLyrics)
|
|
||||||
} else if (lyricData.lyric) {
|
} else if (lyricData.lyric) {
|
||||||
lyricText = lyricData.lyric
|
lyricText = lyricData.lyric
|
||||||
parsedLyrics = parseLrc(lyricText)
|
parsedLyrics = parseLrc(lyricText)
|
||||||
console.log('使用网易云普通歌词', parsedLyrics)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lyricData.tlyric) {
|
if (lyricData.tlyric) {
|
||||||
const translatedline = parseLrc(lyricData.tlyric)
|
const translatedline = parseLrc(lyricData.tlyric)
|
||||||
console.log('网易云翻译歌词:', translatedline)
|
|
||||||
for (let i = 0; i < parsedLyrics.length; i++) {
|
for (let i = 0; i < parsedLyrics.length; i++) {
|
||||||
if (translatedline[i] && translatedline[i].words[0]) {
|
if (translatedline[i] && translatedline[i].words[0]) {
|
||||||
parsedLyrics[i].translatedLyric = translatedline[i].words[0].word
|
parsedLyrics[i].translatedLyric = translatedline[i].words[0].word
|
||||||
@@ -152,7 +148,6 @@ watch(
|
|||||||
source: source,
|
source: source,
|
||||||
songInfo: cleanSongInfo
|
songInfo: cleanSongInfo
|
||||||
})
|
})
|
||||||
console.log(`${source}歌词数据:`, lyricData)
|
|
||||||
|
|
||||||
if (lyricData.crlyric) {
|
if (lyricData.crlyric) {
|
||||||
// 使用逐字歌词
|
// 使用逐字歌词
|
||||||
@@ -162,16 +157,14 @@ watch(
|
|||||||
} else {
|
} else {
|
||||||
parsedLyrics = parseYrc(lyricText)
|
parsedLyrics = parseYrc(lyricText)
|
||||||
}
|
}
|
||||||
console.log(`使用${source}逐字歌词`, parsedLyrics)
|
|
||||||
} else if (lyricData.lyric) {
|
} else if (lyricData.lyric) {
|
||||||
lyricText = lyricData.lyric
|
lyricText = lyricData.lyric
|
||||||
parsedLyrics = parseLrc(lyricText)
|
parsedLyrics = parseLrc(lyricText)
|
||||||
console.log(`使用${source}普通歌词`, parsedLyrics)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lyricData.tlyric) {
|
if (lyricData.tlyric) {
|
||||||
const translatedline = parseLrc(lyricData.tlyric)
|
const translatedline = parseLrc(lyricData.tlyric)
|
||||||
console.log(`${source}翻译歌词:`, translatedline)
|
|
||||||
for (let i = 0; i < parsedLyrics.length; i++) {
|
for (let i = 0; i < parsedLyrics.length; i++) {
|
||||||
if (translatedline[i] && translatedline[i].words[0]) {
|
if (translatedline[i] && translatedline[i].words[0]) {
|
||||||
parsedLyrics[i].translatedLyric = translatedline[i].words[0].word
|
parsedLyrics[i].translatedLyric = translatedline[i].words[0].word
|
||||||
@@ -182,10 +175,8 @@ watch(
|
|||||||
|
|
||||||
if (parsedLyrics.length > 0) {
|
if (parsedLyrics.length > 0) {
|
||||||
state.lyricLines = parsedLyrics
|
state.lyricLines = parsedLyrics
|
||||||
console.log('歌词加载成功', parsedLyrics.length)
|
|
||||||
} else {
|
} else {
|
||||||
state.lyricLines = []
|
state.lyricLines = []
|
||||||
console.log('未找到歌词或解析失败')
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取歌词失败:', error)
|
console.error('获取歌词失败:', error)
|
||||||
@@ -197,6 +188,7 @@ watch(
|
|||||||
|
|
||||||
const bgRef = ref<BackgroundRenderRef | undefined>(undefined)
|
const bgRef = ref<BackgroundRenderRef | undefined>(undefined)
|
||||||
const lyricPlayerRef = ref<LyricPlayerRef | undefined>(undefined)
|
const lyricPlayerRef = ref<LyricPlayerRef | undefined>(undefined)
|
||||||
|
|
||||||
// 订阅音频事件,保持数据同步
|
// 订阅音频事件,保持数据同步
|
||||||
const unsubscribeTimeUpdate = ref<(() => void) | undefined>(undefined)
|
const unsubscribeTimeUpdate = ref<(() => void) | undefined>(undefined)
|
||||||
const unsubscribePlay = ref<(() => void) | undefined>(undefined)
|
const unsubscribePlay = ref<(() => void) | undefined>(undefined)
|
||||||
@@ -214,7 +206,6 @@ const useBlackText = ref(false)
|
|||||||
async function updateTextColor() {
|
async function updateTextColor() {
|
||||||
try {
|
try {
|
||||||
useBlackText.value = await shouldUseBlackText(actualCoverImage.value)
|
useBlackText.value = await shouldUseBlackText(actualCoverImage.value)
|
||||||
console.log('使用黑色文本:', useBlackText.value)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取对比色失败:', error)
|
console.error('获取对比色失败:', error)
|
||||||
useBlackText.value = false // 默认使用白色文本
|
useBlackText.value = false // 默认使用白色文本
|
||||||
@@ -247,7 +238,6 @@ watch(
|
|||||||
// 组件挂载时初始化
|
// 组件挂载时初始化
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
updateTextColor()
|
updateTextColor()
|
||||||
console.log('组件挂载完成', bgRef.value, lyricPlayerRef.value)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 组件卸载前清理订阅
|
// 组件卸载前清理订阅
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, onUnmounted, provide, ref, onActivated, onDeactivated } from 'vue'
|
import {
|
||||||
|
onMounted,
|
||||||
|
onUnmounted,
|
||||||
|
provide,
|
||||||
|
ref,
|
||||||
|
onActivated,
|
||||||
|
onDeactivated,
|
||||||
|
watch,
|
||||||
|
nextTick
|
||||||
|
} from 'vue'
|
||||||
import { ControlAudioStore } from '@renderer/store/ControlAudio'
|
import { ControlAudioStore } from '@renderer/store/ControlAudio'
|
||||||
|
|
||||||
const audioStore = ControlAudioStore()
|
const audioStore = ControlAudioStore()
|
||||||
@@ -17,6 +26,26 @@ onMounted(() => {
|
|||||||
// window.api.ping(handleEnded)
|
// window.api.ping(handleEnded)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听 URL 变化,先重置旧音频再加载新音频,避免旧解码/缓冲滞留
|
||||||
|
*/
|
||||||
|
watch(
|
||||||
|
() => audioStore.Audio.url,
|
||||||
|
async (newUrl) => {
|
||||||
|
const a = audioMeta.value
|
||||||
|
if (!a) return
|
||||||
|
try {
|
||||||
|
a.pause()
|
||||||
|
} catch {}
|
||||||
|
a.removeAttribute('src')
|
||||||
|
a.load()
|
||||||
|
await nextTick()
|
||||||
|
// 模板绑定会把 src 更新为 newUrl,这里再触发一次 load
|
||||||
|
if (newUrl) {
|
||||||
|
a.load()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
// 组件被激活时(从缓存中恢复)
|
// 组件被激活时(从缓存中恢复)
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
console.log('音频组件被激活')
|
console.log('音频组件被激活')
|
||||||
@@ -71,22 +100,29 @@ const handlePlay = (): void => {
|
|||||||
audioStore.publish('play')
|
audioStore.publish('play')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let rafId: number | null = null
|
||||||
const startSetupInterval = (): void => {
|
const startSetupInterval = (): void => {
|
||||||
|
if (rafId !== null) return
|
||||||
const onFrame = () => {
|
const onFrame = () => {
|
||||||
if (audioMeta.value && !audioMeta.value.paused) {
|
if (audioMeta.value && !audioMeta.value.paused) {
|
||||||
audioStore.publish('timeupdate')
|
audioStore.publish('timeupdate')
|
||||||
|
|
||||||
audioStore.setCurrentTime((audioMeta.value && audioMeta.value.currentTime) || 0)
|
audioStore.setCurrentTime((audioMeta.value && audioMeta.value.currentTime) || 0)
|
||||||
|
|
||||||
requestAnimationFrame(onFrame)
|
|
||||||
}
|
}
|
||||||
|
rafId = requestAnimationFrame(onFrame)
|
||||||
}
|
}
|
||||||
requestAnimationFrame(onFrame)
|
rafId = requestAnimationFrame(onFrame)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePause = (): void => {
|
const handlePause = (): void => {
|
||||||
audioStore.Audio.isPlay = false
|
audioStore.Audio.isPlay = false
|
||||||
audioStore.publish('pause')
|
audioStore.publish('pause')
|
||||||
|
// 停止单实例 rAF
|
||||||
|
if (rafId !== null) {
|
||||||
|
try {
|
||||||
|
cancelAnimationFrame(rafId)
|
||||||
|
} catch {}
|
||||||
|
rafId = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleError = (event: Event): void => {
|
const handleError = (event: Event): void => {
|
||||||
@@ -112,8 +148,23 @@ const handleCanPlay = (): void => {
|
|||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
// 组件卸载时清空所有订阅者
|
// 组件卸载时清空所有订阅者
|
||||||
|
try {
|
||||||
window.api.pingService.stop()
|
window.api.pingService.stop()
|
||||||
|
} catch {}
|
||||||
|
// 停止 rAF
|
||||||
|
if (rafId !== null) {
|
||||||
|
try {
|
||||||
|
cancelAnimationFrame(rafId)
|
||||||
|
} catch {}
|
||||||
|
rafId = null
|
||||||
|
}
|
||||||
|
if (audioMeta.value) {
|
||||||
|
try {
|
||||||
|
audioMeta.value.pause()
|
||||||
|
} catch {}
|
||||||
|
audioMeta.value.removeAttribute('src')
|
||||||
|
audioMeta.value.load()
|
||||||
|
}
|
||||||
audioStore.clearAllSubscribers()
|
audioStore.clearAllSubscribers()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -132,12 +132,6 @@ let isFull = false
|
|||||||
|
|
||||||
// 播放指定歌曲
|
// 播放指定歌曲
|
||||||
const playSong = async (song: SongList) => {
|
const playSong = async (song: SongList) => {
|
||||||
// 保存当前播放状态,用于失败时恢复
|
|
||||||
const previousSong = { ...songInfo.value }
|
|
||||||
const previousSongId = userInfo.value.lastPlaySongId
|
|
||||||
const previousTime = Audio.value.currentTime
|
|
||||||
const wasPlaying = Audio.value.isPlay
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 设置加载状态
|
// 设置加载状态
|
||||||
isLoadingSong.value = true
|
isLoadingSong.value = true
|
||||||
@@ -194,35 +188,21 @@ const playSong = async (song: SongList) => {
|
|||||||
try {
|
try {
|
||||||
urlToPlay = await getSongRealUrl(toRaw(song))
|
urlToPlay = await getSongRealUrl(toRaw(song))
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('获取歌曲 URL 失败,恢复原歌曲:', error)
|
console.error('获取歌曲 URL 失败,播放下一首原歌曲:', error)
|
||||||
isLoadingSong.value = false
|
isLoadingSong.value = false
|
||||||
MessagePlugin.error('播放失败,原因:' + error.message)
|
tryAutoNext('获取歌曲 URL 失败')
|
||||||
|
|
||||||
// 恢复歌曲信息
|
|
||||||
songInfo.value = { ...previousSong }
|
|
||||||
userInfo.value.lastPlaySongId = previousSongId
|
|
||||||
mediaSessionController.updateMetadata({
|
|
||||||
title: previousSong.name,
|
|
||||||
artist: previousSong.singer,
|
|
||||||
album: previousSong.albumName || '未知专辑',
|
|
||||||
artworkUrl: previousSong.img || defaultCoverImg
|
|
||||||
})
|
|
||||||
|
|
||||||
// 如果原来在播放,恢复播放
|
|
||||||
if (wasPlaying && previousSong.songmid && Audio.value.audio) {
|
|
||||||
try {
|
|
||||||
if (previousTime > 0) {
|
|
||||||
Audio.value.audio.currentTime = previousTime
|
|
||||||
}
|
|
||||||
await start()
|
|
||||||
} catch (resumeError) {
|
|
||||||
console.error('恢复播放失败:', resumeError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 在切换前彻底重置旧音频,释放缓冲与解码器
|
||||||
|
if (Audio.value.audio) {
|
||||||
|
const a = Audio.value.audio
|
||||||
|
try {
|
||||||
|
a.pause()
|
||||||
|
} catch {}
|
||||||
|
a.removeAttribute('src')
|
||||||
|
a.load()
|
||||||
|
}
|
||||||
// 设置 URL(这会触发音频重新加载)
|
// 设置 URL(这会触发音频重新加载)
|
||||||
setUrl(urlToPlay)
|
setUrl(urlToPlay)
|
||||||
|
|
||||||
@@ -242,31 +222,13 @@ const playSong = async (song: SongList) => {
|
|||||||
/**
|
/**
|
||||||
* 异步开始播放(不await,以免阻塞UI)
|
* 异步开始播放(不await,以免阻塞UI)
|
||||||
*/
|
*/
|
||||||
start().catch(async (error: any) => {
|
start()
|
||||||
|
.catch(async (error: any) => {
|
||||||
console.error('启动播放失败:', error)
|
console.error('启动播放失败:', error)
|
||||||
MessagePlugin.error('播放失败,原因:' + error.message)
|
tryAutoNext('启动播放失败')
|
||||||
|
|
||||||
// 恢复旧歌曲状态
|
|
||||||
songInfo.value = { ...previousSong }
|
|
||||||
userInfo.value.lastPlaySongId = previousSongId
|
|
||||||
mediaSessionController.updateMetadata({
|
|
||||||
title: previousSong.name,
|
|
||||||
artist: previousSong.singer,
|
|
||||||
album: previousSong.albumName || '未知专辑',
|
|
||||||
artworkUrl: previousSong.img || defaultCoverImg
|
|
||||||
})
|
})
|
||||||
|
.then(() => {
|
||||||
// 如果原来在播放,恢复旧播放
|
autoNextCount.value = 0
|
||||||
if (wasPlaying && previousSong.songmid && Audio.value.audio) {
|
|
||||||
try {
|
|
||||||
if (previousTime > 0) {
|
|
||||||
Audio.value.audio.currentTime = previousTime
|
|
||||||
}
|
|
||||||
await start()
|
|
||||||
} catch (resumeError) {
|
|
||||||
console.error('恢复播放失败:', resumeError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -274,17 +236,25 @@ const playSong = async (song: SongList) => {
|
|||||||
* (多一道保险)
|
* (多一道保险)
|
||||||
*/
|
*/
|
||||||
if (Audio.value.audio) {
|
if (Audio.value.audio) {
|
||||||
Audio.value.audio.addEventListener('playing', () => {
|
Audio.value.audio.addEventListener(
|
||||||
|
'playing',
|
||||||
|
() => {
|
||||||
isLoadingSong.value = false
|
isLoadingSong.value = false
|
||||||
})
|
},
|
||||||
Audio.value.audio.addEventListener('error', () => {
|
{ once: true }
|
||||||
|
)
|
||||||
|
Audio.value.audio.addEventListener(
|
||||||
|
'error',
|
||||||
|
() => {
|
||||||
isLoadingSong.value = false
|
isLoadingSong.value = false
|
||||||
})
|
},
|
||||||
|
{ once: true }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('播放歌曲失败(外层捕获):', error)
|
console.error('播放歌曲失败(外层捕获):', error)
|
||||||
MessagePlugin.error('播放失败,原因:' + error.message)
|
tryAutoNext('播放歌曲失败')
|
||||||
|
// MessagePlugin.error('播放失败,原因:' + error.message)
|
||||||
isLoadingSong.value = false
|
isLoadingSong.value = false
|
||||||
} finally {
|
} finally {
|
||||||
// 最后的保险,确保加载状态一定会被关闭
|
// 最后的保险,确保加载状态一定会被关闭
|
||||||
@@ -300,6 +270,23 @@ const playMode = ref(userInfo.value.playMode || PlayMode.SEQUENCE)
|
|||||||
// 歌曲加载状态
|
// 歌曲加载状态
|
||||||
const isLoadingSong = ref(false)
|
const isLoadingSong = ref(false)
|
||||||
|
|
||||||
|
// 自动下一首次数限制:不超过当前列表的30%
|
||||||
|
const autoNextCount = ref(0)
|
||||||
|
const getAutoNextLimit = () => Math.max(1, Math.floor(list.value.length * 0.3))
|
||||||
|
const tryAutoNext = (reason: string) => {
|
||||||
|
const limit = getAutoNextLimit()
|
||||||
|
MessagePlugin.error(`自动跳过当前歌曲:原因:${reason}`)
|
||||||
|
|
||||||
|
if (autoNextCount.value >= limit && autoNextCount.value > 2) {
|
||||||
|
MessagePlugin.error(
|
||||||
|
`自动下一首失败:超过当前列表30%限制(${autoNextCount.value}/${limit})。原因:${reason}`
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
autoNextCount.value++
|
||||||
|
playNext()
|
||||||
|
}
|
||||||
|
|
||||||
// 更新播放模式
|
// 更新播放模式
|
||||||
const updatePlayMode = () => {
|
const updatePlayMode = () => {
|
||||||
const modes = [PlayMode.SEQUENCE, PlayMode.RANDOM, PlayMode.SINGLE]
|
const modes = [PlayMode.SEQUENCE, PlayMode.RANDOM, PlayMode.SINGLE]
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ class AudioManager {
|
|||||||
private audioSources = new WeakMap<HTMLAudioElement, MediaElementAudioSourceNode>()
|
private audioSources = new WeakMap<HTMLAudioElement, MediaElementAudioSourceNode>()
|
||||||
private audioContexts = new WeakMap<HTMLAudioElement, AudioContext>()
|
private audioContexts = new WeakMap<HTMLAudioElement, AudioContext>()
|
||||||
private analysers = new Map<string, AnalyserNode>()
|
private analysers = new Map<string, AnalyserNode>()
|
||||||
|
// 为每个 audioElement 复用一个分流器,避免重复断开重连主链路
|
||||||
|
private splitters = new WeakMap<HTMLAudioElement, GainNode>()
|
||||||
|
|
||||||
static getInstance(): AudioManager {
|
static getInstance(): AudioManager {
|
||||||
if (!AudioManager.instance) {
|
if (!AudioManager.instance) {
|
||||||
@@ -60,16 +62,19 @@ class AudioManager {
|
|||||||
analyser.fftSize = fftSize
|
analyser.fftSize = fftSize
|
||||||
analyser.smoothingTimeConstant = 0.6
|
analyser.smoothingTimeConstant = 0.6
|
||||||
|
|
||||||
// 创建增益节点作为中介,避免直接断开主音频链
|
// 复用每个 audioElement 的分流器:source -> splitter -> destination
|
||||||
const gainNode = context.createGain()
|
let splitter = this.splitters.get(audioElement)
|
||||||
gainNode.gain.value = 1.0
|
if (!splitter) {
|
||||||
|
splitter = context.createGain()
|
||||||
|
splitter.gain.value = 1.0
|
||||||
|
// 仅第一次建立主链路,不要断开已有连接,避免累积
|
||||||
|
source.connect(splitter)
|
||||||
|
splitter.connect(context.destination)
|
||||||
|
this.splitters.set(audioElement, splitter)
|
||||||
|
}
|
||||||
|
|
||||||
// 连接:source -> gainNode -> analyser
|
// 将分析器挂到分流器上,不影响主链路
|
||||||
// -> destination (保持音频播放)
|
splitter.connect(analyser)
|
||||||
source.disconnect() // 先断开所有连接
|
|
||||||
source.connect(gainNode)
|
|
||||||
gainNode.connect(context.destination) // 确保音频继续播放
|
|
||||||
gainNode.connect(analyser) // 连接到分析器
|
|
||||||
|
|
||||||
// 存储分析器引用
|
// 存储分析器引用
|
||||||
this.analysers.set(id, analyser)
|
this.analysers.set(id, analyser)
|
||||||
@@ -104,6 +109,15 @@ class AudioManager {
|
|||||||
context.close()
|
context.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 断开并移除分流器
|
||||||
|
const splitter = this.splitters.get(audioElement)
|
||||||
|
if (splitter) {
|
||||||
|
try {
|
||||||
|
splitter.disconnect()
|
||||||
|
} catch {}
|
||||||
|
this.splitters.delete(audioElement)
|
||||||
|
}
|
||||||
|
|
||||||
this.audioSources.delete(audioElement)
|
this.audioSources.delete(audioElement)
|
||||||
this.audioContexts.delete(audioElement)
|
this.audioContexts.delete(audioElement)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user