473 lines
15 KiB
C#
473 lines
15 KiB
C#
using System.IO;
|
||||
|
|
using System.Windows.Controls;
|
|||
|
|
using NAudio;
|
|||
|
|
using NAudio.Wave;
|
|||
|
|
using PCL.Core.App;
|
|||
|
|
using PCL.Core.App.Localization;
|
|||
|
|
using PCL.Core.Utils;
|
|||
|
|
|
|||
|
|
namespace PCL;
|
|||
|
|
|
|||
|
|
public static class ModMusic
|
|||
|
|
{
|
|||
|
|
/// <summary>
|
|||
|
|
/// 当前正在播放的 NAudio.Wave.WaveOutEvent。
|
|||
|
|
/// </summary>
|
|||
|
|
public static WaveOutEvent musicNAudio;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 当前播放的音乐地址。
|
|||
|
|
/// </summary>
|
|||
|
|
private static string musicCurrent = "";
|
|||
|
|
|
|||
|
|
private static void MusicLoop(bool isFirstLoad = false)
|
|||
|
|
{
|
|||
|
|
WaveOutEvent currentWave = null;
|
|||
|
|
AudioFileReader reader = null;
|
|||
|
|
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
currentWave = new WaveOutEvent();
|
|||
|
|
musicNAudio = currentWave;
|
|||
|
|
currentWave.DeviceNumber = -1; // 使用默认设备
|
|||
|
|
|
|||
|
|
reader = new AudioFileReader(musicCurrent);
|
|||
|
|
currentWave.Init(reader);
|
|||
|
|
currentWave.Play();
|
|||
|
|
|
|||
|
|
// 首次加载且用户未启用自动播放,则暂停
|
|||
|
|
if (isFirstLoad && !Config.Preference.Music.StartOnStartup) currentWave.Pause();
|
|||
|
|
|
|||
|
|
MusicRefreshUI();
|
|||
|
|
|
|||
|
|
var lastVolume = Config.Preference.Music.Volume;
|
|||
|
|
currentWave.Volume = lastVolume / 1000.0f;
|
|||
|
|
|
|||
|
|
// 播放主循环
|
|||
|
|
while (currentWave.Equals(musicNAudio) && currentWave.PlaybackState != PlaybackState.Stopped)
|
|||
|
|
{
|
|||
|
|
// 音量动态更新
|
|||
|
|
var currentVolume = Config.Preference.Music.Volume;
|
|||
|
|
if (currentVolume != lastVolume)
|
|||
|
|
{
|
|||
|
|
lastVolume = currentVolume;
|
|||
|
|
currentWave.Volume = currentVolume / 1000.0f;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新进度条
|
|||
|
|
if (reader.TotalTime.TotalMilliseconds > 0d)
|
|||
|
|
{
|
|||
|
|
var progress = reader.CurrentTime.TotalMilliseconds / reader.TotalTime.TotalMilliseconds;
|
|||
|
|
ModBase.RunInUi(() => ModMain.frmMain.BtnExtraMusic.Progress = progress);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Thread.Sleep(100);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 播放结束,继续下一首
|
|||
|
|
if (currentWave.PlaybackState == PlaybackState.Stopped &&
|
|||
|
|
(musicAllList?.Any() is { } arg5 ? arg5 : (bool?)null).GetValueOrDefault())
|
|||
|
|
MusicStartPlay(DequeueNextMusicAddress(), isFirstLoad);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
ModBase.Log(ex, $"播放音乐出现内部错误({musicCurrent})", ModBase.LogLevel.Developer);
|
|||
|
|
|
|||
|
|
// 错误处理:精准提示用户
|
|||
|
|
var fileName = ModBase.GetFileNameFromPath(musicCurrent);
|
|||
|
|
if (ex is MmException)
|
|||
|
|
{
|
|||
|
|
var msg = ex.Message;
|
|||
|
|
if (msg.Contains("AlreadyAllocated"))
|
|||
|
|
HintService.Hint(Lang.Text("Music.Error.DeviceBusy"), HintType.Error);
|
|||
|
|
else if (msg.Contains("NoDriver") || msg.Contains("BadDeviceId"))
|
|||
|
|
HintService.Hint(Lang.Text("Music.Error.DeviceChanged"), HintType.Error);
|
|||
|
|
else
|
|||
|
|
ModBase.Log(
|
|||
|
|
ex,
|
|||
|
|
$"播放失败({fileName})",
|
|||
|
|
ModBase.LogLevel.Hint,
|
|||
|
|
userSummary: Lang.Text("Music.Error.PlaybackFailed", fileName));
|
|||
|
|
}
|
|||
|
|
else if (ex.Message.Contains("Got a frame at sample rate") ||
|
|||
|
|
ex.Message.Contains("does not support changes to"))
|
|||
|
|
{
|
|||
|
|
HintService.Hint(
|
|||
|
|
Lang.Text("Music.Error.PropertyChangeUnsupported", fileName),
|
|||
|
|
HintType.Error);
|
|||
|
|
}
|
|||
|
|
else if ((!musicCurrent.EndsWithF(".wav", true) && !musicCurrent.EndsWithF(".mp3", true) &&
|
|||
|
|
!musicCurrent.EndsWithF(".flac", true)) || ex.Message.Contains("0xC00D36C4"))
|
|||
|
|
{
|
|||
|
|
HintService.Hint(
|
|||
|
|
Lang.Text("Music.Error.UnsupportedFormat", fileName),
|
|||
|
|
HintType.Error);
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
ModBase.Log(
|
|||
|
|
ex,
|
|||
|
|
$"播放失败({fileName})",
|
|||
|
|
ModBase.LogLevel.Hint,
|
|||
|
|
userSummary: Lang.Text("Music.Error.PlaybackFailed", fileName));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 移除无效文件
|
|||
|
|
musicAllList?.Remove(musicCurrent);
|
|||
|
|
musicWaitingList?.Remove(musicCurrent);
|
|||
|
|
MusicRefreshUI();
|
|||
|
|
|
|||
|
|
Thread.Sleep(2000);
|
|||
|
|
|
|||
|
|
// 尝试播放下一首
|
|||
|
|
if (ex is FileNotFoundException)
|
|||
|
|
MusicRefreshPlay(true, isFirstLoad);
|
|||
|
|
else
|
|||
|
|
MusicStartPlay(DequeueNextMusicAddress(), isFirstLoad);
|
|||
|
|
}
|
|||
|
|
finally
|
|||
|
|
{
|
|||
|
|
currentWave?.Dispose();
|
|||
|
|
reader?.Dispose();
|
|||
|
|
MusicRefreshUI();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#region 播放列表
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 接下来要播放的音乐文件路径。未初始化时为 Nothing。
|
|||
|
|
/// </summary>
|
|||
|
|
public static List<string> musicWaitingList;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 全部音乐文件路径。未初始化时为 Nothing。
|
|||
|
|
/// </summary>
|
|||
|
|
public static List<string> musicAllList;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 初始化音乐播放列表。
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="forceReload">强制全部重新载入列表。</param>
|
|||
|
|
/// <param name="preventFirst">在重载列表时避免让某项成为第一项。</param>
|
|||
|
|
private static void MusicListInit(bool forceReload, string preventFirst = null)
|
|||
|
|
{
|
|||
|
|
if (forceReload)
|
|||
|
|
musicAllList = null;
|
|||
|
|
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
if (musicAllList is null)
|
|||
|
|
{
|
|||
|
|
musicAllList = new List<string>();
|
|||
|
|
var musicDir = Path.Combine(ModBase.exePath, "PCL", "Musics");
|
|||
|
|
Directory.CreateDirectory(musicDir);
|
|||
|
|
foreach (var file in ModBase.EnumerateFiles(musicDir))
|
|||
|
|
{
|
|||
|
|
var ext = file.Extension.ToLowerInvariant();
|
|||
|
|
// 忽略非音频文件
|
|||
|
|
if (new[] { ".ini", ".jpg", ".txt", ".cfg", ".lrc", ".db", ".png" }.Contains(ext))
|
|||
|
|
continue;
|
|||
|
|
musicAllList.Add(file.FullName);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 根据设置决定是否随机
|
|||
|
|
if (Config.Preference.Music.ShufflePlayback)
|
|||
|
|
musicWaitingList = RandomUtils.Shuffle(new List<string>(musicAllList));
|
|||
|
|
else
|
|||
|
|
musicWaitingList = new List<string>(musicAllList);
|
|||
|
|
|
|||
|
|
// 避免 PreventFirst 成为第一项
|
|||
|
|
if (preventFirst is not null && musicWaitingList.Count > 0 && string.Equals(musicWaitingList[0],
|
|||
|
|
preventFirst, StringComparison.OrdinalIgnoreCase))
|
|||
|
|
{
|
|||
|
|
musicWaitingList.RemoveAt(0);
|
|||
|
|
musicWaitingList.Add(preventFirst);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
ModBase.Log(
|
|||
|
|
ex,
|
|||
|
|
"初始化音乐列表失败",
|
|||
|
|
ModBase.LogLevel.Feedback,
|
|||
|
|
userSummary: Lang.Text("Music.Error.OperationFailed"));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取下一首播放的音乐路径并将其从列表中移除。
|
|||
|
|
/// 如果没有,可能会返回 Nothing。
|
|||
|
|
/// </summary>
|
|||
|
|
private static string DequeueNextMusicAddress()
|
|||
|
|
{
|
|||
|
|
if (musicAllList is null || !musicAllList.Any() || !musicWaitingList.Any()) MusicListInit(false);
|
|||
|
|
|
|||
|
|
if (musicWaitingList.Any())
|
|||
|
|
{
|
|||
|
|
var nextMusic = musicWaitingList[0];
|
|||
|
|
musicWaitingList.RemoveAt(0);
|
|||
|
|
if (!musicWaitingList.Any()) MusicListInit(false, nextMusic);
|
|||
|
|
return nextMusic;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region UI 控制
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 刷新背景音乐按钮 UI 与设置页 UI。
|
|||
|
|
/// </summary>
|
|||
|
|
private static void MusicRefreshUI()
|
|||
|
|
{
|
|||
|
|
ModBase.RunInUi(() =>
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
if ((musicAllList?.Any() is { } arg1 ? arg1 : null) == false)
|
|||
|
|
{
|
|||
|
|
ModMain.frmMain.BtnExtraMusic.Show = false;
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
ModMain.frmMain.BtnExtraMusic.Show = true;
|
|||
|
|
var fileName = ModBase.GetFileNameWithoutExtentionFromPath(musicCurrent);
|
|||
|
|
var isSingle = musicAllList.Count == 1;
|
|||
|
|
string tipText;
|
|||
|
|
if (MusicState == MusicStates.Pause)
|
|||
|
|
{
|
|||
|
|
ModMain.frmMain.BtnExtraMusic.SvgIcon = "lucide/play";
|
|||
|
|
ModMain.frmMain.BtnExtraMusic.LogoScale = 0.8d;
|
|||
|
|
tipText = Lang.Text(
|
|||
|
|
isSingle
|
|||
|
|
? "Music.ToolTip.Paused.Single"
|
|||
|
|
: "Music.ToolTip.Paused.Multiple",
|
|||
|
|
fileName);
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
ModMain.frmMain.BtnExtraMusic.SvgIcon = "lucide/music";
|
|||
|
|
ModMain.frmMain.BtnExtraMusic.LogoScale = 1d;
|
|||
|
|
tipText = Lang.Text(
|
|||
|
|
isSingle
|
|||
|
|
? "Music.ToolTip.Playing.Single"
|
|||
|
|
: "Music.ToolTip.Playing.Multiple",
|
|||
|
|
fileName);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ModMain.frmMain.BtnExtraMusic.ToolTip = tipText;
|
|||
|
|
ToolTipService.SetVerticalOffset(ModMain.frmMain.BtnExtraMusic,
|
|||
|
|
tipText.Contains("\n") ? 10 : 16);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ModMain.frmSetupUI?.MusicRefreshUI();
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
ModBase.Log(
|
|||
|
|
ex,
|
|||
|
|
"刷新背景音乐 UI 失败",
|
|||
|
|
ModBase.LogLevel.Feedback,
|
|||
|
|
userSummary: Lang.Text("Music.Error.OperationFailed"));
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static void MusicControlPause()
|
|||
|
|
{
|
|||
|
|
if (musicNAudio is null)
|
|||
|
|
{
|
|||
|
|
HintService.Hint(Lang.Text("Music.Error.NotStarted"), HintType.Error);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
switch (MusicState)
|
|||
|
|
{
|
|||
|
|
case MusicStates.Pause:
|
|||
|
|
{
|
|||
|
|
MusicResume();
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
case MusicStates.Play:
|
|||
|
|
{
|
|||
|
|
MusicPause(); // Stop
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
default:
|
|||
|
|
{
|
|||
|
|
ModBase.Log("[Music] 音乐目前为停止状态,已强制尝试开始播放", ModBase.LogLevel.Debug);
|
|||
|
|
MusicRefreshPlay(false);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static void MusicControlNext()
|
|||
|
|
{
|
|||
|
|
if (musicAllList?.Count is { } arg2 && arg2 == 1)
|
|||
|
|
{
|
|||
|
|
MusicStartPlay(musicCurrent);
|
|||
|
|
HintService.Hint(Lang.Text("Music.Hint.Replaying", ModBase.GetFileNameFromPath(musicCurrent)),
|
|||
|
|
HintType.Success);
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
var addr = DequeueNextMusicAddress();
|
|||
|
|
if (addr is null)
|
|||
|
|
{
|
|||
|
|
HintService.Hint(Lang.Text("Music.Error.NoAvailable"), HintType.Error);
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
MusicStartPlay(addr);
|
|||
|
|
HintService.Hint(Lang.Text("Music.Hint.Playing", ModBase.GetFileNameFromPath(addr)),
|
|||
|
|
HintType.Success);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
MusicRefreshUI();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
|
|||
|
|
#region 主状态控制
|
|||
|
|
|
|||
|
|
public static MusicStates MusicState
|
|||
|
|
{
|
|||
|
|
get
|
|||
|
|
{
|
|||
|
|
if (musicNAudio is null)
|
|||
|
|
return MusicStates.Stop;
|
|||
|
|
return musicNAudio.PlaybackState == PlaybackState.Paused ? MusicStates.Pause :
|
|||
|
|
musicNAudio.PlaybackState == PlaybackState.Stopped ? MusicStates.Stop : MusicStates.Play;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public enum MusicStates
|
|||
|
|
{
|
|||
|
|
Stop,
|
|||
|
|
Play,
|
|||
|
|
Pause
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static void MusicRefreshPlay(bool showHint, bool isFirstLoad = false)
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
MusicListInit(true);
|
|||
|
|
|
|||
|
|
if ((musicAllList?.Any() is { } arg3 ? arg3 : null) == false)
|
|||
|
|
{
|
|||
|
|
if (musicNAudio is not null)
|
|||
|
|
{
|
|||
|
|
musicNAudio = null;
|
|||
|
|
if (showHint)
|
|||
|
|
HintService.Hint(Lang.Text("Music.Hint.Cleared"), HintType.Success);
|
|||
|
|
}
|
|||
|
|
else if (showHint)
|
|||
|
|
{
|
|||
|
|
HintService.Hint(Lang.Text("Music.Error.NotDetected"), HintType.Error);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
var addr = DequeueNextMusicAddress();
|
|||
|
|
if (addr is null)
|
|||
|
|
{
|
|||
|
|
if (showHint)
|
|||
|
|
HintService.Hint(Lang.Text("Music.Error.NoAvailable"), HintType.Error);
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
MusicStartPlay(addr, isFirstLoad);
|
|||
|
|
if (showHint)
|
|||
|
|
HintService.Hint(
|
|||
|
|
Lang.Text("Music.Hint.Refreshed", ModBase.GetFileNameFromPath(addr)),
|
|||
|
|
HintType.Success,
|
|||
|
|
false);
|
|||
|
|
}
|
|||
|
|
catch
|
|||
|
|
{
|
|||
|
|
// 容错:播放失败已在 MusicLoop 中处理
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
MusicRefreshUI();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
ModBase.Log(
|
|||
|
|
ex,
|
|||
|
|
"刷新背景音乐播放失败",
|
|||
|
|
ModBase.LogLevel.Feedback,
|
|||
|
|
userSummary: Lang.Text("Music.Error.OperationFailed"));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static void MusicStartPlay(string address, bool isFirstLoad = false)
|
|||
|
|
{
|
|||
|
|
if (string.IsNullOrEmpty(address))
|
|||
|
|
return;
|
|||
|
|
ModBase.Log("[Music] 播放开始:" + address);
|
|||
|
|
musicCurrent = address;
|
|||
|
|
ModBase.RunInNewThread(() => MusicLoop(isFirstLoad), "Music", ThreadPriority.BelowNormal);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static bool MusicPause()
|
|||
|
|
{
|
|||
|
|
if (MusicState != MusicStates.Play)
|
|||
|
|
{
|
|||
|
|
ModBase.Log($"[Music] 无需暂停播放,当前状态为 {MusicState}");
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ModBase.RunInThread(() =>
|
|||
|
|
{
|
|||
|
|
ModBase.Log("[Music] 已暂停播放");
|
|||
|
|
musicNAudio?.Pause();
|
|||
|
|
MusicRefreshUI();
|
|||
|
|
});
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public static bool MusicResume()
|
|||
|
|
{
|
|||
|
|
if (MusicState == MusicStates.Play || musicAllList.Count == 0)
|
|||
|
|
{
|
|||
|
|
ModBase.Log($"[Music] 无需继续播放,当前状态为 {MusicState}");
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ModBase.RunInThread(() =>
|
|||
|
|
{
|
|||
|
|
ModBase.Log("[Music] 已恢复播放");
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
musicNAudio?.Play();
|
|||
|
|
}
|
|||
|
|
catch
|
|||
|
|
{
|
|||
|
|
// 参考 PR #5415:设备变更后需 Stop + Play
|
|||
|
|
musicNAudio?.Stop();
|
|||
|
|
musicNAudio?.Play();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
MusicRefreshUI();
|
|||
|
|
});
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#endregion
|
|||
|
|
}
|