//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" "strings" "syscall" "unsafe" ) var ( user32 = syscall.NewLazyDLL("user32.dll") kernel32 = syscall.NewLazyDLL("kernel32.dll") findWindow = user32.NewProc("FindWindowW") sendMessage = user32.NewProc("SendMessageW") getWindowTextW = user32.NewProc("GetWindowTextW") getWindowThreadProcessId = user32.NewProc("GetWindowThreadProcessId") openProcess = kernel32.NewProc("OpenProcess") readProcessMemory = kernel32.NewProc("ReadProcessMemory") multiByteToWideChar = kernel32.NewProc("MultiByteToWideChar") ) 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 userSetPlaylistPos = 121 userGetPlaylistTitle = 212 userGetPlaylistFile = 211 userRestart = 135 // OpenProcess access right processVMRead = 0x0010 ) // 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. // Returns 0 when stopped (Winamp returns 0xFFFFFFFF in that state). func (c *Controller) GetPosition() int { v, ok := c.user(0, userGetPosition) if !ok || v > 0xF0000000 { // 0xFFFFFFFF = stopped/no track 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. // // Winamp 5.x formats the window title as one of: // // "N. Artist - Title - Winamp" (playing) // "Winamp" (stopped, no playlist) // // We strip the " - Winamp" suffix and the leading "N. " playlist prefix. 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 " - Winamp" suffix (use last occurrence so track titles // containing " - Winamp" are handled correctly). const suffix = " - Winamp" if idx := strings.LastIndex(title, suffix); idx >= 0 { title = title[:idx] } else { // Title is just "Winamp" (stopped, empty playlist). return "" } // Strip leading playlist-number prefix: digits followed by ". " // e.g. "4. " or "12. " if dot := strings.Index(title, ". "); dot >= 0 && dot <= 4 { title = title[dot+2:] } return title } // ── Playlist ────────────────────────────────────────────────────────────────── // TrackInfo is one entry in the Winamp playlist. type TrackInfo struct { Index int `json:"index"` // 1-based Title string `json:"title"` } // readRemoteString reads a null-terminated ANSI string from another process // and converts it to UTF-8 via MultiByteToWideChar (CP_ACP = 0) so that // umlauts and other non-ASCII characters are handled correctly regardless of // the active Windows code page. func readRemoteString(proc syscall.Handle, ptr uintptr) string { if ptr == 0 { return "" } buf := make([]byte, 512) var read uintptr ok, _, _ := readProcessMemory.Call( uintptr(proc), ptr, uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)), uintptr(unsafe.Pointer(&read)), ) if ok == 0 || read == 0 { return "" } // Trim to null terminator. ansi := buf[:read] for i, b := range ansi { if b == 0 { ansi = ansi[:i] break } } if len(ansi) == 0 { return "" } // First call: query required UTF-16 buffer size. const cpACP = 0 n, _, _ := multiByteToWideChar.Call( cpACP, 0, uintptr(unsafe.Pointer(&ansi[0])), uintptr(len(ansi)), 0, 0, ) if n == 0 { return string(ansi) // fallback: treat as latin-1 } // Second call: do the conversion. wide := make([]uint16, n) multiByteToWideChar.Call( cpACP, 0, uintptr(unsafe.Pointer(&ansi[0])), uintptr(len(ansi)), uintptr(unsafe.Pointer(&wide[0])), n, ) return syscall.UTF16ToString(wide) } // GetPlaylist returns all titles in the current Winamp playlist. // Titles are read from Winamp's process memory via ReadProcessMemory. func (c *Controller) GetPlaylist() []TrackInfo { h := c.handle() if h == 0 { return nil } n := int(send(h, wmUser, 0, userGetPlaylistLen)) if n <= 0 { return nil } var pid uint32 getWindowThreadProcessId.Call(uintptr(h), uintptr(unsafe.Pointer(&pid))) proc, _, _ := openProcess.Call(processVMRead, 0, uintptr(pid)) if proc == 0 { return nil } defer syscall.CloseHandle(syscall.Handle(proc)) tracks := make([]TrackInfo, n) for i := 0; i < n; i++ { ptr := send(h, wmUser, uintptr(i), userGetPlaylistTitle) tracks[i] = TrackInfo{ Index: i + 1, Title: readRemoteString(syscall.Handle(proc), ptr), } } return tracks } // JumpToTrack sets the playlist position (0-based) and starts playback. func (c *Controller) JumpToTrack(zeroBasedIndex int) bool { h := c.handle() if h == 0 { return false } send(h, wmUser, uintptr(zeroBasedIndex), userSetPlaylistPos) send(h, wmCommand, cmdPlay, 0) return true } // GetCurrentFile returns the file system path of the currently playing track. // It reads the path from Winamp's process memory via IPC_GETPLAYLISTFILE. func (c *Controller) GetCurrentFile() string { h := c.handle() if h == 0 { return "" } // userGetPlaylistPos returns 0-based current index pos := send(h, wmUser, 0, userGetPlaylistPos) ptr := send(h, wmUser, pos, userGetPlaylistFile) if ptr == 0 { return "" } var pid uint32 getWindowThreadProcessId.Call(uintptr(h), uintptr(unsafe.Pointer(&pid))) proc, _, _ := openProcess.Call(processVMRead, 0, uintptr(pid)) if proc == 0 { return "" } defer syscall.CloseHandle(syscall.Handle(proc)) return readRemoteString(syscall.Handle(proc), ptr) }