XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

HaloPixelToolBox

【WinUI3】基于USB HID通讯的花再音响工具箱

公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/HaloPixelToolBox

add ambient RGB sync with album art

f8f953b
Bartholomew Ho <qiangqiang101@hotmail.com>
提交于

代码差异

16 个文件 +648 -57
Added HaloPixelToolBox/HaloPixelToolBox.Core/Models/Display/HaloPixelColor.cs +14 -0
@@ -0,0 +1,14 @@
1 namespace HaloPixelToolBox.Core.Models.Display;
2
3 /// <summary>
4 /// 字幕颜色配置。当前设备文本协议未公开颜色字段,先作为上层样式参数保留。
5 /// </summary>
6 public record HaloPixelColor(byte Red, byte Green, byte Blue)
7 {
8 public static HaloPixelColor White { get; } = new(255, 255, 255);
9 public static HaloPixelColor RedColor { get; } = new(255, 64, 64);
10 public static HaloPixelColor GreenColor { get; } = new(64, 255, 128);
11 public static HaloPixelColor BlueColor { get; } = new(64, 160, 255);
12
13 public string ToHex() => $"#{Red:X2}{Green:X2}{Blue:X2}";
14 }
Added HaloPixelToolBox/HaloPixelToolBox.Core/Models/Lighting/AmbientLightBrightness.cs +8 -0
@@ -0,0 +1,8 @@
1 namespace HaloPixelToolBox.Core.Models.Lighting;
2
3 public enum AmbientLightBrightness
4 {
5 Low = 1,
6 Medium = 2,
7 High = 3
8 }
Added HaloPixelToolBox/HaloPixelToolBox.Core/Models/Lighting/AmbientLightEffect.cs +11 -0
@@ -0,0 +1,11 @@
1 namespace HaloPixelToolBox.Core.Models.Lighting;
2
3 public enum AmbientLightEffect
4 {
5 Breathing = 1,
6 ColorTide = 2,
7 Static = 3,
8 Ripple = 4,
9 Flow = 5,
10 Dynamic = 6
11 }
Added HaloPixelToolBox/HaloPixelToolBox.Core/Models/Lighting/AmbientLightOptions.cs +16 -0
@@ -0,0 +1,16 @@
1 using HaloPixelToolBox.Core.Models.Display;
2
3 namespace HaloPixelToolBox.Core.Models.Lighting;
4
5 public sealed class AmbientLightOptions
6 {
7 public bool IsEnabled { get; set; } = true;
8
9 public AmbientLightEffect Effect { get; set; } = AmbientLightEffect.Static;
10
11 public HaloPixelColor Color { get; set; } = HaloPixelColor.White;
12
13 public AmbientLightBrightness Brightness { get; set; } = AmbientLightBrightness.High;
14
15 public byte Speed { get; set; } = 10;
16 }
Added HaloPixelToolBox/HaloPixelToolBox.Core/Models/Scenes/PixelSceneUploadProgress.cs +12 -0
@@ -0,0 +1,12 @@
1 namespace HaloPixelToolBox.Core.Models.Scenes;
2
3 /// <summary>
4 /// 像素屏场景资源上传进度。
5 /// 进度按已发送的 .bin 资源字节数计算,后续如果改为更底层的流式上传,可继续扩展传输阶段字段。
6 /// </summary>
7 public sealed record PixelSceneUploadProgress(long SentBytes, long TotalBytes, string Stage)
8 {
9 public double Ratio => TotalBytes <= 0 ? 0 : Math.Clamp(SentBytes / (double)TotalBytes, 0, 1);
10
11 public double Percent => Ratio * 100;
12 }
Modified HaloPixelToolBox/HaloPixelToolBox.Core/Utilities/HaloPixelDevice.cs +24 -1
@@ -1,4 +1,6 @@
1 using HaloPixelToolBox.Core.Models;
1 using HaloPixelToolBox.Core.Models;
2 using HaloPixelToolBox.Core.Models.Lighting;
3 using HaloPixelToolBox.Core.Models.Display;
2 4 using HidSharp;
3 5 using XFEExtension.NetCore.StringExtension;
4 6
@@ -97,4 +99,25 @@ public partial class HaloPixelDevice
97 99 catch { }
98 100 }
99 101 }
102
103 public void SetAmbientLight(AmbientLightOptions options)
104 {
105 using var stream = CurrentDevice?.Open();
106 stream?.Write(HidPacketBuilder.BuildAmbientLight(options));
107 stream?.Close();
108 }
109
110 public void SetAmbientLightEnabled(bool enabled)
111 {
112 using var stream = CurrentDevice?.Open();
113 stream?.Write(HidPacketBuilder.BuildAmbientLightPower(enabled));
114 stream?.Close();
115 }
116
117 public void SetPixelScreenColor(HaloPixelColor color)
118 {
119 using var stream = CurrentDevice?.Open();
120 stream?.Write(HidPacketBuilder.BuildPixelScreenColor(color));
121 stream?.Close();
122 }
100 123 }
Modified HaloPixelToolBox/HaloPixelToolBox.Core/Utilities/HidPacketBuilder.cs +138 -22
@@ -1,4 +1,6 @@
1 using HaloPixelToolBox.Core.Models;
1 using HaloPixelToolBox.Core.Models;
2 using HaloPixelToolBox.Core.Models.Lighting;
3 using HaloPixelToolBox.Core.Models.Display;
2 4 using System.Text;
3 5
4 6 namespace HaloPixelToolBox.Core.Utilities;
@@ -55,35 +57,75 @@ public class HidPacketBuilder
55 57 return Build(list);
56 58 }
57 59
60 /// <summary>
61 /// 转换布局枚举到对应的 HID 包字节数组
62 public static (byte R, byte G, byte B) CurrentColor { get; set; } = (0xf0, 0xb4, 0xc8);
63
58 64 /// <summary>
59 65 /// 转换布局枚举到对应的 HID 包字节数组
60 66 /// </summary>
61 67 /// <param name="haloPixelTextLayout"></param>
62 68 /// <returns></returns>
63 public static byte[] ConvertLayout(HaloPixelTextLayout haloPixelTextLayout) => haloPixelTextLayout switch
69 public static byte[] ConvertLayout(HaloPixelTextLayout haloPixelTextLayout)
64 70 {
65 HaloPixelTextLayout.Left => [0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09, 0x01, 0xf0, 0xb4, 0xc8, 0x00, 0x02, 0x00, 0x00, 0xff, 0xfc, 0x00],
66 HaloPixelTextLayout.Center => [0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09, 0x01, 0xf0, 0xb4, 0xc8, 0x00, 0x02, 0x00, 0x01, 0xff, 0xfd, 0x00],
67 HaloPixelTextLayout.Right => [0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09, 0x01, 0xf0, 0xb4, 0xc8, 0x00, 0x02, 0x00, 0x02, 0xff, 0xfe, 0x00],
68 HaloPixelTextLayout.Stretch => [0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09, 0x01, 0xf0, 0xb4, 0xc8, 0x00, 0x02, 0x00, 0x03, 0xff, 0xff, 0x00],
69 HaloPixelTextLayout.ScrollLeftToRight => [0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09, 0x01, 0xf0, 0xb4, 0xc8, 0x00, 0x02, 0x01, 0x00, 0xff, 0xfd, 0x00],
70 HaloPixelTextLayout.ScrollRightToLeft => [0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09, 0x01, 0xf0, 0xb4, 0xc8, 0x00, 0x02, 0x01, 0x01, 0xff, 0xfe, 0x00],
71 _ => [],
72 };
71 byte[] payload = haloPixelTextLayout switch
72 {
73 HaloPixelTextLayout.Left => [0x01, CurrentColor.R, CurrentColor.G, CurrentColor.B, 0x00, 0x02, 0x00, 0x00, 0xff],
74 HaloPixelTextLayout.Center => [0x01, CurrentColor.R, CurrentColor.G, CurrentColor.B, 0x00, 0x02, 0x00, 0x01, 0xff],
75 HaloPixelTextLayout.Right => [0x01, CurrentColor.R, CurrentColor.G, CurrentColor.B, 0x00, 0x02, 0x00, 0x02, 0xff],
76 HaloPixelTextLayout.Stretch => [0x01, CurrentColor.R, CurrentColor.G, CurrentColor.B, 0x00, 0x02, 0x00, 0x03, 0xff],
77 HaloPixelTextLayout.ScrollLeftToRight => [0x01, CurrentColor.R, CurrentColor.G, CurrentColor.B, 0x00, 0x02, 0x01, 0x00, 0xff],
78 HaloPixelTextLayout.ScrollRightToLeft => [0x01, CurrentColor.R, CurrentColor.G, CurrentColor.B, 0x00, 0x02, 0x01, 0x01, 0xff],
79 _ => [],
80 };
81
82 if (payload.Length == 0) return [];
83
84 var list = new List<byte> { 0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09 };
85 list.AddRange(payload);
86
87 int acc = 124;
88 foreach (var b in payload)
89 {
90 acc += b + 2;
91 }
92 list.Add((byte)(acc % 256));
93 list.Add(0x00);
73 94
74 public static byte[] ConvertUIModel(HaloPixelUIModel haloPixelUIModel) => haloPixelUIModel switch
95 return Build(list);
96 }
97
98 public static byte[] ConvertUIModel(HaloPixelUIModel haloPixelUIModel)
75 99 {
76 HaloPixelUIModel.Clock => [0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09, 0x02, 0xf0, 0xb4, 0xc8, 0x00, 0x01, 0x00, 0xff, 0xff, 0xfb, 0x00],
77 HaloPixelUIModel.Game => [0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09, 0x02, 0xf0, 0xb4, 0xc8, 0x00, 0x01, 0x01, 0xff, 0xff, 0xfc, 0x00],
78 HaloPixelUIModel.Work => [0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09, 0x02, 0xf0, 0xb4, 0xc8, 0x00, 0x01, 0x02, 0xff, 0xff, 0xfd, 0x00],
79 HaloPixelUIModel.Read => [0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09, 0x02, 0xf0, 0xb4, 0xc8, 0x00, 0x01, 0x03, 0xff, 0xff, 0xfe, 0x00],
80 HaloPixelUIModel.Cats => [0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09, 0x02, 0xf0, 0xb4, 0xc8, 0x00, 0x01, 0x04, 0xff, 0xff, 0xff, 0x00],
81 HaloPixelUIModel.Dogs => [0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09, 0x02, 0xf0, 0xb4, 0xc8, 0x00, 0x01, 0x05, 0xff, 0xff, 0x00, 0x00],
82 HaloPixelUIModel.Memes => [0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09, 0x02, 0xf0, 0xb4, 0xc8, 0x00, 0x01, 0x06, 0xff, 0xff, 0x01, 0x00],
83 HaloPixelUIModel.Cyber => [0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09, 0x02, 0xf0, 0xb4, 0xc8, 0x00, 0x01, 0x07, 0xff, 0xff, 0x02, 0x00],
84 HaloPixelUIModel.Waves => [0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09, 0x02, 0xf0, 0xb4, 0xc8, 0x00, 0x01, 0x08, 0xff, 0xff, 0x03, 0x00],
85 _ => [],
86 };
100 byte[] payload = haloPixelUIModel switch
101 {
102 HaloPixelUIModel.Clock => [0x02, CurrentColor.R, CurrentColor.G, CurrentColor.B, 0x00, 0x01, 0x00, 0xff, 0xff],
103 HaloPixelUIModel.Game => [0x02, CurrentColor.R, CurrentColor.G, CurrentColor.B, 0x00, 0x01, 0x01, 0xff, 0xff],
104 HaloPixelUIModel.Work => [0x02, CurrentColor.R, CurrentColor.G, CurrentColor.B, 0x00, 0x01, 0x02, 0xff, 0xff],
105 HaloPixelUIModel.Read => [0x02, CurrentColor.R, CurrentColor.G, CurrentColor.B, 0x00, 0x01, 0x03, 0xff, 0xff],
106 HaloPixelUIModel.Cats => [0x02, CurrentColor.R, CurrentColor.G, CurrentColor.B, 0x00, 0x01, 0x04, 0xff, 0xff],
107 HaloPixelUIModel.Dogs => [0x02, CurrentColor.R, CurrentColor.G, CurrentColor.B, 0x00, 0x01, 0x05, 0xff, 0xff],
108 HaloPixelUIModel.Memes => [0x02, CurrentColor.R, CurrentColor.G, CurrentColor.B, 0x00, 0x01, 0x06, 0xff, 0xff],
109 HaloPixelUIModel.Cyber => [0x02, CurrentColor.R, CurrentColor.G, CurrentColor.B, 0x00, 0x01, 0x07, 0xff, 0xff],
110 HaloPixelUIModel.Waves => [0x02, CurrentColor.R, CurrentColor.G, CurrentColor.B, 0x00, 0x01, 0x08, 0xff, 0xff],
111 _ => [],
112 };
113
114 if (payload.Length == 0) return [];
115
116 var list = new List<byte> { 0x2e, 0xaa, 0xec, 0xef, 0x00, 0x09 };
117 list.AddRange(payload);
118
119 int acc = 124;
120 foreach (var b in payload)
121 {
122 acc += b + 2;
123 }
124 list.Add((byte)(acc % 256));
125 list.Add(0x00);
126
127 return Build(list);
128 }
87 129
88 130 /// <summary>
89 131 /// 构造 HID 协议包
@@ -122,4 +164,78 @@ public class HidPacketBuilder
122 164 {
123 165 return BitConverter.ToString(packet).Replace("-", "").ToLower();
124 166 }
167
168 /// <summary>
169 /// 构造像素屏主题颜色包。
170 /// 协议来自 LiLyric 的 RGBController.set_pixel_color:2E AA EC EF 00 04 03 + RGB + checksum。
171 /// </summary>
172 public static byte[] BuildPixelScreenColor(HaloPixelColor color)
173 => BuildEdifierPacket(0xef, [0x03, color.Red, color.Green, color.Blue]);
174
175 /// <summary>
176 /// 构造氛围灯效果包。
177 /// 协议来自 LiLyric 的 RGBController._set_light_color:2E AA EC 6B 00 07 13 + effect + RGB + brightness + speed + checksum。
178 /// </summary>
179 public static byte[] BuildAmbientLight(AmbientLightOptions options)
180 {
181 var speed = Math.Clamp(options.Speed, (byte)1, (byte)10);
182 byte[] payload =
183 [
184 0x13,
185 (byte)options.Effect,
186 options.Color.Red,
187 options.Color.Green,
188 options.Color.Blue,
189 ConvertAmbientBrightness(options.Brightness),
190 speed
191 ];
192
193 return BuildEdifierPacket(0x6b, payload);
194 }
195
196 /// <summary>
197 /// 构造氛围灯开关包。
198 /// 协议来自 TempoHub 的 mood_lighting_splice/set_device_light:B 字段 1=开启、0=关闭。
199 /// </summary>
200 public static byte[] BuildAmbientLightPower(bool enabled)
201 {
202 byte mode = enabled ? (byte)0x01 : (byte)0x00;
203 return BuildEdifierPacket(0x6b, [0x00, 0x00, 0x00, 0x00, mode, 0xff, 0xff]);
204 }
205
206 private static byte ConvertAmbientBrightness(AmbientLightBrightness brightness) => brightness switch
207 {
208 AmbientLightBrightness.Low => 0x14,
209 AmbientLightBrightness.Medium => 0x28,
210 AmbientLightBrightness.High => 0x3c,
211 _ => 0x3c
212 };
213
214 /// <summary>
215 /// 构造通用 Edifier HID 包:2E AA EC + 指令号 + payload 长度 + payload + 校验。
216 /// </summary>
217 public static byte[] BuildEdifierPacket(byte commandIndex, IReadOnlyCollection<byte> payload)
218 {
219 var packet = new List<byte>(6 + payload.Count + 1)
220 {
221 0x2e,
222 0xaa,
223 0xec,
224 commandIndex,
225 (byte)((payload.Count >> 8) & 0xff),
226 (byte)(payload.Count & 0xff)
227 };
228 packet.AddRange(payload);
229 packet.Add(CalculateEdifierChecksum(packet));
230 return Build(packet);
231 }
232
233 private static byte CalculateEdifierChecksum(IReadOnlyList<byte> packetWithoutChecksum)
234 {
235 var sum = 0;
236 for (var index = 1; index < packetWithoutChecksum.Count; index++)
237 sum += packetWithoutChecksum[index];
238
239 return (byte)(sum & 0xff);
240 }
125 241 }
Modified HaloPixelToolBox/HaloPixelToolBox/Profiles/CrossVersionProfiles/CloudMusicLyricsProfile.cs +13 -1
@@ -1,4 +1,4 @@
1 using HaloPixelToolBox.Core.Models;
1 using HaloPixelToolBox.Core.Models;
2 2 using XFEExtension.NetCore.AutoConfig;
3 3 using XFEExtension.NetCore.WinUIHelper.Utilities.Helper;
4 4
@@ -42,4 +42,16 @@ public partial class CloudMusicLyricsProfile : XFEProfile
42 42 /// </summary>
43 43 [ProfileProperty]
44 44 private HaloPixelUIModel defaultHaloPixelUIModel = HaloPixelUIModel.Clock;
45
46 /// <summary>
47 /// 是否开启屏幕颜色同步
48 /// </summary>
49 [ProfileProperty]
50 private bool enableScreenColorSync = false;
51
52 /// <summary>
53 /// 是否开启氛围灯颜色同步
54 /// </summary>
55 [ProfileProperty]
56 private bool enableAmbientColorSync = false;
45 57 }
Modified HaloPixelToolBox/HaloPixelToolBox/Profiles/CrossVersionProfiles/SpotifyLyricsProfile.cs +12 -0
@@ -37,4 +37,16 @@ public partial class SpotifyLyricsProfile : XFEProfile
37 37 /// </summary>
38 38 [ProfileProperty]
39 39 private HaloPixelUIModel defaultHaloPixelUIModel = HaloPixelUIModel.Clock;
40
41 /// <summary>
42 /// 是否开启屏幕颜色同步
43 /// </summary>
44 [ProfileProperty]
45 private bool enableScreenColorSync = false;
46
47 /// <summary>
48 /// 是否开启氛围灯颜色同步
49 /// </summary>
50 [ProfileProperty]
51 private bool enableAmbientColorSync = false;
40 52 }
Modified HaloPixelToolBox/HaloPixelToolBox/Utilities/SpotifyLyricsReader.cs +60 -0
@@ -119,6 +119,48 @@ public class SpotifyLyricsReader
119 119 private DateTimeOffset _songStartTime = DateTimeOffset.Now;
120 120 private bool _isSmtcSynced = false;
121 121
122 public (byte R, byte G, byte B)? CurrentAlbumColor { get; private set; }
123
124 public static async Task<(byte R, byte G, byte B)?> GetDominantColorAsync(Windows.Storage.Streams.IRandomAccessStreamReference thumbnail)
125 {
126 try
127 {
128 using var stream = await thumbnail.OpenReadAsync();
129 var decoder = await Windows.Graphics.Imaging.BitmapDecoder.CreateAsync(stream);
130 var transform = new Windows.Graphics.Imaging.BitmapTransform
131 {
132 ScaledWidth = 16,
133 ScaledHeight = 16,
134 InterpolationMode = Windows.Graphics.Imaging.BitmapInterpolationMode.Linear
135 };
136 var pixelData = await decoder.GetPixelDataAsync(
137 Windows.Graphics.Imaging.BitmapPixelFormat.Rgba8,
138 Windows.Graphics.Imaging.BitmapAlphaMode.Ignore,
139 transform,
140 Windows.Graphics.Imaging.ExifOrientationMode.IgnoreExifOrientation,
141 Windows.Graphics.Imaging.ColorManagementMode.ColorManageToSRgb
142 );
143 byte[] pixels = pixelData.DetachPixelData();
144 long rSum = 0, gSum = 0, bSum = 0;
145 int pixelCount = pixels.Length / 4;
146 for (int i = 0; i < pixels.Length; i += 4)
147 {
148 rSum += pixels[i];
149 gSum += pixels[i + 1];
150 bSum += pixels[i + 2];
151 }
152 if (pixelCount > 0)
153 {
154 return ((byte)(rSum / pixelCount), (byte)(gSum / pixelCount), (byte)(bSum / pixelCount));
155 }
156 }
157 catch (Exception ex)
158 {
159 Console.WriteLine($"[ERROR] GetDominantColorAsync failed: {ex.Message}\n{ex.StackTrace}");
160 }
161 return null;
162 }
163
122 164 public string CurrentTitle => _currentTitle;
123 165 public string CurrentArtist => _currentArtist;
124 166
@@ -312,6 +354,24 @@ public class SpotifyLyricsReader
312 354 _currentDuration = timeline?.EndTime.TotalSeconds ?? 0;
313 355 _isSmtcSynced = true;
314 356 Console.WriteLine($"SMTC Sync Achieved for: {_currentArtist} - {_currentTitle} (Duration: {_currentDuration}s)");
357
358 if (media.Thumbnail != null)
359 {
360 var color = await GetDominantColorAsync(media.Thumbnail);
361 if (color.HasValue)
362 {
363 CurrentAlbumColor = color.Value;
364 Console.WriteLine($"Extracted album dominant color: RGB({color.Value.R}, {color.Value.G}, {color.Value.B})");
365 }
366 else
367 {
368 Console.WriteLine("GetDominantColorAsync returned null for the thumbnail.");
369 }
370 }
371 else
372 {
373 Console.WriteLine("media.Thumbnail is null for this track.");
374 }
315 375 }
316 376 else
317 377 {
Modified HaloPixelToolBox/HaloPixelToolBox/ViewModels/CloudMusicLyricsToolPageViewModel.cs +199 -15
@@ -1,6 +1,7 @@
1 using CommunityToolkit.Mvvm.ComponentModel;
1 using CommunityToolkit.Mvvm.ComponentModel;
2 2 using HaloPixelToolBox.Core.Utilities;
3 3 using HaloPixelToolBox.Profiles.CrossVersionProfiles;
4 using HaloPixelToolBox.Utilities;
4 5 using System.Diagnostics;
5 6 using XFEExtension.NetCore.StringExtension;
6 7 using XFEExtension.NetCore.WinUIHelper.Implements;
@@ -26,6 +27,10 @@ public partial class CloudMusicLyricsToolPageViewModel : ServiceBaseViewModelBas
26 27 [ObservableProperty]
27 28 private int switchBackTimeout = CloudMusicLyricsProfile.SwitchBackTimeout;
28 29 [ObservableProperty]
30 private bool enableScreenColorSync = CloudMusicLyricsProfile.EnableScreenColorSync;
31 [ObservableProperty]
32 private bool enableAmbientColorSync = CloudMusicLyricsProfile.EnableAmbientColorSync;
33 [ObservableProperty]
29 34 private string cloudMusicVersion = string.Empty;
30 35 [ObservableProperty]
31 36 private string supportedVersion = CloudMusicLyricsReader.VersionResolverDictionary.Keys.FirstOrDefault() ?? string.Empty;
@@ -53,7 +58,19 @@ public partial class CloudMusicLyricsToolPageViewModel : ServiceBaseViewModelBas
53 58 }
54 59 }
55 60
56 partial void OnEnableCloudMusicLyricsChanged(bool value) => CloudMusicLyricsProfile.EnableCloudMusicLyrics = value;
61 private bool _forceRefresh;
62
63 public void OnNavigatedTo()
64 {
65 _forceRefresh = true;
66 }
67
68 partial void OnEnableCloudMusicLyricsChanged(bool value)
69 {
70 CloudMusicLyricsProfile.EnableCloudMusicLyrics = value;
71 if (value)
72 _forceRefresh = true;
73 }
57 74
58 75 partial void OnUseInputedAddressChanged(bool value)
59 76 {
@@ -70,9 +87,94 @@ public partial class CloudMusicLyricsToolPageViewModel : ServiceBaseViewModelBas
70 87 partial void OnSwitchBackWhenPauseChanged(bool value) => CloudMusicLyricsProfile.SwitchBackWhenPause = value;
71 88
72 89 partial void OnSwitchBackTimeoutChanged(int value) => CloudMusicLyricsProfile.SwitchBackTimeout = value;
90 partial void OnEnableScreenColorSyncChanged(bool value)
91 {
92 CloudMusicLyricsProfile.EnableScreenColorSync = value;
93 _forceRefresh = true;
94 }
95 partial void OnEnableAmbientColorSyncChanged(bool value)
96 {
97 CloudMusicLyricsProfile.EnableAmbientColorSync = value;
98 _forceRefresh = true;
99 }
100
101 private Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager? _smtcManager;
102 private Windows.Media.Control.GlobalSystemMediaTransportControlsSession? _cloudMusicSession;
103 public (byte R, byte G, byte B)? CurrentAlbumColor { get; private set; }
104
105 private async Task InitSmtcAsync()
106 {
107 try
108 {
109 _smtcManager = await Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager.RequestAsync();
110 UpdateCloudMusicSession();
111 _smtcManager.SessionsChanged += (s, e) => UpdateCloudMusicSession();
112 }
113 catch (Exception ex)
114 {
115 Console.WriteLine($"SMTC init failed: {ex.Message}");
116 }
117 }
118
119 private void UpdateCloudMusicSession()
120 {
121 if (_smtcManager == null) return;
122 var sessions = _smtcManager.GetSessions();
123 Windows.Media.Control.GlobalSystemMediaTransportControlsSession? targetSession = null;
124 foreach (var s in sessions)
125 {
126 if (s.SourceAppUserModelId.Contains("cloudmusic", StringComparison.OrdinalIgnoreCase))
127 {
128 targetSession = s;
129 break;
130 }
131 }
132
133 if (targetSession != _cloudMusicSession)
134 {
135 if (_cloudMusicSession != null)
136 {
137 _cloudMusicSession.MediaPropertiesChanged -= OnSmtcMediaPropertiesChanged;
138 }
139 _cloudMusicSession = targetSession;
140 if (_cloudMusicSession != null)
141 {
142 _cloudMusicSession.MediaPropertiesChanged += OnSmtcMediaPropertiesChanged;
143 _ = UpdateAlbumColorAsync();
144 }
145 }
146 }
147
148 private void OnSmtcMediaPropertiesChanged(Windows.Media.Control.GlobalSystemMediaTransportControlsSession sender, Windows.Media.Control.MediaPropertiesChangedEventArgs args)
149 {
150 _ = UpdateAlbumColorAsync();
151 }
152
153 private async Task UpdateAlbumColorAsync()
154 {
155 if (_cloudMusicSession == null) return;
156 try
157 {
158 var media = await _cloudMusicSession.TryGetMediaPropertiesAsync();
159 if (media?.Thumbnail != null)
160 {
161 var color = await SpotifyLyricsReader.GetDominantColorAsync(media.Thumbnail);
162 if (color.HasValue)
163 {
164 CurrentAlbumColor = color.Value;
165 Console.WriteLine($"CloudMusic extracted album color: RGB({color.Value.R}, {color.Value.G}, {color.Value.B})");
166 }
167 }
168 }
169 catch (Exception ex)
170 {
171 Console.WriteLine($"Failed to update CloudMusic album color: {ex.Message}");
172 }
173 }
73 174
74 175 public CloudMusicLyricsToolPageViewModel()
75 176 {
177 _ = InitSmtcAsync();
76 178 Console.WriteLine("初始化网易云歌词读取器");
77 179 Reader = new CloudMusicLyricsReader
78 180 {
@@ -149,7 +251,7 @@ public partial class CloudMusicLyricsToolPageViewModel : ServiceBaseViewModelBas
149 251 Console.WriteLine("等待花再设备...");
150 252 while (!DeviceReady)
151 253 await Task.Delay(500);
152 if (DeviceReady)
254 if (DeviceReady && EnableCloudMusicLyrics)
153 255 {
154 256 Console.WriteLine("花再设备已就绪,显示启动信息");
155 257 Device.SetTextLayout(Core.Models.HaloPixelTextLayout.Center);
@@ -168,28 +270,83 @@ public partial class CloudMusicLyricsToolPageViewModel : ServiceBaseViewModelBas
168 270 Console.WriteLine("[DEBUG]设备均在线,准备进入主循环");
169 271 string lastRead = string.Empty;
170 272 bool scrolled = false;
273 (byte R, byte G, byte B) lastScreenColor = (0, 0, 0);
274 (byte R, byte G, byte B) lastAmbientColor = (0, 0, 0);
171 275 while (true)
172 276 {
173 277 try
174 278 {
175 279 if (!DeviceReady || !CloudMusicReady || !EnableCloudMusicLyrics)
176 280 break;
177 if (Reader.TryReadLyrics(out var lyrics) && lastRead != lyrics)
281
282 var albumColor = CurrentAlbumColor;
283
284 // 1. Determine screen text color
285 (byte R, byte G, byte B) screenColor = ((byte)0xf0, (byte)0xb4, (byte)0xc8);
286 if (EnableScreenColorSync && albumColor.HasValue)
287 {
288 screenColor = albumColor.Value;
289 }
290
291 // 2. Determine ambient backlight color
292 (byte R, byte G, byte B) ambientColor = ((byte)0xf0, (byte)0xb4, (byte)0xc8);
293 if (EnableAmbientColorSync && albumColor.HasValue)
178 294 {
179 Console.WriteLine($"已读取到歌词:{lyrics}");
180 lastRead = lyrics;
181 isClockUI = false;
182 time = 0;
183 if (scrolled)
295 ambientColor = albumColor.Value;
296 }
297
298 bool colorChanged = lastScreenColor != screenColor || lastAmbientColor != ambientColor;
299 bool lyricsChanged = Reader.TryReadLyrics(out var lyrics) && lastRead != lyrics;
300
301 if (lyricsChanged || colorChanged || _forceRefresh)
302 {
303 _forceRefresh = false;
304 lastScreenColor = screenColor;
305 lastAmbientColor = ambientColor;
306 HidPacketBuilder.CurrentColor = screenColor;
307
308 try
184 309 {
185 Device.ShowText(string.Empty);
186 await Task.Delay(100);
187 scrolled = false;
310 Device.SetAmbientLight(new Core.Models.Lighting.AmbientLightOptions
311 {
312 Effect = Core.Models.Lighting.AmbientLightEffect.Static,
313 Color = new Core.Models.Display.HaloPixelColor(ambientColor.R, ambientColor.G, ambientColor.B),
314 Brightness = Core.Models.Lighting.AmbientLightBrightness.High,
315 Speed = 5
316 });
188 317 }
318 catch (Exception ex)
319 {
320 Console.WriteLine($"[ERROR] SetAmbientLight failed: {ex.Message}");
321 }
322
323 try
324 {
325 Device.SetPixelScreenColor(new Core.Models.Display.HaloPixelColor(screenColor.R, screenColor.G, screenColor.B));
326 }
327 catch (Exception ex)
328 {
329 Console.WriteLine($"[ERROR] SetPixelScreenColor failed: {ex.Message}");
330 }
331
332 if (lyricsChanged)
333 {
334 Console.WriteLine($"已读取到歌词:{lyrics}");
335 lastRead = lyrics;
336 isClockUI = false;
337 time = 0;
338 if (scrolled)
339 {
340 Device.ShowText(string.Empty);
341 await Task.Delay(100);
342 scrolled = false;
343 }
344 }
345
189 346 Device.SetTextLayout(CloudMusicLyricsProfile.DefaultHaloPixelTextLayout);
190 Device.ShowText(lyrics);
191 Debug.WriteLine(lyrics.DisplayLength());
192 if (lyrics.DisplayLength() > 30)
347 Device.ShowText(lastRead);
348
349 if (lyricsChanged && lastRead.DisplayLength() > 30)
193 350 {
194 351 scrolled = true;
195 352 await Task.Delay(500);
@@ -201,6 +358,33 @@ public partial class CloudMusicLyricsToolPageViewModel : ServiceBaseViewModelBas
201 358 if (!isClockUI && time >= CloudMusicLyricsProfile.SwitchBackTimeout * 1000)
202 359 {
203 360 isClockUI = true;
361 (byte R, byte G, byte B) finalScreenColor = ((byte)0xf0, (byte)0xb4, (byte)0xc8);
362 if (EnableScreenColorSync && CurrentAlbumColor.HasValue)
363 {
364 finalScreenColor = CurrentAlbumColor.Value;
365 }
366 (byte R, byte G, byte B) finalAmbientColor = ((byte)0xf0, (byte)0xb4, (byte)0xc8);
367 if (EnableAmbientColorSync && CurrentAlbumColor.HasValue)
368 {
369 finalAmbientColor = CurrentAlbumColor.Value;
370 }
371 HidPacketBuilder.CurrentColor = finalScreenColor;
372 try
373 {
374 Device.SetAmbientLight(new Core.Models.Lighting.AmbientLightOptions
375 {
376 Effect = Core.Models.Lighting.AmbientLightEffect.Static,
377 Color = new Core.Models.Display.HaloPixelColor(finalAmbientColor.R, finalAmbientColor.G, finalAmbientColor.B),
378 Brightness = Core.Models.Lighting.AmbientLightBrightness.High,
379 Speed = 5
380 });
381 }
382 catch {}
383 try
384 {
385 Device.SetPixelScreenColor(new Core.Models.Display.HaloPixelColor(finalScreenColor.R, finalScreenColor.G, finalScreenColor.B));
386 }
387 catch {}
204 388 Device.SetUIModel(CloudMusicLyricsProfile.DefaultHaloPixelUIModel);
205 389 Console.WriteLine("已切换至时钟界面");
206 390 }
Modified HaloPixelToolBox/HaloPixelToolBox/ViewModels/SpotifyLyricsToolPageViewModel.cs +121 -14
@@ -25,6 +25,10 @@ public partial class SpotifyLyricsToolPageViewModel : ServiceBaseViewModelBase<s
25 25 [ObservableProperty]
26 26 private int switchBackTimeout = SpotifyLyricsProfile.SwitchBackTimeout;
27 27 [ObservableProperty]
28 private bool enableScreenColorSync = SpotifyLyricsProfile.EnableScreenColorSync;
29 [ObservableProperty]
30 private bool enableAmbientColorSync = SpotifyLyricsProfile.EnableAmbientColorSync;
31 [ObservableProperty]
28 32 private string spotifyTrackInfo = "未检测到播放中的歌曲";
29 33
30 34 public HaloPixelDevice Device { get; set; } = new();
@@ -32,9 +36,31 @@ public partial class SpotifyLyricsToolPageViewModel : ServiceBaseViewModelBase<s
32 36
33 37 public ISettingService SettingService { get; } = ServiceManager.GetService<ISettingService>();
34 38
35 partial void OnEnableSpotifyLyricsChanged(bool value) => SpotifyLyricsProfile.EnableSpotifyLyrics = value;
39 private bool _forceRefresh;
40
41 public void OnNavigatedTo()
42 {
43 _forceRefresh = true;
44 }
45
46 partial void OnEnableSpotifyLyricsChanged(bool value)
47 {
48 SpotifyLyricsProfile.EnableSpotifyLyrics = value;
49 if (value)
50 _forceRefresh = true;
51 }
36 52 partial void OnSwitchBackWhenPauseChanged(bool value) => SpotifyLyricsProfile.SwitchBackWhenPause = value;
37 53 partial void OnSwitchBackTimeoutChanged(int value) => SpotifyLyricsProfile.SwitchBackTimeout = value;
54 partial void OnEnableScreenColorSyncChanged(bool value)
55 {
56 SpotifyLyricsProfile.EnableScreenColorSync = value;
57 _forceRefresh = true;
58 }
59 partial void OnEnableAmbientColorSyncChanged(bool value)
60 {
61 SpotifyLyricsProfile.EnableAmbientColorSync = value;
62 _forceRefresh = true;
63 }
38 64
39 65 public SpotifyLyricsToolPageViewModel()
40 66 {
@@ -96,7 +122,7 @@ public partial class SpotifyLyricsToolPageViewModel : ServiceBaseViewModelBase<s
96 122 while (!DeviceReady)
97 123 await Task.Delay(500);
98 124
99 if (DeviceReady)
125 if (DeviceReady && EnableSpotifyLyrics)
100 126 {
101 127 Console.WriteLine("花再设备已就绪,显示启动信息");
102 128 Device.SetTextLayout(Core.Models.HaloPixelTextLayout.Center);
@@ -115,6 +141,8 @@ public partial class SpotifyLyricsToolPageViewModel : ServiceBaseViewModelBase<s
115 141 Console.WriteLine("[DEBUG]设备均在线,准备进入主循环");
116 142 string lastRead = string.Empty;
117 143 bool scrolled = false;
144 (byte R, byte G, byte B) lastScreenColor = (0, 0, 0);
145 (byte R, byte G, byte B) lastAmbientColor = (0, 0, 0);
118 146 while (true)
119 147 {
120 148 try
@@ -128,22 +156,74 @@ public partial class SpotifyLyricsToolPageViewModel : ServiceBaseViewModelBase<s
128 156 SpotifyTrackInfo = !string.IsNullOrEmpty(Reader.CurrentTitle) ? $"{Reader.CurrentArtist} - {Reader.CurrentTitle}" : "无播放中的歌曲";
129 157 });
130 158
131 if (Reader.TryReadLyrics(out var lyrics) && lastRead != lyrics)
159 var albumColor = Reader.CurrentAlbumColor;
160
161 // 1. Determine screen text color
162 (byte R, byte G, byte B) screenColor = ((byte)0xf0, (byte)0xb4, (byte)0xc8);
163 if (EnableScreenColorSync && albumColor.HasValue)
164 {
165 screenColor = albumColor.Value;
166 }
167
168 // 2. Determine ambient backlight color
169 (byte R, byte G, byte B) ambientColor = ((byte)0xf0, (byte)0xb4, (byte)0xc8);
170 if (EnableAmbientColorSync && albumColor.HasValue)
171 {
172 ambientColor = albumColor.Value;
173 }
174
175 bool colorChanged = lastScreenColor != screenColor || lastAmbientColor != ambientColor;
176 bool lyricsChanged = Reader.TryReadLyrics(out var lyrics) && lastRead != lyrics;
177
178 if (lyricsChanged || colorChanged || _forceRefresh)
132 179 {
133 Console.WriteLine($"已读取到歌词:{lyrics}");
134 lastRead = lyrics;
135 isClockUI = false;
136 time = 0;
137 if (scrolled)
180 _forceRefresh = false;
181 lastScreenColor = screenColor;
182 lastAmbientColor = ambientColor;
183 HidPacketBuilder.CurrentColor = screenColor;
184
185 try
186 {
187 Device.SetAmbientLight(new Core.Models.Lighting.AmbientLightOptions
188 {
189 Effect = Core.Models.Lighting.AmbientLightEffect.Static,
190 Color = new Core.Models.Display.HaloPixelColor(ambientColor.R, ambientColor.G, ambientColor.B),
191 Brightness = Core.Models.Lighting.AmbientLightBrightness.High,
192 Speed = 5
193 });
194 }
195 catch (Exception ex)
196 {
197 Console.WriteLine($"[ERROR] SetAmbientLight failed: {ex.Message}");
198 }
199
200 try
201 {
202 Device.SetPixelScreenColor(new Core.Models.Display.HaloPixelColor(screenColor.R, screenColor.G, screenColor.B));
203 }
204 catch (Exception ex)
205 {
206 Console.WriteLine($"[ERROR] SetPixelScreenColor failed: {ex.Message}");
207 }
208
209 if (lyricsChanged)
138 210 {
139 Device.ShowText(string.Empty);
140 await Task.Delay(100);
141 scrolled = false;
211 Console.WriteLine($"已读取到歌词:{lyrics}");
212 lastRead = lyrics;
213 isClockUI = false;
214 time = 0;
215 if (scrolled)
216 {
217 Device.ShowText(string.Empty);
218 await Task.Delay(100);
219 scrolled = false;
220 }
142 221 }
222
143 223 Device.SetTextLayout(SpotifyLyricsProfile.DefaultHaloPixelTextLayout);
144 Device.ShowText(lyrics);
145 Debug.WriteLine(lyrics.DisplayLength());
146 if (lyrics.DisplayLength() > 30)
224 Device.ShowText(lastRead);
225
226 if (lyricsChanged && lastRead.DisplayLength() > 30)
147 227 {
148 228 scrolled = true;
149 229 await Task.Delay(500);
@@ -155,6 +235,33 @@ public partial class SpotifyLyricsToolPageViewModel : ServiceBaseViewModelBase<s
155 235 if (!isClockUI && time >= SpotifyLyricsProfile.SwitchBackTimeout * 1000)
156 236 {
157 237 isClockUI = true;
238 (byte R, byte G, byte B) finalScreenColor = ((byte)0xf0, (byte)0xb4, (byte)0xc8);
239 if (EnableScreenColorSync && Reader.CurrentAlbumColor.HasValue)
240 {
241 finalScreenColor = Reader.CurrentAlbumColor.Value;
242 }
243 (byte R, byte G, byte B) finalAmbientColor = ((byte)0xf0, (byte)0xb4, (byte)0xc8);
244 if (EnableAmbientColorSync && Reader.CurrentAlbumColor.HasValue)
245 {
246 finalAmbientColor = Reader.CurrentAlbumColor.Value;
247 }
248 HidPacketBuilder.CurrentColor = finalScreenColor;
249 try
250 {
251 Device.SetAmbientLight(new Core.Models.Lighting.AmbientLightOptions
252 {
253 Effect = Core.Models.Lighting.AmbientLightEffect.Static,
254 Color = new Core.Models.Display.HaloPixelColor(finalAmbientColor.R, finalAmbientColor.G, finalAmbientColor.B),
255 Brightness = Core.Models.Lighting.AmbientLightBrightness.High,
256 Speed = 5
257 });
258 }
259 catch {}
260 try
261 {
262 Device.SetPixelScreenColor(new Core.Models.Display.HaloPixelColor(finalScreenColor.R, finalScreenColor.G, finalScreenColor.B));
263 }
264 catch {}
158 265 Device.SetUIModel(SpotifyLyricsProfile.DefaultHaloPixelUIModel);
159 266 Console.WriteLine("已切换至时钟界面");
160 267 }
Modified HaloPixelToolBox/HaloPixelToolBox/Views/CloudMusicLyricsToolPage.xaml +6 -0
Modified HaloPixelToolBox/HaloPixelToolBox/Views/CloudMusicLyricsToolPage.xaml.cs +6 -4
Modified HaloPixelToolBox/HaloPixelToolBox/Views/SpotifyLyricsToolPage.xaml +6 -0
Modified HaloPixelToolBox/HaloPixelToolBox/Views/SpotifyLyricsToolPage.xaml.cs +2 -0