Files
MRCC/launcher/PCL-CE/Plain Craft Launcher 2/Modules/ModMusic.cs
T
xyou f70b061d1a
CI / Go Backend (push) Canceled after 0s
feat: 项目初始化 + 3D方块世界原型 + AI助搭系统
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器

HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块

Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚

AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
2026-08-08 14:07:56 +08:00

473 lines
15 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.
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
}