Web-based Winamp controller for CarPC � Go backend, mobile-first UI
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

338 lines
8.4KB

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