mirror of
https://github.com/LanZhan-Harmony/WindowsMusicPlayer-TheUntamedMusicPlayer.git
synced 2026-05-06 19:20:18 +08:00
临时
This commit is contained in:
@@ -9,7 +9,7 @@ using ZLogger;
|
||||
|
||||
namespace The_Untamed_Music_Player.Playback;
|
||||
|
||||
public class AudioEngine : IDisposable
|
||||
public partial class AudioEngine : IDisposable
|
||||
{
|
||||
private readonly ILogger _logger = LoggingService.CreateLogger<AudioEngine>();
|
||||
private readonly PlaybackState _state;
|
||||
@@ -39,14 +39,14 @@ public class AudioEngine : IDisposable
|
||||
case nameof(PlaybackState.CurrentVolume):
|
||||
if (!_state.IsMute)
|
||||
{
|
||||
SetVolumeValue(_state.CurrentVolume / 100.0);
|
||||
SetVolume(_state.CurrentVolume / 100.0);
|
||||
}
|
||||
break;
|
||||
case nameof(PlaybackState.IsMute):
|
||||
SetVolumeValue(_state.IsMute ? 0.0 : _state.CurrentVolume / 100.0);
|
||||
SetVolume(_state.IsMute ? 0.0 : _state.CurrentVolume / 100.0);
|
||||
break;
|
||||
case nameof(PlaybackState.PlaySpeed):
|
||||
SetPlaybackSpeed(_state.PlaySpeed);
|
||||
SetSpeed(_state.PlaySpeed);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -65,8 +65,8 @@ public class AudioEngine : IDisposable
|
||||
LoadBassPlugins();
|
||||
|
||||
// 设置同步回调
|
||||
_syncEndCallback += (_, _, _, _) => PlaybackEnded?.Invoke();
|
||||
_syncFailCallback += (_, _, _, _) => PlaybackFailed?.Invoke();
|
||||
_syncEndCallback += OnPlaybackEnded;
|
||||
_syncFailCallback += OnPlaybackFailed;
|
||||
|
||||
// 设置WASAPI回调
|
||||
_wasapiProc = WasapiProc;
|
||||
@@ -98,6 +98,10 @@ public class AudioEngine : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlaybackEnded(int _1, int _2, int _3, nint _4) => PlaybackEnded?.Invoke();
|
||||
|
||||
private void OnPlaybackFailed(int _1, int _2, int _3, nint _4) => PlaybackFailed?.Invoke();
|
||||
|
||||
/// <summary>
|
||||
/// WASAPI回调处理程序
|
||||
/// </summary>
|
||||
@@ -115,13 +119,13 @@ public class AudioEngine : IDisposable
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
public bool LoadSong(string path)
|
||||
public bool LoadSong()
|
||||
{
|
||||
try
|
||||
{
|
||||
FreeCurrentStreams();
|
||||
CreateStreamFromPath(path);
|
||||
SetVolumeValue(_state.IsMute ? 0.0 : _state.CurrentVolume / 100.0);
|
||||
FreeStreams();
|
||||
CreateStream();
|
||||
SetVolume(_state.IsMute ? 0.0 : _state.CurrentVolume / 100.0);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
@@ -134,13 +138,14 @@ public class AudioEngine : IDisposable
|
||||
/// 从文件路径创建音频流
|
||||
/// </summary>
|
||||
/// <param name="path">文件路径</param>
|
||||
private void CreateStreamFromPath(string path)
|
||||
private void CreateStream()
|
||||
{
|
||||
FreeCurrentStreams();
|
||||
FreeStreams();
|
||||
const BassFlags flags =
|
||||
BassFlags.Unicode | BassFlags.Float | BassFlags.AsyncFile | BassFlags.Decode;
|
||||
|
||||
_currentStream = _state.CurrentSong!.IsOnline
|
||||
var path = _state.CurrentSong!.Path;
|
||||
_currentStream = _state.CurrentSong.IsOnline
|
||||
? Bass.CreateStream(path, 0, flags, null)
|
||||
: Bass.CreateStream(path, 0, 0, flags);
|
||||
if (_currentStream == 0)
|
||||
@@ -160,7 +165,7 @@ public class AudioEngine : IDisposable
|
||||
_currentStream = 0;
|
||||
}
|
||||
|
||||
SetPlaybackSpeed(_state.PlaySpeed);
|
||||
SetSpeed(_state.PlaySpeed);
|
||||
|
||||
Bass.ChannelSetSync(_fxStream, SyncFlags.End, 0, _syncEndCallback);
|
||||
Bass.ChannelSetSync(_fxStream, SyncFlags.Stalled, 0, _syncFailCallback);
|
||||
@@ -171,10 +176,19 @@ public class AudioEngine : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放当前音频流
|
||||
/// 释放音频流
|
||||
/// </summary>
|
||||
private void FreeCurrentStreams()
|
||||
private void FreeStreams()
|
||||
{
|
||||
if (BassWasapi.IsStarted)
|
||||
{
|
||||
BassWasapi.Stop(true);
|
||||
}
|
||||
if (_wasapiDevice != -1)
|
||||
{
|
||||
BassWasapi.Free();
|
||||
_wasapiDevice = -1;
|
||||
}
|
||||
if (_fxStream != 0)
|
||||
{
|
||||
Bass.StreamFree(_fxStream);
|
||||
@@ -221,7 +235,7 @@ public class AudioEngine : IDisposable
|
||||
/// 设置播放速度
|
||||
/// </summary>
|
||||
/// <param name="speed"></param>
|
||||
private void SetPlaybackSpeed(double speed)
|
||||
private void SetSpeed(double speed)
|
||||
{
|
||||
if (_fxStream != 0)
|
||||
{
|
||||
@@ -234,7 +248,7 @@ public class AudioEngine : IDisposable
|
||||
/// 设置音量
|
||||
/// </summary>
|
||||
/// <param name="volume"></param>
|
||||
private void SetVolumeValue(double volume)
|
||||
private void SetVolume(double volume)
|
||||
{
|
||||
if (_fxStream != 0)
|
||||
{
|
||||
@@ -242,5 +256,12 @@ public class AudioEngine : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
public void Dispose()
|
||||
{
|
||||
FreeStreams();
|
||||
Bass.Free();
|
||||
_syncEndCallback -= OnPlaybackEnded;
|
||||
_syncFailCallback -= OnPlaybackFailed;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,49 @@
|
||||
using System.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.UI.Xaml;
|
||||
using The_Untamed_Music_Player.Contracts.Models;
|
||||
using The_Untamed_Music_Player.Helpers;
|
||||
using The_Untamed_Music_Player.Models;
|
||||
using The_Untamed_Music_Player.Services;
|
||||
using Windows.Media;
|
||||
using Windows.Media.Playback;
|
||||
using Windows.System.Threading;
|
||||
using ZLinq;
|
||||
using ZLogger;
|
||||
|
||||
namespace The_Untamed_Music_Player.Playback;
|
||||
|
||||
public partial class MusicPlayer : ObservableRecipient, IDisposable
|
||||
{
|
||||
private readonly ILogger _logger = LoggingService.CreateLogger<MusicPlayer>();
|
||||
private readonly PlaybackState _state;
|
||||
private readonly PlayQueueManager _queueManager;
|
||||
private readonly AudioEngine _audioEngine;
|
||||
private readonly SystemMediaTransportControlsManager _smtcManager;
|
||||
private readonly SMTCManager _smtcManager;
|
||||
|
||||
/// <summary>
|
||||
/// 线程计时器
|
||||
/// </summary>
|
||||
private ThreadPoolTimer? _positionUpdateTimer;
|
||||
|
||||
/// <summary>
|
||||
/// 当前歌词切片在集合中的索引
|
||||
/// </summary>
|
||||
private int _currentLyricIndex = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 当前歌词内容
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
public partial string CurrentLyricContent { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 当前歌词切片集合
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
public partial List<LyricSlice> CurrentLyric { get; set; } = [];
|
||||
|
||||
public MusicPlayer()
|
||||
: base(StrongReferenceMessenger.Default)
|
||||
@@ -18,7 +51,7 @@ public partial class MusicPlayer : ObservableRecipient, IDisposable
|
||||
_state = new PlaybackState();
|
||||
_queueManager = new PlayQueueManager(_state);
|
||||
_audioEngine = new AudioEngine(_state);
|
||||
_smtcManager = new SystemMediaTransportControlsManager();
|
||||
_smtcManager = new SMTCManager(_state);
|
||||
|
||||
// 设置事件处理
|
||||
_audioEngine.PlaybackEnded += OnPlaybackEnded;
|
||||
@@ -28,30 +61,61 @@ public partial class MusicPlayer : ObservableRecipient, IDisposable
|
||||
LoadCurrentStateAsync();
|
||||
}
|
||||
|
||||
private void OnPlaybackFailed() => throw new NotImplementedException();
|
||||
private void InitializeSmtc()
|
||||
{
|
||||
_smtcManager.ButtonPressed += button =>
|
||||
{
|
||||
switch (button)
|
||||
{
|
||||
case SystemMediaTransportControlsButton.Play:
|
||||
case SystemMediaTransportControlsButton.Pause:
|
||||
PlayPauseUpdate();
|
||||
break;
|
||||
case SystemMediaTransportControlsButton.Previous:
|
||||
PlayPreviousSong();
|
||||
break;
|
||||
case SystemMediaTransportControlsButton.Next:
|
||||
PlayNextSong();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void OnPlaybackEnded() => throw new NotImplementedException();
|
||||
|
||||
// 公开必要的属性
|
||||
public PlaybackState State => _state;
|
||||
public PlayQueueManager QueueManager => _queueManager;
|
||||
private void OnPlaybackFailed() => throw new NotImplementedException();
|
||||
|
||||
public async void PlaySongByInfo(IBriefSongInfoBase info)
|
||||
private void HandleSongNotAvailable() => throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// 按索引播放歌曲
|
||||
/// </summary>
|
||||
/// <param name="index"></param>
|
||||
/// <param name="shouldStop"></param>
|
||||
private async void PlaySongByIndex(int index, bool shouldStop = false)
|
||||
{
|
||||
_audioEngine.Stop();
|
||||
_state.PlayState = 2; // 加载中
|
||||
_state.CurrentBriefSong = info;
|
||||
_state.CurrentSong = await IDetailedSongInfoBase.CreateDetailedSongInfoAsync(info);
|
||||
|
||||
var currentSong = _queueManager.GetCurrentSong();
|
||||
_state.PlayQueueIndex = currentSong?.Index ?? 0;
|
||||
|
||||
Stop();
|
||||
_state.PlayState = MediaPlaybackState.Buffering;
|
||||
var songToPlay = _queueManager.CurrentQueue[index];
|
||||
_state.CurrentBriefSong = songToPlay.Song;
|
||||
_state.CurrentSong = await IDetailedSongInfoBase.CreateDetailedSongInfoAsync(
|
||||
songToPlay.Song
|
||||
);
|
||||
_state.PlayQueueIndex = index;
|
||||
if (_state.CurrentSong!.IsPlayAvailable)
|
||||
{
|
||||
if (await _audioEngine.LoadSong(_state.CurrentSong.Path))
|
||||
await SetSource();
|
||||
UpdateLyric(_state.CurrentSong!.Lyric);
|
||||
_smtcManager.SetButtonsEnabled(true, true, true, true);
|
||||
if (shouldStop)
|
||||
{
|
||||
_audioEngine.Play();
|
||||
_state.PlayState = 1; // 播放中
|
||||
_state.PlayState = MediaPlaybackState.Paused;
|
||||
}
|
||||
else
|
||||
{
|
||||
Play();
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -60,14 +124,155 @@ public partial class MusicPlayer : ObservableRecipient, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public void PlayNextSong()
|
||||
private async Task SetSource()
|
||||
{
|
||||
var nextIndex = _queueManager.GetNextSongIndex();
|
||||
var nextSong = (
|
||||
_state.ShuffleMode ? _queueManager.ShuffledPlayQueue : _queueManager.PlayQueue
|
||||
)[nextIndex];
|
||||
PlaySongByInfo(nextSong.Song);
|
||||
try
|
||||
{
|
||||
Data.RootPlayBarViewModel?.ButtonVisibility = Visibility.Visible;
|
||||
Data.RootPlayBarViewModel?.Availability = true;
|
||||
_audioEngine.LoadSong();
|
||||
_smtcManager.UpdateMediaInfo();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ZLogInformation(ex, $"SetSource失败");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await _smtcManager.SetCoverImageAndUpdateAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
public async void UpdateLyric(string lyric)
|
||||
{
|
||||
CurrentLyric = await LyricHelper.GetLyricSlices(lyric);
|
||||
_currentLyricIndex = 0;
|
||||
|
||||
if (CurrentLyric.Count > 0)
|
||||
{
|
||||
CurrentLyric[0].IsCurrent = true;
|
||||
CurrentLyricContent = CurrentLyric[0].Content;
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentLyricContent = "";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前歌词切片索引
|
||||
/// </summary>
|
||||
/// <param name="currentTime"></param>
|
||||
/// <returns></returns>
|
||||
public int GetCurrentLyricIndex(double currentTime)
|
||||
{
|
||||
for (var i = 0; i < CurrentLyric.Count; i++)
|
||||
{
|
||||
if (CurrentLyric[i].Time > currentTime)
|
||||
{
|
||||
return i > 0 ? i - 1 : 0;
|
||||
}
|
||||
}
|
||||
return CurrentLyric.Count - 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新当前歌词索引和状态
|
||||
/// </summary>
|
||||
/// <param name="currentTime">当前播放时间(毫秒)</param>
|
||||
public void UpdateCurrentLyricIndex(double currentTime)
|
||||
{
|
||||
if (CurrentLyric.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var newIndex = GetCurrentLyricIndex(currentTime);
|
||||
if (newIndex != _currentLyricIndex)
|
||||
{
|
||||
if (_currentLyricIndex >= 0 && _currentLyricIndex < CurrentLyric.Count)
|
||||
{
|
||||
CurrentLyric[_currentLyricIndex].IsCurrent = false;
|
||||
}
|
||||
_currentLyricIndex = newIndex;
|
||||
|
||||
if (_currentLyricIndex >= 0 && _currentLyricIndex < CurrentLyric.Count)
|
||||
{
|
||||
CurrentLyric[_currentLyricIndex].IsCurrent = true;
|
||||
CurrentLyricContent = CurrentLyric[_currentLyricIndex].Content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void PlaySongByInfo(IBriefSongInfoBase info)
|
||||
{
|
||||
var index =
|
||||
_queueManager
|
||||
.CurrentQueue.AsValueEnumerable()
|
||||
.FirstOrDefault(song => song.Song == info)
|
||||
?.Index ?? 0;
|
||||
PlaySongByIndex(index, false);
|
||||
}
|
||||
|
||||
public void PlayPreviousSong()
|
||||
{
|
||||
var prevIndex = _queueManager.GetPreviousSongIndex();
|
||||
PlaySongByIndex(prevIndex, false);
|
||||
}
|
||||
|
||||
public void PlayNextSong()
|
||||
{
|
||||
var (nextIndex, isLast) = _queueManager.GetNextSongIndex();
|
||||
PlaySongByIndex(nextIndex, isLast);
|
||||
}
|
||||
|
||||
public void PlayPauseUpdate() { }
|
||||
|
||||
/// <summary>
|
||||
/// 播放
|
||||
/// </summary>
|
||||
public void Play()
|
||||
{
|
||||
_positionUpdateTimer = ThreadPoolTimer.CreatePeriodicTimer(
|
||||
UpdateTimerHandler,
|
||||
TimeSpan.FromMilliseconds(250)
|
||||
);
|
||||
_audioEngine.Play();
|
||||
_state.PlayState = MediaPlaybackState.Playing;
|
||||
_smtcManager.UpdatePlaybackStatus(MediaPlaybackStatus.Playing);
|
||||
}
|
||||
|
||||
private void UpdateTimerHandler(ThreadPoolTimer timer) { }
|
||||
|
||||
/// <summary>
|
||||
/// 暂停
|
||||
/// </summary>
|
||||
public void Pause()
|
||||
{
|
||||
_audioEngine.Pause();
|
||||
_state.PlayState = MediaPlaybackState.Paused;
|
||||
_smtcManager.UpdatePlaybackStatus(MediaPlaybackStatus.Paused);
|
||||
_positionUpdateTimer?.Cancel();
|
||||
_positionUpdateTimer = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
_audioEngine.Stop();
|
||||
_state.PlayState = MediaPlaybackState.Paused;
|
||||
_currentLyricIndex = 0;
|
||||
CurrentLyricContent = "";
|
||||
_positionUpdateTimer?.Cancel();
|
||||
_positionUpdateTimer = null;
|
||||
}
|
||||
|
||||
public async void LoadCurrentStateAsync() => throw new NotImplementedException();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_audioEngine.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,20 @@ namespace The_Untamed_Music_Player.Playback;
|
||||
public partial class PlayQueueManager : ObservableObject
|
||||
{
|
||||
private readonly PlaybackState _state;
|
||||
public int PlayQueueLength { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(CurrentQueue))]
|
||||
private partial ObservableCollection<IndexedPlayQueueSong> NormalPlayQueue { get; set; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(CurrentQueue))]
|
||||
private partial ObservableCollection<IndexedPlayQueueSong> ShuffledPlayQueue { get; set; } = [];
|
||||
|
||||
public ObservableCollection<IndexedPlayQueueSong> CurrentQueue =>
|
||||
_state.ShuffleMode == ShuffleState.Normal ? NormalPlayQueue : ShuffledPlayQueue;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string PlayQueueName { get; set; } = "";
|
||||
|
||||
public PlayQueueManager(PlaybackState state)
|
||||
{
|
||||
@@ -18,15 +31,6 @@ public partial class PlayQueueManager : ObservableObject
|
||||
_state.PropertyChanged += OnStateChanged;
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<IndexedPlayQueueSong> PlayQueue { get; set; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<IndexedPlayQueueSong> ShuffledPlayQueue { get; set; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string PlayQueueName { get; set; } = "";
|
||||
|
||||
private void OnStateChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(PlaybackState.ShuffleMode))
|
||||
@@ -35,18 +39,16 @@ public partial class PlayQueueManager : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
public void SetPlayQueue(string name, IEnumerable<IBriefSongInfoBase> list)
|
||||
public void SetPlayQueue(string name, IList<IBriefSongInfoBase> list)
|
||||
{
|
||||
PlayQueueName = name;
|
||||
PlayQueue =
|
||||
NormalPlayQueue =
|
||||
[
|
||||
.. list.AsValueEnumerable()
|
||||
.Select((song, index) => new IndexedPlayQueueSong(index, song)),
|
||||
];
|
||||
|
||||
PlayQueueLength = list.AsValueEnumerable().Count();
|
||||
|
||||
if (_state.ShuffleMode)
|
||||
_state.PlayQueueCount = NormalPlayQueue.Count;
|
||||
if (_state.ShuffleMode == ShuffleState.Shuffled)
|
||||
{
|
||||
UpdateShufflePlayQueue();
|
||||
UpdateQueueIndexes();
|
||||
@@ -55,57 +57,55 @@ public partial class PlayQueueManager : ObservableObject
|
||||
|
||||
public void AddSongToNextPlay(IBriefSongInfoBase info)
|
||||
{
|
||||
var queue = _state.ShuffleMode ? ShuffledPlayQueue : PlayQueue;
|
||||
var insertIndex = _state.PlayQueueIndex + 1;
|
||||
|
||||
queue.Insert(insertIndex, new IndexedPlayQueueSong(insertIndex, info));
|
||||
PlayQueueLength++;
|
||||
|
||||
CurrentQueue.Insert(insertIndex, new IndexedPlayQueueSong(insertIndex, info));
|
||||
_state.PlayQueueCount++;
|
||||
UpdateQueueIndexes(insertIndex + 1);
|
||||
|
||||
if (_state.ShuffleMode)
|
||||
if (_state.ShuffleMode == ShuffleState.Shuffled)
|
||||
{
|
||||
PlayQueue.Add(new IndexedPlayQueueSong(PlayQueue.Count, info));
|
||||
NormalPlayQueue.Add(new IndexedPlayQueueSong(NormalPlayQueue.Count, info));
|
||||
}
|
||||
}
|
||||
|
||||
public int GetNextSongIndex()
|
||||
{
|
||||
if (_state.RepeatMode == 1) // 列表循环
|
||||
{
|
||||
return (_state.PlayQueueIndex + 1) % PlayQueueLength;
|
||||
}
|
||||
|
||||
return _state.PlayQueueIndex < PlayQueueLength - 1 ? _state.PlayQueueIndex + 1 : 0;
|
||||
}
|
||||
|
||||
public int GetPreviousSongIndex()
|
||||
{
|
||||
if (_state.RepeatMode == 1) // 列表循环
|
||||
if (_state.RepeatMode == RepeatState.RepeatAll)
|
||||
{
|
||||
return (_state.PlayQueueIndex + PlayQueueLength - 1) % PlayQueueLength;
|
||||
return (_state.PlayQueueIndex + _state.PlayQueueCount - 1) % _state.PlayQueueCount;
|
||||
}
|
||||
|
||||
return _state.PlayQueueIndex > 0 ? _state.PlayQueueIndex - 1 : _state.PlayQueueIndex;
|
||||
}
|
||||
|
||||
public (int, bool) GetNextSongIndex()
|
||||
{
|
||||
var newIndex =
|
||||
_state.PlayQueueIndex < _state.PlayQueueCount - 1 ? _state.PlayQueueIndex + 1 : 0;
|
||||
var isLast = _state.PlayQueueIndex >= _state.PlayQueueCount - 1;
|
||||
if (_state.RepeatMode == RepeatState.RepeatAll)
|
||||
{
|
||||
newIndex = (_state.PlayQueueIndex + 1) % _state.PlayQueueCount;
|
||||
isLast = false;
|
||||
}
|
||||
return (newIndex, isLast);
|
||||
}
|
||||
|
||||
public IndexedPlayQueueSong? GetCurrentSong()
|
||||
{
|
||||
var queue = _state.ShuffleMode ? ShuffledPlayQueue : PlayQueue;
|
||||
return _state.PlayQueueIndex < queue.Count ? queue[_state.PlayQueueIndex] : null;
|
||||
return _state.PlayQueueIndex < CurrentQueue.Count
|
||||
? CurrentQueue[_state.PlayQueueIndex]
|
||||
: null;
|
||||
}
|
||||
|
||||
private void UpdateShufflePlayQueue()
|
||||
{
|
||||
ShuffledPlayQueue = [.. PlayQueue.AsValueEnumerable().OrderBy(x => Guid.NewGuid())];
|
||||
ShuffledPlayQueue = [.. NormalPlayQueue.AsValueEnumerable().OrderBy(x => Guid.NewGuid())];
|
||||
}
|
||||
|
||||
private void UpdateQueueIndexes(int startIndex = 0)
|
||||
{
|
||||
var queue = _state.ShuffleMode ? ShuffledPlayQueue : PlayQueue;
|
||||
for (var i = startIndex; i < queue.Count; i++)
|
||||
for (var i = startIndex; i < CurrentQueue.Count; i++)
|
||||
{
|
||||
queue[i].Index = i;
|
||||
CurrentQueue[i].Index = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using The_Untamed_Music_Player.Contracts.Models;
|
||||
using Windows.Media.Playback;
|
||||
|
||||
namespace The_Untamed_Music_Player.Playback;
|
||||
|
||||
public partial class PlaybackState : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
public partial bool ShuffleMode { get; set; } = false;
|
||||
public partial ShuffleState ShuffleMode { get; set; } = ShuffleState.Normal;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial byte RepeatMode { get; set; } = 0;
|
||||
public partial RepeatState RepeatMode { get; set; } = RepeatState.NoRepeat;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int PlayQueueIndex { get; set; } = 0;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial byte PlayState { get; set; } = 0;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial IBriefSongInfoBase? CurrentBriefSong { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial IDetailedSongInfoBase? CurrentSong { get; set; }
|
||||
public partial MediaPlaybackState PlayState { get; set; } = MediaPlaybackState.Paused;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double CurrentVolume { get; set; } = 100;
|
||||
@@ -42,5 +34,45 @@ public partial class PlaybackState : ObservableObject
|
||||
public partial TimeSpan TotalPlayingTime { get; set; } = TimeSpan.Zero;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double CurrentPosition { get; set; } = 0;
|
||||
public partial int PlayQueueIndex { get; set; } = -1;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int PlayQueueCount { get; set; } = 0;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial IBriefSongInfoBase? CurrentBriefSong { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial IDetailedSongInfoBase? CurrentSong { get; set; }
|
||||
}
|
||||
|
||||
public enum ShuffleState
|
||||
{
|
||||
/// <summary>
|
||||
/// 正常模式
|
||||
/// </summary>
|
||||
Normal = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 随机模式
|
||||
/// </summary>
|
||||
Shuffled = 1,
|
||||
}
|
||||
|
||||
public enum RepeatState
|
||||
{
|
||||
/// <summary>
|
||||
/// 不循环
|
||||
/// </summary>
|
||||
NoRepeat = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 列表循环
|
||||
/// </summary>
|
||||
RepeatAll = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 单曲循环
|
||||
/// </summary>
|
||||
RepeatOne = 2,
|
||||
}
|
||||
|
||||
206
The Untamed Music Player/Playback/SMTCManager.cs
Normal file
206
The Untamed Music Player/Playback/SMTCManager.cs
Normal file
@@ -0,0 +1,206 @@
|
||||
using System.Runtime.InteropServices.WindowsRuntime;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using The_Untamed_Music_Player.Contracts.Models;
|
||||
using The_Untamed_Music_Player.Helpers;
|
||||
using The_Untamed_Music_Player.Models;
|
||||
using Windows.Media;
|
||||
using Windows.Media.Playback;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace The_Untamed_Music_Player.Playback;
|
||||
|
||||
/// <summary>
|
||||
/// 系统媒体传输控件管理器
|
||||
/// </summary>
|
||||
public partial class SMTCManager : IDisposable
|
||||
{
|
||||
private readonly PlaybackState _state;
|
||||
|
||||
/// <summary>
|
||||
/// 用于获取SMTC的临时播放器
|
||||
/// </summary>
|
||||
private readonly MediaPlayer _tempPlayer = new();
|
||||
|
||||
/// <summary>
|
||||
/// 用于SMTC显示封面图片的流
|
||||
/// </summary>
|
||||
private static InMemoryRandomAccessStream? _currentCoverStream = null!;
|
||||
|
||||
/// <summary>
|
||||
/// SMTC控件
|
||||
/// </summary>
|
||||
private readonly SystemMediaTransportControls _systemControls;
|
||||
|
||||
/// <summary>
|
||||
/// SMTC显示内容更新器
|
||||
/// </summary>
|
||||
private readonly SystemMediaTransportControlsDisplayUpdater _displayUpdater;
|
||||
|
||||
/// <summary>
|
||||
/// SMTC时间线属性
|
||||
/// </summary>
|
||||
private readonly SystemMediaTransportControlsTimelineProperties _timelineProperties = new();
|
||||
|
||||
/// <summary>
|
||||
/// 播放状态变化事件
|
||||
/// </summary>
|
||||
public event Action<SystemMediaTransportControlsButton>? ButtonPressed;
|
||||
|
||||
public SMTCManager(PlaybackState state)
|
||||
{
|
||||
_state = state;
|
||||
_systemControls = _tempPlayer.SystemMediaTransportControls;
|
||||
_displayUpdater = _systemControls.DisplayUpdater;
|
||||
_displayUpdater.Type = MediaPlaybackType.Music;
|
||||
_displayUpdater.AppMediaId = "AppDisplayName".GetLocalized();
|
||||
_systemControls.IsEnabled = true;
|
||||
_systemControls.ButtonPressed += OnSystemControlsButtonPressed;
|
||||
_timelineProperties.StartTime = TimeSpan.Zero;
|
||||
_timelineProperties.MinSeekTime = TimeSpan.Zero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 系统媒体控制按钮按下事件处理
|
||||
/// </summary>
|
||||
private void OnSystemControlsButtonPressed(
|
||||
SystemMediaTransportControls sender,
|
||||
SystemMediaTransportControlsButtonPressedEventArgs args
|
||||
)
|
||||
{
|
||||
App.MainWindow?.DispatcherQueue.TryEnqueue(
|
||||
DispatcherQueuePriority.Low,
|
||||
() => ButtonPressed?.Invoke(args.Button)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新播放状态
|
||||
/// </summary>
|
||||
/// <param name="playbackStatus">播放状态</param>
|
||||
public void UpdatePlaybackStatus(MediaPlaybackStatus playbackStatus)
|
||||
{
|
||||
_systemControls.PlaybackStatus = playbackStatus;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置按钮是否可用
|
||||
/// </summary>
|
||||
/// <param name="isPlayEnabled">播放按钮是否可用</param>
|
||||
/// <param name="isPauseEnabled">暂停按钮是否可用</param>
|
||||
/// <param name="isPreviousEnabled">上一首按钮是否可用</param>
|
||||
/// <param name="isNextEnabled">下一首按钮是否可用</param>
|
||||
public void SetButtonsEnabled(
|
||||
bool isPlayEnabled,
|
||||
bool isPauseEnabled,
|
||||
bool isPreviousEnabled,
|
||||
bool isNextEnabled
|
||||
)
|
||||
{
|
||||
_systemControls.IsPlayEnabled = isPlayEnabled;
|
||||
_systemControls.IsPauseEnabled = isPauseEnabled;
|
||||
_systemControls.IsPreviousEnabled = isPreviousEnabled;
|
||||
_systemControls.IsNextEnabled = isNextEnabled;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新播放队列信息以计算按钮状态
|
||||
/// </summary>
|
||||
public void UpdateButtonState()
|
||||
{
|
||||
var isFirstSong = _state.PlayQueueIndex == 0;
|
||||
var isLastSong = _state.PlayQueueIndex == _state.PlayQueueCount - 1;
|
||||
var isRepeatOffOrSingle =
|
||||
_state.RepeatMode == RepeatState.NoRepeat || _state.RepeatMode == RepeatState.RepeatOne;
|
||||
|
||||
_systemControls.IsPreviousEnabled = !(isFirstSong && isRepeatOffOrSingle);
|
||||
_systemControls.IsNextEnabled = !(isLastSong && isRepeatOffOrSingle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新媒体信息
|
||||
/// </summary>
|
||||
public void UpdateMediaInfo()
|
||||
{
|
||||
_displayUpdater.MusicProperties.Title = _state.CurrentSong!.Title;
|
||||
_displayUpdater.MusicProperties.Artist =
|
||||
_state.CurrentSong.ArtistsStr == "SongInfo_UnknownArtist".GetLocalized()
|
||||
? ""
|
||||
: _state.CurrentSong.ArtistsStr;
|
||||
_timelineProperties.MaxSeekTime = _state.TotalPlayingTime;
|
||||
_timelineProperties.EndTime = _state.TotalPlayingTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置封面图片
|
||||
/// </summary>
|
||||
/// <param name="song">当前歌曲</param>
|
||||
public async Task SetCoverImageAndUpdateAsync()
|
||||
{
|
||||
var song = _state.CurrentSong!;
|
||||
var defaultCoverUri = new Uri("ms-appx:///Assets/NoCover.png");
|
||||
|
||||
if (song.Cover is null) // 没有封面时使用默认图片
|
||||
{
|
||||
_displayUpdater.Thumbnail = RandomAccessStreamReference.CreateFromUri(defaultCoverUri);
|
||||
_displayUpdater.Update();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (song.IsOnline) // 在线歌曲:直接使用 URL
|
||||
{
|
||||
var coverPath = (song as IDetailedOnlineSongInfo)?.CoverPath;
|
||||
_displayUpdater.Thumbnail = coverPath is not null
|
||||
? RandomAccessStreamReference.CreateFromUri(new Uri(coverPath))
|
||||
: RandomAccessStreamReference.CreateFromUri(defaultCoverUri);
|
||||
}
|
||||
else // 本地歌曲:从byte[]加载
|
||||
{
|
||||
var localSong = song as DetailedLocalSongInfo;
|
||||
if (localSong?.CoverBuffer is not null)
|
||||
{
|
||||
_currentCoverStream?.Dispose();
|
||||
_currentCoverStream = new InMemoryRandomAccessStream();
|
||||
await _currentCoverStream.WriteAsync(localSong.CoverBuffer.AsBuffer());
|
||||
_currentCoverStream.Seek(0);
|
||||
_displayUpdater.Thumbnail = RandomAccessStreamReference.CreateFromStream(
|
||||
_currentCoverStream
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
_displayUpdater.Thumbnail = RandomAccessStreamReference.CreateFromUri(
|
||||
defaultCoverUri
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
_displayUpdater.Thumbnail = RandomAccessStreamReference.CreateFromUri(defaultCoverUri);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_displayUpdater.Update();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新时间轴属性
|
||||
/// </summary>
|
||||
/// <param name="currentTime">当前播放时间</param>
|
||||
public void UpdateTimelinePosition(TimeSpan currentTime)
|
||||
{
|
||||
_timelineProperties.Position = currentTime;
|
||||
_systemControls.UpdateTimelineProperties(_timelineProperties);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_systemControls.ButtonPressed -= OnSystemControlsButtonPressed;
|
||||
_currentCoverStream?.Dispose();
|
||||
_tempPlayer?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Runtime.InteropServices.WindowsRuntime;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using The_Untamed_Music_Player.Contracts.Models;
|
||||
using The_Untamed_Music_Player.Helpers;
|
||||
using The_Untamed_Music_Player.Models;
|
||||
@@ -58,8 +59,6 @@ public partial class SystemMediaTransportControlsManager : IDisposable
|
||||
/// </summary>
|
||||
public event Action<SystemMediaTransportControlsButton>? ButtonPressed;
|
||||
|
||||
public SystemMediaTransportControls Controls => _systemControls;
|
||||
|
||||
public SystemMediaTransportControlsManager()
|
||||
{
|
||||
_systemControls = _tempPlayer.SystemMediaTransportControls;
|
||||
@@ -80,7 +79,10 @@ public partial class SystemMediaTransportControlsManager : IDisposable
|
||||
SystemMediaTransportControlsButtonPressedEventArgs args
|
||||
)
|
||||
{
|
||||
ButtonPressed?.Invoke(args.Button);
|
||||
App.MainWindow?.DispatcherQueue.TryEnqueue(
|
||||
DispatcherQueuePriority.Low,
|
||||
() => ButtonPressed?.Invoke(args.Button)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user