Files
BetterLyrics/BetterLyrics.WinUI3/BetterLyrics.WinUI3/ViewModels/LyricsRendererViewModel/LyricsRendererViewModel.cs

427 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 2025/6/23 by Zhe Fang
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Events;
using BetterLyrics.WinUI3.Helper;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Models.Settings;
using BetterLyrics.WinUI3.Services;
using BetterLyrics.WinUI3.Services.LastFMService;
using BetterLyrics.WinUI3.Services.LibWatcherService;
using BetterLyrics.WinUI3.Services.LiveStatesService;
using BetterLyrics.WinUI3.Services.LyricsSearchService;
using BetterLyrics.WinUI3.Services.MediaSessionsService;
using BetterLyrics.WinUI3.Services.SettingsService;
using BetterLyrics.WinUI3.Services.TranslateService;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.WinUI;
using Microsoft.Extensions.Logging;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Text;
using Microsoft.UI;
using Microsoft.UI.Text;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Windows.Graphics.Imaging;
using Windows.UI;
namespace BetterLyrics.WinUI3.ViewModels.LyricsRendererViewModel
{
public partial class LyricsRendererViewModel : BaseViewModel
{
[ObservableProperty]
public partial AppSettings AppSettings { get; set; }
private bool _isLastFMTrackEnabled = false;
private bool _isLastFMTracked = false;
private TimeSpan _totalPlayingTime = TimeSpan.Zero;
private TimeSpan _elapsedTime = TimeSpan.Zero;
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial TimeSpan TotalTime { get; set; } = TimeSpan.Zero;
private TimeSpan _positionOffset = TimeSpan.Zero;
private int _songDurationMs = (int)TimeSpan.FromMinutes(99).TotalMilliseconds;
private Stopwatch? _drawFrameStopwatch;
private int _drawFrameCount = 0;
private int _displayedDrawFrameCount = 0;
private Queue<SoftwareBitmap?> _cachedAlbumArtSwBitmaps = [];
private SoftwareBitmap? _lastAlbumArtSwBitmap = null;
private SoftwareBitmap? _albumArtSwBitmap = null;
private CanvasBitmap? _lastAlbumArtCanvasBitmap = null;
private CanvasBitmap? _albumArtCanvasBitmap = null;
private CanvasRenderTarget? _albumArtBgRenderTarget;
private CanvasRenderTarget? _albumArtRenderTarget;
private CanvasBitmap? _coverAcrylicNoiseCanvasBitmap = null;
private double _albumArtSize = 0f;
private string? _lastSongTitle;
private string? _songTitle;
private string? _lastSongArtist;
private string? _songArtist;
private double _canvasWidth = 0f;
private double _canvasHeight = 0f;
private readonly double _defaultScale = 0.75f;
private readonly double _highlightedScale = 1.0f;
private readonly double _coverRotateBaseSpeed = 0.003f;
private double _rotateAngle = 0f;
private double _canvasTargetYScrollOffset = 0;
private double _maxLyricsWidth = 0f;
private readonly ISettingsService _settingsService;
private readonly IMediaSessionsService _mediaSessionsService;
private readonly ILastFMService _lastFMService;
private readonly ILiveStatesService _liveStatesService;
private readonly ILogger _logger;
private readonly double _leftMargin = 36f;
private readonly double _middleMargin = 36f;
private readonly double _rightMargin = 36f;
private readonly double _topMargin = 36f;
private readonly double _bottomMargin = 36f;
private Color _adaptiveGrayedFontColor = Colors.Transparent;
private Color? _adaptiveColoredFontColor = null;
private Color _albumArtLightAccentColor = Colors.Transparent;
private Color _albumArtDarkAccentColor = Colors.Transparent;
private Color _environmentalColor = Colors.Transparent;
private Color _grayedEnvironmentalColor = Colors.Transparent;
private Color _lightColor = Colors.White;
private Color _darkColor = Colors.Black;
private Color _bgFontColor;
private Color _fgFontColor;
private Color _strokeFontColor;
private int _playingLineIndex = -1;
private int _startVisibleLineIndex = -1;
private int _endVisibleLineIndex = -1;
private bool _isDebugOverlayEnabled = false;
[ObservableProperty]
public partial bool IsPlaying { get; set; } = false;
private bool _isLyricsWindowLocked = false;
private bool _isMouseWithinWindow = false;
private bool _isLayoutChanged = true;
private int _timelineSyncThreshold = 0;
private CanvasTextFormat _lyricsTextFormat = new()
{
HorizontalAlignment = CanvasHorizontalAlignment.Left,
VerticalAlignment = CanvasVerticalAlignment.Top,
FontSize = 12,
};
private CanvasTextFormat _titleTextFormat = new()
{
FontSize = 18,
FontWeight = FontWeights.Bold,
HorizontalAlignment = CanvasHorizontalAlignment.Left,
WordWrapping = CanvasWordWrapping.NoWrap,
TrimmingSign = CanvasTrimmingSign.Ellipsis,
TrimmingGranularity = CanvasTextTrimmingGranularity.Character,
};
private CanvasTextFormat _artistTextFormat = new()
{
FontSize = 16,
FontWeight = FontWeights.Bold,
HorizontalAlignment = CanvasHorizontalAlignment.Left,
WordWrapping = CanvasWordWrapping.NoWrap,
TrimmingSign = CanvasTrimmingSign.Ellipsis,
TrimmingGranularity = CanvasTextTrimmingGranularity.Character,
};
private CanvasTextFormat _debugTextFormat = new()
{
FontSize = 12,
FontWeight = FontWeights.ExtraBlack,
};
private LyricsLayoutOrientation _lyricsLayoutOrientation;
[ObservableProperty]
public partial SongInfo? SongInfo { get; set; }
[ObservableProperty]
[NotifyPropertyChangedRecipients]
public partial ElementTheme ThemeTypeSent { get; set; }
public LyricsRendererViewModel(
ISettingsService settingsService,
IMediaSessionsService mediaSessionsService,
ILastFMService lastFMService,
ILiveStatesService liveStatesService)
{
_settingsService = settingsService;
_mediaSessionsService = mediaSessionsService;
_liveStatesService = liveStatesService;
_lastFMService = lastFMService;
_logger = Ioc.Default.GetRequiredService<ILogger<LyricsRendererViewModel>>();
AppSettings = _settingsService.AppSettings;
_titleTextFormat.HorizontalAlignment = _artistTextFormat.HorizontalAlignment = _settingsService.AppSettings.AlbumArtLayoutSettings.SongInfoAlignmentType.ToCanvasHorizontalAlignment();
_timelineSyncThreshold = 0;
_mediaSessionsService.IsPlayingChanged += MediaSessionsService_IsPlayingChanged;
_mediaSessionsService.SongInfoChanged += MediaSessionsService_SongInfoChanged;
_mediaSessionsService.AlbumArtChanged += MediaSessionsService_AlbumArtChangedChanged;
_mediaSessionsService.LyricsChanged += MediaSessionsService_LyricsChanged;
_mediaSessionsService.TimelineChanged += MediaSessionsService_TimelineChanged;
IsPlaying = _mediaSessionsService.IsPlaying;
UpdateColorConfig();
}
private void MediaSessionsService_LyricsChanged(object? sender, LyricsChangedEventArgs e)
{
_isLayoutChanged = true;
}
private int GetCurrentPlayingLineIndex()
{
var totalMs = TotalTime.TotalMilliseconds + _positionOffset.TotalMilliseconds;
if (totalMs < _mediaSessionsService.CurrentLyricsData?.LyricsLines.FirstOrDefault()?.StartMs) return 0;
for (int i = 0; i < _mediaSessionsService.CurrentLyricsData?.LyricsLines.Count; i++)
{
var line = _mediaSessionsService.CurrentLyricsData?.LyricsLines.ElementAtOrDefault(i);
if (line == null) continue;
var nextLine = _mediaSessionsService.CurrentLyricsData?.LyricsLines.ElementAtOrDefault(i + 1);
if (nextLine != null && line.StartMs <= totalMs && totalMs < nextLine.StartMs)
{
return i;
}
else if (nextLine == null && line.StartMs <= totalMs)
{
return i;
}
}
return GetMaxLyricsLineIndexBoundaries().Item2;
}
private void GetLinePlayingProgress(int lineIndex, out int charStartIndex, out int charLength, out double charProgress)
{
charStartIndex = 0;
charLength = 0;
charProgress = 0f;
var line = _mediaSessionsService.CurrentLyricsData?.LyricsLines.ElementAtOrDefault(lineIndex);
if (line == null) return;
var nextLine = _mediaSessionsService.CurrentLyricsData?.LyricsLines.ElementAtOrDefault(lineIndex + 1);
int lineEndMs;
if (line.EndMs != null) lineEndMs = line.EndMs.Value;
else if (nextLine != null) lineEndMs = nextLine.StartMs;
else lineEndMs = _songDurationMs;
double now = (double)TotalTime.TotalMilliseconds + (double)_positionOffset.TotalMilliseconds;
// 1. 还没到本句
if (now < line.StartMs)
{
return;
}
// 2. 已经超过本句
if (now > lineEndMs)
{
charProgress = 1f;
charStartIndex = line.OriginalText.Length - 1;
charLength = 1;
return;
}
// 3. 有逐字时间轴
if (line.LyricsChars != null && line.LyricsChars.Count > 1)
{
int charTimingsCount = line.LyricsChars.Count;
for (int i = 0; i < charTimingsCount; i++)
{
var timing = line.LyricsChars[i];
var nextTiming = line.LyricsChars.ElementAtOrDefault(i + 1);
int timingEndMs;
if (timing.EndMs != null) timingEndMs = timing.EndMs.Value;
else if (nextTiming != null) timingEndMs = nextTiming.StartMs;
else timingEndMs = lineEndMs;
charStartIndex = timing.StartIndex;
charLength = timing.Text.Length;
// 当前时间在某个字的高亮区间
if (now >= timing.StartMs && now <= timingEndMs)
{
if (timingEndMs != timing.StartMs)
{
charProgress = (now - timing.StartMs) / (timingEndMs - timing.StartMs);
}
else
{
charProgress = 0f;
}
return;
}
else if (now > timingEndMs && (nextTiming == null || now < nextTiming?.StartMs))
{
charProgress = 1f;
return;
}
}
}
else
{
// 没有逐字时间轴,均匀分配每个字的高亮时间
int textLength = line.OriginalText.Length;
if (textLength == 0) return;
double lineProgress = (now - line.StartMs) / (lineEndMs - line.StartMs);
lineProgress = Math.Clamp(lineProgress, 0f, 1f);
// 计算当前高亮到第几个字
double charFloatIndex = lineProgress * textLength;
int charIndex = (int)charFloatIndex;
charStartIndex = Math.Clamp(charIndex, 0, textLength - 1);
charLength = 1;
// 当前字的进度0~1
charProgress = charFloatIndex - charIndex;
}
}
private Tuple<int, int> GetMaxLyricsLineIndexBoundaries()
{
if (
SongInfo == null
|| _mediaSessionsService.CurrentLyricsData == null
|| _mediaSessionsService.CurrentLyricsData.LyricsLines.Count == 0
)
{
return new Tuple<int, int>(-1, -1);
}
return new Tuple<int, int>(0, _mediaSessionsService.CurrentLyricsData.LyricsLines.Count - 1);
}
private void MediaSessionsService_IsPlayingChanged(object? sender, IsPlayingChangedEventArgs e)
{
IsPlaying = e.IsPlaying;
}
private void MediaSessionsService_TimelineChanged(object? sender, TimelineChangedEventArgs e)
{
var diff = Math.Abs(TotalTime.TotalMilliseconds - e.Position.TotalMilliseconds);
if (diff >= _timelineSyncThreshold)
{
TotalTime = e.Position;
if (TotalTime.TotalSeconds <= 1)
{
_totalPlayingTime = TimeSpan.Zero;
_isLastFMTracked = false;
}
}
// 大跨度,刷新布局,避免歌词不显示
if (diff >= _timelineSyncThreshold + 5000)
{
_isLayoutChanged = true;
}
}
private void MediaSessionsService_SongInfoChanged(object? sender, SongInfoChangedEventArgs e)
{
SongInfo = e.SongInfo;
UpdateTimelineSyncThreshold();
UpdatePositionOffset();
UpdateIsLastFMTrackEnabled();
if (SongInfo?.Title != _songTitle || SongInfo?.Artist != _songArtist)
{
_lastSongTitle = _songTitle;
_songTitle = SongInfo?.Title;
_lastSongArtist = _songArtist;
_songArtist = SongInfo?.Artist;
_songDurationMs = (int)(SongInfo?.DurationMs ?? TimeSpan.FromMinutes(99).TotalMilliseconds);
_songInfoOpacityTransition.Reset(0f);
_songInfoOpacityTransition.StartTransition(1f);
TotalTime = TimeSpan.Zero;
// 处理 Last.fm 追踪
_totalPlayingTime = TimeSpan.Zero;
_isLastFMTracked = false;
}
}
private void MediaSessionsService_AlbumArtChangedChanged(object? sender, AlbumArtChangedEventArgs e)
{
if (e.AlbumArtSwBitmap != _albumArtSwBitmap)
{
_cachedAlbumArtSwBitmaps.Append(_albumArtSwBitmap);
_lastAlbumArtSwBitmap = _albumArtSwBitmap;
if (_cachedAlbumArtSwBitmaps.Count > 2)
{
_cachedAlbumArtSwBitmaps.Dequeue()?.Dispose();
}
_cachedAlbumArtSwBitmaps.Append(e.AlbumArtSwBitmap);
_albumArtSwBitmap = e.AlbumArtSwBitmap;
if (_cachedAlbumArtSwBitmaps.Count > 2)
{
_cachedAlbumArtSwBitmaps.Dequeue()?.Dispose();
}
_albumArtChanged = true;
_albumArtLightAccentColor = e.AlbumArtLightAccentColor ?? Colors.Transparent;
_albumArtDarkAccentColor = e.AlbumArtDarkAccentColor ?? Colors.Transparent;
UpdateColorConfig();
}
else
{
e.AlbumArtSwBitmap?.Dispose();
}
}
}
}