ModMusic.vb 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  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="IgnoreFirst">在重载列表时避免让某项成为第一项。</param>
  16. Private Sub MusicListInit(ForceReload As Boolean, Optional IgnoreFirst As String = "")
  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.ToLower
  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(Setup.Get("UiMusicRandom"), Shuffle(New List(Of String)(MusicAllList)), New List(Of String)(MusicAllList))
  32. If Not IgnoreFirst = "" AndAlso Not Not MusicWaitingList.Any() AndAlso MusicWaitingList(0) = IgnoreFirst Then
  33. '若需要避免成为第一项的为第一项,则将它放在最后
  34. MusicWaitingList.RemoveAt(0)
  35. MusicWaitingList.Add(IgnoreFirst)
  36. End If
  37. Catch ex As Exception
  38. Log(ex, "初始化音乐列表失败", LogLevel.Feedback)
  39. End Try
  40. End Sub
  41. ''' <summary>
  42. ''' 获取下一首播放的音乐路径并将其从列表中移除。
  43. ''' </summary>
  44. Private Function DequeueNextMusicAddress() As String
  45. '初始化,确保存在音乐
  46. If MusicAllList Is Nothing OrElse Not MusicAllList.Any() OrElse Not MusicWaitingList.Any() Then MusicListInit(False)
  47. '出列下一个音乐,如果出列结束则生成新列表
  48. If MusicWaitingList.Any() Then
  49. DequeueNextMusicAddress = MusicWaitingList(0)
  50. MusicWaitingList.RemoveAt(0)
  51. If Not MusicWaitingList.Any() Then MusicListInit(False, DequeueNextMusicAddress)
  52. Else
  53. DequeueNextMusicAddress = ""
  54. End If
  55. End Function
  56. #End Region
  57. #Region "UI 控制"
  58. ''' <summary>
  59. ''' 刷新背景音乐按钮 UI 与设置页 UI。
  60. ''' </summary>
  61. Private Sub MusicRefreshUI()
  62. RunInUi(Sub()
  63. Try
  64. If Not MusicAllList.Any() Then
  65. '无背景音乐
  66. FrmMain.BtnExtraMusic.Show = False
  67. Else
  68. '有背景音乐
  69. FrmMain.BtnExtraMusic.Show = True
  70. Dim ToolTipText As String
  71. If MusicState = MusicStates.Pause Then
  72. FrmMain.BtnExtraMusic.Logo = Logo.IconPlay
  73. FrmMain.BtnExtraMusic.LogoScale = 0.8
  74. ToolTipText = "已暂停:" & GetFileNameWithoutExtentionFromPath(MusicCurrent)
  75. If MusicAllList.Count > 1 Then
  76. ToolTipText += vbCrLf & "左键恢复播放,右键播放下一曲。"
  77. Else
  78. ToolTipText += vbCrLf & "左键恢复播放,右键重新从头播放。"
  79. End If
  80. Else
  81. FrmMain.BtnExtraMusic.Logo = Logo.IconMusic
  82. FrmMain.BtnExtraMusic.LogoScale = 1
  83. ToolTipText = "正在播放:" & GetFileNameWithoutExtentionFromPath(MusicCurrent)
  84. If MusicAllList.Count > 1 Then
  85. ToolTipText += vbCrLf & "左键暂停,右键播放下一曲。"
  86. Else
  87. ToolTipText += vbCrLf & "左键暂停,右键重新从头播放。"
  88. End If
  89. End If
  90. FrmMain.BtnExtraMusic.ToolTip = ToolTipText
  91. ToolTipService.SetVerticalOffset(FrmMain.BtnExtraMusic, If(ToolTipText.Contains(vbLf), 10, 16))
  92. End If
  93. If FrmSetupUI IsNot Nothing Then FrmSetupUI.MusicRefreshUI()
  94. Catch ex As Exception
  95. Log(ex, "刷新背景音乐 UI 失败", LogLevel.Feedback)
  96. End Try
  97. End Sub)
  98. End Sub
  99. ''' <summary>
  100. ''' 让音乐在暂停、播放间切换,并显示提示文本。
  101. ''' </summary>
  102. Public Sub MusicControlPause()
  103. If MusicNAudio Is Nothing Then
  104. Hint("音乐播放尚未开始!", HintType.Critical)
  105. Else
  106. Select Case MusicState
  107. Case MusicStates.Pause
  108. MusicResume()
  109. Case MusicStates.Play
  110. MusicPause()
  111. Case Else
  112. Log("[Music] 音乐目前为停止状态,已强制尝试开始播放", LogLevel.Debug)
  113. MusicRefreshPlay(False)
  114. End Select
  115. End If
  116. End Sub
  117. ''' <summary>
  118. ''' 播放下一曲,并显示提示文本。
  119. ''' </summary>
  120. Public Sub MusicControlNext()
  121. If MusicAllList.Count = 1 Then
  122. MusicStartPlay(MusicCurrent)
  123. Hint("重新播放:" & GetFileNameFromPath(MusicCurrent), HintType.Finish)
  124. Else
  125. Dim Address As String = DequeueNextMusicAddress()
  126. MusicStartPlay(Address)
  127. Hint("正在播放:" & GetFileNameFromPath(Address), HintType.Finish)
  128. End If
  129. MusicRefreshUI()
  130. End Sub
  131. #End Region
  132. #Region "主状态控制"
  133. ''' <summary>
  134. ''' 获取当前的音乐播放状态。
  135. ''' </summary>
  136. Public ReadOnly Property MusicState As MusicStates
  137. Get
  138. If MusicNAudio Is Nothing Then Return MusicStates.Stop
  139. Select Case MusicNAudio.PlaybackState
  140. Case 0 'NAudio.Wave.PlaybackState.Stopped
  141. Return MusicStates.Stop
  142. Case 2 'NAudio.Wave.PlaybackState.Paused
  143. Return MusicStates.Pause
  144. Case Else
  145. Return MusicStates.Play
  146. End Select
  147. End Get
  148. End Property
  149. Public Enum MusicStates
  150. [Stop]
  151. Play
  152. Pause
  153. End Enum
  154. '重载与开始
  155. ''' <summary>
  156. ''' 重载播放列表并尝试开始播放背景音乐。
  157. ''' </summary>
  158. ''' <param name="ShowHint">是否显示刷新提示。</param>
  159. Public Sub MusicRefreshPlay(ShowHint As Boolean, Optional IsFirstLoad As Boolean = False)
  160. Try
  161. MusicListInit(True)
  162. If Not MusicAllList.Any() Then
  163. If MusicNAudio Is Nothing Then
  164. If ShowHint Then Hint("未检测到可用的背景音乐!", HintType.Critical)
  165. Else
  166. MusicNAudio = Nothing
  167. If ShowHint Then Hint("背景音乐已清除!", HintType.Finish)
  168. End If
  169. Else
  170. Dim Address As String = DequeueNextMusicAddress()
  171. Try
  172. MusicStartPlay(Address, IsFirstLoad)
  173. If ShowHint Then Hint("背景音乐已刷新:" & GetFileNameFromPath(Address), HintType.Finish, False)
  174. Catch
  175. End Try
  176. End If
  177. MusicRefreshUI()
  178. Catch ex As Exception
  179. Log(ex, "刷新背景音乐播放失败", LogLevel.Feedback)
  180. End Try
  181. End Sub
  182. ''' <summary>
  183. ''' 开始播放音乐。
  184. ''' </summary>
  185. Private Sub MusicStartPlay(Address As String, Optional IsFirstLoad As Boolean = False)
  186. If Not Address = "" Then
  187. Log("[Music] 播放开始:" & Address)
  188. MusicCurrent = Address
  189. RunInNewThread(Sub() MusicLoop(IsFirstLoad), "Music", ThreadPriority.BelowNormal)
  190. End If
  191. End Sub
  192. '播放与暂停
  193. ''' <summary>
  194. ''' 暂停音乐播放,返回是否成功切换了状态。
  195. ''' </summary>
  196. Public Function MusicPause() As Boolean
  197. If MusicState = MusicStates.Play Then
  198. RunInThread(
  199. Sub()
  200. Log("[Music] 已暂停播放")
  201. MusicNAudio?.Pause()
  202. MusicRefreshUI()
  203. End Sub)
  204. Return True
  205. Else
  206. Log($"[Music] 无需暂停播放,当前状态为 {MusicState}")
  207. Return False
  208. End If
  209. End Function
  210. ''' <summary>
  211. ''' 继续音乐播放,返回是否成功切换了状态。
  212. ''' </summary>
  213. Public Function MusicResume() As Boolean
  214. If MusicState = MusicStates.Play OrElse Not MusicAllList.Any() Then
  215. Log($"[Music] 无需继续播放,当前状态为 {MusicState}")
  216. Return False
  217. Else
  218. RunInThread(
  219. Sub()
  220. Log("[Music] 已恢复播放")
  221. MusicNAudio?.Play()
  222. MusicRefreshUI()
  223. End Sub)
  224. Return True
  225. End If
  226. End Function
  227. #End Region
  228. ''' <summary>
  229. ''' 当前正在播放的 NAudio.Wave.WaveOut。
  230. ''' </summary>
  231. Public MusicNAudio = Nothing
  232. ''' <summary>
  233. ''' 当前播放的音乐地址。
  234. ''' </summary>
  235. Private MusicCurrent As String = ""
  236. ''' <summary>
  237. ''' 在 MusicUuid 不变的前提下,持续播放某地址的音乐,且在播放结束后随机播放下一曲。
  238. ''' </summary>
  239. Private Sub MusicLoop(Optional IsFirstLoad As Boolean = False)
  240. Dim CurrentWave As NAudio.Wave.WaveOut = Nothing
  241. Dim Reader As NAudio.Wave.WaveStream = Nothing
  242. Try
  243. '开始播放
  244. CurrentWave = New NAudio.Wave.WaveOut()
  245. MusicNAudio = CurrentWave
  246. Reader = New NAudio.Wave.AudioFileReader(MusicCurrent)
  247. CurrentWave.Init(Reader)
  248. CurrentWave.Play()
  249. '第一次打开的暂停
  250. If IsFirstLoad AndAlso Not Setup.Get("UiMusicAuto") Then CurrentWave.Pause()
  251. MusicRefreshUI()
  252. '停止条件:播放完毕或变化
  253. Dim PreviousVolume = 0
  254. While CurrentWave.Equals(MusicNAudio) AndAlso Not CurrentWave.PlaybackState = NAudio.Wave.PlaybackState.Stopped
  255. If Setup.Get("UiMusicVolume") <> PreviousVolume Then
  256. '更新音量
  257. PreviousVolume = Setup.Get("UiMusicVolume")
  258. CurrentWave.Volume = PreviousVolume / 1000
  259. End If
  260. '更新进度条
  261. Dim Percent = Reader.CurrentTime.TotalMilliseconds / Reader.TotalTime.TotalMilliseconds
  262. RunInUi(Sub() FrmMain.BtnExtraMusic.Progress = Percent)
  263. Thread.Sleep(100)
  264. End While
  265. '当前音乐已播放结束,继续下一曲
  266. If CurrentWave.PlaybackState = NAudio.Wave.PlaybackState.Stopped AndAlso MusicAllList.Any Then MusicStartPlay(DequeueNextMusicAddress)
  267. Catch ex As Exception
  268. Log(ex, "播放音乐出现内部错误(" & MusicCurrent & ")", LogLevel.Developer)
  269. If TypeOf ex Is NAudio.MmException AndAlso (ex.Message.Contains("NoDriver") OrElse ex.Message.Contains("BadDeviceId")) Then
  270. Hint("由于音频设备变更,音乐播放功能在重启 PCL 后才能恢复!", HintType.Critical)
  271. Thread.Sleep(1000000000)
  272. End If
  273. If ex.Message.Contains("Got a frame at sample rate") OrElse ex.Message.Contains("does not support changes to") Then
  274. Hint("播放音乐失败(" & GetFileNameFromPath(MusicCurrent) & "):PCL 不支持播放音频属性在中途发生变化的音乐", HintType.Critical)
  275. ElseIf Not (MusicCurrent.EndsWithF(".wav", True) OrElse MusicCurrent.EndsWithF(".mp3", True) OrElse MusicCurrent.EndsWithF(".flac", True)) Then
  276. Hint("播放音乐失败(" & GetFileNameFromPath(MusicCurrent) & "):PCL 可能不支持此音乐格式,请将格式转换为 .wav、.mp3 或 .flac 后再试", HintType.Critical)
  277. Else
  278. Log(ex, "播放音乐失败(" & GetFileNameFromPath(MusicCurrent) & ")", LogLevel.Hint)
  279. End If
  280. '将播放错误的音乐从列表中移除
  281. MusicAllList.Remove(MusicCurrent)
  282. MusicWaitingList.Remove(MusicCurrent)
  283. If TypeOf ex Is FileNotFoundException Then
  284. MusicRefreshPlay(True, IsFirstLoad)
  285. Else
  286. MusicStartPlay(DequeueNextMusicAddress(), IsFirstLoad)
  287. End If
  288. Finally
  289. If CurrentWave IsNot Nothing Then CurrentWave.Dispose()
  290. If Reader IsNot Nothing Then Reader.Dispose()
  291. MusicRefreshUI()
  292. End Try
  293. End Sub
  294. End Module