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.

362 lines
9.2KB

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