Web-based Winamp controller for CarPC � Go backend, mobile-first UI
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

227 lines
5.1KB

  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. "syscall"
  9. "unsafe"
  10. )
  11. var (
  12. user32 = syscall.NewLazyDLL("user32.dll")
  13. findWindow = user32.NewProc("FindWindowW")
  14. sendMessage = user32.NewProc("SendMessageW")
  15. getWindowTextW = user32.NewProc("GetWindowTextW")
  16. )
  17. const (
  18. wmCommand = 0x0111
  19. wmUser = 0x0400
  20. // Winamp WM_COMMAND IDs
  21. cmdPrevTrack = 40044
  22. cmdPlay = 40045
  23. cmdPause = 40046
  24. cmdStop = 40047
  25. cmdNextTrack = 40048
  26. cmdFadeStop = 40147
  27. cmdStopAfter = 40157
  28. cmdToggleRepeat = 40022
  29. cmdToggleShuffle = 40023
  30. cmdClose = 40001
  31. cmdVolumeUp = 40058
  32. cmdVolumeDown = 40059
  33. // Winamp WM_USER lParam IDs
  34. userGetVersion = 0
  35. userGetPlayState = 104
  36. userGetPosition = 105
  37. userSeek = 106
  38. userSetVolume = 122
  39. userGetPlaylistPos = 125
  40. userGetPlaylistLen = 124
  41. userRestart = 135
  42. )
  43. // Controller talks to a running Winamp instance.
  44. type Controller struct{}
  45. func New() *Controller { return &Controller{} }
  46. func (c *Controller) handle() syscall.Handle {
  47. winampClass, _ := syscall.UTF16PtrFromString("Winamp v1.x")
  48. h, _, _ := findWindow.Call(
  49. uintptr(unsafe.Pointer(winampClass)),
  50. 0,
  51. )
  52. return syscall.Handle(h)
  53. }
  54. func (c *Controller) IsRunning() bool {
  55. return c.handle() != 0
  56. }
  57. func send(h syscall.Handle, msg, wparam, lparam uintptr) uintptr {
  58. r, _, _ := sendMessage.Call(uintptr(h), msg, wparam, lparam)
  59. return r
  60. }
  61. func (c *Controller) cmd(id uintptr) bool {
  62. h := c.handle()
  63. if h == 0 {
  64. return false
  65. }
  66. send(h, wmCommand, id, 0)
  67. return true
  68. }
  69. func (c *Controller) user(wparam, lparam uintptr) (uintptr, bool) {
  70. h := c.handle()
  71. if h == 0 {
  72. return 0, false
  73. }
  74. return send(h, wmUser, wparam, lparam), true
  75. }
  76. // Playback controls
  77. func (c *Controller) Play() bool { return c.cmd(cmdPlay) }
  78. func (c *Controller) Pause() bool { return c.cmd(cmdPause) }
  79. func (c *Controller) Stop() bool { return c.cmd(cmdStop) }
  80. func (c *Controller) NextTrack() bool { return c.cmd(cmdNextTrack) }
  81. func (c *Controller) PrevTrack() bool { return c.cmd(cmdPrevTrack) }
  82. func (c *Controller) Close() bool { return c.cmd(cmdClose) }
  83. // State: 0=stopped, 1=playing, 3=paused
  84. func (c *Controller) PlayState() int {
  85. v, ok := c.user(0, userGetPlayState)
  86. if !ok {
  87. return 0
  88. }
  89. return int(v)
  90. }
  91. func (c *Controller) IsPlaying() bool { return c.PlayState() == 1 }
  92. func (c *Controller) IsPaused() bool { return c.PlayState() == 3 }
  93. func (c *Controller) IsStopped() bool { return c.PlayState() == 0 }
  94. // GetPosition returns current playback offset in seconds.
  95. func (c *Controller) GetPosition() int {
  96. v, ok := c.user(0, userGetPosition)
  97. if !ok {
  98. return 0
  99. }
  100. return int(v) / 1000
  101. }
  102. // GetLength returns total track length in seconds.
  103. func (c *Controller) GetLength() int {
  104. v, ok := c.user(1, userGetPosition)
  105. if !ok {
  106. return 0
  107. }
  108. return int(v)
  109. }
  110. // Seek sets the playback position to offsetSeconds.
  111. func (c *Controller) Seek(offsetSeconds int) bool {
  112. h := c.handle()
  113. if h == 0 {
  114. return false
  115. }
  116. send(h, wmUser, uintptr(offsetSeconds*1000), userSeek)
  117. return true
  118. }
  119. // SetVolume sets Winamp's internal volume (0–255).
  120. func (c *Controller) SetVolume(v int) bool {
  121. if v < 0 {
  122. v = 0
  123. }
  124. if v > 255 {
  125. v = 255
  126. }
  127. h := c.handle()
  128. if h == 0 {
  129. return false
  130. }
  131. send(h, wmUser, uintptr(v), userSetVolume)
  132. return true
  133. }
  134. // GetPlaylistPosition returns the 1-based current playlist index.
  135. func (c *Controller) GetPlaylistPosition() int {
  136. v, ok := c.user(0, userGetPlaylistPos)
  137. if !ok {
  138. return 0
  139. }
  140. return int(v) + 1
  141. }
  142. // GetPlaylistLength returns the total number of tracks in the playlist.
  143. func (c *Controller) GetPlaylistLength() int {
  144. v, ok := c.user(0, userGetPlaylistLen)
  145. if !ok {
  146. return 0
  147. }
  148. return int(v)
  149. }
  150. // GetVersion returns a human-readable Winamp version string (e.g. "5.66").
  151. func (c *Controller) GetVersion() string {
  152. v, ok := c.user(0, userGetVersion)
  153. if !ok {
  154. return ""
  155. }
  156. hex := fmt.Sprintf("%04X", v)
  157. if len(hex) < 3 {
  158. return ""
  159. }
  160. return string(hex[0]) + "." + hex[1:3]
  161. }
  162. // GetTitle returns the title of the currently playing track by reading
  163. // the Winamp window title (format: "N. Artist - Track - Winamp").
  164. func (c *Controller) GetTitle() string {
  165. h := c.handle()
  166. if h == 0 {
  167. return ""
  168. }
  169. buf := make([]uint16, 512)
  170. getWindowTextW.Call(uintptr(h), uintptr(unsafe.Pointer(&buf[0])), 512)
  171. title := syscall.UTF16ToString(buf)
  172. // Strip trailing " - Winamp" suffix
  173. const suffix = " - Winamp"
  174. if idx := lastIndex(title, suffix); idx >= 0 {
  175. title = title[:idx]
  176. }
  177. // Strip leading playlist-number prefix "NNN. "
  178. if idx := indexOf(title, ". "); idx >= 0 {
  179. title = title[idx+2:]
  180. }
  181. return title
  182. }
  183. func lastIndex(s, sub string) int {
  184. last := -1
  185. for i := 0; i <= len(s)-len(sub); i++ {
  186. if s[i:i+len(sub)] == sub {
  187. last = i
  188. }
  189. }
  190. return last
  191. }
  192. func indexOf(s, sub string) int {
  193. for i := 0; i <= len(s)-len(sub); i++ {
  194. if s[i:i+len(sub)] == sub {
  195. return i
  196. }
  197. }
  198. return -1
  199. }