Web-based Winamp controller for CarPC � Go backend, mobile-first UI
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

305 рядки
7.6KB

  1. //go:build windows
  2. // Package winamp provides IPC control of a running Winamp instance via
  3. // Windows messages (WM_COMMAND / WM_USER), mirroring the TWinampControl
  4. // Delphi component by SpECTre.
  5. package winamp
  6. import (
  7. "fmt"
  8. "strings"
  9. "syscall"
  10. "unsafe"
  11. )
  12. var (
  13. user32 = syscall.NewLazyDLL("user32.dll")
  14. kernel32 = syscall.NewLazyDLL("kernel32.dll")
  15. findWindow = user32.NewProc("FindWindowW")
  16. sendMessage = user32.NewProc("SendMessageW")
  17. getWindowTextW = user32.NewProc("GetWindowTextW")
  18. getWindowThreadProcessId = user32.NewProc("GetWindowThreadProcessId")
  19. openProcess = kernel32.NewProc("OpenProcess")
  20. readProcessMemory = kernel32.NewProc("ReadProcessMemory")
  21. )
  22. const (
  23. wmCommand = 0x0111
  24. wmUser = 0x0400
  25. // Winamp WM_COMMAND IDs
  26. cmdPrevTrack = 40044
  27. cmdPlay = 40045
  28. cmdPause = 40046
  29. cmdStop = 40047
  30. cmdNextTrack = 40048
  31. cmdFadeStop = 40147
  32. cmdStopAfter = 40157
  33. cmdToggleRepeat = 40022
  34. cmdToggleShuffle = 40023
  35. cmdClose = 40001
  36. cmdVolumeUp = 40058
  37. cmdVolumeDown = 40059
  38. // Winamp WM_USER lParam IDs
  39. userGetVersion = 0
  40. userGetPlayState = 104
  41. userGetPosition = 105
  42. userSeek = 106
  43. userSetVolume = 122
  44. userGetPlaylistPos = 125
  45. userGetPlaylistLen = 124
  46. userSetPlaylistPos = 121
  47. userGetPlaylistTitle = 212
  48. userRestart = 135
  49. // OpenProcess access right
  50. processVMRead = 0x0010
  51. )
  52. // Controller talks to a running Winamp instance.
  53. type Controller struct{}
  54. func New() *Controller { return &Controller{} }
  55. func (c *Controller) handle() syscall.Handle {
  56. winampClass, _ := syscall.UTF16PtrFromString("Winamp v1.x")
  57. h, _, _ := findWindow.Call(
  58. uintptr(unsafe.Pointer(winampClass)),
  59. 0,
  60. )
  61. return syscall.Handle(h)
  62. }
  63. func (c *Controller) IsRunning() bool {
  64. return c.handle() != 0
  65. }
  66. func send(h syscall.Handle, msg, wparam, lparam uintptr) uintptr {
  67. r, _, _ := sendMessage.Call(uintptr(h), msg, wparam, lparam)
  68. return r
  69. }
  70. func (c *Controller) cmd(id uintptr) bool {
  71. h := c.handle()
  72. if h == 0 {
  73. return false
  74. }
  75. send(h, wmCommand, id, 0)
  76. return true
  77. }
  78. func (c *Controller) user(wparam, lparam uintptr) (uintptr, bool) {
  79. h := c.handle()
  80. if h == 0 {
  81. return 0, false
  82. }
  83. return send(h, wmUser, wparam, lparam), true
  84. }
  85. // Playback controls
  86. func (c *Controller) Play() bool { return c.cmd(cmdPlay) }
  87. func (c *Controller) Pause() bool { return c.cmd(cmdPause) }
  88. func (c *Controller) Stop() bool { return c.cmd(cmdStop) }
  89. func (c *Controller) NextTrack() bool { return c.cmd(cmdNextTrack) }
  90. func (c *Controller) PrevTrack() bool { return c.cmd(cmdPrevTrack) }
  91. func (c *Controller) Close() bool { return c.cmd(cmdClose) }
  92. // State: 0=stopped, 1=playing, 3=paused
  93. func (c *Controller) PlayState() int {
  94. v, ok := c.user(0, userGetPlayState)
  95. if !ok {
  96. return 0
  97. }
  98. return int(v)
  99. }
  100. func (c *Controller) IsPlaying() bool { return c.PlayState() == 1 }
  101. func (c *Controller) IsPaused() bool { return c.PlayState() == 3 }
  102. func (c *Controller) IsStopped() bool { return c.PlayState() == 0 }
  103. // GetPosition returns current playback offset in seconds.
  104. // Returns 0 when stopped (Winamp returns 0xFFFFFFFF in that state).
  105. func (c *Controller) GetPosition() int {
  106. v, ok := c.user(0, userGetPosition)
  107. if !ok || v > 0xF0000000 { // 0xFFFFFFFF = stopped/no track
  108. return 0
  109. }
  110. return int(v) / 1000
  111. }
  112. // GetLength returns total track length in seconds.
  113. func (c *Controller) GetLength() int {
  114. v, ok := c.user(1, userGetPosition)
  115. if !ok {
  116. return 0
  117. }
  118. return int(v)
  119. }
  120. // Seek sets the playback position to offsetSeconds.
  121. func (c *Controller) Seek(offsetSeconds int) bool {
  122. h := c.handle()
  123. if h == 0 {
  124. return false
  125. }
  126. send(h, wmUser, uintptr(offsetSeconds*1000), userSeek)
  127. return true
  128. }
  129. // SetVolume sets Winamp's internal volume (0–255).
  130. func (c *Controller) SetVolume(v int) bool {
  131. if v < 0 {
  132. v = 0
  133. }
  134. if v > 255 {
  135. v = 255
  136. }
  137. h := c.handle()
  138. if h == 0 {
  139. return false
  140. }
  141. send(h, wmUser, uintptr(v), userSetVolume)
  142. return true
  143. }
  144. // GetPlaylistPosition returns the 1-based current playlist index.
  145. func (c *Controller) GetPlaylistPosition() int {
  146. v, ok := c.user(0, userGetPlaylistPos)
  147. if !ok {
  148. return 0
  149. }
  150. return int(v) + 1
  151. }
  152. // GetPlaylistLength returns the total number of tracks in the playlist.
  153. func (c *Controller) GetPlaylistLength() int {
  154. v, ok := c.user(0, userGetPlaylistLen)
  155. if !ok {
  156. return 0
  157. }
  158. return int(v)
  159. }
  160. // GetVersion returns a human-readable Winamp version string (e.g. "5.66").
  161. func (c *Controller) GetVersion() string {
  162. v, ok := c.user(0, userGetVersion)
  163. if !ok {
  164. return ""
  165. }
  166. hex := fmt.Sprintf("%04X", v)
  167. if len(hex) < 3 {
  168. return ""
  169. }
  170. return string(hex[0]) + "." + hex[1:3]
  171. }
  172. // GetTitle returns the title of the currently playing track by reading
  173. // the Winamp window title.
  174. //
  175. // Winamp 5.x formats the window title as one of:
  176. //
  177. // "N. Artist - Title - Winamp" (playing)
  178. // "Winamp" (stopped, no playlist)
  179. //
  180. // We strip the " - Winamp" suffix and the leading "N. " playlist prefix.
  181. func (c *Controller) GetTitle() string {
  182. h := c.handle()
  183. if h == 0 {
  184. return ""
  185. }
  186. buf := make([]uint16, 512)
  187. getWindowTextW.Call(uintptr(h), uintptr(unsafe.Pointer(&buf[0])), 512)
  188. title := syscall.UTF16ToString(buf)
  189. // Strip " - Winamp" suffix (use last occurrence so track titles
  190. // containing " - Winamp" are handled correctly).
  191. const suffix = " - Winamp"
  192. if idx := strings.LastIndex(title, suffix); idx >= 0 {
  193. title = title[:idx]
  194. } else {
  195. // Title is just "Winamp" (stopped, empty playlist).
  196. return ""
  197. }
  198. // Strip leading playlist-number prefix: digits followed by ". "
  199. // e.g. "4. " or "12. "
  200. if dot := strings.Index(title, ". "); dot >= 0 && dot <= 4 {
  201. title = title[dot+2:]
  202. }
  203. return title
  204. }
  205. // ── Playlist ──────────────────────────────────────────────────────────────────
  206. // TrackInfo is one entry in the Winamp playlist.
  207. type TrackInfo struct {
  208. Index int `json:"index"` // 1-based
  209. Title string `json:"title"`
  210. }
  211. // readRemoteString reads a null-terminated ANSI string from another process.
  212. func readRemoteString(proc syscall.Handle, ptr uintptr) string {
  213. if ptr == 0 {
  214. return ""
  215. }
  216. buf := make([]byte, 512)
  217. var read uintptr
  218. readProcessMemory.Call(
  219. uintptr(proc),
  220. ptr,
  221. uintptr(unsafe.Pointer(&buf[0])),
  222. uintptr(len(buf)),
  223. uintptr(unsafe.Pointer(&read)),
  224. )
  225. for i, b := range buf {
  226. if b == 0 {
  227. return string(buf[:i])
  228. }
  229. }
  230. return string(buf)
  231. }
  232. // GetPlaylist returns all titles in the current Winamp playlist.
  233. // Titles are read from Winamp's process memory via ReadProcessMemory.
  234. func (c *Controller) GetPlaylist() []TrackInfo {
  235. h := c.handle()
  236. if h == 0 {
  237. return nil
  238. }
  239. n := int(send(h, wmUser, 0, userGetPlaylistLen))
  240. if n <= 0 {
  241. return nil
  242. }
  243. var pid uint32
  244. getWindowThreadProcessId.Call(uintptr(h), uintptr(unsafe.Pointer(&pid)))
  245. proc, _, _ := openProcess.Call(processVMRead, 0, uintptr(pid))
  246. if proc == 0 {
  247. return nil
  248. }
  249. defer syscall.CloseHandle(syscall.Handle(proc))
  250. tracks := make([]TrackInfo, n)
  251. for i := 0; i < n; i++ {
  252. ptr := send(h, wmUser, uintptr(i), userGetPlaylistTitle)
  253. tracks[i] = TrackInfo{
  254. Index: i + 1,
  255. Title: readRemoteString(syscall.Handle(proc), ptr),
  256. }
  257. }
  258. return tracks
  259. }
  260. // JumpToTrack sets the playlist position (0-based) and starts playback.
  261. func (c *Controller) JumpToTrack(zeroBasedIndex int) bool {
  262. h := c.handle()
  263. if h == 0 {
  264. return false
  265. }
  266. send(h, wmUser, uintptr(zeroBasedIndex), userSetPlaylistPos)
  267. send(h, wmCommand, cmdPlay, 0)
  268. return true
  269. }