ModMusic.vb 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. Public Module ModMusic
  2. #Region "播放列表"
  3. ''' <summary>
  4. ''' 接下来要播放的音乐文件路径。未初始化时为 Nothing。
  5. ''' </summary>
  6. Public MusicWaitingList As List(Of String) = Nothing
  7. ''' <summary>
  8. ''' 全部音乐文件路径。未初始化时为 Nothing。
  9. ''' </summary>
  10. Public MusicAllList As List(Of String) = Nothing
  11. ''' <summary>
  12. ''' 初始化音乐播放列表。
  13. ''' </summary>
  14. ''' <param name="ForceReload">强制全部重新载入列表。</param>
  15. ''' <param name="PreventFirst">在重载列表时避免让某项成为第一项。</param>
  16. Private Sub MusicListInit(ForceReload As Boolean, Optional PreventFirst As String = Nothing)
  17. If ForceReload Then MusicAllList = Nothing
  18. Try
  19. '初始化全部可用音乐列表
  20. If MusicAllList Is Nothing Then
  21. MusicAllList = New List(Of String)
  22. Directory.CreateDirectory(Path & "PCL\Musics\")
  23. For Each File In EnumerateFiles(Path & "PCL\Musics\")
  24. '文件夹可能会被加入 .ini 文件夹配置文件、一些乱七八糟的 .jpg 文件啥的
  25. Dim Ext As String = File.Extension.Lower
  26. If {".ini", ".jpg", ".txt", ".cfg", ".lrc", ".db", ".png"}.Contains(Ext) Then Continue For
  27. MusicAllList.Add(File.FullName)
  28. Next
  29. End If
  30. '打乱顺序播放
  31. MusicWaitingList = If(Settings.Get("UiMusicRandom"), New List(Of String)(MusicAllList).Shuffle().ToList, New List(Of String)(MusicAllList))
  32. If PreventFirst IsNot Nothing AndAlso MusicWaitingList.FirstOrDefault = PreventFirst Then
  33. '若需要避免成为第一项的为第一项,则将它放在最后
  34. MusicWaitingList.RemoveAt(0)
  35. MusicWaitingList.Add(PreventFirst)
  36. End If
  37. Catch ex As Exception
  38. Log(ex, "初始化音乐列表失败", LogLevel.Feedback)
  39. End Try
  40. End Sub
  41. ''' <summary>
  42. ''' 获取下一首播放的音乐路径并将其从列表中移除。
  43. ''' 如果没有,可能会返回 Nothing。
  44. ''' </summary>
  45. Private Function DequeueNextMusicAddress() As String
  46. '初始化,确保存在音乐
  47. If MusicAllList Is Nothing OrElse Not MusicAllList.Any() OrElse Not MusicWaitingList.Any() Then MusicListInit(False)
  48. '出列下一个音乐,如果出列结束则生成新列表
  49. If MusicWaitingList.Any() Then
  50. DequeueNextMusicAddress = MusicWaitingList(0)
  51. MusicWaitingList.RemoveAt(0)
  52. Else
  53. DequeueNextMusicAddress = Nothing
  54. End If
  55. If Not MusicWaitingList.Any() Then MusicListInit(False, DequeueNextMusicAddress)
  56. End Function
  57. #End Region
  58. #Region "UI 控制"
  59. ''' <summary>
  60. ''' 刷新背景音乐按钮 UI 与设置页 UI。
  61. ''' </summary>
  62. Private Sub MusicRefreshUI()
  63. RunInUi(
  64. Sub()
  65. Try
  66. If Not MusicAllList.Any() Then
  67. '无背景音乐
  68. FrmMain.BtnExtraMusic.Show = False
  69. Else
  70. '有背景音乐
  71. FrmMain.BtnExtraMusic.Show = True
  72. Dim ToolTipText As String
  73. If MusicState = MusicStates.Pause Then
  74. FrmMain.BtnExtraMusic.Logo = Logo.IconPlay
  75. FrmMain.BtnExtraMusic.LogoScale = 0.8
  76. ToolTipText = "已暂停:" & IO.Path.GetFileNameWithoutExtension(MusicCurrent)
  77. If MusicAllList.Count > 1 Then
  78. ToolTipText += vbCrLf & "左键恢复播放,右键播放下一曲。"
  79. Else
  80. ToolTipText += vbCrLf & "左键恢复播放,右键重新从头播放。"
  81. End If
  82. Else
  83. FrmMain.BtnExtraMusic.Logo = Logo.IconMusic
  84. FrmMain.BtnExtraMusic.LogoScale = 1
  85. ToolTipText = "正在播放:" & IO.Path.GetFileNameWithoutExtension(MusicCurrent)
  86. If MusicAllList.Count > 1 Then
  87. ToolTipText += vbCrLf & "左键暂停,右键播放下一曲。"
  88. Else
  89. ToolTipText += vbCrLf & "左键暂停,右键重新从头播放。"
  90. End If
  91. End If
  92. FrmMain.BtnExtraMusic.ToolTip = ToolTipText
  93. ToolTipService.SetVerticalOffset(FrmMain.BtnExtraMusic, If(ToolTipText.Contains(vbLf), 10, 16))
  94. End If
  95. If FrmSetupUI IsNot Nothing Then FrmSetupUI.MusicRefreshUI()
  96. Catch ex As Exception
  97. Log(ex, "刷新背景音乐 UI 失败", LogLevel.Feedback)
  98. End Try
  99. End Sub)
  100. End Sub
  101. ''' <summary>
  102. ''' 让音乐在暂停、播放间切换,并显示提示文本。
  103. ''' </summary>
  104. Public Sub MusicControlPause()
  105. If MusicNAudio Is Nothing Then
  106. Hint("音乐播放尚未开始!", HintType.Red)
  107. Else
  108. Select Case MusicState
  109. Case MusicStates.Pause
  110. MusicResume()
  111. Case MusicStates.Play
  112. MusicPause()
  113. Case Else
  114. Log("[Music] 音乐目前为停止状态,已强制尝试开始播放", LogLevel.Debug)
  115. MusicRefreshPlay(False)
  116. End Select
  117. End If
  118. End Sub
  119. ''' <summary>
  120. ''' 播放下一曲,并显示提示文本。
  121. ''' </summary>
  122. Public Sub MusicControlNext()
  123. If MusicAllList.IsSingle Then
  124. MusicStartPlay(MusicCurrent)
  125. Hint("重新播放:" & GetFileNameFromPath(MusicCurrent), HintType.Green)
  126. Else
  127. Dim Address As String = DequeueNextMusicAddress()
  128. If Address Is Nothing Then
  129. Hint("没有可以播放的音乐!", HintType.Red)
  130. Else
  131. MusicStartPlay(Address)
  132. Hint("正在播放:" & GetFileNameFromPath(Address), HintType.Green)
  133. End If
  134. End If
  135. MusicRefreshUI()
  136. End Sub
  137. #End Region
  138. #Region "主状态控制"
  139. ''' <summary>
  140. ''' 获取当前的音乐播放状态。
  141. ''' </summary>
  142. Public ReadOnly Property MusicState As MusicStates
  143. Get
  144. If MusicNAudio Is Nothing Then Return MusicStates.Stop
  145. Select Case MusicNAudio.PlaybackState
  146. Case 0 'NAudio.Wave.PlaybackState.Stopped
  147. Return MusicStates.Stop
  148. Case 2 'NAudio.Wave.PlaybackState.Paused
  149. Return MusicStates.Pause
  150. Case Else
  151. Return MusicStates.Play
  152. End Select
  153. End Get
  154. End Property
  155. Public Enum MusicStates
  156. [Stop]
  157. Play
  158. Pause
  159. End Enum
  160. '重载与开始
  161. ''' <summary>
  162. ''' 重载播放列表并尝试开始播放背景音乐。
  163. ''' </summary>
  164. ''' <param name="ShowHint">是否显示刷新提示。</param>
  165. Public Sub MusicRefreshPlay(ShowHint As Boolean, Optional IsFirstLoad As Boolean = False)
  166. Try
  167. MusicListInit(True)
  168. If Not MusicAllList.Any() Then
  169. If MusicNAudio Is Nothing Then
  170. If ShowHint Then Hint("未检测到可用的背景音乐!", HintType.Red)
  171. Else
  172. MusicNAudio = Nothing
  173. If ShowHint Then Hint("背景音乐已清除!", HintType.Green)
  174. End If
  175. Else
  176. Dim Address As String = DequeueNextMusicAddress()
  177. If Address Is Nothing Then
  178. If ShowHint Then Hint("没有可以播放的音乐!", HintType.Red)
  179. Else
  180. Try
  181. MusicStartPlay(Address, IsFirstLoad)
  182. If ShowHint Then Hint("背景音乐已刷新:" & GetFileNameFromPath(Address), HintType.Green, False)
  183. Catch
  184. End Try
  185. End If
  186. End If
  187. MusicRefreshUI()
  188. Catch ex As Exception
  189. Log(ex, "刷新背景音乐播放失败", LogLevel.Feedback)
  190. End Try
  191. End Sub
  192. ''' <summary>
  193. ''' 开始播放音乐。
  194. ''' </summary>
  195. Private Sub MusicStartPlay(Address As String, Optional IsFirstLoad As Boolean = False)
  196. If Address Is Nothing Then Return
  197. Log("[Music] 播放开始:" & Address)
  198. MusicCurrent = Address
  199. RunInNewThread(Sub() MusicLoop(IsFirstLoad), "Music", ThreadPriority.BelowNormal)
  200. End Sub
  201. '播放与暂停
  202. ''' <summary>
  203. ''' 暂停音乐播放,返回是否成功切换了状态。
  204. ''' </summary>
  205. Public Function MusicPause() As Boolean
  206. If MusicState = MusicStates.Play Then
  207. RunInThread(
  208. Sub()
  209. Log("[Music] 已暂停播放")
  210. MusicNAudio?.Pause()
  211. MusicRefreshUI()
  212. End Sub)
  213. Return True
  214. Else
  215. Log($"[Music] 无需暂停播放,当前状态为 {MusicState}")
  216. Return False
  217. End If
  218. End Function
  219. ''' <summary>
  220. ''' 继续音乐播放,返回是否成功切换了状态。
  221. ''' </summary>
  222. Public Function MusicResume() As Boolean
  223. If MusicState = MusicStates.Play OrElse Not MusicAllList.Any() Then
  224. Log($"[Music] 无需继续播放,当前状态为 {MusicState}")
  225. Return False
  226. Else
  227. RunInThread(
  228. Sub()
  229. Log("[Music] 已恢复播放")
  230. Try
  231. MusicNAudio?.Play()
  232. Catch 'https://github.com/Meloong-Git/PCL/pull/5415#issuecomment-2751135223
  233. MusicNAudio?.Stop()
  234. MusicNAudio?.Play()
  235. End Try
  236. MusicRefreshUI()
  237. End Sub)
  238. Return True
  239. End If
  240. End Function
  241. #End Region
  242. ''' <summary>
  243. ''' 当前正在播放的 NAudio.Wave.WaveOutEvent。
  244. ''' 如果为它赋了类别,则会没有必要地加载 NAudio.dll。
  245. ''' </summary>
  246. Public MusicNAudio = Nothing
  247. ''' <summary>
  248. ''' 当前播放的音乐地址。
  249. ''' </summary>
  250. Private MusicCurrent As String = ""
  251. ''' <summary>
  252. ''' 在 MusicUuid 不变的前提下,持续播放某地址的音乐,且在播放结束后随机播放下一曲。
  253. ''' </summary>
  254. Private Sub MusicLoop(Optional IsFirstLoad As Boolean = False)
  255. Dim CurrentWave As NAudio.Wave.WaveOutEvent = Nothing
  256. Dim Reader As NAudio.Wave.WaveStream = Nothing
  257. Try
  258. '开始播放
  259. CurrentWave = New NAudio.Wave.WaveOutEvent()
  260. MusicNAudio = CurrentWave
  261. CurrentWave.DeviceNumber = -1
  262. Try
  263. Reader = New NAudio.Wave.AudioFileReader(MusicCurrent)
  264. Catch ex As Exception
  265. Log(ex, "使用 AudioFileReader 加载音频文件失败,换用 MediaFoundationReader 重试", LogLevel.Developer)
  266. Reader = New NAudio.Wave.MediaFoundationReader(MusicCurrent)
  267. End Try
  268. CurrentWave.Init(Reader)
  269. CurrentWave.Play()
  270. '第一次打开的暂停
  271. If IsFirstLoad AndAlso Not Settings.Get("UiMusicAuto") Then CurrentWave.Pause()
  272. MusicRefreshUI()
  273. '停止条件:播放完毕或变化
  274. Dim PreviousVolume = 0
  275. While CurrentWave.Equals(MusicNAudio) AndAlso Not CurrentWave.PlaybackState = NAudio.Wave.PlaybackState.Stopped
  276. If Settings.Get("UiMusicVolume") <> PreviousVolume Then
  277. '更新音量
  278. PreviousVolume = Settings.Get("UiMusicVolume")
  279. CurrentWave.Volume = PreviousVolume / 1000
  280. End If
  281. '更新进度条
  282. Dim Percent = Reader.CurrentTime.TotalMilliseconds / Reader.TotalTime.TotalMilliseconds
  283. RunInUi(Sub() FrmMain.BtnExtraMusic.Progress = Percent)
  284. Thread.Sleep(100)
  285. End While
  286. '当前音乐已播放结束,继续下一曲
  287. If CurrentWave.PlaybackState = NAudio.Wave.PlaybackState.Stopped AndAlso MusicAllList.Any Then MusicStartPlay(DequeueNextMusicAddress)
  288. Catch ex As Exception
  289. Log(ex, "播放音乐出现内部错误(" & MusicCurrent & ")", LogLevel.Developer)
  290. If TypeOf ex Is NAudio.MmException AndAlso ex.Message.Contains("AlreadyAllocated") Then
  291. Hint("你的音频设备正被其他程序占用。请在关闭占用的程序后重启 PCL,才能恢复音乐播放功能!", HintType.Red)
  292. Thread.Sleep(1000000000)
  293. End If
  294. If TypeOf ex Is NAudio.MmException AndAlso (ex.Message.Contains("NoDriver") OrElse ex.Message.Contains("BadDeviceId")) Then
  295. Hint("由于音频设备变更,音乐播放功能在重启 PCL 后才能恢复!", HintType.Red)
  296. Thread.Sleep(1000000000)
  297. End If
  298. If Not (MusicCurrent.EndsWithF(".wav", True) OrElse MusicCurrent.EndsWithF(".mp3", True) OrElse MusicCurrent.EndsWithF(".flac", True)) OrElse
  299. ex.Message.Contains("0xC00D36C4") Then '#5096:不支持给定的 URL 的字节流类型。 (异常来自 HRESULT:0xC00D36C4)
  300. Hint("播放音乐失败(" & GetFileNameFromPath(MusicCurrent) & "):PCL 可能不支持此音乐格式,请将格式转换为 .wav、.mp3 或 .flac 后再试", HintType.Red)
  301. Else
  302. Log(ex, "播放音乐失败(" & GetFileNameFromPath(MusicCurrent) & ")", LogLevel.Hint)
  303. End If
  304. '将播放错误的音乐从列表中移除
  305. MusicAllList.Remove(MusicCurrent)
  306. MusicWaitingList.Remove(MusicCurrent)
  307. MusicRefreshUI()
  308. '等待 2 秒后继续播放
  309. Thread.Sleep(2000)
  310. If TypeOf ex Is FileNotFoundException Then
  311. MusicRefreshPlay(True, IsFirstLoad)
  312. Else
  313. MusicStartPlay(DequeueNextMusicAddress(), IsFirstLoad)
  314. End If
  315. Finally
  316. If CurrentWave IsNot Nothing Then CurrentWave.Dispose()
  317. If Reader IsNot Nothing Then Reader.Dispose()
  318. MusicRefreshUI()
  319. End Try
  320. End Sub
  321. End Module