|
- //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")
- )
-
- 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
- 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.
- func readRemoteString(proc syscall.Handle, ptr uintptr) string {
- if ptr == 0 {
- return ""
- }
- buf := make([]byte, 512)
- var read uintptr
- readProcessMemory.Call(
- uintptr(proc),
- ptr,
- uintptr(unsafe.Pointer(&buf[0])),
- uintptr(len(buf)),
- uintptr(unsafe.Pointer(&read)),
- )
- for i, b := range buf {
- if b == 0 {
- return string(buf[:i])
- }
- }
- return string(buf)
- }
-
- // 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
- }
|