//go:build windows // Package winamp provides IPC control of a running Winamp instance via // Windows messages (WM_COMMAND / WM_USER), mirroring the TWinampControl // Delphi component by SpECTre. package winamp import ( "fmt" "syscall" "unsafe" ) var ( user32 = syscall.NewLazyDLL("user32.dll") findWindow = user32.NewProc("FindWindowW") sendMessage = user32.NewProc("SendMessageW") getWindowTextW = user32.NewProc("GetWindowTextW") ) const ( wmCommand = 0x0111 wmUser = 0x0400 // Winamp WM_COMMAND IDs cmdPrevTrack = 40044 cmdPlay = 40045 cmdPause = 40046 cmdStop = 40047 cmdNextTrack = 40048 cmdFadeStop = 40147 cmdStopAfter = 40157 cmdToggleRepeat = 40022 cmdToggleShuffle = 40023 cmdClose = 40001 cmdVolumeUp = 40058 cmdVolumeDown = 40059 // Winamp WM_USER lParam IDs userGetVersion = 0 userGetPlayState = 104 userGetPosition = 105 userSeek = 106 userSetVolume = 122 userGetPlaylistPos = 125 userGetPlaylistLen = 124 userRestart = 135 ) // Controller talks to a running Winamp instance. type Controller struct{} func New() *Controller { return &Controller{} } func (c *Controller) handle() syscall.Handle { winampClass, _ := syscall.UTF16PtrFromString("Winamp v1.x") h, _, _ := findWindow.Call( uintptr(unsafe.Pointer(winampClass)), 0, ) return syscall.Handle(h) } func (c *Controller) IsRunning() bool { return c.handle() != 0 } func send(h syscall.Handle, msg, wparam, lparam uintptr) uintptr { r, _, _ := sendMessage.Call(uintptr(h), msg, wparam, lparam) return r } func (c *Controller) cmd(id uintptr) bool { h := c.handle() if h == 0 { return false } send(h, wmCommand, id, 0) return true } func (c *Controller) user(wparam, lparam uintptr) (uintptr, bool) { h := c.handle() if h == 0 { return 0, false } return send(h, wmUser, wparam, lparam), true } // Playback controls func (c *Controller) Play() bool { return c.cmd(cmdPlay) } func (c *Controller) Pause() bool { return c.cmd(cmdPause) } func (c *Controller) Stop() bool { return c.cmd(cmdStop) } func (c *Controller) NextTrack() bool { return c.cmd(cmdNextTrack) } func (c *Controller) PrevTrack() bool { return c.cmd(cmdPrevTrack) } func (c *Controller) Close() bool { return c.cmd(cmdClose) } // State: 0=stopped, 1=playing, 3=paused func (c *Controller) PlayState() int { v, ok := c.user(0, userGetPlayState) if !ok { return 0 } return int(v) } func (c *Controller) IsPlaying() bool { return c.PlayState() == 1 } func (c *Controller) IsPaused() bool { return c.PlayState() == 3 } func (c *Controller) IsStopped() bool { return c.PlayState() == 0 } // GetPosition returns current playback offset in seconds. func (c *Controller) GetPosition() int { v, ok := c.user(0, userGetPosition) if !ok { return 0 } return int(v) / 1000 } // GetLength returns total track length in seconds. func (c *Controller) GetLength() int { v, ok := c.user(1, userGetPosition) if !ok { return 0 } return int(v) } // Seek sets the playback position to offsetSeconds. func (c *Controller) Seek(offsetSeconds int) bool { h := c.handle() if h == 0 { return false } send(h, wmUser, uintptr(offsetSeconds*1000), userSeek) return true } // SetVolume sets Winamp's internal volume (0–255). func (c *Controller) SetVolume(v int) bool { if v < 0 { v = 0 } if v > 255 { v = 255 } h := c.handle() if h == 0 { return false } send(h, wmUser, uintptr(v), userSetVolume) return true } // GetPlaylistPosition returns the 1-based current playlist index. func (c *Controller) GetPlaylistPosition() int { v, ok := c.user(0, userGetPlaylistPos) if !ok { return 0 } return int(v) + 1 } // GetPlaylistLength returns the total number of tracks in the playlist. func (c *Controller) GetPlaylistLength() int { v, ok := c.user(0, userGetPlaylistLen) if !ok { return 0 } return int(v) } // GetVersion returns a human-readable Winamp version string (e.g. "5.66"). func (c *Controller) GetVersion() string { v, ok := c.user(0, userGetVersion) if !ok { return "" } hex := fmt.Sprintf("%04X", v) if len(hex) < 3 { return "" } return string(hex[0]) + "." + hex[1:3] } // GetTitle returns the title of the currently playing track by reading // the Winamp window title (format: "N. Artist - Track - Winamp"). func (c *Controller) GetTitle() string { h := c.handle() if h == 0 { return "" } buf := make([]uint16, 512) getWindowTextW.Call(uintptr(h), uintptr(unsafe.Pointer(&buf[0])), 512) title := syscall.UTF16ToString(buf) // Strip trailing " - Winamp" suffix const suffix = " - Winamp" if idx := lastIndex(title, suffix); idx >= 0 { title = title[:idx] } // Strip leading playlist-number prefix "NNN. " if idx := indexOf(title, ". "); idx >= 0 { title = title[idx+2:] } return title } func lastIndex(s, sub string) int { last := -1 for i := 0; i <= len(s)-len(sub); i++ { if s[i:i+len(sub)] == sub { last = i } } return last } func indexOf(s, sub string) int { for i := 0; i <= len(s)-len(sub); i++ { if s[i:i+len(sub)] == sub { return i } } return -1 }