ModMusic.vb 16 KB

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