初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
@@ -0,0 +1,472 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user